attrib.py 9.4 KB

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