Repository: Axelrod-Python/Axelrod

Issue:
Strategy Filtering by Dictionary
Within the api, there are a set of functions to filter strategies from their classifier entries using a dictionary. (This is because parameters within a url are passed as dictionary).

For example, the url https://axelrod-api.herokuapp.com/strategies/?stochastic=false&inspects_source=true passes the following dictionary:
```
{
    'stochastic': 'false',
    'inspects_source': 'true'
}
```
and it's then used to generate the list of strategies which satisfy those filtering criteria.

We could implement that functionality within the library itself, so that we could do something like:
```
filter = {
    'stochastic': 'false',
    'inspects_source': 'true'
}
print(axelrod.filtered_strategies(filter))
```

This wouldn't add anything that couldn't already be done with a list comprehension. The same filtering can be done already with:
```
print([[s in axl.strategies if not s.classifier[’stochastic’] and s.classifier[‘inspects_source']]))
```

However, it would add an extra capability to the library rather than it existing only within the api.

Discussion:

Patch:
diff --git a/axelrod/strategies/__init__.py b/axelrod/strategies/__init__.py
index c86195be..83169217 100644
--- a/axelrod/strategies/__init__.py
+++ b/axelrod/strategies/__init__.py
@@ -1,5 +1,6 @@
 from ..player import is_basic, obey_axelrod
 from ._strategies import *
+from ._filters import passes_filterset
 
 # `from ._strategies import *` import the collection `strategies`
 # Now import the Meta strategies. This cannot be done in _strategies
@@ -29,3 +30,44 @@ long_run_time_strategies = [s for s in all_strategies if
 cheating_strategies = [s for s in all_strategies if not obey_axelrod(s())]
 
 ordinary_strategies = strategies  # This is a legacy and will be removed
+
+
+def filtered_strategies(filterset, strategies=all_strategies):
+    """
+    Applies the filters defined in the given filterset dict and returns those
+    strategy classes which pass all of those filters from the given list of
+    strategies.
+
+    e.g.
+
+    For the filterset dict:
+        {
+            'stochastic': True,
+            'min_memory_depth': 2
+        }
+
+    the function will return a list of all deterministic strategies with a
+    memory_depth of 2 or more.
+
+    Parameters
+    ----------
+        filterset : dict
+            mapping filter name to criterion.
+            e.g.
+                {
+                    'stochastic': True,
+                    'min_memory_depth': 2
+                }
+        strategies: list
+            of subclasses of axelrod.Player
+
+    Returns
+    -------
+        list
+
+        of subclasses of axelrod.Player
+
+    """
+    return [
+        s for s in strategies
+        if passes_filterset(s, filterset)]
diff --git a/axelrod/strategies/_filters.py b/axelrod/strategies/_filters.py
new file mode 100644
index 00000000..c18a8652
--- /dev/null
+++ b/axelrod/strategies/_filters.py
@@ -0,0 +1,219 @@
+from collections import namedtuple
+import operator
+
+
+def passes_operator_filter(strategy, classifier_key, value, operator):
+    """
+    Tests whether a given strategy passes a filter for a
+    given key in its classifier dict using a given (in)equality operator.
+
+    e.g.
+
+    For the following strategy:
+
+    class ExampleStrategy(Player):
+        classifier = {
+            'stochastic': True,
+            'inspects_source': False,
+            'memory_depth': 10,
+            'makes_use_of': ['game', 'length']
+        }
+
+    passes_operator_filter(ExampleStrategy, 'memory_depth', 10, operator.eq)
+
+    would test whether the 'memory_depth' entry equals 10 and return True
+
+    Parameters
+    ----------
+        strategy : a descendant class of axelrod.Player
+        classifier_key: string
+            Defining which entry from the strategy's classifier dict is to be
+            tested (e.g. 'memory_depth').
+        value: int
+            The value against which the strategy's classifier dict entry is to
+            be tested.
+        operator: operator.le, operator.ge or operator.eq
+            Indicating whether a 'less than or equal to' or 'greater than or
+            equal to' test should be applied.
+
+    Returns
+    -------
+        boolean
+
+        True if the value from the strategy's classifier dictionary matches
+        the value and operator passed to the function.
+    """
+    classifier_value = strategy.classifier[classifier_key]
+    if (isinstance(classifier_value, str) and
+            classifier_value.lower() == 'infinity'):
+        classifier_value = float('inf')
+
+    return operator(classifier_value, value)
+
+
+def passes_in_list_filter(strategy, classifier_key, value):
+    """
+    Tests whether a given list of values exist in the list returned from the
+    given strategy's classifier dict for the given classifier_key.
+
+    e.g.
+
+    For the following strategy:
+
+    class ExampleStrategy(Player):
+        classifier = {
+            'stochastic': True,
+            'inspects_source': False,
+            'memory_depth': 10,
+            'makes_use_of': ['game', 'length']
+        }
+
+    passes_in_list_filter(ExampleStrategy, 'makes_use_of', 'game', operator.eq)
+
+    would test whether 'game' exists in the strategy's' 'makes_use_of' entry
+    and return True.
+
+    Parameters
+    ----------
+        strategy : a descendant class of axelrod.Player
+        classifier_key: string
+            Defining which entry from the strategy's classifier dict is to be
+            tested (e.g. 'makes_use_of').
+        value: list
+            The values against which the strategy's classifier dict entry is to
+            be tested.
+
+    Returns
+    -------
+        boolean
+    """
+    result = True
+    for entry in value:
+        if entry not in strategy.classifier[classifier_key]:
+            result = False
+    return result
+
+
+def passes_filterset(strategy, filterset):
+    """
+    Determines whether a given strategy meets the criteria defined in a
+    dictionary of filters.
+
+    e.g.
+
+    For the following strategy:
+
+    class ExampleStrategy(Player):
+        classifier = {
+            'stochastic': True,
+            'inspects_source': False,
+            'memory_depth': 10,
+            'makes_use_of': ['game', 'length']
+        }
+
+    and this filterset dict:
+
+    example_filterset = {
+        'stochastic': True,
+        'memory_depth': 10
+    }
+
+    passes_filterset(ExampleStrategy, example_filterset)
+
+    would test whether both the strategy's 'stochastic' entry is True AND
+    that its 'memory_depth' equals 10 and return True.
+
+    Parameters
+    ----------
+        strategy : a descendant class of axelrod.Player
+        filterset : dict
+            mapping filter name to criterion.
+            e.g.
+                {
+                    'stochastic': True,
+                    'min_memory_depth': 2
+                }
+
+    Returns
+    -------
+        boolean
+
+        True if the given strategy meets all the supplied criteria in the
+        filterset, otherwise false.
+
+    """
+    FilterFunction = namedtuple('FilterFunction', 'function kwargs')
+
+    # A dictionary mapping filter name (from the supplied filterset) to
+    # the relevant function and arguments for that filter.
+    filter_functions = {
+        'stochastic': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'stochastic',
+                'operator': operator.eq
+            }),
+        'long_run_time': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'long_run_time',
+                'operator': operator.eq
+            }),
+        'manipulates_state': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'manipulates_state',
+                'operator': operator.eq
+            }),
+        'manipulates_source': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'manipulates_source',
+                'operator': operator.eq
+            }),
+        'inspects_source': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'inspects_source',
+                'operator': operator.eq
+            }),
+        'memory_depth': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'memory_depth',
+                'operator': operator.eq
+            }),
+        'min_memory_depth': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'memory_depth',
+                'operator': operator.ge
+            }),
+        'max_memory_depth': FilterFunction(
+            function=passes_operator_filter,
+            kwargs={
+                'classifier_key': 'memory_depth',
+                'operator': operator.le
+            }),
+        'makes_use_of': FilterFunction(
+            function=passes_in_list_filter,
+            kwargs={'classifier_key': 'makes_use_of'})
+    }
+
+    # A list of boolean values to record whether the strategy passed or failed
+    # each of the filters in the supplied filterset.
+    passes_filters = []
+
+    # Loop through each of the entries in the filter_functions dict and, if
+    # that filter is defined in the supplied filterset, call the relevant
+    # function and record its result in the passes_filters list.
+    for _filter, filter_function in filter_functions.items():
+
+        if filterset.get(_filter, None) is not None:
+            kwargs = filter_function.kwargs
+            kwargs['strategy'] = strategy
+            kwargs['value'] = filterset[_filter]
+            passes_filters.append(filter_function.function(**kwargs))
+
+    # Return True if the strategy passed all the supplied filters
+    return all(passes_filters)
diff --git a/docs/tutorials/advanced/classification_of_strategies.rst b/docs/tutorials/advanced/classification_of_strategies.rst
index 8f95b64a..61037b44 100644
--- a/docs/tutorials/advanced/classification_of_strategies.rst
+++ b/docs/tutorials/advanced/classification_of_strategies.rst
@@ -10,7 +10,15 @@ various dimensions.
 Here is the :code:`classifier` for the :code:`Cooperator` strategy::
 
     >>> import axelrod as axl
-    >>> expected_dictionary = {'manipulates_state': False, 'makes_use_of': set([]), 'long_run_time': False, 'stochastic': False, 'manipulates_source': False, 'inspects_source': False, 'memory_depth': 0}  # Order of this dictionary might be different on your machine
+    >>> expected_dictionary = {
+    ...    'manipulates_state': False,
+    ...    'makes_use_of': set([]),
+    ...    'long_run_time': False,
+    ...    'stochastic': False,
+    ...    'manipulates_source': False,
+    ...    'inspects_source': False,
+    ...    'memory_depth': 0
+    ... }  # Order of this dictionary might be different on your machine
     >>> axl.Cooperator.classifier == expected_dictionary
     True
 
@@ -20,37 +28,82 @@ Note that instances of the class also have this classifier::
     >>> s.classifier == expected_dictionary
     True
 
-This allows us to, for example, quickly identify all the stochastic
+and that we can retrieve individual entries from that :code:`classifier` dictionary::
+
+    >>> s = axl.TitForTat
+    >>> s.classifier['memory_depth']
+    1
+    >>> s = axl.Random
+    >>> s.classifier['stochastic']
+    True
+
+We can use this classification to generate sets of strategies according to
+filters which we define in a 'filterset' dictionary and then pass to the
+'filtered_strategies' function. For example, to identify all the stochastic
 strategies::
 
-    >>> len([s for s in axl.strategies if s().classifier['stochastic']])
-    43
+    >>> filterset = {
+    ...     'stochastic': True
+    ... }
+    >>> strategies = axl.filtered_strategies(filterset)
+    >>> len(strategies)
+    35
+
 
-Or indeed find out how many strategy only use 1 turn worth of memory to
+Or, to find out how many strategies only use 1 turn worth of memory to
 make a decision::
 
-    >>> len([s for s in axl.strategies if s().classifier['memory_depth']==1])
+    >>> filterset = {
+    ...     'memory_depth': 1
+    ... }
+    >>> strategies = axl.filtered_strategies(filterset)
+    >>> len(strategies)
     24
 
+Multiple filter can be specified within the filterset dictionary. To specify a
+range of memory_depth values, we can use the 'min_memory_depth' and
+'max_memory_depth' filters::
+
+    >>> filterset = {
+    ...     'min_memory_depth': 1,
+    ...     'max_memory_depth': 4
+    ... }
+    >>> strategies = axl.filtered_strategies(filterset)
+    >>> len(strategies)
+    41
+
 We can also identify strategies that make use of particular properties of the
 tournament. For example, here is the number of strategies that  make use of the
 length of each match of the tournament::
 
-    >>> len([s() for s in axl.strategies if 'length' in s().classifier['makes_use_of']])
-    10
+    >>> filterset = {
+    ...     'makes_use_of': ['length']
+    ... }
+    >>> strategies = axl.filtered_strategies(filterset)
+    >>> len(strategies)
+    4
 
-Here are how many of the strategies that make use of the particular game being
-played (whether or not it's the default Prisoner's dilemma)::
+Note that in the filterset dictionary, the value for the 'makes_use_of' key
+must be a list. Here is how we might identify the number of strategies that use
+both the length of the tournament and the game being played::
 
-    >>> len([s() for s in axl.strategies if 'game' in s().classifier['makes_use_of']])
-    22
+    >>> filterset = {
+    ...     'makes_use_of': ['length', 'game']
+    ... }
+    >>> strategies = axl.filtered_strategies(filterset)
+    >>> len(strategies)
+    0
 
 Some strategies have been classified as having a particularly long run time::
 
-    >>> len([s() for s in axl.strategies if s().classifier['long_run_time']])
+    >>> filterset = {
+    ...     'long_run_time': True
+    ... }
+    >>> strategies = axl.filtered_strategies(filterset)
+    >>> len(strategies)
     10
 
-Similarly, strategies that :code:`manipulate_source`, :code:`manipulate_state`
+Strategies that :code:`manipulate_source`, :code:`manipulate_state`
 and/or :code:`inspect_source` return :code:`False` for the :code:`obey_axelrod`
 function::
 


Test patch:
diff --git a/axelrod/tests/integration/test_filtering.py b/axelrod/tests/integration/test_filtering.py
new file mode 100644
index 00000000..8bfc2873
--- /dev/null
+++ b/axelrod/tests/integration/test_filtering.py
@@ -0,0 +1,86 @@
+import unittest
+from hypothesis import given, example
+from hypothesis.strategies import integers
+from axelrod import all_strategies, filtered_strategies
+
+
+class TestFiltersAgainstComprehensions(unittest.TestCase):
+    """
+    Test that the results of filtering strategies via a filterset dict
+    match the results from using a list comprehension.
+    """
+
+    def test_boolean_filtering(self):
+
+        classifiers = [
+            'stochastic',
+            'long_run_time',
+            'manipulates_state',
+            'manipulates_source',
+            'inspects_source']
+
+        for classifier in classifiers:
+            comprehension = set([
+                s for s in all_strategies if
+                s.classifier[classifier]])
+            filterset = {
+                classifier: True
+            }
+        filtered = set(filtered_strategies(filterset))
+        self.assertEqual(comprehension, filtered)
+
+    @given(
+        min_memory_depth=integers(min_value=1, max_value=10),
+        max_memory_depth=integers(min_value=1, max_value=10),
+        memory_depth=integers(min_value=1, max_value=10))
+    @example(
+        min_memory_depth=float('inf'),
+        max_memory_depth=float('inf'),
+        memory_depth=float('inf'))
+    def test_memory_depth_filtering(self, min_memory_depth, max_memory_depth,
+                                    memory_depth):
+
+        min_comprehension = set([
+            s for s in all_strategies if
+            s.classifier['memory_depth'] >= min_memory_depth])
+        min_filterset = {
+            'min_memory_depth': min_memory_depth
+        }
+        min_filtered = set(filtered_strategies(min_filterset))
+        self.assertEqual(min_comprehension, min_filtered)
+
+        max_comprehension = set([
+            s for s in all_strategies if
+            s.classifier['memory_depth'] <= max_memory_depth])
+        max_filterset = {
+            'max_memory_depth': max_memory_depth
+        }
+        max_filtered = set(filtered_strategies(max_filterset))
+        self.assertEqual(max_comprehension, max_filtered)
+
+        comprehension = set([
+            s for s in all_strategies if
+            s.classifier['memory_depth'] == memory_depth])
+        filterset = {
+            'memory_depth': memory_depth
+        }
+        filtered = set(filtered_strategies(filterset))
+        self.assertEqual(comprehension, filtered)
+
+    def test_makes_use_of_filtering(self):
+        classifiers = [
+            ['game'],
+            ['length'],
+            ['game', 'length']
+        ]
+
+        for classifier in classifiers:
+            comprehension = set([
+                s for s in all_strategies if
+                set(classifier).issubset(set(s.classifier['makes_use_of']))
+            ])
+            filterset = {
+                'makes_use_of': classifier
+            }
+            filtered = set(filtered_strategies(filterset))
+            self.assertEqual(comprehension, filtered)
diff --git a/axelrod/tests/unit/test_classification.py b/axelrod/tests/unit/test_classification.py
index d6fba108..110fbaa5 100644
--- a/axelrod/tests/unit/test_classification.py
+++ b/axelrod/tests/unit/test_classification.py
@@ -17,7 +17,8 @@ class TestClassification(unittest.TestCase):
 
         for s in axelrod.all_strategies:
             s = s()
-            self.assertTrue(None not in [s.classifier[key] for key in known_keys])
+            self.assertTrue(
+                None not in [s.classifier[key] for key in known_keys])
 
     def test_multiple_instances(self):
         """Certain instances of classes of strategies will have different
@@ -184,7 +185,8 @@ class TestStrategies(unittest.TestCase):
         self.assertTrue(axelrod.MetaMajority in axelrod.strategies)
 
         self.assertTrue(axelrod.MetaHunter in axelrod.strategies)
-        self.assertFalse(axelrod.MetaHunter in axelrod.long_run_time_strategies)
+        self.assertFalse(
+            axelrod.MetaHunter in axelrod.long_run_time_strategies)
 
     def test_demo_strategies(self):
         demo_strategies = [axelrod.Cooperator,
diff --git a/axelrod/tests/unit/test_filters.py b/axelrod/tests/unit/test_filters.py
new file mode 100644
index 00000000..65da7c77
--- /dev/null
+++ b/axelrod/tests/unit/test_filters.py
@@ -0,0 +1,161 @@
+import unittest
+from axelrod.strategies._filters import *
+from axelrod import filtered_strategies
+from hypothesis import given, example
+from hypothesis.strategies import integers
+import operator
+
+
+class TestFilters(unittest.TestCase):
+
+    class TestStrategy(object):
+        classifier = {
+            'stochastic': True,
+            'inspects_source': False,
+            'memory_depth': 10,
+            'makes_use_of': ['game', 'length']
+        }
+
+    def test_equality_filter(self):
+        self.assertTrue(
+            passes_operator_filter(
+                self.TestStrategy, 'stochastic', True, operator.eq))
+        self.assertFalse(
+            passes_operator_filter(
+                self.TestStrategy, 'stochastic', False, operator.eq))
+        self.assertTrue(
+            passes_operator_filter(
+                self.TestStrategy, 'inspects_source', False, operator.eq))
+        self.assertFalse(
+            passes_operator_filter(
+                self.TestStrategy, 'inspects_source', True, operator.eq))
+
+    @given(
+        smaller=integers(min_value=0, max_value=9),
+        larger=integers(min_value=11, max_value=100),
+    )
+    @example(smaller=0, larger=float('inf'))
+    def test_inequality_filter(self, smaller, larger):
+        self.assertTrue(passes_operator_filter(
+            self.TestStrategy, 'memory_depth', smaller, operator.ge))
+        self.assertTrue(passes_operator_filter(
+            self.TestStrategy, 'memory_depth', larger, operator.le))
+        self.assertFalse(passes_operator_filter(
+            self.TestStrategy, 'memory_depth', smaller, operator.le))
+        self.assertFalse(passes_operator_filter(
+            self.TestStrategy, 'memory_depth', larger, operator.ge))
+
+    def test_list_filter(self):
+        self.assertTrue(passes_in_list_filter(
+            self.TestStrategy, 'makes_use_of', ['game']))
+        self.assertTrue(passes_in_list_filter(
+            self.TestStrategy, 'makes_use_of', ['length']))
+        self.assertTrue(passes_in_list_filter(
+            self.TestStrategy, 'makes_use_of', ['game', 'length']))
+        self.assertFalse(passes_in_list_filter(
+            self.TestStrategy, 'makes_use_of', 'test'))
+
+    @given(
+        smaller=integers(min_value=0, max_value=9),
+        larger=integers(min_value=11, max_value=100),
+    )
+    @example(smaller=0, larger=float('inf'))
+    def test_passes_filterset(self, smaller, larger):
+
+        full_passing_filterset_1 = {
+            'stochastic': True,
+            'inspects_source': False,
+            'min_memory_depth': smaller,
+            'max_memory_depth': larger,
+            'makes_use_of': ['game', 'length']
+        }
+
+        full_passing_filterset_2 = {
+            'stochastic': True,
+            'inspects_source': False,
+            'memory_depth': 10,
+            'makes_use_of': ['game', 'length']
+        }
+
+        sparse_passing_filterset = {
+            'stochastic': True,
+            'inspects_source': False,
+            'makes_use_of': ['length']
+        }
+
+        full_failing_filterset = {
+            'stochastic': False,
+            'inspects_source': False,
+            'min_memory_depth': smaller,
+            'max_memory_depth': larger,
+            'makes_use_of': ['length']
+        }
+
+        sparse_failing_filterset = {
+            'stochastic': False,
+            'inspects_source': False,
+            'min_memory_depth': smaller,
+        }
+
+        self.assertTrue(passes_filterset(
+            self.TestStrategy, full_passing_filterset_1))
+        self.assertTrue(passes_filterset(
+            self.TestStrategy, full_passing_filterset_2))
+        self.assertTrue(passes_filterset(
+            self.TestStrategy, sparse_passing_filterset))
+        self.assertFalse(passes_filterset(
+            self.TestStrategy, full_failing_filterset))
+        self.assertFalse(passes_filterset(
+            self.TestStrategy, sparse_failing_filterset))
+
+    def test_filtered_strategies(self):
+
+        class StochasticTestStrategy(object):
+            classifier = {
+                'stochastic': True,
+                'memory_depth': float('inf'),
+                'makes_use_of': []
+            }
+
+        class MemoryDepth2TestStrategy(object):
+            classifier = {
+                'stochastic': False,
+                'memory_depth': 2,
+                'makes_use_of': []
+            }
+
+        class UsesLengthTestStrategy(object):
+            classifier = {
+                'stochastic': True,
+                'memory_depth': float('inf'),
+                'makes_use_of': ['length']
+            }
+
+        strategies = [
+            StochasticTestStrategy,
+            MemoryDepth2TestStrategy,
+            UsesLengthTestStrategy
+        ]
+
+        stochastic_filterset = {
+            'stochastic': True
+        }
+
+        deterministic_filterset = {
+            'stochastic': False
+        }
+
+        uses_length_filterset = {
+            'stochastic': True,
+            'makes_use_of': ['length']
+        }
+
+        self.assertEqual(
+            filtered_strategies(stochastic_filterset, strategies),
+            [StochasticTestStrategy, UsesLengthTestStrategy])
+        self.assertEqual(
+            filtered_strategies(deterministic_filterset, strategies),
+            [MemoryDepth2TestStrategy])
+        self.assertEqual(
+            filtered_strategies(uses_length_filterset, strategies),
+            [UsesLengthTestStrategy])