From 2cbbc7df5de01f5b26d4d7ad9845fcf209ec4f3d Mon Sep 17 00:00:00 2001 From: Kang Hyojun Date: Sat, 22 Oct 2016 18:37:35 +0900 Subject: [PATCH] Python27 support (#36) * Simple py2 * py27 basic support * py27 support * Ignore import order when python 27 * tox * rebase * Use typing._type_repr() * Use .get_full_url() always * Run with python27 on travis-ci * In py27, have to inherit object * Add py34 in tox --- .gitignore | 1 + .travis.yml | 1 + lint.sh | 5 +- nirum/_compat.py | 24 +++ nirum/deserialize.py | 40 ++-- nirum/exc.py | 4 +- nirum/func.py | 4 +- nirum/rpc.py | 78 +++----- nirum/test.py | 21 +- nirum/validate.py | 10 +- setup.py | 9 +- tests/__init__.py | 0 tests/conftest.py | 355 ++-------------------------------- tests/deserialize_test.py | 51 +++-- tests/nirum_schema.py | 20 ++ tests/py2_nirum.py | 394 +++++++++++++++++++++++++++++++++++++ tests/py3_nirum.py | 397 ++++++++++++++++++++++++++++++++++++++ tests/rpc_test.py | 118 +++-------- tests/serialize_test.py | 17 +- tests/validate_test.py | 19 +- tox.ini | 7 + 21 files changed, 1004 insertions(+), 571 deletions(-) create mode 100644 nirum/_compat.py create mode 100644 tests/__init__.py create mode 100644 tests/nirum_schema.py create mode 100644 tests/py2_nirum.py create mode 100644 tests/py3_nirum.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 6db252a..45e80c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .cache *.egg-info +.tox diff --git a/.travis.yml b/.travis.yml index 4bc955d..68afb7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python python: + - 2.7 - 3.4 - 3.5 install: diff --git a/lint.sh b/lint.sh index 40d106a..181fa0c 100755 --- a/lint.sh +++ b/lint.sh @@ -2,4 +2,7 @@ set -e flake8 . -import-order nirum ./nirum + +if [[ "$(python -c "import sys;print(sys.version[0])")" != "2" ]]; then + import-order nirum ./nirum +fi diff --git a/nirum/_compat.py b/nirum/_compat.py new file mode 100644 index 0000000..d4de1b4 --- /dev/null +++ b/nirum/_compat.py @@ -0,0 +1,24 @@ +import datetime + +__all__ = 'utc', + + +try: + utc = datetime.timezone.utc +except AttributeError: + ZERO = datetime.timedelta(0) + HOUR = datetime.timedelta(hours=1) + + class UTC(datetime.tzinfo): + """UTC""" + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + + utc = UTC() diff --git a/nirum/deserialize.py b/nirum/deserialize.py index c15f84c..03968df 100644 --- a/nirum/deserialize.py +++ b/nirum/deserialize.py @@ -9,6 +9,7 @@ import uuid from iso8601 import iso8601, parse_date +from six import text_type __all__ = ( 'deserialize_abstract_type', @@ -24,8 +25,8 @@ 'is_support_abstract_type', ) _NIRUM_PRIMITIVE_TYPE = { - str, float, decimal.Decimal, uuid.UUID, datetime.datetime, - datetime.date, bool, int + float, decimal.Decimal, uuid.UUID, datetime.datetime, + datetime.date, bool, int, text_type } @@ -126,13 +127,13 @@ def deserialize_primitive(cls, data): d = cls(data) except decimal.InvalidOperation: raise ValueError("'{}' is not a decimal.".format(data)) - elif cls is str: - if not isinstance(data, str): + 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.__qualname__}' is not a primitive type.".format(cls) + "'{0}' is not a primitive type.".format(typing._type_repr(cls)) ) return d @@ -207,9 +208,13 @@ 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.__class__.__name__} expect "_type" equal to' - ' "{0.__nirum_record_behind_name__}"' - ', but found {1}.'.format(cls, 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(): @@ -235,19 +240,20 @@ def deserialize_union_type(cls, value): break else: raise ValueError( - '{0!r} is not deserialzable tag of ' - '`{1.__name__}`.'.format( - value, cls + '{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.__class__.__name__} expect "_type" equal to' - ' "{0.__nirum_union_behind_name__}"' - ', but found {1}.'.format(cls, 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.__class__.__name__} expect "_tag" equal to' - ' "{0.__nirum_tag__.value}"' - ', but found {1}.'.format(cls, 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(): diff --git a/nirum/exc.py b/nirum/exc.py index 5b66a2a..2ba572a 100644 --- a/nirum/exc.py +++ b/nirum/exc.py @@ -2,7 +2,7 @@ ~~~~~~~~~~~~~~~~~~ """ -import urllib.error +from six.moves import urllib __all__ = ( 'InvalidNirumServiceMethodNameError', @@ -48,7 +48,7 @@ class NirumHttpError(urllib.error.HTTPError, NirumServiceError): class NirumUrlError(urllib.error.URLError, NirumServiceError): """TODO""" - def __init__(self, exc: urllib.error.URLError): + def __init__(self, exc): self.text = exc.read() super().__init__(exc.reason) diff --git a/nirum/func.py b/nirum/func.py index d826806..bcb7a44 100644 --- a/nirum/func.py +++ b/nirum/func.py @@ -1,9 +1,9 @@ -import urllib.parse +from six.moves import urllib __all__ = 'url_endswith_slash', -def url_endswith_slash(url: str) -> str: +def url_endswith_slash(url): scheme, netloc, path, _, _ = urllib.parse.urlsplit(url) if not (scheme and netloc): raise ValueError("{} isn't URL.".format(url)) diff --git a/nirum/rpc.py b/nirum/rpc.py index 2bf7ebb..4fde906 100644 --- a/nirum/rpc.py +++ b/nirum/rpc.py @@ -4,9 +4,8 @@ """ import json import typing -import urllib.parse -import urllib.request +from six.moves import urllib from werkzeug.exceptions import HTTPException from werkzeug.http import HTTP_STATUS_CODES from werkzeug.routing import Map, Rule @@ -42,14 +41,14 @@ def __init__(self): method = getattr(self, method_name) except AttributeError: raise InvalidNirumServiceMethodNameError( - "{0.__class__.__qualname__}.{1} inexist.".format( - self, method_name + "{0}.{1} inexist.".format( + typing._type_repr(self.__class__), method_name ) ) if not callable(method): raise InvalidNirumServiceMethodTypeError( - "{0.__class__.__qualname__}.{1} isn't callable".format( - self, method_name + "{0}.{1} isn't callable".format( + typing._type_repr(self.__class__), method_name ) ) @@ -67,11 +66,10 @@ class WsgiApp: Rule('/ping/', endpoint='ping'), ]) - def __init__(self, service: Service): + def __init__(self, service): self.service = service - def __call__(self, environ: typing.Mapping[str, object], - start_response: typing.Callable) -> WsgiResponse: + def __call__(self, environ, start_response): """ :param environ: @@ -80,8 +78,7 @@ def __call__(self, environ: typing.Mapping[str, object], """ return self.route(environ, start_response) - def route(self, environ: typing.Mapping[str, object], - start_response: typing.Callable) -> WsgiResponse: + def route(self, environ, start_response): """Route :param environ: @@ -99,14 +96,14 @@ def route(self, environ: typing.Mapping[str, object], response = procedure(request, args) return response(environ, start_response) - def ping(self, request: WsgiRequest, args: typing.Any) -> WsgiResponse: + def ping(self, request, args): return WsgiResponse( '"Ok"', 200, content_type='application/json' ) - def rpc(self, request: WsgiRequest, args: typing.Any) -> WsgiResponse: + def rpc(self, request, args): """RPC :param request: @@ -145,8 +142,7 @@ def rpc(self, request: WsgiRequest, args: typing.Any) -> WsgiResponse: ) if not callable(service_method): return self.error( - 400, - request, + 400, request, message="Remote procedure '{}' is not callable.".format( request_method ) @@ -173,19 +169,17 @@ def rpc(self, request: WsgiRequest, args: typing.Any) -> WsgiResponse: return self.error( 400, request, - message="Incorrect return type '{0.__class__.__qualname__}' " - "for '{1}'. expected '{2.__qualname__}'.".format( - result, request_method, type_hints['_return'] + 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._json_response(200, serialize_meta(result)) - def _parse_procedure_arguments( - self, - type_hints: typing.Mapping[str, typing.Union[type, NameDict]], - request_json: JSONType - ) -> typing.Mapping[str, typing.Union[str, float, int, bool, object]]: + def _parse_procedure_arguments(self, type_hints, request_json): arguments = {} name_map = type_hints['_names'] for argument_name, type_ in type_hints.items(): @@ -204,17 +198,15 @@ def _parse_procedure_arguments( arguments[argument_name] = deserialize_meta(type_, data) except ValueError: raise NirumProcedureArgumentValueError( - "Incorrect type '{0.__class__.__name__}' for '{1}'. " - "expected '{2.__name__}'.".format( - data, behind_name, type_ + "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: type, procedure_result: typing.Any - ) -> bool: + def _check_return_type(self, type_hint, procedure_result): try: deserialize_meta(type_hint, serialize_meta(procedure_result)) except ValueError: @@ -222,9 +214,7 @@ def _check_return_type( else: return True - def _make_error_response( - self, error_type: str, message: typing.Optional[str]=None - ) -> typing.Mapping[str, str]: + def _make_error_response(self, error_type, message=None): """Create error response json temporary. .. code-block:: nirum @@ -242,10 +232,7 @@ def _make_error_response( 'message': message, } - def error( - self, status_code: int, request: WsgiRequest, - *, message: typing.Optional[str]=None - ) -> WsgiResponse: + def error(self, status_code, request, message=None): """Handle error response. :param int status_code: @@ -278,9 +265,7 @@ def error( ) ) - def _json_response( - self, status_code: int, response_json: JSONType, **kwargs - ) -> WsgiResponse: + def _json_response(self, status_code, response_json, **kwargs): return WsgiResponse( json.dumps(response_json), status_code, @@ -291,10 +276,7 @@ def _json_response( class Client: - def __init__( - self, url: str, - opener: urllib.request.OpenerDirector=urllib.request.build_opener() - ): + def __init__(self, url, opener=urllib.request.build_opener()): self.url = url_endswith_slash(url) self.opener = opener @@ -306,9 +288,7 @@ def ping(self): ) return self.make_request(req) - def remote_call(self, - method_name: str, - payload: JSONType={}) -> JSONType: + 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(( @@ -321,9 +301,9 @@ def remote_call(self, ) return self.make_request(req) - def make_request(self, request: urllib.request.Request) -> JSONType: + def make_request(self, request): try: - response = self.opener.open(request) + response = self.opener.open(request, None) except urllib.error.URLError as e: raise NirumUrlError(e) except urllib.error.HTTPError as e: diff --git a/nirum/test.py b/nirum/test.py index d0f2252..9c77982 100644 --- a/nirum/test.py +++ b/nirum/test.py @@ -1,9 +1,7 @@ -import http.client import socket -import typing -import urllib.parse -import urllib.request +from six.moves import urllib +from six.moves.http_client import HTTPResponse from werkzeug.test import Client from werkzeug.wrappers import Response @@ -13,13 +11,13 @@ __all__ = 'MockHttpResponse', 'MockOpener' -class MockHttpResponse(http.client.HTTPResponse): +class MockHttpResponse(HTTPResponse): - def __init__(self, body: str, status_code: int): + def __init__(self, body, status_code): self.body = body self.status = status_code - def read(self) -> bytes: + def read(self): return self.body.encode('utf-8') @@ -33,17 +31,12 @@ def __init__(self, url, service_impl_cls): ) self.wsgi_test_client = Client(self.wsgi_app, Response) - def open( - self, - fullurl: typing.Union[str, urllib.request.Request], - data: typing.Optional[bytes]=None, - timeout: int=socket._GLOBAL_DEFAULT_TIMEOUT - ) -> MockHttpResponse: + def open(self, fullurl, data, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): if isinstance(fullurl, str): req = urllib.request.Request(fullurl, data=data) else: req = fullurl - scheme, host, path, qs, _ = urllib.parse.urlsplit(req.full_url) + scheme, host, path, qs, _ = urllib.parse.urlsplit(req.get_full_url()) assert self.scheme == scheme assert self.host == host assert self.path == path diff --git a/nirum/validate.py b/nirum/validate.py index 742c71e..05d4d48 100644 --- a/nirum/validate.py +++ b/nirum/validate.py @@ -41,8 +41,9 @@ def validate_record_type(record): data = getattr(record, attr) if not validate_type(data, type_): raise TypeError( - 'expect {0.__class__.__name__}.{1} to be {2}' - ', but found: {3}'.format(record, attr, type_, type(data)) + 'expect {0}.{1} to be {2}' + ', but found: {3}'.format(typing._type_repr(record.__class__), + attr, type_, type(data)) ) else: return record @@ -53,8 +54,9 @@ def validate_union_type(union): data = getattr(union, attr) if not validate_type(data, type_): raise TypeError( - 'expect {0.__class__.__name__}.{1} to be {2}' - ', but found: {3}'.format(union, attr, type_, type(data)) + '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.py b/setup.py index 6978c29..394bf00 100644 --- a/setup.py +++ b/setup.py @@ -30,12 +30,13 @@ def get_version(): 'Werkzeug >= 0.11, < 0.12', ] install_requires = [ - 'iso8601', + 'six', 'iso8601', ] + service_requires tests_require = [ 'pytest >= 2.9.0', 'import-order', 'flake8', + 'tox', ] docs_require = [ 'Sphinx', @@ -48,6 +49,9 @@ def get_version(): below35_requires = [ 'typing', ] +below34_requires = [ + 'enum34', +] if 'bdist_wheel' not in sys.argv and sys.version_info < (3, 5): @@ -57,8 +61,11 @@ def get_version(): if tuple(map(int, setuptools_version.split('.'))) < (17, 1): setup_requires = ['setuptools >= 17.1'] extras_require.update({":python_version=='3.4'": below35_requires}) + extras_require.update({":python_version=='2.7'": below35_requires}) + extras_require.update({":python_version=='2.7'": below34_requires}) else: extras_require.update({":python_version<'3.5'": below35_requires}) + extras_require.update({":python_version<'3.4'": below34_requires}) setup( diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index d3ec62b..8348ed0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,257 +1,14 @@ -import enum -import decimal -import typing -import uuid - from pytest import fixture -from nirum.serialize import serialize_record_type, serialize_unboxed_type -from nirum.deserialize import deserialize_record_type, deserialize_unboxed_type -from nirum.validate import (validate_unboxed_type, validate_record_type, - validate_union_type) -from nirum.constructs import NameDict, name_dict_type - - -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 Offset: - - __nirum_inner_type__ = float - - def __init__(self, value: float) -> None: - validate_unboxed_type(value, float) - self.value = value - - def __eq__(self, other) -> bool: - return (isinstance(other, Offset) 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] - ) -> 'Offset': - return deserialize_unboxed_type(cls, value) - - def __hash__(self) -> int: # 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: Offset, top: Offset) -> None: - self.left = left - self.top = top - validate_record_type(self) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other) -> bool: - return isinstance(other, Point) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __nirum_serialize__(self) -> typing.Mapping[str, typing.Any]: - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls: type, values) -> 'Point': - return deserialize_record_type(cls, values) - - def __hash__(self) -> int: - 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) -> typing.Mapping[str, typing.Any]: - pass - - @classmethod - def __nirum_deserialize__(cls: type, value) -> 'Shape': - 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: Point, lower_right: Point) -> None: - self.upper_left = upper_left - self.lower_right = lower_right - validate_union_type(self) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) +from .nirum_schema import import_nirum_fixture - def __eq__(self, other) -> bool: - 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: Point, radius: Offset) -> None: - self.origin = origin - self.radius = radius - validate_union_type(self) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other) -> bool: - 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: typing.Optional[str], - lat: decimal.Decimal, lng: decimal.Decimal) -> None: - self.name = name - self.lat = lat - self.lng = lng - validate_record_type(self) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1})'.format( - type(self), - ', '.join('{}={}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) - ) - - def __eq__(self, other) -> bool: - return isinstance(other, Location) and all( - getattr(self, attr) == getattr(other, attr) - for attr in self.__slots__ - ) - - def __nirum_serialize__(self) -> typing.Mapping[str, typing.Any]: - return serialize_record_type(self) - - @classmethod - def __nirum_deserialize__(cls: type, value) -> 'Location': - return deserialize_record_type(cls, value) +nirum_fixture = import_nirum_fixture() @fixture def fx_unboxed_type(): - return Offset + return nirum_fixture.Offset @fixture @@ -261,7 +18,7 @@ def fx_offset(fx_unboxed_type): @fixture def fx_record_type(): - return Point + return nirum_fixture.Point @fixture @@ -271,12 +28,12 @@ def fx_point(fx_record_type, fx_unboxed_type): @fixture def fx_circle_type(): - return Circle + return nirum_fixture.Circle @fixture def fx_rectangle_type(): - return Rectangle + return nirum_fixture.Rectangle @fixture @@ -284,111 +41,21 @@ def fx_rectangle(fx_rectangle_type, fx_point): return fx_rectangle_type(fx_point, fx_point) -class A: - - __nirum_inner_type__ = str - - def __init__(self, value: str) -> None: - validate_unboxed_type(value, str) - self.value = value # type: Text - - def __eq__(self, other) -> bool: - return (isinstance(other, A) and - self.value == other.value) - - def __hash__(self) -> int: - return hash(self.value) - - def __nirum_serialize__(self) -> str: - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__( - cls: type, value: typing.Mapping[str, typing.Any] - ) -> 'A': - return deserialize_unboxed_type(cls, value) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class B: - - __nirum_inner_type__ = A - - def __init__(self, value: A) -> None: - validate_unboxed_type(value, A) - self.value = value # type: A - - def __eq__(self, other) -> bool: - return (isinstance(other, B) and - self.value == other.value) - - def __hash__(self) -> int: - return hash(self.value) - - def __nirum_serialize__(self) -> str: - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__( - cls: type, value: typing.Mapping[str, typing.Any] - ) -> 'B': - return deserialize_unboxed_type(cls, value) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - -class C: - - __nirum_inner_type__ = B - - def __init__(self, value: B) -> None: - validate_unboxed_type(value, B) - self.value = value # type: B - - def __eq__(self, other) -> bool: - return (isinstance(other, C) and - self.value == other.value) - - def __hash__(self) -> int: - return hash(self.value) - - def __nirum_serialize__(self) -> str: - return serialize_unboxed_type(self) - - @classmethod - def __nirum_deserialize__( - cls: type, value: typing.Mapping[str, typing.Any] - ) -> 'C': - return deserialize_unboxed_type(cls, value) - - def __repr__(self) -> str: - return '{0.__module__}.{0.__qualname__}({1!r})'.format( - type(self), self.value - ) - - @fixture -def fx_layered_unboxed_types(): - return A, B, C +def fx_layered_boxed_types(): + return nirum_fixture.A, nirum_fixture.B, nirum_fixture.C @fixture def fx_location_record(): - return Location + return nirum_fixture.Location @fixture def fx_shape_type(): - return Shape + return nirum_fixture.Shape @fixture def fx_token_type(): - return Token + return nirum_fixture.Token diff --git a/tests/deserialize_test.py b/tests/deserialize_test.py index 59d8e08..6108ae2 100644 --- a/tests/deserialize_test.py +++ b/tests/deserialize_test.py @@ -4,7 +4,9 @@ import typing from pytest import raises, mark +from six import 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, @@ -108,10 +110,10 @@ def test_deserialize_meta_unboxed(fx_unboxed_type, fx_record_type, fx_point, assert meta == unboxed -def test_deserialize_multiple_unboxed_type(fx_layered_unboxed_types): - A, B, C = fx_layered_unboxed_types - assert B.__nirum_deserialize__('lorem') == B(A('lorem')) - assert C.__nirum_deserialize__('x') == C(B(A('x'))) +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) @@ -121,14 +123,14 @@ def test_deserialize_multiple_unboxed_type(fx_layered_unboxed_types): [ (1, int, 1), (1.1, float, 1.1), - ('hello', str, 'hello'), + (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=datetime.timezone.utc + 2016, 8, 4, 1, 42, 43, tzinfo=utc ) ), ( @@ -156,8 +158,8 @@ def test_deserialize_primitive(data, t, expect): ('a', datetime.datetime), ('a', datetime.date), ('a', uuid.UUID), - (1, str), - (1.1, str), + (1, text_type), + (1.1, text_type), ] ) def test_deserialize_primitive_error(data, t): @@ -168,7 +170,7 @@ def test_deserialize_primitive_error(data, t): @mark.parametrize( 'primitive_type, iter_, expect_iter', [ - (str, ['a', 'b'], None), + (text_type, [u'a', u'b'], None), (float, [3.14, 1.592], None), ( decimal.Decimal, @@ -190,21 +192,16 @@ def test_deserialize_primitive_error(data, t): datetime.datetime, [ '2016-08-04T01:29:16Z', - '2016-08-05T01:00:01+09:00', '20160804T012916Z', ], [ datetime.datetime( 2016, 8, 4, 1, 29, 16, - tzinfo=datetime.timezone.utc - ), - datetime.datetime( - 2016, 8, 5, 1, 0, 1, - tzinfo=datetime.timezone(datetime.timedelta(hours=9)) + tzinfo=utc ), datetime.datetime( 2016, 8, 4, 1, 29, 16, - tzinfo=datetime.timezone.utc + tzinfo=utc ), ], ), @@ -242,26 +239,28 @@ def test_deserialize_meta_iterable( def test_deserialize_tuple(): assert deserialize_tuple_type(typing.Tuple, (1, 2)) == (1, 2) - assert deserialize_tuple_type(typing.Tuple[str, int], ('a', 1)) == ('a', 1) + assert deserialize_tuple_type( + typing.Tuple[text_type, int], (u'a', 1) + ) == (u'a', 1) with raises(ValueError): - deserialize_tuple_type(typing.Tuple[str], ('a', 1)) + deserialize_tuple_type(typing.Tuple[text_type], (u'a', 1)) with raises(ValueError): - deserialize_tuple_type(typing.Tuple[str, str], ('a')) + deserialize_tuple_type(typing.Tuple[text_type, text_type], (u'a')) with raises(ValueError): - deserialize_tuple_type(typing.Tuple[str, str], ('a', 1)) + deserialize_tuple_type(typing.Tuple[text_type, text_type], (u'a', 1)) def test_deserialize_optional(fx_record_type): - assert deserialize_optional(typing.Optional[str], 'abc') == 'abc' - assert deserialize_optional(typing.Optional[str], None) is None + 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[str, int], 'str') + deserialize_optional(typing.Union[text_type, int], u'text_type') with raises(ValueError): - deserialize_optional(typing.Union[str, int], 1) + deserialize_optional(typing.Union[text_type, int], 1) with raises(ValueError): - deserialize_optional(typing.Union[str, int], None) + deserialize_optional(typing.Union[text_type, int], None) with raises(ValueError): - deserialize_optional(typing.Optional[str], 1) + deserialize_optional(typing.Optional[text_type], 1) diff --git a/tests/nirum_schema.py b/tests/nirum_schema.py new file mode 100644 index 0000000..d06edc9 --- /dev/null +++ b/tests/nirum_schema.py @@ -0,0 +1,20 @@ +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', + ] + ) diff --git a/tests/py2_nirum.py b/tests/py2_nirum.py new file mode 100644 index 0000000..7cacc40 --- /dev/null +++ b/tests/py2_nirum.py @@ -0,0 +1,394 @@ +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')]) + } + } + __nirum_method_names__ = NameDict([ + ('get_music_by_artist_name', 'get_music_by_artist_name'), + ('incorrect_return', 'incorrect_return'), + ('get_artist_by_music', 'find_artist'), + ]) + + 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') + + +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) diff --git a/tests/py3_nirum.py b/tests/py3_nirum.py new file mode 100644 index 0000000..e8a7809 --- /dev/null +++ b/tests/py3_nirum.py @@ -0,0 +1,397 @@ +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')]) + } + } + __nirum_method_names__ = NameDict([ + ('get_music_by_artist_name', 'get_music_by_artist_name'), + ('incorrect_return', 'incorrect_return'), + ('get_artist_by_music', 'find_artist'), + ]) + + 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') + + +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)) diff --git a/tests/rpc_test.py b/tests/rpc_test.py index ebf590e..4df0508 100644 --- a/tests/rpc_test.py +++ b/tests/rpc_test.py @@ -1,126 +1,53 @@ import json -import typing 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.constructs import NameDict from nirum.exc import (InvalidNirumServiceMethodTypeError, InvalidNirumServiceMethodNameError) -from nirum.deserialize import deserialize_meta -from nirum.serialize import serialize_meta -from nirum.rpc import Client, Service, WsgiApp +from nirum.rpc import Client, WsgiApp from nirum.test import MockOpener +from .nirum_schema import import_nirum_fixture -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')]) - } - } - __nirum_method_names__ = NameDict([ - ('get_music_by_artist_name', 'get_music_by_artist_name'), - ('incorrect_return', 'incorrect_return'), - ('get_artist_by_music', 'find_artist'), - ]) - - def get_music_by_artist_name( - self, - artist_name: str - ) -> typing.Sequence[str]: - raise NotImplementedError('get_music_by_artist_name') - - def incorrect_return(self) -> str: - raise NotImplementedError('incorrect_return') +nf = import_nirum_fixture() - def get_artist_by_music(self, music: str) -> str: - raise NotImplementedError('get_artist_by_music') - -class MusicServiceImpl(MusicService): +class MusicServiceImpl(nf.MusicService): music_map = { - 'damien rice': ['9 crimes', 'Elephant'], - 'ed sheeran': ['Thinking out loud', 'Photograph'], + 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: str - ) -> typing.Sequence[str]: + def get_music_by_artist_name(self, artist_name): return self.music_map.get(artist_name) - def incorrect_return(self) -> str: + def incorrect_return(self): return 1 - def get_artist_by_music(self, music: str) -> str: + def get_artist_by_music(self, music): for k, v in self.music_map.items(): if music in v: return k - return 'none' + return u'none' -class MusicServiceNameErrorImpl(MusicService): +class MusicServiceNameErrorImpl(nf.MusicService): __nirum_service_methods__ = { 'foo': {} } -class MusicServiceTypeErrorImpl(MusicService): +class MusicServiceTypeErrorImpl(nf.MusicService): get_music_by_artist_name = 1 -class MusicServiceClient(Client, MusicService): - - def get_music_by_artist_name( - self, artist_name: str - ) -> typing.Sequence[str]: - 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: str - ) -> typing.Sequence[str]: - 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 - ) - ) - ) - - @mark.parametrize('impl, error_class', [ (MusicServiceNameErrorImpl, InvalidNirumServiceMethodNameError), (MusicServiceTypeErrorImpl, InvalidNirumServiceMethodTypeError), @@ -148,7 +75,7 @@ def test_wsgi_app_ping(fx_music_wsgi, fx_test_client): def assert_response(response, status_code, expect_json): - assert response.status_code == status_code + assert response.status_code == status_code, response.get_data(as_text=True) actual_response_json = json.loads( response.get_data(as_text=True) ) @@ -211,7 +138,7 @@ def test_wsgi_app_error(fx_test_client): '_type': 'error', '_tag': 'bad_request', 'message': "Incorrect return type 'int' for 'incorrect_return'. " - "expected 'str'." + "expected '{}'.".format(text_type.__name__) } ) @@ -241,7 +168,7 @@ def test_procedure_bad_request(fx_test_client): '_type': 'error', '_tag': 'bad_request', 'message': "Incorrect type 'int' for 'artist_name'. " - "expected 'str'." + "expected '{}'.".format(text_type.__name__) } ) @@ -249,8 +176,11 @@ def test_procedure_bad_request(fx_test_client): @mark.parametrize( 'payload, expected_json', [ - ({'artist_name': 'damien rice'}, ['9 crimes', 'Elephant']), - ({'artist_name': 'ed sheeran'}, ['Thinking out loud', 'Photograph']), + ({'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): @@ -275,7 +205,7 @@ def test_wsgi_app_http_error(fx_test_client): def test_wsgi_app_with_behind_name(fx_test_client): - payload = {'norae': '9 crimes'} + payload = {'norae': u'9 crimes'} assert_response( fx_test_client.post( '/?method=get_artist_by_music', @@ -312,7 +242,7 @@ def test_wsgi_app_with_behind_name(fx_test_client): content_type='application/json' ), 200, - 'damien rice' + u'damien rice' ) @@ -334,7 +264,7 @@ def test_rpc_client_error(url): def test_rpc_client_service(monkeypatch): url = 'http://foobar.com/' - client = MusicServiceClient(url, MockOpener(url, MusicServiceImpl)) + client = nf.MusicServiceClient(url, MockOpener(url, MusicServiceImpl)) nine_crimes = '9 crimes' damien_music = [nine_crimes, 'Elephant'] damien_rice = 'damien rice' diff --git a/tests/serialize_test.py b/tests/serialize_test.py index 4f3b1cf..ba35cca 100644 --- a/tests/serialize_test.py +++ b/tests/serialize_test.py @@ -4,6 +4,7 @@ from pytest import mark +from nirum._compat import utc from nirum.serialize import (serialize_unboxed_type, serialize_record_type, serialize_meta, serialize_union_type) @@ -14,9 +15,9 @@ def test_serialize_unboxed_type(fx_offset, fx_token_type): assert serialize_unboxed_type(fx_token_type(token)) == str(token) -def test_serialize_layered_unboxed_type(fx_layered_unboxed_types): - actual = fx_layered_unboxed_types[1](fx_layered_unboxed_types[0]('test')) - assert actual.__nirum_serialize__() == 'test' +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): @@ -42,10 +43,10 @@ def test_serialize_union_type(fx_point, fx_offset, fx_circle_type, assert serialize_union_type(rectangle) == s -def test_multiple_unboxed_type(fx_layered_unboxed_types): - A, B, _ = fx_layered_unboxed_types - assert B(A('hello')).value.value == 'hello' - assert B(A('lorem')).__nirum_serialize__() == 'lorem' +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( @@ -61,7 +62,7 @@ def test_multiple_unboxed_type(fx_layered_unboxed_types): (decimal.Decimal('3.14'), '3.14'), ( datetime.datetime(2016, 8, 5, 3, 46, 37, - tzinfo=datetime.timezone.utc), + tzinfo=utc), '2016-08-05T03:46:37+00:00' ), ( diff --git a/tests/validate_test.py b/tests/validate_test.py index 9782cd1..64165b2 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -1,6 +1,7 @@ import decimal from pytest import raises +from six import text_type from nirum.validate import (validate_unboxed_type, validate_record_type, validate_union_type) @@ -9,7 +10,7 @@ def test_validate_unboxed_type(): assert validate_unboxed_type(3.14, float) with raises(TypeError): - validate_unboxed_type('hello', float) + validate_unboxed_type(u'hello', float) def test_validate_record_type(fx_point, fx_record_type, fx_offset, @@ -37,16 +38,16 @@ def test_validate_union_type(fx_rectangle, fx_rectangle_type, fx_point): validate_union_type(fx_rectangle_type(1, 1)) -def test_validate_layered_unboxed_types(fx_layered_unboxed_types): - A, B, C = fx_layered_unboxed_types - assert validate_unboxed_type('test', str) - assert validate_unboxed_type(A('test'), A) - assert validate_unboxed_type(B(A('test')), B) +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('test', A) + assert validate_unboxed_type(u'test', A) with raises(TypeError): - assert validate_unboxed_type('test', B) + assert validate_unboxed_type(u'test', B) with raises(TypeError): - assert validate_unboxed_type(A('test'), B) + assert validate_unboxed_type(A(u'test'), B) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9e34570 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py27,py34,py35 + +[testenv] +deps = -e.[tests] +commands= + py.test -v tests