Skip to content

Commit

Permalink
Merge pull request #6 from ccpgames/fix/typing-side-effects
Browse files Browse the repository at this point in the history
Version 5.3.0 - Support for Structs
  • Loading branch information
CCP-Zeulix authored Sep 24, 2024
2 parents 85ee496 + 913d5cc commit a4be26f
Show file tree
Hide file tree
Showing 21 changed files with 447 additions and 162 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker/alpy/base.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION}-alpine3.19
FROM python:${PYTHON_VERSION}-alpine3.20

COPY requirements.txt .

Expand Down
2 changes: 1 addition & 1 deletion protoplasm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '5.2.0'
__version__ = '5.3.0'

__author__ = 'Thordur Matthiasson <[email protected]>'
__license__ = 'MIT License'
Expand Down
6 changes: 3 additions & 3 deletions protoplasm/casting/dictator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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([email protected]>) 2024-04-15: To include "default/empty" fields or not?
if dicted is not ...:
d[_get_proto_field_name(field)] = dicted

return d
Expand All @@ -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)
Expand Down
53 changes: 49 additions & 4 deletions protoplasm/casting/dictators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
'DurationDictator',
'AnyDictator',
'StructDictator',
'ValueDictator',
'ListValueDictator',
'NullValueDictator',
]
import datetime
import base64
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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]:
Expand All @@ -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
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions requirements-neobuilder.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
neobuilder >=5.3, <6
4 changes: 3 additions & 1 deletion requirements-unittesting.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
neobuilder >=5.2.0-rc.1, <6
-r requirements.txt
Jinja2 >=3.1, <4
semver >= 3.0.2, <4
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions tests/res/proto/sandbox/test/googlestruct.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


19 changes: 19 additions & 0 deletions tests/res/proto/sandbox/test/importcollision.proto
Original file line number Diff line number Diff line change
@@ -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;
}
139 changes: 119 additions & 20 deletions tests/test_casting.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,25 @@
import unittest
import os
import sys
import shutil
import time

from tests.testutils import *

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):
def setUp(self):
self.maxDiff = None

@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)

Expand Down Expand Up @@ -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)))
Loading

0 comments on commit a4be26f

Please sign in to comment.