PageRenderTime 40ms CodeModel.GetById 28ms app.highlight 8ms RepoModel.GetById 1ms app.codeStats 0ms

/nose/plugins/attrib.py

https://bitbucket.org/jpellerin/nose/
Python | 286 lines | 211 code | 22 blank | 53 comment | 41 complexity | ed46749552833e57262b40c1e6d2f263 MD5 | raw file
  1"""Attribute selector plugin.
  2
  3Oftentimes when testing you will want to select tests based on
  4criteria rather then simply by filename. For example, you might want
  5to run all tests except for the slow ones. You can do this with the
  6Attribute selector plugin by setting attributes on your test methods.
  7Here 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
 17Once you've assigned an attribute ``slow = 1`` you can exclude that
 18test and all other tests having the slow attribute by running ::
 19
 20    $ nosetests -a '!slow'
 21
 22There is also a decorator available for you that will set attributes.
 23Here'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
 33And 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
 43This test could be run with ::
 44
 45    $ nosetests -a speed=slow
 46
 47In Python 2.6 and higher, ``@attr`` can be used on a class to set attributes
 48on 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
 60Below is a reference to the different syntaxes available.
 61
 62Simple syntax
 63-------------
 64
 65Examples 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
 91Expression Evaluation
 92---------------------
 93
 94Examples 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"""
103import inspect
104import logging
105import os
106import sys
107from inspect import isfunction
108from nose.plugins.base import Plugin
109from nose.util import tolist
110
111log = logging.getLogger('nose.plugins.attrib')
112compat_24 = sys.version_info >= (2, 4)
113
114def 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
126def 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
140class 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
152class 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)