Skip to content

Commit

Permalink
The ImmutableDeclaration also has immutable _v_attrs.
Browse files Browse the repository at this point in the history
Fixes #204
  • Loading branch information
jamadden committed Apr 7, 2020
1 parent a404e5f commit c500360
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Expand Up @@ -45,6 +45,12 @@

See `issue 3 <https://github.com/zopefoundation/zope.interface/issues/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
<https://github.com/zopefoundation/zope.interface/issues/204>`_.

5.0.2 (2020-03-30)
==================

Expand Down
12 changes: 12 additions & 0 deletions src/zope/interface/declarations.py
Expand Up @@ -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


##############################################################################
#
Expand Down
33 changes: 27 additions & 6 deletions src/zope/interface/tests/test_declarations.py
Expand Up @@ -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):

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit c500360

Please sign in to comment.