Imported Upstream version 1.1.2
[platform/upstream/python-nose.git] / nose / plugins / attrib.py
1 """Attribute selector plugin.
2
3 Oftentimes when testing you will want to select tests based on
4 criteria rather then simply by filename. For example, you might want
5 to run all tests except for the slow ones. You can do this with the
6 Attribute selector plugin by setting attributes on your test methods.
7 Here is an example:
8
9 .. code-block:: python
10
11     def test_big_download():
12         import urllib
13         # commence slowness...
14
15     test_big_download.slow = 1
16
17 Once you've assigned an attribute ``slow = 1`` you can exclude that
18 test and all other tests having the slow attribute by running ::
19
20     $ nosetests -a '!slow'
21
22 There is also a decorator available for you that will set attributes.
23 Here's how to set ``slow=1`` like above with the decorator:
24
25 .. code-block:: python
26
27     from nose.plugins.attrib import attr
28     @attr('slow')
29     def test_big_download():
30         import urllib
31         # commence slowness...
32
33 And here's how to set an attribute with a specific value:
34
35 .. code-block:: python
36
37     from nose.plugins.attrib import attr
38     @attr(speed='slow')
39     def test_big_download():
40         import urllib
41         # commence slowness...
42
43 This test could be run with ::
44
45     $ nosetests -a speed=slow
46
47 In Python 2.6 and higher, ``@attr`` can be used on a class to set attributes
48 on all its test methods at once.  For example:
49
50 .. code-block:: python
51
52     from nose.plugins.attrib import attr
53     @attr(speed='slow')
54     class MyTestCase:
55         def test_long_integration(self):
56             pass
57         def test_end_to_end_something(self):
58             pass
59
60 Below is a reference to the different syntaxes available.
61
62 Simple syntax
63 -------------
64
65 Examples of using the ``-a`` and ``--attr`` options:
66
67 * ``nosetests -a status=stable``
68    Only runs tests with attribute "status" having value "stable"
69
70 * ``nosetests -a priority=2,status=stable``
71    Runs tests having both attributes and values
72
73 * ``nosetests -a priority=2 -a slow``
74    Runs tests that match either attribute
75
76 * ``nosetests -a tags=http``
77    If a test's ``tags`` attribute was a list and it contained the value
78    ``http`` then it would be run
79
80 * ``nosetests -a slow``
81    Runs tests with the attribute ``slow`` if its value does not equal False
82    (False, [], "", etc...)
83
84 * ``nosetests -a '!slow'``
85    Runs tests that do NOT have the attribute ``slow`` or have a ``slow``
86    attribute that is equal to False
87    **NOTE**:
88    if your shell (like bash) interprets '!' as a special character make sure to
89    put single quotes around it.
90
91 Expression Evaluation
92 ---------------------
93
94 Examples using the ``-A`` and ``--eval-attr`` options:
95
96 * ``nosetests -A "not slow"``
97   Evaluates the Python expression "not slow" and runs the test if True
98
99 * ``nosetests -A "(priority > 5) and not slow"``
100   Evaluates a complex Python expression and runs the test if True
101
102 """
103 import inspect
104 import logging
105 import os
106 import sys
107 from inspect import isfunction
108 from nose.plugins.base import Plugin
109 from nose.util import tolist
110
111 log = logging.getLogger('nose.plugins.attrib')
112 compat_24 = sys.version_info >= (2, 4)
113
114 def attr(*args, **kwargs):
115     """Decorator that adds attributes to classes or functions
116     for use with the Attribute (-a) plugin.
117     """
118     def wrap_ob(ob):
119         for name in args:
120             setattr(ob, name, True)
121         for name, value in kwargs.iteritems():
122             setattr(ob, name, value)
123         return ob
124     return wrap_ob
125
126 def get_method_attr(method, cls, attr_name, default = False):
127     """Look up an attribute on a method/ function. 
128     If the attribute isn't found there, looking it up in the
129     method's class, if any.
130     """
131     Missing = object()
132     value = getattr(method, attr_name, Missing)
133     if value is Missing and cls is not None:
134         value = getattr(cls, attr_name, Missing)
135     if value is Missing:
136         return default
137     return value
138
139
140 class ContextHelper:
141     """Object that can act as context dictionary for eval and looks up
142     names as attributes on a method/ function and its class. 
143     """
144     def __init__(self, method, cls):
145         self.method = method
146         self.cls = cls
147
148     def __getitem__(self, name):
149         return get_method_attr(self.method, self.cls, name)
150
151
152 class AttributeSelector(Plugin):
153     """Selects test cases to be run based on their attributes.
154     """
155
156     def __init__(self):
157         Plugin.__init__(self)
158         self.attribs = []
159
160     def options(self, parser, env):
161         """Register command line options"""
162         parser.add_option("-a", "--attr",
163                           dest="attr", action="append",
164                           default=env.get('NOSE_ATTR'),
165                           metavar="ATTR",
166                           help="Run only tests that have attributes "
167                           "specified by ATTR [NOSE_ATTR]")
168         # disable in < 2.4: eval can't take needed args
169         if compat_24:
170             parser.add_option("-A", "--eval-attr",
171                               dest="eval_attr", metavar="EXPR", action="append",
172                               default=env.get('NOSE_EVAL_ATTR'),
173                               help="Run only tests for whose attributes "
174                               "the Python expression EXPR evaluates "
175                               "to True [NOSE_EVAL_ATTR]")
176
177     def configure(self, options, config):
178         """Configure the plugin and system, based on selected options.
179
180         attr and eval_attr may each be lists.
181
182         self.attribs will be a list of lists of tuples. In that list, each
183         list is a group of attributes, all of which must match for the rule to
184         match.
185         """
186         self.attribs = []
187
188         # handle python eval-expression parameter
189         if compat_24 and options.eval_attr:
190             eval_attr = tolist(options.eval_attr)
191             for attr in eval_attr:
192                 # "<python expression>"
193                 # -> eval(expr) in attribute context must be True
194                 def eval_in_context(expr, obj, cls):
195                     return eval(expr, None, ContextHelper(obj, cls))
196                 self.attribs.append([(attr, eval_in_context)])
197
198         # attribute requirements are a comma separated list of
199         # 'key=value' pairs
200         if options.attr:
201             std_attr = tolist(options.attr)
202             for attr in std_attr:
203                 # all attributes within an attribute group must match
204                 attr_group = []
205                 for attrib in attr.strip().split(","):
206                     # don't die on trailing comma
207                     if not attrib:
208                         continue
209                     items = attrib.split("=", 1)
210                     if len(items) > 1:
211                         # "name=value"
212                         # -> 'str(obj.name) == value' must be True
213                         key, value = items
214                     else:
215                         key = items[0]
216                         if key[0] == "!":
217                             # "!name"
218                             # 'bool(obj.name)' must be False
219                             key = key[1:]
220                             value = False
221                         else:
222                             # "name"
223                             # -> 'bool(obj.name)' must be True
224                             value = True
225                     attr_group.append((key, value))
226                 self.attribs.append(attr_group)
227         if self.attribs:
228             self.enabled = True
229
230     def validateAttrib(self, method, cls = None):
231         """Verify whether a method has the required attributes
232         The method is considered a match if it matches all attributes
233         for any attribute group.
234         ."""
235         # TODO: is there a need for case-sensitive value comparison?
236         any = False
237         for group in self.attribs:
238             match = True
239             for key, value in group:
240                 attr = get_method_attr(method, cls, key)
241                 if callable(value):
242                     if not value(key, method, cls):
243                         match = False
244                         break
245                 elif value is True:
246                     # value must exist and be True
247                     if not bool(attr):
248                         match = False
249                         break
250                 elif value is False:
251                     # value must not exist or be False
252                     if bool(attr):
253                         match = False
254                         break
255                 elif type(attr) in (list, tuple):
256                     # value must be found in the list attribute
257                     if not str(value).lower() in [str(x).lower()
258                                                   for x in attr]:
259                         match = False
260                         break
261                 else:
262                     # value must match, convert to string and compare
263                     if (value != attr
264                         and str(value).lower() != str(attr).lower()):
265                         match = False
266                         break
267             any = any or match
268         if any:
269             # not True because we don't want to FORCE the selection of the
270             # item, only say that it is acceptable
271             return None
272         return False
273
274     def wantFunction(self, function):
275         """Accept the function if its attributes match.
276         """
277         return self.validateAttrib(function)
278
279     def wantMethod(self, method):
280         """Accept the method if its attributes match.
281         """
282         try:
283             cls = method.im_class
284         except AttributeError:
285             return False
286         return self.validateAttrib(method, cls)