Skip to content

Commit

Permalink
Merge pull request #6 from todofixthis/release/2.0.1
Browse files Browse the repository at this point in the history
Filters v2.0.1
  • Loading branch information
todofixthis authored Sep 30, 2019
2 parents 6866e72 + 089b85d commit a832697
Show file tree
Hide file tree
Showing 14 changed files with 410 additions and 79 deletions.
65 changes: 61 additions & 4 deletions docs/filters_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is in contrast to :ref:`Complex Filters <complex-filters>`, which operate
on collections of values.

String Filters
--------------
^^^^^^^^^^^^^^
These filters are designed to operate on (or convert to) string values.

*Important:* to ensure consistent behavior between Python 2 and Python 3,
Expand Down Expand Up @@ -129,7 +129,7 @@ string filters only accept unicode strings, unless otherwise noted.
version in the filter initializer.

Number Filters
--------------
^^^^^^^^^^^^^^
These filters are designed to operate on (or convert to) numeric types.

:py:class:`filters.Decimal`
Expand Down Expand Up @@ -172,7 +172,7 @@ These filters are designed to operate on (or convert to) numeric types.
`floating-point precision <https://en.wikipedia.org/wiki/Floating_point#Accuracy_problems>`_.

Collection Filters
------------------
^^^^^^^^^^^^^^^^^^
These filters are designed to operate on collections of values.
Most of these filters can also operate on strings, except where noted.

Expand Down Expand Up @@ -227,7 +227,7 @@ Most of these filters can also operate on strings, except where noted.


Miscellaneous Filters
---------------------
^^^^^^^^^^^^^^^^^^^^^
These filters do various things that defy categorization.

:py:class:`filters.Array`
Expand All @@ -236,6 +236,18 @@ These filters do various things that defy categorization.
For example, ``list`` or any class that extends ``typing.Sequence`` will
pass, but any string type (or subclass thereof) will fail.

:py:class:`filters.Call`
Calls an arbitrary function on the incoming value.

This filter is almost always inferior to
:doc:`creating a custom filter </writing_filters>`, but it can be a useful
way to quickly inject a function into a filter workflow to see if it will
work.

.. important::
The function must raise a :py:class:`filters.FilterError` to indicate that
the incoming value is not valid.

:py:class:`filters.NoOp`
This filter returns the incoming value unmodified.

Expand Down Expand Up @@ -309,6 +321,51 @@ These filters are covered in more detail in :doc:`/complex_filters`.
``FilterRepeater`` can also process mappings (e.g., ``dict``); it will apply
the filters to every value in the mapping, preserving the keys.

:py:class:`filters.FilterSwitch`
Conditionally invokes a filter based on the output of a function.

``FilterSwitch`` takes 2-3 parameters:

- ``getter: Callable[[Any], Hashable]`` - a function that extracts the
comparison value from the incoming value. Whatever this function returns
will be matched against the keys in ``cases``.
- ``cases: Mapping[Hashable, FilterCompatible]`` - a mapping of valid
comparison values and their corresponding filters.
- ``default: Optional[FilterCompatible]`` - if specified, this is the filter
that will be used if the comparison value doesn't match any cases. If not
specified, then the incoming value will be considered invalid if the
comparison value doesn't match any cases.

Example of a ``FilterSwitch`` that selects the correct filter to use based
upon the incoming value's ``name`` value:

.. code-block:: py
switch = f.FilterSwitch(
# This function will extract the comparison value.
getter=lambda value: value['name'],
# These are the cases that the comparison value might
# match.
cases={
'price': f.FilterMapper({'value': f.Int | f.Min(0)}),
'color': f.FilterMapper({'value': f.Choice({'r', 'g', 'b'})}),
# etc.
},
# This is the filter that will be used if none of the cases match.
default=f.FilterMapper({'value': f.Unicode}),
)
# Applies the 'price' filter:
switch.apply({'name': price, 'value': 42})
# Applies the 'color' filter:
switch.apply({'name': color, 'value': 'b'})
# Applies the default filter:
switch.apply({'name': 'mfg', 'value': 'Acme Widget Co.'})
Extensions
==========
The following filters are provided by the
Expand Down
6 changes: 3 additions & 3 deletions docs/writing_filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ To create a new filter, write a class that extends
Validation
----------
^^^^^^^^^^
To implement validation in your filter, add the following:

- Define a unique code for each validation error.
Expand Down Expand Up @@ -136,7 +136,7 @@ Here's the ``Pkcs7Pad`` filter with a little bit of validation logic:
Unit Tests
----------
^^^^^^^^^^
To help you unit test your custom filters, the Filters library provides a helper
class called :py:class:`test.BaseFilterTestCase`.

Expand Down Expand Up @@ -181,7 +181,7 @@ Here's a starter test case for ``Pkcs7Pad``:
Registering Your Filters (Optional)
-----------------------------------
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Once you've packaged up your filters, you can register them with the Extensions
framework to add them to the (nearly) top-level ``filters.ext`` namespace.

Expand Down
26 changes: 26 additions & 0 deletions filters/aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from collections import OrderedDict
from typing import Mapping, Sequence

__all__ = [
'JSON_ALIASES',
]

# Used by e.g. :py:class:`Type` and :py:class:`Array` to mask
# python-specific names in error messages.
JSON_ALIASES = {
# Builtins
bool: 'Boolean',
bytes: 'String',
dict: 'Object',
float: 'Number',
int: 'Number',
list: 'Array',
str: 'String',

# Collections
OrderedDict: 'Object',

# Typing
Mapping: 'Object',
Sequence: 'Array',
}
38 changes: 10 additions & 28 deletions filters/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABCMeta, abstractmethod as abstract_method
from copy import copy
from typing import Any, Callable, Iterable, List, Mapping, \
MutableMapping, Optional as OptionalType, Sequence, Tuple, Union
MutableMapping, Optional as OptionalType, Tuple, Union
from weakref import ProxyTypes, proxy

__all__ = [
Expand Down Expand Up @@ -31,7 +31,7 @@ class FilterMeta(ABCMeta):
# noinspection PyShadowingBuiltins
def __init__(cls, what, bases=None, dict=None, **kwargs):
# noinspection PyArgumentList
super(FilterMeta, cls).__init__(what, bases, dict, **kwargs)
super().__init__(what, bases, dict, **kwargs)

if not hasattr(cls, 'templates'):
cls.templates = {}
Expand Down Expand Up @@ -75,11 +75,11 @@ class BaseFilter(metaclass=FilterMeta):
}

def __init__(self):
super(BaseFilter, self).__init__()
super().__init__()

self._parent = None # type: Optional[BaseFilter]
self._handler = None # type: Optional[BaseInvalidValueHandler]
self._key = None # type: Optional[str]
self._parent = None # type: OptionalType[BaseFilter]
self._handler = None # type: OptionalType[BaseInvalidValueHandler]
self._key = None # type: OptionalType[str]

#
# Indicates whether the Filter detected any invalid values.
Expand Down Expand Up @@ -465,7 +465,7 @@ class FilterChain(BaseFilter):
"""

def __init__(self, start_filter: FilterCompatible = None) -> None:
super(FilterChain, self).__init__()
super().__init__()

self._filters = [] # type: List[BaseFilter]

Expand Down Expand Up @@ -498,7 +498,7 @@ def __copy__(cls, the_filter: 'FilterChain') -> 'FilterChain':
"""
Creates a shallow copy of the object.
"""
new_filter = super(FilterChain, cls).__copy__(the_filter)
new_filter = super().__copy__(the_filter)
new_filter._filters = the_filter._filters[:]
# noinspection PyTypeChecker
return new_filter
Expand Down Expand Up @@ -583,7 +583,7 @@ def __init__(self, *args, **kwargs):
# Exception kwargs are deprecated in Python 3, but keeping them
# around for compatibility with Python 2.
# noinspection PyArgumentList
super(FilterError, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.context = {}


Expand All @@ -603,24 +603,6 @@ def handle_invalid_value(
raise error


# Used by :py:meth:`Type.__init__` to inject JSON data types in place of
# Python type names in error messages.
JSON_ALIASES = {
# Builtins
bool: 'Boolean',
bytes: 'String',
dict: 'Object',
float: 'Number',
int: 'Number',
list: 'Array',
str: 'String',

# Typing
Mapping: 'Array',
Sequence: 'Array',
}


# This filter is used extensively by other filters.
# To avoid lots of needless "circular import" hacks, we'll put it in
# the base module.
Expand Down Expand Up @@ -655,7 +637,7 @@ def __init__(
This is useful for providing more context- appropriate
names to end users and/or masking native Python type names.
"""
super(Type, self).__init__()
super().__init__()

# A pinch of syntactic sugar.
self.allowed_types = (
Expand Down
59 changes: 54 additions & 5 deletions filters/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from collections import OrderedDict

from filters.base import BaseFilter, FilterCompatible, FilterError, Type
from filters.simple import Length
from filters.simple import Choice, Length
from filters.string import Unicode

__all__ = [
'FilterMapper',
'FilterRepeater',
'FilterSwitch',
'NamedTuple',
]

Expand Down Expand Up @@ -51,7 +52,7 @@ def __init__(
Set to ``None`` (default) to allow any key/index.
"""
super(FilterRepeater, self).__init__()
super().__init__()

self._filter_chain = self.resolve_filter(filter_chain, parent=self)

Expand All @@ -72,7 +73,7 @@ def __copy__(cls, the_filter: 'FilterRepeater') -> 'FilterRepeater':
"""
Creates a shallow copy of the object.
"""
new_filter = super(FilterRepeater, cls).__copy__(the_filter)
new_filter = super().__copy__(the_filter)

new_filter._filter_chain = the_filter._filter_chain
new_filter.restrict_keys = the_filter.restrict_keys
Expand Down Expand Up @@ -225,7 +226,7 @@ def __init__(
omitted from the filtered value.
- <Iterable>: Only the specified extra keys are allowed.
"""
super(FilterMapper, self).__init__()
super().__init__()

self._filters = OrderedDict()

Expand Down Expand Up @@ -390,6 +391,54 @@ def unicodify_key(key: typing.Any) -> str:
return repr(key)


class FilterSwitch(BaseFilter):
"""
Chooses the next filter to apply based on the output of a callable.
"""

def __init__(
self,
getter: typing.Callable[[typing.Any], typing.Hashable],
cases: typing.Mapping[typing.Hashable, FilterCompatible],
default: typing.Optional[FilterCompatible] = None,
) -> None:
"""
:param getter:
Callable used to extract the value to match against switch
cases.
:param cases:
Mapping of possible values to the corresponding filters.
:param default:
Default filter to use, if none of the cases are matched.
If null (default) then the value will be considered invalid
if it doesn't match any cases.
"""
super().__init__()

self.getter = getter
self.cases = cases
self.default = default

def _apply(self, value):
gotten = self.getter(value) # type: typing.Hashable

if not self.default:
gotten = self._filter(gotten, Choice(self.cases.keys()))

if self._has_errors:
return None

if gotten in self.cases:
return self._filter(value, self.cases[gotten])

# If we get here, then we have set a default filter.
return self._filter(value, self.default)



class NamedTuple(BaseFilter):
"""
Attempts to convert the incoming value into a namedtuple.
Expand Down Expand Up @@ -426,7 +475,7 @@ def __init__(
>>> filter_chain.apply(['64', '128', '192'])
Color(r=64, g=128, b=192)
"""
super(NamedTuple, self).__init__()
super().__init__()

self.type = type_

Expand Down
2 changes: 1 addition & 1 deletion filters/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class FilterExtensionRegistry(EntryPointClassRegistry):
"""

def __init__(self, group: str = GROUP_NAME) -> None:
super(FilterExtensionRegistry, self).__init__(group)
super().__init__(group)

def __getattr__(self, item: str) -> typing.Type[BaseFilter]:
return self[item]
Expand Down
Loading

0 comments on commit a832697

Please sign in to comment.