Skip to content

Commit

Permalink
- Bug fix: when a venusian decorator used as a class decorator was
Browse files Browse the repository at this point in the history
  used against both a class *and* a subclass of that class, the
  superclass and subclass would effectively share the same set of
  callbacks.  This was not the intent: each class declaration should
  have its own local set of callbacks; callbacks added via decorations
  should not be inherited.
  • Loading branch information
mcdonc committed Sep 4, 2010
1 parent 8398dc7 commit 5cb16e8
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 33 deletions.
7 changes: 7 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Change Log
Next release
------------

- Bug fix: when a venusian decorator used as a class decorator was
used against both a class *and* a subclass of that class, the
superclass and subclass would effectively share the same set of
callbacks. This was not the intent: each class declaration should
have its own local set of callbacks; callbacks added via decorations
should not be inherited.

- Arrange test fixtures into a single directory.

0.3 (2010-06-24)
Expand Down
6 changes: 5 additions & 1 deletion venusian/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ def attach(wrapped, callback, category=None, depth=1):
callbacks = categories.setdefault(category, [])
callbacks.append(callback)
else:
categories = getattr(wrapped, ATTACH_ATTR, {})
if inspect.isclass(wrapped):
# ignore any superclass attachments, these should not be inherited
categories = wrapped.__dict__.get(ATTACH_ATTR, {})
else:
categories = getattr(wrapped, ATTACH_ATTR, {})
callbacks = categories.setdefault(category, [])
callbacks.append(callback)
setattr(wrapped, ATTACH_ATTR, categories)
Expand Down
4 changes: 2 additions & 2 deletions venusian/compat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
try:
try: # pragma: no cover
from pkgutil import walk_packages
except ImportError:
except ImportError: # pragma: no cover
from pkgutil_26 import walk_packages
10 changes: 10 additions & 0 deletions venusian/tests/fixtures/classdecorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from venusian.tests.fixtures import decorator

@decorator(superclass=True)
class SuperClass(object):
pass

@decorator(subclass=True)
class SubClass(SuperClass):
pass

58 changes: 28 additions & 30 deletions venusian/tests/test_venusian.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import sys

class TestScanner(unittest.TestCase):
def _makeOne(self, **kw):
Expand All @@ -7,11 +8,6 @@ def _makeOne(self, **kw):

def test_package(self):
from venusian.tests.fixtures import one
class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(one)
Expand Down Expand Up @@ -56,11 +52,6 @@ def test_package_with_orphaned_pyc_file(self):
# has no corresponding .py source file. Such orphaned .pyc
# files should be ignored during scanning.
from venusian.tests.fixtures import pyc
class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(pyc)
Expand Down Expand Up @@ -92,11 +83,6 @@ def __call__(self, **kw):

def test_module(self):
from venusian.tests.fixtures.one import module
class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(module)
Expand All @@ -123,11 +109,6 @@ def __call__(self, **kw):

def test_one_category(self):
from venusian.tests.fixtures import category
class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(category, categories=('mycategory',))
Expand All @@ -138,11 +119,6 @@ def __call__(self, **kw):

def test_all_categories_implicit(self):
from venusian.tests.fixtures import category
class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(category)
Expand All @@ -156,11 +132,6 @@ def __call__(self, **kw):

def test_all_categories_explicit(self):
from venusian.tests.fixtures import category
class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(category, categories=('mycategory', 'mycategory2'))
Expand All @@ -171,3 +142,30 @@ def __call__(self, **kw):
self.assertEqual(test.registrations[1]['name'], 'function2')
self.assertEqual(test.registrations[1]['ob'], category.function2)
self.assertEqual(test.registrations[1]['function'], True)

if sys.version_info >= (2, 6):

def test_classdecorator(self):
from venusian.tests.fixtures import classdecorator
test = Test()
scanner = self._makeOne(test=test)
scanner.scan(classdecorator)
test.registrations.sort(
lambda x, y: cmp((x['name'], x['ob'].__module__),
(y['name'], y['ob'].__module__))
)
self.assertEqual(len(test.registrations), 2)
self.assertEqual(test.registrations[0]['name'], 'SubClass')
self.assertEqual(test.registrations[0]['ob'],
classdecorator.SubClass)
self.assertEqual(test.registrations[0]['subclass'], True)
self.assertEqual(test.registrations[1]['name'], 'SuperClass')
self.assertEqual(test.registrations[1]['ob'],
classdecorator.SuperClass)
self.assertEqual(test.registrations[1]['superclass'], True)

class Test(object):
def __init__(self):
self.registrations = []
def __call__(self, **kw):
self.registrations.append(kw)

0 comments on commit 5cb16e8

Please sign in to comment.