"""Attribute selector plugin.
Oftentimes when testing you will want to select tests based on
criteria rather then simply by filename. For example, you might want
to run all tests except for the slow ones. You can do this with the
Attribute selector plugin by setting attributes on your test methods.
Here is an example:

    def test_big_download():
        import urllib
        # commence slowness...

    test_big_download.slow = 1

Once you've assigned an attribute ``slow = 1`` you can exclude that
test and all other tests having the slow attribute by running ::

    $ nosetests -a '!slow'

There is also a decorator available for you that will set attributes.
Here's how to set ``slow=1`` like above with the decorator:

    from nose.plugins.attrib import attr
    @attr('slow')
    def test_big_download():
        import urllib
        # commence slowness...

And here's how to set an attribute with a specific value:

    from nose.plugins.attrib import attr
    @attr(speed='slow')
    def test_big_download():
        import urllib
        # commence slowness...

This test could be run with:

    $ nosetests -a speed=slow

``@attr`` can be used on a class to set attributes
on all its test methods at once. For example:

    from nose.plugins.attrib import attr
    @attr(speed='slow')
    class MyTestCase:
        def test_long_integration(self):
            pass
        def test_end_to_end_something(self):
            pass

Below is a reference to the different syntaxes available.

Simple syntax
-------------

Examples of using the ``-a`` and ``--attr`` options:
* ``nosetests -a status=stable``
   Only runs tests with attribute "status" having value "stable"
* ``nosetests -a priority=2,status=stable``
   Runs tests having both attributes and values
* ``nosetests -a priority=2 -a slow``
   Runs tests that match either attribute
* ``nosetests -a tags=http``
   If a test's ``tags`` attribute was a list and it contained the value
   ``http`` then it would be run
* ``nosetests -a slow``
   Runs tests with the attribute ``slow`` if its value does not equal False
   (False, [], "", etc...)
* ``nosetests -a '!slow'``
   Runs tests that do NOT have the attribute ``slow`` or have a ``slow``
   attribute that is equal to False
   **NOTE**:
   if your shell (like bash) interprets '!' as a special character make sure to
   put single quotes around it.

Expression Evaluation
---------------------

Examples using the ``-A`` and ``--eval-attr`` options:
* ``nosetests -A "not slow"``
  Evaluates the Python expression "not slow" and runs the test if True
* ``nosetests -A "(priority > 5) and not slow"``
  Evaluates a complex Python expression and runs the test if True """
import logging
from nose.plugins.base import Plugin
from nose.util import tolist
import collections

log = logging.getLogger('nose.plugins.attrib')


def attr(*args, **kwargs):
    """Decorator that adds attributes to classes or functions
    for use with the Attribute (-a) plugin."""
    def wrap_ob(ob):
        for name in args:
            setattr(ob, name, True)
        for name, value in kwargs.items():
            setattr(ob, name, value)
        return ob
    return wrap_ob


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 ContextHelper:
    """Object that can act as context dictionary for eval and looks up
    names as attributes on a method/ function and its class."""
    def __init__(self, method, cls):
        self.method = method
        self.cls = cls

    def __getitem__(self, name):
        return get_method_attr(self.method, self.cls, name)


class AttributeSelector(Plugin):
    """Selects test cases to be run based on their attributes."""
    def __init__(self):
        Plugin.__init__(self)
        self.attribs = []

    def options(self, parser, env):
        """Register command line options"""
        parser.add_option(
            "-a", "--attr", dest="attr", action="append",
            default=env.get('NOSE_ATTR'), metavar="ATTR",
            help="Run only tests that have attributes "
            "specified by ATTR [NOSE_ATTR]"
        )
        parser.add_option(
            "-A", "--eval-attr", dest="eval_attr", metavar="EXPR",
            action="append", default=env.get('NOSE_EVAL_ATTR'),
            help="Run only tests for whose attributes the Python "
            "expression EXPR evaluates to True [NOSE_EVAL_ATTR]"
        )

    def configure(self, options, config):
        """Configure the plugin and system, based on selected options.
        Both attr and eval_attr may each be lists.
        self.attribs will be a list of lists of tuples.
        In that list, each list is a group of attributes,
        all of which must match for the rule to match."""
        self.attribs = []
        if options.eval_attr:
            eval_attr = tolist(options.eval_attr)
            for attr in eval_attr:
                def eval_in_context(expr, obj, cls):
                    return eval(expr, None, ContextHelper(obj, cls))
                self.attribs.append([(attr, eval_in_context)])
        # attr requirements are a comma separated list of 'key=value' pairs
        if options.attr:
            std_attr = tolist(options.attr)
            for attr in std_attr:
                attr_group = []
                for attrib in attr.strip().split(","):
                    if not attrib:
                        continue
                    items = attrib.split("=", 1)
                    if len(items) > 1:
                        key, value = items
                    else:
                        key = items[0]
                        if key[0] == "!":
                            key = key[1:]
                            value = False
                        else:
                            value = True
                    attr_group.append((key, value))
                self.attribs.append(attr_group)
        if self.attribs:
            self.enabled = True

    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."""
        any = False
        for group in self.attribs:
            match = True
            for key, value in group:
                attr = get_method_attr(method, cls, key)
                if isinstance(value, collections.abc.Callable):
                    if not value(key, method, cls):
                        match = False
                        break
                elif value is True:
                    if not bool(attr):
                        match = False
                        break
                elif value is False:
                    if bool(attr):
                        match = False
                        break
                elif type(attr) in (list, tuple):
                    if str(value).lower() not in [str(x).lower()
                                                  for x in attr]:
                        match = False
                        break
                elif (
                    value != attr
                    and str(value).lower() != str(attr).lower()
                ):
                    match = False
                    break
            any = any or match
        if any:
            return None
        return False

    def wantFunction(self, function):
        """Accept the function if its attributes match."""
        return self.validateAttrib(function)

    def wantMethod(self, method):
        """Accept the method if its attributes match."""
        try:
            cls = method.__self__.__class__
        except AttributeError:
            return False
        return self.validateAttrib(method, cls)
