Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 5.3.0 - Support for Structs #6

Merged
merged 4 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading