diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e149cca..8ccd489 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: uses: ./.github/workflows/images_alpy.yml with: PROTOPLASM_PY_PKG_VERSION: ${{ needs.retrieve_version.outputs.version }} - GRPCIO_IMAGE_VERSION: "1.62.1" + GRPCIO_IMAGE_VERSION: "1.66.1" PSYCOPG_VERSION: "3.1" secrets: REGISTRY_USER: ${{ secrets.REGISTRY_USER }} diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 0d0490c..3932e63 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -22,7 +22,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements-unittesting.txt - pip uninstall -y protoplasm + pip install --no-deps -r requirements-neobuilder.txt - name: Run tests on Python ${{ matrix.python-version }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d96de4..e41d663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.3.0] - 2024-09-24 + +### Added + +- Support for the `google.protobuf.Value` message + + ## [5.2.0] - 2024-09-23 ### Added diff --git a/docker/alpy/base.Dockerfile b/docker/alpy/base.Dockerfile index debcffc..0e627b0 100644 --- a/docker/alpy/base.Dockerfile +++ b/docker/alpy/base.Dockerfile @@ -1,5 +1,5 @@ ARG PYTHON_VERSION -FROM python:${PYTHON_VERSION}-alpine3.19 +FROM python:${PYTHON_VERSION}-alpine3.20 COPY requirements.txt . diff --git a/protoplasm/__init__.py b/protoplasm/__init__.py index 34231bf..38a5c87 100644 --- a/protoplasm/__init__.py +++ b/protoplasm/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.2.0' +__version__ = '5.3.0' __author__ = 'Thordur Matthiasson ' __license__ = 'MIT License' diff --git a/protoplasm/casting/dictator.py b/protoplasm/casting/dictator.py index 4ebc088..2af2571 100644 --- a/protoplasm/casting/dictator.py +++ b/protoplasm/casting/dictator.py @@ -36,8 +36,7 @@ def dataclass_to_dict(dc) -> Dict: 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? + if dicted is not ...: d[_get_proto_field_name(field)] = dicted return d @@ -50,10 +49,11 @@ def _get_proto_field_name(field: dataclasses.Field): def _dataclass_field_to_dict_field(field: dataclasses.Field, dc): val = getattr(dc, field.name) + dictator_cls = field.metadata.get('dictator', dictators.BaseDictator) + 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) diff --git a/protoplasm/casting/dictators/__init__.py b/protoplasm/casting/dictators/__init__.py index 4897595..269454e 100644 --- a/protoplasm/casting/dictators/__init__.py +++ b/protoplasm/casting/dictators/__init__.py @@ -7,6 +7,9 @@ 'DurationDictator', 'AnyDictator', 'StructDictator', + 'ValueDictator', + 'ListValueDictator', + 'NullValueDictator', ] import datetime import base64 @@ -205,7 +208,7 @@ def from_dict_value(cls, proto_value: Optional[Union[int, str]], return enum_type(proto_value) -class LongDictator: +class LongDictator(BaseDictator): @classmethod def to_dict_value(cls, dc_value: Union[str, int], field: dataclasses.Field, parent: DataclassBase) -> str: """ @@ -242,7 +245,7 @@ def from_dict_value(cls, proto_value: Union[str, int], return int(proto_value) -class DurationDictator: +class DurationDictator(BaseDictator): @classmethod def to_dict_value(cls, dc_value: datetime.timedelta, field: dataclasses.Field, parent: DataclassBase) -> Optional[str]: @@ -272,7 +275,7 @@ def from_dict_value(cls, proto_value: Union[str, int, float], return datetime.timedelta(seconds=proto_value) -class AnyDictator: +class AnyDictator(BaseDictator): @classmethod def to_dict_value(cls, dc_value: Optional[DataclassBase], field: dataclasses.Field, parent: DataclassBase) -> Optional[collections.OrderedDict]: @@ -312,7 +315,7 @@ def from_dict_value(cls, proto_value: collections.OrderedDict, return dict_to_dataclass(dc_cls, proto_value) -class StructDictator: +class StructDictator(BaseDictator): @classmethod def to_dict_value(cls, dc_value: Dict[str, Any], field: dataclasses.Field, parent: DataclassBase) -> Dict[str, Any]: @@ -328,3 +331,45 @@ def from_dict_value(cls, proto_value: Dict[str, Any], return {} return proto_value + + +class ValueDictator(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 ListValueDictator(BaseDictator): + pass + + +class NullValueDictator(BaseDictator): + pass diff --git a/pyproject.toml b/pyproject.toml index 6cb3fbb..2e71e89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,10 @@ classifiers = [ dependencies = [ "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" + "protobuf >=5.28.2, <6", + "grpcio >=1.66.1, <2", + "grpcio-tools >=1.66.1, <2", + "googleapis-common-protos >=1.65, <2" ] [project.urls] diff --git a/requirements-neobuilder.txt b/requirements-neobuilder.txt new file mode 100644 index 0000000..389e58f --- /dev/null +++ b/requirements-neobuilder.txt @@ -0,0 +1 @@ +neobuilder >=5.3, <6 \ No newline at end of file diff --git a/requirements-unittesting.txt b/requirements-unittesting.txt index 143b725..e36a032 100644 --- a/requirements-unittesting.txt +++ b/requirements-unittesting.txt @@ -1 +1,3 @@ -neobuilder >=5.2.0-rc.1, <6 +-r requirements.txt +Jinja2 >=3.1, <4 +semver >= 3.0.2, <4 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c10b505..79e8f24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ ccptools >= 1.1, <2 -protobuf >=5.28.2, <6 grpcio >=1.66.1, <2 grpcio-tools >=1.66.1, <2 googleapis-common-protos >=1.65, <2 +protobuf == 5.27.2 diff --git a/tests/res/proto/sandbox/test/googlestruct.proto b/tests/res/proto/sandbox/test/googlestruct.proto index 4830578..2b41ed4 100644 --- a/tests/res/proto/sandbox/test/googlestruct.proto +++ b/tests/res/proto/sandbox/test/googlestruct.proto @@ -7,4 +7,9 @@ import "google/protobuf/struct.proto"; message StructMessage { google.protobuf.Struct my_struct = 1; + google.protobuf.Value my_value = 2; + // google.protobuf.ListValue my_list_value = 3; + // google.protobuf.NullValue my_null_value = 4; } + + diff --git a/tests/res/proto/sandbox/test/importcollision.proto b/tests/res/proto/sandbox/test/importcollision.proto new file mode 100644 index 0000000..819a703 --- /dev/null +++ b/tests/res/proto/sandbox/test/importcollision.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package sandbox.test; + + +// This should collide with `typing.Type` when used with `from typing import *` +message Type { + int32 type_id = 1; + string name = 2; +} + + +// This should collide with `typing.Collection` when used with `from typing import *` +message Collection { + string name = 1; + + // Not sure if this causes issues though :D + repeated Type types = 2; +} diff --git a/tests/test_casting.py b/tests/test_casting.py index cd18611..39b9146 100644 --- a/tests/test_casting.py +++ b/tests/test_casting.py @@ -3,6 +3,10 @@ import unittest import os import sys +import shutil +import time + +from tests.testutils import * from protoplasm import casting @@ -10,12 +14,6 @@ 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): def setUp(self): @@ -23,20 +21,7 @@ def setUp(self): @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() - + build_new_protos() # Add build root to path to access its modules sys.path.append(BUILD_ROOT) @@ -574,3 +559,117 @@ def test_cloning(self): self.assertEqual(thing_1.my_subthing, thing_2.my_subthing) self.assertNotEqual(thing_1.my_unique_string, thing_2.my_special_string) + + def test_struct_things_proto_to_dataclass_and_back(self): + from sandbox.test.googlestruct_dc import StructMessage + from sandbox.test.googlestruct_pb2 import StructMessage as StructMessageProto + + structs_pb = StructMessageProto() + structs_pb.my_struct['my_string'] = 'I am String, hear me spell!' + structs_pb.my_struct['my_int'] = 42 + structs_pb.my_struct['my_float'] = 4.2 + structs_pb.my_struct['my_null'] = None + structs_pb.my_struct['my_bool'] = True + structs_pb.my_struct['my_list'] = [1, 3, 5, 8] + structs_pb.my_struct['my_dict'] = {'foo': 'bar', 'you': 'tube'} + structs_pb.my_value.string_value = "Look mom! I'm a string!" + + structs_dc = StructMessage( + my_struct={ + 'my_bool': True, + 'my_float': 4.2, + 'my_null': None, + 'my_dict': {'foo': 'bar', 'you': 'tube'}, + 'my_list': [1.0, 3.0, 5.0, 8.0], + 'my_string': 'I am String, hear me spell!', + 'my_int': 42.0 + }, + my_value="Look mom! I'm a string!" + ) + + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = 7 + structs_pb.my_value.number_value = 7 + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = 43.1234 + structs_pb.my_value.number_value = 43.1234 + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = True + structs_pb.my_value.bool_value = True + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = 123456789 + structs_pb.my_value.number_value = 123456789 + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = False + structs_pb.my_value.bool_value = False + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = 1.23456789123456789 + structs_pb.my_value.number_value = 1.23456789123456789 + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = None + structs_pb.my_value.null_value = 0 + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc), 'A) dataclass_to_proto failed!') + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb), 'B) proto_to_dataclass failed!') + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc)), 'C) proto_to_dataclass(dataclass_to_proto) failed!') + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb)), 'D) dataclass_to_proto(proto_to_dataclass) failed!') + + structs_dc.my_value = [1, 2, 3] + structs_pb.my_value.list_value.append(1) + structs_pb.my_value.list_value.append(2) + structs_pb.my_value.list_value.append(3) + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = True + structs_pb.my_value.bool_value = True + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = ['a', 7, True] + structs_pb.my_value.list_value.append('a') + structs_pb.my_value.list_value.append(7) + structs_pb.my_value.list_value.append(True) + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) + + structs_dc.my_value = {'a': 7, 'b': True} + structs_pb.my_value.struct_value['a'] = 7 + structs_pb.my_value.struct_value['b'] = True + self.assertEqual(structs_pb, casting.dataclass_to_proto(structs_dc)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(structs_pb)) + self.assertEqual(structs_dc, casting.proto_to_dataclass(casting.dataclass_to_proto(structs_dc))) + self.assertEqual(structs_pb, casting.dataclass_to_proto(casting.proto_to_dataclass(structs_pb))) diff --git a/tests/test_castutils.py b/tests/test_castutils.py index 87e943f..e905255 100644 --- a/tests/test_castutils.py +++ b/tests/test_castutils.py @@ -8,33 +8,19 @@ import shutil import time +from tests.testutils import * + 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() - + build_new_protos() # Add build root to path to access its modules sys.path.append(BUILD_ROOT) diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index 4ff5bbc..b8e48d0 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -2,40 +2,20 @@ 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') + +from tests.testutils import * 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() - + build_new_protos() # Add build root to path to access its modules sys.path.append(BUILD_ROOT) @@ -125,3 +105,32 @@ def test_shortcut_dict_args(self): self.assertEqual(dc3, dc4) self.assertEqual(dc3, dc5) self.assertNotEqual(dc1, dc3) + + def test_load_symbols(self): + import sandbox + sandbox.load_symbols() + + def test_import_collision(self): + from protoplasm import plasm + from typing import Type as typing_Type + from sandbox.test.importcollision_dc import Type + self.assertNotEqual(typing_Type, Type) + self.assertTrue(issubclass(Type, plasm.DataclassBase)) + + from sandbox.test.importcollision_dc import Collection + from typing import Collection as typing_Collection + self.assertNotEqual(typing_Collection, Collection) + self.assertTrue(issubclass(Collection, plasm.DataclassBase)) + + def test_import_collision_via_string(self): + from protoplasm import plasm + from ccptools.tpu import strimp + from typing import Type as typing_Type + Type = strimp.get_class('sandbox.test.importcollision_dc.Type') + self.assertNotEqual(typing_Type, Type) + self.assertTrue(issubclass(Type, plasm.DataclassBase)) + + Collection = strimp.get_class('sandbox.test.importcollision_dc.Collection') + from typing import Collection as typing_Collection + self.assertNotEqual(typing_Collection, Collection) + self.assertTrue(issubclass(Collection, plasm.DataclassBase)) diff --git a/tests/test_dictator.py b/tests/test_dictator.py index 0bf0ffe..dc2f0d6 100644 --- a/tests/test_dictator.py +++ b/tests/test_dictator.py @@ -4,39 +4,24 @@ import os import sys +import shutil +import time + from protoplasm import casting from protoplasm.casting import dictator from protoplasm.casting import castutils +from tests.testutils import * + 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() - + build_new_protos() # Add build root to path to access its modules sys.path.append(BUILD_ROOT) @@ -229,7 +214,8 @@ def test_proto_struct_to_dict(self): } expected_b = { - 'my_struct': expected_a + 'my_struct': expected_a, + 'my_value': 'This is a basic string', } proto_struct = googlestruct_pb2.StructMessage() @@ -241,28 +227,65 @@ def test_proto_struct_to_dict(self): proto_struct.my_struct['my_list'] = [1, 3, 5, 8] proto_struct.my_struct['my_dict'] = {'foo': 'bar', 'you': 'tube'} + proto_struct.my_value.string_value = 'This is a basic string' + self.assertEqual(expected_a, dictator.proto_to_dict(proto_struct.my_struct)) + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + expected_b['my_value'] = 7 + proto_struct.my_value.number_value = 7 self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + expected_b['my_value'] = 43.1234 + proto_struct.my_value.number_value = 43.1234 + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) -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) + expected_b['my_value'] = True + proto_struct.my_value.bool_value = True + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) - from neobuilder.neobuilder import NeoBuilder + expected_b['my_value'] = 123456789 + proto_struct.my_value.number_value = 123456789 + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + + expected_b['my_value'] = False + proto_struct.my_value.bool_value = False + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + + expected_b['my_value'] = 1.23456789123456789 + proto_struct.my_value.number_value = 1.23456789123456789 + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + + expected_b['my_value'] = None + proto_struct.my_value.null_value = 0 + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + + expected_b['my_value'] = [1, 2, 3] + proto_struct.my_value.list_value.append(1) + proto_struct.my_value.list_value.append(2) + proto_struct.my_value.list_value.append(3) + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) - # Build stuff... - builder = NeoBuilder(package='sandbox', - protopath=PROTO_ROOT, - build_root=BUILD_ROOT) - builder.build() + expected_b['my_value'] = True + proto_struct.my_value.bool_value = True + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + + expected_b['my_value'] = ['a', 7, True] + proto_struct.my_value.list_value.append('a') + proto_struct.my_value.list_value.append(7) + proto_struct.my_value.list_value.append(True) + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + + expected_b['my_value'] = {'a': 7, 'b': True} + proto_struct.my_value.struct_value['a'] = 7 + proto_struct.my_value.struct_value['b'] = True + self.assertEqual(expected_b, dictator.proto_to_dict(proto_struct)) + +class DataclassToDictTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + build_new_protos() # Add build root to path to access its modules sys.path.append(BUILD_ROOT) @@ -282,7 +305,7 @@ def test_dataclass_to_dict(self): '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?!? + 'bar': ''}, 'donald': {'foo': 'duck', 'bar': 'trump'}}} diff --git a/tests/test_dictators.py b/tests/test_dictators.py index de76ce3..47b5a1f 100644 --- a/tests/test_dictators.py +++ b/tests/test_dictators.py @@ -9,18 +9,17 @@ 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') + +from tests.testutils import * + +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) class FauxEnum(enum.IntEnum): diff --git a/tests/test_objectifier.py b/tests/test_objectifier.py index 96e35d5..8a414fc 100644 --- a/tests/test_objectifier.py +++ b/tests/test_objectifier.py @@ -6,35 +6,20 @@ import os import sys +import shutil +import time + +from tests.testutils import * + 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 + build_new_protos() sys.path.append(BUILD_ROOT) def test_dict_to_proto(self): @@ -676,7 +661,8 @@ def test_struct_dict_to_proto(self): 'my_list': [1.0, 3.0, 5.0, 8.0], 'my_string': 'I am String, hear me spell!', 'my_int': 42.0 - } + }, + 'my_value': 'This is a basic string' } expected_pb = googlestruct_pb2.StructMessage() @@ -687,7 +673,57 @@ def test_struct_dict_to_proto(self): expected_pb.my_struct['my_bool'] = True expected_pb.my_struct['my_list'] = [1, 3, 5, 8] expected_pb.my_struct['my_dict'] = {'foo': 'bar', 'you': 'tube'} + expected_pb.my_value.string_value = 'This is a basic string' + + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = 7 + expected_pb.my_value.number_value = 7 + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = 43.1234 + expected_pb.my_value.number_value = 43.1234 + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = True + expected_pb.my_value.bool_value = True + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = 123456789 + expected_pb.my_value.number_value = 123456789 + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = False + expected_pb.my_value.bool_value = False + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = 1.23456789123456789 + expected_pb.my_value.number_value = 1.23456789123456789 + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = None + expected_pb.my_value.null_value = 0 + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = [1, 2, 3] + expected_pb.my_value.list_value.append(1) + expected_pb.my_value.list_value.append(2) + expected_pb.my_value.list_value.append(3) + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = True + expected_pb.my_value.bool_value = True + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + + the_dict['my_value'] = ['a', 7, True] + expected_pb.my_value.list_value.append('a') + expected_pb.my_value.list_value.append(7) + expected_pb.my_value.list_value.append(True) + self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) + the_dict['my_value'] = {'a': 7, 'b': True} + expected_pb.my_value.struct_value['a'] = 7 + expected_pb.my_value.struct_value['b'] = True self.assertEqual(expected_pb, objectifier.dict_to_proto(googlestruct_pb2.StructMessage, the_dict)) def test_struct_dict_to_dataclass(self): diff --git a/tests/test_services.py b/tests/test_services.py index b20e9e8..d9952eb 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -9,33 +9,18 @@ from protoplasm import errors +from tests.testutils import * + 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() - + build_new_protos() + build_new_protos(package_name='unittesting') # Add build root to path to access its modules sys.path.append(BUILD_ROOT) diff --git a/tests/testutils/__init__.py b/tests/testutils/__init__.py new file mode 100644 index 0000000..b4ea38e --- /dev/null +++ b/tests/testutils/__init__.py @@ -0,0 +1,69 @@ +__all__ = [ + 'run_command', + 'dump_sys', + 'clear_imports', + 'build_new_protos', + + 'HERE', + 'PROTO_ROOT', + 'BUILD_ROOT', + 'PROJECT_ROOT', +] +import os +import sys +import subprocess +import shutil +import time + +import logging +log = logging.getLogger(__file__) + +HERE = os.path.dirname(os.path.dirname(__file__)) +PROTO_ROOT = os.path.join(HERE, 'res', 'proto') +BUILD_ROOT = os.path.join(HERE, 'res', 'build') +PROJECT_ROOT = os.path.dirname(HERE) + + +def run_command(command): + result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=PROJECT_ROOT) + return result.stdout, result.stderr, result.returncode + + +def dump_sys(): + log.info('------ DUMPING SYS -------') + for k, v in sys.modules.items(): + if k.startswith('sandbox'): + log.info(f'{k}: {v}') + + +def clear_imports(): + import importlib + dump_list = [] + for k, v in sys.modules.items(): + if k.startswith('sandbox'): + dump_list.append(k) + for k in dump_list: + del sys.modules[k] + importlib.invalidate_caches() + + +def build_new_protos(package_name: str = 'sandbox'): + build_package = os.path.join(BUILD_ROOT, package_name) + 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() + log.info(f'{PROJECT_ROOT=}') + + stdout, stderr, retcode = run_command(f'python -m neobuilder.cli.neobuilder -b {BUILD_ROOT} {package_name} {PROTO_ROOT}') + log.info(f'{stdout=}') + log.info(f'{stderr=}') + log.info(f'{retcode=}') + # _clear_imports() \ No newline at end of file