summaryrefslogtreecommitdiff
path: root/nose
diff options
context:
space:
mode:
authorBobby Impollonia <bobbyi@gmail.com>2011-05-01 13:39:41 -0700
committerBobby Impollonia <bobbyi@gmail.com>2011-05-01 13:39:41 -0700
commit0e2ef2befd4e35bf8488c066893d91ff15dd2ac0 (patch)
tree95ab01b94936b68b8a060447c0e12459889b614d /nose
parentdc095969fc322ff3e2ab70d6d70962021450c101 (diff)
downloadnose-0e2ef2befd4e35bf8488c066893d91ff15dd2ac0.tar.gz
Overhaul the Attribute Selector plugin to fix the following bugs:
Issue #412 : Attributes don't work on inherited test method Issue #411 : Plugin doesn't work on classes containing static methods Issue #324 : Mixing method and class attributes doesn't work Issue #381 : Plugin doesn't work on classes containing methods with no __dict__ (e.g., from boost.python) Issue #382 : Dupe of #381 with less info * When used as class decorator, attr() was only applying the attributes to methods with names starting with "test_". It would skip, e.g., my_cool_test. * The class decorator form of attr() did not work for test methods inherited from parent classes, nor were the attributes inherited by child classes. They also didn't work for methods added to the class later (e.g., by another decorator). The primary changes are to use getattr/ setattr exlusively rather than direct access to __dict__ and to remove the (unneeded and problematic) wantClass method. Most of the bugs were caused by __dict__ access or wantClass not doing the right thing. In addition to fixing the above bugs, these changes make the plugin smaller and simpler to understand. Add regression tests for fixed bugs. Change assert not plug.wantFunction(h) to assert plug.wantFunction(h) is False where plug.wantFunction only return None or False Refactor duplicated code in functional tests into base class.
Diffstat (limited to 'nose')
-rw-r--r--nose/plugins/attrib.py120
1 files changed, 46 insertions, 74 deletions
diff --git a/nose/plugins/attrib.py b/nose/plugins/attrib.py
index bc318c1..3d4422a 100644
--- a/nose/plugins/attrib.py
+++ b/nose/plugins/attrib.py
@@ -112,59 +112,42 @@ log = logging.getLogger('nose.plugins.attrib')
compat_24 = sys.version_info >= (2, 4)
def attr(*args, **kwargs):
- """Decorator that adds attributes to objects
+ """Decorator that adds attributes to classes or functions
for use with the Attribute (-a) plugin.
"""
- def apply_to_fn(fn):
- for name in args:
- # these are just True flags:
- fn.__dict__[name] = 1
- fn.__dict__.update(kwargs)
-
def wrap_ob(ob):
- if inspect.isclass(ob):
- cls = ob
- for k, v in cls.__dict__.items():
- if not k.lower().startswith('test'):
- continue
- fn = getattr(cls, k)
- apply_to_fn(fn)
- else:
- apply_to_fn(ob)
+ for name in args:
+ setattr(ob, name, True)
+ for name, value in kwargs.iteritems():
+ setattr(ob, name, value)
return ob
-
return wrap_ob
-class ContextHelper:
- """Returns default values for dictionary lookups."""
- def __init__(self, obj):
- self.obj = obj
-
- def __getitem__(self, name):
- return self.obj.get(name, False)
-
+def get_method_attr(method, cls, attr_name, default = False):
+ """Look up an attribute on a method/ function.
+ If the attribute isn't found there, looking it up in the
+ method's class, if any.
+ """
+ Missing = object()
+ value = getattr(method, attr_name, Missing)
+ if value is Missing and cls is not None:
+ value = getattr(cls, attr_name, Missing)
+ if value is Missing:
+ return default
+ return value
-class AttributeGetter:
- """Helper for looking up attributes
- First we check the method, and if the attribute is not present,
- we check the method's class.
+class ContextHelper:
+ """Object that can act as context dictionary for eval and looks up
+ names as attributes on a method/ function and its class.
"""
- missing = object()
-
- def __init__(self, cls, method):
- self.cls = cls
+ def __init__(self, method, cls):
self.method = method
+ self.cls = cls
+
+ def __getitem__(self, name):
+ return get_method_attr(self.method, self.cls, name)
- def get(self, name, default=None):
- log.debug('Get %s from %s.%s', name, self.cls, self.method)
- val = self.method.__dict__.get(name, self.missing)
- if val is self.missing:
- log.debug('No attribute %s in method, getting from class',
- name)
- val = getattr(self.cls, name, default)
- log.debug('Class attribute %s value: %s', name, val)
- return val
class AttributeSelector(Plugin):
"""Selects test cases to be run based on their attributes.
@@ -208,8 +191,8 @@ class AttributeSelector(Plugin):
for attr in eval_attr:
# "<python expression>"
# -> eval(expr) in attribute context must be True
- def eval_in_context(expr, attribs):
- return eval(expr, None, ContextHelper(attribs))
+ def eval_in_context(expr, obj, cls):
+ return eval(expr, None, ContextHelper(obj, cls))
self.attribs.append([(attr, eval_in_context)])
# attribute requirements are a comma separated list of
@@ -244,41 +227,41 @@ class AttributeSelector(Plugin):
if self.attribs:
self.enabled = True
- def validateAttrib(self, attribs):
+ def validateAttrib(self, method, cls = None):
+ """Verify whether a method has the required attributes
+ The method is considered a match if it matches all attributes
+ for any attribute group.
+ ."""
# TODO: is there a need for case-sensitive value comparison?
- # within each group, all must match for the group to match
- # if any group matches, then the attribute set as a whole
- # has matched
any = False
for group in self.attribs:
match = True
for key, value in group:
- obj_value = attribs.get(key)
+ attr = get_method_attr(method, cls, key)
if callable(value):
- if not value(key, attribs):
+ if not value(key, method, cls):
match = False
break
elif value is True:
# value must exist and be True
- if not bool(obj_value):
+ if not bool(attr):
match = False
break
elif value is False:
# value must not exist or be False
- if bool(obj_value):
+ if bool(attr):
match = False
break
- elif type(obj_value) in (list, tuple):
+ elif type(attr) in (list, tuple):
# value must be found in the list attribute
-
if not str(value).lower() in [str(x).lower()
- for x in obj_value]:
+ for x in attr]:
match = False
break
else:
# value must match, convert to string and compare
- if (value != obj_value
- and str(value).lower() != str(obj_value).lower()):
+ if (value != attr
+ and str(value).lower() != str(attr).lower()):
match = False
break
any = any or match
@@ -288,27 +271,16 @@ class AttributeSelector(Plugin):
return None
return False
- def wantClass(self, cls):
- """Accept the class if the class or any method is wanted.
- """
- cls_attr = cls.__dict__
- if self.validateAttrib(cls_attr) is not False:
- return None
- # Methods in __dict__.values() are functions, oddly enough.
- methods = filter(isfunction, cls_attr.values())
- wanted = filter(lambda m: m is not False,
- map(self.wantFunction, methods))
- if wanted:
- return None
- return False
-
def wantFunction(self, function):
"""Accept the function if its attributes match.
"""
- return self.validateAttrib(function.__dict__)
+ return self.validateAttrib(function)
def wantMethod(self, method):
"""Accept the method if its attributes match.
"""
- attribs = AttributeGetter(method.im_class, method)
- return self.validateAttrib(attribs)
+ try:
+ cls = method.im_class
+ except AttributeError:
+ return False
+ return self.validateAttrib(method, cls)