diff --git a/.travis.yml b/.travis.yml index d58deae..919cfd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,24 @@ language: python -python: - - 2.7 - - 3.4 - - 3.5 +python: [2.7, 3.4, 3.5, 3.6] +env: +- FIXTURE_PATH=/tmp/schema_fixture/ install: - - pip install -U pip setuptools - - pip install -e .[tests] +- pip install tox-travis +- mkdir -p "$HOME/bin" +- curl -o "$HOME/bin/nirum" https://nightly-builds.nirum.org/travis-builds/nirum-linux-x86_64 +- chmod +x "$HOME/bin/nirum" script: - - py.test tests -v - - pip install 'typing<3.5.2' && py.test tests -v - - | - if [ -z "$(git diff --name-only "$TRAVIS_COMMIT_RANGE" | grep CHANGES\.rst)" ]; then - exit 1 - fi +- tox -e buildfixture +- tox +- tox -e docs +- '[[ "$TRAVIS_TAG" = "" ]] || [[ "$TRAVIS_TAG" = "$(python setup.py --version)" ]]' +- | + if git show --format=%B --quiet "${TRAVIS_PULL_REQUEST_SHA:-${TRAVIS_TAG:-${TRAVIS_COMMIT}}}" \ + | grep '\[changelog skip\]' > /dev/null; then + echo "Skip changelog checker..." + elif [[ "$TRAVIS_TAG" != "" ]]; then + ! grep -i "to be released" CHANGES.rst + else + [[ "$TRAVIS_COMMIT_RANGE" = "" ]] || \ + [[ "$(git diff --name-only "$TRAVIS_COMMIT_RANGE" | grep CHANGES\.rst)" != "" ]] + fi diff --git a/CHANGES.rst b/CHANGES.rst index 0d824e5..8485106 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,44 @@ Changelog ========= -Version 0.4.3 +Version 0.7.1 +------------- + +To be released. + + +Version 0.7.0 +------------- + +Released on February 23, 2021. + +- ``nirum.rpc`` module and ``Client``, ``Service``, and ``WsgiApp`` in + the module, had been deprecated since 0.6.0, are now completely obsolete. +- Annotation parameters (``service_annotations``, ``method_annotations``, and + ``parameter_annotations``) of ``nirum.transport.Transport.call()`` method + now can take multiple arguments. +- Since Nirum compiler became to give ``__nirum_type__`` attribute to + every generated classes (see also the `pull request`__), + ``nirum.deserialize.deserialize_meta()`` function also became to leverage + it if present. +- Since Nirum compiler became to give ``__nirum_tag_classes__`` mapping to + every generated union classes (see also the `pull request`__), + ``nirum.deserialize.deserialize_union_type()`` function also became to + lerverage it if present. +- Removed unmaintained below codes. Now these will be generated by Nirum, + automatically. + - nirum/deserialize.py + - nirum/serialize.py + - nirum/validate.py + - tests/deserialize_test.py + - tests/serialize_test.py + - tests/validate_test.py + +__ https://github.com/spoqa/nirum/pull/192 +__ https://github.com/spoqa/nirum/pull/192 + + +Version 0.6.3 ------------- Released on April 5, 2018. @@ -10,10 +47,125 @@ Released on April 5, 2018. function) to ``nirum.datastructures.Map`` and ``nirum.datastructures.List``. [`#110`_] -.. _#110: https://github.com/spoqa/nirum-python/issues/110 +Version 0.6.2 +------------- -Version 0.4.2 +Released on February 11, 2018. + +- Added ``is_optional_type()`` to ensure optional type includes ``None`` type. +- ``nirum.datastructures.List`` became to show its contents when it's passed + to ``repr()``. [`#103`__, `#108`__ by Chang-soo Han] + +__ https://github.com/nirum-lang/nirum-python/issues/103 +__ https://github.com/nirum-lang/nirum-python/pull/108 + + +Version 0.6.1 +------------- + +Released on December 9, 2017. + +- Made ``nirum.datastructures.List`` to copy the given value so that + it doesn't refer given value's state and is immutable. + + +Version 0.6.0 +------------- + +Released on July 11, 2017. + +- Deprecated ``nirum.rpc`` module. + + This module and all it has provided are deprecated or obsolete. The most + of them are now distributed as separated packages, or replaced by a newer + concept. See also the below for details. + + It will be completely obsolete at version 0.7.0. + +- Client transport layer. [`#79`_] + + - Added ``nirum.transport.Transport`` interface. + + The recent builds of Nirum compiler became to generate ``*_Client`` classes + taking a ``nirum.transport.Transport`` instance through their constructor. + + Use nirum-python-http_ (PyPI handle: ``nirum-http``) instead for HTTP + client of services e.g.: + + .. code-block:: python + + from yourservice import YourService_Client + from nirum_http import HttpTransport + + transport = HttpTransport('https://service-host/') + client = YourService_Client(transport) + + - Deprecated ``nirum.rpc.Client`` type. The recent builds of Nirum compiler + became to generate ``*_Client`` classes for services without subclassing + ``nirum.rpc.Client``. + + The deprecated ``nirum.rpc.Client`` will be completely obsolete at + version 0.7.0. + +- ``nirum.rpc.Service`` was moved to ``nirum.service.Service``. + + The recent builds of Nirum compiler became to generate service classes + that inherit ``nirum.service.Service`` instead of ``nirum.rpc.Service``. + + The deprecated ``nirum.rpc.Service`` will be completely obsolete at + version 0.7.0. + +- Deprecated ``nirum.rpc.WsgiApp``. This will be completely obsolete at + version 0.7.0. + + Use nirum-python-wsgi_ (PyPI handle: ``nirum-wsgi``) instead. + +- ``nirum-server`` command is obsolete. The same command is now provided + by nirum-python-wsgi_ (PyPI handle: ``nirum-wsgi``), a separated package. + +- ``nirum.func.import_string()`` function and ``nirum.func.IMPORT_RE`` constant + are obsolete. + +- Fixed ``NameError`` raised from forward references. [`compiler #138`_] + +.. _#79: https://github.com/nirum-lang/nirum-python/issues/79 +.. _compiler #138: https://github.com/nirum-lang/nirum/issues/138 +.. _nirum-python-http: https://github.com/nirum-lang/nirum-python-http +.. _nirum-python-wsgi: https://github.com/nirum-lang/nirum-python-wsgi + + +Version 0.5.6 +------------- + +Released on April 5, 2018. + +- Fixed a bug that ``hash()`` on ``nirum.datastructures.List`` had raised + ``TypeError``. + + +Version 0.5.5 +------------- + +Released on April 5, 2018. + +- Added missing equality functions (i.e., ``==``, ``!=`` operators, & ``hash()`` + function) to ``nirum.datastructures.Map`` and ``nirum.datastructures.List``. + [`#110`_] + +.. _#110: https://github.com/nirum-lang/nirum-python/issues/110 + + +Version 0.5.4 +------------- + +Released on December 9, 2017. + +- Made ``nirum.datastructures.List`` to copy the given value so that + it doesn't refer given value's state and is immutable. + + +Version 0.5.3 ------------- Released on July 6, 2017. @@ -26,11 +178,88 @@ Released on July 6, 2017. that they can be encoded to JSON. +Version 0.5.2 +------------- + +Released on June 23, 2017. + +- ``url`` of ``nirum.rpc.Client`` and + ``method`` of ``nirum.rpc.Client.make_request`` + now can be both ``unicode`` and ``str`` on Python 2.7. [`#87`_] +- ``nirum.rpc.Client`` had been an old-style class on Python 2, but now + it became a new-style class also on Python 2. (As Python 3 has only new-style + class, there's no change on Python 3.) + +.. _#87: https://github.com/nirum-lang/nirum-python/pull/87 + + +Version 0.5.1 +------------- + +Released on June 22, 2017. + +- Added Python 3.6 support. +- Fixed a bug that service client methods hadn't raised the proper error + type but ``nirum.exc.UnexpectedNirumResponseError`` instead. [`#71`_] +- Wheel distributions (``nirum-*.whl``) are now universal between Python 2 + and 3. [`#78`_] +- ``nirum.rpc.Service`` had been an old-style class on Python 2, but now + it became a new-style class also on Python 2. (As Python 3 has only new-style + class, there's no change on Python 3.) [`#83`_] +- ``nirum.rpc.Client`` and its subtype became to raise ``TypeError`` with + a better error message when its ``make_request()`` method is overridden and + it returns a wrong artity of tuple. [`#80`_] +- ``nirum.rpc.WsgiApp`` and its subtype became to raise ``TypeError`` with + a better error message when its ``make_response()`` method is overridden and + it returns a wrong artity of tuple. [`#80`_] +- Fixed a bug that ``Client.ping()`` method had always raised ``TypeError``. + [`#80`_] +- Corrected a typo ``Accepts`` on request headers ``Client`` makes to + ``Accept``. + +.. _#78: https://github.com/nirum-lang/nirum-python/pull/78 +.. _#83: https://github.com/nirum-lang/nirum-python/issues/83 +.. _#80: https://github.com/nirum-lang/nirum-python/pull/80 + + +Version 0.5.0 +------------- + +Released on June 1, 2017. + +- Service methods became able to specify its error type. [`#71`_] +- Added ``nirum-server`` command to run simply Nirum service. + +.. _#71: https://github.com/nirum-lang/nirum-python/issues/71 + + +Version 0.4.3 +------------- + +Released on April 5, 2018. + +- Added missing equality functions (i.e., ``==``, ``!=`` operators, & ``hash()`` + function) to ``nirum.datastructures.Map`` and ``nirum.datastructures.List``. + [`#110`_] + + +Version 0.4.2 +------------- + +Released on July 6, 2017. + +- Fixed a serialization bug that other set-like (i.e. ``collections.Set``) types + than Python built-in ``set`` hadn't been reduced to simpler forms so that they + can be encoded to JSON. +- Fixed a serialization bug that other list-like (i.e. ``collections.Sequence``) + types than Python built-in ``list`` hadn't been reduced to simpler forms so + that they can be encoded to JSON. + Version 0.4.1 ------------- -Release on May 2, 2017. +Released on May 2, 2017. - Compare type with its abstract type in ``nirum.validate.validate_type``. @@ -38,17 +267,18 @@ Release on May 2, 2017. Version 0.4.0 ------------- -Release on March 20, 2017. +Released on March 20, 2017. - Encoding of map types was changed according to the `Nirum serialization specification`__. [`#66`_] - Added ``nirum.datastructures`` module and ``nirum.datastructures.Map`` which is an immutable dictionary. [`#66`_] -- Added ``nirum.datastructures.List`` which is an immutable list. [`#49`_] +- Added ``nirum.datastructures.List`` which is an immutable list. + [`#49`_] - Aliased ``nirum.datastructures.Map`` as ``map_type``, and ``nirum.datastructures.List`` as ``list_type`` to avoid name conflict with user-defined types. -__ https://github.com/spoqa/nirum/blob/f1629787f45fef17eeab8b4f030c34580e0446b8/docs/serialization.md -.. _#66: https://github.com/spoqa/nirum-python/pull/66 -.. _#49: https://github.com/spoqa/nirum-python/issues/49 +__ https://github.com/nirum-lang/nirum/blob/f1629787f45fef17eeab8b4f030c34580e0446b8/docs/serialization.md +.. _#66: https://github.com/nirum-lang/nirum-python/pull/66 +.. _#49: https://github.com/nirum-lang/nirum-python/issues/49 diff --git a/README.rst b/README.rst index e39c25a..5a3fe86 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,19 @@ nirum-python ============ -Python rumtime for Nirum_ IDL. Distributed under MIT license. +.. image:: https://badge.fury.io/py/nirum.svg + :target: https://pypi.org/project/nirum/ + :alt: Latest PyPI version -(You probably don't need directly use this package.) +.. image:: https://api.travis-ci.com/nirum-lang/nirum-python.svg + :target: https://travis-ci.com/nirum-lang/nirum-python + :alt: Build status -.. _Nirum: https://github.com/spoqa/nirum +The Nirum_ runtime library for Python. Distributed under MIT license. + +(You probably don't need to directly use this package.) + +.. _Nirum: https://github.com/nirum-lang/nirum .. include:: CHANGES.rst diff --git a/lint.sh b/lint.sh index 181fa0c..a6f63dd 100755 --- a/lint.sh +++ b/lint.sh @@ -2,7 +2,3 @@ set -e flake8 . - -if [[ "$(python -c "import sys;print(sys.version[0])")" != "2" ]]; then - import-order nirum ./nirum -fi diff --git a/nirum/__init__.py b/nirum/__init__.py index 3f47450..b5d3122 100644 --- a/nirum/__init__.py +++ b/nirum/__init__.py @@ -2,5 +2,5 @@ ~~~~~~~~~~~~~~~ """ -__version_info__ = 0, 4, 3 +__version_info__ = 0, 7, 1 __version__ = '.'.join(str(v) for v in __version_info__) diff --git a/nirum/_compat.py b/nirum/_compat.py index d953523..02930fd 100644 --- a/nirum/_compat.py +++ b/nirum/_compat.py @@ -1,7 +1,7 @@ import datetime import typing -__all__ = 'utc', 'is_union_type', 'get_union_types' +__all__ = 'utc', 'is_optional_type', 'is_union_type', 'get_union_types' try: @@ -43,6 +43,10 @@ def get_union_types(type_): else type_.__args__ +def is_optional_type(type_): + return is_union_type(type_) and type(None) in get_union_types(type_) + + def get_abstract_param_types(type_): return type_.__parameters__ if type_.__parameters__ else type_.__args__ diff --git a/nirum/datastructures.py b/nirum/datastructures.py index 228d7c4..377ce6a 100644 --- a/nirum/datastructures.py +++ b/nirum/datastructures.py @@ -69,8 +69,8 @@ def __repr__(self): class List(collections.Sequence): - def __init__(self, l): - self.l = l # noqa: E741 + def __init__(self, items): + self.items = list(items) def __eq__(self, other): if not (isinstance(other, collections.Sequence) and @@ -86,25 +86,29 @@ def __ne__(self, other): return not (self == other) def __getitem__(self, index): - return self.l[index] + return self.items[index] def __len__(self): - return len(self.l) + return len(self.items) def __contains__(self, item): - return item in self.l + return item in self.items def __iter__(self): - return iter(self.l) + return iter(self.items) def __hash__(self): - return hash(tuple(self)) + return hash(tuple(self.items)) def index(self, item): - return self.l.index(item) + return self.items.index(item) def count(self, item): - return self.l.count(item) + return self.items.count(item) + + def __repr__(self): + args = repr(self.items) + return '{0.__module__}.{0.__name__}({1})'.format(type(self), args) map_type = Map diff --git a/nirum/deserialize.py b/nirum/deserialize.py deleted file mode 100644 index 607f579..0000000 --- a/nirum/deserialize.py +++ /dev/null @@ -1,304 +0,0 @@ -""":mod:`nirum.deserialize` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -""" -import collections -import datetime -import decimal -import enum -import numbers -import typing -import uuid - -from iso8601 import iso8601, parse_date -from six import text_type - -from .datastructures import Map -from ._compat import get_tuple_param_types, get_union_types, is_union_type - -__all__ = ( - 'deserialize_abstract_type', - 'deserialize_boxed_type', - 'deserialize_iterable_abstract_type', - 'deserialize_meta', - 'deserialize_optional', - 'deserialize_primitive', - 'deserialize_record_type', - 'deserialize_tuple_type', - 'deserialize_unboxed_type', - 'deserialize_union_type', - 'is_support_abstract_type', -) -_NIRUM_PRIMITIVE_TYPE = { - float, decimal.Decimal, uuid.UUID, datetime.datetime, - datetime.date, bool, int, text_type, numbers.Integral -} - - -def is_support_abstract_type(t): - """FIXME: 3.5 only""" - if hasattr(t, '__origin__') and t.__origin__: - data_type = t.__origin__ - else: - data_type = t - abstract_types = { - typing.Sequence, - typing.List, - typing.Set, - typing.AbstractSet, - typing.Mapping, - typing.Dict, - } - return any(type_ is data_type for type_ in abstract_types) - - -def deserialize_iterable_abstract_type(cls, cls_origin_type, data): - abstract_type_map = { - typing.Sequence: list, - typing.List: list, - typing.Set: set, - typing.AbstractSet: set, - typing.Mapping: Map, - } - deserialized_data = data - cls_primitive_type = abstract_type_map[cls_origin_type] - # Whereas on Python/typing < 3.5.2 type parameters are stored in - # __parameters__ attribute, on Python/typing >= 3.5.2 __parameters__ - # attribute is gone and __args__ comes instead. - type_params = (cls.__args__ - if hasattr(cls, '__args__') - else cls.__parameters__) - if len(type_params) == 1: - elem_type, = type_params - if isinstance(elem_type, typing.TypeVar): - deserialized_data = cls_primitive_type(data) - else: - deserialized_data = cls_primitive_type( - deserialize_meta(elem_type, d) for d in data - ) - elif len(type_params) == 2: - # Key-value - key_type, value_type = type_params - assert not (isinstance(key_type, typing.TypeVar) or - isinstance(value_type, typing.TypeVar)) - if not isinstance(data, collections.Sequence): - raise ValueError('map must be an array of item objects e.g. ' - '[{"key": ..., "value": ...}, ...]') - - def parse_pair(pair): - if not isinstance(pair, collections.Mapping): - raise ValueError('map item must be a JSON object') - try: - key = pair['key'] - value = pair['value'] - except KeyError: - raise ValueError('map item must consist of "key" and "value" ' - 'fields e.g. {"key": ..., "value": ...}') - return ( - deserialize_meta(key_type, key), - deserialize_meta(value_type, value), - ) - deserialized_data = cls_primitive_type(map(parse_pair, data)) - return deserialized_data - - -def deserialize_abstract_type(cls, data): - abstract_type_map = { - typing.Sequence: list, - typing.List: list, - typing.Dict: dict, - typing.Set: set, - typing.AbstractSet: set, - } - cls_origin_type = cls.__origin__ - if cls_origin_type is None: - cls_origin_type = cls - iterable_types = { - typing.Sequence, typing.List, typing.Tuple, typing.Set, - typing.AbstractSet, typing.Mapping, - } - if cls_origin_type in iterable_types: - return deserialize_iterable_abstract_type(cls, cls_origin_type, data) - else: - return abstract_type_map[cls_origin_type](data) - - -def deserialize_tuple_type(cls, data): - tuple_types = get_tuple_param_types(cls) - if tuple_types is None and isinstance(data, tuple): - return data - tuple_type_length = len(tuple_types) - data_length = len(data) - if not tuple_types: - return tuple(data) - if tuple_type_length != data_length: - raise ValueError( - 'Expected {}-tuple, not {}-tuple'.format( - tuple_type_length, data_length - ) - ) - return tuple( - deserialize_meta(t, d) - for t, d in zip(tuple_types, data) - ) - - -def deserialize_primitive(cls, data): - if cls is datetime.datetime: - try: - d = parse_date(data) - except iso8601.ParseError: - raise ValueError("'{}' is not a datetime.".format(data)) - elif cls is datetime.date: - try: - d = parse_date(data).date() - except iso8601.ParseError: - raise ValueError("'{}' is not a date.".format(data)) - elif cls in {int, float, uuid.UUID, bool}: - d = cls(data) - elif cls is numbers.Integral: - d = data - elif cls is decimal.Decimal: - try: - d = cls(data) - except decimal.InvalidOperation: - raise ValueError("'{}' is not a decimal.".format(data)) - elif cls is text_type: - if not isinstance(data, text_type): - raise ValueError("'{}' is not a string.".format(data)) - d = cls(data) - else: - raise TypeError( - "'{0}' is not a primitive type.".format(typing._type_repr(cls)) - ) - return d - - -def deserialize_optional(cls, data): - union_types = get_union_types(cls) - if not any(isinstance(None, ut) for ut in union_types): - raise ValueError(cls) - if data is None: - return data - for union_type in union_types: - if isinstance(None, union_type): - continue - else: - try: - return deserialize_meta(union_type, data) - except ValueError: - continue - else: - raise ValueError() - - -def deserialize_meta(cls, data): - if hasattr(cls, '__nirum_tag__') or hasattr(cls, 'Tag'): - d = deserialize_union_type(cls, data) - elif hasattr(cls, '__nirum_record_behind_name__'): - d = deserialize_record_type(cls, data) - elif (hasattr(cls, '__nirum_inner_type__') or - hasattr(cls, '__nirum_boxed_type__')): - # FIXME: __nirum_boxed_type__ is for backward compatibility; - # remove __nirum_boxed_type__ in the near future - d = deserialize_unboxed_type(cls, data) - elif type(cls) is typing.TupleMeta: - # typing.Tuple dosen't have either `__origin__` and `__args__` - # so it have to be handled special case. - d = deserialize_tuple_type(cls, data) - elif is_support_abstract_type(cls): - d = deserialize_abstract_type(cls, data) - elif is_union_type(cls): - d = deserialize_optional(cls, data) - elif callable(cls) and cls in _NIRUM_PRIMITIVE_TYPE: - d = deserialize_primitive(cls, data) - elif isinstance(cls, enum.EnumMeta): - d = cls(data) - else: - raise TypeError('data is not deserializable: {!r} as {!r}'.format( - data, cls - )) - return d - - -def deserialize_unboxed_type(cls, value): - try: - inner_type = cls.__nirum_inner_type__ - except AttributeError: - # FIXME: __nirum_boxed_type__ is for backward compatibility; - # remove __nirum_boxed_type__ in the near future - inner_type = cls.__nirum_boxed_type__ - deserializer = getattr(inner_type, '__nirum_deserialize__', None) - if deserializer: - value = deserializer(value) - else: - value = deserialize_meta(inner_type, value) - return cls(value=value) - - -deserialize_boxed_type = deserialize_unboxed_type -# FIXME: deserialize_boxed_type() is for backward compatibility; -# remove it in the near future - - -def deserialize_record_type(cls, value): - if '_type' not in value: - raise ValueError('"_type" field is missing.') - if not cls.__nirum_record_behind_name__ == value['_type']: - raise ValueError( - '{0} expect "_type" equal to "{1.__nirum_record_behind_name__}"' - ', but found {2}.'.format( - typing._type_repr(cls), - cls, value['_type'] - ) - ) - args = {} - behind_names = cls.__nirum_field_names__.behind_names - for attribute_name, item in value.items(): - if attribute_name == '_type': - continue - if attribute_name in behind_names: - name = behind_names[attribute_name] - else: - name = attribute_name - args[name] = deserialize_meta(cls.__nirum_field_types__[name], item) - return cls(**args) - - -def deserialize_union_type(cls, value): - if '_type' not in value: - raise ValueError('"_type" field is missing.') - if '_tag' not in value: - raise ValueError('"_tag" field is missing.') - if not hasattr(cls, '__nirum_tag__'): - for sub_cls in cls.__subclasses__(): - if sub_cls.__nirum_tag__.value == value['_tag']: - cls = sub_cls - break - else: - raise ValueError( - '{0!r} is not deserialzable tag of `{1}`.'.format( - value, typing._type_repr(cls) - ) - ) - if not cls.__nirum_union_behind_name__ == value['_type']: - raise ValueError('{0} expect "_type" equal to' - ' "{1.__nirum_union_behind_name__}"' - ', but found {2}.'.format(typing._type_repr(cls), cls, - value['_type'])) - if not cls.__nirum_tag__.value == value['_tag']: - raise ValueError('{0} expect "_tag" equal to' - ' "{1.__nirum_tag__.value}"' - ', but found {1}.'.format(typing._type_repr(cls), - cls, value['_tag'])) - args = {} - behind_names = cls.__nirum_tag_names__.behind_names - for attribute_name, item in value.items(): - if attribute_name in {'_type', '_tag'}: - continue - if attribute_name in behind_names: - name = behind_names[attribute_name] - else: - name = attribute_name - args[name] = deserialize_meta(cls.__nirum_tag_types__[name], item) - return cls(**args) diff --git a/nirum/func.py b/nirum/func.py index bcb7a44..7d8f1fa 100644 --- a/nirum/func.py +++ b/nirum/func.py @@ -1,6 +1,6 @@ from six.moves import urllib -__all__ = 'url_endswith_slash', +__all__ = 'url_endswith_slash' def url_endswith_slash(url): diff --git a/nirum/rpc.py b/nirum/rpc.py deleted file mode 100644 index 1fc00e0..0000000 --- a/nirum/rpc.py +++ /dev/null @@ -1,404 +0,0 @@ -""":mod:`nirum.rpc` -~~~~~~~~~~~~~~~~~~~ - -""" -import collections -import json -import typing - -from six import integer_types, text_type -from six.moves import urllib -from werkzeug.exceptions import HTTPException -from werkzeug.http import HTTP_STATUS_CODES -from werkzeug.routing import Map, Rule -from werkzeug.wrappers import Request as WsgiRequest, Response as WsgiResponse - -from .constructs import NameDict -from .deserialize import deserialize_meta -from .exc import (InvalidNirumServiceMethodNameError, - InvalidNirumServiceMethodTypeError, - NirumProcedureArgumentRequiredError, - NirumProcedureArgumentValueError, - UnexpectedNirumResponseError) -from .func import url_endswith_slash -from .serialize import serialize_meta - -__all__ = 'Client', 'WsgiApp', 'Service', 'client_type', 'service_type' -JSONType = typing.Mapping[ - str, typing.Union[str, float, int, bool, object] -] - - -class Service: - """Nirum RPC service.""" - - __nirum_service_methods__ = {} - __nirum_method_names__ = NameDict([]) - - def __init__(self): - for method_name in self.__nirum_service_methods__: - try: - method = getattr(self, method_name) - except AttributeError: - raise InvalidNirumServiceMethodNameError( - "{0}.{1} inexist.".format( - typing._type_repr(self.__class__), method_name - ) - ) - if not callable(method): - raise InvalidNirumServiceMethodTypeError( - "{0}.{1} isn't callable".format( - typing._type_repr(self.__class__), method_name - ) - ) - - -class WsgiApp: - """Create WSGI application adapt Nirum service. - - :param service: A nirum service. - - """ - - #: (:class:`werkzeug.routing.Map`) url map - url_map = Map([ - Rule('/', endpoint='rpc'), - Rule('/ping/', endpoint='ping'), - ]) - - def __init__(self, service): - self.service = service - - def __call__(self, environ, start_response): - """ - - :param environ: - :param start_response: - - """ - return self.route(environ, start_response) - - def route(self, environ, start_response): - """Route - - :param environ: - :param start_response: - - """ - urls = self.url_map.bind_to_environ(environ) - request = WsgiRequest(environ) - try: - endpoint, args = urls.match() - except HTTPException as e: - return self.error(e.code, request)(environ, start_response) - else: - procedure = getattr(self, endpoint) - response = procedure(request, args) - return response(environ, start_response) - - def ping(self, request, args): - return WsgiResponse( - '"Ok"', - 200, - content_type='application/json' - ) - - def rpc(self, request, args): - """RPC - - :param request: - :args ???: - - """ - if request.method != 'POST': - return self.error(405, request) - payload = request.get_data(as_text=True) or '{}' - request_method = request.args.get('method') - if not request_method: - return self.error( - 400, request, - message="A query string parameter method= is missing." - ) - name_map = self.service.__nirum_method_names__ - try: - method_facial_name = name_map.behind_names[request_method] - except KeyError: - return self.error( - 400, - request, - message="Service dosen't have procedure named '{}'.".format( - request_method - ) - ) - try: - service_method = getattr(self.service, method_facial_name) - except AttributeError: - return self.error( - 400, - request, - message="Service has no procedure '{}'.".format( - request_method - ) - ) - if not callable(service_method): - return self.error( - 400, request, - message="Remote procedure '{}' is not callable.".format( - request_method - ) - ) - try: - request_json = json.loads(payload) - except ValueError: - return self.error( - 400, - request, - message="Invalid JSON payload: '{}'.".format(payload) - ) - type_hints = self.service.__nirum_service_methods__[method_facial_name] - try: - arguments = self._parse_procedure_arguments( - type_hints, - request_json - ) - except (NirumProcedureArgumentValueError, - NirumProcedureArgumentRequiredError) as e: - return self.error(400, request, message=str(e)) - try: - result = service_method(**arguments) - except Exception as e: - return self.error(500, request, str(e)) - if not self._check_return_type(type_hints['_return'], result): - return self.error( - 400, - request, - message="Incorrect return type '{0}' " - "for '{1}'. expected '{2}'.".format( - typing._type_repr(result.__class__), - request_method, - typing._type_repr(type_hints['_return']) - ) - ) - else: - return self._raw_response(200, serialize_meta(result)) - - def _parse_procedure_arguments(self, type_hints, request_json): - arguments = {} - name_map = type_hints['_names'] - for argument_name, type_ in type_hints.items(): - if argument_name.startswith('_'): - continue - behind_name = name_map[argument_name] - try: - data = request_json[behind_name] - except KeyError: - raise NirumProcedureArgumentRequiredError( - "A argument named '{}' is missing, it is required.".format( - behind_name - ) - ) - try: - arguments[argument_name] = deserialize_meta(type_, data) - except ValueError: - raise NirumProcedureArgumentValueError( - "Incorrect type '{0}' for '{1}'. " - "expected '{2}'.".format( - typing._type_repr(data.__class__), behind_name, - typing._type_repr(type_) - ) - ) - return arguments - - def _check_return_type(self, type_hint, procedure_result): - try: - deserialize_meta(type_hint, serialize_meta(procedure_result)) - except ValueError: - return False - else: - return True - - def make_error_response(self, error_type, message=None): - """Create error response json temporary. - - .. code-block:: nirum - - union error - = not-found (text message) - | bad-request (text message) - | ... - - """ - # FIXME error response has to be generated from nirum core. - return { - '_type': 'error', - '_tag': error_type, - 'message': message, - } - - def error(self, status_code, request, message=None): - """Handle error response. - - :param int status_code: - :param request: - :return: - - """ - status_code_text = HTTP_STATUS_CODES.get(status_code, 'http error') - status_error_tag = status_code_text.lower().replace(' ', '_') - custom_response_map = { - 404: self.make_error_response( - status_error_tag, - 'The requested URL {} was not found on this service.'.format( - request.path - ) - ), - 400: self.make_error_response(status_error_tag, message), - 405: self.make_error_response( - status_error_tag, - 'The requested URL {} was not allowed HTTP method {}.'.format( - request.path, request.method - ) - ), - } - return self._raw_response( - status_code, - custom_response_map.get( - status_code, - self.make_error_response( - status_error_tag, message or status_code_text - ) - ) - ) - - def make_response(self, status_code, headers, content): - return status_code, headers, content - - def _raw_response(self, status_code, response_json, **kwargs): - response_tuple = self.make_response( - status_code, headers=[('Content-type', 'application/json')], - content=json.dumps(response_json).encode('utf-8') - ) - if not isinstance(response_tuple, collections.Sequence) and \ - len(response_tuple) == 3: - raise TypeError( - 'make_response() must return a triple of ' - '(status_code, content, headers): {}'.format(response_tuple) - ) - status_code, headers, content = response_tuple - if not isinstance(status_code, integer_types): - raise TypeError( - '`status_code` have to be instance of integer. not {}'.format( - typing._type_repr(type(status_code)) - ) - ) - if not isinstance(headers, collections.Sequence): - raise TypeError( - '`headers` have to be instance of sequence. not {}'.format( - typing._type_repr(type(headers)) - ) - ) - if not isinstance(content, bytes): - raise TypeError( - '`content` have to be instance of bytes. not {}'.format( - typing._type_repr(type(content)) - ) - ) - return WsgiResponse(content, status_code, headers, **kwargs) - - -class Client: - - def __init__(self, url, opener=urllib.request.build_opener()): - self.url = url_endswith_slash(url) - self.opener = opener - - def ping(self): - req = urllib.request.Request( - urllib.parse.urljoin(self.url, './ping/'), - headers={'Content-Type': 'application/json;charset=utf-8', - 'Accepts': 'application/json'} - ) - return self.make_request(req) - - def remote_call(self, method_name, payload={}): - qs = urllib.parse.urlencode({'method': method_name}) - scheme, netloc, path, _, _ = urllib.parse.urlsplit(self.url) - request_url = urllib.parse.urlunsplit(( - scheme, netloc, path, qs, '' - )) - return self.do_request(request_url, payload) - - def make_request(self, method, request_url, headers, payload): - return ( - method, request_url, headers, json.dumps(payload).encode('utf-8') - ) - - def do_request(self, request_url, payload): - request_tuple = self.make_request( - u'POST', - request_url, - [ - ('Content-type', 'application/json;charset=utf-8'), - ('Accepts', 'application/json'), - ], - payload - ) - if not isinstance(request_tuple, collections.Sequence) and \ - len(request_tuple) == 3: - raise TypeError( - 'make_request() must return a triple of ' - '(status_code, content, headers): {}'.format(request_tuple) - ) - http_method, request_url, headers, content = request_tuple - if not isinstance(request_url, text_type): - raise TypeError( - '`request_url` have to be instance of text. not {}'.format( - typing._type_repr(type(request_url)) - ) - ) - if not isinstance(headers, collections.Sequence): - raise TypeError( - '`headers` have to be instance of sequence. not {}'.format( - typing._type_repr(type(headers)) - ) - ) - if not isinstance(content, bytes): - raise TypeError( - '`content` have to be instance of bytes. not {}'.format( - typing._type_repr(type(content)) - ) - ) - if not isinstance(http_method, text_type): - raise TypeError( - '`method` have to be instance of text. not {}'.format( - typing._type_repr(type(http_method)) - ) - ) - http_method = http_method.upper() - proper_http_method_names = { - 'GET', 'POST', 'PUT', 'DELETE', - 'OPTIONS', 'TRACE', 'CONNECT', 'HEAD' - } - if http_method not in proper_http_method_names: - raise ValueError( - '`method` have to be one of {!r}.: {}'.format( - proper_http_method_names, http_method - ) - ) - request = urllib.request.Request(request_url, data=content) - request.get_method = lambda: http_method.upper() - for header_name, header_content in headers: - request.add_header(header_name, header_content) - response = self.opener.open(request, None) - response_text = response.read() - if 200 <= response.code < 300: - return response_text.decode('utf-8') - else: - raise UnexpectedNirumResponseError(response_text) - - -# To eliminate imported vars from being overridden by -# the runtime class, aliasing runtime class into lower case with underscore -# with postfix named `_type` -service_type = Service -client_type = Client diff --git a/nirum/serialize.py b/nirum/serialize.py deleted file mode 100644 index 309c9e3..0000000 --- a/nirum/serialize.py +++ /dev/null @@ -1,84 +0,0 @@ -""":mod:`nirum.serialize` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -""" -import collections -import datetime -import decimal -import uuid - -from six import string_types - -__all__ = ( - 'serialize_boxed_type', 'serialize_meta', - 'serialize_record_type', 'serialize_unboxed_type', - 'serialize_union_type', -) - - -def serialize_unboxed_type(data): - value = data.value - serialize = getattr(value, '__nirum_serialize__', None) - if callable(serialize): - return serialize() - else: - return serialize_meta(value) - - -serialize_boxed_type = serialize_unboxed_type -# FIXME: serialize_boxed_type() is for backward compatibility; -# remove it in the near future - - -def serialize_type_with_names(data, names): - s = {} - for slot_name in data.__slots__: - value = getattr(data, slot_name) - serialized_data = serialize_meta(value) - if slot_name in names: - behind_name = names[slot_name] - else: - behind_name = slot_name - s[behind_name] = serialized_data - return s - - -def serialize_record_type(data): - s = {'_type': data.__nirum_record_behind_name__} - s.update(serialize_type_with_names(data, data.__nirum_field_names__)) - return s - - -def serialize_union_type(data): - s = { - '_type': data.__nirum_union_behind_name__, - '_tag': data.__nirum_tag__.value, - } - s.update(serialize_type_with_names(data, data.__nirum_tag_names__)) - return s - - -def serialize_meta(data): - if hasattr(data, '__nirum_serialize__'): - d = data.__nirum_serialize__() - elif isinstance(data, (string_types, bool, int, float)): - # FIXME: str in py2 represents binary string as well as text string. - # It should be refactored so that the function explicitly takes - # an expected type as like deserialize_meta() does. - d = data - elif (isinstance(data, datetime.datetime) or - isinstance(data, datetime.date)): - d = data.isoformat() - elif isinstance(data, decimal.Decimal) or isinstance(data, uuid.UUID): - d = str(data) - elif (isinstance(data, collections.Set) or - isinstance(data, collections.Sequence)): - d = [serialize_meta(e) for e in data] - elif isinstance(data, collections.Mapping): - d = [ - {'key': serialize_meta(k), 'value': serialize_meta(v)} - for k, v in data.items() - ] - else: - d = data - return d diff --git a/nirum/service.py b/nirum/service.py new file mode 100644 index 0000000..e73a266 --- /dev/null +++ b/nirum/service.py @@ -0,0 +1,43 @@ +""":mod:`nirum.service` --- Runtime base of Nirum services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +""" +import typing + +from .constructs import NameDict +from .exc import (InvalidNirumServiceMethodNameError, + InvalidNirumServiceMethodTypeError) + +__all__ = 'Service', + + +class Service(object): + """Abstract base of Nirum services. + + All service classes generated by Nirum compiler inherit this. + + """ + + __nirum_service_methods__ = {} + __nirum_method_names__ = NameDict([]) + + @staticmethod + def __nirum_method_error_types__(k, d=None): + return d + + def __init__(self): + for method_name in self.__nirum_service_methods__: + try: + method = getattr(self, method_name) + except AttributeError: + raise InvalidNirumServiceMethodNameError( + '{0}.{1}() method has to be implemented.'.format( + typing._type_repr(type(self)), method_name + ) + ) + if not callable(method): + raise InvalidNirumServiceMethodTypeError( + '{0}.{1} has to be callable so that is a method'.format( + typing._type_repr(type(self)), method_name + ) + ) diff --git a/nirum/test.py b/nirum/test.py index a61ddd5..e7011e5 100644 --- a/nirum/test.py +++ b/nirum/test.py @@ -1,3 +1,4 @@ +import email import socket from six import PY3 @@ -18,6 +19,9 @@ class MockHttpResponse(HTTPResponse): def __init__(self, body, status_code): self.body = body self.code = status_code + self.headers = email.message_from_string( + 'Content-Type: application/json\n' + ) if PY3: self.status = status_code @@ -54,7 +58,7 @@ def open(self, fullurl, data, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): scheme, host, path, qs, _ = urllib.parse.urlsplit(req.get_full_url()) assert self.scheme == scheme assert self.host == host - assert self.path == path + assert self.path == path or self.path + 'ping/' == path path_only = urllib.parse.urlunsplit(('', '', path, qs, '')) request_func = getattr(self.wsgi_test_client, req.get_method().lower()) wsgi_response = request_func( diff --git a/nirum/transport.py b/nirum/transport.py new file mode 100644 index 0000000..2f6847e --- /dev/null +++ b/nirum/transport.py @@ -0,0 +1,88 @@ +""":mod:`nirum.transport` --- Transport interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +""" +__all__ = 'Transport', + + +class Transport(object): + + def __init__(self, url): + self.url = url + + def call(self, + method_name, + payload, + service_annotations, + method_annotations, + parameter_annotations): + """Call the method of the service. + + :param method_name: A normalized behind name of the method to + call. See also `Nirum's serialization format + docs `_. + :type method_name: :class:`str` + :param payload: A mapping of parameter names to serialized argument + values. The keys have to be normalized behind names + of parameters. The values have to be serialized + argument values. See also `Nirum's serialization + format docs `_. + :type payload: :class:`~typing.Mapping`\ [:class:`str`, + :class:`~typing.Union`\ [:class:`~typing.Mapping`, + :class:`~typing.Sequence`, :class:`int`, :class:`float`, + :class:`bool`, :const:`None`]] + :param service_annotations: A mapping of annotations of the service. + The keys are normalized names of annotations. + The values are mapping objects of annotation parameter names to + argument values. + :type service_annotations: :class:`~typing.Mapping`\ [:class:`str`, + :class:`~typing.Mapping`\ [:class:`str`, :class:`str`]] + :param method_annotations: A mapping of annotations of the method. + The keys are normalized names of annotations. + The values are mapping objects of annotation parameter names to + argument values. + :type method_annotations: :class:`~typing.Mapping`\ [:class:`str`, + :class:`~typing.Mapping`\ [:class:`str`, :class:`str`]] + :param parameter_annotations: A mapping of parameter annotations. + Its structure is similar to ``service_annotations`` and + ``method_annotations`` except it's one more level nested. + The keys are normalized names of parameters. + The values are the annotation mappings of their corresponding + parameter. + :type parameter_annotations: :class:`~typing.Mapping`\ [:class:`str`, + :class:`~typing.Mapping`\ [:class:`str`, + :class:`~typing.Optional`\ [:class:`str`]]] + :return: A pair of the method result. The first arity is a + :class:`bool` value which represents whether the call + is successful, and the second arity is a serialized + return value (if it's successful) or error value + (if it's not successful). The responsibility of + deserializing the result value is caller's. + :rtype: :class:`~typing.Tuple`\ [:class:`bool`, + :class:`~typing.Union`\ [:class:`~typing.Mapping`, + :class:`~typing.Sequence`, :class:`int`, :class:`float`, + :class:`bool`, :const:`None`]] + + .. note:: + + Every transport has to implement this method. + + .. _serialization-format: \ +https://github.com/nirum-lang/nirum/blob/master/docs/serialization.md + + """ + raise NotImplementedError('Transport has to implement call() method') + + def __call__(self, + method_name, + payload, + service_annotations, + method_annotations, + parameter_annotations): + return self.call( + method_name, + payload, + service_annotations, + method_annotations, + parameter_annotations + ) diff --git a/nirum/validate.py b/nirum/validate.py deleted file mode 100644 index 02e6c4e..0000000 --- a/nirum/validate.py +++ /dev/null @@ -1,76 +0,0 @@ -""":mod:`nirum.validate` -~~~~~~~~~~~~~~~~~~~~~~~~ - -""" -import collections -import typing - -from ._compat import get_abstract_param_types, get_union_types, is_union_type - -__all__ = ( - 'validate_boxed_type', 'validate_record_type', 'validate_type', - 'validate_unboxed_type', 'validate_union_type', -) - - -def validate_type(data, type_): - instance_check = False - abstract_types = {typing.AbstractSet, typing.Sequence, typing.Mapping} - if hasattr(type_, '__origin__') and type_.__origin__ in abstract_types: - param_type = get_abstract_param_types(type_) - imp_types = { - typing.AbstractSet: collections.Set, - typing.Sequence: collections.Sequence, - typing.Mapping: collections.Mapping, - } - instance_check = isinstance(data, imp_types[type_.__origin__]) and \ - all(isinstance(item, param_type[0]) for item in data) - else: - try: - instance_check = isinstance(data, type_) - except TypeError: - if is_union_type(type_): - instance_check = any( - isinstance(data, t) for t in get_union_types(type_) - ) - else: - raise ValueError('{!r} cannot validated.'.format(type_)) - return instance_check - - -def validate_unboxed_type(unboxed, type_hint): - if not isinstance(unboxed, type_hint): - raise TypeError('{0} expected, found: {1}'.format(type_hint, - type(unboxed))) - return unboxed - - -validate_boxed_type = validate_unboxed_type -# FIXME: validate_boxed_type() is for backward compatibility; -# remove it in the near future - - -def validate_record_type(record): - for attr, type_ in record.__nirum_field_types__.items(): - data = getattr(record, attr) - if not validate_type(data, type_): - raise TypeError( - 'expect {0}.{1} to be {2}' - ', but found: {3}'.format(typing._type_repr(record.__class__), - attr, type_, type(data)) - ) - else: - return record - - -def validate_union_type(union): - for attr, type_ in union.__nirum_tag_types__.items(): - data = getattr(union, attr) - if not validate_type(data, type_): - raise TypeError( - 'expect {0}.{1} to be {2}' - ', but found: {3}'.format(typing._type_repr(union.__class__), - attr, type_, type(data)) - ) - else: - return union diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py index 2ac0653..374e540 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,19 @@ import ast +import re import sys -from setuptools import find_packages, setup, __version__ as setuptools_version +from setuptools import find_packages, setup, __version__ as setuptools_version -def readme(): +def readme(name='README.rst'): try: - with open('README.rst') as f: - return f.read() + with open(name) as f: + rst = f.read() + return re.sub( + r'(^|\n).. include::\s*([^\n]+)($|\n)', + lambda m: m.group(1) + (readme(m.group(2)) or '') + m.group(3), + rst + ) except (IOError, OSError): return @@ -25,24 +31,23 @@ def get_version(): setup_requires = [] -service_requires = [ - # FIXME Test Werkzeug 0.9, 0.10, 0.11 as well - 'Werkzeug >= 0.11, < 1.0', -] install_requires = [ 'six', 'iso8601', -] + service_requires +] tests_require = [ - 'pytest >= 2.9.0', - 'import-order', - 'flake8', - 'tox', + # flake8 does not yet support pycodestyle 2.4.0. + # Can be remove after this issue is fixed: + # https://gitlab.com/pycqa/flake8/issues/415 + 'pycodestyle >= 2.0, < 2.4.0', + 'pytest >= 3.2.3, < 4.0.0', + 'pytest-flake8 >= 0.9.1, < 1.0.0', + 'flake8-import-order >= 0.12, < 1.0', + 'flake8-import-order-spoqa >= 1.0.1, < 2.0.0', ] docs_require = [ 'Sphinx', ] extras_require = { - 'service': service_requires, 'tests': tests_require, 'docs': docs_require, } @@ -71,9 +76,10 @@ def get_version(): setup( name='nirum', version=get_version(), - description='', + description='The Nirum runtime library for Python', long_description=readme(), - url='https://github.com/spoqa/nirum-python', + url='https://github.com/nirum-lang/nirum-python', + bugtrack_url='https://github.com/nirum-lang/nirum/issues', author='Kang Hyojun', author_email='iam.kanghyojun' '@' 'gmail.com', license='MIT license', @@ -81,5 +87,18 @@ def get_version(): install_requires=install_requires, setup_requires=setup_requires, extras_require=extras_require, - classifiers=[] + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + 'Topic :: Software Development :: Code Generators', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Software Development :: Object Brokering', + ] ) diff --git a/tests/compat_test.py b/tests/compat_test.py index a3079c6..c5bc921 100644 --- a/tests/compat_test.py +++ b/tests/compat_test.py @@ -3,6 +3,7 @@ from pytest import mark from six import text_type + from nirum._compat import (get_abstract_param_types, get_tuple_param_types, get_union_types, diff --git a/tests/conftest.py b/tests/conftest.py index 8348ed0..a834193 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,11 @@ +from fixture import (A, B, C, Circle, Location, Offset, Point, + Rectangle, Shape, Token) from pytest import fixture -from .nirum_schema import import_nirum_fixture - - -nirum_fixture = import_nirum_fixture() - @fixture def fx_unboxed_type(): - return nirum_fixture.Offset + return Offset @fixture @@ -18,44 +15,45 @@ def fx_offset(fx_unboxed_type): @fixture def fx_record_type(): - return nirum_fixture.Point + return Point @fixture def fx_point(fx_record_type, fx_unboxed_type): - return fx_record_type(fx_unboxed_type(3.14), fx_unboxed_type(1.592)) + return fx_record_type(left=fx_unboxed_type(3.14), + top=fx_unboxed_type(1.592)) @fixture def fx_circle_type(): - return nirum_fixture.Circle + return Circle @fixture def fx_rectangle_type(): - return nirum_fixture.Rectangle + return Rectangle @fixture def fx_rectangle(fx_rectangle_type, fx_point): - return fx_rectangle_type(fx_point, fx_point) + return fx_rectangle_type(upper_left=fx_point, lower_right=fx_point) @fixture def fx_layered_boxed_types(): - return nirum_fixture.A, nirum_fixture.B, nirum_fixture.C + return A, B, C @fixture def fx_location_record(): - return nirum_fixture.Location + return Location @fixture def fx_shape_type(): - return nirum_fixture.Shape + return Shape @fixture def fx_token_type(): - return nirum_fixture.Token + return Token diff --git a/tests/datastructures_test.py b/tests/datastructures_test.py index 4695c81..51e7255 100644 --- a/tests/datastructures_test.py +++ b/tests/datastructures_test.py @@ -138,3 +138,18 @@ def test_list_equality(fx_record_type, fx_unboxed_type): assert hash(b) != hash(c) assert a != c assert not (a == c) + + +def test_list_immutable(): + mutable_list = [1, 2] + immutable_list = List(mutable_list) + mutable_list.append(3) + assert immutable_list.items != mutable_list + assert immutable_list.items == [1, 2] + assert mutable_list == [1, 2, 3] + + +def test_list_repr(): + assert repr(List([])) == 'nirum.datastructures.List([])' + assert repr(List([1])) == 'nirum.datastructures.List([1])' + assert repr(List([1, 2])) == 'nirum.datastructures.List([1, 2])' diff --git a/tests/deserialize_test.py b/tests/deserialize_test.py deleted file mode 100644 index 2951a3c..0000000 --- a/tests/deserialize_test.py +++ /dev/null @@ -1,348 +0,0 @@ -import collections -import datetime -import decimal -import numbers -import uuid -import typing - -from pytest import raises, mark -from six import PY3, text_type - -from nirum._compat import utc -from nirum.serialize import serialize_record_type -from nirum.deserialize import (deserialize_unboxed_type, deserialize_meta, - deserialize_tuple_type, - deserialize_record_type, deserialize_union_type, - deserialize_optional, deserialize_primitive) - - -def test_deserialize_unboxed_type(fx_unboxed_type, fx_token_type): - v = 3.14 - assert fx_unboxed_type(v) == deserialize_unboxed_type(fx_unboxed_type, v) - uuid_ = uuid.uuid4() - t = str(uuid_) - assert fx_token_type(uuid_) == deserialize_unboxed_type(fx_token_type, t) - - -def test_deserialize_record_type(fx_unboxed_type, fx_record_type): - with raises(ValueError): - deserialize_record_type(fx_record_type, {}) - - with raises(ValueError): - deserialize_record_type(fx_record_type, {'_type': 'hello'}) - - left = fx_unboxed_type(1.1) - top = fx_unboxed_type(2.2) - deserialized = deserialize_record_type( - fx_record_type, {'_type': 'point', 'x': left.value, 'top': top.value} - ) - instance = fx_record_type(left=left, top=top) - assert deserialized == instance - - -def test_deserialize_union_type(fx_circle_type, fx_rectangle_type, - fx_point, fx_shape_type): - with raises(ValueError): - deserialize_union_type(fx_circle_type, {}) - - with raises(ValueError): - deserialize_union_type(fx_circle_type, {'_type': 'shape'}) - - with raises(ValueError): - deserialize_union_type(fx_circle_type, - {'_type': 'foo', '_tag': 'circle'}) - with raises(ValueError): - deserialize_union_type(fx_rectangle_type, - {'_type': 'shape', '_tag': 'circle'}) - - with raises(ValueError): - deserialize_union_type(fx_shape_type, - {'_type': 'shape', '_tag': 'semo'}) - - deserialize = deserialize_union_type( - fx_rectangle_type, - { - '_type': 'shape', '_tag': 'rectangle', - 'upper_left': serialize_record_type(fx_point), - 'lower_right': serialize_record_type(fx_point), - } - ) - assert deserialize.upper_left == fx_point - assert deserialize.lower_right == fx_point - - -def test_deserialize_meta_error(): - with raises(TypeError): - deserialize_meta(None, {}) - - -def test_deserialize_meta_record(fx_unboxed_type, fx_record_type, fx_point): - left = fx_unboxed_type(1.1) - top = fx_unboxed_type(2.2) - d = {'_type': 'point', 'x': left.value, 'top': top.value} - meta = deserialize_meta(fx_record_type, d) - record = deserialize_record_type(fx_record_type, d) - assert meta == record - - -def test_deserialize_meta_union(fx_rectangle_type, fx_point, fx_shape_type): - d = { - '_type': 'shape', '_tag': 'rectangle', - 'upper_left': serialize_record_type(fx_point), - 'lower_right': serialize_record_type(fx_point), - } - meta = deserialize_meta(fx_rectangle_type, d) - union = deserialize_union_type( - fx_rectangle_type, d - ) - assert meta == union - meta_from_shape = deserialize_meta(fx_shape_type, d) - assert meta_from_shape == meta - - -def test_deserialize_meta_unboxed(fx_unboxed_type, fx_record_type, fx_point, - fx_token_type): - v = 3.14 - meta = deserialize_meta(fx_unboxed_type, v) - unboxed = fx_unboxed_type(v) - assert meta == unboxed - v = uuid.uuid4() - meta = deserialize_meta(fx_token_type, str(v)) - unboxed = fx_token_type(v) - assert meta == unboxed - - -def test_deserialize_multiple_boxed_type(fx_layered_boxed_types): - A, B, C = fx_layered_boxed_types - assert B.__nirum_deserialize__(u'lorem') == B(A(u'lorem')) - assert C.__nirum_deserialize__(u'x') == C(B(A(u'x'))) - with raises(ValueError): - B.__nirum_deserialize__(1) - - -@mark.parametrize( - 'data, t, expect', - [ - (1, int if PY3 else numbers.Integral, 1), - (1.1, float, 1.1), - (u'hello', text_type, 'hello'), - (True, bool, True), - ('1.1', decimal.Decimal, decimal.Decimal('1.1')), - ( - '2016-08-04T01:42:43Z', - datetime.datetime, - datetime.datetime( - 2016, 8, 4, 1, 42, 43, tzinfo=utc - ) - ), - ( - '2016-08-04', - datetime.date, - datetime.date(2016, 8, 4) - ), - ( - 'E41E8A85-2E99-4493-8192-0D3AA3D8D005', - uuid.UUID, - uuid.UUID('E41E8A85-2E99-4493-8192-0D3AA3D8D005'), - ) - ] -) -def test_deserialize_primitive(data, t, expect): - assert deserialize_primitive(t, data) == expect - - -@mark.parametrize( - 'data, t', - [ - ('a', int), - ('a', float), - ('a', decimal.Decimal), - ('a', datetime.datetime), - ('a', datetime.date), - ('a', uuid.UUID), - (1, text_type), - (1.1, text_type), - ] -) -def test_deserialize_primitive_error(data, t): - with raises(ValueError): - deserialize_primitive(t, data) - - -@mark.parametrize( - 'primitive_type, iter_, expect_iter', - [ - (text_type, [u'a', u'b'], None), - (float, [3.14, 1.592], None), - ( - decimal.Decimal, - ['3.14', '1.59'], - [decimal.Decimal('3.14'), decimal.Decimal('1.59')] - ), - ( - uuid.UUID, - [ - '862c4c1d-ece5-4d04-aa1f-485797244e14', - 'e05b4319-fca1-4637-992b-344e45be7ff9' - ], - [ - uuid.UUID('862c4c1d-ece5-4d04-aa1f-485797244e14'), - uuid.UUID('e05b4319-fca1-4637-992b-344e45be7ff9') - ], - ), - ( - datetime.datetime, - [ - '2016-08-04T01:29:16Z', - '20160804T012916Z', - ], - [ - datetime.datetime( - 2016, 8, 4, 1, 29, 16, - tzinfo=utc - ), - datetime.datetime( - 2016, 8, 4, 1, 29, 16, - tzinfo=utc - ), - ], - ), - ( - datetime.date, - [ - '2016-08-04', - '2016-08-05' - ], - [ - datetime.date(2016, 8, 4), - datetime.date(2016, 8, 5) - ], - ), - (bool, [True, False], None), - ] -) -@mark.parametrize( - 'abstract_type, python_type', - [ - (typing.Sequence, list), - (typing.List, list), - (typing.Set, set), - (typing.AbstractSet, set), - ] -) -def test_deserialize_meta_iterable( - primitive_type, iter_, abstract_type, python_type, expect_iter -): - deserialized = deserialize_meta(abstract_type[primitive_type], iter_) - if expect_iter is None: - expect_iter = iter_ - assert deserialized == python_type(expect_iter) - - -def test_deserialize_meta_map(fx_record_type, fx_unboxed_type): - map_type = typing.Mapping[fx_record_type, fx_record_type] - with raises(ValueError): - deserialize_meta(map_type, {}) - with raises(ValueError): - deserialize_meta(map_type, {'_type': 'hello'}) - empty = deserialize_meta(map_type, []) - assert isinstance(empty, collections.Mapping) - assert not isinstance(empty, collections.MutableMapping) - assert not list(empty) - with raises(ValueError): - deserialize_meta(map_type, [{}]) - with raises(ValueError): - deserialize_meta(map_type, - [{'key': {'_type': 'point', 'x': 1.0, 'top': 1.0}}]) - with raises(ValueError): - deserialize_meta(map_type, - [{'value': {'_type': 'point', 'x': 1.0, 'top': 1.0}}]) - with raises(ValueError): - deserialize_meta(map_type, [ - {'key': {'_type': 'point', 'x': 1.0, 'top': 1.0}, 'value': 'V'} - ]) - with raises(ValueError): - deserialize_meta(map_type, [ - {'key': 'K', 'value': {'_type': 'point', 'x': 1.0, 'top': 1.0}} - ]) - map_ = deserialize_meta(map_type, [ - { - 'key': {'_type': 'point', 'x': 1.0, 'top': 2.0}, - 'value': {'_type': 'point', 'x': 3.0, 'top': 4.0}, - }, - ]) - assert isinstance(map_, collections.Mapping) - assert not isinstance(map_, collections.MutableMapping) - assert list(map_.items()) == [ - ( - fx_record_type( - left=fx_unboxed_type(1.0), - top=fx_unboxed_type(2.0) - ), - fx_record_type( - left=fx_unboxed_type(3.0), - top=fx_unboxed_type(4.0) - ), - ), - ] - map2 = deserialize_meta(map_type, [ - { - 'key': {'_type': 'point', 'x': 1.0, 'top': 2.0}, - 'value': {'_type': 'point', 'x': 3.0, 'top': 4.0}, - }, - { - 'key': {'_type': 'point', 'x': 5.0, 'top': 6.0}, - 'value': {'_type': 'point', 'x': 7.0, 'top': 8.0}, - }, - ]) - assert sorted(map2.items(), key=lambda item: item[0].left.value) == [ - ( - fx_record_type( - left=fx_unboxed_type(1.0), - top=fx_unboxed_type(2.0) - ), - fx_record_type( - left=fx_unboxed_type(3.0), - top=fx_unboxed_type(4.0) - ), - ), - ( - fx_record_type( - left=fx_unboxed_type(5.0), - top=fx_unboxed_type(6.0) - ), - fx_record_type( - left=fx_unboxed_type(7.0), - top=fx_unboxed_type(8.0) - ), - ), - ] - - -def test_deserialize_tuple(): - assert deserialize_tuple_type(typing.Tuple, (1, 2)) == (1, 2) - assert deserialize_tuple_type( - typing.Tuple[text_type, int], (u'a', 1) - ) == (u'a', 1) - with raises(ValueError): - deserialize_tuple_type(typing.Tuple[text_type], (u'a', 1)) - - with raises(ValueError): - deserialize_tuple_type(typing.Tuple[text_type, text_type], (u'a')) - - with raises(ValueError): - deserialize_tuple_type(typing.Tuple[text_type, text_type], (u'a', 1)) - - -def test_deserialize_optional(fx_record_type): - assert deserialize_optional(typing.Optional[text_type], u'abc') == u'abc' - assert deserialize_optional(typing.Optional[text_type], None) is None - assert deserialize_optional(typing.Optional[fx_record_type], None) is None - with raises(ValueError): - deserialize_optional(typing.Union[text_type, int], u'text_type') - with raises(ValueError): - deserialize_optional(typing.Union[text_type, int], 1) - with raises(ValueError): - deserialize_optional(typing.Union[text_type, int], None) - with raises(ValueError): - deserialize_optional(typing.Optional[text_type], 1) diff --git a/tests/nirum_schema.py b/tests/nirum_schema.py deleted file mode 100644 index 299f168..0000000 --- a/tests/nirum_schema.py +++ /dev/null @@ -1,20 +0,0 @@ -from six import PY2, PY3 - - -def import_nirum_fixture(): - if PY2: - nirum_fixture_name = 'tests.py2_nirum' - elif PY3: - nirum_fixture_name = 'tests.py3_nirum' - else: - raise ImportError() - return __import__( - nirum_fixture_name, - globals(), - locals(), - [ - 'A', 'B', 'C', 'Circle', 'Location', 'MusicService', - 'MusicServiceClient', 'Offset', 'Point', 'Rectangle', 'Shape', - 'Token', 'ComplexKeyMap', - ] - ) diff --git a/tests/py2_nirum.py b/tests/py2_nirum.py deleted file mode 100644 index e57947a..0000000 --- a/tests/py2_nirum.py +++ /dev/null @@ -1,448 +0,0 @@ -import enum -import typing -import decimal -import json -import uuid - -from six import text_type - -from nirum.serialize import (serialize_record_type, serialize_unboxed_type, - serialize_meta) -from nirum.deserialize import (deserialize_record_type, - deserialize_unboxed_type, - deserialize_meta) -from nirum.validate import (validate_unboxed_type, validate_record_type, - validate_union_type) -from nirum.constructs import NameDict, name_dict_type -from nirum.rpc import Client, Service - - -class Offset(object): - - __nirum_inner_type__ = float - - def __init__(self, value): - self.value = value - - def __eq__(self, other): - return (isinstance(other, Offset) and self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __hash__(self): # noqa - return hash((self.__class__, self.value)) - - -class Point(object): - - __slots__ = ( - 'left', - 'top' - ) - __nirum_record_behind_name__ = 'point' - __nirum_field_types__ = { - 'left': Offset, - 'top': Offset - } - __nirum_field_names__ = NameDict([ - ('left', 'x') - ]) - - def __init__(self, left, top): - self.left = left - self.top = top - validate_record_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Point) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __nirum_serialize__(self): - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls, values): - return deserialize_record_type(cls, values) - - def __hash__(self): - return hash((self.__class__, self.left, self.top)) - - -class Shape(object): - - __nirum_union_behind_name__ = 'shape' - __nirum_field_names__ = NameDict([ - ]) - - class Tag(enum.Enum): - rectangle = 'rectangle' - circle = 'circle' - - def __init__(self, *args, **kwargs): - raise NotImplementedError( - "{0.__module__}.{0.__qualname__} cannot be instantiated " - "since it is an abstract class. Instantiate a concrete subtype " - "of it instead.".format( - type(self) - ) - ) - - def __nirum_serialize__(self): - pass - - @classmethod - def __nirum_deserialize__(cls, value): - pass - - -class Rectangle(Shape): - - __slots__ = ( - 'upper_left', - 'lower_right' - ) - __nirum_tag__ = Shape.Tag.rectangle - __nirum_tag_types__ = { - 'upper_left': Point, - 'lower_right': Point - } - __nirum_tag_names__ = NameDict([]) - - def __init__(self, upper_left, lower_right): - self.upper_left = upper_left - self.lower_right = lower_right - validate_union_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Rectangle) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - -class Circle(Shape): - - __slots__ = ( - 'origin', - 'radius' - ) - __nirum_tag__ = Shape.Tag.circle - __nirum_tag_types__ = { - 'origin': Point, - 'radius': Offset - } - __nirum_tag_names__ = NameDict([]) - - def __init__(self, origin, radius): - self.origin = origin - self.radius = radius - validate_union_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Circle) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - -class Location(object): - # TODO: docstring - - __slots__ = ( - 'name', - 'lat', - 'lng', - ) - __nirum_record_behind_name__ = 'location' - __nirum_field_types__ = { - 'name': typing.Optional[text_type], - 'lat': decimal.Decimal, - 'lng': decimal.Decimal - } - __nirum_field_names__ = name_dict_type([ - ('name', 'name'), - ('lat', 'lat'), - ('lng', 'lng') - ]) - - def __init__(self, name, lat, lng): - self.name = name - self.lat = lat - self.lng = lng - validate_record_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Location) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __nirum_serialize__(self): - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_record_type(cls, value) - - -class A(object): - - __nirum_inner_type__ = text_type - - def __init__(self, value): - validate_unboxed_type(value, text_type) - self.value = value # type: Text - - def __eq__(self, other): - return (isinstance(other, A) and - self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class B(object): - - __nirum_inner_type__ = A - - def __init__(self, value): - validate_unboxed_type(value, A) - self.value = value # type: A - - def __eq__(self, other): - return (isinstance(other, B) and - self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class C(object): - - __nirum_inner_type__ = B - - def __init__(self, value): - validate_unboxed_type(value, B) - self.value = value # type: B - - def __eq__(self, other): - return (isinstance(other, C) and - self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class MusicService(Service): - - __nirum_service_methods__ = { - 'get_music_by_artist_name': { - 'artist_name': text_type, - '_return': typing.Sequence[text_type], - '_names': NameDict([ - ('artist_name', 'artist_name') - ]) - }, - 'incorrect_return': { - '_return': text_type, - '_names': NameDict([]) - }, - 'get_artist_by_music': { - 'music': text_type, - '_return': text_type, - '_names': NameDict([('music', 'norae')]) - }, - 'raise_application_error_request': { - '_return': text_type, - '_names': NameDict([]) - }, - } - __nirum_method_names__ = NameDict([ - ('get_music_by_artist_name', 'get_music_by_artist_name'), - ('incorrect_return', 'incorrect_return'), - ('get_artist_by_music', 'find_artist'), - ('raise_application_error_request', 'raise_application_error_request'), - ]) - - def get_music_by_artist_name(self, artist_name): - raise NotImplementedError('get_music_by_artist_name') - - def incorrect_return(self): - raise NotImplementedError('incorrect_return') - - def get_artist_by_music(self, music): - raise NotImplementedError('get_artist_by_music') - - def raise_application_error_request(self): - raise NotImplementedError('raise_application_error_request') - - -class MusicServiceClient(Client, MusicService): - - def get_music_by_artist_name(self, artist_name): - meta = self.__nirum_service_methods__['get_music_by_artist_name'] - payload = {meta['_names']['artist_name']: serialize_meta(artist_name)} - return deserialize_meta( - meta['_return'], - json.loads( - self.remote_call( - self.__nirum_method_names__['get_music_by_artist_name'], - payload=payload - ) - ) - ) - - def get_artist_by_music(self, music): - meta = self.__nirum_service_methods__['get_artist_by_music'] - payload = {meta['_names']['music']: serialize_meta(music)} - return deserialize_meta( - meta['_return'], - json.loads( - self.remote_call( - self.__nirum_method_names__['get_artist_by_music'], - payload=payload - ) - ) - ) - - -class Token: - - __nirum_inner_type__ = uuid.UUID - - def __init__(self, value): - validate_unboxed_type(value, uuid.UUID) - self.value = value - - def __eq__(self, other): - return (isinstance(other, Token) and self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - -class ComplexKeyMap(object): - - __slots__ = ( - 'value', - ) - __nirum_record_behind_name__ = ( - 'complex_key_map' - ) - __nirum_field_types__ = { - 'value': typing.Mapping[Point, Point] - } - __nirum_field_names__ = name_dict_type([ - ('value', 'value') - ]) - - def __init__(self, value): - self.value = value - validate_record_type(self) - - def __repr__(self): - return '{0}({1})'.format( - (type(self).__module__ + '.' + type(self).__name__), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, ComplexKeyMap) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __ne__(self, other): - return not self == other - - def __nirum_serialize__(self): - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_record_type(cls, value) - - def __hash__(self): - return hash((self.value,)) diff --git a/tests/py3_nirum.py b/tests/py3_nirum.py deleted file mode 100644 index e445e12..0000000 --- a/tests/py3_nirum.py +++ /dev/null @@ -1,452 +0,0 @@ -import enum -import typing -import decimal -import json -import uuid - -from nirum.serialize import (serialize_record_type, serialize_unboxed_type, - serialize_meta) -from nirum.deserialize import (deserialize_record_type, - deserialize_unboxed_type, - deserialize_meta) -from nirum.validate import (validate_unboxed_type, validate_record_type, - validate_union_type) -from nirum.constructs import NameDict, name_dict_type -from nirum.rpc import Client, Service - - -class Offset: - - __nirum_inner_type__ = float - - def __init__(self, value): - self.value = value - - def __eq__(self, other): - return (isinstance(other, Offset) and self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __hash__(self): # noqa - return hash((self.__class__, self.value)) - - -class Point: - - __slots__ = ( - 'left', - 'top' - ) - __nirum_record_behind_name__ = 'point' - __nirum_field_types__ = { - 'left': Offset, - 'top': Offset - } - __nirum_field_names__ = NameDict([ - ('left', 'x') - ]) - - def __init__(self, left, top): - self.left = left - self.top = top - validate_record_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Point) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __nirum_serialize__(self): - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls, values): - return deserialize_record_type(cls, values) - - def __hash__(self): - return hash((self.__class__, self.left, self.top)) - - -class Shape: - - __nirum_union_behind_name__ = 'shape' - __nirum_field_names__ = NameDict([ - ]) - - class Tag(enum.Enum): - rectangle = 'rectangle' - circle = 'circle' - - def __init__(self, *args, **kwargs): - raise NotImplementedError( - "{0.__module__}.{0.__qualname__} cannot be instantiated " - "since it is an abstract class. Instantiate a concrete subtype " - "of it instead.".format( - type(self) - ) - ) - - def __nirum_serialize__(self): - pass - - @classmethod - def __nirum_deserialize__(cls, value): - pass - - -class Rectangle(Shape): - - __slots__ = ( - 'upper_left', - 'lower_right' - ) - __nirum_tag__ = Shape.Tag.rectangle - __nirum_tag_types__ = { - 'upper_left': Point, - 'lower_right': Point - } - __nirum_tag_names__ = NameDict([]) - - def __init__(self, upper_left, lower_right): - self.upper_left = upper_left - self.lower_right = lower_right - validate_union_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Rectangle) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - -class Circle(Shape): - - __slots__ = ( - 'origin', - 'radius' - ) - __nirum_tag__ = Shape.Tag.circle - __nirum_tag_types__ = { - 'origin': Point, - 'radius': Offset - } - __nirum_tag_names__ = NameDict([]) - - def __init__(self, origin, radius): - self.origin = origin - self.radius = radius - validate_union_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Circle) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - -class Location: - # TODO: docstring - - __slots__ = ( - 'name', - 'lat', - 'lng', - ) - __nirum_record_behind_name__ = 'location' - __nirum_field_types__ = { - 'name': typing.Optional[str], - 'lat': decimal.Decimal, - 'lng': decimal.Decimal - } - __nirum_field_names__ = name_dict_type([ - ('name', 'name'), - ('lat', 'lat'), - ('lng', 'lng') - ]) - - def __init__(self, name, lat, lng): - self.name = name - self.lat = lat - self.lng = lng - validate_record_type(self) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other): - return isinstance(other, Location) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __nirum_serialize__(self): - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_record_type(cls, value) - - -class A: - - __nirum_inner_type__ = str - - def __init__(self, value): - validate_unboxed_type(value, str) - self.value = value # type: Text - - def __eq__(self, other): - return (isinstance(other, A) and - self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class B: - - __nirum_inner_type__ = A - - def __init__(self, value): - validate_unboxed_type(value, A) - self.value = value # type: A - - def __eq__(self, other): - return (isinstance(other, B) and - self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class C: - - __nirum_inner_type__ = B - - def __init__(self, value): - validate_unboxed_type(value, B) - self.value = value # type: B - - def __eq__(self, other): - return (isinstance(other, C) and - self.value == other.value) - - def __hash__(self): - return hash(self.value) - - def __nirum_serialize__(self): - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__(cls, value): - return deserialize_unboxed_type(cls, value) - - def __repr__(self): - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class MusicService(Service): - - __nirum_service_methods__ = { - 'get_music_by_artist_name': { - 'artist_name': str, - '_return': typing.Sequence[str], - '_names': NameDict([ - ('artist_name', 'artist_name') - ]) - }, - 'incorrect_return': { - '_return': str, - '_names': NameDict([]) - }, - 'get_artist_by_music': { - 'music': str, - '_return': str, - '_names': NameDict([('music', 'norae')]) - }, - 'raise_application_error_request': { - '_return': str, - '_names': NameDict([]) - }, - } - __nirum_method_names__ = NameDict([ - ('get_music_by_artist_name', 'get_music_by_artist_name'), - ('incorrect_return', 'incorrect_return'), - ('get_artist_by_music', 'find_artist'), - ('raise_application_error_request', 'raise_application_error_request'), - ]) - - def get_music_by_artist_name(self, artist_name): - raise NotImplementedError('get_music_by_artist_name') - - def incorrect_return(self): - raise NotImplementedError('incorrect_return') - - def get_artist_by_music(self, music): - raise NotImplementedError('get_artist_by_music') - - def raise_application_error_request(self): - raise NotImplementedError('raise_application_error_request') - - -class MusicServiceClient(Client, MusicService): - - def get_music_by_artist_name(self, artist_name): - meta = self.__nirum_service_methods__['get_music_by_artist_name'] - payload = {meta['_names']['artist_name']: serialize_meta(artist_name)} - return deserialize_meta( - meta['_return'], - json.loads( - self.remote_call( - self.__nirum_method_names__['get_music_by_artist_name'], - payload=payload - ) - ) - ) - - def get_artist_by_music(self, music): - meta = self.__nirum_service_methods__['get_artist_by_music'] - payload = {meta['_names']['music']: serialize_meta(music)} - return deserialize_meta( - meta['_return'], - json.loads( - self.remote_call( - self.__nirum_method_names__['get_artist_by_music'], - payload=payload - ) - ) - ) - - -class Token: - - __nirum_inner_type__ = uuid.UUID - - def __init__(self, value: uuid.UUID) -> None: - validate_unboxed_type(value, uuid.UUID) - self.value = value - - def __eq__(self, other) -> bool: - return (isinstance(other, Token) and self.value == other.value) - - def __hash__(self) -> int: - return hash(self.value) - - def __nirum_serialize__(self) -> typing.Mapping[str, typing.Any]: - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__( - cls: type, value: typing.Mapping[str, typing.Any] - ) -> 'Token': - return deserialize_unboxed_type(cls, value) - - def __hash__(self) -> int: # noqa - return hash((self.__class__, self.value)) - - -class ComplexKeyMap(object): - # TODO: docstring - - __slots__ = ( - 'value', - ) - __nirum_record_behind_name__ = ( - 'complex_key_map' - ) - __nirum_field_types__ = { - 'value': typing.Mapping[Point, Point] - } - __nirum_field_names__ = name_dict_type([ - ('value', 'value') - ]) - - def __init__(self, value: typing.Mapping[Point, Point]) -> None: - self.value = value - validate_record_type(self) - - def __repr__(self) -> bool: - return '{0}({1})'.format( - typing._type_repr(type(self)), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other) -> bool: - return isinstance(other, ComplexKeyMap) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __ne__(self, other) -> bool: - return not self == other - - def __nirum_serialize__(self) -> typing.Mapping[str, typing.Any]: - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls: type, value) -> 'ComplexKeyMap': - return deserialize_record_type(cls, value) - - def __hash__(self) -> int: - return hash((self.value,)) diff --git a/tests/rpc_test.py b/tests/rpc_test.py deleted file mode 100644 index a90cdff..0000000 --- a/tests/rpc_test.py +++ /dev/null @@ -1,327 +0,0 @@ -import json - -from pytest import fixture, raises, mark -from six import text_type -from werkzeug.test import Client as TestClient -from werkzeug.wrappers import Response - -from nirum.exc import (InvalidNirumServiceMethodTypeError, - InvalidNirumServiceMethodNameError) -from nirum.rpc import Client, WsgiApp -from nirum.test import MockOpener - -from .nirum_schema import import_nirum_fixture - - -nf = import_nirum_fixture() - - -class MusicServiceImpl(nf.MusicService): - - music_map = { - u'damien rice': [u'9 crimes', u'Elephant'], - u'ed sheeran': [u'Thinking out loud', u'Photograph'], - } - - def get_music_by_artist_name(self, artist_name): - return self.music_map.get(artist_name) - - def incorrect_return(self): - return 1 - - def get_artist_by_music(self, music): - for k, v in self.music_map.items(): - if music in v: - return k - return u'none' - - def raise_application_error_request(self): - raise ValueError('hello world') - - -class MusicServiceNameErrorImpl(nf.MusicService): - - __nirum_service_methods__ = { - 'foo': {} - } - - -class MusicServiceTypeErrorImpl(nf.MusicService): - - get_music_by_artist_name = 1 - - -@mark.parametrize('impl, error_class', [ - (MusicServiceNameErrorImpl, InvalidNirumServiceMethodNameError), - (MusicServiceTypeErrorImpl, InvalidNirumServiceMethodTypeError), -]) -def test_service(impl, error_class): - with raises(error_class): - impl() - - -@fixture -def fx_music_wsgi(): - return WsgiApp(MusicServiceImpl()) - - -@fixture -def fx_test_client(fx_music_wsgi): - return TestClient(fx_music_wsgi, Response) - - -def test_wsgi_app_ping(fx_music_wsgi, fx_test_client): - assert fx_music_wsgi.service - response = fx_test_client.get('/ping/') - data = json.loads(response.get_data(as_text=True)) - assert 'Ok' == data - - -def assert_response(response, status_code, expect_json): - assert response.status_code == status_code, response.get_data(as_text=True) - actual_response_json = json.loads( - response.get_data(as_text=True) - ) - assert actual_response_json == expect_json - - -def test_rpc_internal_error(fx_test_client): - response = fx_test_client.post('/?method=raise_application_error_request') - assert response.status_code == 500, response.get_data(as_text=True) - actual_response_json = json.loads( - response.get_data(as_text=True) - ) - expected_json = { - '_type': 'error', - '_tag': 'internal_server_error', - 'message': 'hello world' - } - assert actual_response_json == expected_json - - -def test_wsgi_app_error(fx_test_client): - # method not allowed - assert_response( - fx_test_client.get('/?method=get_music_by_artist_name'), - 405, - { - '_type': 'error', - '_tag': 'method_not_allowed', - 'message': 'The requested URL / was not allowed HTTP method GET.' - - } - ) - # method missing - assert_response( - fx_test_client.post('/'), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "A query string parameter method= is missing." - - } - ) - # invalid procedure name - assert_response( - fx_test_client.post('/?method=foo'), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "Service dosen't have procedure named 'foo'." - - } - ) - # invalid json - assert_response( - fx_test_client.post( - '/?method=get_music_by_artist_name', data="!", - content_type='application/json' - ), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "Invalid JSON payload: '!'." - - } - ) - # incorrect return - assert_response( - fx_test_client.post('/?method=incorrect_return'), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "Incorrect return type 'int' for 'incorrect_return'. " - "expected '{}'.".format(text_type.__name__) - } - ) - - -def test_procedure_bad_request(fx_test_client): - assert_response( - fx_test_client.post('/?method=get_music_by_artist_name'), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "A argument named 'artist_name' is missing, " - "it is required.", - } - ) - payload = { - 'artist_name': 1 - } - assert_response( - fx_test_client.post( - '/?method=get_music_by_artist_name', - data=json.dumps(payload), - content_type='application/json' - ), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "Incorrect type 'int' for 'artist_name'. " - "expected '{}'.".format(text_type.__name__) - } - ) - - -@mark.parametrize( - 'payload, expected_json', - [ - ({'artist_name': u'damien rice'}, [u'9 crimes', u'Elephant']), - ( - {'artist_name': u'ed sheeran'}, - [u'Thinking out loud', u'Photograph'] - ), - ] -) -def test_wsgi_app_method(fx_test_client, payload, expected_json): - response = fx_test_client.post( - '/?method=get_music_by_artist_name', - data=json.dumps(payload), - content_type='application/json' - ) - data = json.loads(response.get_data(as_text=True)) - assert data == expected_json - - -def test_wsgi_app_http_error(fx_test_client): - response = fx_test_client.post('/foobar') - assert response.status_code == 404 - response_json = json.loads(response.get_data(as_text=True)) - assert response_json == { - '_type': 'error', - '_tag': 'not_found', - 'message': 'The requested URL /foobar was not found on this service.', - } - - -def test_wsgi_app_with_behind_name(fx_test_client): - payload = {'norae': u'9 crimes'} - assert_response( - fx_test_client.post( - '/?method=get_artist_by_music', - data=json.dumps(payload), - content_type='application/json' - ), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "Service dosen't have procedure named " - "'get_artist_by_music'." - - } - ) - assert_response( - fx_test_client.post( - '/?method=find_artist', - data=json.dumps({'music': '9 crimes'}), - content_type='application/json' - ), - 400, - { - '_type': 'error', - '_tag': 'bad_request', - 'message': "A argument named 'norae' is missing, " - "it is required.", - } - ) - assert_response( - fx_test_client.post( - '/?method=find_artist', - data=json.dumps(payload), - content_type='application/json' - ), - 200, - u'damien rice' - ) - - -@mark.parametrize('url, expected_url', [ - (u'http://foobar.com', u'http://foobar.com/'), - (u'http://foobar.com/', u'http://foobar.com/'), - (u'http://foobar.com?a=1#a', u'http://foobar.com/'), - (u'http://foobar.com/?a=1#a', u'http://foobar.com/'), -]) -def test_rpc_client(url, expected_url): - assert Client(url).url == expected_url - - -@mark.parametrize('url', ['adfoj', 'http://']) -def test_rpc_client_error(url): - with raises(ValueError): - Client(url) - - -@mark.parametrize('url', [u'http://foobar.com/', u'http://foobar.com/rpc/']) -def test_rpc_client_service(monkeypatch, url): - client = nf.MusicServiceClient(url, MockOpener(url, MusicServiceImpl)) - nine_crimes = '9 crimes' - damien_music = [nine_crimes, 'Elephant'] - damien_rice = 'damien rice' - assert client.get_music_by_artist_name(damien_rice) == damien_music - assert client.get_artist_by_music(nine_crimes) == damien_rice - - -def test_rpc_mock_opener_null_app(monkeypatch): - url = u'http://foobar.com/rpc/' - client = nf.MusicServiceClient(url, MockOpener(url, MusicServiceImpl)) - response = client.opener.wsgi_test_client.post('/') - assert response.status_code == 404 - - -@mark.parametrize('method_name', ['POST', 'post']) -def test_rpc_client_make_request(method_name, monkeypatch): - naver = u'http://naver.com' - payload = {'hello': 'world'} - client = nf.MusicServiceClient(naver, MockOpener(naver, MusicServiceImpl)) - actual_method, request_url, header, actual_payload = client.make_request( - method_name, - naver, - { - 'Content-type': 'application/json;charset=utf-8', - 'Accepts': 'application/json' - }, - payload - ) - assert actual_method == method_name - assert request_url == naver - assert payload == json.loads(actual_payload.decode('utf-8')) - assert header == {'Content-type': 'application/json;charset=utf-8', - 'Accepts': 'application/json'} - with raises(ValueError): - request_url, header, actual_payload = client.make_request( - u'FOO', - naver, - { - 'Content-type': 'application/json;charset=utf-8', - 'Accepts': 'application/json' - }, - payload - ) diff --git a/tests/schema-fixture/fixture.nrm b/tests/schema-fixture/fixture.nrm new file mode 100644 index 0000000..8b25979 --- /dev/null +++ b/tests/schema-fixture/fixture.nrm @@ -0,0 +1,37 @@ +unboxed offset (float64); + +record point ( + offset left/x, + offset top, +); + +union shape + # Type constructors in a sum type become translated to subtypes in OO + # languages, and datatypes in functional languages. + = rectangle (point upper-left, point lower-right) + | circle (point origin, offset radius) + ; + +record location ( + text? name, + decimal lat, + decimal lng +); + +unboxed a (text); +unboxed b (a); +unboxed c (b); + +@error +union hello-error = unknown | bad-request; + +service music-service ( + [text] get-music-by-artist-name (text artist-name) throws hello-error, + text incorrect-return (), + text get-artist-by-music/find-artist (text music/norae), + text raise-application-error-request (), +); + +unboxed token (uuid); + +record complex-key-map ({point: point} value); diff --git a/tests/schema-fixture/package.toml b/tests/schema-fixture/package.toml new file mode 100644 index 0000000..20f4adb --- /dev/null +++ b/tests/schema-fixture/package.toml @@ -0,0 +1,5 @@ +version = "0.6.0" + +[targets.python] +name = "nirum-schema-fixture" +minimum_runtime = "0.6.0" diff --git a/tests/serialize_test.py b/tests/serialize_test.py deleted file mode 100644 index f4381f0..0000000 --- a/tests/serialize_test.py +++ /dev/null @@ -1,141 +0,0 @@ -import datetime -import decimal -import uuid - -from pytest import mark - -from nirum._compat import utc -from nirum.datastructures import List -from nirum.serialize import (serialize_unboxed_type, serialize_record_type, - serialize_meta, serialize_union_type) -from .nirum_schema import import_nirum_fixture - - -nirum_fixture = import_nirum_fixture() - - -def test_serialize_unboxed_type(fx_offset, fx_token_type): - assert serialize_unboxed_type(fx_offset) == fx_offset.value - token = uuid.uuid4() - assert serialize_unboxed_type(fx_token_type(token)) == str(token) - - -def test_serialize_layered_boxed_type(fx_layered_boxed_types): - actual = fx_layered_boxed_types[1](fx_layered_boxed_types[0](u'test')) - assert actual.__nirum_serialize__() == u'test' - - -def test_serialize_record_type(fx_point): - assert serialize_record_type(fx_point) == {'_type': 'point', 'x': 3.14, - 'top': 1.592} - - -def test_serialize_union_type(fx_point, fx_offset, fx_circle_type, - fx_rectangle_type): - circle = fx_circle_type(fx_point, fx_offset) - s = { - '_type': 'shape', '_tag': 'circle', - 'origin': serialize_record_type(fx_point), - 'radius': serialize_unboxed_type(fx_offset) - } - assert serialize_union_type(circle) == s - rectangle = fx_rectangle_type(fx_point, fx_point) - s = { - '_type': 'shape', '_tag': 'rectangle', - 'upper_left': serialize_record_type(fx_point), - 'lower_right': serialize_record_type(fx_point), - } - assert serialize_union_type(rectangle) == s - - -def test_multiple_boxed_type(fx_layered_boxed_types): - A, B, _ = fx_layered_boxed_types - assert B(A(u'hello')).value.value == u'hello' - assert B(A(u'lorem')).__nirum_serialize__() == u'lorem' - - -@mark.parametrize( - 'd, expect', - [ - (1, 1), - (1.1, 1.1), - (True, True), - ( - uuid.UUID('7471A1F2-442E-4991-B6E8-77C6BD286785'), - '7471a1f2-442e-4991-b6e8-77c6bd286785' - ), - (decimal.Decimal('3.14'), '3.14'), - ( - datetime.datetime(2016, 8, 5, 3, 46, 37, - tzinfo=utc), - '2016-08-05T03:46:37+00:00' - ), - ( - datetime.date(2016, 8, 5), - '2016-08-05' - ), - ] -) -def test_serialize_meta(d, expect): - assert serialize_meta(d) == expect - - -@mark.parametrize( - 'd, expect', [ - ({1, 2, 3}, [1, 2, 3]), - ( - {datetime.date(2016, 8, 5), datetime.date(2016, 8, 6)}, - ['2016-08-05', '2016-08-06'] - ), - ] -) -def test_serialize_meta_set(d, expect): - serialized = serialize_meta(d) - for e in expect: - e in serialized - - -def test_serialize_meta_list(fx_record_type, fx_unboxed_type, fx_offset): - record = fx_record_type(fx_offset, fx_offset) - record2 = fx_record_type(fx_unboxed_type(1.1), fx_unboxed_type(1.2)) - serialize_result = serialize_meta([record, record2]) - assert serialize_result == [ - {'_type': 'point', 'x': 1.2, 'top': 1.2}, - {'_type': 'point', 'x': 1.1, 'top': 1.2}, - ] - assert serialize_meta(List([record, record2])) == serialize_result - - -def test_serialize_meta_set_of_record(fx_record_type, fx_unboxed_type, - fx_offset): - record = fx_record_type(fx_offset, fx_offset) - record2 = fx_record_type(fx_unboxed_type(1.1), fx_unboxed_type(1.2)) - serialize_result = serialize_meta({record, record2}) - assert record.__nirum_serialize__() in serialize_result - assert record2.__nirum_serialize__() in serialize_result - assert (sorted(serialize_meta(frozenset([record, record2])), key=repr) == - sorted(serialize_result, key=repr)) - - -def test_serialize_meta_map(fx_point): - Point = nirum_fixture.Point - Offset = nirum_fixture.Offset - record = nirum_fixture.ComplexKeyMap(value={ - fx_point: Point(left=Offset(1.23), top=Offset(4.56)), - Point(left=Offset(1.23), top=Offset(4.56)): - Point(left=Offset(7.89), top=Offset(10.11)), - }) - result = serialize_meta(record) - assert sorted(result) == sorted({ - '_type': 'complex_key_map', - 'value': [ - { - 'key': {'_type': 'point', 'x': 3.14, 'top': 1.592}, - 'value': {'_type': 'point', 'x': 1.23, 'top': 4.56}, - }, - { - 'key': {'_type': 'point', 'x': 1.23, 'top': 4.56}, - 'value': {'_type': 'point', 'x': 7.89, 'top': 10.11}, - }, - ], - }) diff --git a/tests/service_test.py b/tests/service_test.py new file mode 100644 index 0000000..eecd11d --- /dev/null +++ b/tests/service_test.py @@ -0,0 +1,26 @@ +from fixture import MusicService +from pytest import mark, raises + +from nirum.exc import (InvalidNirumServiceMethodNameError, + InvalidNirumServiceMethodTypeError) + + +class MusicServiceNameErrorImpl(MusicService): + + __nirum_service_methods__ = { + 'foo': {'_v': 2} + } + + +class MusicServiceTypeErrorImpl(MusicService): + + get_music_by_artist_name = 1 + + +@mark.parametrize('impl, error_class', [ + (MusicServiceNameErrorImpl, InvalidNirumServiceMethodNameError), + (MusicServiceTypeErrorImpl, InvalidNirumServiceMethodTypeError), +]) +def test_service(impl, error_class): + with raises(error_class): + impl() diff --git a/tests/validate_test.py b/tests/validate_test.py deleted file mode 100644 index b15063b..0000000 --- a/tests/validate_test.py +++ /dev/null @@ -1,65 +0,0 @@ -import decimal -import typing - -from pytest import raises -from six import text_type - -from nirum.datastructures import List -from nirum.validate import (validate_unboxed_type, validate_record_type, - validate_union_type, validate_type) - - -def test_validate_unboxed_type(): - assert validate_unboxed_type(3.14, float) - with raises(TypeError): - validate_unboxed_type(u'hello', float) - - -def test_validate_record_type(fx_point, fx_record_type, fx_offset, - fx_location_record): - assert validate_record_type(fx_point) - with raises(TypeError): - validate_record_type(fx_record_type(left=fx_offset, top=1)) - with raises(TypeError): - validate_record_type(fx_record_type(left=1, top=fx_offset)) - assert validate_record_type( - fx_location_record(name=None, lat=decimal.Decimal('3.14'), - lng=decimal.Decimal('1.592')) - ) - - -def test_validate_union_type(fx_rectangle, fx_rectangle_type, fx_point): - assert validate_union_type(fx_rectangle) - with raises(TypeError): - validate_union_type(fx_rectangle_type(1, fx_point)) - - with raises(TypeError): - validate_union_type(fx_rectangle_type(fx_point, 1)) - - with raises(TypeError): - validate_union_type(fx_rectangle_type(1, 1)) - - -def test_validate_layered_boxed_types(fx_layered_boxed_types): - A, B, C = fx_layered_boxed_types - assert validate_unboxed_type(u'test', text_type) - assert validate_unboxed_type(A(u'test'), A) - assert validate_unboxed_type(B(A(u'test')), B) - with raises(TypeError): - assert validate_unboxed_type(u'test', A) - - with raises(TypeError): - assert validate_unboxed_type(u'test', B) - - with raises(TypeError): - assert validate_unboxed_type(A(u'test'), B) - - -def test_validate_abstract_set(): - assert validate_type({1, 2, 3}, typing.AbstractSet[int]) - assert validate_type(frozenset([1, 2, 3]), typing.AbstractSet[int]) - - -def test_validate_list(): - assert validate_type([1, 2, 3], typing.Sequence[int]) - assert validate_type(List([1, 2, 3]), typing.Sequence[int]) diff --git a/tox.ini b/tox.ini index 9e34570..7d515a9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,39 @@ [tox] -envlist = py27,py34,py35 +envlist = buildfixture,{py27,py34,py35,py36}-{typing351,typing352},docs [testenv] -deps = -e.[tests] +deps = + -e.[tests] + typing351: typing<3.5.2 + typing352: typing>=3.5.2 commands= - py.test -v tests + pip install -e {env:FIXTURE_PATH:{distdir}/schema_fixture/} + pytest -v + +[testenv:buildfixture] +skipinstall = true +deps = +passenv = * +whitelist_externals = + {env:NIRUM:nirum} +commands = + {env:NIRUM:nirum} -o {env:FIXTURE_PATH:{distdir}/schema_fixture/} -t python tests/schema-fixture/ + + +[testenv:docs] +basepython = python3 +deps = + docutils + Pygments +commands = + rst2html.py --strict CHANGES.rst + rst2html.py --strict README.rst + python3 setup.py --long-description | rst2html.py --strict + +[pytest] +addopts = --ff --flake8 + +[flake8] +exclude = .env, .tox, tests/py2_nirum.py, tests/py3_nirum.py +import-order-style = spoqa +application-import-names = nirum, tests