Skip to content

Commit

Permalink
switch from class to instance based `__annotations__ check:
Browse files Browse the repository at this point in the history
We now check whether it comes from the instance dict or a slot
  • Loading branch information
d-maurer committed Dec 11, 2024
1 parent 0affb04 commit 530c441
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 4 deletions.
36 changes: 32 additions & 4 deletions src/zope/annotation/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Attribute Annotations implementation"""
import logging
from collections.abc import MutableMapping as DictMixin
from weakref import WeakKeyDictionary


try:
Expand All @@ -38,7 +39,7 @@
class AttributeAnnotations(DictMixin):
"""Store annotations on an object
Store annotations in the `__annotations__` attribute on a
Store annotations in the attribute given by ``ATTR`` on a
`IAttributeAnnotatable` object.
"""

Expand All @@ -57,10 +58,9 @@ class AttributeAnnotations(DictMixin):
def __init__(self, obj, context=None):
self.obj = obj
if getattr(obj, ATTR, None) is None:
# try to migrate
# check for migration
ann = getattr(obj, "__annotations__", None)
if ann is not None \
and ann is not getattr(type(obj), "__annotations__", None):
if ann is not None and _check_ann(obj, ann):
# migrate
setattr(obj, ATTR, ann)
delattr(obj, "__annotations__")
Expand Down Expand Up @@ -115,3 +115,31 @@ def __delitem__(self, key):
raise KeyError(key)

del annotation[key]


_with_annotations_slot = WeakKeyDictionary()


def _check_ann(obj, ann):
"""check whether *ann* is an annotation on *obj*.
We assume ``obj.__annotations__ is ann is not None``.
"""
# *ann* can come from *obj* itself or its class of one of the base classes
# we check whether it comes from *obj* itself
try:
if obj.__dict__["__annotations__"] is ann:
return True
except (AttributeError, KeyError):
pass
# it does not come from *obj.__dict__"
# it may come from an ``__annotations__`` slot
oc = obj.__class__
if oc not in _with_annotations_slot:
_with_annotations_slot[oc] = \
any("__annotations__" in c.__dict__.get("__slots__", ())
for c in oc.__mro__)
# even without ``__annotations__`` slot, it may still
# come from *obj* (mediated by some weird descriptor)
# but we ignore this case
return _with_annotations_slot[oc]
35 changes: 35 additions & 0 deletions src/zope/annotation/tests/test_attributeannotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,41 @@ class NotifyingAttributeAnnotations(AttributeAnnotations):
ann = NotifyingAttributeAnnotations(obj)
self.assertEqual(counter.c, 2)

def test_check_ann(self):
from zope.annotation.attribute import _check_ann

ann = dict(a=1)

class C:
pass

c = C()
c.__annotations__ = ann
self.assertTrue(_check_ann(c, ann))

class WithAnnotationsSlot:
__slots__ = "__annotations__",

c = WithAnnotationsSlot()
c.__annotations__ = ann
self.assertTrue(_check_ann(c, ann))

class WithTypeHints:
x: int

c = WithTypeHints()
self.assertFalse(_check_ann(c, c.__annotations__))

# the following checks are there only to make
# ``coverage`` happy
self.assertFalse(_check_ann(c, c.__annotations__))

c = C()
c.__annotations__ = dict()
# we violate ``_check_ann``'s precondition
# but it does not really depend on it
self.assertFalse(_check_ann(c, ann))


def test_suite():
return unittest.defaultTestLoader.loadTestsFromName(__name__)

0 comments on commit 530c441

Please sign in to comment.