Skip to content

Commit e35c58a

Browse files
fix local classes
1 parent 43cea0b commit e35c58a

2 files changed

Lines changed: 50 additions & 4 deletions

File tree

pytools/persistent_dict.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,22 @@ def __call__(self, key: Any) -> str:
244244
# understandably don't like it.
245245

246246
def update_for_type(self, key_hash: Hash, key: type) -> None:
247-
key_hash.update(
248-
f"{key.__module__}.{key.__qualname__}.{key.__name__}".encode())
247+
try:
248+
module = sys.modules[key.__module__]
249+
resolved = module
250+
for attr in key.__qualname__.split("."):
251+
resolved = getattr(resolved, attr)
252+
if resolved is key:
253+
# Globally accessible: hash based on name
254+
self.rec(key_hash,
255+
f"{key.__module__}.{key.__qualname__}".encode()
256+
)
257+
else:
258+
# Name collision or local class; fall back
259+
self.rec(key_hash, f"{key.__module__}.{key.__qualname__}_{id(key)}")
260+
except (KeyError, AttributeError):
261+
# Can't resolve the class name, must be local
262+
self.rec(key_hash, f"{key.__module__}.{key.__qualname__}_{id(key)}")
249263

250264
update_for_ABCMeta = update_for_type
251265

pytools/test/test_persistent_dict.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,19 +525,52 @@ def update_persistent_hash(self, key_hash, key_builder):
525525
assert keyb(MyABC3) != keyb(MyABC) != keyb(MyABC3())
526526

527527

528+
class WithoutUpdateMethodGlobal:
529+
pass
530+
531+
532+
class CollidingNameClass:
533+
pass
534+
535+
528536
def test_class_hashing() -> None:
529537
keyb = KeyBuilder()
530538

531539
class WithoutUpdateMethod:
532540
pass
533541

534542
assert keyb(WithoutUpdateMethod) == keyb(WithoutUpdateMethod)
535-
assert keyb(WithoutUpdateMethod) == "da060d601d180d4c"
543+
assert keyb(WithoutUpdateMethodGlobal) == keyb(WithoutUpdateMethodGlobal)
544+
545+
# This doesn't work with the function-local class "WithoutUpdateMethod", because
546+
# local classes are instantiated at each function call, and thus their hash
547+
# includes the id() of the class:
548+
# assert keyb(WithoutUpdateMethod) == "N/A"
549+
assert keyb(WithoutUpdateMethodGlobal) == "49c4673089d30507"
536550

537551
with pytest.raises(TypeError):
538552
# does not have update_persistent_hash() = > will raise
539553
keyb(WithoutUpdateMethod())
540554

555+
with pytest.raises(TypeError):
556+
# does not have update_persistent_hash() = > will raise
557+
keyb(WithoutUpdateMethodGlobal())
558+
559+
# {{{ test for name collisions between top-level and function-local classes
560+
561+
def make_colliding_name_class():
562+
class CollidingNameClass:
563+
pass
564+
565+
return CollidingNameClass
566+
567+
top_level_cls = CollidingNameClass
568+
shadowed_cls = make_colliding_name_class()
569+
570+
assert keyb(top_level_cls) != keyb(shadowed_cls)
571+
572+
# }}}
573+
541574
class WithUpdateMethod:
542575
def update_persistent_hash(self, key_hash, key_builder):
543576
# Only called for instances of this class, not for the class itself
@@ -558,7 +591,6 @@ class TagClass2(Tag):
558591
assert keyb(TagClass()) != keyb(TagClass2())
559592

560593
assert keyb(TagClass()) == "7b3e4e66503438f6"
561-
assert keyb(TagClass2) == "690b86bbf51aad83"
562594

563595
@tag_dataclass
564596
class TagClass3(Tag):

0 commit comments

Comments
 (0)