diff --git a/CHANGES.rst b/CHANGES.rst index 70f85e95..b86e14d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,6 +45,12 @@ See `issue 3 `_. +- Make the internal singleton object returned by APIs like + ``implementedBy`` and ``directlyProvidedBy`` for objects that + implement or provide no interfaces more immutable. Previously an + internal cache could be mutated. See `issue 204 + `_. + 5.0.2 (2020-03-30) ================== diff --git a/src/zope/interface/declarations.py b/src/zope/interface/declarations.py index b42c906e..d77c824c 100644 --- a/src/zope/interface/declarations.py +++ b/src/zope/interface/declarations.py @@ -195,6 +195,18 @@ def weakref(self, callback=None): # object, and that includes a method.) return _ImmutableDeclaration + @property + def _v_attrs(self): + # _v_attrs is not a public, documented property, but some client + # code uses it anyway as a convenient place to cache things. To keep + # the empty declaration truly immutable, we must ignore that. That includes + # ignoring assignments as well. + return {} + + @_v_attrs.setter + def _v_attrs(self, new_attrs): + pass + ############################################################################## # diff --git a/src/zope/interface/tests/test_declarations.py b/src/zope/interface/tests/test_declarations.py index 83815d7e..fc2ab684 100644 --- a/src/zope/interface/tests/test_declarations.py +++ b/src/zope/interface/tests/test_declarations.py @@ -122,6 +122,20 @@ def test___iro___(self): decl = self._getEmpty() self.assertEqual(decl.__iro__, (Interface,)) + def test_get(self): + decl = self._getEmpty() + self.assertIsNone(decl.get('attr')) + self.assertEqual(decl.get('abc', 'def'), 'def') + # It's a positive cache only (when it even exists) + # so this added nothing. + self.assertFalse(decl._v_attrs) + + def test_changed_w_existing__v_attrs(self): + decl = self._getEmpty() + decl._v_attrs = object() + decl.changed(decl) + self.assertFalse(decl._v_attrs) + class DeclarationTests(EmptyDeclarationTests): @@ -153,12 +167,6 @@ def test_changed_wo_existing__v_attrs(self): decl.changed(decl) # doesn't raise self.assertIsNone(decl._v_attrs) - def test_changed_w_existing__v_attrs(self): - decl = self._makeOne() - decl._v_attrs = object() - decl.changed(decl) - self.assertIsNone(decl._v_attrs) - def test___contains__w_self(self): decl = self._makeOne() self.assertNotIn(decl, decl) @@ -335,6 +343,19 @@ def test_get_always_default(self): self.assertIsNone(self._getEmpty().get('name')) self.assertEqual(self._getEmpty().get('name', 42), 42) + def test_v_attrs(self): + decl = self._getEmpty() + self.assertEqual(decl._v_attrs, {}) + + decl._v_attrs['attr'] = 42 + self.assertEqual(decl._v_attrs, {}) + self.assertIsNone(decl.get('attr')) + + attrs = decl._v_attrs = {} + attrs['attr'] = 42 + self.assertEqual(decl._v_attrs, {}) + self.assertIsNone(decl.get('attr')) + class TestImplements(NameAndModuleComparisonTestsMixin, unittest.TestCase):