From cba09372a24743c7ba59e227fc2be333849696c8 Mon Sep 17 00:00:00 2001 From: ccp_zeulix Date: Mon, 15 Apr 2024 14:46:23 +0000 Subject: [PATCH] Version 5.0.0-beta.1 - Going live - Migrated from internal repo --- .github/workflows/publish-to-pypi.yml | 29 + .gitignore | 17 + CHANGELOG.md | 159 +++ LICENSE | 2 +- README.md | 63 +- protoplasm/__init__.py | 5 + protoplasm/bases/__init__.py | 3 + protoplasm/bases/_client.py | 61 ++ protoplasm/bases/_exwrap.py | 113 ++ protoplasm/bases/_methwrap.py | 220 ++++ protoplasm/bases/_remotewrap.py | 261 +++++ protoplasm/bases/_servicer.py | 46 + protoplasm/bases/dataclass_bases.py | 2 + protoplasm/bases/grpc_bases.py | 3 + protoplasm/casting/__init__.py | 4 + protoplasm/casting/casters.py | 80 ++ protoplasm/casting/castutils.py | 384 +++++++ protoplasm/casting/dictator.py | 74 ++ protoplasm/casting/dictators/__init__.py | 311 ++++++ protoplasm/casting/objectifier.py | 85 ++ protoplasm/decorators/__init__.py | 1 + protoplasm/decorators/apihelpers.py | 100 ++ protoplasm/errors/__init__.py | 4 + protoplasm/errors/_api.py | 238 +++++ protoplasm/errors/_base.py | 24 + protoplasm/grpcserver.py | 53 + protoplasm/loader/__init__.py | 17 + protoplasm/plasm/__init__.py | 7 + protoplasm/structs/__init__.py | 8 + protoplasm/structs/_base.py | 4 + protoplasm/structs/_ctx/__init__.py | 2 + protoplasm/structs/_ctx/_basectx.py | 15 + protoplasm/structs/_ctx/_grpcctx.py | 53 + protoplasm/structs/_methodtype.py | 33 + protoplasm/structs/_reqestsiter.py | 32 + protoplasm/structs/_responseiter.py | 172 +++ protoplasm/structs/dataclassbase.py | 139 +++ pyproject.toml | 50 + requirements-unittesting.txt | 1 + requirements.txt | 6 + setup.py | 6 + tests/__init__.py | 0 tests/_sandbox/__init__.py | 0 tests/_sandbox/_plasm_stream_client.py | 55 + tests/_sandbox/_plasm_stream_server.py | 89 ++ tests/_sandbox/_raw_stream_client.py | 131 +++ tests/_sandbox/_raw_stream_service.py | 99 ++ tests/_sandbox/_sandboxclient.py | 9 + tests/plain/__init__.py | 0 tests/plain/test_apihelpers.py | 997 ++++++++++++++++++ tests/plain/test_base64stuff.py | 29 + tests/res/build/empty.txt | 1 + tests/res/proto/sandbox/test/alpha.proto | 9 + tests/res/proto/sandbox/test/anytest.proto | 12 + tests/res/proto/sandbox/test/beta.proto | 12 + tests/res/proto/sandbox/test/clones.proto | 31 + tests/res/proto/sandbox/test/delta.proto | 12 + tests/res/proto/sandbox/test/enums.proto | 64 ++ .../proto/sandbox/test/illnamedfields.proto | 42 + .../proto/sandbox/test/illnamedservice.proto | 18 + .../proto/sandbox/test/interfaceonly.proto | 21 + tests/res/proto/sandbox/test/nested.proto | 18 + tests/res/proto/sandbox/test/rainbow.proto | 115 ++ tests/res/proto/sandbox/test/river.proto | 42 + tests/res/proto/sandbox/test/service.proto | 80 ++ .../test/service_with_imported_io.proto | 25 + .../sandbox/test/service_with_oneof.proto | 20 + tests/res/proto/sandbox/test/test.proto | 80 ++ .../res/proto/sandbox/test/timeduration.proto | 12 + .../unittesting/unary/unaryservice.proto | 58 + tests/servicetestutils/__init__.py | 0 tests/servicetestutils/plasm_server.py | 79 ++ tests/servicetestutils/raw_client.py | 91 ++ tests/servicetestutils/raw_server.py | 98 ++ tests/test_casting.py | 565 ++++++++++ tests/test_castutils.py | 799 ++++++++++++++ tests/test_dataclasses.py | 127 +++ tests/test_dictator.py | 425 ++++++++ tests/test_dictators.py | 391 +++++++ tests/test_objectifier.py | 665 ++++++++++++ tests/test_services.py | 278 +++++ 81 files changed, 8483 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-to-pypi.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 protoplasm/__init__.py create mode 100644 protoplasm/bases/__init__.py create mode 100644 protoplasm/bases/_client.py create mode 100644 protoplasm/bases/_exwrap.py create mode 100644 protoplasm/bases/_methwrap.py create mode 100644 protoplasm/bases/_remotewrap.py create mode 100644 protoplasm/bases/_servicer.py create mode 100644 protoplasm/bases/dataclass_bases.py create mode 100644 protoplasm/bases/grpc_bases.py create mode 100644 protoplasm/casting/__init__.py create mode 100644 protoplasm/casting/casters.py create mode 100644 protoplasm/casting/castutils.py create mode 100644 protoplasm/casting/dictator.py create mode 100644 protoplasm/casting/dictators/__init__.py create mode 100644 protoplasm/casting/objectifier.py create mode 100644 protoplasm/decorators/__init__.py create mode 100644 protoplasm/decorators/apihelpers.py create mode 100644 protoplasm/errors/__init__.py create mode 100644 protoplasm/errors/_api.py create mode 100644 protoplasm/errors/_base.py create mode 100644 protoplasm/grpcserver.py create mode 100644 protoplasm/loader/__init__.py create mode 100644 protoplasm/plasm/__init__.py create mode 100644 protoplasm/structs/__init__.py create mode 100644 protoplasm/structs/_base.py create mode 100644 protoplasm/structs/_ctx/__init__.py create mode 100644 protoplasm/structs/_ctx/_basectx.py create mode 100644 protoplasm/structs/_ctx/_grpcctx.py create mode 100644 protoplasm/structs/_methodtype.py create mode 100644 protoplasm/structs/_reqestsiter.py create mode 100644 protoplasm/structs/_responseiter.py create mode 100644 protoplasm/structs/dataclassbase.py create mode 100644 pyproject.toml create mode 100644 requirements-unittesting.txt create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/_sandbox/__init__.py create mode 100644 tests/_sandbox/_plasm_stream_client.py create mode 100644 tests/_sandbox/_plasm_stream_server.py create mode 100644 tests/_sandbox/_raw_stream_client.py create mode 100644 tests/_sandbox/_raw_stream_service.py create mode 100644 tests/_sandbox/_sandboxclient.py create mode 100644 tests/plain/__init__.py create mode 100644 tests/plain/test_apihelpers.py create mode 100644 tests/plain/test_base64stuff.py create mode 100644 tests/res/build/empty.txt create mode 100644 tests/res/proto/sandbox/test/alpha.proto create mode 100644 tests/res/proto/sandbox/test/anytest.proto create mode 100644 tests/res/proto/sandbox/test/beta.proto create mode 100644 tests/res/proto/sandbox/test/clones.proto create mode 100644 tests/res/proto/sandbox/test/delta.proto create mode 100644 tests/res/proto/sandbox/test/enums.proto create mode 100644 tests/res/proto/sandbox/test/illnamedfields.proto create mode 100644 tests/res/proto/sandbox/test/illnamedservice.proto create mode 100644 tests/res/proto/sandbox/test/interfaceonly.proto create mode 100644 tests/res/proto/sandbox/test/nested.proto create mode 100644 tests/res/proto/sandbox/test/rainbow.proto create mode 100644 tests/res/proto/sandbox/test/river.proto create mode 100644 tests/res/proto/sandbox/test/service.proto create mode 100644 tests/res/proto/sandbox/test/service_with_imported_io.proto create mode 100644 tests/res/proto/sandbox/test/service_with_oneof.proto create mode 100644 tests/res/proto/sandbox/test/test.proto create mode 100644 tests/res/proto/sandbox/test/timeduration.proto create mode 100644 tests/res/proto/unittesting/unary/unaryservice.proto create mode 100644 tests/servicetestutils/__init__.py create mode 100644 tests/servicetestutils/plasm_server.py create mode 100644 tests/servicetestutils/raw_client.py create mode 100644 tests/servicetestutils/raw_server.py create mode 100644 tests/test_casting.py create mode 100644 tests/test_castutils.py create mode 100644 tests/test_dataclasses.py create mode 100644 tests/test_dictator.py create mode 100644 tests/test_dictators.py create mode 100644 tests/test_objectifier.py create mode 100644 tests/test_services.py diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..2fdfc61 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,29 @@ +name: Publish Python Package + +on: + release: + types: [ created ] + +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Install Dependencies + run: | + python --version + python -m pip install --upgrade pip + pip install --upgrade setuptools wheel twine + - name: Build and Package + run: | + python setup.py sdist bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + verbose: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..773fb07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +venv*/ +.venv*/ + +# IDEA Stuff +.idea/ + +# Local stuff +/proto/ +/build/ +/tests/res/build/ +-/tests/res/build/empty.txt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bff8c02 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,159 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [5.0.0] - 2024-04-15 + +### Changed + +- Moved this entire project over to Github +- Bumped the version in order to not confuse older stuff that doesn't expect + protoplasm to exist in Pypi.org (if we end up migriting this there and + just open-sourcing the whole thing) + - Also in case something changes in the API while migrating, cause I tend + to fiddle with the code and tidy up and refactor when moving stuff + + +## [4.6.0] - 2024-03-19 + +### Added + +- Docker image building of the latest stable version of Protoplasm and all + its dependencies to this project for both Alpine and Debian and Python + versions 3.8-3.12 + - Thus, checking in and publishing a new stable version of Protoplasm + should trigger new Docker base images with Protoplasm and all the gRPC + binaries up-to-date and ready to be used by other projects (speeding up + their testing and building considerably) + - ...at least in the old CI/CD environment... We'll see what's what once + this is all in Github and Github Actions + +### Changed + +- A ton of CI/CD stuff that's irrelevant now that we've moved to Github + + + +## [4.3.0] - 2022-08-03 + +Fixed ANOTHER bug in type hint importing + +### Fixed + +- Now importing `List` and `Dict` with nested dots work (e.g. `List[foo.Bar]`) + + +## [4.2.0] - 2022-08-03 + +Fixed bug in type hint importing + +### Fixed + +- Now importing `List` and `Dict` works (was only `typing.List` and `typing.Dict` before) + + + +## [4.1.0] - 2022-04-24 + +### Added + +- Added the `ResponseIterator` which remote method calls with stream output now return as a wrapper around the previously returned iterable results. +- Added `TimeoutResponseIterator` which wraps the new `ResponseIterator` with timeout functionality while waiting for response stream iterations. +- `ResponseIterator` has a `with_timeout()` method which automatically wraps it in a `TimeoutResponseIterator` +- Both `ResponseIterator` and `TimeoutResponseIterator` have a `one_and_done()` method which waits for one next response stream iteration and then closes the stream. +- `RequestIterator` can now take in a `list` or `tuple` as `initial_request` to load up multiple initial requests. +- Added `close()` method to `RequestIterator` to close the request stream (force a cancel event) +- Added `unpack()` to `DataclassBase` + + +## [4.0.0] - 2022-06-18 + +Major update that unifies the API and functionality of versions 2 and 3. + +Skipping a bunch of versions and checkins between 3.0 and 4.0 cause they're +not really important at the moment (it's 2024 and I'm migrating this from +our internal Gitlab repo to Github). + +### Added + +- Unify the unary functionality of Protoplasm 2 with the streaming + functionality of Protoplasm 3 + - The two turn out to be completely incompatible and API shattering + Protoplasm 4 must incorporate BOTH functionalities wile being backwards + compatible enough for both Protoplasm 2 and 3 projects to be able to + migrate to 4 + -The key here is detecting the `stream` keyword in protos that denote + streaming input and/or output +- Add piled up functionality/utility/QoL improvements/bugs that's been on The + List™ for a while + - Cast to/from base64 encoded strings + - Utilize the `__all__` directive to isolate `import *` side effects + - Integrate the Neobuf Builder CLI (from various other projects) into + Protoplasm and generalize it + - Address the "`None` is default value" issue + - Explore the pros/cons of making non-existing Message/Object attributes + return `Empty` or `EmptyDict` to simplify nested attribute fetching...? + +### Changed + +- Refactor and restructure the package properly + - Separate the 4 main roles of the package logically + 1. Cross-piling `*.proto` to `*_pb2.py` and `*_pb2_grpc.py` + 2. Cross-piling `*_pb2.py` to `*_dc.py` Neobuf Dataclasses + 3. Generating `*_api.py` interfaces + 4. Generating gRPC implementation of Services + + +## [3.0.0] - 2021-05-11 + +Major update including support for gRPC streams. + +Skipping a bunch of versions and checkins between 2.0 and 3.0 cause they're +not really important at the moment (it's 2024 and I'm migrating this from +our internal Gitlab repo to Github). + +## Added + +- Support for gRPC streams + +## Changed + +- This broke backwards compatibility with version 1 and 2's non-streaming + gRPC calls + + +## [2.0.0] - 2021-02-18 + +Major update including support for secure channels. + +Skipping a bunch of versions and checkins between 1.0 and 2.0 cause they're +not really important at the moment (it's 2024 and I'm migrating this from +our internal Gitlab repo to Github). + +### Added + +- Support for Secure gRPC channels + + +## [1.0.0] - 2019-07-11 + +Initial stable release. + +Skipping a bunch of versions and checkins between 0.1 and 1.0 cause they're +not really important at the moment (it's 2024 and I'm migrating this from +our internal Gitlab repo to Github). + +### Added + +- A bunch of features + + +## [0.1.0] - 2019-01-17 + +### Added + +- The initial checkin of this project \ No newline at end of file diff --git a/LICENSE b/LICENSE index e07ad05..0960c41 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 CCP Games +Copyright (c) 2019-2024 CCP Games Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 731ea9c..d310a1c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ -# protoplasm -Utilities for working with Protobuf & gRPC in Python, e.g; compiling Python 3 Dataclasses from compiled proto-message-python code and casting between the two, quickly initializing large/nested Protobuf object, simplifying gRPC service interfaces etc. +# Protoplasm + +Utilities for working with Protobuf & gRPC in Python, e.g; compiling Python 3 +Dataclasses from compiled proto-message-python code and casting between the two, +quickly initializing large/nested Protobuf object, simplifying gRPC service +interfaces etc. + +## IMPORTANT + +Update this README file after moving from CCP's internal code repo to Github. + + + +## Protoplasm 4 + +* Unify the unary functionality of Protoplasm 2 with the streaming + functionality of Protoplasm 3 + * The two turn out to be completely incompatible and API shattering + * Protoplasm 4 must incorporate BOTH functionalities wile being backwards + compatible enough for both Protoplasm 2 and 3 projects to be able to + migrate to 4 + * The key here is detecting the `stream` keyword in protos that denote + streaming input and/or output +* Add piled up functionality/utility/QoL improvements/bugs that's been on The + List™ for a while + * Cast to/from base64 encoded strings + * Utilize the `__all__` directive to isolate `import *` side effects + * Integrate the Neobuf Builder CLI (from various other projects) into + Protoplasm and generalize it + * Address the "`None` is default value" issue + * Explore the pros/cons of making non-existing Message/Object attributes + return `Empty` or `EmptyDict` to simplify nested attribute fetching...? +* Refactor and restructure the package properly + * Separate the 4 main roles of the package logically + 1. Cross-piling `*.proto` to `*_pb2.py` and `*_pb2_grpc.py` + 2. Cross-piling `*_pb2.py` to `*_dc.py` Neobuf Dataclasses + 3. Generating `*_api.py` interfaces + 4. Generating gRPC implementation of Services + +## Troubleshooting + +* I get `TypeError: Plain typing.NoReturn is not valid as type argument` + * Upgrade to Python 3.9. This TypeError arises from [a bug in Python 3.7](https://bugs.python.org/issue34921) + + +## Clever bits to document... + +- Code Generation (e.g. `foo.proto`) + how to build + - Dataclasses -> `foo_dc.py` + how to use (+ DataclassBase freebies) + - Extending Dataclasses (a no-no for pb2 files apparently) + - Service API -> `foo_api.py` + how to use and implement + - Automatic parameter unpacking + - Return value packing + - Raising `protoplasm.errors.api.*` on errors or non-ok returns + - Using `protoplasm.decorators` for param and type checking + - The `takes_context` decorator and how to use it + - GRPC DC Service Servicer -> `foo_dc_grpc.py` + how to use +- Utilities + - Proto <-> dict <-> Dataclass casters + - The `mkproto` and `mkdataclass` helpers + - The `unpack_dataclass_request` and `pack_dataclass_response` helpers diff --git a/protoplasm/__init__.py b/protoplasm/__init__.py new file mode 100644 index 0000000..af6beb2 --- /dev/null +++ b/protoplasm/__init__.py @@ -0,0 +1,5 @@ +__version__ = '5.0.0-beta.1' + +__author__ = 'Thordur Matthiasson ' +__license__ = 'MIT License' +__copyright__ = 'Copyright 2019-2024 - CCP Games ehf' diff --git a/protoplasm/bases/__init__.py b/protoplasm/bases/__init__.py new file mode 100644 index 0000000..01ea93d --- /dev/null +++ b/protoplasm/bases/__init__.py @@ -0,0 +1,3 @@ +from .dataclass_bases import * +from .grpc_bases import * +from ._exwrap import * diff --git a/protoplasm/bases/_client.py b/protoplasm/bases/_client.py new file mode 100644 index 0000000..ffb42e5 --- /dev/null +++ b/protoplasm/bases/_client.py @@ -0,0 +1,61 @@ +__all__ = [ + 'BaseGrpcClientImplementation', +] +import grpc # noqa + +from protoplasm.structs import * +from ._remotewrap import * +from .dataclass_bases import * + +import logging +log = logging.getLogger(__name__) + + +class BaseGrpcClientImplementation: + def __init__(self, service_stub_cls: Type, grpc_host='localhost:50051', + credentials: Optional[Union[bool, grpc.ChannelCredentials]] = None, + options: Optional[Dict] = None, *args, **kwargs): + """ + credentials: + - None -> Automatic... will use `grpc.ssl_channel_credentials()` and secure_channel if port is 443 + - True -> Will use `grpc.ssl_channel_credentials()` and secure_channel + - False -> Will use insecure_channel and no creds + - grpc.ChannelCredentials -> Will use given creds and secure_channel + """ + options = options or {} + if 'grpc.enable_http_proxy' not in options: + options['grpc.enable_http_proxy'] = 0 + option_tuple = tuple(options.items()) + + # Backwards compatibility + if kwargs: + if not credentials and 'credential' in kwargs: + credentials = kwargs.pop('credential') + log.warning('Deprecated keyword "credential" used when initiating BaseGrpcClient.' + ' Please use "credentials" instead!') + + # This keeps happening...! + if ':' not in grpc_host: + if credentials: + log.warning('No port used in host... adding 443 by default (cause this is a secure connection)!') + grpc_host = f'{grpc_host}:443' + else: + log.warning('No port used in host... adding 50051 by default!') + grpc_host = f'{grpc_host}:50051' + + if credentials is None: + if grpc_host.endswith(':443'): + credentials = grpc.ssl_channel_credentials() + elif credentials is True: + credentials = grpc.ssl_channel_credentials() + + if credentials: + self.grpc_channel: grpc.Channel = grpc.secure_channel(grpc_host, credentials, options=option_tuple) + else: + self.grpc_channel: grpc.Channel = grpc.insecure_channel(grpc_host, options=option_tuple) + + self.stub = service_stub_cls(self.grpc_channel) + + def _forward_to_grpc(self, request_cls: Type[T_DCB], stub_method: Callable, + *args, **kwargs) -> Optional[Union[Any, Tuple[Any], Iterable[T_DCB]]]: + return RemoteMethodWrapper(self, request_cls, stub_method, *args, **kwargs).call() diff --git a/protoplasm/bases/_exwrap.py b/protoplasm/bases/_exwrap.py new file mode 100644 index 0000000..d3d24c6 --- /dev/null +++ b/protoplasm/bases/_exwrap.py @@ -0,0 +1,113 @@ +__all__ = [ + 'GrpcExceptionWrapper', +] + +import grpc +from typing import * +from protoplasm import errors + +_STATUS_CODE_TO_EXCEPTION_MAP = { + grpc.StatusCode.UNKNOWN: errors.api.Unknown, + grpc.StatusCode.NOT_FOUND: errors.api.NotFound, + grpc.StatusCode.INVALID_ARGUMENT: errors.api.InvalidArgument, + grpc.StatusCode.UNIMPLEMENTED: errors.api.Unimplemented, + grpc.StatusCode.PERMISSION_DENIED: errors.api.PermissionDenied, + grpc.StatusCode.UNAUTHENTICATED: errors.api.Unauthenticated, + grpc.StatusCode.UNAVAILABLE: errors.api.Unavailable, + grpc.StatusCode.DEADLINE_EXCEEDED: errors.api.DeadlineExceeded, + grpc.StatusCode.ALREADY_EXISTS: errors.api.AlreadyExists, + grpc.StatusCode.FAILED_PRECONDITION: errors.api.FailedPrecondition, + grpc.StatusCode.DATA_LOSS: errors.api.DataLoss, + grpc.StatusCode.RESOURCE_EXHAUSTED: errors.api.ResourceExhausted, + grpc.StatusCode.OUT_OF_RANGE: errors.api.OutOfRange, + grpc.StatusCode.INTERNAL: errors.api.Internal, + grpc.StatusCode.CANCELLED: errors.api.Cancelled, + grpc.StatusCode.ABORTED: errors.api.Aborted, +} + + +class GrpcExceptionWrapper(object): + def __init__(self, ex: grpc.RpcError): + self.ex = ex + self._code = False + self._debug_error_string = False + self._details = False + self._initial_metadata = False + self._traceback = False + self._trailing_metadata = False + + @property + def code(self) -> Optional[grpc.StatusCode]: + if self._code is False: + self._code = None + code_call = getattr(self.ex, 'code', None) + if code_call and hasattr(code_call, '__call__'): + self._code = code_call() + return self._code + + @property + def debug_error_string(self) -> str: + if self._debug_error_string is False: + self._debug_error_string = '' + code_call = getattr(self.ex, 'debug_error_string', None) + if code_call and hasattr(code_call, '__call__'): + self._debug_error_string = code_call() + return self._debug_error_string + + @property + def details(self) -> str: + if self._details is False: + self._details = '' + code_call = getattr(self.ex, 'details', None) + if code_call and hasattr(code_call, '__call__'): + self._details = code_call() + return self._details + + @property + def initial_metadata(self) -> dict: + if self._initial_metadata is False: + self._initial_metadata = {} + code_call = getattr(self.ex, 'initial_metadata', None) + if code_call and hasattr(code_call, '__call__'): + for k, v in code_call(): + k = k.lower() + if k not in self._initial_metadata: + self._initial_metadata[k] = v + else: + if not isinstance(self._initial_metadata[k], list): + self._initial_metadata[k] = [self._initial_metadata[k]] + self._initial_metadata[k].append(v) + return self._initial_metadata + + @property + def traceback(self): + if self._traceback is False: + self._traceback = None + code_call = getattr(self.ex, 'traceback', None) + if code_call and hasattr(code_call, '__call__'): + self._traceback = code_call() + return self._traceback + + @property + def trailing_metadata(self) -> dict: + if self._trailing_metadata is False: + self._trailing_metadata = {} + code_call = getattr(self.ex, 'trailing_metadata', None) + if code_call and hasattr(code_call, '__call__'): + for k, v in code_call(): + k = k.lower() + if k not in self._trailing_metadata: + self._trailing_metadata[k] = v + else: + if not isinstance(self._trailing_metadata[k], list): + self._trailing_metadata[k] = [self._trailing_metadata[k]] + self._trailing_metadata[k].append(v) + return self._trailing_metadata + + def get_reraise(self): + return _STATUS_CODE_TO_EXCEPTION_MAP.get(self.code, errors.api.Unknown)(self.details, + ref_code=self.trailing_metadata.get('ref_code', None)) + + + + diff --git a/protoplasm/bases/_methwrap.py b/protoplasm/bases/_methwrap.py new file mode 100644 index 0000000..51f141a --- /dev/null +++ b/protoplasm/bases/_methwrap.py @@ -0,0 +1,220 @@ +__all__ = [ + 'PlasmMethodWrapper', +] + +import grpc +import json +import uuid + +from ccptools.tpu import casting as typeutils_casting + +from protoplasm import casting +from protoplasm import errors +from protoplasm.bases.dataclass_bases import * +from protoplasm.structs import * + +if TYPE_CHECKING: + from .grpc_bases import BaseGrpcServicer + +import logging +log = logging.getLogger(__name__) + + +class PlasmMethodWrapper: + JSON_SERIALIZER = typeutils_casting.JsonSafeSerializer() + + __slots__ = ('servicer', 'data_cls', 'impl_method', 'request', 'request_iterator', 'context', 'request_dc', + 'unpacked_args', 'raw_response_iterable', 'raw_response', 'response_dc', 'response_proto', + '_ref_code', 'method_io') + + def __init__(self, servicer: 'BaseGrpcServicer', data_cls: Type[T_DCB], impl_method: Callable, + request_or_iterator: Union[GeneratedProtocolMessageType, Iterable[GeneratedProtocolMessageType]], + context: grpc.ServicerContext, method_io: MethodIoType): + """Container and worker for processing Servicer (i.e. Server Side) + requests and responses, packing, unpacking, casting and so on as well as + handling errors and log-dumping and all that bulky noisy jass that just + clutters up the actual servicer code. + """ + self.servicer: 'BaseGrpcServicer' = servicer + self.data_cls: Type[DataclassBase] = data_cls + self.impl_method: Callable = impl_method + + self.request: Optional[GeneratedProtocolMessageType] = None + self.request_iterator: Optional[Iterable[GeneratedProtocolMessageType]] = None + + self.context: grpc.ServicerContext = context + self.method_io: MethodIoType = method_io + + if self.method_io.is_input_stream(): + self.request_iterator = request_or_iterator + else: + self.request = request_or_iterator + + self.request_dc: Optional[DataclassBase] = None + self.unpacked_args: Optional[Dict] = None + self.raw_response_iterable: Optional[Iterable[Any]] = None + self.raw_response: Any = None + self.response_dc: Optional[DataclassBase] = None + self.response_proto: Optional[GeneratedProtocolMessageType] = None + self._ref_code: str = '' + + @property + def request_dc_iterator(self) -> Iterable[T_DCB]: + for raw_req in self.request_iterator: + self.request = raw_req + self._p2d() + yield self.request_dc + + @property + def ref_code(self) -> str: + if not self._ref_code: + self._ref_code = str(uuid.uuid4()) + return self._ref_code + + def _get_dump(self, **kwargs) -> dict: + d = { + 'ref_code': self.ref_code, + 'class': str(self.servicer.__class__), + 'data_cls': str(self.data_cls), + 'impl_method': str(self.impl_method), + 'request': str(self.request), + 'method_io': self.method_io, + } + + if self.context: + try: + d['invocation_metadata'] = dict(self.context.invocation_metadata()) # noqa + except Exception: + d['invocation_metadata'] = 'FAILED TO FETCH METADATA!' + + if self.request_dc: + d['request_dc'] = repr(self.request_dc) + if self.unpacked_args: + d['unpacked_args'] = self.unpacked_args + if self.raw_response: + d['raw_response'] = repr(self.raw_response) + if self.response_dc: + d['response_dc'] = repr(self.response_dc) + if self.response_proto: + d['response_proto'] = repr(self.response_proto) + + if kwargs: + d.update(kwargs) + + return d + + def _get_json_dump(self, **kwargs) -> str: + return json.dumps(self.JSON_SERIALIZER.serialize(self._get_dump(**kwargs)), indent=4) + + def _abort(self, message: str, error_code, ex): + self.context.set_trailing_metadata([('ref_code', self.ref_code)]) + log.exception(f'{message}::{ex!r}\n---\n{self._get_json_dump(details=repr(ex))}') + return self.context.abort(error_code, f'{ex!r};\nref_code={self.ref_code}') + + def _abort_internal(self, ex: errors.ServiceApiException): + details = [ex.details] + if ex.should_log: + details.append(f'ref_code={self.ref_code}') + self.context.set_trailing_metadata([('ref_code', self.ref_code)]) + log.exception(f'Logging ServiceApiException::{ex!r}\n---\n' + f'{self._get_json_dump(status_code=ex.status_code, details=ex.details)}') + return self.context.abort(ex.status_code, ';\n'.join(details)) + + def _p2d(self): + try: + self.request_dc = casting.proto_to_dataclass(self.request) + except Exception as ex: + return self._abort('pre_request_error;Error while casting proto to dataclass', + grpc.StatusCode.FAILED_PRECONDITION, ex) + + def _unpack(self): + try: + if self.method_io.is_input_stream(): + self.unpacked_args = {'request_iterator': self.request_dc_iterator} + else: + self.unpacked_args = self.request_dc.unpack() + except Exception as ex: + return self._abort('pre_request_error;Error while unpacking arguments', + grpc.StatusCode.FAILED_PRECONDITION, ex) + + def _add_ctx(self): + try: + ctx_name = getattr(self.impl_method, '__takes_context_as__', False) + if ctx_name: + self.unpacked_args[ctx_name] = GrpcContext(self.context, self.method_io) + except Exception as ex: + return self._abort('pre_request_error;Error while adding context to parameters', + grpc.StatusCode.FAILED_PRECONDITION, ex) + + def prepare(self): + if not self.method_io.is_input_stream(): + self._p2d() + self._unpack() + self._add_ctx() + + def _pack(self): + self.response_dc = None + try: + self.response_dc = casting.pack_dataclass_response(self.data_cls, self.raw_response) + except Exception as ex: + return self._abort('post_request_error;Error while packing raw response to dataclass', + grpc.StatusCode.CANCELLED, ex) + + def _d2p(self): + self.response_proto = None + try: + self.response_proto = casting.dataclass_to_proto(self.response_dc) + except Exception as ex: + return self._abort('post_request_error;Error while casting dataclass to proto', + grpc.StatusCode.CANCELLED, ex) + + def call(self) -> Union[GeneratedProtocolMessageType, + Iterable[GeneratedProtocolMessageType]]: + # Prepare the call + self.prepare() + + try: + if self.method_io.is_output_stream(): + self.raw_response_iterable = self.impl_method(**self.unpacked_args) + + else: + self.raw_response = self.impl_method(**self.unpacked_args) + + except errors.ServiceApiException as ex: + return self._abort_internal(ex) + + except Exception as ex: + return self._abort('Unexpected exception in implementation method', + grpc.StatusCode.UNKNOWN, ex) + + if not self.method_io.is_output_stream(): + # Pack up results for unary results + self._pack() + self._d2p() + return self.response_proto + + else: + return self.iterable_response_proto + + @property + def wrapped_iterable_raw_response(self) -> Iterable[Any]: + try: + for res in self.raw_response_iterable: + yield res + except errors.ServiceApiException as ex: + return self._abort_internal(ex) + except Exception as ex: + return self._abort('Unexpected exception in implementation method', + grpc.StatusCode.UNKNOWN, ex) + + @property + def iterable_response_proto(self) -> Iterable[GeneratedProtocolMessageType]: + for res in self.wrapped_iterable_raw_response: + self.raw_response = res + self._pack() + self._d2p() + yield self.response_proto + + self.raw_response = None + self.response_dc = None + self.response_proto = None diff --git a/protoplasm/bases/_remotewrap.py b/protoplasm/bases/_remotewrap.py new file mode 100644 index 0000000..69347d4 --- /dev/null +++ b/protoplasm/bases/_remotewrap.py @@ -0,0 +1,261 @@ +__all__ = [ + 'RemoteMethodWrapper', +] +import json +import uuid + +import grpc # noqa +from grpc._channel import _UnaryUnaryMultiCallable as _UnaryUnaryStub # noqa +from grpc._channel import _UnaryStreamMultiCallable as _UnaryStreamStub # noqa +from grpc._channel import _StreamUnaryMultiCallable as _StreamUnaryStub # noqa +from grpc._channel import _StreamStreamMultiCallable as _StreamStreamStub # noqa + +from ccptools.tpu import casting as typeutils_casting + +from protoplasm.structs import * +from protoplasm import casting +from protoplasm import errors + +from .dataclass_bases import * +from ._exwrap import * + +import logging +log = logging.getLogger(__name__) + +if TYPE_CHECKING: + from .grpc_bases import BaseGrpcClientImplementation + from grpc._channel import _MultiThreadedRendezvous # noqa + + +class RemoteMethodWrapper: + JSON_SERIALIZER = typeutils_casting.JsonSafeSerializer() + + __slots__ = ('client', 'request_cls', 'stub_method', 'args', 'kwargs', + 'request_iterator', 'response_iterator', 'request_dc', + 'request_proto', 'response_proto', 'response_dc', + 'unpacked_results', 'method_io', '_ref_code') + + def __init__(self, client: 'BaseGrpcClientImplementation', request_cls: Type[T_DCB], + stub_method: Callable, *args, **kwargs): + """Container and worker for processing BaseGrpcClient (i.e. Client Side) + requests and responses, packing, unpacking, casting and so on as well as + handling errors and log-dumping and all that bulky noisy jass that just + clutters up the actual client code. + """ + self.client: 'BaseGrpcClientImplementation' = client + self.request_cls: Type[T_DCB] = request_cls + self.stub_method: Callable = stub_method + self.args: List[Any] = args or [] + self.kwargs: Dict[str, Any] = kwargs or {} + + self.request_iterator: Optional[Iterable[DataclassBase]] = None + self.response_iterator: Optional['_MultiThreadedRendezvous'] = None + + self.request_dc: Optional[DataclassBase] = None + self.request_proto: Optional[GeneratedProtocolMessageType] = None + self.response_proto: Optional[GeneratedProtocolMessageType] = None + self.response_dc: Optional[DataclassBase] = None + self.unpacked_results: Optional[Any, Tuple[Any]] = None + + self.method_io: MethodIoType = MethodIoType._UNKNOWN # noqa + + self._ref_code: str = '' + self._eval_method_io() + + def _eval_method_io(self): + if isinstance(self.stub_method, _UnaryUnaryStub): + self.method_io = MethodIoType.UNARY_IN_UNARY_OUT + + elif isinstance(self.stub_method, _UnaryStreamStub): + self.method_io = MethodIoType.UNARY_IN_STREAM_OUT + + elif isinstance(self.stub_method, _StreamUnaryStub): + self.method_io = MethodIoType.STREAM_IN_UNARY_OUT + + elif isinstance(self.stub_method, _StreamStreamStub): + self.method_io = MethodIoType.STREAM_IN_STREAM_OUT + + else: + self._abort_preflight('Unable to determine stub method io channels (unary vs. stream)') + + @property + def ref_code(self) -> str: + if not self._ref_code: + self._ref_code = str(uuid.uuid4()) + return self._ref_code + + @property + def method_name(self) -> str: + m = getattr(self.stub_method, '_method', str(self.stub_method)) + if isinstance(m, bytes): + return m.decode() + return m + + def _get_dump(self, **kwargs) -> dict: + d = { + 'ref_code': self.ref_code, + 'class': str(self.client.__class__), + 'request_cls': str(self.request_cls), + 'method_name': self.method_name, + 'method_io': self.method_io + } + if self.args: + d['args'] = repr(self.args) + if self.kwargs: + d['kwargs'] = repr(self.kwargs) + + if self.request_dc: + d['request_dc'] = repr(self.request_dc) + if self.request_proto: + d['request_proto'] = repr(self.request_proto) + + if self.response_proto: + d['response_proto'] = repr(self.response_proto) + + if self.response_dc: + d['response_dc'] = repr(self.response_dc) + + if self.unpacked_results: + d['unpacked_results'] = repr(self.unpacked_results) + + if kwargs: + d.update(kwargs) + + return d + + def _get_json_dump(self, **kwargs) -> str: + return json.dumps(self.JSON_SERIALIZER.serialize(self._get_dump(**kwargs)), indent=4) + + def _abort_preflight(self, message: str, ex: Optional[Exception] = None): + if ex: + log.exception(f'{message}::{ex!r}\n---\n{self._get_json_dump(details=repr(ex))}') + raise_msg = f'{message}: {ex!r}' + else: + log.exception(f'{message}\n---\n{self._get_json_dump()}') + raise_msg = message + raise errors.api.PreRequestError(raise_msg, self.ref_code, True) + + def _abort_inflight(self, message: str, ex: grpc.RpcError): + gex = GrpcExceptionWrapper(ex) + reex = gex.get_reraise() + if reex.should_log: + d = dict(details=gex.details, + debug_message=gex.debug_error_string, + trailing_metadata=gex.trailing_metadata, + initial_metadata=gex.initial_metadata, + gex=gex, + status_code=gex.code, + ref_code=reex.ref_code) + log.exception(f'{message}::{ex!r}\n---\n{self._get_json_dump(**d)}') + raise reex + + def _abort_postflight(self, message: str, ex: Exception): + log.exception(f'{message}::{ex!r}\n---\n{self._get_json_dump(details=repr(ex))}') + raise errors.api.PostRequestError(f'{message}: {ex!r}', self.ref_code, True) + + def _abort(self, message: str, ex: Exception): + log.exception(f'{message}::{ex!r}\n---\n{self._get_json_dump(details=repr(ex))}') + raise errors.api.Unknown(f'{message}: {ex!r}', self.ref_code, True) + + def prepare(self): + if not self.method_io.is_input_stream(): + self._mkdc() + self._d2p() + else: + self._find_request_iterator() + if isinstance(self.request_iterator, (list, tuple)): + self.request_iterator = RequestIterator(self.request_iterator) + + elif isinstance(self.request_iterator, self.request_cls): + self.request_iterator = RequestIterator(self.request_iterator) + + def _find_request_iterator(self): + if not self.kwargs and self.args and len(self.args) == 1: + self.request_iterator = self.args[0] + return + + if not self.args and self.kwargs and len(self.kwargs) == 1 and 'request_iterator' in self.kwargs: + self.request_iterator = self.kwargs['request_iterator'] + return + + self._abort_preflight('Expected a single arg or kwarg called "request_iterator" as input') + + @property + def request_proto_iterator(self) -> Iterable[GeneratedProtocolMessageType]: + for raw_req in self.request_iterator: + self.request_dc = raw_req + self._d2p() + yield self.request_proto + log.debug('Request Iterator Cancelled!') + + def _mkdc(self): + try: + self.request_dc = self.request_cls(*self.args, **self.kwargs) + except Exception as ex: + self._abort_preflight('Error while populating request class with arguments', ex) + + def _d2p(self): + try: + self.request_proto = self.request_dc.to_proto() + except Exception as ex: + self._abort_preflight('Error while casting request dataclass to proto', ex) + + def call(self) -> Optional[Union[ResponseIterator[T_DCB], Any, Tuple[Any]]]: + # Prepare the call + self.prepare() + + try: + if self.method_io.is_input_stream(): + if self.method_io.is_output_stream(): + self.response_iterator = self.stub_method(self.request_proto_iterator) + else: + self.response_proto = self.stub_method(self.request_proto_iterator) + + else: + if self.method_io.is_output_stream(): + self.response_iterator = self.stub_method(self.request_proto) + else: + self.response_proto = self.stub_method(self.request_proto) + + except grpc.RpcError as ex: + self._abort_inflight('RpcError exception while performing gRPC request', ex) + + except Exception as ex: + self._abort('Unknown exception while performing gRPC request', ex) + + if self.method_io.is_output_stream(): + return ResponseIterator(self) + + else: + self._p2d() + self._unpack() + return self.unpacked_results + + @property + def iterable_results(self) -> Iterable[T_DCB]: + for raw_response in self.response_iterator: + self.response_proto = raw_response + self._p2d() + yield self.response_dc + + def close(self): + if self.response_iterator: + self.response_iterator.cancel() + + @property + def is_closed(self) -> Optional[bool]: + if self.response_iterator: + return self.response_iterator.cancelled() + return None # If we don't have a result stream this doesn't make sense + + def _p2d(self): + try: + self.response_dc = casting.proto_to_dataclass(self.response_proto) + except Exception as ex: + self._abort_postflight('Error while casting response proto to dataclass', ex) + + def _unpack(self): + try: + self.unpacked_results = self.response_dc.explode() + except Exception as ex: + self._abort_postflight('Error while unpacking dataclass arguments', ex) diff --git a/protoplasm/bases/_servicer.py b/protoplasm/bases/_servicer.py new file mode 100644 index 0000000..d89fbc6 --- /dev/null +++ b/protoplasm/bases/_servicer.py @@ -0,0 +1,46 @@ +__all__ = [ + 'BaseGrpcServicer', +] +import grpc # noqa + +from protoplasm.structs import * +from .dataclass_bases import * +from ._methwrap import * + + +class BaseGrpcServicer: + def __init__(self, implementation, *args, **kwargs): + self.impl = implementation + + def add_to_server(self, server): + raise NotImplementedError() + + def _forward_to_impl(self, data_cls: Type[T_DCB], impl_method: Callable, + request: GeneratedProtocolMessageType, + context: grpc.ServicerContext) -> GeneratedProtocolMessageType: + """Alias to _forward_to_unary_unary_impl for backwards compatibility""" + return self._forward_to_unary_unary_impl(data_cls, impl_method, request, context) + + def _forward_to_unary_unary_impl(self, data_cls: Type[T_DCB], impl_method: Callable, + request: GeneratedProtocolMessageType, + context: grpc.ServicerContext) -> GeneratedProtocolMessageType: + return PlasmMethodWrapper(self, data_cls, impl_method, request, context, + MethodIoType.UNARY_IN_UNARY_OUT).call() + + def _forward_to_unary_stream_impl(self, data_cls: Type[T_DCB], impl_method: Callable, + request: GeneratedProtocolMessageType, + context: grpc.ServicerContext) -> Iterable[GeneratedProtocolMessageType]: + return PlasmMethodWrapper(self, data_cls, impl_method, request, context, + MethodIoType.UNARY_IN_STREAM_OUT).call() + + def _forward_to_stream_unary_impl(self, data_cls: Type[T_DCB], impl_method: Callable, + request_iterator: Iterable[GeneratedProtocolMessageType], + context: grpc.ServicerContext): + return PlasmMethodWrapper(self, data_cls, impl_method, request_iterator, context, + MethodIoType.STREAM_IN_UNARY_OUT).call() + + def _forward_to_stream_stream_impl(self, data_cls: Type[T_DCB], impl_method: Callable, + request_iterator: Iterable[GeneratedProtocolMessageType], + context: grpc.ServicerContext) -> Iterable[GeneratedProtocolMessageType]: + return PlasmMethodWrapper(self, data_cls, impl_method, request_iterator, context, + MethodIoType.STREAM_IN_STREAM_OUT).call() diff --git a/protoplasm/bases/dataclass_bases.py b/protoplasm/bases/dataclass_bases.py new file mode 100644 index 0000000..3e8edde --- /dev/null +++ b/protoplasm/bases/dataclass_bases.py @@ -0,0 +1,2 @@ +# Alias for backwards compatibility only... +from protoplasm.structs.dataclassbase import * diff --git a/protoplasm/bases/grpc_bases.py b/protoplasm/bases/grpc_bases.py new file mode 100644 index 0000000..5731038 --- /dev/null +++ b/protoplasm/bases/grpc_bases.py @@ -0,0 +1,3 @@ +from ._exwrap import * +from ._client import * +from ._servicer import * diff --git a/protoplasm/casting/__init__.py b/protoplasm/casting/__init__.py new file mode 100644 index 0000000..4dd9504 --- /dev/null +++ b/protoplasm/casting/__init__.py @@ -0,0 +1,4 @@ +from .casters import * +from .castutils import * +from .dictator import * +from .objectifier import * diff --git a/protoplasm/casting/casters.py b/protoplasm/casting/casters.py new file mode 100644 index 0000000..1b648e1 --- /dev/null +++ b/protoplasm/casting/casters.py @@ -0,0 +1,80 @@ +__all__ = [ + 'mkproto', + 'mkdataclass', + 'proto_to_dataclass', + 'dataclass_to_proto', + 'unpack_dataclass_return', + 'pack_dataclass_response', +] +# Alias imports +from protoplasm.structs import * +from protoplasm.casting.castutils import * +from protoplasm.casting.dictator import * +from protoplasm.casting.objectifier import * + +import logging +log = logging.getLogger(__name__) + + +def mkproto(proto_class: Type[GeneratedProtocolMessageType], **kwargs) -> GeneratedProtocolMessageType: + """Creates a new Protobuf object of the given proto_class type and populates + it using the given kwargs, first converting the kwargs to a dict via the + kwdict function that turns keywords using double-underscores into sub-dicts + for use as data for nested proto messages. + + :param proto_class: The Protobuf message class (type) + :return: A fully initialized and populated Profobuf object + """ + return dict_to_proto(proto_class, kwdict(**kwargs)) + + +def mkdataclass(dataclass_type: Type[T_DCB], **kwargs) -> T_DCB: + """Creates a new Dataclass object of the given dataclass_type and populates + it using the given kwargs, first converting the kwargs to a dict via the + kwdict function that turns keywords using double-underscores into sub-dicts + for use as data for nested proto messages. + + :param dataclass_type: Dataclass type to create + :return: A fully initialized and populated Profobuf object + """ + return dict_to_dataclass(dataclass_type, kwdict(**kwargs)) + + +def proto_to_dataclass(proto: GeneratedProtocolMessageType) -> DataclassBase: + return dict_to_dataclass(import_dataclass_by_proto(proto.__class__), proto_to_dict(proto)) + + +def dataclass_to_proto(dc: DataclassBase) -> GeneratedProtocolMessageType: + return dict_to_proto(dc.__proto_cls__, dataclass_to_dict(dc)) + + +def unpack_dataclass_return(dc: DataclassBase) -> Optional[Tuple[Any]]: + l = [] + for field in dataclasses.fields(dc): + l.append(getattr(dc, field.name)) + ln = len(l) + if ln == 0: + return + elif ln == 1: + return l[0] + return tuple(l) + + +def pack_dataclass_response(dc_response_type: Type[T_DCB], return_value) -> T_DCB: + if isinstance(return_value, dc_response_type): + return return_value + + if return_value is None: + return dc_response_type() + + if isinstance(return_value, dict): + return dc_response_type(**return_value) # TODO(thordurm@ccpgames.com>): Temporary solution + # TODO(thordurm@ccpgames.com>): If keys match dc_response_type fields, it's a simple kwargs mapping + # TODO(thordurm@ccpgames.com>): ...else If the first field it a dict, treat this as the first field + # TODO(thordurm@ccpgames.com>): ...else we're fucked! + + elif isinstance(return_value, tuple): + return dc_response_type(*return_value) # TODO(thordurm@ccpgames.com>): Temporary solution + # TODO(thordurm@ccpgames.com>): If value types match field types and len is <= fields, it's a simple args mapping + else: + return dc_response_type(return_value) diff --git a/protoplasm/casting/castutils.py b/protoplasm/casting/castutils.py new file mode 100644 index 0000000..0d957a2 --- /dev/null +++ b/protoplasm/casting/castutils.py @@ -0,0 +1,384 @@ +__all__ = [ + 'force_object', + 'kwdict', + 'import_dataclass_by_proto', + 'get_type_from_module_import_as', + 'get_dc_field_obj_type', + 'humps_to_under', + 'check_type', + 'get_type_url', + 'get_type_name_from_url', + 'get_proto_descriptor_by_type_url', + 'get_proto_file_by_type_url', + 'base64_stripper', + 'base64_filler', + 'fuzzy_base64_to_bytes', +] + +from collections import abc +import sys +import re +import builtins +import base64 +import typing +import enum + +from google.protobuf import symbol_database +from google.protobuf import json_format +from ccptools.tpu import strimp + +from protoplasm.structs import * + +import logging +log = logging.getLogger(__name__) + + +T_ANY_CLASS = TypeVar('T_ANY_CLASS') + +TYPING_MATCHER = re.compile(r'typing.(?P[a-zA-Z]+)\[(?P[a-zA-Z0-9.,_ ]+)]') +STAR_IMPORT_TYPING_MATCHER = re.compile(r'(?PList|Dict)\[(?P.+)]') + +_POST_39 = sys.version_info >= (3, 9) + + +def force_object(object_cls: Type[T_ANY_CLASS], value: Any) -> T_ANY_CLASS: + """Takes a `value` in the form of a single argument, list of arguments, a + dict of keyword arguments or an object of type `object_cls`. + + If the value is already an object of type `object_cls` or a `None`, it's + simply returned as is. + + If the `value` is a single argument variable, it is used as the first (and + only) arg to instantiate a new object of the `object_cls` type which is then + returned. + + If the `value` is a `list` or `tuple`, the values therein are used as + `*args` to instantiate a new object of the `object_cls` type which is then + returned. + + If the `value` is a `dict`, the keys and values therein are used as + `**kwargs` to instantiate a new object of the `object_cls` type which is + then returned. + + This guarantees a return value of either an object of `object_cls` type, a + none or a raised `Exception`, e.g. if the supplied `value` can't be used as + `__init__()` arguments in the `object_cls` (most likely a `TypeError` or + `ValueError`) or if the `__init__()` of that class raises any other + exception. + + If you need a `None` value forced into a new empty instance of the given + `object_cls` a simple way to achieve that is: + + ```my_foo = force_object(Foo, my_values) or Foo()``` + + One thing worth mentioning is that if `object_cls` takes a `list`, `tuple` + or a `dict` as it's first `__init__()` argument and the given `value` is of + that same type and intended to be used as the first and only argument for + the `__init__()`, that will not work as `value` of those three types will be + treated as a collection of `*args` or mapping og `**kwargs` for the + `__init__()` instead of a single argument. + + Solutions to that include: + - Not using this function for classes that take `list`, `tuple` or `dict` + as their first arguments. This isn't intended to be a universal solution + that fits all cases and classes so if you have a square hole, don't use a + round peg. ;) + - Wrapping the `value` in a `list` or `tuple` of its own before passing it + to this function as that will get treated as a list of `*args` for the + `__init__()`, with the inner `list`, `tuple` or `dict` used correctly as + the first (and only) argument. + - This may however not work correctly if `value` is a None and the + `__init__()` doesn't take kindly to None values for the argument it + expects to be a `list`, `tuple` or `dict` + + :param object_cls: The class/type to force the given value into + :param value: + :type value: + :return: + :rtype: + """ + if value is None or isinstance(value, object_cls): + return value + if isinstance(value, dict): + return object_cls(**value) + elif isinstance(value, (list, tuple)): + return object_cls(*value) + else: + return object_cls(value) + + +def kwdict(**kwargs) -> dict: + """Takes kwargs and turns into a dict using double underscores in order to + build nested dicts. + + Examples: + + >>> kwdict(foo='one', bar='two') + {'foo': 'one', 'bar': 'two'} + + >>> kwdict(foo='one', bar__one=1, bar__two=2) + {'foo': 'one', 'bar': {'one': 1, 'two': 2}} + """ + d = {} + subs = {} + for k, v in kwargs.items(): + first, *rest = k.split('__', 1) + if rest: + if first not in subs: + subs[first] = {} + subs[first][rest[0]] = v + else: + d[first] = v + + for k, v in subs.items(): + d[k] = kwdict(**v) + + return d + + +def import_dataclass_by_proto(proto_class: GeneratedProtocolMessageType) -> Type[DataclassBase]: + """Dynamically imports and returns the dataclass type corresponding to the + given Protobuf message class. + + This will also cache that dataclass type in the Protobuf message class in + order to speed up subsequent future imports by a tiny tiny fraction. + + :param proto_class: The Protobuf message class that we need to find the + dataclass version of. + """ + cls = getattr(proto_class, '__dataclass_cls__', None) + if not cls: + if not isinstance(proto_class, type): + proto_class = proto_class.__class__ + cls = strimp.get_class(''.join([proto_class.__module__[:-3], 'dc.', proto_class.__name__]), reraise=True) + setattr(proto_class, '__dataclass_cls__', cls) + return cls + + +# TODO(thordurm@ccpgames.com>): Maybe this should live in typeutils strimp?!? +def get_type_from_module_import_as(type_name: str, cls: Type) -> Type: + cls_module = sys.modules.get(cls.__module__) + if not cls_module: + raise ImportError(f'get_type_from_module_import_as failed to find {cls.__module__} in sys.modules') + + if '.' in type_name: + m = TYPING_MATCHER.match(type_name) + if m: # From 'typing' (this is pre-version 4.1 stuff, kept in for backwards compatibility) + return _get_from_typing(m.group('type'), m.group('inner'), cls) + else: + m = STAR_IMPORT_TYPING_MATCHER.match(type_name) + if m: # From 'typing' via 'from typing import *' + return _get_from_typing(m.group('type'), m.group('inner'), cls) + + else: # From somewhere else + as_name, type_name = type_name.rsplit('.', 1) + cls_module = sys.modules.get(cls.__module__) + if not cls_module: + raise ImportError(f'get_type_from_module_import_as failed to find {cls.__module__} in sys.modules') + + alias_module = cls_module + for as_part in as_name.split('.'): + alias_module = getattr(alias_module, as_part, None) + + if not alias_module: + raise ImportError(f'get_type_from_module_import_as failed to find {as_name} in {cls_module}') + else: + builtin_type = getattr(builtins, type_name, None) + if not builtin_type: + m = STAR_IMPORT_TYPING_MATCHER.match(type_name) + if m: # From 'typing' via 'from typing import *' + return _get_from_typing(m.group('type'), m.group('inner'), cls) + else: + if isinstance(builtin_type, type): + return builtin_type + + alias_module = cls_module + + actual_type = getattr(alias_module, type_name, None) + if not actual_type: + raise ImportError(f'get_type_from_module_import_as failed to find {actual_type} in {alias_module}') + + return actual_type + + +def _get_from_typing(type_name: str, inner_str: str, cls: Type) -> Type: + typing_type = getattr(typing, type_name, None) # E.g. List + if not typing_type: + raise ImportError(f'get_type_from_module_import_as->_get_from_typing failed to find {type_name} in typing module') + if inner_str: + args = tuple([get_type_from_module_import_as(a.strip(), cls) for a in inner_str.split(',')]) + return typing_type[args] + return typing_type + + +def get_dc_field_obj_type(dc_field: dataclasses.Field, + parent_dataclass: Type[DataclassBase]) -> Union[Type, Type[DataclassBase], enum.EnumMeta]: + """Fetches the "actual" type of a dataclass field. In the case of lists and + maps/dicts this means the type of the values the lists/maps/dicts contain. + + This assumes a dataclass generated by protoplasm. + + :param dc_field: The dataclass field who's type we need. + :param parent_dataclass: The dataclass itself + :return: The type defined in the dataclass field + """ + if isinstance(dc_field.type, str): + dc_field.type = get_type_from_module_import_as(dc_field.type, parent_dataclass) + + if dc_field.metadata.get('is_map', False): + return dc_field.type.__args__[1] + elif dc_field.metadata.get('is_list', False): + return dc_field.type.__args__[0] + else: + return dc_field.type + + +def humps_to_under(string: str) -> str: + buf = [] + if string.endswith('ID'): + if len(string) > 2 and string[-3].islower(): + string = f'{string[:-2]}_id' + else: + return string.lower() + + for i, c in enumerate(string): + if c.isupper(): + if i != 0: + buf.append('_') + c = c.lower() + buf.append(c) + return ''.join(buf) + + +def check_type(val: Any, type_or_annotation: Any) -> bool: + # TODO(thordurm@ccpgames.com>): Move to or copy to typeutils + + def _is_special(toa: Any) -> bool: + if _POST_39: + if isinstance(toa, typing._SpecialGenericAlias): # noqa + return True + else: + return toa._special # noqa + + if val is None and type_or_annotation is None: + return True + + m = getattr(type_or_annotation, '__module__', None) + if m == 'typing': + if type_or_annotation == Any: + return True + if isinstance(type_or_annotation, typing._GenericAlias): # noqa + if _is_special(type_or_annotation): # noqa Not subscripted + return isinstance(val, type_or_annotation.__origin__) + + else: + if type_or_annotation.__origin__ == Union: + if not type_or_annotation.__args__: + return True + if Any in type_or_annotation.__args__: + return True + return isinstance(val, type_or_annotation.__args__) + + elif type_or_annotation.__origin__ in (list, set): # Check list and set + if not isinstance(val, type_or_annotation.__origin__): + return False + if not type_or_annotation.__args__ or Any in type_or_annotation.__args__: + return True + for sub_val in val: + if not check_type(sub_val, Union[type_or_annotation.__args__]): + return False + return True # Should be good now! :) + + elif type_or_annotation.__origin__ == tuple: # Check tuple! + if not isinstance(val, tuple): + return False + if not type_or_annotation.__args__: + return True + + if type_or_annotation.__args__[-1] is ... and len(type_or_annotation.__args__) == 2: + for sub_val in val: + if not check_type(sub_val, Union[type_or_annotation.__args__[0]]): + return False + return True # Should be good now! :) + + if len(type_or_annotation.__args__) != len(val): + return False + + for i, sub_val in enumerate(val): + if not check_type(sub_val, Union[type_or_annotation.__args__[i]]): + return False + + return True # Should be good now! :) + + elif issubclass(type_or_annotation.__origin__, abc.Mapping): + if not isinstance(val, type_or_annotation.__origin__): + return False + if not type_or_annotation.__args__ or type_or_annotation.__args__ == (Any, Any): + return True + + for k, v in val.items(): + if not (check_type(k, Union[type_or_annotation.__args__[0]]) and check_type(v, Union[type_or_annotation.__args__[1]])): + return False + return True + else: + log.warning('I do not know how to check type %r of typing module:(', type_or_annotation) + if isinstance(type_or_annotation, TypeVar): + # TODO(thordurm@ccpgames.com>): Not really tested and supported yet officially... + # TODO(thordurm@ccpgames.com>): Main issue is nested types (if they don't flatten) + return check_type(val, Union[type_or_annotation.__constraints__]) + return isinstance(val, type_or_annotation) + + +def get_type_url(proto: GeneratedProtocolMessageType, prefix: str = 'type.googleapis.com/') -> str: + # TODO(thordurm@ccpgames.com>) 2024-04-15: Not sure how best to support any prefix other than "type.googleapis.com/" here + if not prefix: + return f'/{proto.DESCRIPTOR.full_name}' + if prefix[-1] == '/': + return f'{prefix}{proto.DESCRIPTOR.full_name}' + return f'{prefix}/{proto.DESCRIPTOR.full_name}' + + +def get_type_name_from_url(type_url: str) -> str: + # TODO(thordurm@ccpgames.com>) 2024-04-15: Not sure how best to support any prefix other than "type.googleapis.com/" here + return type_url.split('/')[-1] + + +def get_proto_descriptor_by_type_url(type_url: str) -> json_format.descriptor.Descriptor: + # TODO(thordurm@ccpgames.com>) 2024-04-15: Not sure how best to support any prefix other than "type.googleapis.com/" here + type_name = get_type_name_from_url(type_url) + db = symbol_database.Default() + try: + message_descriptor = db.pool.FindMessageTypeByName(type_name) + except KeyError: + raise TypeError('Can not find message descriptor by type_url: {0}. You may need to import the pb2 file' + ' before it registers in the SymbolDatabase'.format(type_url)) + return message_descriptor + + +def get_proto_file_by_type_url(type_url: str) -> json_format.descriptor.FileDescriptor: + # TODO(thordurm@ccpgames.com>) 2024-04-15: Not sure how best to support any prefix other than "type.googleapis.com/" here + type_name = get_type_name_from_url(type_url) + db = symbol_database.Default() + try: + file_descriptor = db.pool.FindFileContainingSymbol(type_name) + except KeyError: + raise TypeError('Can not find file descriptor by type_url: {0}. You may need to import the pb2 file before' + ' it registers in the SymbolDatabase'.format(type_url)) + return file_descriptor + + +def base64_stripper(base64str: Union[bytes, str]) -> bytes: + if isinstance(base64str, str): + base64str = base64str.encode() + return base64str.strip().replace(b'\r', b'').replace(b'\n', b'').replace(b'\t', b'').replace(b'=', b'') + + +def base64_filler(base64str: Union[bytes, str]) -> bytes: + if isinstance(base64str, str): + base64str = base64str.encode() + base64str = base64str.strip().replace(b'\r', b'').replace(b'\n', b'').replace(b'\t', b'') + return base64str + b'=' * (4 - (len(base64str) % 4) or 4) + + +def fuzzy_base64_to_bytes(base64str: Union[bytes, str]) -> bytes: + return base64.decodebytes(base64_filler(base64str)) diff --git a/protoplasm/casting/dictator.py b/protoplasm/casting/dictator.py new file mode 100644 index 0000000..4ebc088 --- /dev/null +++ b/protoplasm/casting/dictator.py @@ -0,0 +1,74 @@ +__all__ = [ + 'proto_to_dict', + 'dataclass_to_dict', +] +from protoplasm.casting import dictators +from protoplasm.structs import * + +from google.protobuf import json_format + +import logging +log = logging.getLogger(__name__) + + +def proto_to_dict(proto: GeneratedProtocolMessageType) -> Dict: + """Turns a Protobuf message object into a dict. + """ + return json_format.MessageToDict(proto, + preserving_proto_field_name=True, + use_integers_for_enums=True) + + +def dataclass_to_dict(dc) -> Dict: + """Turns a Dataclass object into a dict with the data values serialized so + that they can be turned into Protobuf objects. + + This will also turn non-proto related dataclasses into dicts but those won't + necessarily be usable to build Protobuf messages as this will simply use + the asdict call in the standard dataclass library. + + :param dc: The dataclass to turn into a dict + :return: A dictionary of Proto-ready values. + """ + if not hasattr(dc, '__proto_cls__'): # Plain dataclass + return dataclasses.asdict(dc) + + d = {} + for field in dataclasses.fields(dc): + dicted = _dataclass_field_to_dict_field(field, dc) + # if dicted: + if dicted is not None: # TODO(thordurm@ccpgames.com>) 2024-04-15: To include "default/empty" fields or not? + d[_get_proto_field_name(field)] = dicted + + return d + + +def _get_proto_field_name(field: dataclasses.Field): + return field.metadata.get('pb_name', field.name) + + +def _dataclass_field_to_dict_field(field: dataclasses.Field, dc): + val = getattr(dc, field.name) + + if val is None: + return None + + dictator_cls = field.metadata.get('dictator', dictators.BaseDictator) + is_obj = field.metadata.get('is_obj', False) + is_list = field.metadata.get('is_list', False) + is_map = field.metadata.get('is_map', False) + + if is_obj: + if is_map: + return {k: dataclass_to_dict(v) for k, v in val.items()} if val else None + elif is_list: + return [dataclass_to_dict(v) for v in val] if val else None + else: + return dataclass_to_dict(val) + else: + if is_map: + return {k: dictator_cls.to_dict_value(v, field, dc) for k, v in val.items()} if val else None + elif is_list: + return [dictator_cls.to_dict_value(v, field, dc) for v in val] if val else None + else: + return dictator_cls.to_dict_value(val, field, dc) diff --git a/protoplasm/casting/dictators/__init__.py b/protoplasm/casting/dictators/__init__.py new file mode 100644 index 0000000..b3b5ece --- /dev/null +++ b/protoplasm/casting/dictators/__init__.py @@ -0,0 +1,311 @@ +__all__ = [ + 'BaseDictator', + 'TimestampDictator', + 'ByteDictator', + 'EnumDictator', + 'LongDictator', + 'DurationDictator', + 'AnyDictator', +] +import datetime +import base64 +import dataclasses +from ccptools import dtu +import enum +import collections + +from protoplasm.structs import * +from protoplasm.casting import castutils + + +class BaseDictator: + @classmethod + def to_dict_value(cls, dc_value: Any, field: dataclasses.Field, parent: DataclassBase) -> Any: + """Casts data from whatever a dataclass stores to a value that protobuf + messages can parse from a dict. + + :param dc_value: Dataclass value + :type dc_value: Any + :param field: The dataclass field descriptor the value comes from + :type field: dataclasses.Field + :param parent: The dataclass the field belongs to + :type parent: object + :return: A value that the protobuf ParseDict function can use + :rtype: Any + """ + return dc_value + + @classmethod + def from_dict_value(cls, proto_value: Any, field: dataclasses.Field, parent_type: Type[DataclassBase]) -> Any: + """Casts data from a dict version of a protobuf message into whatever + value the corresponding dataclass uses. + + :param proto_value: Protobuf dict value + :type proto_value: Any + :param field: The dataclass field descriptor the value is going to + :type field: dataclasses.Field + :param parent_type: The dataclass the field belongs to + :type parent_type: object + :return: A value that the dataclass uses + :rtype: Any + """ + return proto_value + + +class TimestampDictator(BaseDictator): + @classmethod + def to_dict_value(cls, dc_value: Optional[datetime.datetime], + field: dataclasses.Field, parent: DataclassBase) -> Optional[str]: + """Casts data from a dataclass datetime values to iso string (with + tailing Z). + + Format: %Y-%m-%dT%H:%M:%S.%fZ + + :param dc_value: Dataclass value of a python datetime + :type dc_value: datetime.datetime | None + :param field: The dataclass field descriptor the value comes from + :type field: dataclasses.Field + :param parent: The dataclass the field belongs to + :type parent: object + :return: Datetime as an iso string (with trailing Z) + :rtype: str | None + """ + if not dc_value: + return None + + if not isinstance(dc_value, datetime.datetime): + raise TypeError(f'TimestampDictator.to_dict_value expected datetime, got {type(dc_value)}') + + if dc_value.microsecond: + return dc_value.strftime('%Y-%m-%dT%H:%M:%S.%fZ') + + return dc_value.strftime('%Y-%m-%dT%H:%M:%SZ') + + @classmethod + def from_dict_value(cls, proto_value: Optional[str], + field: dataclasses.Field, parent_type: Type[DataclassBase]) -> Optional[datetime.datetime]: + """Casts a timestamp value from a iso-string (with trailing Z) to a + datetime. + + Format: %Y-%m-%dT%H:%M:%S.%fZ + + :param proto_value: Protobuf dict value of an iso-string (with trailing Z) + :type proto_value: str | None + :param field: The dataclass field descriptor the value is going to + :type field: dataclasses.Field + :param parent_type: The dataclass the field belongs to + :type parent_type: object + :return: A python datetime value + :rtype: datetime.datetime | None + """ + if not proto_value: + return None + + val = dtu.any_to_datetime(proto_value, None) + if val is None: + raise ValueError(f'TimestampDictator.from_dict_value failed to convert "{proto_value!r}" to datetime') + + return val + + +class ByteDictator(BaseDictator): + @classmethod + def to_dict_value(cls, dc_value: Optional[bytes], + field: dataclasses.Field, parent: DataclassBase) -> str: + """Casts data from python bytes to base64 encoded string (bytes) + + :param dc_value: Dataclass value of python bytes + :type dc_value: bytes | None + :param field: The dataclass field descriptor the value comes from + :type field: dataclasses.Field + :param parent: The dataclass the field belongs to + :type parent: object + :return: A base64 encoded string (bytes) version of the bytes + :rtype: bytes + """ + if not dc_value: + return '' + + if not isinstance(dc_value, bytes): + raise TypeError(f'ByteDictator.to_dict_value expected bytes, got {type(dc_value)}') + + return base64.encodebytes(dc_value).decode('utf-8').strip() + + @classmethod + def from_dict_value(cls, proto_value: Optional[Union[str, bytes]], + field: dataclasses.Field, parent_type: Type[DataclassBase]) -> bytes: + """Casts a base64 encodes string of bytes into python bytes from a iso-string (with trailing Z) to a + datetime. + + :param proto_value: Protobuf dict value of a base64 endoced string + :type proto_value: str | bytes | None + :param field: The dataclass field descriptor the value is going to + :type field: dataclasses.Field + :param parent_type: The dataclass the field belongs to + :type parent_type: object + :return: A python bytes value + :rtype: bytes | None + """ + if not proto_value: + return b'' + + if isinstance(proto_value, bytes): + return proto_value + + if isinstance(proto_value, str): + return base64.decodebytes(proto_value.encode('utf-8')) + + +class EnumDictator(BaseDictator): + @classmethod + def to_dict_value(cls, dc_value: Optional[Union[int, enum.IntEnum]], + field: dataclasses.Field, parent: DataclassBase) -> int: + """Casts data from python enum type to plain int for Protobuf + + :param dc_value: Dataclass value of python enum + :type dc_value: int | enum.IntEnum | None + :param field: The dataclass field descriptor the value comes from + :type field: dataclasses.Field + :param parent: The dataclass the field belongs to + :type parent: object + :return: Integer value of the enum + :rtype: int | None + """ + if dc_value is None: + return 0 + + if isinstance(dc_value, enum.IntEnum): + return dc_value.value + + return dc_value + + @classmethod + def from_dict_value(cls, proto_value: Optional[Union[int, str]], + field: dataclasses.Field, parent_type: Type[DataclassBase]) -> enum.IntEnum: + """Casts an integer or string from a Protobuf enum into a Python enum object. + + :param proto_value: Protobuf int or string version of an enum + :type proto_value: int | str | None + :param field: The dataclass field descriptor the value is going to + :type field: dataclasses.Field + :param parent_type: The dataclass the field belongs to + :type parent_type: object + :return: A Python enum.IntEnum object + :rtype: enum.IntEnum | None + """ + if proto_value is None: + proto_value = 0 + + enum_type: enum.EnumMeta = castutils.get_dc_field_obj_type(field, parent_type) + + if isinstance(proto_value, str): + return enum_type[proto_value] + + return enum_type(proto_value) + + +class LongDictator: + @classmethod + def to_dict_value(cls, dc_value: Union[str, int], field: dataclasses.Field, parent: DataclassBase) -> str: + """ + + :param dc_value: Dataclass value + :type dc_value: Any + :param field: The dataclass field descriptor the value comes from + :type field: dataclasses.Field + :param parent: The dataclass the field belongs to + :type parent: object + :return: A value that the protobuf ParseDict function can use + :rtype: Any + """ + if dc_value is None: + return '0' + return str(dc_value) + + @classmethod + def from_dict_value(cls, proto_value: Union[str, int], + field: dataclasses.Field, parent_type: Type[DataclassBase]) -> int: + """ + + :param proto_value: Protobuf dict value + :type proto_value: Any + :param field: The dataclass field descriptor the value is going to + :type field: dataclasses.Field + :param parent_type: The dataclass the field belongs to + :type parent_type: object + :return: A value that the dataclass uses + :rtype: Any + """ + if proto_value is None: + return 0 + return int(proto_value) + + +class DurationDictator: + @classmethod + def to_dict_value(cls, dc_value: datetime.timedelta, + field: dataclasses.Field, parent: DataclassBase) -> Optional[str]: + """google.protobuf.duration + datetime.timedelta -> '123.456789s' + """ + if dc_value is None: + return None + + return f'{dc_value.total_seconds():f}s' + + @classmethod + def from_dict_value(cls, proto_value: Union[str, int, float], + field: dataclasses.Field, parent_type: Type[DataclassBase]) -> Optional[datetime.timedelta]: + """google.protobuf.duration + '123.456789s' -> datetime.timedelta + """ + if proto_value is None: + return None + + if isinstance(proto_value, str): + proto_value = proto_value.strip().lower() + if proto_value[-1] == 's': + proto_value = proto_value[:-1] + proto_value = float(proto_value) + + return datetime.timedelta(seconds=proto_value) + + +class AnyDictator: + @classmethod + def to_dict_value(cls, dc_value: Optional[DataclassBase], + field: dataclasses.Field, parent: DataclassBase) -> Optional[collections.OrderedDict]: + """google.protobuf.Any + DataclassBase -> OrderedDict([('@type', '?'), ('key_1', 'value_1'), ('key_n', 'value_n')]) + """ + if dc_value is None: + return None + + from protoplasm.casting.dictator import dataclass_to_dict + from protoplasm.casting import castutils + + base_dict = dataclass_to_dict(dc_value) + + ordered_dict = collections.OrderedDict() + ordered_dict['@type'] = castutils.get_type_url(dc_value.__proto_cls__) + + ordered_dict.update(base_dict) + return ordered_dict + + @classmethod + def from_dict_value(cls, proto_value: collections.OrderedDict, + field: dataclasses.Field, parent_type: Type[DataclassBase]) -> Optional[DataclassBase]: + """google.protobuf.Any + OrderedDict([('@type', '?'), ('key_1', 'value_1'), ('key_n', 'value_n')]) -> DataclassBase + """ + if proto_value is None or '@type' not in proto_value: + return None + + from protoplasm.casting import castutils + from protoplasm.casting.objectifier import dict_to_dataclass + + type_url = proto_value.pop('@type') + proto_class = castutils.get_proto_descriptor_by_type_url(type_url)._concrete_class # noqa + dc_cls = castutils.import_dataclass_by_proto(proto_class) + + return dict_to_dataclass(dc_cls, proto_value) diff --git a/protoplasm/casting/objectifier.py b/protoplasm/casting/objectifier.py new file mode 100644 index 0000000..4957930 --- /dev/null +++ b/protoplasm/casting/objectifier.py @@ -0,0 +1,85 @@ +__all__ = [ + 'dict_to_proto', + 'dict_to_dataclass', +] +from google.protobuf import json_format +from protoplasm.casting import dictators +from protoplasm.casting import castutils +from protoplasm.structs import * +import dataclasses + +import logging + +log = logging.getLogger(__name__) + + +def dict_to_proto(proto_class: Type[GeneratedProtocolMessageType], + dict_data: Optional[Dict] = None) -> GeneratedProtocolMessageType: + """Creates a new instance of the given Protobuf class/type and populates it + with the data given in the dict. + + :param proto_class: Protobuf class to create + :param dict_data: Python dict with the data to populate the object with + :return: A populated Protobuf object (message) + """ + proto = proto_class() + json_format.ParseDict(dict_data or {}, proto) + return proto + + +def _get_proto_field_name(field: dataclasses.Field): + return field.metadata.get('pb_name', field.name) + + +def dict_to_dataclass(dataclass_type: Type[T_DCB], dict_data: Optional[Dict] = None, ignore_extras: bool = False) -> T_DCB: + """Creates a new instance of the given dataclass type and populates it + (recursively) with the data given in the dict. + + :param dataclass_type: Dataclass type to create + :param dict_data: Python dict with the data to populate the object with + :param ignore_extras: Should dict keys that aren't fields in the target + dataclass type be ignored or should they raise + ValueErrors? + :return: A populated Dataclass object + """ + if not dict_data: + return dataclass_type() + if isinstance(dict_data, dict): + new_dict = {} + field_map = {_get_proto_field_name(f): f for f in dataclasses.fields(dataclass_type)} # type: Dict[str, dataclasses.Field] + for k, v in dict_data.items(): + field = field_map.get(k, None) + + if field: + is_obj = field.metadata.get('is_obj', False) + is_list = field.metadata.get('is_list', False) + is_map = field.metadata.get('is_map', False) + if is_obj: + field_type = castutils.get_dc_field_obj_type(field, dataclass_type) + if is_map: + new_dict[field.name] = {vk: dict_to_dataclass(field_type, vv, ignore_extras) for vk, vv in v.items()} + elif is_list: + new_dict[field.name] = [dict_to_dataclass(field_type, vv, ignore_extras) for vv in v] + else: + new_dict[field.name] = dict_to_dataclass(field_type, v, ignore_extras) + else: + dictator = field.metadata.get('dictator', dictators.BaseDictator) + if is_map: + new_dict[field.name] = {vk: dictator.from_dict_value(vv, field, dataclass_type) for vk, vv in v.items()} + elif is_list: + new_dict[field.name] = [dictator.from_dict_value(vv, field, dataclass_type) for vv in v] + else: + new_dict[field.name] = dictator.from_dict_value(v, field, dataclass_type) + elif not ignore_extras: + raise ValueError(f'Field "{field.name}" not found in dataclass "{dataclass_type}"') + return dataclass_type(**new_dict) + elif isinstance(dict_data, (list, tuple)): + new_dict = {} + for field, v in zip(dataclasses.fields(dataclass_type), dict_data): + dictator = field.metadata.get('dictator', dictators.BaseDictator) + new_dict[field.name] = dictator.from_dict_value(v, field, dataclass_type) + return dataclass_type(**new_dict) + else: + field = dataclasses.fields(dataclass_type)[0] + dictator = field.metadata.get('dictator', dictators.BaseDictator) + return dataclass_type(dictator.from_dict_value(dict_data, field, dataclass_type)) diff --git a/protoplasm/decorators/__init__.py b/protoplasm/decorators/__init__.py new file mode 100644 index 0000000..18129ff --- /dev/null +++ b/protoplasm/decorators/__init__.py @@ -0,0 +1 @@ +from .apihelpers import * diff --git a/protoplasm/decorators/apihelpers.py b/protoplasm/decorators/apihelpers.py new file mode 100644 index 0000000..5c73f7b --- /dev/null +++ b/protoplasm/decorators/apihelpers.py @@ -0,0 +1,100 @@ +__all__ = [ + 'require_params', + 'takes_context', +] + +import functools +import inspect +import typing +from protoplasm import errors +from protoplasm.casting import castutils + +import logging +log = logging.getLogger(__name__) + + +def _map_args_to_params(param_map, arg_list, kw_map): + mapped_params = {k: None for k in param_map.keys()} + if arg_list: + mapped_params.update(dict(zip(list(param_map.keys())[:len(arg_list)], arg_list))) + if kw_map: + mapped_params.update(kw_map) + return mapped_params + + +def require_params(arg_name: typing.Union[str, typing.Iterable] = '*', *more_arg_names, typecheck=False): + arg_is_func = False + func = None + if isinstance(arg_name, typing.Callable): + arg_is_func = True + func = arg_name + arg_name = '*' + + def wrap(f): + param_signature: typing.Dict[str, inspect.Parameter] = dict(inspect.signature(f).parameters) + required_params = set() + + if arg_name == '*': + key_list = list(param_signature.keys()) + if key_list and key_list[0] in ('self', 'cls'): + key_list.pop(0) + ctx_arg = getattr(f, '__takes_context_as__', None) + if ctx_arg and key_list[-1] == ctx_arg: + key_list.pop(-1) + required_params = set(key_list) + else: + arg_list = [] + if isinstance(arg_name, (list, tuple, set)): + arg_list.extend(arg_name) + else: + arg_list.append(arg_name) + if more_arg_names: + arg_list.extend(more_arg_names) + + arg_list = set(arg_list) + + for a in arg_list: + if a not in param_signature: + raise NameError(f'Required argument {a} not found in function {f!r} -> {param_signature!r}') + required_params.add(a) + + @functools.wraps(f) + def wrapper(*args, **kwargs): + given_params = _map_args_to_params(param_signature, args, kwargs) + for p in required_params: + val = given_params[p] + if val is None: + raise errors.api.InvalidArgument(f'required parameter "{p}" missing [{f!r}]') + else: + if typecheck: + if not castutils.check_type(val, param_signature[p].annotation): + raise errors.api.InvalidArgument(f'required parameter "{p}" type is {type(val)} but should be {param_signature[p].annotation} [{f!r}]') + + return f(*args, **kwargs) + return wrapper + if arg_is_func: + return wrap(func) + return wrap + + +def takes_context(context_arg_name='context', optional=True): + arg_is_func = False + func = None + if isinstance(context_arg_name, typing.Callable): + arg_is_func = True + func = context_arg_name + context_arg_name = 'context' + + def wrap(f): + param_signature: typing.Dict[str, inspect.Parameter] = dict(inspect.signature(f).parameters) + + if context_arg_name not in param_signature: + if not optional: + raise NameError(f'Context argument {context_arg_name} (non-optional) not found in function {f!r} -> {param_signature!r}') + else: + setattr(f, '__takes_context_as__', context_arg_name) + + return f + if arg_is_func: + return wrap(func) + return wrap diff --git a/protoplasm/errors/__init__.py b/protoplasm/errors/__init__.py new file mode 100644 index 0000000..ffee6dc --- /dev/null +++ b/protoplasm/errors/__init__.py @@ -0,0 +1,4 @@ +from ._base import * +from ._api import * + +from . import _api as api # This is for backwards compatability diff --git a/protoplasm/errors/_api.py b/protoplasm/errors/_api.py new file mode 100644 index 0000000..8775b2c --- /dev/null +++ b/protoplasm/errors/_api.py @@ -0,0 +1,238 @@ +__all__ = [ + 'Unknown', + 'NotFound', + 'InvalidArgument', + 'Unimplemented', + 'PermissionDenied', + 'Unauthenticated', + 'Unavailable', + 'DeadlineExceeded', + 'AlreadyExists', + 'FailedPrecondition', + 'DataLoss', + 'ResourceExhausted', + 'OutOfRange', + 'Internal', + 'Cancelled', + 'Aborted', + 'ClientApiError', + 'PreRequestError', + 'PostRequestError', +] + +import grpc +from ._base import * + + +class Unknown(ServiceApiException): + def __init__(self, details='unknown', ref_code=None, should_log=True): + """For example, this error may be returned when a Status value received + from another address space belongs to an error-space that is not known + in this address space. Also errors raised by APIs that do not return + enough error information may be converted to this error. + """ + self.status_code = grpc.StatusCode.UNKNOWN + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class NotFound(ServiceApiException): + def __init__(self, details='not found', ref_code=None, should_log=False): + """Some requested entity was not found. + """ + self.status_code = grpc.StatusCode.NOT_FOUND + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class InvalidArgument(ServiceApiException): + def __init__(self, details='invalid argument', ref_code=None, should_log=False): + """The client specified an invalid argument. + """ + self.status_code = grpc.StatusCode.INVALID_ARGUMENT + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class Unimplemented(ServiceApiException): + def __init__(self, details='unimplemented', ref_code=None, should_log=False): + """The operation is not implemented or is not supported/enabled in this + service. + """ + self.status_code = grpc.StatusCode.UNIMPLEMENTED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class PermissionDenied(ServiceApiException): + def __init__(self, details='permission denied', ref_code=None, should_log=True): + """The caller does not have permission to execute the specified + operation. PERMISSION_DENIED must not be used for rejections caused by + exhausting some resource (use RESOURCE_EXHAUSTED instead for those + errors). PERMISSION_DENIED must not be used if the caller can not be + identified (use UNAUTHENTICATED instead for those errors). This error + code does not imply the request is valid or the requested entity exists + or satisfies other pre-conditions. + """ + self.status_code = grpc.StatusCode.PERMISSION_DENIED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class Unauthenticated(ServiceApiException): + def __init__(self, details='unauthenticated', ref_code=None, should_log=False): + """The request is not authenticated but needs to be. + """ + self.status_code = grpc.StatusCode.UNAUTHENTICATED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class Unavailable(ServiceApiException): + def __init__(self, details='unavailable', ref_code=None, should_log=False): + """The service is currently unavailable. This is most likely a transient + condition, which can be corrected by retrying with a backoff. + """ + self.status_code = grpc.StatusCode.UNAVAILABLE + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class DeadlineExceeded(ServiceApiException): + def __init__(self, details='deadline exceeded', ref_code=None, should_log=False): + """The deadline expired before the operation could complete. For + operations that change the state of the system, this error may be + returned even if the operation has completed successfully. For example, + a successful response from a server could have been delayed long enough + for the deadline to expire. + """ + self.status_code = grpc.StatusCode.DEADLINE_EXCEEDED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class AlreadyExists(ServiceApiException): + def __init__(self, details='already exists', ref_code=None, should_log=False): + """The entity that a client attempted to create already exists. + """ + self.status_code = grpc.StatusCode.ALREADY_EXISTS + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class FailedPrecondition(ServiceApiException): + def __init__(self, details='failed precondition', ref_code=None, should_log=False): + """The operation was rejected because the system is not in a state + required for the operation's execution. For example, the directory to be + deleted is non-empty, an rmdir operation is applied to a non-directory, + etc. + """ + self.status_code = grpc.StatusCode.FAILED_PRECONDITION + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class DataLoss(ServiceApiException): + def __init__(self, details='data loss', ref_code=None, should_log=True): + """Unrecoverable data loss or corruption. + """ + self.status_code = grpc.StatusCode.DATA_LOSS + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class ResourceExhausted(ServiceApiException): + def __init__(self, details='resource exhausted', ref_code=None, should_log=True): + """Some resource has been exhausted, perhaps a per-user quota, or + perhaps the entire file system is out of space. + """ + self.status_code = grpc.StatusCode.RESOURCE_EXHAUSTED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class OutOfRange(ServiceApiException): + def __init__(self, details='out of range', ref_code=None, should_log=False): + """The operation was attempted past the valid range. + """ + self.status_code = grpc.StatusCode.OUT_OF_RANGE + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class Internal(ServiceApiException): + def __init__(self, details='internal', ref_code=None, should_log=True): + """Internal errors. This means that some invariants expected by the + underlying system have been broken. This error code is reserved for + serious errors. + """ + self.status_code = grpc.StatusCode.INTERNAL + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class Cancelled(ServiceApiException): + def __init__(self, details='cancelled', ref_code=None, should_log=False): + """The operation was cancelled, typically by the caller. + """ + self.status_code = grpc.StatusCode.CANCELLED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class Aborted(ServiceApiException): + def __init__(self, details='aborted', ref_code=None, should_log=False): + """The operation was aborted, typically due to a concurrency issue such + as a sequencer check failure or transaction abort. + """ + self.status_code = grpc.StatusCode.ABORTED + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class ClientApiError(ServiceApiException): + def __init__(self, details='client_error', ref_code=None, should_log=True): + """Error raised by a gRPC client when non-gRPX protoplasm errors occur. + """ + self.status_code = grpc.StatusCode.UNKNOWN + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class PreRequestError(ClientApiError): + def __init__(self, details='pre_request_error', ref_code=None, should_log=True): + """Error raised by a gRPC client when exceptions occur before the actual + gRPC request (e.g. while casting). + """ + self.status_code = grpc.StatusCode.FAILED_PRECONDITION + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class PostRequestError(ClientApiError): + def __init__(self, details='post_request_error', ref_code=None, should_log=True): + """Error raised by a gRPC client when exceptions occur AFTER the actual + gRPC request (e.g. while casting). + """ + self.status_code = grpc.StatusCode.CANCELLED + self.details = details + self.should_log = should_log + self.ref_code = ref_code diff --git a/protoplasm/errors/_base.py b/protoplasm/errors/_base.py new file mode 100644 index 0000000..daf3d38 --- /dev/null +++ b/protoplasm/errors/_base.py @@ -0,0 +1,24 @@ +__all__ = [ + 'ProtoplasmError', + 'ServiceApiException', + 'ResponseIterationTimoutError', +] + + +class ProtoplasmError(Exception): + pass + + +class ServiceApiException(ProtoplasmError): + def __init__(self, status_code=2, details='unknown', ref_code=None, should_log=False): + """Base Error/Exception raised by service API implementations that can + not return normally. + """ + self.status_code = status_code + self.details = details + self.should_log = should_log + self.ref_code = ref_code + + +class ResponseIterationTimoutError(ProtoplasmError, TimeoutError): + pass diff --git a/protoplasm/grpcserver.py b/protoplasm/grpcserver.py new file mode 100644 index 0000000..7933f6a --- /dev/null +++ b/protoplasm/grpcserver.py @@ -0,0 +1,53 @@ +__all__ = [ + 'GrpcServer', +] + +# TODO(thordurm@ccpgames.com) 2022-06-07: MOVE THIS TO A BETTER LOCATION!! + +from concurrent import futures +import grpc # noqa grpc is in the grpcio package +import time + +from protoplasm.bases import grpc_bases + +import logging +log = logging.getLogger(__name__) + + +class GrpcServer(object): + _ONE_DAY_IN_SECONDS = 60 * 60 * 24 + + def __init__(self, port='[::]:50051', max_workers=10): + """A simple gRPC server. + + Jusd add Servicer implementations (extending + `protoplasm.bases.grpc_bases.BaseGrpcServicer`) and start it up with + `serve()`. + + :param port: String listening address and port (default is `[::]:50051`) + :param max_workers: Maximum workers via `concurrent.futures.ThreadPoolExecutor` + """ + self.grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers)) + self.grpc_server.add_insecure_port(port) + log.info(f'GrpcServer created on port={port}, max_workers={max_workers}') + + def add_servicer(self, servicer: grpc_bases.BaseGrpcServicer): + """Adds a `protoplasm.bases.grpc_bases.BaseGrpcServicer` based services + for this server to run. + + :param servicer: A servicer extending `protoplasm.bases.grpc_bases.BaseGrpcServicer` + """ + servicer.add_to_server(self.grpc_server) + log.info('Servicer added: %r', servicer) + + def serve(self): + self.grpc_server.start() + log.info('Server started') + try: + while True: + log.info('Server listening...') + time.sleep(GrpcServer._ONE_DAY_IN_SECONDS) + except KeyboardInterrupt: + log.info('Server interrupted!') + self.grpc_server.stop(0) + log.info('Server Stopped!') diff --git a/protoplasm/loader/__init__.py b/protoplasm/loader/__init__.py new file mode 100644 index 0000000..3883d23 --- /dev/null +++ b/protoplasm/loader/__init__.py @@ -0,0 +1,17 @@ +__all__ = [ + 'get_dataclass_by_message_name', +] + +import typing +from google.protobuf import symbol_database +from protoplasm.bases import dataclass_bases +from protoplasm.casting import castutils + +import logging +log = logging.getLogger(__name__) + + +def get_dataclass_by_message_name(message_name: str) -> typing.Type[dataclass_bases.DataclassBase]: + sym_db = symbol_database.Default() + proto_msg = sym_db.pool.FindMessageTypeByName(message_name) + return castutils.import_dataclass_by_proto(proto_msg._concrete_class) diff --git a/protoplasm/plasm/__init__.py b/protoplasm/plasm/__init__.py new file mode 100644 index 0000000..371a48c --- /dev/null +++ b/protoplasm/plasm/__init__.py @@ -0,0 +1,7 @@ +# Alias import for use of Protoplasm.... just go "from protoplasm.plasm import *" or "from protoplasm import plasm" +from protoplasm.structs import * +from protoplasm.errors._base import * # noqa +from protoplasm.errors._api import * # noqa +from protoplasm.bases import * +from protoplasm.grpcserver import * +from protoplasm.decorators import * diff --git a/protoplasm/structs/__init__.py b/protoplasm/structs/__init__.py new file mode 100644 index 0000000..25810b7 --- /dev/null +++ b/protoplasm/structs/__init__.py @@ -0,0 +1,8 @@ +# Alias imports... + +from ._base import * +from ._methodtype import * +from ._ctx import * +from .dataclassbase import * +from ._reqestsiter import * +from ._responseiter import * diff --git a/protoplasm/structs/_base.py b/protoplasm/structs/_base.py new file mode 100644 index 0000000..b55c526 --- /dev/null +++ b/protoplasm/structs/_base.py @@ -0,0 +1,4 @@ +from typing import * +import datetime +import dataclasses +from google.protobuf.reflection import GeneratedProtocolMessageType diff --git a/protoplasm/structs/_ctx/__init__.py b/protoplasm/structs/_ctx/__init__.py new file mode 100644 index 0000000..50aa246 --- /dev/null +++ b/protoplasm/structs/_ctx/__init__.py @@ -0,0 +1,2 @@ +from ._basectx import * +from ._grpcctx import * diff --git a/protoplasm/structs/_ctx/_basectx.py b/protoplasm/structs/_ctx/_basectx.py new file mode 100644 index 0000000..393ccfa --- /dev/null +++ b/protoplasm/structs/_ctx/_basectx.py @@ -0,0 +1,15 @@ +__all__ = [ + 'BaseContext', +] + + +class BaseContext(object): + def __init__(self, wrapped_context, *args, **kwargs): + self.wrapped_context = wrapped_context + self.metadata = {} + + def get_origin_ip(self, default=None): + raise NotImplementedError('BaseContext needs to be extended to account for different invocation contexts') + + def get_meta(self, name, default=None): + raise NotImplementedError('BaseContext needs to be extended to account for different invocation contexts') diff --git a/protoplasm/structs/_ctx/_grpcctx.py b/protoplasm/structs/_ctx/_grpcctx.py new file mode 100644 index 0000000..adaef67 --- /dev/null +++ b/protoplasm/structs/_ctx/_grpcctx.py @@ -0,0 +1,53 @@ +__all__ = [ + 'GrpcContext', +] + +import re +import grpc +from ._basectx import * +from protoplasm.structs._methodtype import * + + +class GrpcContext(BaseContext): + IP_MATCHER = re.compile(r'(?:(?Pipv[46]):)?(?P(?:\[?[\da-f]{0,4}(?::[\da-f]{0,4}){2,8}]?)|(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(?P:\d{1,5})?', re.IGNORECASE) + + def __init__(self, wrapped_context: grpc.ServicerContext, method_io: MethodIoType = None): + super().__init__(wrapped_context) + self.metadata = {} + self.method_io = method_io or MethodIoType._UNKNOWN # noqa + self._load_metadata() + self._origin_ip = None + + def _load_metadata(self): + self.metadata = {} + for k, v in self.wrapped_context.invocation_metadata(): + k = k.lower() + if k not in self.metadata: + self.metadata[k] = v + else: + if not isinstance(self.metadata[k], list): + self.metadata[k] = [self.metadata[k]] + self.metadata[k].append(v) + + def get_origin_ip(self, default=None): + if not self._origin_ip: + if self._origin_ip is False: + return default + # Try x-forwarded! + self._origin_ip = self.get_meta('x-forwarded-for', None) + if not self._origin_ip: + # If not, try peer + peer = self.wrapped_context.peer() + if peer: + m = GrpcContext.IP_MATCHER.match(peer) + if m: + self._origin_ip = m.group('ip') + if not self._origin_ip: + self._origin_ip = False + return self._origin_ip + + def get_meta(self, name, default=None): + return self.metadata.get(name, default) + + def __repr__(self): + return 'GrpcContext[metadata=%r, peer=%r]' % (self.metadata, self.wrapped_context.peer()) diff --git a/protoplasm/structs/_methodtype.py b/protoplasm/structs/_methodtype.py new file mode 100644 index 0000000..c658590 --- /dev/null +++ b/protoplasm/structs/_methodtype.py @@ -0,0 +1,33 @@ +__all__ = [ + 'MethodIoType', +] + +import enum + + +class MethodIoType(enum.IntFlag): + _UNKNOWN = 0b0000 # 0 + _INPUT_UNARY = 0b0001 # 1 + _INPUT_STREAM = 0b0010 # 2 + _INPUT_MASK = 0b0011 # 3 + + _OUTPUT_UNARY = 0b0100 # 4 + _OUTPUT_STREAM = 0b1000 # 8 + _OUTPUT_MASK = 0b1100 # 12 + + UNARY_IN_UNARY_OUT = _INPUT_UNARY | _OUTPUT_UNARY # 0b0101 # 5 + UNARY_IN_STREAM_OUT = _INPUT_UNARY | _OUTPUT_STREAM # 0b0110 # 6 + STREAM_IN_UNARY_OUT = _INPUT_STREAM | _OUTPUT_UNARY # 0b1001 # 9 + STREAM_IN_STREAM_OUT = _INPUT_STREAM | _OUTPUT_STREAM # 0b1010 # 10 + + def is_valid(self) -> bool: + return self.value in (self.UNARY_IN_UNARY_OUT.value, + self.UNARY_IN_STREAM_OUT.value, + self.STREAM_IN_UNARY_OUT.value, + self.STREAM_IN_STREAM_OUT.value) + + def is_input_stream(self) -> bool: + return (self.value & self._INPUT_STREAM) == self._INPUT_STREAM + + def is_output_stream(self) -> bool: + return (self.value & self._OUTPUT_STREAM) == self._OUTPUT_STREAM diff --git a/protoplasm/structs/_reqestsiter.py b/protoplasm/structs/_reqestsiter.py new file mode 100644 index 0000000..3e72df0 --- /dev/null +++ b/protoplasm/structs/_reqestsiter.py @@ -0,0 +1,32 @@ +__all__ = [ + 'RequestIterator', +] +from ._base import * +from .dataclassbase import T_DCB +import queue + + +class RequestIterator(Generic[T_DCB]): + def __init__(self, initial_request: Optional[Union[T_DCB, List[T_DCB], Tuple[T_DCB]]] = None): + self._queue = queue.SimpleQueue() + if initial_request: + if isinstance(initial_request, (list, tuple)): + for ir in initial_request: + self.send(ir) + else: + self.send(initial_request) + self._do_cancel: bool = False + + def close(self): + self._do_cancel = True + + def send(self, request: T_DCB): + self._queue.put(request) + + def __iter__(self): + return self + + def __next__(self) -> T_DCB: + if self._do_cancel: + raise StopIteration() + return self._queue.get(block=True) diff --git a/protoplasm/structs/_responseiter.py b/protoplasm/structs/_responseiter.py new file mode 100644 index 0000000..99d0fd6 --- /dev/null +++ b/protoplasm/structs/_responseiter.py @@ -0,0 +1,172 @@ +__all__ = [ + 'IResponseIterator', + 'ResponseIterator', + 'TimeoutResponseIterator', +] +import abc +from ._base import * +from protoplasm.errors import * +from .dataclassbase import T_DCB +import threading +import grpc # noqa + +import logging +log = logging.getLogger(__name__) + +if TYPE_CHECKING: + from protoplasm.bases._remotewrap import RemoteMethodWrapper # noqa + + +class IResponseIterator(Iterable, abc.ABC): + @abc.abstractmethod + def close(self): + pass + + @abc.abstractmethod + def next(self) -> Optional[T_DCB]: + pass + + def __iter__(self): + return self + + def __next__(self) -> T_DCB: + n = self.next() + if n is None: + raise StopIteration() + return n + + +class ResponseIterator(IResponseIterator, Generic[T_DCB]): + __slots__ = ('_wrapper', '_response_iterator') + + def __init__(self, remote_wrapper: 'RemoteMethodWrapper'): + """Super simple basic Response Iterator to wrap gRPC RPC stream responses. + + Can be used as an iterator directly in a loop or by calling `next()` + directly to get the next iteration from the result stream. + + Note that `next()` will simply block until it receives a value to return + from the stream (unless the stream has been closed in which case + `next()` returns None). + """ + self._wrapper = remote_wrapper + self._response_iterator = iter(remote_wrapper.iterable_results) + + def close(self): + """Cancels the RPC call mid-stream. + """ + self._wrapper.close() + + def next(self) -> Optional[T_DCB]: + if self._wrapper.is_closed: + return None + return next(self._response_iterator) + + def with_timeout(self, timeout_seconds: Optional[float] = None, + raise_timeout: Optional[bool] = None) -> 'TimeoutResponseIterator': + return TimeoutResponseIterator(self, timeout_seconds, raise_timeout) + + def one_and_done(self, timeout_seconds: Optional[float] = 0, + raise_timeout: Optional[bool] = None) -> Optional[T_DCB]: + """Waits for a single iteration from the stream to return and then closes the stream. + + Note the default timeout of 0, meaning this will wait forever by default! + """ + return self.with_timeout(timeout_seconds, raise_timeout).one_and_done() + + +class TimeoutResponseIterator(IResponseIterator, Generic[T_DCB]): + __slots__ = ('_response_iterator', '_timeout_seconds', '_timeout_thread', '_next', + '_exception', '_timed_out', '_raise_timeout') + + def __init__(self, response_iterator: ResponseIterator[T_DCB], + timeout_seconds: float = 60.0, raise_timeout: bool = True): + """Response Iterator wrapper with builtin timeout (deadline) + while waiting for next stream iteration. + + Can be used as an iterator directly in a loop or by calling `next()` + directly to get the next iteration from the result stream. + + Note that `next()` will block until it receives a value to return + from the stream OR it times out. + + If raise_timeout is True then a ResponseIterationTimoutError will be + raised in case of a timeout but if not, then the iteration loop will + simply end if this is used as such (StopIteration is raised) or the + timed-out `next()` call returns None. + """ + self._response_iterator: ResponseIterator = response_iterator + self._timeout_seconds = timeout_seconds + if self._timeout_seconds is None: + self._timeout_seconds = 60.0 + self._timeout_thread: Optional[threading.Thread] = None + self._next: Optional[T_DCB] = None + self._exception: Optional[Exception] = None + self._timed_out: bool = False + self._raise_timeout = raise_timeout + if self._raise_timeout is None: + self._raise_timeout = True + + def next(self, timeout_seconds: Optional[float] = None, raise_timeout: Optional[bool] = None) -> Optional[T_DCB]: + """The `timeout_seconds` and `raise_timeout` parameters are optional + and override the wrappers default ones given when it was initialized. + + Returns None if `raise_timeout` is False. + """ + self._next = None + self._exception = None + self._timed_out = False + if timeout_seconds is None: + timeout_seconds = self._timeout_seconds + if raise_timeout is None: + raise_timeout = self._raise_timeout + if not timeout_seconds: # Just wait forever! :D + return self._response_iterator.next() + + self._timeout_thread = threading.Thread(target=self._wait_for_next) + self._timeout_thread.start() + self._timeout_thread.join(timeout_seconds) + if self._timeout_thread.is_alive(): + log.info('thread is still alive... closing!') + self._timed_out = True + self.close() + if raise_timeout: + raise ResponseIterationTimoutError(f'timout while waiting for next response from' + f' {self._response_iterator._wrapper.method_name}') # noqa + else: + log.info(f'stopping iteration after timeout') + return None + + if self._exception: # Something bad happened! + raise self._exception + + return self._next # noqa + + def _wait_for_next(self): + log.info('_wait_for_next started...') + try: + self._next = self._response_iterator.next() + log.info('_next happened!!') + except grpc.RpcError as ex: + from protoplasm.bases._exwrap import GrpcExceptionWrapper # noqa + wrex = GrpcExceptionWrapper(ex) + if wrex.code == grpc.StatusCode.CANCELLED and self._timed_out: + log.info(f'Self inflicted timeout in thread') + return # this means we timed out and cancelled this ourselves! + log.exception(f'RpcError in thread: {ex!r}') + self._exception = wrex.get_reraise() + except Exception as ex: + log.exception(f'Exception in thread: {ex!r}') + self._exception = ex + log.info('_wait_for_next ended!!') + + def close(self): + self._response_iterator.close() + + def one_and_done(self, timeout_seconds: Optional[float] = None, + raise_timeout: Optional[bool] = None) -> Optional[T_DCB]: + """Waits for a single iteration from the stream to return and then closes the stream. + """ + the_one = self.next(timeout_seconds, raise_timeout) + self.close() + return the_one diff --git a/protoplasm/structs/dataclassbase.py b/protoplasm/structs/dataclassbase.py new file mode 100644 index 0000000..7e17118 --- /dev/null +++ b/protoplasm/structs/dataclassbase.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +__all__ = [ + 'DataclassBase', + 'T_DCB', # Required...?!? +] + +import json +import base64 + +from protoplasm.structs._base import * +import dataclasses + +import logging +log = logging.getLogger(__name__) + +T_DCB = TypeVar('T_DCB', bound='DataclassBase') + + +@dataclasses.dataclass +class DataclassBase(Generic[T_DCB]): + __proto_cls__: ClassVar[GeneratedProtocolMessageType] + + def __post_init__(self): + from protoplasm.casting import castutils + for f in dataclasses.fields(self): + if f.metadata.get('is_obj', False): + f_val = getattr(self, f.name, None) + if f_val: + f_type = castutils.get_dc_field_obj_type(f, self) + if f.metadata.get('is_map', False): + for k in f_val.keys(): + f_val[k] = castutils.force_object(f_type, f_val[k]) + elif f.metadata.get('is_list', False): + for i in range(len(f_val)): + f_val[i] = castutils.force_object(f_type, f_val[i]) + else: + setattr(self, f.name, castutils.force_object(f_type, f_val)) + + def to_dict(self) -> Dict: + """Turns this `DataclassBase` into a dict of its fields and values + recursively and returns, so for any field value that is itself a + `DataclassBase`, `to_dict` is also called, resulting in a nested dict of + only "primitive" (and JSON serializable) values. + """ + from protoplasm import casting + return casting.dataclass_to_dict(self) + + def to_json(self, indent: Optional[int] = None) -> str: + return json.dumps(self.to_dict(), indent=indent) + + def to_proto(self) -> GeneratedProtocolMessageType: + from protoplasm import casting + return casting.dataclass_to_proto(self) + + def to_protostr(self) -> bytes: + from protoplasm import casting + return casting.dataclass_to_proto(self).SerializeToString() + + def to_base64(self) -> bytes: + return base64.encodebytes(self.to_protostr()).strip().replace(b'\r', + b'').replace(b'\n', b'').replace(b'\t', b'') + + def unpack(self) -> Dict[str, Any]: + """Returns a dict with the fields of this `DataclassBase` as keys and + their values as they are. + + The main difference between this and the `to_dict` method is that + `to_dict` is recursive so on any field value that is itself a + `DataclassBase`, `to_dict` is also called, resulting in a nested dict of + only "primitive" (and JSON serializable) values. + + This `unpack` method on the other hand will leave any `DataclassBase` + values be. + + The main use-case for this method is to "unpack"/"explode"/"break up" a + gRPC Proto Service Request/Response message up into individual method + arguments. + """ + d = {} + for field in dataclasses.fields(self): + d[field.name] = getattr(self, field.name) + return d + + def explode(self) -> Optional[Union[Tuple[Any], Any]]: + """Returns a tuple with the field values of this `DataclassBase` or if + this `DataclassBase` only contains one field, returns that field's value + alone. + + This is similar to the `unpack()` method except this only returns values + and if there's only a single field (as many RPC Response messages do), + it's not wrapped in a tuple. + + The main use-case for this method is to "unpack" a gRPC Proto Service + RPC Unary Response message up into individual return values (often just + one). + """ + tu = [] + for field in dataclasses.fields(self): + tu.append(getattr(self, field.name)) + if not tu: + return # We can theoretically have "empty" Response message classes + if len(tu) == 1: + return tu[0] + return tuple(tu) + + @classmethod + def from_kwdict(cls, **kwargs) -> T_DCB: + from protoplasm.casting import castutils + return cls.from_dict(castutils.kwdict(**kwargs)) + + @classmethod + def from_dict(cls, dict_data: dict, ignore_extras: bool = False) -> T_DCB: + from protoplasm import casting + return casting.dict_to_dataclass(cls, dict_data, ignore_extras) + + @classmethod + def from_json(cls, json_string: str) -> T_DCB: + return cls.from_dict(json.loads(json_string)) + + @classmethod + def from_proto(cls, proto: GeneratedProtocolMessageType) -> T_DCB: + from protoplasm import casting + return cls.from_dict(casting.proto_to_dict(proto)) + + @classmethod + def from_protostr(cls, protostr: bytes) -> T_DCB: + proto = cls.__proto_cls__() + proto.ParseFromString(protostr) + return cls.from_proto(proto) + + @classmethod + def from_base64(cls, base64str: Union[bytes, str]) -> T_DCB: + from protoplasm.casting import castutils + return cls.from_protostr(castutils.fuzzy_base64_to_bytes(base64str)) + + @classmethod + def from_clone(cls, clone: DataclassBase) -> T_DCB: + return cls.from_dict(clone.to_dict(), ignore_extras=True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1d26c44 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = [ "setuptools>=42", "wheel" ] +build-backend = "setuptools.build_meta" + +[project] +name = "protoplasm" +dynamic = ["version"] +description = "Utilities for working with Protobuf & gRPC in Python." +readme = { file = "README.md", content-type = "text/markdown" } +license = { file = "LICENSE" } +authors = [ + { name = "Thordur Matthiasson", email = "thordurm@ccpgames.com" }, + { name = "Daniel Maxson", email = "dmaxson@ccpgames.com" }, + { name = "John Aldis", email = "johnaldis@ccpgames.com" } +] +keywords = [ "protobuf", "proto", "dataclasses", "tools", "ccp", "utils" ] +classifiers = [ + "Development Status :: 5 - Production/Stable", + + "License :: OSI Approved :: MIT License", + + "Intended Audience :: Developers", + + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities" +] + +[project.urls] +Homepage = "https://github.com/ccpgames/protoplasm" +Documentation = "https://github.com/ccpgames/protoplasm/blob/main/README.md" +Repository = "https://github.com/ccpgames/protoplasm.git" +Issues = "https://github.com/ccpgames/protoplasm/issues" +Changelog = "https://github.com/ccpgames/protoplasm/blob/main/CHANGELOG.md" + +[tool.setuptools.dynamic] +version = {attr = "protoplasm.__version__"} + +[tool.setuptools.packages.find] +where = [ "." ] +exclude = [ "tests", "tests.*" ] \ No newline at end of file diff --git a/requirements-unittesting.txt b/requirements-unittesting.txt new file mode 100644 index 0000000..f9a9d8d --- /dev/null +++ b/requirements-unittesting.txt @@ -0,0 +1 @@ +neobuilder >=5, <6 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f874001 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +ccptools >= 1.1, <2 + +protobuf >=4.25.3, <5 +grpcio >=1.62.1, <2 +grpcio-tools >=1.62.1, <2 +googleapis-common-protos >=1.63.0, <2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..81f4db7 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +import setuptools + + +if __name__ == '__main__': + setuptools.setup() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/_sandbox/__init__.py b/tests/_sandbox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/_sandbox/_plasm_stream_client.py b/tests/_sandbox/_plasm_stream_client.py new file mode 100644 index 0000000..4439e45 --- /dev/null +++ b/tests/_sandbox/_plasm_stream_client.py @@ -0,0 +1,55 @@ +from protoplasm.plasm import * + +from sandbox.test import river_grpc_sender +from sandbox.test.river_dc import GuessTheNumberRequest, MarcoPoloRequest +import time +import random + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level='DEBUG') + + +def _guess_a_lot() -> Iterable[GuessTheNumberRequest]: + for i in range(35, 45): + r = GuessTheNumberRequest(number=i) + log.info(f'yielding {r}') + yield r + time.sleep(2) + + +def _get_a_bunch_of_marcos() -> Iterable[MarcoPoloRequest]: + for i in range(0, 100): + r = MarcoPoloRequest(question=f'Marco! #{i}') + log.info(f'yielding {r}') + yield r + time.sleep(random.randint(1, 5)) + + +if __name__ == '__main__': + srv = river_grpc_sender.StreamingService('localhost:50051') + ret = srv.reverse_my_shit('this is not an anagram') + print(ret) + + answer = srv.guess_the_number(_guess_a_lot()) + print(answer) + + ret = srv.reverse_my_shit('this is not an anagram') + print(ret) + + for response in srv.tell_me_a_story('Three Little Pigs'): + print(response.line) + + ret = srv.reverse_my_shit('this is not an anagram') + print(ret) + + i = 1 + req_iter: RequestIterator[MarcoPoloRequest] = RequestIterator(MarcoPoloRequest(question=f'Marco! #{i}')) + + for response in srv.marco_polo(req_iter): + print(response.answer) + i += 1 + req_iter.send(MarcoPoloRequest(question=f'Marco! #{i}')) + + ret = srv.reverse_my_shit('this is not an anagram') + print(ret) diff --git a/tests/_sandbox/_plasm_stream_server.py b/tests/_sandbox/_plasm_stream_server.py new file mode 100644 index 0000000..6b2c051 --- /dev/null +++ b/tests/_sandbox/_plasm_stream_server.py @@ -0,0 +1,89 @@ +import sandbox +from sandbox.test import river_dc as dc +from sandbox.test.river_dc import * +from sandbox.test.river_api import StreamingServiceInterface +from sandbox.test.river_grpc_receiver import StreamingServiceGrpcServicer + +from protoplasm.plasm import * + +import random + +import time +import logging +logging.basicConfig(level='DEBUG') +log = logging.getLogger(__name__) + +sandbox.load_symbols() # This is the old "from sandbox import __everything__" + + +class PlasmStreamingService(StreamingServiceInterface): + def reverse_my_shit(self, shit: str = None) -> str: + log.info(f'PlasmStreamingService.reverse_my_shit(shit={shit})') + return shit[::-1] + + def marco_polo(self, request_iterator: Iterable[dc.MarcoPoloRequest]) -> Iterable[MarcoPoloResponse]: + log.info(f'PlasmStreamingService.marco_polo(story={request_iterator})') + i = 0 + for request in request_iterator: + log.info(f'PlasmStreamingService.marco_polo()...request={request}') + if request.question.lower().startswith('marco'): + response = MarcoPoloResponse(answer=f'Polo! #{i}') + log.info(f'PlasmStreamingService.marco_polo()...{response}') + i += 1 + yield response + s = random.randint(1, 5) + log.info(f'PlasmStreamingService.marco_polo()... sleeping for {s}') + + if i >= 14: + log.info(f'PlasmStreamingService.marco_polo()... Im tired!!!') + break + time.sleep(s) + else: + raise PostRequestError('that was not marco') + + log.info(f'PlasmStreamingService.marco_polo()... DONE!') + + def tell_me_a_story(self, story: str = None) -> Iterable[dc.TellMeAStoryResponse]: + log.info(f'PlasmStreamingService.tell_me_a_story(story={story})') + i = 0 + while i < 10: + response = dc.TellMeAStoryResponse(line=f'This is line #{i} of the "{story}" story') + log.info(f'PlasmStreamingService.tell_me_a_story()...{response}') + i += 1 + yield response + # yield f'This is line #{i} of the "{story}" story' + log.info(f'PlasmStreamingService.tell_me_a_story()... sleeping') + time.sleep(3) + + log.info(f'PlasmStreamingService.tell_me_a_story()... DONE!') + + def guess_the_number(self, request_iterator: Iterable[dc.GuessTheNumberRequest]) -> str: + log.info(f'PlasmStreamingService.guess_the_number({request_iterator})') + + for request in request_iterator: + log.info(f'PlasmStreamingService.guess_the_number()...request={request}') + if request.number == 42: + log.info(f'PlasmStreamingService.guess_the_number()...CORRECT!!!!') + return 'YOU WON!!!' + + else: + log.info(f'PlasmStreamingService.guess_the_number()...WRONG!') + + log.info(f'PlasmStreamingService.guess_the_number()... DONE!') + + +def forward_stuff(impl, stuff): + return impl.tell_me_a_story(stuff) + + +def main(): + server = GrpcServer(port='[::]:50051', max_workers=10) + server.add_servicer(StreamingServiceGrpcServicer(PlasmStreamingService())) + server.serve() + # impl = PlasmStreamingService() + # for r in forward_stuff(impl, 'Odyssey'): + # print(r) + + +if __name__ == '__main__': + main() diff --git a/tests/_sandbox/_raw_stream_client.py b/tests/_sandbox/_raw_stream_client.py new file mode 100644 index 0000000..4a4bfa7 --- /dev/null +++ b/tests/_sandbox/_raw_stream_client.py @@ -0,0 +1,131 @@ +from sandbox.test import river_pb2 +from sandbox.test import river_pb2_grpc +import grpc +from concurrent import futures +from typing import * +import asyncio +import time +import random +from grpc._channel import _MultiThreadedRendezvous + + +import logging +log = logging.getLogger(__name__) + +logging.basicConfig(level='DEBUG') + +_I_AM_WAITING = False + + +def _guess_a_lot() -> Iterable[river_pb2.GuessTheNumberRequest]: + for i in range(35, 45): + r = river_pb2.GuessTheNumberRequest(number=i) + log.info(f'yielding {r}') + yield r + time.sleep(2) + + +def guess(): + log.info(f'Opening channel...') + with grpc.insecure_channel('localhost:50051') as channel: + stub = river_pb2_grpc.StreamingServiceStub(channel) + results: river_pb2.GuessTheNumberResponse = stub.GuessTheNumber( + _guess_a_lot() + ) + log.info(f'results={results}') + log.info(f'results={results.did_i_win}') + log.info(f'Done!') + + +def reverse_shit(): # Direct Request/Response + log.info(f'Opening channel...') + with grpc.insecure_channel('localhost:50051') as channel: + stub = river_pb2_grpc.StreamingServiceStub(channel) + results: river_pb2.ReverseMyShitResponse = stub.ReverseMyShit(river_pb2.ReverseMyShitRequest( + shit='this is not an anagram' + )) + log.info(f'results={results}') + log.info(f'results={results.tihs}') + + log.info(f'Done!') + + +def story(): # Direct Request, Streaming Response + log.info(f'Opening channel...') + with grpc.insecure_channel('localhost:50051') as channel: + stub = river_pb2_grpc.StreamingServiceStub(channel) + results: Iterable[river_pb2.TellMeAStoryResponse] = stub.TellMeAStory( + river_pb2.TellMeAStoryRequest(story='odyssey') + ) + # TODO(thordurm@ccpgames.com) 2022-06-02: How should we "hang up" from the Client side? + # TODO(thordurm@ccpgames.com) 2022-06-02: How should we handle hang-ups from the server side (and how do they manifest)? + for response in results: + log.info(f'response={response}') + log.info(f'Done!') + + +def _get_a_bunch_of_marcos() -> Iterable[river_pb2.MarcoPoloRequest]: + for i in range(0, 100): + r = river_pb2.MarcoPoloRequest(question=f'Marco! #{i}') + log.info(f'yielding {r}') + yield r + time.sleep(random.randint(1, 5)) + + +def marco(): # Dual-side streaming! + log.info(f'Opening channel...') + with grpc.insecure_channel('localhost:50051') as channel: + + marco_iterator: Iterable[river_pb2.MarcoPoloRequest] = _get_a_bunch_of_marcos() + + stub = river_pb2_grpc.StreamingServiceStub(channel) + result_iterator: Iterable[river_pb2.MarcoPoloResponse] = stub.MarcoPolo( + marco_iterator + ) + + for response in result_iterator: + log.info(f'response={response}') + + log.info(f'Done!') + + + +async def _do_marco(stub): + async for response in stub.MarcoPolo(_get_a_bunch_of_marcos()): + yield response + + +async def aio_marco(): # Dual-side streaming! + global _I_AM_WAITING + log.info(f'Opening channel...') + async with grpc.aio.insecure_channel('localhost:50051') as channel: + stub = river_pb2_grpc.StreamingServiceStub(channel) + log.info(f'foo') + async for response in _do_marco(stub): + log.info(f'response={response}') + _I_AM_WAITING = False + log.info(f'Done!') + + +def main(): + log.info(f'Beginning...') + marco() + # guess() + reverse_shit() + # try: + # story() + # except Exception as ex: + # log.exception(f'Damnit! {ex!r}') + # reverse_shit() + log.info(f'Done...') + + +async def aio_main(): + log.info(f'Beginning...') + await aio_marco() + + +if __name__ == '__main__': + main() + # asyncio.get_event_loop().run_until_complete(aio_main()) + diff --git a/tests/_sandbox/_raw_stream_service.py b/tests/_sandbox/_raw_stream_service.py new file mode 100644 index 0000000..b65702b --- /dev/null +++ b/tests/_sandbox/_raw_stream_service.py @@ -0,0 +1,99 @@ +from sandbox.test import river_pb2 +from sandbox.test import river_pb2_grpc +import grpc +from concurrent import futures +from typing import * +import time +import random + +import logging +log = logging.getLogger(__name__) + +logging.basicConfig(level='DEBUG') + + +class RiverServicer(river_pb2_grpc.StreamingServiceServicer): + + def ReverseMyShit(self, + request: river_pb2.ReverseMyShitRequest, + context: grpc.ServicerContext) -> river_pb2.ReverseMyShitResponse: + log.info(f'RiverServicer.ReverseMyShit({request}, {context})') + log.info(f'RiverServicer.ReverseMyShit(request.shit={request.shit})') + + return river_pb2.ReverseMyShitResponse(tihs=request.shit[::-1]) + + def MarcoPolo(self, + request_iterator: Iterable[river_pb2.MarcoPoloRequest], + context: grpc.ServicerContext) -> Iterable[river_pb2.MarcoPoloResponse]: + log.info(f'RiverServicer.MarcoPolo({request_iterator}, {context})') + i = 0 + for request in request_iterator: + log.info(f'RiverServicer.MarcoPolo()...request={request}') + if request.question.lower().startswith('marco'): + response = river_pb2.MarcoPoloResponse(answer=f'Polo! #{i}') + log.info(f'RiverServicer.MarcoPolo()...{response}') + i += 1 + yield response + s = random.randint(1, 5) + log.info(f'RiverServicer.MarcoPolo()... sleeping for {s}') + + if i >= 14: + log.info(f'RiverServicer.MarcoPolo()... Im tired!!!') + break + time.sleep(s) + else: + raise ValueError('that was not marco') + + log.info(f'RiverServicer.MarcoPolo()... DONE!') + + def TellMeAStory(self, + request: river_pb2.TellMeAStoryRequest, + context: grpc.ServicerContext) -> Iterable[river_pb2.TellMeAStoryResponse]: + log.info(f'RiverServicer.TellMeAStory({request}, {context})') + i = 0 + while i < 10: + response = river_pb2.TellMeAStoryResponse(line=f'This is line #{i}') + log.info(f'RiverServicer.TellMeAStory()...{response}') + i += 1 + yield response + log.info(f'RiverServicer.TellMeAStory()... sleeping') + time.sleep(3) + + log.info(f'RiverServicer.TellMeAStory()... DONE!') + + def GuessTheNumber(self, + request_iterator: Iterable[river_pb2.GuessTheNumberRequest], + context: grpc.ServicerContext) -> river_pb2.GuessTheNumberResponse: + log.info(f'RiverServicer.GuessTheNumber({request_iterator}, {context})') + + for request in request_iterator: + log.info(f'RiverServicer.GuessTheNumber()...request={request}') + if request.number == 42: + response = river_pb2.GuessTheNumberResponse(did_i_win=f'YOU WON!!!') + log.info(f'RiverServicer.GuessTheNumber()...CORRECT!!!! {response}') + return response + + else: + log.info(f'RiverServicer.GuessTheNumber()...WRONG!') + + log.info(f'RiverServicer.GuessTheNumber()... DONE!') + + +def serve(): + log.info(f'Beginning...') + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + river_pb2_grpc.add_StreamingServiceServicer_to_server(RiverServicer(), server) + server.add_insecure_port('[::]:50051') + log.info(f'Starting...') + server.start() + log.info(f'Started! Waiting...') + server.wait_for_termination() + + +def main(): + serve() + + +if __name__ == '__main__': + main() + diff --git a/tests/_sandbox/_sandboxclient.py b/tests/_sandbox/_sandboxclient.py new file mode 100644 index 0000000..abd92d1 --- /dev/null +++ b/tests/_sandbox/_sandboxclient.py @@ -0,0 +1,9 @@ +from sandbox.test import service_grpc_sender + +if __name__ == '__main__': + srv = service_grpc_sender.SimpleService('localhost:50051') + ret = srv.hello('darkeness my old friend') + print(ret) + + ret2 = srv.nested_hello() + print(ret2) \ No newline at end of file diff --git a/tests/plain/__init__.py b/tests/plain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/plain/test_apihelpers.py b/tests/plain/test_apihelpers.py new file mode 100644 index 0000000..ab979f3 --- /dev/null +++ b/tests/plain/test_apihelpers.py @@ -0,0 +1,997 @@ +import unittest +import datetime +import typing + +import protoplasm.casting.castutils +from protoplasm.decorators import apihelpers +from protoplasm import errors + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + + +class Foo(object): + def __init__(self, alpha=''): + self.alpha = alpha + + +class Bar(object): + def __init__(self, beta=''): + self.beta = beta + + +class _Values: + my_none = None + my_int = 1 + my_int_empty = 0 + my_float = 1.1 + my_float_empty = 0.0 + my_str = 'a string' + my_str_empty = '' + my_bytes = b'byte' + my_bytes_empty = b'' + + my_foo = Foo('foo') + my_bar = Bar('bar') + + my_list = [1, 2, 3] + my_list_empty = [] + my_list_of_strings = ['one', 'two', 'three'] + my_list_of_foos = [Foo('one'), Foo('two'), Foo('three')] + my_list_of_bars = [Bar('eno'), Bar('owt'), Bar('eerht')] + my_list_of_foobars = [Bar('eno'), Foo('two'), Bar('eerht')] + + my_tuple = (4, 5, 6) + my_tuple_empty = tuple() + my_tuple_of_strings = ('four', 'five', 'six') + my_tuple_of_int_str_byte = (4, 'five', b'three') + my_tuple_of_2_str = ('first', 'second') + my_tuple_of_str_foo = ('first', Foo('second')) + my_tuple_of_str_bar = ('first', Bar('second')) + + my_set = {7, 8, 9} + my_set_empty = set() + my_set_of_strings = {'seven', 'eight', 'nine'} + my_set_of_ints_and_str = {'seven', 8, 'nine'} + my_set_of_foos = {Foo('seven'), Foo('8'), Foo('nine')} + my_set_of_bars = {Bar('seven'), Bar('8'), Bar('nine')} + my_set_of_foobars = {Foo('seven'), Bar('8'), Foo('nine')} + + my_dict = {10: 11, 12: 13} + my_dict_empty = {} + my_dict_intstr_optional_foobar = { + 1: None, + 'two': Foo('dos'), + 3: Bar('three'), + } + my_dict_of_str_datetimes = {'ten': datetime.datetime(2019, 1, 10, 11, 10, 13), + 'twelve': datetime.datetime(2019, 1, 13, 14, 15, 16)} + + my_datetime = datetime.datetime(2001, 1, 1, 1, 1, 1) + + +class _Types: + type_none = type(None) + type_int = type(1) + type_float = type(1.1) + type_str = type('a string') + type_bytes = type(b'byte') + + type_foo = type(Foo('foo')) + type_bar = type(Bar('bar')) + + type_list = type([1, 2, 3]) + type_tuple = type((4, 5, 6)) + type_set = type({7, 8, 9}) + type_dict = type({10: 11, 12: 13}) + + type_datetime = type(datetime.datetime(2001, 1, 1, 1, 1, 1)) + + +class _Anotations: + a_string_optional = typing.Optional[str] + a_byte_or_string_optional = typing.Optional[typing.Union[str, bytes]] + + a_foo = Foo + a_foobar_union = typing.Union[Foo, Bar] + a_foobar_str_union = typing.Union[Foo, Bar, str] + a_foo_optional = typing.Optional[Foo] + a_foobar_optional_union = typing.Optional[typing.Union[Foo, Bar]] + a_foobar_str_optional_union = typing.Optional[typing.Union[Foo, Bar, str]] + a_foobar_str_optional_union_of_unions = typing.Union[typing.Union[Foo, Bar], typing.Optional[str]] + + a_int_or_float = typing.Union[int, float] + a_int_or_float_or_string = typing.Union[int, float, str] + a_int_or_datetime = typing.Union[int, datetime.datetime] + + a_list = list + a_list_no_param = typing.List + a_list_of_any = typing.List[typing.Any] + a_list_of_ints = typing.List[int] + a_list_of_strings = typing.List[str] + a_list_of_string_or_ints = typing.List[typing.Union[int, str]] + a_list_of_foos = typing.List[Foo] + a_list_of_bars = typing.List[Bar] + a_list_of_foobars = typing.List[typing.Union[Foo, Bar]] + + a_tuple = tuple + a_tuple_no_param = typing.Tuple + a_tuple_of_any = typing.Tuple[typing.Any, ...] # Is this (x,) or (x, y, ...)??? + a_tuple_of_strings = typing.Tuple[str, ...] + a_tuple_of_2_strings = typing.Tuple[str, str] + a_tuple_3_of_int_str_byte = typing.Tuple[int, str, bytes] + a_tuple_of_str_and_foobars = typing.Tuple[str, typing.Union[Foo, Bar]] + + a_set = set + a_set_no_param = typing.Set + a_set_of_any = typing.Set[typing.Any] + a_set_of_strings = typing.Set[str] + a_set_of_ints = typing.Set[int] + a_set_of_ints_or_str = typing.Set[typing.Union[int, str]] + a_set_of_foobars = typing.Set[typing.Union[Foo, Bar]] + + a_dict = dict + a_dict_no_param = typing.Dict + a_dict_of_any = typing.Dict[typing.Any, typing.Any] + a_dict_of_int_ints = typing.Dict[int, int] + a_dict_of_intstr_optional_foobars = typing.Dict[typing.Union[int, str], typing.Optional[typing.Union[Foo, Bar]]] + a_dict_of_str_datetimes = typing.Dict[str, datetime.datetime] + + +class TypeCheckerTest(unittest.TestCase): + def test_simple_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_int)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_int, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_bytes)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, bytes)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Types.type_datetime)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_float)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_float, float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_bytes)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, bytes)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_float, _Types.type_datetime)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_str)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_str, str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_bytes)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, bytes)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Types.type_datetime)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_bytes)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_bytes, bytes)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, str)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bytes, _Types.type_datetime)) + + def test_none_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_none)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_str)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_bytes)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Types.type_datetime)) + + def test_obj_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_foo, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, _Types.type_bar)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_bar, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bar, _Types.type_foo)) + + def test_datetime_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_datetime, _Types.type_datetime)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_datetime, _Types.type_int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_datetime, _Types.type_bar)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_datetime, _Types.type_tuple)) + + def test_list_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, _Types.type_list)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, list)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, _Types.type_list)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, list)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, _Types.type_list)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, _Types.type_bar)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple, _Types.type_tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple, tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, _Types.type_tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, _Types.type_tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, _Types.type_tuple)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, tuple)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, _Types.type_bar)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, _Types.type_set)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, set)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, _Types.type_set)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, set)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, _Types.type_set)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, set)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, dict)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, _Types.type_bar)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict, _Types.type_dict)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict, dict)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, _Types.type_dict)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, dict)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, _Types.type_dict)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, dict)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, list)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, set)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, tuple)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, int)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, _Types.type_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, _Types.type_bar)) + + def test_simple_union_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_int_or_float)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_float, _Anotations.a_int_or_float)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_int_or_float)) + + def test_optional_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_string_optional)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_string_optional)) + self.assertTrue(protoplasm.casting.castutils.check_type(None, _Anotations.a_string_optional)) + self.assertTrue(protoplasm.casting.castutils.check_type('', _Anotations.a_string_optional)) + self.assertTrue(protoplasm.casting.castutils.check_type('Foo', _Anotations.a_string_optional)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_string_optional)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_byte_or_string_optional)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_byte_or_string_optional)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_bytes, _Anotations.a_byte_or_string_optional)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_bytes_empty, _Anotations.a_byte_or_string_optional)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_str_empty, _Anotations.a_byte_or_string_optional)) + self.assertFalse( + protoplasm.casting.castutils.check_type(_Values.my_datetime, _Anotations.a_byte_or_string_optional)) + + def test_objects_type_check(self): + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foo)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foo)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foobar_union)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foobar_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foobar_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foobar_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foobar_union)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foobar_str_union)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foobar_str_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foobar_str_union)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foobar_str_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foobar_str_union)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foo_optional)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foo_optional)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foo_optional)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foo_optional)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foo_optional)) + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foobar_optional_union)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foobar_optional_union)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foobar_optional_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foobar_optional_union)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foobar_optional_union)) + + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foobar_str_optional_union)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foobar_str_optional_union)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foobar_str_optional_union)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foobar_str_optional_union)) + self.assertFalse( + protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foobar_str_optional_union)) + + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_foo, _Anotations.a_foobar_str_optional_union_of_unions)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_bar, _Anotations.a_foobar_str_optional_union_of_unions)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_none, _Anotations.a_foobar_str_optional_union_of_unions)) + self.assertTrue( + protoplasm.casting.castutils.check_type(_Values.my_str, _Anotations.a_foobar_str_optional_union_of_unions)) + self.assertFalse( + protoplasm.casting.castutils.check_type(_Values.my_int, _Anotations.a_foobar_str_optional_union_of_unions)) + + def test_lists_type_check(self): + + anote = _Anotations.a_list + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_no_param + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_any + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_ints + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_strings + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_string_or_ints + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_foos + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_bars + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_list_of_foobars + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_list_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + def test_tuples_type_check(self): + anote = _Anotations.a_tuple + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_tuple_no_param + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_tuple_of_any + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_tuple_of_strings + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_tuple_of_2_strings + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_tuple_3_of_int_str_byte + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_tuple_of_str_and_foobars + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_int_str_byte, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_2_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_foo, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_tuple_of_str_bar, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + def test_sets_type_check(self): + anote = _Anotations.a_set + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_set_no_param + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_set_of_any + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_set_of_strings + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_set_of_ints + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_set_of_ints_or_str + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + anote = _Anotations.a_set_of_foobars + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_strings, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set_of_ints_and_str, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foos, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_bars, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_set_of_foobars, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + + def test_dicts_type_check(self): + anote = _Anotations.a_dict + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_intstr_optional_foobar, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + + anote = _Anotations.a_dict_no_param + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_intstr_optional_foobar, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + + anote = _Anotations.a_dict_of_any + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_intstr_optional_foobar, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + + anote = _Anotations.a_dict_of_int_ints + + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict_intstr_optional_foobar, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + + anote = _Anotations.a_dict_of_intstr_optional_foobars + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_intstr_optional_foobar, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + + anote = _Anotations.a_dict_of_str_datetimes + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_empty, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_dict_intstr_optional_foobar, anote)) + self.assertTrue(protoplasm.casting.castutils.check_type(_Values.my_dict_of_str_datetimes, anote)) + + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_foo, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_str, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_list, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_tuple, anote)) + self.assertFalse(protoplasm.casting.castutils.check_type(_Values.my_set, anote)) + + +class _TestMethods(object): + @apihelpers.require_params + def need_all(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params(typecheck=True) + def need_all_typed(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params('x', 'y', 'z') + def need_three(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params('x', 'y', 'z', typecheck=True) + def need_three_typed(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params('x', 'y') + def need_two(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params('x', 'y', typecheck=True) + def need_two_typed(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params('x', 'z') + def need_first_last(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + @apihelpers.require_params('x', 'z', typecheck=True) + def need_first_last_typed(self, x: int = None, y: int = None, z: int = None) -> int: + return x + y + z + + +class RequireParamTest(unittest.TestCase): + + def test_require_param(self): + t = _TestMethods() + + f = t.need_all + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(errors.api.InvalidArgument): + f(1, 2) + self.assertEqual('abc', f('a', 'b', 'c')) + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + self.assertEqual(6.6, f(1.1, 2.2, 3.3)) + self.assertEqual(6, f(1, 2, 3)) + + f = t.need_three + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(errors.api.InvalidArgument): + f(1, 2) + self.assertEqual('abc', f('a', 'b', 'c')) + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + self.assertEqual(6.6, f(1.1, 2.2, 3.3)) + self.assertEqual(6, f(1, 2, 3)) + + f = t.need_all_typed + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(errors.api.InvalidArgument): + f(1, 2) + with self.assertRaises(errors.api.InvalidArgument): + f('a', 'b', 'c') + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + with self.assertRaises(errors.api.InvalidArgument): + f(1.1, 2.2, 3.3) + self.assertEqual(6, f(1, 2, 3)) + + f = t.need_three_typed + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(errors.api.InvalidArgument): + f(1, 2) + with self.assertRaises(errors.api.InvalidArgument): + f('a', 'b', 'c') + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + with self.assertRaises(errors.api.InvalidArgument): + f(1.1, 2.2, 3.3) + self.assertEqual(6, f(1, 2, 3)) + + + f = t.need_two + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(TypeError): # 1 + 2 + None + f(1, 2) + self.assertEqual('abc', f('a', 'b', 'c')) + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + self.assertEqual(6.6, f(1.1, 2.2, 3.3)) + self.assertEqual(6, f(1, 2, 3)) + + f = t.need_two_typed + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(TypeError): # 1 + 2 + None + f(1, 2) + with self.assertRaises(errors.api.InvalidArgument): + f('a', 'b', 'c') + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + with self.assertRaises(errors.api.InvalidArgument): + f(1.1, 2.2, 3.3) + self.assertEqual(6, f(1, 2, 3)) + + + f = t.need_first_last + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(errors.api.InvalidArgument): # 1 + 2 + None + f(1, 2) + with self.assertRaises(TypeError): # 1 + None + 3 + f(1, None, 3) + with self.assertRaises(TypeError): # 1 + None + 3 + f(1, z=3) + + self.assertEqual('abc', f('a', 'b', 'c')) + with self.assertRaises(TypeError): # 1 + None + 3 + f('a', z='c') + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + self.assertEqual(6.6, f(1.1, 2.2, 3.3)) + self.assertEqual(6, f(1, 2, 3)) + + f = t.need_first_last_typed + + self.assertEqual(1368, f(123, 456, 789)) + with self.assertRaises(errors.api.InvalidArgument): + f() + with self.assertRaises(errors.api.InvalidArgument): + f(1) + with self.assertRaises(errors.api.InvalidArgument): # 1 + 2 + None + f(1, 2) + with self.assertRaises(TypeError): # 1 + None + 3 + f(1, None, 3) + with self.assertRaises(TypeError): # 1 + None + 3 + f(1, z=3) + with self.assertRaises(errors.api.InvalidArgument): + f('a', 'b', 'c') + with self.assertRaises(errors.api.InvalidArgument): # 1 + None + 3 + f('a', z='c') + with self.assertRaises(errors.api.InvalidArgument): + f(None, None, None) + with self.assertRaises(errors.api.InvalidArgument): + f(1.1, 2.2, 3.3) + self.assertEqual(6, f(1, 2, 3)) \ No newline at end of file diff --git a/tests/plain/test_base64stuff.py b/tests/plain/test_base64stuff.py new file mode 100644 index 0000000..85364af --- /dev/null +++ b/tests/plain/test_base64stuff.py @@ -0,0 +1,29 @@ +import unittest +import base64 +from protoplasm.casting import castutils + +import logging + +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +TEXT_BLARG = '''In computer programming, Base64 is a group of binary-to-text encoding schemes that represent binary data (more specifically, a sequence of 8-bit bytes) in sequences of 24 bits that can be represented by four 6-bit Base64 digits. + +Common to all binary-to-text encoding schemes, Base64 is designed to carry data stored in binary formats across channels that only reliably support text content. Base64 is particularly prevalent on the World Wide Web[1] where one of its uses is the ability to embed image files or other binary assets inside textual assets such as HTML and CSS files.[2] + +Base64 is also widely used for sending e-mail attachments. This is required because SMTP – in its original form – was designed to transport 7-bit ASCII characters only. This encoding causes an overhead of 33–37% (33% by the encoding itself; up to 4% more by the inserted line breaks).''' + + +class Base64StuffTest(unittest.TestCase): + def test_padd_fixer(self): + for i in range(len(TEXT_BLARG)): + part = TEXT_BLARG[0:i] + part_enc = part.encode() + raw64 = base64.encodebytes(part.encode()) + stripped = castutils.base64_stripper(raw64) + + a = castutils.fuzzy_base64_to_bytes(raw64) + self.assertEqual(a, part_enc, msg=f'A failed with length={i}') + b = castutils.fuzzy_base64_to_bytes(stripped) + self.assertEqual(b, part_enc, msg=f'B failed with length={i}') + diff --git a/tests/res/build/empty.txt b/tests/res/build/empty.txt new file mode 100644 index 0000000..6a955ad --- /dev/null +++ b/tests/res/build/empty.txt @@ -0,0 +1 @@ +Intentionally empty directory... diff --git a/tests/res/proto/sandbox/test/alpha.proto b/tests/res/proto/sandbox/test/alpha.proto new file mode 100644 index 0000000..a4f1108 --- /dev/null +++ b/tests/res/proto/sandbox/test/alpha.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package sandbox.test; + + +message AlphaMessage { + string my_string = 1; + int32 my_number = 2; +} diff --git a/tests/res/proto/sandbox/test/anytest.proto b/tests/res/proto/sandbox/test/anytest.proto new file mode 100644 index 0000000..d4fd2d3 --- /dev/null +++ b/tests/res/proto/sandbox/test/anytest.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package sandbox.test; + +import "google/protobuf/any.proto"; + + +message AnyMessage { + google.protobuf.Any my_any = 1; + repeated google.protobuf.Any my_any_list = 2; + map my_any_map = 3; +} diff --git a/tests/res/proto/sandbox/test/beta.proto b/tests/res/proto/sandbox/test/beta.proto new file mode 100644 index 0000000..1596bbb --- /dev/null +++ b/tests/res/proto/sandbox/test/beta.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package sandbox.test; + +import "sandbox/test/alpha.proto"; + + +message BetaMessage { + string my_string = 1; + int32 my_number = 2; + sandbox.test.AlphaMessage my_foreign_message = 3; +} diff --git a/tests/res/proto/sandbox/test/clones.proto b/tests/res/proto/sandbox/test/clones.proto new file mode 100644 index 0000000..97ba0cd --- /dev/null +++ b/tests/res/proto/sandbox/test/clones.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package sandbox.test; + +import "google/protobuf/timestamp.proto"; + + +message SubThing { + string foo = 1; + string bar = 2; +} + + +message ThingOne { + string my_string = 1; + int32 my_number = 2; + float my_float = 3; + google.protobuf.Timestamp my_timestamp = 4; + SubThing my_subthing = 5; + string my_unique_string = 6; +} + + +message ThingTwo { + string my_string = 1; + int32 my_number = 2; + float my_float = 3; + google.protobuf.Timestamp my_timestamp = 4; + SubThing my_subthing = 5; + string my_special_string = 6; +} diff --git a/tests/res/proto/sandbox/test/delta.proto b/tests/res/proto/sandbox/test/delta.proto new file mode 100644 index 0000000..e030d52 --- /dev/null +++ b/tests/res/proto/sandbox/test/delta.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package sandbox.test; + +import "sandbox/test/beta.proto"; + + +message DeltaMessage { + string my_string = 1; + int32 my_number = 2; + sandbox.test.BetaMessage my_level_three_message = 3; +} diff --git a/tests/res/proto/sandbox/test/enums.proto b/tests/res/proto/sandbox/test/enums.proto new file mode 100644 index 0000000..82dec50 --- /dev/null +++ b/tests/res/proto/sandbox/test/enums.proto @@ -0,0 +1,64 @@ +syntax = "proto3"; + +package sandbox.test; + + +enum ExternalEnum { + ZERO_AND_DEFAULT = 0; + ONE = 1; + TWO = 2; + THREE = 3; +} + +enum ExternalAliasEnum { + option allow_alias = true; + DEFAULT = 0; + ZERO = 0; + FOUR = 1; + FJORIR = 1; + FIVE = 2; + FIMM = 2; + SIX = 3; + SEX = 3; +} + + +message WithExternalEnum { + ExternalEnum my_enum = 1; + ExternalAliasEnum my_alias_enum = 2; + + repeated ExternalEnum my_enum_list = 3; + repeated ExternalAliasEnum my_alias_enum_list = 4; + + map my_enum_map = 5; + map my_alias_enum_map = 6; +} + + +message WithInternalEnum { + enum InternalEnum { + ZERO_AND_DEFAULT = 0; + FOUR = 4; + FIVE = 5; + SIX = 6; + } + enum InternalAliasEnum { + option allow_alias = true; + DEFAULT = 0; + ZERO = 0; + SEVEN = 7; + SJO = 7; + EIGHT = 8; + ATTA = 8; + NINE = 9; + NIU = 9; + } + InternalEnum my_internal_enum = 1; + InternalAliasEnum my_internal_alias_enum = 2; + + repeated InternalEnum my_internal_enum_list = 3; + repeated InternalAliasEnum my_internal_alias_enum_list = 4; + + map my_internal_enum_map = 5; + map my_internal_alias_enum_map = 6; +} diff --git a/tests/res/proto/sandbox/test/illnamedfields.proto b/tests/res/proto/sandbox/test/illnamedfields.proto new file mode 100644 index 0000000..700e567 --- /dev/null +++ b/tests/res/proto/sandbox/test/illnamedfields.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package sandbox.test; + + +message BadNames { + string False = 1; + string None = 2; + string True = 3; + string and = 4; + string as = 5; + string assert = 6; + string async = 7; + string await = 8; + string break = 9; + string class = 10; + string continue = 11; + string def = 12; + string del = 13; + string elif = 14; + string else = 15; + string except = 16; + string finally = 17; + string for = 18; + string from = 19; + string global = 20; + string if = 21; + string import = 22; + string in = 23; + string is = 24; + string lambda = 25; + string nonlocal = 26; + string not = 27; + string or = 28; + string pass = 29; + string raise = 30; + string return = 31; + string try = 32; + string while = 33; + string with = 34; + string yield = 35; +} diff --git a/tests/res/proto/sandbox/test/illnamedservice.proto b/tests/res/proto/sandbox/test/illnamedservice.proto new file mode 100644 index 0000000..83b18ea --- /dev/null +++ b/tests/res/proto/sandbox/test/illnamedservice.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package sandbox.test; + + +service ServiceWithBadRequestNames { + // Say hello + rpc DoSomething(MyAction) returns (MyConsequence); + +} + +message MyAction { + string foo = 1; +} + +message MyConsequence { + string bar = 1; +} \ No newline at end of file diff --git a/tests/res/proto/sandbox/test/interfaceonly.proto b/tests/res/proto/sandbox/test/interfaceonly.proto new file mode 100644 index 0000000..6624724 --- /dev/null +++ b/tests/res/proto/sandbox/test/interfaceonly.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package sandbox.test; + +// This should skip grpc stub generation +option py_generic_services = true; + + +// This is a really Simple Service +service InterfaceOnlyService { + // Say hello + rpc OnHello(OnHelloRequest) returns (OnHelloResponse); +} + +message OnHelloRequest { + string greeting = 1; +} + +message OnHelloResponse { + string reply = 1; +} diff --git a/tests/res/proto/sandbox/test/nested.proto b/tests/res/proto/sandbox/test/nested.proto new file mode 100644 index 0000000..6c844af --- /dev/null +++ b/tests/res/proto/sandbox/test/nested.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package sandbox.test; + + +message OuterMessage { + + + int32 my_int = 1; + string my_string = 2; + + InnerMessage my_message = 3; + + message InnerMessage { + int32 my_inner_int = 1; + string my_inner_string = 2; + } +} diff --git a/tests/res/proto/sandbox/test/rainbow.proto b/tests/res/proto/sandbox/test/rainbow.proto new file mode 100644 index 0000000..593c7bb --- /dev/null +++ b/tests/res/proto/sandbox/test/rainbow.proto @@ -0,0 +1,115 @@ +syntax = "proto3"; + +package sandbox.test; + +import "google/protobuf/timestamp.proto"; + + +message SubMessage { + string foo = 1; + string bar = 2; +} + + +// Used in testing to see exactly how each field type and combo descriptor looks like. +message RainbowMessage { + + // type=9 | has_options=False | lable=1 | message_type=None + string simple_field = 1; + // type=11 | has_options=False | lable=1 | message_type=SubMessage.DESCRIPTOR + SubMessage message_field = 2; + + // type=9 | has_options=False | lable=3 | message_type=None + repeated string simple_list = 3; + // type=11 | has_options=False | lable=3 | message_type=SubMessage.DESCRIPTOR + repeated SubMessage message_list = 4; + + // type=11 | has_options=False | lable=3 | message_type=SimpleMapEntry.DESCRIPTOR -> has_options=True | fields_by_name[value].type=9 + map simple_map = 5; + // type=11 | has_options=False | lable=3 | message_type=MessageMapEntry.DESCRIPTOR -> has_options=True | fields_by_name[value].type=11 + map message_map = 6; +} + + +message TimestampMessage { + google.protobuf.Timestamp my_timestamp = 1; + repeated google.protobuf.Timestamp my_timestamp_list = 2; + map my_timestamp_map = 3; +} + + +message BytesMessage { + bytes my_bytes = 1; + repeated bytes my_bytes_list = 2; + map my_bytes_map = 3; +} + + +message AllTypes { + string my_string = 1; + float my_float = 2; + double my_double = 3; + int32 my_int32 = 4; + int64 my_int64 = 5; + uint32 my_uint32 = 6; + uint64 my_uint64 = 7; + sint32 my_sint32 = 8; + sint64 my_sint64 = 9; + fixed32 my_fixed32 = 10; + fixed64 my_fixed64 = 11; + sfixed32 my_sfixed32 = 12; + sfixed64 my_sfixed64 = 13; + bool my_bool = 14; + bytes my_bytes = 15; +} + + +message AllTypesList { + repeated string my_string_list = 1; + repeated float my_float_list = 2; + repeated double my_double_list = 3; + repeated int32 my_int32_list = 4; + repeated int64 my_int64_list = 5; + repeated uint32 my_uint32_list = 6; + repeated uint64 my_uint64_list = 7; + repeated sint32 my_sint32_list = 8; + repeated sint64 my_sint64_list = 9; + repeated fixed32 my_fixed32_list = 10; + repeated fixed64 my_fixed64_list = 11; + repeated sfixed32 my_sfixed32_list = 12; + repeated sfixed64 my_sfixed64_list = 13; + repeated bool my_bool_list = 14; + repeated bytes my_bytes_list = 15; +} + + +message AllTypesMap { + map my_string_map = 1; + map my_int32_map = 4; + map my_int64_map = 5; + map my_uint32_map = 6; + map my_uint64_map = 7; + map my_sint32_map = 8; + map my_sint64_map = 9; + map my_fixed32_map = 10; + map my_fixed64_map = 11; + map my_sfixed32_map = 12; + map my_sfixed64_map = 13; + map my_bool_map = 14; +} + + +message AllTypesNestedMap { + map my_string_map = 1; + map my_int32_map = 4; + map my_int64_map = 5; + map my_uint32_map = 6; + map my_uint64_map = 7; + map my_sint32_map = 8; + map my_sint64_map = 9; + map my_fixed32_map = 10; + map my_fixed64_map = 11; + map my_sfixed32_map = 12; + map my_sfixed64_map = 13; + map my_bool_map = 14; +} diff --git a/tests/res/proto/sandbox/test/river.proto b/tests/res/proto/sandbox/test/river.proto new file mode 100644 index 0000000..65d8cf9 --- /dev/null +++ b/tests/res/proto/sandbox/test/river.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package sandbox.test; + +service StreamingService { + rpc ReverseMyShit(ReverseMyShitRequest) returns (ReverseMyShitResponse); + rpc MarcoPolo(stream MarcoPoloRequest) returns (stream MarcoPoloResponse); + rpc TellMeAStory(TellMeAStoryRequest) returns (stream TellMeAStoryResponse); + rpc GuessTheNumber(stream GuessTheNumberRequest) returns (GuessTheNumberResponse); +} + +message ReverseMyShitRequest { + string shit = 1; +} + +message ReverseMyShitResponse { + string tihs = 1; +} + +message TellMeAStoryRequest { + string story = 1; +} + +message TellMeAStoryResponse { + string line = 1; +} + +message MarcoPoloRequest { + string question = 1; +} + +message MarcoPoloResponse { + string answer = 1; +} + +message GuessTheNumberRequest { + int32 number = 1; +} + +message GuessTheNumberResponse { + string did_i_win = 1; +} \ No newline at end of file diff --git a/tests/res/proto/sandbox/test/service.proto b/tests/res/proto/sandbox/test/service.proto new file mode 100644 index 0000000..42e2e2d --- /dev/null +++ b/tests/res/proto/sandbox/test/service.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; + +package sandbox.test; + +// This is a really Simple Service +service SimpleService { + // Say hello + rpc Hello(HelloRequest) returns (HelloResponse); + rpc NestedHello(NestedHelloRequest) returns (NestedHelloResponse); + rpc EmptyHello(EmptyHelloRequest) returns (EmptyHelloResponse); +} + +message HelloRequest { + string greeting = 1; +} + +message HelloResponse { + string reply = 1; +} + +message GreetingMessage { + string greeting = 1; + string language = 2; +} + +message NestedHelloRequest { + GreetingMessage my_message = 1; +} + +message NestedHelloResponse { + GreetingMessage my_reply = 1; +} + +message EmptyHelloRequest {} + +message EmptyHelloResponse {} + +// Does math stuff +service Math { + // Adds two numbers + rpc Add(AddRequest) returns (AddResponse); + // Subtracts the later number from the former + rpc Subtract(SubtractRequest) returns (SubtractResponse); + + rpc IntegerDivision(IntegerDivisionRequest) returns (IntegerDivisionResponse); + +} + +message AddRequest { + int64 x = 1; + int64 y = 2; +} + +message AddResponse { + int64 result = 1; +} + +// The request to do some bad-ass subtraction +message SubtractRequest { + // Original number + int64 x = 1; + // Number to subtract + int64 y = 2; +} + +// Results of the subtraction +message SubtractResponse { + // The results of x - y + int64 result = 1; +} + +message IntegerDivisionRequest { + int64 x = 1; + int64 y = 2; +} + +message IntegerDivisionResponse { + int64 result = 1; + int64 remainder = 2; +} \ No newline at end of file diff --git a/tests/res/proto/sandbox/test/service_with_imported_io.proto b/tests/res/proto/sandbox/test/service_with_imported_io.proto new file mode 100644 index 0000000..5f4bb42 --- /dev/null +++ b/tests/res/proto/sandbox/test/service_with_imported_io.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package sandbox.test; + +import "sandbox/test/delta.proto"; +import "sandbox/test/beta.proto"; +import "sandbox/test/alpha.proto"; +import "sandbox/test/nested.proto"; +import "sandbox/test/rainbow.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; + + + +// This is a really Simple Service +service ServiceWithImportedInputAndOutput { + // Say hello + rpc Simple(stream sandbox.test.DeltaMessage) returns (sandbox.test.SubMessage); + rpc Nested(sandbox.test.RainbowMessage) returns (sandbox.test.BetaMessage); + rpc Renest(sandbox.test.OuterMessage) returns (sandbox.test.SubMessage); + rpc TimestampIn(sandbox.test.TimestampMessage) returns (sandbox.test.AlphaMessage); + rpc TimestampOut(sandbox.test.AlphaMessage) returns (google.protobuf.Timestamp); + rpc AnyIn(google.protobuf.Any) returns (sandbox.test.AlphaMessage); + rpc AnyOut(sandbox.test.AlphaMessage) returns (google.protobuf.Any); +} diff --git a/tests/res/proto/sandbox/test/service_with_oneof.proto b/tests/res/proto/sandbox/test/service_with_oneof.proto new file mode 100644 index 0000000..953a5b0 --- /dev/null +++ b/tests/res/proto/sandbox/test/service_with_oneof.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package sandbox.test; + +// This is a really Simple Service +service SimpleOneOfService { + // Say hello + rpc HelloAgain(HelloAgainRequest) returns (HelloAgainResponse); +} + +message HelloAgainRequest { + string greeting = 1; +} + +message HelloAgainResponse { + oneof reply { + string response = 1; + int32 number = 2; + }; +} diff --git a/tests/res/proto/sandbox/test/test.proto b/tests/res/proto/sandbox/test/test.proto new file mode 100644 index 0000000..434e355 --- /dev/null +++ b/tests/res/proto/sandbox/test/test.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; + +package sandbox.test; + +import "google/protobuf/timestamp.proto"; + + +message Simple { + string my_string = 1; + float my_float = 2; + double my_double = 3; + int32 my_int32 = 4; + int64 my_int64 = 5; + uint32 my_uint32 = 6; + uint64 my_uint64 = 7; + sint32 my_sint32 = 8; + sint64 my_sint64 = 9; + fixed32 my_fixed32 = 10; + fixed64 my_fixed64 = 11; + sfixed32 my_sfixed32 = 12; + sfixed64 my_sfixed64 = 13; + bool my_bool = 14; + bytes my_bytes = 15; +} + +message SimpleList { + repeated string my_string_list = 1; + repeated float my_float_list = 2; + repeated double my_double_list = 3; + repeated int32 my_int32_list = 4; + repeated int64 my_int64_list = 5; + repeated uint32 my_uint32_list = 6; + repeated uint64 my_uint64_list = 7; + repeated sint32 my_sint32_list = 8; + repeated sint64 my_sint64_list = 9; + repeated fixed32 my_fixed32_list = 10; + repeated fixed64 my_fixed64_list = 11; + repeated sfixed32 my_sfixed32_list = 12; + repeated sfixed64 my_sfixed64_list = 13; + repeated bool my_bool_list = 14; + repeated bytes my_bytes_list = 15; +} + +message SimpleMap { + map my_string_map = 1; + map my_int32_map = 4; + map my_int64_map = 5; + map my_uint32_map = 6; + map my_uint64_map = 7; + map my_sint32_map = 8; + map my_sint64_map = 9; + map my_fixed32_map = 10; + map my_fixed64_map = 11; + map my_sfixed32_map = 12; + map my_sfixed64_map = 13; + map my_bool_map = 14; +} + +message SimpleTimestamp { + google.protobuf.Timestamp my_timestamp = 1; +} + +message NestedDude { + Simple my_simple = 1; +} + +message NestedList { + repeated Simple my_simple_list = 1; +} + +message NestedMap { + map my_string_simple_map = 1; + map my_int32_simple_map = 2; +} + +message VeryNestedDude { + NestedDude my_nested_dude = 1; + Simple my_non_nested_simple = 2; + repeated NestedList my_list_of_lists = 3; +} diff --git a/tests/res/proto/sandbox/test/timeduration.proto b/tests/res/proto/sandbox/test/timeduration.proto new file mode 100644 index 0000000..4d93d49 --- /dev/null +++ b/tests/res/proto/sandbox/test/timeduration.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package sandbox.test; + +import "google/protobuf/duration.proto"; + + +message DurationMessage { + google.protobuf.Duration my_duration = 1; + repeated google.protobuf.Duration my_duration_list = 2; + map my_duration_map = 3; +} diff --git a/tests/res/proto/unittesting/unary/unaryservice.proto b/tests/res/proto/unittesting/unary/unaryservice.proto new file mode 100644 index 0000000..4b9972c --- /dev/null +++ b/tests/res/proto/unittesting/unary/unaryservice.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package unittesting.unary; + +service UnaryService { + rpc WithNoData(WithNoDataRequest) returns (WithNoDataResponse); + rpc WithInput(WithInputRequest) returns (WithInputResponse); + rpc WithOutput(WithOutputRequest) returns (WithOutputResponse); + rpc WithBoth(WithBothRequest) returns (WithBothResponse); + rpc WithManyInputs(WithManyInputsRequest) returns (WithManyInputsResponse); + rpc WithManyOutputs(WithManyOutputsRequest) returns (WithManyOutputsResponse); + rpc WithManyBoths(WithManyBothsRequest) returns (WithManyBothsResponse); +} + +message WithNoDataRequest {} +message WithNoDataResponse {} + +message WithInputRequest { + string unnamed_input = 1; +} +message WithInputResponse {} + +message WithOutputRequest {} +message WithOutputResponse { + string unnamed_output = 1; +} + +message WithBothRequest { + string some_input = 1; +} +message WithBothResponse { + string some_output = 1; +} + +message WithManyInputsRequest { + string first_input = 1; + int32 second_input = 2; + bool third_input = 3; +} +message WithManyInputsResponse {} + +message WithManyOutputsRequest {} +message WithManyOutputsResponse { + string first_output = 1; + int32 second_output = 2; + bool third_output = 3; +} + +message WithManyBothsRequest { + string another_first_input = 1; + int32 another_second_input = 2; + bool another_third_input = 3; +} +message WithManyBothsResponse { + string another_first_output = 1; + int32 another_second_output = 2; + bool another_third_output = 3; +} diff --git a/tests/servicetestutils/__init__.py b/tests/servicetestutils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/servicetestutils/plasm_server.py b/tests/servicetestutils/plasm_server.py new file mode 100644 index 0000000..2b35188 --- /dev/null +++ b/tests/servicetestutils/plasm_server.py @@ -0,0 +1,79 @@ +import typing +import grpc +from concurrent import futures +from unittesting.unary.unaryservice_api import UnaryServiceInterface +from unittesting.unary.unaryservice_grpc_receiver import UnaryServiceGrpcServicer +from protoplasm import errors + +import logging +log = logging.getLogger(__name__) + + +class UnaryServiceImplementation(UnaryServiceInterface): + def __init__(self): + self.calls: typing.Dict[str, int] = { + 'WithNoData': 0, + 'WithInput': 0, + 'WithOutput': 0, + 'WithBoth': 0, + 'WithManyInputs': 0, + 'WithManyOutputs': 0, + 'WithManyBoths': 0, + } + + def with_no_data(self) -> typing.NoReturn: + self.calls['WithNoData'] += 1 + log.info('with_no_data called!') + + def with_input(self, unnamed_input: str = None) -> typing.NoReturn: + self.calls['WithInput'] += 1 + log.info(f'with_input called with {unnamed_input}!') + if unnamed_input == 'explode': + raise errors.NotFound('totally fake not found error') + + def with_output(self) -> str: + self.calls['WithOutput'] += 1 + ret = 'you win' + log.info(f'with_output called, returning "{ret}"') + return ret + + def with_both(self, some_input: str = None) -> str: + self.calls['WithBoth'] += 1 + ret = some_input[::-1] + log.info(f'WithBoth called with "{some_input}", returning "{ret}"') + return ret + + def with_many_inputs(self, first_input: str = None, second_input: int = None, + third_input: bool = None) -> typing.NoReturn: + self.calls['WithManyInputs'] += 1 + log.info(f'WithManyInputs called with {first_input}, {second_input}, {third_input}!') + + def with_many_outputs(self) -> typing.Tuple[str, int, bool]: + self.calls['WithManyOutputs'] += 1 + ret = 'snorlax', 7, True + log.info(f'with_many_outputs called, returning {ret}') + return ret + + def with_many_boths(self, another_first_input: str = None, another_second_input: int = None, + another_third_input: bool = None) -> typing.Tuple[str, int, bool]: + self.calls['WithManyBoths'] += 1 + ret = another_first_input.upper(), another_second_input // 2, not another_third_input + log.info(f'with_many_boths called with {another_first_input}, {another_second_input}, {another_third_input} returning {ret}') + return ret + + +class UnaryProtoplasmServer(object): + def __init__(self): + self.grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + self.servicer_implementation = UnaryServiceImplementation() + self.servicer = UnaryServiceGrpcServicer(self.servicer_implementation) + self.servicer.add_to_server(self.grpc_server) + + def start(self, port: str = '[::]:50051'): + self.grpc_server.add_insecure_port(port) + log.info(f'Starting UnaryProtoplasmServer on port {port}...') + self.grpc_server.start() + + def stop(self): + self.grpc_server.stop(0) + diff --git a/tests/servicetestutils/raw_client.py b/tests/servicetestutils/raw_client.py new file mode 100644 index 0000000..8eca676 --- /dev/null +++ b/tests/servicetestutils/raw_client.py @@ -0,0 +1,91 @@ +__all__ = [ + 'call_WithNoData', + 'call_WithInput', + 'call_WithOutput', + 'call_WithBoth', + 'call_WithManyInputs', + 'call_WithManyOutputs', + 'call_WithManyBoths', +] +from unittesting.unary import unaryservice_pb2 +from unittesting.unary import unaryservice_pb2_grpc +import grpc +from typing import * + +import logging +log = logging.getLogger(__name__) + + +def call_WithNoData(port: str = 'localhost:50051'): + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + result: unaryservice_pb2.WithNoDataResponse = stub.WithNoData( + unaryservice_pb2.WithNoDataRequest() + ) + log.info(f'Called WithNoData -> {result}') + + +def call_WithInput(port: str = 'localhost:50051', unnamed_input: str = '?'): + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + req = unaryservice_pb2.WithInputRequest(unnamed_input=unnamed_input) + result: unaryservice_pb2.WithInputResponse = stub.WithInput(req) + log.info(f'Called WithInput with {req} -> {result}') + + +def call_WithOutput(port: str = 'localhost:50051') -> str: + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + req = unaryservice_pb2.WithOutputRequest() + result: unaryservice_pb2.WithOutputResponse = stub.WithOutput(req) + log.info(f'Called WithOutput with {req} -> {result}') + return result.unnamed_output + + +def call_WithBoth(port: str = 'localhost:50051', some_input: str = '?') -> str: + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + req = unaryservice_pb2.WithBothRequest(some_input=some_input) + result: unaryservice_pb2.WithBothResponse = stub.WithBoth(req) + log.info(f'Called WithBoth with {req} -> {result}') + return result.some_output + + +def call_WithManyInputs(port: str = 'localhost:50051', + first_input: str = '?', + second_input: int = 0, + third_input: bool = False): + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + req = unaryservice_pb2.WithManyInputsRequest( + first_input=first_input, + second_input=second_input, + third_input=third_input, + ) + result: unaryservice_pb2.WithManyInputsResponse = stub.WithManyInputs(req) + log.info(f'Called WithManyInputs with {req} -> {result}') + + +def call_WithManyOutputs(port: str = 'localhost:50051') -> Tuple[str, int, bool]: + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + req = unaryservice_pb2.WithManyOutputsRequest() + result: unaryservice_pb2.WithManyOutputsResponse = stub.WithManyOutputs(req) + log.info(f'Called WithManyOutputs with {req} -> {result}') + return result.first_output, result.second_output, result.third_output + + +def call_WithManyBoths(port: str = 'localhost:50051', + another_first_input: str = '?', + another_second_input: int = 0, + another_third_input: bool = False) -> Tuple[str, int, bool]: + with grpc.insecure_channel(port) as channel: + stub = unaryservice_pb2_grpc.UnaryServiceStub(channel) + req = unaryservice_pb2.WithManyBothsRequest( + another_first_input=another_first_input, + another_second_input=another_second_input, + another_third_input=another_third_input, + ) + result: unaryservice_pb2.WithManyBothsResponse = stub.WithManyBoths(req) + log.info(f'Called WithManyBoths with {req} -> {result}') + return result.another_first_output, result.another_second_output, result.another_third_output diff --git a/tests/servicetestutils/raw_server.py b/tests/servicetestutils/raw_server.py new file mode 100644 index 0000000..ddf5d0b --- /dev/null +++ b/tests/servicetestutils/raw_server.py @@ -0,0 +1,98 @@ +from unittesting.unary import unaryservice_pb2 +from unittesting.unary import unaryservice_pb2_grpc +import grpc +from concurrent import futures +from typing import * + +import logging +log = logging.getLogger(__name__) + + +class UnaryServiceServicerImplementation(unaryservice_pb2_grpc.UnaryServiceServicer): + def __init__(self): + self.calls: Dict[str, int] = { + 'WithNoData': 0, + 'WithInput': 0, + 'WithOutput': 0, + 'WithBoth': 0, + 'WithManyInputs': 0, + 'WithManyOutputs': 0, + 'WithManyBoths': 0, + } + + def WithNoData(self, + request: unaryservice_pb2.WithNoDataRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithNoDataResponse: + self.calls['WithNoData'] += 1 + log.info('WithNoData called!') + return unaryservice_pb2.WithNoDataResponse() + + def WithInput(self, + request: unaryservice_pb2.WithInputRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithInputResponse: + self.calls['WithInput'] += 1 + log.info(f'WithInput called with {request}!') + + if request.unnamed_input == 'explode': + return context.abort(grpc.StatusCode.NOT_FOUND, 'totally fake not found error') + + return unaryservice_pb2.WithInputResponse() + + def WithOutput(self, + request: unaryservice_pb2.WithOutputRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithOutputResponse: + self.calls['WithOutput'] += 1 + ret = unaryservice_pb2.WithOutputResponse(unnamed_output='you win') + log.info(f'WithOutput called, returning {ret}') + return ret + + def WithBoth(self, + request: unaryservice_pb2.WithBothRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithBothResponse: + self.calls['WithBoth'] += 1 + ret = unaryservice_pb2.WithBothResponse(some_output=request.some_input[::-1]) + log.info(f'WithBoth called with {request}, returning {ret}') + return ret + + def WithManyInputs(self, + request: unaryservice_pb2.WithManyInputsRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithManyInputsResponse: + self.calls['WithManyInputs'] += 1 + log.info(f'WithManyInputs called with {request}!') + return unaryservice_pb2.WithManyInputsResponse() + + def WithManyOutputs(self, + request: unaryservice_pb2.WithManyOutputsRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithManyOutputsResponse: + self.calls['WithManyOutputs'] += 1 + ret = unaryservice_pb2.WithManyOutputsResponse(first_output='snorlax', + second_output=7, + third_output=True) + log.info(f'WithManyOutputs called, returning {ret}') + return ret + + def WithManyBoths(self, + request: unaryservice_pb2.WithManyBothsRequest, + context: grpc.ServicerContext) -> unaryservice_pb2.WithManyBothsResponse: + self.calls['WithManyBoths'] += 1 + ret = unaryservice_pb2.WithManyBothsResponse(another_first_output=request.another_first_input.upper(), + another_second_output=request.another_second_input // 2, + another_third_output=not request.another_third_input) + log.info(f'WithManyBoths called with {request}, returning {ret}') + return ret + + +class UnaryServiceServer(object): + def __init__(self): + self.grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + self.servicer_implementation = UnaryServiceServicerImplementation() + unaryservice_pb2_grpc.add_UnaryServiceServicer_to_server(self.servicer_implementation, + self.grpc_server) + + def start(self, port: str = '[::]:50051'): + self.grpc_server.add_insecure_port(port) + log.info(f'Starting UnaryServiceServer on port {port}...') + self.grpc_server.start() + + def stop(self): + self.grpc_server.stop(0) diff --git a/tests/test_casting.py b/tests/test_casting.py new file mode 100644 index 0000000..a334486 --- /dev/null +++ b/tests/test_casting.py @@ -0,0 +1,565 @@ +import base64 +import datetime +import unittest +import os +import sys + +from protoplasm import casting + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +import shutil +import time +HERE = os.path.dirname(__file__) +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + + +class CastingTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_proto_to_dataclass_and_back(self): + from sandbox.test import rainbow_pb2 + from sandbox.test import rainbow_dc + + p1 = rainbow_pb2.SubMessage() + p1.foo = 'Foo!' + p1.bar = 'Bar!!!' + + dc1 = rainbow_dc.SubMessage() + dc1.foo = 'Foo!' + dc1.bar = 'Bar!!!' + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + + p2 = rainbow_pb2.SubMessage() + p2.foo = 'Foo Two!' + + dc2 = rainbow_dc.SubMessage() + dc2.foo = 'Foo Two!' + + self.assertEqual(p2, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2)) + + p3 = rainbow_pb2.SubMessage() + p3.foo = '这是中国人' + + dc3 = rainbow_dc.SubMessage() + dc3.foo = '这是中国人' + + self.assertEqual(p3, casting.dataclass_to_proto(dc3)) + self.assertEqual(dc3, casting.proto_to_dataclass(p3)) + + p4 = rainbow_pb2.SubMessage() + dc4 = rainbow_dc.SubMessage() + + self.assertEqual(p4, casting.dataclass_to_proto(dc4)) + self.assertEqual(dc4, casting.proto_to_dataclass(p4)) + + self.assertNotEqual(dc1, casting.proto_to_dataclass(p2)) + + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + def test_nested_proto_to_dataclass_and_back(self): + from sandbox.test import rainbow_pb2 + from sandbox.test import rainbow_dc + + p1 = rainbow_pb2.RainbowMessage() + dc1 = rainbow_dc.RainbowMessage() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + p4 = rainbow_pb2.RainbowMessage() + p4.simple_field = 'Green' + p4.message_field.foo = 'Blue' + p4.message_field.bar = 'Yellow' + + dc4 = rainbow_dc.RainbowMessage() + dc4.simple_field = 'Green' + dc4.message_field = rainbow_dc.SubMessage() + dc4.message_field.foo = 'Blue' + dc4.message_field.bar = 'Yellow' + + self.assertEqual(p4, casting.dataclass_to_proto(dc4)) + self.assertEqual(dc4, casting.proto_to_dataclass(p4)) + self.assertEqual(dc4, casting.proto_to_dataclass(casting.dataclass_to_proto(dc4))) + self.assertEqual(p4, casting.dataclass_to_proto(casting.proto_to_dataclass(p4))) + + p10 = rainbow_pb2.RainbowMessage() + p10.simple_field = 'Green' + p10.message_field.foo = 'Blue' + p10.message_field.bar = 'Yellow' + p10.simple_list.append('Four') + p10.simple_list.append('Seven') + p10.simple_list.append('99') + m7 = p10.message_list.add() + m7.foo = 'Foo in a list' + m7.bar = 'Candybar' + m8 = p10.message_list.add() + m8.foo = 'a Fool in a list' + m8.bar = 'Tequila bar' + p10.simple_map['dora'] = 'Imamap!' + p10.simple_map['diego'] = 'Camera!' + p10.message_map['mickey'].foo = 'mouse' + p10.message_map['donald'].foo = 'duck' + p10.message_map['donald'].bar = 'trump' + + dc10 = rainbow_dc.RainbowMessage() + dc10.simple_field = 'Green' + dc10.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc10.simple_list.append('Four') + dc10.simple_list.append('Seven') + dc10.simple_list.append('99') + dc10.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc10.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + dc10.simple_map['dora'] = 'Imamap!' + dc10.simple_map['diego'] = 'Camera!' + dc10.message_map['mickey'] = rainbow_dc.SubMessage(foo='mouse') + dc10.message_map['donald'] = rainbow_dc.SubMessage(foo='duck', bar='trump') + + self.assertEqual(p10, casting.dataclass_to_proto(dc10)) + self.assertEqual(dc10, casting.proto_to_dataclass(p10)) + self.assertEqual(dc10, casting.proto_to_dataclass(casting.dataclass_to_proto(dc10))) + self.assertEqual(p10, casting.dataclass_to_proto(casting.proto_to_dataclass(p10))) + + def test_timestamp_proto_to_dataclass_and_back(self): + from sandbox.test import rainbow_pb2 + from sandbox.test import rainbow_dc + + p1 = rainbow_pb2.TimestampMessage() + dc1 = rainbow_dc.TimestampMessage() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00.000000Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts4 = '2049-01-01T12:00:00.000000Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts5 = '2049-01-01T00:00:00.000000Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + p_expect = rainbow_pb2.TimestampMessage() + p_expect.my_timestamp.FromJsonString(ts1) + p_expect.my_timestamp_list.add().FromJsonString(ts2) + p_expect.my_timestamp_list.add().FromJsonString(ts3) + p_expect.my_timestamp_map['noon'].FromJsonString(ts4) + p_expect.my_timestamp_map['midnight'].FromJsonString(ts5) + + dc_expect = rainbow_dc.TimestampMessage() + dc_expect.my_timestamp = dt1 + dc_expect.my_timestamp_list.append(dt2) + dc_expect.my_timestamp_list.append(dt3) + dc_expect.my_timestamp_map['noon'] = dt4 + dc_expect.my_timestamp_map['midnight'] = dt5 + + self.assertEqual(p_expect, casting.dataclass_to_proto(dc_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(p_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(casting.dataclass_to_proto(dc_expect))) + self.assertEqual(p_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p_expect))) + + def test_byte_proto_to_dataclass_and_back(self): + from sandbox.test import rainbow_pb2 + from sandbox.test import rainbow_dc + + p1 = rainbow_pb2.BytesMessage() + dc1 = rainbow_dc.BytesMessage() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + p_expect = rainbow_pb2.BytesMessage() + p_expect.my_bytes = as_bytes_1 + p_expect.my_bytes_list.append(as_bytes_2) + p_expect.my_bytes_list.append(as_bytes_3) + p_expect.my_bytes_map['zero'] = as_bytes_4 + p_expect.my_bytes_map['one'] = as_bytes_5 + + dc_expect = rainbow_dc.BytesMessage() + dc_expect.my_bytes = as_bytes_1 + dc_expect.my_bytes_list.append(as_bytes_2) + dc_expect.my_bytes_list.append(as_bytes_3) + dc_expect.my_bytes_map['zero'] = as_bytes_4 + dc_expect.my_bytes_map['one'] = as_bytes_5 + + self.assertEqual(p_expect, casting.dataclass_to_proto(dc_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(p_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(casting.dataclass_to_proto(dc_expect))) + self.assertEqual(p_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p_expect))) + + def test_enum_proto_to_dataclass_and_back(self): + from sandbox.test import enums_pb2 + from sandbox.test import enums_dc + + p1 = enums_pb2.WithExternalEnum() + dc1 = enums_dc.WithExternalEnum() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + p_expect = enums_pb2.WithExternalEnum() + p_expect.my_enum = enums_pb2.TWO + + p_expect.my_enum_list.append(enums_pb2.ONE) + p_expect.my_enum_list.append(enums_pb2.THREE) + + p_expect.my_enum_map['one'] = enums_pb2.ONE + p_expect.my_enum_map['two'] = enums_pb2.TWO + + p_expect.my_alias_enum = enums_pb2.SIX + + p_expect.my_alias_enum_list.append(enums_pb2.FOUR) + p_expect.my_alias_enum_list.append(enums_pb2.FIVE) + p_expect.my_alias_enum_list.append(enums_pb2.FJORIR) + + p_expect.my_alias_enum_map['six'] = enums_pb2.SIX + p_expect.my_alias_enum_map['sex'] = enums_pb2.SEX + p_expect.my_alias_enum_map['fimm'] = enums_pb2.FIVE + + dc_expect = enums_dc.WithExternalEnum() + dc_expect.my_enum = enums_dc.TWO + + dc_expect.my_enum_list.append(enums_dc.ONE) + dc_expect.my_enum_list.append(enums_dc.THREE) + + dc_expect.my_enum_map['one'] = enums_dc.ONE + dc_expect.my_enum_map['two'] = enums_dc.TWO + + dc_expect.my_alias_enum = enums_dc.SIX + + dc_expect.my_alias_enum_list.append(enums_dc.FOUR) + dc_expect.my_alias_enum_list.append(enums_dc.FIVE) + dc_expect.my_alias_enum_list.append(enums_dc.FJORIR) + + dc_expect.my_alias_enum_map['six'] = enums_dc.SIX + dc_expect.my_alias_enum_map['sex'] = enums_dc.SEX + dc_expect.my_alias_enum_map['fimm'] = enums_dc.FIVE + + self.assertEqual(p_expect, casting.dataclass_to_proto(dc_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(p_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(casting.dataclass_to_proto(dc_expect))) + self.assertEqual(p_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p_expect))) + + p_expect2 = enums_pb2.WithInternalEnum() + p_expect2.my_internal_enum = enums_pb2.WithInternalEnum.SIX + + p_expect2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FIVE) + p_expect2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FOUR) + + p_expect2.my_internal_enum_map['no5'] = enums_pb2.WithInternalEnum.FIVE + p_expect2.my_internal_enum_map['no6'] = enums_pb2.WithInternalEnum.SIX + + p_expect2.my_internal_alias_enum = enums_pb2.WithInternalEnum.SEVEN + + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.SJO) + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.ATTA) + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.EIGHT) + + p_expect2.my_internal_alias_enum_map['no9'] = enums_pb2.WithInternalEnum.NIU + p_expect2.my_internal_alias_enum_map['no9B'] = enums_pb2.WithInternalEnum.NINE + p_expect2.my_internal_alias_enum_map['default'] = enums_pb2.WithInternalEnum.ZERO + + dc_expect2 = enums_dc.WithInternalEnum() + dc_expect2.my_internal_enum = enums_dc.WithInternalEnum.SIX + + dc_expect2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FIVE) + dc_expect2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FOUR) + + dc_expect2.my_internal_enum_map['no5'] = enums_dc.WithInternalEnum.FIVE + dc_expect2.my_internal_enum_map['no6'] = enums_dc.WithInternalEnum.SIX + + dc_expect2.my_internal_alias_enum = enums_dc.WithInternalEnum.SEVEN + + dc_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.SJO) + dc_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.ATTA) + dc_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.EIGHT) + + dc_expect2.my_internal_alias_enum_map['no9'] = enums_dc.WithInternalEnum.NIU + dc_expect2.my_internal_alias_enum_map['no9B'] = enums_dc.WithInternalEnum.NINE + dc_expect2.my_internal_alias_enum_map['default'] = enums_dc.WithInternalEnum.ZERO + + self.assertEqual(p_expect2, casting.dataclass_to_proto(dc_expect2)) + self.assertEqual(dc_expect2, casting.proto_to_dataclass(p_expect2)) + self.assertEqual(dc_expect2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc_expect2))) + self.assertEqual(p_expect2, casting.dataclass_to_proto(casting.proto_to_dataclass(p_expect2))) + + def test_duration_proto_to_dataclass_and_back(self): + from sandbox.test import timeduration_pb2 + from sandbox.test import timeduration_dc + + p1 = timeduration_pb2.DurationMessage() + dc1 = timeduration_dc.DurationMessage() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + p_expect = timeduration_pb2.DurationMessage() + p_expect.my_duration.seconds = 987 + p_expect.my_duration.nanos = 654321000 + + dl_1 = p_expect.my_duration_list.add() + dl_1.seconds = 0 + dl_1.nanos = 42000 + + dl_2 = p_expect.my_duration_list.add() + dl_2.seconds = 100001 + + p_expect.my_duration_map['short'].nanos = 7000 + p_expect.my_duration_map['long'].seconds = 60*60*24*4 # 4 days + + dc_expect = timeduration_dc.DurationMessage(datetime.timedelta(seconds=987.654321), + [ + datetime.timedelta(seconds=0.000042), + datetime.timedelta(seconds=100001) + ], + { + 'short': datetime.timedelta(microseconds=7), + 'long': datetime.timedelta(days=4) + }) + + self.assertEqual(p_expect, casting.dataclass_to_proto(dc_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(p_expect)) + self.assertEqual(dc_expect, casting.proto_to_dataclass(casting.dataclass_to_proto(dc_expect))) + self.assertEqual(p_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p_expect))) + + def test_any_proto_to_dataclass_and_back(self): + from sandbox.test import rainbow_pb2 + from sandbox.test import rainbow_dc + from sandbox.test import anytest_pb2 + from sandbox.test import anytest_dc + + p1 = anytest_pb2.AnyMessage() + dc1 = anytest_dc.AnyMessage() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + p2_sub = rainbow_pb2.SubMessage() + p2_sub.foo = 'OOF' + p2_sub.bar = 'RAB' + + p2_expect = anytest_pb2.AnyMessage() + p2_expect.my_any.Pack(p2_sub) + + dc2_sub = rainbow_dc.SubMessage('OOF', 'RAB') + dc2 = anytest_dc.AnyMessage() + dc2.my_any = dc2_sub + + self.assertEqual(p2_sub, casting.dataclass_to_proto(dc2_sub)) + self.assertEqual(dc2_sub, casting.proto_to_dataclass(p2_sub)) + self.assertEqual(dc2_sub, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2_sub))) + self.assertEqual(p2_sub, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_sub))) + + self.assertEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2_expect)) + self.assertEqual(dc2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2))) + self.assertEqual(p2_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_expect))) + + i1 = p2_expect.my_any_list.add() + i1.Pack(casting.mkproto(rainbow_pb2.SubMessage, foo='too', bar='beer')) + i2 = p2_expect.my_any_list.add() + i2.Pack(casting.mkproto(rainbow_pb2.SubMessage, foo='three', bar='tequila')) + + self.assertNotEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertNotEqual(dc2, casting.proto_to_dataclass(p2_expect)) + + dc2.my_any_list = [rainbow_dc.SubMessage(foo='too', bar='beer'), rainbow_dc.SubMessage(foo='three', bar='tequila')] + + self.assertEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2_expect)) + self.assertEqual(dc2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2))) + self.assertEqual(p2_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_expect))) + + p2_expect.my_any_map['mars'].Pack(casting.mkproto(rainbow_pb2.SubMessage, foo='xy')) + p2_expect.my_any_map['venus'].Pack(casting.mkproto(rainbow_pb2.SubMessage, foo='xx')) + + self.assertNotEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertNotEqual(dc2, casting.proto_to_dataclass(p2_expect)) + + dc2.my_any_map['mars'] = rainbow_dc.SubMessage('xy') + dc2.my_any_map['venus'] = rainbow_dc.SubMessage('xx') + + self.assertEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2_expect)) + self.assertEqual(dc2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2))) + self.assertEqual(p2_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_expect))) + + def test_any_nested_proto_to_dataclass_and_back(self): + from sandbox.test import rainbow_pb2 + from sandbox.test import rainbow_dc + from sandbox.test import anytest_pb2 + from sandbox.test import anytest_dc + + p1 = anytest_pb2.AnyMessage() + dc1 = anytest_dc.AnyMessage() + + self.assertEqual(p1, casting.dataclass_to_proto(dc1)) + self.assertEqual(dc1, casting.proto_to_dataclass(p1)) + self.assertEqual(dc1, casting.proto_to_dataclass(casting.dataclass_to_proto(dc1))) + self.assertEqual(p1, casting.dataclass_to_proto(casting.proto_to_dataclass(p1))) + + p10 = rainbow_pb2.RainbowMessage() + p10.simple_field = 'Green' + p10.message_field.foo = 'Blue' + p10.message_field.bar = 'Yellow' + p10.simple_list.append('Four') + p10.simple_list.append('Seven') + p10.simple_list.append('99') + m7 = p10.message_list.add() + m7.foo = 'Foo in a list' + m7.bar = 'Candybar' + m8 = p10.message_list.add() + m8.foo = 'a Fool in a list' + m8.bar = 'Tequila bar' + p10.simple_map['dora'] = 'Imamap!' + p10.simple_map['diego'] = 'Camera!' + p10.message_map['mickey'].foo = 'mouse' + p10.message_map['donald'].foo = 'duck' + p10.message_map['donald'].bar = 'trump' + + dc10 = rainbow_dc.RainbowMessage() + dc10.simple_field = 'Green' + dc10.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc10.simple_list.append('Four') + dc10.simple_list.append('Seven') + dc10.simple_list.append('99') + dc10.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc10.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + dc10.simple_map['dora'] = 'Imamap!' + dc10.simple_map['diego'] = 'Camera!' + dc10.message_map['mickey'] = rainbow_dc.SubMessage(foo='mouse') + dc10.message_map['donald'] = rainbow_dc.SubMessage(foo='duck', bar='trump') + + self.assertEqual(p10, casting.dataclass_to_proto(dc10)) + self.assertEqual(dc10, casting.proto_to_dataclass(p10)) + self.assertEqual(dc10, casting.proto_to_dataclass(casting.dataclass_to_proto(dc10))) + self.assertEqual(p10, casting.dataclass_to_proto(casting.proto_to_dataclass(p10))) + + p2_expect = anytest_pb2.AnyMessage() + p2_expect.my_any.Pack(p10) + + dc2 = anytest_dc.AnyMessage() + dc2.my_any = dc10 + + self.assertEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2_expect)) + self.assertEqual(dc2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2))) + self.assertEqual(p2_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_expect))) + + i1 = p2_expect.my_any_list.add() + i1.Pack(p10) + i2 = p2_expect.my_any_list.add() + i2.Pack(p10) + + self.assertNotEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertNotEqual(dc2, casting.proto_to_dataclass(p2_expect)) + + dc2.my_any_list = [dc10, dc10] + + self.assertEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2_expect)) + self.assertEqual(dc2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2))) + self.assertEqual(p2_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_expect))) + + p2_expect.my_any_map['mars'].Pack(p10) + p2_expect.my_any_map['venus'].Pack(p10) + + self.assertNotEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertNotEqual(dc2, casting.proto_to_dataclass(p2_expect)) + + dc2.my_any_map['mars'] = dc10 + dc2.my_any_map['venus'] = dc10 + + self.assertEqual(p2_expect, casting.dataclass_to_proto(dc2)) + self.assertEqual(dc2, casting.proto_to_dataclass(p2_expect)) + self.assertEqual(dc2, casting.proto_to_dataclass(casting.dataclass_to_proto(dc2))) + self.assertEqual(p2_expect, casting.dataclass_to_proto(casting.proto_to_dataclass(p2_expect))) + + def test_cloning(self): + from sandbox.test import clones_dc + + thing_1 = clones_dc.ThingOne( + my_string='this is a string', + my_number=42, + my_float=4.2, + my_timestamp=datetime.datetime(2019, 3, 28, 10, 13, 8, 123456), + my_subthing=clones_dc.SubThing(foo='F', bar='B'), + my_unique_string='I REFUSE TO BE CLONED!!!' + ) + + thing_2 = clones_dc.ThingTwo.from_clone(thing_1) + + self.assertEqual(thing_1.my_string, thing_2.my_string) + self.assertEqual(thing_1.my_number, thing_2.my_number) + self.assertEqual(thing_1.my_float, thing_2.my_float) + self.assertEqual(thing_1.my_timestamp, thing_2.my_timestamp) + self.assertEqual(thing_1.my_subthing, thing_2.my_subthing) + + self.assertNotEqual(thing_1.my_unique_string, thing_2.my_special_string) diff --git a/tests/test_castutils.py b/tests/test_castutils.py new file mode 100644 index 0000000..8167025 --- /dev/null +++ b/tests/test_castutils.py @@ -0,0 +1,799 @@ +import unittest +import datetime +import base64 +import protoplasm.casting +from protoplasm.casting import castutils +import os +import sys +import shutil +import time + +from sandbox.test import rainbow_pb2 +from sandbox.test import rainbow_dc +from sandbox.test import enums_pb2 +from sandbox.test import enums_dc + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +HERE = os.path.dirname(__file__) + +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + + +class CastutilsTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_kwdict(self): + self.assertEqual({}, castutils.kwdict()) + self.assertEqual({'foo': 'bar'}, castutils.kwdict(foo='bar')) + self.assertEqual({'foo': 'bar', 'bar': 'foo'}, castutils.kwdict(foo='bar', bar='foo')) + self.assertEqual({'bar': 1, 'foo': 'two', 'oof': None}, castutils.kwdict(oof=None, foo='two', bar=1)) + + self.assertNotEqual({'bar': 1, 'foo': 'two'}, castutils.kwdict(oof=None, foo='two', bar=1)) + + def test_nested_kwdict(self): + self.assertEqual({'foo': 'bar', 'bar': {'one': 1, 'two': 2}}, castutils.kwdict(foo='bar', bar=castutils.kwdict(one=1, two=2))) + self.assertEqual({'foo': 'bar', 'bar': {'one': 1, 'two': 2}}, castutils.kwdict(foo='bar', bar__one=1, bar__two=2)) + self.assertEqual({'foo': 'bar', 'bar': {'one': 1, 'two': {'more': 2, 'less': 3}}}, castutils.kwdict(foo='bar', bar__one=1, bar__two__more=2, bar__two__less=3)) + + self.assertNotEqual({'foo': 'bar', 'bar': {'one': 1, 'two': {'more': 2, 'less': 3}}}, castutils.kwdict(foo='bar', bar__one=1, bar__two__more=2, bar__two_less=3)) + + def test_big_kwdict(self): + expected = {'simple_field': 'Green', + 'message_field': { + 'foo': 'Blue', + 'bar': 'Yellow' + }, + 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', + 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'simple_map': {'dora': 'Imamap!', + 'diego': 'Camera!'}, + 'message_map': {'mickey': {'foo': 'mouse'}, + 'donald': {'foo': 'duck', + 'bar': 'trump'}}} + + self.assertEqual(expected, + castutils.kwdict(simple_map__diego='Camera!', + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + message_map__donald__foo='duck', + message_map__donald__bar='trump', + message_map__mickey__foo='mouse')) + + def test_mkproto(self): + p1 = rainbow_pb2.SubMessage() + p1.foo = 'Foo!' + p1.bar = 'Bar!!!' + + self.assertEqual(p1, protoplasm.casting.mkproto(rainbow_pb2.SubMessage, foo='Foo!', bar='Bar!!!')) + + p2 = rainbow_pb2.SubMessage() + p2.foo = 'Foo Two!' + self.assertEqual(p2, protoplasm.casting.mkproto(rainbow_pb2.SubMessage, foo='Foo Two!')) + + p3 = rainbow_pb2.SubMessage() + p3.foo = 'Not Foo!' + self.assertNotEqual(p3, protoplasm.casting.mkproto(rainbow_pb2.SubMessage, foo='Foo You!')) + + p4 = rainbow_pb2.SubMessage() + self.assertEqual(p4, protoplasm.casting.mkproto(rainbow_pb2.SubMessage)) + + def test_nested_mkproto(self): + p1 = rainbow_pb2.RainbowMessage() + self.assertEqual(p1, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage)) + + p2 = rainbow_pb2.RainbowMessage() + p2.simple_field = 'Green' + self.assertEqual(p2, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, simple_field='Green')) + self.assertNotEqual(p1, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, simple_field='Green')) + + p3 = rainbow_pb2.RainbowMessage() + p3.simple_field = 'Green' + p3.message_field.foo = 'Blue' + self.assertEqual(p3, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue')) + p4 = rainbow_pb2.RainbowMessage() + p4.simple_field = 'Green' + p4.message_field.foo = 'Blue' + p4.message_field.bar = 'Yellow' + self.assertEqual(p4, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow')) + self.assertNotEqual(p3, p4) + self.assertNotEqual(p4, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Red')) + + p5 = rainbow_pb2.RainbowMessage() + p5.simple_field = 'Green' + p5.message_field.foo = 'Blue' + p5.message_field.bar = 'Yellow' + p5.simple_list.append('Four') + p5.simple_list.append('Seven') + p5.simple_list.append('99') + self.assertEqual(p5, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'])) + + p6 = rainbow_pb2.RainbowMessage() + p6.simple_field = 'Green' + p6.message_field.foo = 'Blue' + p6.message_field.bar = 'Yellow' + p6.simple_list.append('Four') + p6.simple_list.append('Seven') + p6.simple_list.append('98') + self.assertNotEqual(p6, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '100'])) + + p7 = rainbow_pb2.RainbowMessage() + p7.simple_field = 'Green' + p7.message_field.foo = 'Blue' + p7.message_field.bar = 'Yellow' + p7.simple_list.append('Four') + p7.simple_list.append('Seven') + p7.simple_list.append('99') + m1 = p7.message_list.add() + m1.foo = 'Foo in a list' + m1.bar = 'Candybar' + m2 = p7.message_list.add() + m2.foo = 'a Fool in a list' + m2.bar = 'Tequila bar' + self.assertEqual(p7, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')])) + + self.assertNotEqual(p7, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')])) + + p8 = rainbow_pb2.RainbowMessage() + p8.simple_field = 'Green' + p8.message_field.foo = 'Blue' + p8.message_field.bar = 'Yellow' + p8.simple_list.append('Four') + p8.simple_list.append('Seven') + p8.simple_list.append('99') + m3 = p8.message_list.add() + m3.foo = 'Foo in a list' + m3.bar = 'Candybar' + m4 = p8.message_list.add() + m4.foo = 'a Fool in a list' + m4.bar = 'Tequila bar' + p8.simple_map['dora'] = 'Imamap!' + p8.simple_map['diego'] = 'Camera!' + self.assertEqual(p8, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + simple_map__diego='Camera!')) + + p9 = rainbow_pb2.RainbowMessage() + p9.simple_field = 'Green' + p9.message_field.foo = 'Blue' + p9.message_field.bar = 'Yellow' + p9.simple_list.append('Four') + p9.simple_list.append('Seven') + p9.simple_list.append('99') + m5 = p9.message_list.add() + m5.foo = 'Foo in a list' + m5.bar = 'Candybar' + m6 = p9.message_list.add() + m6.foo = 'a Fool in a list' + m6.bar = 'Tequila bar' + p9.simple_map['dora'] = 'Imamap!' + p9.simple_map['diego'] = 'Camera!' + p9.message_map['donald'].foo = 'duck' + p9.message_map['donald'].bar = 'trump' + p9.message_map['mickey'].foo = 'mouse' + self.assertEqual(p9, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + simple_map__diego='Camera!', + message_map__donald__foo='duck', + message_map__donald__bar='trump', + message_map__mickey__foo='mouse')) + + p10 = rainbow_pb2.RainbowMessage() + p10.simple_field = 'Green' + p10.message_field.foo = 'Blue' + p10.message_field.bar = 'Yellow' + p10.simple_list.append('Four') + p10.simple_list.append('Seven') + p10.simple_list.append('99') + m7 = p10.message_list.add() + m7.foo = 'Foo in a list' + m7.bar = 'Candybar' + m8 = p10.message_list.add() + m8.foo = 'a Fool in a list' + m8.bar = 'Tequila bar' + p10.simple_map['dora'] = 'Imamap!' + p10.simple_map['diego'] = 'Camera!' + p10.message_map['mickey'].foo = 'mouse' + p10.message_map['donald'].foo = 'duck' + p10.message_map['donald'].bar = 'trump' + + self.assertEqual(p10, protoplasm.casting.mkproto(rainbow_pb2.RainbowMessage, + simple_map__diego='Camera!', + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + message_map__donald__foo='duck', + message_map__donald__bar='trump', + message_map__mickey__foo='mouse')) + + def test_mkdataclass(self): + dc1 = rainbow_dc.SubMessage() + dc1.foo = 'Foo!' + dc1.bar = 'Bar!!!' + + self.assertEqual(dc1, protoplasm.casting.mkdataclass(rainbow_dc.SubMessage, foo='Foo!', bar='Bar!!!')) + + dc2 = rainbow_dc.SubMessage() + dc2.foo = 'Foo Two!' + self.assertEqual(dc2, protoplasm.casting.mkdataclass(rainbow_dc.SubMessage, foo='Foo Two!')) + + dc3 = rainbow_dc.SubMessage() + dc3.foo = 'Not Foo!' + self.assertNotEqual(dc3, protoplasm.casting.mkdataclass(rainbow_dc.SubMessage, foo='Foo You!')) + + dc4 = rainbow_dc.SubMessage() + self.assertEqual(dc4, protoplasm.casting.mkdataclass(rainbow_dc.SubMessage)) + + def test_nested_mkdataclass(self): + dc1 = rainbow_dc.RainbowMessage() + self.assertEqual(dc1, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage)) + + dc2 = rainbow_dc.RainbowMessage() + dc2.simple_field = 'Green' + self.assertEqual(dc2, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, simple_field='Green')) + self.assertNotEqual(dc1, + protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, simple_field='Green')) + + dc3 = rainbow_dc.RainbowMessage() + dc3.simple_field = 'Green' + dc3.message_field = rainbow_dc.SubMessage() + dc3.message_field.foo = 'Blue' + + expected_dc3 = protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue') + + self.assertEqual(dc3, expected_dc3) + + dc4 = rainbow_dc.RainbowMessage() + dc4.simple_field = 'Green' + dc4.message_field = rainbow_dc.SubMessage() + dc4.message_field.foo = 'Blue' + dc4.message_field.bar = 'Yellow' + self.assertEqual(dc4, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow')) + self.assertNotEqual(dc3, dc4) + self.assertNotEqual(dc4, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Red')) + + dc5 = rainbow_dc.RainbowMessage() + dc5.simple_field = 'Green' + dc5.message_field = rainbow_dc.SubMessage('Blue', 'Yellow') + dc5.simple_list.append('Four') + dc5.simple_list.append('Seven') + dc5.simple_list.append('99') + self.assertEqual(dc5, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'])) + + dc6 = rainbow_dc.RainbowMessage() + dc6.simple_field = 'Green' + dc6.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc6.simple_list.append('Four') + dc6.simple_list.append('Seven') + dc6.simple_list.append('98') + self.assertNotEqual(dc6, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '100'])) + + dc7 = rainbow_dc.RainbowMessage() + dc7.simple_field = 'Green' + dc7.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc7.simple_list.append('Four') + dc7.simple_list.append('Seven') + dc7.simple_list.append('99') + + dc7.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc7.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + + self.assertEqual(dc7, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')])) + + self.assertNotEqual(dc7, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')])) + + dc8 = rainbow_dc.RainbowMessage() + dc8.simple_field = 'Green' + dc8.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc8.simple_list.append('Four') + dc8.simple_list.append('Seven') + dc8.simple_list.append('99') + + dc8.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc8.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + + dc8.simple_map['dora'] = 'Imamap!' + dc8.simple_map['diego'] = 'Camera!' + self.assertEqual(dc8, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + simple_map__diego='Camera!' + )) + + dc9 = rainbow_dc.RainbowMessage() + dc9.simple_field = 'Green' + dc9.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc9.simple_list.append('Four') + dc9.simple_list.append('Seven') + dc9.simple_list.append('99') + dc9.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc9.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + dc9.simple_map['dora'] = 'Imamap!' + dc9.simple_map['diego'] = 'Camera!' + + dc9.message_map['donald'] = rainbow_dc.SubMessage(foo='duck', bar='trump') + + dc9.message_map['mickey'] = rainbow_dc.SubMessage(foo='mouse') + + expected_dc9 = protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + simple_map__diego='Camera!', + message_map__donald__foo='duck', + message_map__donald__bar='trump', + message_map__mickey__foo='mouse' + ) + + self.assertEqual(dc9, expected_dc9) + + dc10 = rainbow_dc.RainbowMessage() + dc10.simple_field = 'Green' + dc10.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc10.simple_list.append('Four') + dc10.simple_list.append('Seven') + dc10.simple_list.append('99') + dc10.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc10.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + dc10.simple_map['dora'] = 'Imamap!' + dc10.simple_map['diego'] = 'Camera!' + + dc10.message_map['mickey'] = rainbow_dc.SubMessage(foo='mouse') + dc10.message_map['donald'] = rainbow_dc.SubMessage(foo='duck', bar='trump') + + self.assertEqual(dc10, protoplasm.casting.mkdataclass(rainbow_dc.RainbowMessage, + simple_map__diego='Camera!', + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + message_map__donald__foo='duck', + message_map__donald__bar='trump', + message_map__mickey__foo='mouse' + )) + + def test_import_dataclass(self): + self.assertEqual(rainbow_dc.SubMessage, castutils.import_dataclass_by_proto(rainbow_pb2.SubMessage)) + self.assertEqual(rainbow_dc.SubMessage, castutils.import_dataclass_by_proto(rainbow_pb2.SubMessage())) + self.assertEqual(rainbow_dc.RainbowMessage, castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage)) + self.assertEqual(rainbow_dc.RainbowMessage, castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage())) + self.assertEqual(castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage()), castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage())) + self.assertEqual(castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage), castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage())) + self.assertNotEqual(castutils.import_dataclass_by_proto(rainbow_pb2.SubMessage), castutils.import_dataclass_by_proto(rainbow_pb2.RainbowMessage)) + + def test_timestamp_mkproto(self): + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00.000000Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts4 = '2049-01-01T12:00:00.000000Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts5 = '2049-01-01T00:00:00.000000Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + p_expect = rainbow_pb2.TimestampMessage() + p_expect.my_timestamp.FromJsonString(ts1) + p_expect.my_timestamp_list.add().FromJsonString(ts2) + p_expect.my_timestamp_list.add().FromJsonString(ts3) + p_expect.my_timestamp_map['noon'].FromJsonString(ts4) + p_expect.my_timestamp_map['midnight'].FromJsonString(ts5) + + self.assertEqual(p_expect, protoplasm.casting.mkproto(rainbow_pb2.TimestampMessage, my_timestamp=ts1, + my_timestamp_list=[ts2, ts3], + my_timestamp_map__noon=ts4, + my_timestamp_map__midnight=ts5)) + + def test_timestamp_mkdataclass(self): + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00.000000Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts4 = '2049-01-01T12:00:00.000000Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts5 = '2049-01-01T00:00:00.000000Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + dc_expect = rainbow_dc.TimestampMessage() + dc_expect.my_timestamp = dt1 + dc_expect.my_timestamp_list.append(dt2) + dc_expect.my_timestamp_list.append(dt3) + dc_expect.my_timestamp_map['noon'] = dt4 + dc_expect.my_timestamp_map['midnight'] = dt5 + + self.assertEqual(dc_expect, protoplasm.casting.mkdataclass(rainbow_dc.TimestampMessage, + my_timestamp=dt1, + my_timestamp_list=[dt2, dt3], + my_timestamp_map__noon=dt4, + my_timestamp_map__midnight=dt5)) + + def test_byte_mkproto(self): + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + p_expect = rainbow_pb2.BytesMessage() + p_expect.my_bytes = as_bytes_1 + p_expect.my_bytes_list.append(as_bytes_2) + p_expect.my_bytes_list.append(as_bytes_3) + p_expect.my_bytes_map['zero'] = as_bytes_4 + p_expect.my_bytes_map['one'] = as_bytes_5 + + self.assertEqual(p_expect, protoplasm.casting.mkproto(rainbow_pb2.BytesMessage, + my_bytes=as_base64_1, + my_bytes_list=[as_base64_2, as_base64_3], + my_bytes_map__zero=as_base64_4, + my_bytes_map__one=as_base64_5)) + + def test_byte_mkdataclass(self): + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + dc_expect = rainbow_dc.BytesMessage() + dc_expect.my_bytes = as_bytes_1 + dc_expect.my_bytes_list.append(as_bytes_2) + dc_expect.my_bytes_list.append(as_bytes_3) + dc_expect.my_bytes_map['zero'] = as_bytes_4 + dc_expect.my_bytes_map['one'] = as_bytes_5 + + self.assertEqual(dc_expect, protoplasm.casting.mkdataclass(rainbow_dc.BytesMessage, + my_bytes=as_bytes_1, + my_bytes_list=[as_bytes_2, as_base64_3], + my_bytes_map__zero=as_bytes_4, + my_bytes_map__one=as_base64_5)) + + def test_enum_mkproto(self): + p_expect = enums_pb2.WithExternalEnum() + p_expect.my_enum = enums_pb2.TWO + + p_expect.my_enum_list.append(enums_pb2.ONE) + p_expect.my_enum_list.append(enums_pb2.THREE) + + p_expect.my_enum_map['one'] = enums_pb2.ONE + p_expect.my_enum_map['two'] = enums_pb2.TWO + + p_expect.my_alias_enum = enums_pb2.SIX + + p_expect.my_alias_enum_list.append(enums_pb2.FOUR) + p_expect.my_alias_enum_list.append(enums_pb2.FIVE) + p_expect.my_alias_enum_list.append(enums_pb2.FJORIR) + + p_expect.my_alias_enum_map['six'] = enums_pb2.SIX + p_expect.my_alias_enum_map['sex'] = enums_pb2.SEX + p_expect.my_alias_enum_map['fimm'] = enums_pb2.FIVE + + self.assertEqual(p_expect, protoplasm.casting.mkproto(enums_pb2.WithExternalEnum, + my_enum=2, + my_alias_enum=3, + my_enum_list=[1, 3], + my_alias_enum_list=[1, 2, 1], + my_enum_map__one=1, + my_enum_map__two=2, + my_alias_enum_map__six=3, + my_alias_enum_map__sex=3, + my_alias_enum_map__fimm=2)) + self.assertEqual(p_expect, protoplasm.casting.mkproto(enums_pb2.WithExternalEnum, + my_enum=enums_pb2.TWO, + my_alias_enum=enums_pb2.SIX, + my_enum_list=[enums_pb2.ONE, enums_pb2.THREE], + my_alias_enum_list=[enums_pb2.FOUR, enums_pb2.FIVE, enums_pb2.FJORIR], + my_enum_map__one=enums_pb2.ONE, + my_enum_map__two=enums_pb2.TWO, + my_alias_enum_map__six=enums_pb2.SIX, + my_alias_enum_map__sex=enums_pb2.SEX, + my_alias_enum_map__fimm=enums_pb2.FIMM)) + + p_expect2 = enums_pb2.WithInternalEnum() + p_expect2.my_internal_enum = enums_pb2.WithInternalEnum.SIX + + p_expect2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FIVE) + p_expect2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FOUR) + + p_expect2.my_internal_enum_map['no5'] = enums_pb2.WithInternalEnum.FIVE + p_expect2.my_internal_enum_map['no6'] = enums_pb2.WithInternalEnum.SIX + + p_expect2.my_internal_alias_enum = enums_pb2.WithInternalEnum.SEVEN + + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.SJO) + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.ATTA) + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.EIGHT) + + p_expect2.my_internal_alias_enum_map['no9'] = enums_pb2.WithInternalEnum.NIU + p_expect2.my_internal_alias_enum_map['no9B'] = enums_pb2.WithInternalEnum.NINE + p_expect2.my_internal_alias_enum_map['default'] = enums_pb2.WithInternalEnum.ZERO + + self.assertEqual(p_expect2, protoplasm.casting.mkproto(enums_pb2.WithInternalEnum, + my_internal_enum=enums_pb2.WithInternalEnum.SIX, + my_internal_alias_enum=enums_pb2.WithInternalEnum.SEVEN, + my_internal_enum_list=[enums_pb2.WithInternalEnum.FIVE, + enums_pb2.WithInternalEnum.FOUR], + my_internal_alias_enum_list=[enums_pb2.WithInternalEnum.SJO, + enums_pb2.WithInternalEnum.ATTA, + enums_pb2.WithInternalEnum.EIGHT], + my_internal_enum_map__no5=enums_pb2.WithInternalEnum.FIVE, + my_internal_enum_map__no6=enums_pb2.WithInternalEnum.SIX, + my_internal_alias_enum_map__no9=enums_pb2.WithInternalEnum.NIU, + my_internal_alias_enum_map__no9B=enums_pb2.WithInternalEnum.NINE, + my_internal_alias_enum_map__default=enums_pb2.WithInternalEnum.ZERO)) + + self.assertEqual(p_expect2, protoplasm.casting.mkproto(enums_pb2.WithInternalEnum, + my_internal_enum=6, + my_internal_alias_enum=7, + my_internal_enum_list=[5, 4], + my_internal_alias_enum_list=[7, 8, 8], + my_internal_enum_map__no5=5, + my_internal_enum_map__no6=6, + my_internal_alias_enum_map__no9=9, + my_internal_alias_enum_map__no9B=9, + my_internal_alias_enum_map__default=0)) + + def test_enum_mkdataclass(self): + dc_expect = enums_dc.WithExternalEnum() + dc_expect.my_enum = enums_dc.TWO + + dc_expect.my_enum_list.append(enums_dc.ONE) + dc_expect.my_enum_list.append(enums_dc.THREE) + + dc_expect.my_enum_map['one'] = enums_dc.ONE + dc_expect.my_enum_map['two'] = enums_dc.TWO + + dc_expect.my_alias_enum = enums_dc.SIX + + dc_expect.my_alias_enum_list.append(enums_dc.FOUR) + dc_expect.my_alias_enum_list.append(enums_dc.FIVE) + dc_expect.my_alias_enum_list.append(enums_dc.FJORIR) + + dc_expect.my_alias_enum_map['six'] = enums_dc.SIX + dc_expect.my_alias_enum_map['sex'] = enums_dc.SEX + dc_expect.my_alias_enum_map['fimm'] = enums_dc.FIVE + + self.assertEqual(dc_expect, protoplasm.casting.mkdataclass(enums_dc.WithExternalEnum, + my_enum=2, + my_alias_enum=3, + my_enum_list=[1, 3], + my_alias_enum_list=[1, 2, 1], + my_enum_map__one=1, + my_enum_map__two=2, + my_alias_enum_map__six=3, + my_alias_enum_map__sex=3, + my_alias_enum_map__fimm=2)) + self.assertEqual(dc_expect, protoplasm.casting.mkdataclass(enums_dc.WithExternalEnum, + my_enum=enums_dc.TWO, + my_alias_enum=enums_dc.SIX, + my_enum_list=[enums_dc.ONE, enums_dc.THREE], + my_alias_enum_list=[enums_dc.FOUR, enums_dc.FIVE, + enums_dc.FJORIR], + my_enum_map__one=enums_dc.ONE, + my_enum_map__two=enums_dc.TWO, + my_alias_enum_map__six=enums_dc.SIX, + my_alias_enum_map__sex=enums_dc.SEX, + my_alias_enum_map__fimm=enums_dc.FIMM)) + + dc_expect2 = enums_dc.WithInternalEnum() + dc_expect2.my_internal_enum = enums_dc.WithInternalEnum.SIX + + dc_expect2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FIVE) + dc_expect2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FOUR) + + dc_expect2.my_internal_enum_map['no5'] = enums_dc.WithInternalEnum.FIVE + dc_expect2.my_internal_enum_map['no6'] = enums_dc.WithInternalEnum.SIX + + dc_expect2.my_internal_alias_enum = enums_dc.WithInternalEnum.SEVEN + + dc_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.SJO) + dc_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.ATTA) + dc_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.EIGHT) + + dc_expect2.my_internal_alias_enum_map['no9'] = enums_dc.WithInternalEnum.NIU + dc_expect2.my_internal_alias_enum_map['no9B'] = enums_dc.WithInternalEnum.NINE + dc_expect2.my_internal_alias_enum_map['default'] = enums_dc.WithInternalEnum.ZERO + + self.assertEqual(dc_expect2, protoplasm.casting.mkdataclass(enums_dc.WithInternalEnum, + my_internal_enum=enums_dc.WithInternalEnum.SIX, + my_internal_alias_enum=enums_dc.WithInternalEnum.SEVEN, + my_internal_enum_list=[enums_dc.WithInternalEnum.FIVE, + enums_dc.WithInternalEnum.FOUR], + my_internal_alias_enum_list=[ + enums_dc.WithInternalEnum.SJO, + enums_dc.WithInternalEnum.ATTA, + enums_dc.WithInternalEnum.EIGHT], + my_internal_enum_map__no5=enums_dc.WithInternalEnum.FIVE, + my_internal_enum_map__no6=enums_dc.WithInternalEnum.SIX, + my_internal_alias_enum_map__no9=enums_dc.WithInternalEnum.NIU, + my_internal_alias_enum_map__no9B=enums_dc.WithInternalEnum.NINE, + my_internal_alias_enum_map__default=enums_dc.WithInternalEnum.ZERO)) + + self.assertEqual(dc_expect2, protoplasm.casting.mkdataclass(enums_dc.WithInternalEnum, + my_internal_enum=6, + my_internal_alias_enum=7, + my_internal_enum_list=[5, 4], + my_internal_alias_enum_list=[7, 8, 8], + my_internal_enum_map__no5=5, + my_internal_enum_map__no6=6, + my_internal_alias_enum_map__no9=9, + my_internal_alias_enum_map__no9B=9, + my_internal_alias_enum_map__default=0)) diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py new file mode 100644 index 0000000..4ff5bbc --- /dev/null +++ b/tests/test_dataclasses.py @@ -0,0 +1,127 @@ +import unittest +from protoplasm import casting +import os +import sys + +import shutil +import time +HERE = os.path.dirname(__file__) +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + + +def _get_sub(): + from sandbox.test import rainbow_dc + return rainbow_dc.SubMessage.from_dict({'foo': 'Foo!', 'bar': 'Bar!'}) + + +class DataclassTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_args(self): + from sandbox.test import rainbow_dc + + dc1 = rainbow_dc.RainbowMessage() + dc1.simple_field = 'I iz string' + dc1.message_field = rainbow_dc.SubMessage.from_dict({'foo': 'Foo!', 'bar': 'Bar!'}) + dc1.simple_list = ['one', 'two', "Freddy's coming for you!"] + dc1.message_list = [rainbow_dc.SubMessage(foo='Foo1!', bar='Bar1!'), + rainbow_dc.SubMessage(foo='Foo2!', bar='Bar2!'), + rainbow_dc.SubMessage(foo='Foo3!', bar='Bar3!')] + dc1.simple_map = {'uno': 'einn', 'dos': 'tveir'} + dc1.message_map = {'ein': rainbow_dc.SubMessage(foo='Foo11!', bar='Bar11!'), + 'zwei': rainbow_dc.SubMessage(foo='Foo22!', bar='Bar22!')} + + dc2 = rainbow_dc.RainbowMessage('I iz string', + rainbow_dc.SubMessage(foo='Foo!', bar='Bar!'), + ['one', 'two', "Freddy's coming for you!"], + [rainbow_dc.SubMessage(foo='Foo1!', bar='Bar1!'), + rainbow_dc.SubMessage(foo='Foo2!', bar='Bar2!'), + rainbow_dc.SubMessage(foo='Foo3!', bar='Bar3!')], + {'uno': 'einn', 'dos': 'tveir'}, + {'ein': rainbow_dc.SubMessage(foo='Foo11!', bar='Bar11!'), + 'zwei': rainbow_dc.SubMessage(foo='Foo22!', bar='Bar22!')}) + + self.assertEqual(dc1, dc2) + + def test_shortcut_args(self): + from sandbox.test import rainbow_dc + + dc1 = rainbow_dc.RainbowMessage('I iz string', 'Foo!') + dc2 = rainbow_dc.RainbowMessage('I iz string', rainbow_dc.SubMessage(foo='Foo!')) + + dc2b = rainbow_dc.RainbowMessage.from_kwdict(simple_field='I iz string', message_field='Foo!') + + self.assertEqual(dc1, dc2) + self.assertEqual(dc1, dc2b) + + dc3 = rainbow_dc.RainbowMessage('I iz string', ('Foo!', 'Bar!')) + dc4 = rainbow_dc.RainbowMessage('I iz string', {'foo': 'Foo!', 'bar': 'Bar!'}) + dc5 = rainbow_dc.RainbowMessage('I iz string', rainbow_dc.SubMessage(foo='Foo!', bar='Bar!')) + dc5b = rainbow_dc.RainbowMessage.from_kwdict(simple_field='I iz string', message_field=('Foo!', 'Bar!')) + + self.assertEqual(dc3, dc4) + self.assertEqual(dc3, dc5) + self.assertEqual(dc3, dc5b) + self.assertNotEqual(dc1, dc3) + + def test_shortcut_list_args(self): + from sandbox.test import rainbow_dc + dc1 = rainbow_dc.RainbowMessage(message_list=['Foo!', 'Foo2!']) + dc2 = rainbow_dc.RainbowMessage(message_list=[rainbow_dc.SubMessage(foo='Foo!'), + rainbow_dc.SubMessage(foo='Foo2!')]) + + self.assertEqual(dc1, dc2) + + dc3 = rainbow_dc.RainbowMessage(message_list=[('Foo!', 'Bar!'), ('Foo2!', 'Bar2!')]) + dc4 = rainbow_dc.RainbowMessage(message_list=[{'foo': 'Foo!', 'bar': 'Bar!'}, {'foo': 'Foo2!', 'bar': 'Bar2!'}]) + dc5 = rainbow_dc.RainbowMessage(message_list=[rainbow_dc.SubMessage(foo='Foo!', bar='Bar!'), + rainbow_dc.SubMessage(foo='Foo2!', bar='Bar2!')]) + dc6 = rainbow_dc.RainbowMessage(message_list=[('Foo!', 'Bar!'), ('Foo2!', 'Bar2!')]) + dc7 = rainbow_dc.RainbowMessage(message_list=[('Foo!', 'Bar!'), {'foo': 'Foo2!', 'bar': 'Bar2!'}]) + dc8 = rainbow_dc.RainbowMessage(message_list=[rainbow_dc.SubMessage(foo='Foo!', bar='Bar!'), {'foo': 'Foo2!', 'bar': 'Bar2!'}]) + self.assertEqual(dc3, dc4) + self.assertEqual(dc3, dc5) + self.assertEqual(dc3, dc6) + self.assertEqual(dc3, dc7) + self.assertEqual(dc3, dc8) + self.assertNotEqual(dc1, dc3) + + def test_shortcut_dict_args(self): + from sandbox.test import rainbow_dc + dc1 = rainbow_dc.RainbowMessage(message_map={'one': 'Foo!', 'two': 'Foo2!'}) + dc2 = rainbow_dc.RainbowMessage(message_map={'one': rainbow_dc.SubMessage(foo='Foo!'), + 'two': rainbow_dc.SubMessage(foo='Foo2!')}) + dc2b = rainbow_dc.RainbowMessage.from_kwdict(message_map={'one': 'Foo!', 'two': 'Foo2!'}) + + self.assertEqual(dc1, dc2) + self.assertEqual(dc1, dc2b) + + dc3 = rainbow_dc.RainbowMessage(message_map={'one': ('Foo!', 'Bar!'), 'two': ('Foo2!', 'Bar2!')}) + dc4 = rainbow_dc.RainbowMessage(message_map={'one': {'foo': 'Foo!', 'bar': 'Bar!'}, 'two': {'foo': 'Foo2!', 'bar': 'Bar2!'}}) + dc5 = rainbow_dc.RainbowMessage(message_map={'one': rainbow_dc.SubMessage(foo='Foo!', bar='Bar!'), + 'two': rainbow_dc.SubMessage(foo='Foo2!', bar='Bar2!')}) + self.assertEqual(dc3, dc4) + self.assertEqual(dc3, dc5) + self.assertNotEqual(dc1, dc3) diff --git a/tests/test_dictator.py b/tests/test_dictator.py new file mode 100644 index 0000000..c4c3318 --- /dev/null +++ b/tests/test_dictator.py @@ -0,0 +1,425 @@ +import datetime +import unittest +import base64 +import os +import sys + +from protoplasm import casting +from protoplasm.casting import dictator +from protoplasm.casting import castutils + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +import shutil +import time +HERE = os.path.dirname(__file__) +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + + + +class ProtoToDictTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_proto_to_dict(self): + from sandbox.test import rainbow_pb2 + + expected = {'simple_field': 'Green', + 'message_field': { + 'foo': 'Blue', + 'bar': 'Yellow' + }, + 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', + 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'simple_map': {'dora': 'Imamap!', + 'diego': 'Camera!'}, + 'message_map': {'mickey': {'foo': 'mouse'}, + 'donald': {'foo': 'duck', + 'bar': 'trump'}}} + + p11 = casting.mkproto(rainbow_pb2.RainbowMessage, + simple_map__diego='Camera!', + simple_field='Green', + message_field__foo='Blue', + message_field__bar='Yellow', + simple_list=['Four', 'Seven', '99'], + message_list=[castutils.kwdict(foo='Foo in a list', + bar='Candybar'), + castutils.kwdict(foo='a Fool in a list', + bar='Tequila bar')], + simple_map__dora='Imamap!', + message_map__donald__foo='duck', + message_map__donald__bar='trump', + message_map__mickey__foo='mouse') + + self.assertEqual(expected, dictator.proto_to_dict(p11)) + + def test_proto_timestamp_to_dict(self): + from sandbox.test import rainbow_pb2 + + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%SZ')) + + ts4 = '2049-01-01T12:00:00Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%SZ')) + + ts5 = '2049-01-01T00:00:00Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%SZ')) + + expected = {'my_timestamp': ts1, + 'my_timestamp_list': [ts2, ts3], + 'my_timestamp_map': { + 'noon': ts4, + 'midnight': ts5}} + + p12 = casting.mkproto(rainbow_pb2.TimestampMessage, + my_timestamp=ts1, + my_timestamp_list=[ts2, ts3], + my_timestamp_map__noon=ts4, + my_timestamp_map__midnight=ts5) + + self.assertEqual(expected, dictator.proto_to_dict(p12)) + + def test_proto_bytes_to_dict(self): + from sandbox.test import rainbow_pb2 + + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + expected = {'my_bytes': as_base64_1, + 'my_bytes_list': [as_base64_2, as_base64_3], + 'my_bytes_map': { + 'zero': as_base64_4, + 'one': as_base64_5}} + + p13 = casting.mkproto(rainbow_pb2.BytesMessage, + my_bytes=as_base64_1, + my_bytes_list=[as_base64_2, as_base64_3], + my_bytes_map__zero=as_base64_4, + my_bytes_map__one=as_base64_5) + + self.assertEqual(expected, dictator.proto_to_dict(p13)) + + def test_proto_enums_to_dict(self): + from sandbox.test import enums_pb2 + + p1 = enums_pb2.WithExternalEnum() + p1.my_enum = enums_pb2.TWO + + p1.my_enum_list.append(enums_pb2.ONE) + p1.my_enum_list.append(enums_pb2.THREE) + + p1.my_enum_map['one'] = enums_pb2.ONE + p1.my_enum_map['two'] = enums_pb2.TWO + + p1.my_alias_enum = enums_pb2.SIX + + p1.my_alias_enum_list.append(enums_pb2.FOUR) + p1.my_alias_enum_list.append(enums_pb2.FIVE) + p1.my_alias_enum_list.append(enums_pb2.FJORIR) + + p1.my_alias_enum_map['six'] = enums_pb2.SIX + p1.my_alias_enum_map['sex'] = enums_pb2.SEX + p1.my_alias_enum_map['fimm'] = enums_pb2.FIVE + + expected = {'my_enum': 2, + 'my_alias_enum': 3, + 'my_enum_list': [1, 3], + 'my_alias_enum_list': [1, 2, 1], + 'my_enum_map': {'one': 1, + 'two': 2}, + 'my_alias_enum_map': {'six': 3, + 'sex': 3, + 'fimm': 2}} + + self.assertEqual(expected, dictator.proto_to_dict(p1)) + + p2 = enums_pb2.WithInternalEnum() + p2.my_internal_enum = enums_pb2.WithInternalEnum.SIX + + p2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FIVE) + p2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FOUR) + + p2.my_internal_enum_map['no5'] = enums_pb2.WithInternalEnum.FIVE + p2.my_internal_enum_map['no6'] = enums_pb2.WithInternalEnum.SIX + + p2.my_internal_alias_enum = enums_pb2.WithInternalEnum.SEVEN + + p2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.SJO) + p2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.ATTA) + p2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.EIGHT) + + p2.my_internal_alias_enum_map['no9'] = enums_pb2.WithInternalEnum.NIU + p2.my_internal_alias_enum_map['no9B'] = enums_pb2.WithInternalEnum.NINE + p2.my_internal_alias_enum_map['default'] = enums_pb2.WithInternalEnum.ZERO + + expected2 = {'my_internal_enum': 6, + 'my_internal_alias_enum': 7, + 'my_internal_enum_list': [5, 4], + 'my_internal_alias_enum_list': [7, 8, 8], + 'my_internal_enum_map': {'no5': 5, + 'no6': 6}, + 'my_internal_alias_enum_map': {'no9': 9, + 'no9B': 9, + 'default': 0}} + + self.assertEqual(expected2, dictator.proto_to_dict(p2)) + + +class DataclassToDictTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_dataclass_to_dict(self): + from sandbox.test import rainbow_dc + + expected = {'simple_field': 'Green', + 'message_field': { + 'foo': 'Blue', + 'bar': 'Yellow' + }, + 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', + 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'simple_map': {'dora': 'Imamap!', + 'diego': 'Camera!'}, + 'message_map': {'mickey': {'foo': 'mouse', + 'bar': ''}, # TODO(thordurm@ccpgames.com>) 2024-04-15: Should we include "default/empty" values?!? + 'donald': {'foo': 'duck', + 'bar': 'trump'}}} + + dc = rainbow_dc.RainbowMessage(simple_field='Green', + message_field=rainbow_dc.SubMessage(foo='Blue', + bar='Yellow'), + simple_list=['Four', 'Seven', '99'], + message_list=[rainbow_dc.SubMessage(foo='Foo in a list', + bar='Candybar'), + rainbow_dc.SubMessage(foo='a Fool in a list', + bar='Tequila bar')], + simple_map={'dora': 'Imamap!', + 'diego': 'Camera!'}, + message_map={'donald': rainbow_dc.SubMessage(foo='duck', + bar='trump'), + 'mickey': rainbow_dc.SubMessage(foo='mouse')}) + + self.assertEqual(expected, dictator.dataclass_to_dict(dc)) + + dc2 = rainbow_dc.RainbowMessage(simple_field='Green', + message_field=rainbow_dc.SubMessage(foo='Blue', + bar='Yellow'), + simple_list=['Four', 'Seven', '99'], + message_list=[rainbow_dc.SubMessage(foo='Foo in a list', + bar='Candybar'), + rainbow_dc.SubMessage(foo='a Fool in a list', + bar='Tequila bar')], + simple_map={'dora': 'Imamap!', + 'diego': 'Camera!'}, + message_map={'donald': rainbow_dc.SubMessage(foo='duck', + bar='trump'), + 'mickey': rainbow_dc.SubMessage(foo='rat')}) + + self.assertNotEqual(expected, dictator.dataclass_to_dict(dc2)) + + def test_dataclass_timestamp_to_dict(self): + from sandbox.test import rainbow_dc + + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%SZ')) + + ts4 = '2049-01-01T12:00:00Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%SZ')) + + ts5 = '2049-01-01T00:00:00Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%SZ')) + + expected = {'my_timestamp': ts1, + 'my_timestamp_list': [ts2, ts3], + 'my_timestamp_map': { + 'noon': ts4, + 'midnight': ts5}} + + dc = rainbow_dc.TimestampMessage(my_timestamp=dt1, + my_timestamp_list=[dt2, dt3], + my_timestamp_map={'noon': dt4, + 'midnight': dt5}) + + self.assertEqual(expected, dictator.dataclass_to_dict(dc)) + + def test_dataclass_bytes_to_dict(self): + from sandbox.test import rainbow_dc + + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + expected = {'my_bytes': as_base64_1, + 'my_bytes_list': [as_base64_2, as_base64_3], + 'my_bytes_map': { + 'zero': as_base64_4, + 'one': as_base64_5}} + + dc = rainbow_dc.BytesMessage(my_bytes=as_bytes_1, + my_bytes_list=[as_bytes_2, as_bytes_3], + my_bytes_map={'zero': as_bytes_4, + 'one': as_bytes_5}) + + self.assertEqual(expected, dictator.dataclass_to_dict(dc)) + + def test_dataclass_enums_to_dict(self): + from sandbox.test import enums_dc + dc1 = enums_dc.WithExternalEnum() + dc1.my_enum = enums_dc.TWO + + dc1.my_enum_list.append(enums_dc.ONE) + dc1.my_enum_list.append(enums_dc.THREE) + + dc1.my_enum_map['one'] = enums_dc.ONE + dc1.my_enum_map['two'] = enums_dc.TWO + + dc1.my_alias_enum = enums_dc.SIX + + dc1.my_alias_enum_list.append(enums_dc.FOUR) + dc1.my_alias_enum_list.append(enums_dc.FIVE) + dc1.my_alias_enum_list.append(enums_dc.FJORIR) + + dc1.my_alias_enum_map['six'] = enums_dc.SIX + dc1.my_alias_enum_map['sex'] = enums_dc.SEX + dc1.my_alias_enum_map['fimm'] = enums_dc.FIVE + + expected = {'my_enum': 2, + 'my_alias_enum': 3, + 'my_enum_list': [1, 3], + 'my_alias_enum_list': [1, 2, 1], + 'my_enum_map': {'one': 1, + 'two': 2}, + 'my_alias_enum_map': {'six': 3, + 'sex': 3, + 'fimm': 2}} + + self.assertEqual(expected, dictator.dataclass_to_dict(dc1)) + + dc2 = enums_dc.WithInternalEnum() + dc2.my_internal_enum = enums_dc.WithInternalEnum.SIX + + dc2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FIVE) + dc2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FOUR) + + dc2.my_internal_enum_map['no5'] = enums_dc.WithInternalEnum.FIVE + dc2.my_internal_enum_map['no6'] = enums_dc.WithInternalEnum.SIX + + dc2.my_internal_alias_enum = enums_dc.WithInternalEnum.SEVEN + + dc2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.SJO) + dc2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.ATTA) + dc2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.EIGHT) + + dc2.my_internal_alias_enum_map['no9'] = enums_dc.WithInternalEnum.NIU + dc2.my_internal_alias_enum_map['no9B'] = enums_dc.WithInternalEnum.NINE + dc2.my_internal_alias_enum_map['default'] = enums_dc.WithInternalEnum.ZERO + + expected2 = {'my_internal_enum': 6, + 'my_internal_alias_enum': 7, + 'my_internal_enum_list': [5, 4], + 'my_internal_alias_enum_list': [7, 8, 8], + 'my_internal_enum_map': {'no5': 5, + 'no6': 6}, + 'my_internal_alias_enum_map': {'no9': 9, + 'no9B': 9, + 'default': 0}} + + self.assertEqual(expected2, dictator.dataclass_to_dict(dc2)) diff --git a/tests/test_dictators.py b/tests/test_dictators.py new file mode 100644 index 0000000..de76ce3 --- /dev/null +++ b/tests/test_dictators.py @@ -0,0 +1,391 @@ +import unittest +import datetime +import dataclasses +import base64 +import enum + +import protoplasm.bases.dataclass_bases +from protoplasm.casting import dictators +from protoplasm import bases +import collections + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +import os +import sys +import sys +import shutil +import time +HERE = os.path.dirname(__file__) +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + + +class FauxEnum(enum.IntEnum): + DEFAULT = 0 + FOO = 1 + BAR = 2 + OOF = 3 + RAB = 4 + + +@dataclasses.dataclass +class FauxDataClass(protoplasm.bases.dataclass_bases.DataclassBase): + string: str = dataclasses.field(default=None) + integer: int = dataclasses.field(default=None) + floating: float = dataclasses.field(default=None) + boolean: bool = dataclasses.field(default=None) + real_bytes: bytes = dataclasses.field(default=None) + timestamp: datetime.datetime = dataclasses.field(default=None) + enumerator: FauxEnum = dataclasses.field(default=None) + duration: datetime.timedelta = dataclasses.field(default=None) + any: protoplasm.bases.dataclass_bases.DataclassBase = dataclasses.field(default=None) + + +def _get_field_map(dc): + return {f.name: f for f in dataclasses.fields(dc)} + + +def _get_test_args(dc, field_name): + return getattr(dc, field_name), _get_field_map(dc)[field_name], dc + + +class BaseDictatorTest(unittest.TestCase): + def test_none_to_dict_value(self): + caster = dictators.BaseDictator.to_dict_value + dc = FauxDataClass() + self.assertIsNone(dc.string) + self.assertIsNone(caster(*_get_test_args(dc, 'string'))) + + self.assertIsNone(dc.integer) + self.assertIsNone(caster(*_get_test_args(dc, 'integer'))) + + self.assertIsNone(dc.floating) + self.assertIsNone(caster(*_get_test_args(dc, 'floating'))) + + self.assertIsNone(dc.boolean) + self.assertIsNone(caster(*_get_test_args(dc, 'boolean'))) + + def test_to_dict_value(self): + caster = dictators.BaseDictator.to_dict_value + dc = FauxDataClass(string='Ðiss is a júníkód stíng viþþ tjænís skvígglí bitts 中國潦草的位', + integer=4815162342, + floating=3.14159265359, + boolean=True) + + self.assertIsNotNone(dc.string) + self.assertIsInstance(dc.string, str) + self.assertEqual('Ðiss is a júníkód stíng viþþ tjænís skvígglí bitts 中國潦草的位', + caster(*_get_test_args(dc, 'string'))) + + self.assertIsNotNone(dc.integer) + self.assertIsInstance(dc.integer, int) + self.assertEqual(4815162342, caster(*_get_test_args(dc, 'integer'))) + + self.assertIsNotNone(dc.floating) + self.assertIsInstance(dc.floating, float) + self.assertEqual(3.14159265359, caster(*_get_test_args(dc, 'floating'))) + + self.assertIsNotNone(dc.boolean) + self.assertIsInstance(dc.boolean, bool) + self.assertEqual(True, caster(*_get_test_args(dc, 'boolean'))) + + def test_none_from_dict_value(self): + caster = dictators.BaseDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.string) + dc.string = caster(None, _get_field_map(dc)['string'], dc.__class__) + self.assertIsNone(dc.string) + + self.assertIsNone(dc.integer) + dc.integer = caster(None, _get_field_map(dc)['integer'], dc.__class__) + self.assertIsNone(dc.integer) + + self.assertIsNone(dc.floating) + dc.floating = caster(None, _get_field_map(dc)['floating'], dc.__class__) + self.assertIsNone(dc.floating) + + self.assertIsNone(dc.boolean) + dc.boolean = caster(None, _get_field_map(dc)['boolean'], dc.__class__) + self.assertIsNone(dc.boolean) + + def test_from_dict_value(self): + caster = dictators.BaseDictator.from_dict_value + string = 'Ðiss is a júníkód stíng viþþ tjænís skvígglí bitts 中國潦草的位' + integer = 4815162342 + floating = 3.14159265359 + boolean = True + + dc = FauxDataClass() + + self.assertIsNone(dc.string) + dc.string = caster(string, _get_field_map(dc)['string'], dc) + self.assertIsNotNone(dc.string) + self.assertIsInstance(dc.string, str) + self.assertEqual('Ðiss is a júníkód stíng viþþ tjænís skvígglí bitts 中國潦草的位', dc.string) + + self.assertIsNone(dc.integer) + dc.integer = caster(integer, _get_field_map(dc)['integer'], dc) + self.assertIsNotNone(dc.integer) + self.assertIsInstance(dc.integer, int) + self.assertEqual(4815162342, dc.integer) + + self.assertIsNone(dc.floating) + dc.floating = caster(floating, _get_field_map(dc)['floating'], dc) + self.assertIsNotNone(dc.floating) + self.assertIsInstance(dc.floating, float) + self.assertEqual(3.14159265359, dc.floating) + + self.assertIsNone(dc.boolean) + dc.boolean = caster(boolean, _get_field_map(dc)['boolean'], dc) + self.assertIsNotNone(dc.boolean) + self.assertIsInstance(dc.boolean, bool) + self.assertEqual(True, dc.boolean) + + +class TimestampDictatorTest(unittest.TestCase): + def test_none_to_dict_value(self): + caster = dictators.TimestampDictator.to_dict_value + dc = FauxDataClass() + self.assertIsNone(dc.timestamp) + self.assertIsNone(caster(*_get_test_args(dc, 'timestamp'))) + + def test_to_dict_value(self): + caster = dictators.TimestampDictator.to_dict_value + dc = FauxDataClass(timestamp=datetime.datetime(2010, 4, 9, 22, 38, 39, 987654)) + + self.assertIsNotNone(dc.timestamp) + self.assertIsInstance(dc.timestamp, datetime.datetime) + self.assertEqual('2010-04-09T22:38:39.987654Z', caster(*_get_test_args(dc, 'timestamp'))) + + def test_none_from_dict_value(self): + caster = dictators.TimestampDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.timestamp) + dc.timestamp = caster(None, _get_field_map(dc)['timestamp'], dc.__class__) + self.assertIsNone(dc.timestamp) + + def test_from_dict_value(self): + caster = dictators.TimestampDictator.from_dict_value + timestamp_string = '2012-07-03T14:50:51.654321Z' + + dc = FauxDataClass() + + self.assertIsNone(dc.timestamp) + dc.timestamp = caster(timestamp_string, _get_field_map(dc)['timestamp'], dc.__class__) + self.assertIsNotNone(dc.timestamp) + self.assertIsInstance(dc.timestamp, datetime.datetime) + self.assertEqual(datetime.datetime(2012, 7, 3, 14, 50, 51, 654321), dc.timestamp) + + +class ByteDictatorTest(unittest.TestCase): + def test_none_to_dict_value(self): + caster = dictators.ByteDictator.to_dict_value + dc = FauxDataClass() + self.assertIsNone(dc.real_bytes) + # self.assertEqual(b'', dc.real_bytes) + # self.assertIsNone(caster(*_get_test_args(dc, 'real_bytes'))) + self.assertEqual('', caster(*_get_test_args(dc, 'real_bytes'))) + + def test_to_dict_value(self): + caster = dictators.ByteDictator.to_dict_value + as_str = '♡♠♢♣' + as_bytes = as_str.encode('utf-8') + as_base64 = base64.encodebytes(as_bytes).decode('utf-8').strip() + + dc = FauxDataClass(real_bytes=as_bytes) + + self.assertIsNotNone(dc.real_bytes) + self.assertIsInstance(dc.real_bytes, bytes) + self.assertEqual(as_base64, caster(*_get_test_args(dc, 'real_bytes'))) + + def test_none_from_dict_value(self): + caster = dictators.ByteDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.real_bytes) + # self.assertEqual(b'', dc.real_bytes) + dc.real_bytes = caster(None, _get_field_map(dc)['real_bytes'], dc.__class__) + # self.assertIsNone(dc.real_bytes) + self.assertEqual(b'', dc.real_bytes) + + def test_from_dict_value(self): + caster = dictators.ByteDictator.from_dict_value + as_str = '♡♠♢♣' + as_bytes = as_str.encode('utf-8') + as_base64 = base64.encodebytes(as_bytes).decode('utf-8').strip() + + dc = FauxDataClass() + + self.assertIsNone(dc.real_bytes) + dc.real_bytes = caster(as_base64, _get_field_map(dc)['real_bytes'], dc.__class__) + self.assertIsNotNone(dc.real_bytes) + self.assertIsInstance(dc.real_bytes, bytes) + self.assertEqual(as_bytes, dc.real_bytes) + + +class EnumDictatorTest(unittest.TestCase): + def test_none_to_dict_value(self): + caster = dictators.EnumDictator.to_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.enumerator) + # self.assertEqual(0, dc.enumerator) + # self.assertIsNone(caster(*_get_test_args(dc, 'enumerator'))) + self.assertEqual(0, caster(*_get_test_args(dc, 'enumerator'))) + + def test_to_dict_value(self): + caster = dictators.EnumDictator.to_dict_value + dc = FauxDataClass(enumerator=FauxEnum.FOO) + + self.assertIsNotNone(dc.enumerator) + self.assertIsInstance(dc.enumerator, enum.IntEnum) + self.assertEqual(FauxEnum.FOO.value, caster(*_get_test_args(dc, 'enumerator'))) + + def test_none_from_dict_value(self): + caster = dictators.EnumDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.enumerator) + # self.assertEqual(0, dc.enumerator) + dc.enumerator = caster(None, _get_field_map(dc)['enumerator'], dc.__class__) + # self.assertIsNone(dc.enumerator) + self.assertEqual(FauxEnum.DEFAULT, dc.enumerator) + + def test_from_dict_value(self): + caster = dictators.EnumDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.enumerator) + dc.enumerator = caster(2, _get_field_map(dc)['enumerator'], dc.__class__) + self.assertIsNotNone(dc.enumerator) + self.assertIsInstance(dc.enumerator, enum.IntEnum) + self.assertEqual(FauxEnum.BAR, dc.enumerator) + + dc2 = FauxDataClass() + + self.assertIsNone(dc2.enumerator) + dc2.enumerator = caster('OOF', _get_field_map(dc2)['enumerator'], dc2.__class__) + self.assertIsNotNone(dc2.enumerator) + self.assertIsInstance(dc2.enumerator, enum.IntEnum) + self.assertEqual(FauxEnum.OOF, dc2.enumerator) + + +class DurationDictatorTest(unittest.TestCase): + def test_none_to_dict_value(self): + caster = dictators.DurationDictator.to_dict_value + dc = FauxDataClass() + self.assertIsNone(dc.duration) + self.assertIsNone(caster(*_get_test_args(dc, 'duration'))) + + def test_to_dict_value(self): + caster = dictators.DurationDictator.to_dict_value + dc = FauxDataClass(duration=datetime.timedelta(seconds=12.3456789)) + + self.assertIsNotNone(dc.duration) + self.assertIsInstance(dc.duration, datetime.timedelta) + self.assertEqual('12.345679s', caster(*_get_test_args(dc, 'duration'))) + + def test_none_from_dict_value(self): + caster = dictators.DurationDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.duration) + dc.duration = caster(None, _get_field_map(dc)['duration'], dc.__class__) + self.assertIsNone(dc.duration) + + def test_from_dict_value(self): + caster = dictators.DurationDictator.from_dict_value + duration_string = '12.345679s' + + dc = FauxDataClass() + + self.assertIsNone(dc.duration) + dc.duration = caster(duration_string, _get_field_map(dc)['duration'], dc.__class__) + self.assertIsNotNone(dc.duration) + self.assertIsInstance(dc.duration, datetime.timedelta) + self.assertEqual(datetime.timedelta(seconds=12.3456789), dc.duration) + + self.assertEqual(datetime.timedelta(seconds=12.3456789), caster(12.3456789, _get_field_map(dc)['duration'], dc.__class__)) + self.assertEqual(datetime.timedelta(seconds=123456), caster(123456, _get_field_map(dc)['duration'], dc.__class__)) + self.assertEqual(datetime.timedelta(seconds=12.3456789), caster('12.3456789', _get_field_map(dc)['duration'], dc.__class__)) + + +class AnyDictatorTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_none_to_dict_value(self): + caster = dictators.AnyDictator.to_dict_value + dc = FauxDataClass() + self.assertIsNone(dc.any) + self.assertIsNone(caster(*_get_test_args(dc, 'any'))) + + def test_to_dict_value(self): + from sandbox.test import rainbow_dc + + caster = dictators.AnyDictator.to_dict_value + dc = FauxDataClass(any=rainbow_dc.SubMessage('FOO!', 'BAR!!')) + + self.assertIsNotNone(dc.any) + self.assertIsInstance(dc.any, protoplasm.bases.dataclass_bases.DataclassBase) + self.assertIsInstance(dc.any, rainbow_dc.SubMessage) + + expected = collections.OrderedDict([ + ('@type', 'type.googleapis.com/sandbox.test.SubMessage'), + ('foo', 'FOO!'), + ('bar', 'BAR!!') + ]) + + self.assertEqual(expected, caster(*_get_test_args(dc, 'any'))) + + def test_none_from_dict_value(self): + caster = dictators.AnyDictator.from_dict_value + dc = FauxDataClass() + + self.assertIsNone(dc.any) + dc.duration = caster(None, _get_field_map(dc)['any'], dc.__class__) + self.assertIsNone(dc.any) + + def test_from_dict_value(self): + from sandbox.test import anytest_dc + from sandbox.test import rainbow_dc + + caster = dictators.AnyDictator.from_dict_value + + input_value = collections.OrderedDict([ + ('@type', 'type.evetech.net/sandbox.test.SubMessage'), + ('foo', 'FOO!'), + ('bar', 'BAR!!') + ]) + + dc = anytest_dc.AnyMessage() + + self.assertIsNone(dc.my_any) + + dc.my_any = caster(input_value, _get_field_map(dc)['my_any'], dc.__class__) + + self.assertIsNotNone(dc.my_any) + self.assertIsInstance(dc.my_any, protoplasm.bases.dataclass_bases.DataclassBase) + self.assertIsInstance(dc.my_any, rainbow_dc.SubMessage) + self.assertEqual(rainbow_dc.SubMessage('FOO!', 'BAR!!'), dc.my_any) diff --git a/tests/test_objectifier.py b/tests/test_objectifier.py new file mode 100644 index 0000000..1bc94a5 --- /dev/null +++ b/tests/test_objectifier.py @@ -0,0 +1,665 @@ +import unittest +import datetime +import base64 + +from protoplasm.casting import objectifier +import os +import sys + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +import shutil +import time +HERE = os.path.dirname(__file__) +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + + +class ObjectifierTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'sandbox') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='sandbox', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_dict_to_proto(self): + from sandbox.test import rainbow_pb2 + + p1 = rainbow_pb2.SubMessage() + p1.foo = 'Foo!' + p1.bar = 'Bar!!!' + + self.assertEqual(p1, objectifier.dict_to_proto(rainbow_pb2.SubMessage, {'foo': 'Foo!', 'bar': 'Bar!!!'})) + + p2 = rainbow_pb2.SubMessage() + p2.foo = 'Foo Two!' + self.assertEqual(p2, objectifier.dict_to_proto(rainbow_pb2.SubMessage, {'foo': 'Foo Two!'})) + + p3 = rainbow_pb2.SubMessage() + p3.foo = '这是中国人' + self.assertNotEqual(p3, objectifier.dict_to_proto(rainbow_pb2.SubMessage, {'foo': '这不是中国人'})) + + p4 = rainbow_pb2.SubMessage() + self.assertEqual(p4, objectifier.dict_to_proto(rainbow_pb2.SubMessage)) + + def test_nested_dict_to_proto(self): + from sandbox.test import rainbow_pb2 + + p1 = rainbow_pb2.RainbowMessage() + self.assertEqual(p1, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage)) + + p2 = rainbow_pb2.RainbowMessage() + p2.simple_field = 'Green' + self.assertEqual(p2, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, {'simple_field': 'Green'})) + self.assertNotEqual(p1, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, {'simple_field': 'Green'})) + + p3 = rainbow_pb2.RainbowMessage() + p3.simple_field = 'Green' + p3.message_field.foo = 'Blue' + self.assertEqual(p3, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', 'message_field': {'foo': 'Blue'}})) + p4 = rainbow_pb2.RainbowMessage() + p4.simple_field = 'Green' + p4.message_field.foo = 'Blue' + p4.message_field.bar = 'Yellow' + self.assertEqual(p4, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + self.assertNotEqual(p3, p4) + self.assertNotEqual(p4, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', + 'message_field': {'foo': 'Blue', 'bar': 'Red'}})) + + p5 = rainbow_pb2.RainbowMessage() + p5.simple_field = 'Green' + p5.message_field.foo = 'Blue' + p5.message_field.bar = 'Yellow' + p5.simple_list.append('Four') + p5.simple_list.append('Seven') + p5.simple_list.append('99') + self.assertEqual(p5, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + p6 = rainbow_pb2.RainbowMessage() + p6.simple_field = 'Green' + p6.message_field.foo = 'Blue' + p6.message_field.bar = 'Yellow' + p6.simple_list.append('Four') + p6.simple_list.append('Seven') + p6.simple_list.append('98') + self.assertNotEqual(p6, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', + 'simple_list': ['Four', 'Seven', '100'], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + p7 = rainbow_pb2.RainbowMessage() + p7.simple_field = 'Green' + p7.message_field.foo = 'Blue' + p7.message_field.bar = 'Yellow' + p7.simple_list.append('Four') + p7.simple_list.append('Seven') + p7.simple_list.append('99') + m1 = p7.message_list.add() + m1.foo = 'Foo in a list' + m1.bar = 'Candybar' + m2 = p7.message_list.add() + m2.foo = 'a Fool in a list' + m2.bar = 'Tequila bar' + self.assertEqual(p7, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + self.assertNotEqual(p7, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', + 'simple_list': ['Four', 'Seven', '99'], 'message_list': [ + {'foo': 'a Fool in a list', 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + p8 = rainbow_pb2.RainbowMessage() + p8.simple_field = 'Green' + p8.message_field.foo = 'Blue' + p8.message_field.bar = 'Yellow' + p8.simple_list.append('Four') + p8.simple_list.append('Seven') + p8.simple_list.append('99') + m3 = p8.message_list.add() + m3.foo = 'Foo in a list' + m3.bar = 'Candybar' + m4 = p8.message_list.add() + m4.foo = 'a Fool in a list' + m4.bar = 'Tequila bar' + p8.simple_map['dora'] = 'Imamap!' + p8.simple_map['diego'] = 'Camera!' + self.assertEqual(p8, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}, + 'simple_map': {'dora': 'Imamap!', 'diego': 'Camera!'}} + )) + + p9 = rainbow_pb2.RainbowMessage() + p9.simple_field = 'Green' + p9.message_field.foo = 'Blue' + p9.message_field.bar = 'Yellow' + p9.simple_list.append('Four') + p9.simple_list.append('Seven') + p9.simple_list.append('99') + m5 = p9.message_list.add() + m5.foo = 'Foo in a list' + m5.bar = 'Candybar' + m6 = p9.message_list.add() + m6.foo = 'a Fool in a list' + m6.bar = 'Tequila bar' + p9.simple_map['dora'] = 'Imamap!' + p9.simple_map['diego'] = 'Camera!' + p9.message_map['donald'].foo = 'duck' + p9.message_map['donald'].bar = 'trump' + p9.message_map['mickey'].foo = 'mouse' + self.assertEqual(p9, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}, + 'simple_map': {'dora': 'Imamap!', 'diego': 'Camera!'}, + 'message_map': {'donald': {'foo': 'duck', 'bar': 'trump'}, + 'mickey': {'foo': 'mouse'}}} + )) + + p10 = rainbow_pb2.RainbowMessage() + p10.simple_field = 'Green' + p10.message_field.foo = 'Blue' + p10.message_field.bar = 'Yellow' + p10.simple_list.append('Four') + p10.simple_list.append('Seven') + p10.simple_list.append('99') + m7 = p10.message_list.add() + m7.foo = 'Foo in a list' + m7.bar = 'Candybar' + m8 = p10.message_list.add() + m8.foo = 'a Fool in a list' + m8.bar = 'Tequila bar' + p10.simple_map['dora'] = 'Imamap!' + p10.simple_map['diego'] = 'Camera!' + p10.message_map['mickey'].foo = 'mouse' + p10.message_map['donald'].foo = 'duck' + p10.message_map['donald'].bar = 'trump' + + self.assertEqual(p10, objectifier.dict_to_proto(rainbow_pb2.RainbowMessage, + {'simple_field': 'Green', + 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'simple_map': {'diego': 'Camera!', 'dora': 'Imamap!'}, + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}, + 'message_map': {'donald': {'foo': 'duck', 'bar': 'trump'}, + 'mickey': {'foo': 'mouse'}}} + )) + + def test_dict_to_dataclass(self): + from sandbox.test import rainbow_dc + + dc1 = rainbow_dc.SubMessage() + dc1.foo = 'Foo!' + dc1.bar = 'Bar!!!' + + self.assertEqual(dc1, objectifier.dict_to_dataclass(rainbow_dc.SubMessage, {'foo': 'Foo!', + 'bar': 'Bar!!!'})) + + dc2 = rainbow_dc.SubMessage() + dc2.foo = 'Foo Two!' + self.assertEqual(dc2, objectifier.dict_to_dataclass(rainbow_dc.SubMessage, {'foo': 'Foo Two!'})) + + dc3 = rainbow_dc.SubMessage() + dc3.foo = '这是中国人' + self.assertNotEqual(dc3, objectifier.dict_to_dataclass(rainbow_dc.SubMessage, {'foo': '这不是中国人'})) + + dc4 = rainbow_dc.SubMessage() + self.assertEqual(dc4, objectifier.dict_to_dataclass(rainbow_dc.SubMessage)) + + def test_nested_dict_to_dataclass(self): + from sandbox.test import rainbow_dc + + dc1 = rainbow_dc.RainbowMessage() + self.assertEqual(dc1, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage)) + + dc2 = rainbow_dc.RainbowMessage() + dc2.simple_field = 'Green' + self.assertEqual(dc2, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, {'simple_field': 'Green'})) + self.assertNotEqual(dc1, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, {'simple_field': 'Green'})) + + dc3 = rainbow_dc.RainbowMessage() + dc3.simple_field = 'Green' + dc3.message_field = rainbow_dc.SubMessage() + dc3.message_field.foo = 'Blue' + self.assertEqual(dc3, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', 'message_field': {'foo': 'Blue'}})) + dc4 = rainbow_dc.RainbowMessage() + dc4.simple_field = 'Green' + dc4.message_field = rainbow_dc.SubMessage() + dc4.message_field.foo = 'Blue' + dc4.message_field.bar = 'Yellow' + self.assertEqual(dc4, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + self.assertNotEqual(dc3, dc4) + self.assertNotEqual(dc4, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', + 'message_field': {'foo': 'Blue', 'bar': 'Red'}})) + + dc5 = rainbow_dc.RainbowMessage() + dc5.simple_field = 'Green' + dc5.message_field = rainbow_dc.SubMessage('Blue', 'Yellow') + dc5.simple_list.append('Four') + dc5.simple_list.append('Seven') + dc5.simple_list.append('99') + self.assertEqual(dc5, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + dc6 = rainbow_dc.RainbowMessage() + dc6.simple_field = 'Green' + dc6.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc6.simple_list.append('Four') + dc6.simple_list.append('Seven') + dc6.simple_list.append('98') + self.assertNotEqual(dc6, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', + 'simple_list': ['Four', 'Seven', '100'], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + dc7 = rainbow_dc.RainbowMessage() + dc7.simple_field = 'Green' + dc7.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc7.simple_list.append('Four') + dc7.simple_list.append('Seven') + dc7.simple_list.append('99') + + dc7.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc7.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + + self.assertEqual(dc7, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + self.assertNotEqual(dc7, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', + 'simple_list': ['Four', 'Seven', '99'], 'message_list': [ + {'foo': 'a Fool in a list', 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}})) + + dc8 = rainbow_dc.RainbowMessage() + dc8.simple_field = 'Green' + dc8.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc8.simple_list.append('Four') + dc8.simple_list.append('Seven') + dc8.simple_list.append('99') + + dc8.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc8.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + + dc8.simple_map['dora'] = 'Imamap!' + dc8.simple_map['diego'] = 'Camera!' + self.assertEqual(dc8, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}, + 'simple_map': {'dora': 'Imamap!', 'diego': 'Camera!'}} + )) + + dc9 = rainbow_dc.RainbowMessage() + dc9.simple_field = 'Green' + dc9.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc9.simple_list.append('Four') + dc9.simple_list.append('Seven') + dc9.simple_list.append('99') + dc9.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc9.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + dc9.simple_map['dora'] = 'Imamap!' + dc9.simple_map['diego'] = 'Camera!' + + dc9.message_map['donald'] = rainbow_dc.SubMessage(foo='duck', bar='trump') + + dc9.message_map['mickey'] = rainbow_dc.SubMessage(foo='mouse') + + self.assertEqual(dc9, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}, + 'simple_map': {'dora': 'Imamap!', 'diego': 'Camera!'}, + 'message_map': {'donald': {'foo': 'duck', 'bar': 'trump'}, + 'mickey': {'foo': 'mouse'}}} + )) + + dc10 = rainbow_dc.RainbowMessage() + dc10.simple_field = 'Green' + dc10.message_field = rainbow_dc.SubMessage(foo='Blue', bar='Yellow') + dc10.simple_list.append('Four') + dc10.simple_list.append('Seven') + dc10.simple_list.append('99') + dc10.message_list.append(rainbow_dc.SubMessage(foo='Foo in a list', bar='Candybar')) + dc10.message_list.append(rainbow_dc.SubMessage(foo='a Fool in a list', bar='Tequila bar')) + dc10.simple_map['dora'] = 'Imamap!' + dc10.simple_map['diego'] = 'Camera!' + + dc10.message_map['mickey'] = rainbow_dc.SubMessage(foo='mouse') + dc10.message_map['donald'] = rainbow_dc.SubMessage(foo='duck', bar='trump') + + self.assertEqual(dc10, objectifier.dict_to_dataclass(rainbow_dc.RainbowMessage, + {'simple_field': 'Green', + 'simple_list': ['Four', 'Seven', '99'], + 'message_list': [{'foo': 'Foo in a list', 'bar': 'Candybar'}, + {'foo': 'a Fool in a list', + 'bar': 'Tequila bar'}], + 'simple_map': {'diego': 'Camera!', 'dora': 'Imamap!'}, + 'message_field': {'foo': 'Blue', 'bar': 'Yellow'}, + 'message_map': {'donald': {'foo': 'duck', 'bar': 'trump'}, + 'mickey': {'foo': 'mouse'}}} + )) + + def test_timestamp_dict_to_proto(self): + from sandbox.test import rainbow_pb2 + + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00.000000Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts4 = '2049-01-01T12:00:00.000000Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts5 = '2049-01-01T00:00:00.000000Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + dict_data = {'my_timestamp': ts1, + 'my_timestamp_list': [ts2, ts3], + 'my_timestamp_map': {'noon': ts4, + 'midnight': ts5}} + + p_expect = rainbow_pb2.TimestampMessage() + p_expect.my_timestamp.FromJsonString(ts1) + p_expect.my_timestamp_list.add().FromJsonString(ts2) + p_expect.my_timestamp_list.add().FromJsonString(ts3) + p_expect.my_timestamp_map['noon'].FromJsonString(ts4) + p_expect.my_timestamp_map['midnight'].FromJsonString(ts5) + + self.assertEqual(p_expect, objectifier.dict_to_proto(rainbow_pb2.TimestampMessage, dict_data)) + + def test_timestamp_dict_to_dataclass(self): + from sandbox.test import rainbow_dc + + ts1 = '2012-07-03T14:50:51.654321Z' + dt1 = datetime.datetime(2012, 7, 3, 14, 50, 51, 654321) + self.assertEqual(ts1, dt1.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts2 = '2010-04-09T22:38:39.009870Z' + dt2 = datetime.datetime(2010, 4, 9, 22, 38, 39, 9870) + self.assertEqual(ts2, dt2.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts3 = '1979-07-06T14:30:00.000000Z' + dt3 = datetime.datetime(1979, 7, 6, 14, 30) + self.assertEqual(ts3, dt3.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts4 = '2049-01-01T12:00:00.000000Z' + dt4 = datetime.datetime(2049, 1, 1, 12) + self.assertEqual(ts4, dt4.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + ts5 = '2049-01-01T00:00:00.000000Z' + dt5 = datetime.datetime(2049, 1, 1) + self.assertEqual(ts5, dt5.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + + dict_data = {'my_timestamp': ts1, + 'my_timestamp_list': [ts2, ts3], + 'my_timestamp_map': {'noon': ts4, + 'midnight': ts5}} + + dc_expect = rainbow_dc.TimestampMessage() + dc_expect.my_timestamp = dt1 + dc_expect.my_timestamp_list.append(dt2) + dc_expect.my_timestamp_list.append(dt3) + dc_expect.my_timestamp_map['noon'] = dt4 + dc_expect.my_timestamp_map['midnight'] = dt5 + + self.assertEqual(dc_expect, objectifier.dict_to_dataclass(rainbow_dc.TimestampMessage, dict_data)) + + def test_byte_dict_to_proto(self): + from sandbox.test import rainbow_pb2 + + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + dict_data = {'my_bytes': as_base64_1, + 'my_bytes_list': [as_base64_2, as_base64_3], + 'my_bytes_map': {'zero': as_base64_4, + 'one': as_base64_5}} + + p_expect = rainbow_pb2.BytesMessage() + p_expect.my_bytes = as_bytes_1 + p_expect.my_bytes_list.append(as_bytes_2) + p_expect.my_bytes_list.append(as_bytes_3) + p_expect.my_bytes_map['zero'] = as_bytes_4 + p_expect.my_bytes_map['one'] = as_bytes_5 + + self.assertEqual(p_expect, objectifier.dict_to_proto(rainbow_pb2.BytesMessage, dict_data)) + + def test_byte_dict_to_dataclass(self): + from sandbox.test import rainbow_dc + + as_str_1 = 'Þórður Matthíasson' + as_bytes_1 = as_str_1.encode('utf-8') + as_base64_1 = base64.encodebytes(as_bytes_1).decode('utf-8').strip() + + as_str_2 = '♡' + as_bytes_2 = as_str_2.encode('utf-8') + as_base64_2 = base64.encodebytes(as_bytes_2).decode('utf-8').strip() + + as_str_3 = '♠' + as_bytes_3 = as_str_3.encode('utf-8') + as_base64_3 = base64.encodebytes(as_bytes_3).decode('utf-8').strip() + + as_str_4 = '♢' + as_bytes_4 = as_str_4.encode('utf-8') + as_base64_4 = base64.encodebytes(as_bytes_4).decode('utf-8').strip() + + as_str_5 = '♣' + as_bytes_5 = as_str_5.encode('utf-8') + as_base64_5 = base64.encodebytes(as_bytes_5).decode('utf-8').strip() + + dict_data = {'my_bytes': as_base64_1, + 'my_bytes_list': [as_base64_2, as_base64_3], + 'my_bytes_map': {'zero': as_base64_4, + 'one': as_base64_5}} + + dc_expect = rainbow_dc.BytesMessage() + dc_expect.my_bytes = as_bytes_1 + dc_expect.my_bytes_list.append(as_bytes_2) + dc_expect.my_bytes_list.append(as_bytes_3) + dc_expect.my_bytes_map['zero'] = as_bytes_4 + dc_expect.my_bytes_map['one'] = as_bytes_5 + + self.assertEqual(dc_expect, objectifier.dict_to_dataclass(rainbow_dc.BytesMessage, dict_data)) + + def test_enum_dict_to_proto(self): + from sandbox.test import enums_pb2 + + p_expect = enums_pb2.WithExternalEnum() + p_expect.my_enum = enums_pb2.TWO + + p_expect.my_enum_list.append(enums_pb2.ONE) + p_expect.my_enum_list.append(enums_pb2.THREE) + + p_expect.my_enum_map['one'] = enums_pb2.ONE + p_expect.my_enum_map['two'] = enums_pb2.TWO + + p_expect.my_alias_enum = enums_pb2.SIX + + p_expect.my_alias_enum_list.append(enums_pb2.FOUR) + p_expect.my_alias_enum_list.append(enums_pb2.FIVE) + p_expect.my_alias_enum_list.append(enums_pb2.FJORIR) + + p_expect.my_alias_enum_map['six'] = enums_pb2.SIX + p_expect.my_alias_enum_map['sex'] = enums_pb2.SEX + p_expect.my_alias_enum_map['fimm'] = enums_pb2.FIVE + + dict_data = {'my_enum': 2, + 'my_alias_enum': 3, + 'my_enum_list': [1, 3], + 'my_alias_enum_list': [1, 2, 1], + 'my_enum_map': {'one': 1, + 'two': 2}, + 'my_alias_enum_map': {'six': 3, + 'sex': 3, + 'fimm': 2}} + + self.assertEqual(p_expect, objectifier.dict_to_proto(enums_pb2.WithExternalEnum, dict_data)) + + p_expect2 = enums_pb2.WithInternalEnum() + p_expect2.my_internal_enum = enums_pb2.WithInternalEnum.SIX + + p_expect2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FIVE) + p_expect2.my_internal_enum_list.append(enums_pb2.WithInternalEnum.FOUR) + + p_expect2.my_internal_enum_map['no5'] = enums_pb2.WithInternalEnum.FIVE + p_expect2.my_internal_enum_map['no6'] = enums_pb2.WithInternalEnum.SIX + + p_expect2.my_internal_alias_enum = enums_pb2.WithInternalEnum.SEVEN + + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.SJO) + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.ATTA) + p_expect2.my_internal_alias_enum_list.append(enums_pb2.WithInternalEnum.EIGHT) + + p_expect2.my_internal_alias_enum_map['no9'] = enums_pb2.WithInternalEnum.NIU + p_expect2.my_internal_alias_enum_map['no9B'] = enums_pb2.WithInternalEnum.NINE + p_expect2.my_internal_alias_enum_map['default'] = enums_pb2.WithInternalEnum.ZERO + + dict_data2 = {'my_internal_enum': 6, + 'my_internal_alias_enum': 7, + 'my_internal_enum_list': [5, 4], + 'my_internal_alias_enum_list': [7, 8, 8], + 'my_internal_enum_map': {'no5': 5, + 'no6': 6}, + 'my_internal_alias_enum_map': {'no9': 9, + 'no9B': 9, + 'default': 0}} + + self.assertEqual(p_expect2, objectifier.dict_to_proto(enums_pb2.WithInternalEnum, dict_data2)) + + def test_enum_dict_to_dataclass(self): + from sandbox.test import enums_dc + + p_expect = enums_dc.WithExternalEnum() + p_expect.my_enum = enums_dc.TWO + + p_expect.my_enum_list.append(enums_dc.ONE) + p_expect.my_enum_list.append(enums_dc.THREE) + + p_expect.my_enum_map['one'] = enums_dc.ONE + p_expect.my_enum_map['two'] = enums_dc.TWO + + p_expect.my_alias_enum = enums_dc.SIX + + p_expect.my_alias_enum_list.append(enums_dc.FOUR) + p_expect.my_alias_enum_list.append(enums_dc.FIVE) + p_expect.my_alias_enum_list.append(enums_dc.FJORIR) + + p_expect.my_alias_enum_map['six'] = enums_dc.SIX + p_expect.my_alias_enum_map['sex'] = enums_dc.SEX + p_expect.my_alias_enum_map['fimm'] = enums_dc.FIVE + + dict_data = {'my_enum': 2, + 'my_alias_enum': 3, + 'my_enum_list': [1, 3], + 'my_alias_enum_list': [1, 2, 1], + 'my_enum_map': {'one': 1, + 'two': 2}, + 'my_alias_enum_map': {'six': 3, + 'sex': 3, + 'fimm': 2}} + + self.assertEqual(p_expect, objectifier.dict_to_dataclass(enums_dc.WithExternalEnum, dict_data)) + + p_expect2 = enums_dc.WithInternalEnum() + p_expect2.my_internal_enum = enums_dc.WithInternalEnum.SIX + + p_expect2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FIVE) + p_expect2.my_internal_enum_list.append(enums_dc.WithInternalEnum.FOUR) + + p_expect2.my_internal_enum_map['no5'] = enums_dc.WithInternalEnum.FIVE + p_expect2.my_internal_enum_map['no6'] = enums_dc.WithInternalEnum.SIX + + p_expect2.my_internal_alias_enum = enums_dc.WithInternalEnum.SEVEN + + p_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.SJO) + p_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.ATTA) + p_expect2.my_internal_alias_enum_list.append(enums_dc.WithInternalEnum.EIGHT) + + p_expect2.my_internal_alias_enum_map['no9'] = enums_dc.WithInternalEnum.NIU + p_expect2.my_internal_alias_enum_map['no9B'] = enums_dc.WithInternalEnum.NINE + p_expect2.my_internal_alias_enum_map['default'] = enums_dc.WithInternalEnum.ZERO + + dict_data2 = {'my_internal_enum': 6, + 'my_internal_alias_enum': 7, + 'my_internal_enum_list': [5, 4], + 'my_internal_alias_enum_list': [7, 8, 8], + 'my_internal_enum_map': {'no5': 5, + 'no6': 6}, + 'my_internal_alias_enum_map': {'no9': 9, + 'no9B': 9, + 'default': 0}} + + self.assertEqual(p_expect2, objectifier.dict_to_dataclass(enums_dc.WithInternalEnum, dict_data2)) diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..b20e9e8 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,278 @@ +import unittest + +import os +import sys +import shutil +import time + +import grpc + +from protoplasm import errors + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +HERE = os.path.dirname(__file__) + +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') + + +class ServiceTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # Remove old stuff... + build_package = os.path.join(BUILD_ROOT, 'unittesting') + if os.path.exists(build_package): + shutil.rmtree(build_package) + time.sleep(0.1) + + from neobuilder.neobuilder import NeoBuilder + + # Build stuff... + builder = NeoBuilder(package='unittesting', + protopath=PROTO_ROOT, + build_root=BUILD_ROOT) + builder.build() + + # Add build root to path to access its modules + sys.path.append(BUILD_ROOT) + + def test_raw_server_and_raw_client(self): + port = '50042' + log.info(f'Beginning...') + from tests.servicetestutils.raw_server import UnaryServiceServer + server = UnaryServiceServer() + server.start(f'[::]:{port}') + + from tests.servicetestutils import raw_client + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 0) + raw_client.call_WithNoData(f'localhost:{port}') + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 1) + raw_client.call_WithNoData(f'localhost:{port}') + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 2) + + self.assertEqual(server.servicer_implementation.calls['WithInput'], 0) + raw_client.call_WithInput(f'localhost:{port}', unnamed_input='did I win?') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 0) + res = raw_client.call_WithOutput(f'localhost:{port}') + self.assertEqual(res, 'you win') + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 0) + res = raw_client.call_WithBoth(f'localhost:{port}', some_input='reverse me please') + self.assertEqual(res, 'esaelp em esrever') + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 0) + raw_client.call_WithManyInputs(f'localhost:{port}', first_input='yay', second_input=42, third_input=True) + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 0) + res_a, res_b, res_c = raw_client.call_WithManyOutputs(f'localhost:{port}') + self.assertEqual(res_a, 'snorlax') + self.assertEqual(res_b, 7) + self.assertTrue(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 0) + res_a, res_b, res_c = raw_client.call_WithManyBoths(f'localhost:{port}', + another_first_input='bar', + another_second_input=42, + another_third_input=True) + self.assertEqual(res_a, 'BAR') + self.assertEqual(res_b, 21) + self.assertFalse(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 1) + + # Now lets break it... + self.assertEqual(server.servicer_implementation.calls['WithInput'], 1) + with self.assertRaises(grpc.RpcError) as cm: + raw_client.call_WithInput(f'localhost:{port}', unnamed_input='explode') + self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) # noqa + self.assertEqual(cm.exception.details(), 'totally fake not found error') # noqa + self.assertEqual(server.servicer_implementation.calls['WithInput'], 2) + + raw_client.call_WithInput(f'localhost:{port}', unnamed_input='nevermind') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 3) + + server.stop() + + def test_raw_server_and_protoplasm_client(self): + port = '50043' + log.info(f'Beginning...') + from tests.servicetestutils.raw_server import UnaryServiceServer + server = UnaryServiceServer() + server.start(f'[::]:{port}') + + from unittesting.unary.unaryservice_grpc_sender import UnaryService + client = UnaryService(f'localhost:{port}') + + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 0) + client.with_no_data() + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 1) + client.with_no_data() + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 2) + + self.assertEqual(server.servicer_implementation.calls['WithInput'], 0) + client.with_input('did I win?') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 1) + with self.assertRaises(errors.NotFound) as cm: + client.with_input('explode') + self.assertEqual(cm.exception.status_code, grpc.StatusCode.NOT_FOUND) + self.assertEqual(cm.exception.details, 'totally fake not found error') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 2) + + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 0) + res = client.with_output() + self.assertEqual(res, 'you win') + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 0) + res = client.with_both('reverse me please') + self.assertEqual(res, 'esaelp em esrever') + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 0) + client.with_many_inputs(first_input='yay', second_input=42, third_input=True) + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 0) + res_a, res_b, res_c = client.with_many_outputs() + self.assertEqual(res_a, 'snorlax') + self.assertEqual(res_b, 7) + self.assertTrue(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 0) + res_a, res_b, res_c = client.with_many_boths(another_first_input='bar', + another_second_input=42, + another_third_input=True) + self.assertEqual(res_a, 'BAR') + self.assertEqual(res_b, 21) + self.assertFalse(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 1) + + server.stop() + + def test_protoplasm_server_and_raw_client(self): + port = '50041' + log.info(f'Beginning...') + from tests.servicetestutils.plasm_server import UnaryProtoplasmServer + server = UnaryProtoplasmServer() + server.start(f'[::]:{port}') + + from tests.servicetestutils import raw_client + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 0) + raw_client.call_WithNoData(f'localhost:{port}') + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 1) + raw_client.call_WithNoData(f'localhost:{port}') + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 2) + + self.assertEqual(server.servicer_implementation.calls['WithInput'], 0) + raw_client.call_WithInput(f'localhost:{port}', unnamed_input='did I win?') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 0) + res = raw_client.call_WithOutput(f'localhost:{port}') + self.assertEqual(res, 'you win') + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 0) + res = raw_client.call_WithBoth(f'localhost:{port}', some_input='reverse me please') + self.assertEqual(res, 'esaelp em esrever') + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 0) + raw_client.call_WithManyInputs(f'localhost:{port}', first_input='yay', second_input=42, third_input=True) + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 0) + res_a, res_b, res_c = raw_client.call_WithManyOutputs(f'localhost:{port}') + self.assertEqual(res_a, 'snorlax') + self.assertEqual(res_b, 7) + self.assertTrue(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 0) + res_a, res_b, res_c = raw_client.call_WithManyBoths(f'localhost:{port}', + another_first_input='bar', + another_second_input=42, + another_third_input=True) + self.assertEqual(res_a, 'BAR') + self.assertEqual(res_b, 21) + self.assertFalse(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 1) + + # Now lets break it... + self.assertEqual(server.servicer_implementation.calls['WithInput'], 1) + with self.assertRaises(grpc.RpcError) as cm: + raw_client.call_WithInput(f'localhost:{port}', unnamed_input='explode') + self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) # noqa + self.assertEqual(cm.exception.details(), 'totally fake not found error') # noqa + self.assertEqual(server.servicer_implementation.calls['WithInput'], 2) + + raw_client.call_WithInput(f'localhost:{port}', unnamed_input='nevermind') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 3) + + server.stop() + + def test_protoplasm_server_and_protoplasm_client(self): + port = '50044' + log.info(f'Beginning...') + from tests.servicetestutils.plasm_server import UnaryProtoplasmServer + server = UnaryProtoplasmServer() + server.start(f'[::]:{port}') + + from unittesting.unary.unaryservice_grpc_sender import UnaryService + client = UnaryService(f'localhost:{port}') + + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 0) + client.with_no_data() + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 1) + client.with_no_data() + self.assertEqual(server.servicer_implementation.calls['WithNoData'], 2) + + self.assertEqual(server.servicer_implementation.calls['WithInput'], 0) + client.with_input('did I win?') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 1) + with self.assertRaises(errors.NotFound) as cm: + client.with_input('explode') + self.assertEqual(cm.exception.status_code, grpc.StatusCode.NOT_FOUND) + self.assertEqual(cm.exception.details, 'totally fake not found error') + self.assertEqual(server.servicer_implementation.calls['WithInput'], 2) + + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 0) + res = client.with_output() + self.assertEqual(res, 'you win') + self.assertEqual(server.servicer_implementation.calls['WithOutput'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 0) + res = client.with_both('reverse me please') + self.assertEqual(res, 'esaelp em esrever') + self.assertEqual(server.servicer_implementation.calls['WithBoth'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 0) + client.with_many_inputs(first_input='yay', second_input=42, third_input=True) + self.assertEqual(server.servicer_implementation.calls['WithManyInputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 0) + res_a, res_b, res_c = client.with_many_outputs() + self.assertEqual(res_a, 'snorlax') + self.assertEqual(res_b, 7) + self.assertTrue(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyOutputs'], 1) + + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 0) + res_a, res_b, res_c = client.with_many_boths(another_first_input='bar', + another_second_input=42, + another_third_input=True) + self.assertEqual(res_a, 'BAR') + self.assertEqual(res_b, 21) + self.assertFalse(res_c) + self.assertEqual(server.servicer_implementation.calls['WithManyBoths'], 1) + + server.stop()