Skip to content
This repository has been archived by the owner on Apr 10, 2023. It is now read-only.

Commit

Permalink
Embed 'serde-ext' package code in serde (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
rossmacarthur committed Sep 8, 2019
1 parent 54ca64d commit 707bc3e
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 72 deletions.
9 changes: 5 additions & 4 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,19 @@ Development process

git clone git@github.com:your_name_here/serde.git

3. Setup and activate your virtualenv using pyenv or similar. You can use ``make install-all`` to
install the package and all development dependencies into your virtualenv.
3. Setup and activate your virtualenv using pyenv or similar. You can use
``just install-all`` to install the package and all development dependencies
into your virtualenv.

4. Create a branch for local development::

git checkout -b name-of-your-bugfix-or-feature

5. Make your changes locally.

6. Run all lints using ``make lint``.
6. Run all lints using ``just lint``.

7. Run all tests using ``make test``.
7. Run all tests using ``just test``.

8. Commit your changes and push your branch to GitHub::

Expand Down
6 changes: 3 additions & 3 deletions Justfile
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ check-venv:

# Install package and all features.
install: check-venv
$VIRTUAL_ENV/bin/pip install -e .
$VIRTUAL_ENV/bin/pip install -e ".[ext]"

# Install package, all features, and all development dependencies.
install-all: check-venv
$VIRTUAL_ENV/bin/pip install -r ci/requirements/lint.txt -r requirements/test.txt -e .
$VIRTUAL_ENV/bin/pip install -r ci/requirements/lint.txt -r ci/requirements/test.txt -e ".[ext]"

# Run all lints.
lint:
Expand All @@ -54,7 +54,7 @@ test:
# Run all tests.
test-all:
$VIRTUAL_ENV/bin/pytest -vv --cov=serde --cov-report term-missing --cov-fail-under 100 \
--doctest-modules --doctest-import "*<serde" "datetime"
--doctest-modules --doctest-import "*<serde"

# Build source and wheel package.
dist: clean
Expand Down
4 changes: 2 additions & 2 deletions RELEASES.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
:tocdepth: 1

Releases
========

Expand All @@ -8,6 +6,7 @@ Releases

*Unreleased*

- Embed ``serde-ext`` package code in serde. (`#104`_)
- Rework validators as classes. (`#102`_)
- Documentation overhaul. (`#101`_)
- Rework tags to subclass ``BaseField``. (`#100`_)
Expand All @@ -17,6 +16,7 @@ Releases
.. _#100: https://github.com/rossmacarthur/serde/pull/100
.. _#101: https://github.com/rossmacarthur/serde/pull/101
.. _#102: https://github.com/rossmacarthur/serde/pull/102
.. _#104: https://github.com/rossmacarthur/serde/pull/104

0.6.2
-----
Expand Down
4 changes: 2 additions & 2 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ code.literal {
}

dl.class, dl.exception {
padding-top: 15px;
padding-bottom: 15px;
padding-top: 10px;
padding-bottom: 10px;
}

table.field-list th.field-name {
Expand Down
14 changes: 14 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ Miscellanous
.. autoclass:: serde.fields.Constant
.. autoclass:: serde.fields.Choice

Extended
^^^^^^^^

The following fields are available with the ``ext`` feature.

.. autoclass:: serde.fields.Domain
.. autoclass:: serde.fields.Email
.. autoclass:: serde.fields.Ipv4Address
.. autoclass:: serde.fields.Ipv6Address
.. autoclass:: serde.fields.MacAddress
.. autoclass:: serde.fields.Slug
.. autoclass:: serde.fields.Url

Tags
----

Expand Down Expand Up @@ -77,3 +90,4 @@ Exceptions
.. autoexception:: serde.exceptions.InstantiationError
.. autoexception:: serde.exceptions.NormalizationError
.. autoexception:: serde.exceptions.ValidationError
.. autoexception:: serde.exceptions.MissingDependency
5 changes: 4 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
'sphinx.ext.napoleon',
'sphinx.ext.viewcode'
]
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'validators': ('https://validators.readthedocs.io/en/latest/', None)
}
master_doc = 'index'
autodoc_member_order = 'bysource'
napoleon_include_init_with_doc = True
Expand Down
2 changes: 0 additions & 2 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
:tocdepth: 2

.. include:: ../CONTRIBUTING.rst
2 changes: 2 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
:tocdepth: 1

.. include:: ../RELEASES.rst
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ def get_metadata():

metadata = {
key: re.search(r'__' + key + r"__ = '(.*?)'", about_text).group(1)
for key in ('title', 'version', 'url', 'author', 'author_email', 'license', 'description')
for key in (
'title', 'version', 'url', 'author',
'author_email', 'license', 'description'
)
}
metadata['name'] = metadata.pop('title')

Expand All @@ -40,7 +43,7 @@ def get_metadata():
'six >=1.0.0, <2.0.0'
]
ext_requires = [
'serde-ext >=0.1.0, <0.2.0'
'validators >=0.12.0, <0.15.0'
]

setup(
Expand Down
6 changes: 6 additions & 0 deletions src/serde/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ def __str__(self):
return self.message


class MissingDependency(BaseError):
"""
Raised when a dependency is missing.
"""


class SerdeError(BaseError):
"""
Raised when any `~serde.Model` stage fails.
Expand Down
54 changes: 50 additions & 4 deletions src/serde/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
ValidationError,
map_errors
)
from serde.utils import applied, chained, is_subclass, try_import_all, zip_equal
from serde.utils import applied, chained, is_subclass, try_lookup, zip_equal


def _resolve_to_field_instance(thing, none_allowed=True):
Expand Down Expand Up @@ -354,15 +354,15 @@ def normalize(self, value):
"""
Normalize a value according to this field's specification.
By default this method does not do anything and should be overridden.
By default this method does not do anything.
"""
return value

def validate(self, value):
"""
Validate a value according to this field's specification.
By default this method does not do anything and should be overridden.
By default this method does not do anything.
"""
pass

Expand Down Expand Up @@ -1166,7 +1166,53 @@ def normalize(self, value):
pass


try_import_all('serde_ext.fields', globals())
def create_from(foreign, name=None, human=None):
"""
Create a new `Field` class from a `validators` function.
"""
suffix = foreign.split('.', 1)[1]

if name is None:
name = suffix.title()
if human is None:
human = suffix

doc = """\
A string field that asserts the string is a valid {}.
The validation is delegated to `{}`.
Args:
**kwargs: keyword arguments for the `Field` constructor.
""".format(human, foreign)

field_cls = type(name, (Str,), {'__doc__': doc})

def __init__(self, **kwargs): # noqa: N807
super(field_cls, self).__init__(**kwargs)
self._validator = try_lookup(foreign)

def validate(self, value):
super(field_cls, self).validate(value)
if not self._validator(value):
raise ValidationError('{!r} is not a valid {}'.format(value, human))

field_cls.__init__ = __init__
field_cls.validate = validate

return field_cls


# Generate string fields using functions in the 'validators' package.
Domain = create_from('validators.domain')
Email = create_from('validators.email')
Ipv4Address = create_from('validators.ip_address.ipv4', name='Ipv4Address', human='IPv4 address')
Ipv6Address = create_from('validators.ip_address.ipv6', name='Ipv6Address', human='IPv6 address')
MacAddress = create_from('validators.mac_address', name='MacAddress', human='MAC address')
Slug = create_from('validators.slug')
Url = create_from('validators.url', human='URL')

del create_from

__all__ = [name for name, obj in globals().items() if is_subclass(obj, Field)]
__all__.append('create')
46 changes: 19 additions & 27 deletions src/serde/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from six.moves import zip_longest

from serde.exceptions import MissingDependency


def chained(funcs, value):
"""
Expand Down Expand Up @@ -94,43 +96,33 @@ def subclasses(cls):
return subs + variants


def try_import(name, package=None):
def try_lookup(name):
"""
Try import the given library, ignoring ImportErrors.
Try lookup a fully qualified Python path, importing the module if necessary.
Args:
name (str): the name to import.
package (str): the package this module belongs to.
name (str): the fully qualifed Python path. Example: 'validators.email'.
Returns:
module: the imported module or None.
"""
try:
return importlib.import_module(name, package=package)
except ImportError:
pass

the object at the path.
def try_import_all(name, namespace):
Raises:
serde.exceptions.MissingDepenency: if the path could not be imported.
"""
Try import the names from the given library, ignoring ImportErrors.
module, path = name.split('.', 1)

Args:
name (str): the name to import.
namespace (dict): the namespace to update.
"""
module = try_import(name)
try:
obj = importlib.import_module(module)
except ImportError:
raise MissingDependency(
"{!r} is missing, did you forget to install the 'ext' feature?"
.format(module)
)

if module:
if hasattr(module, '__all__'):
all_names = module.__all__
else:
all_names = (
name for name in dir(module)
if not name.startswith('_')
)
for attr in path.split('.'):
obj = getattr(obj, attr)

namespace.update({name: getattr(module, name) for name in all_names})
return obj


def zip_equal(*iterables):
Expand Down
4 changes: 1 addition & 3 deletions src/serde/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re

from serde.exceptions import ValidationError
from serde.utils import is_subclass, try_import_all
from serde.utils import is_subclass


class Validator(object):
Expand Down Expand Up @@ -230,8 +230,6 @@ def __call__(self, value):
)


try_import_all('serde_ext.validators', globals())

__all__ = [
name for name, obj in globals().items()
if is_subclass(obj, Validator)
Expand Down
33 changes: 11 additions & 22 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import types

from pytest import raises

from serde import utils
from serde import Model, fields, utils
from serde.exceptions import MissingDependency


def test_chained():
Expand Down Expand Up @@ -59,27 +58,17 @@ class B(Example):
assert utils.subclasses(Example) == [A, B]


def test_try_import():
# Check that the returned value is a module.
assert isinstance(utils.try_import('json'), types.ModuleType)

# Check that the returned value is None.
assert utils.try_import('not_a_real_package_i_hope') is None


def test_try_import_all_serde():
ns = {}
utils.try_import_all('serde', ns)
assert set(ns.keys()) == {'Model', 'fields', 'exceptions', 'tags', 'validators'}
def test_try_lookup():
assert utils.try_lookup('serde.fields.Str') is fields.Str
assert utils.try_lookup('serde.Model') is Model

with raises(MissingDependency) as e:
utils.try_lookup('not_a_real_pkg.not_a_real_module')

def test_try_import_all_tests():
ns = {}
utils.try_import_all('bisect', ns)
assert set(ns.keys()) == {
'bisect', 'bisect_left', 'bisect_right',
'insort', 'insort_left', 'insort_right'
}
assert e.value.message == (
"'not_a_real_pkg' is missing, "
"did you forget to install the 'ext' feature?"
)


def test_zip_equal():
Expand Down

0 comments on commit 707bc3e

Please sign in to comment.