Skip to content

Commit

Permalink
[feat] Allow multiple marshals. (#9)
Browse files Browse the repository at this point in the history
This commit removes the "marshal as singleton" component, allowing
different marshals to exist in tandem. In general, each package
should likely use its own marshal.
  • Loading branch information
lukesneeringer committed Dec 27, 2018
1 parent 33da283 commit 84c2c28
Show file tree
Hide file tree
Showing 15 changed files with 137 additions and 83 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ branch = True
fail_under = 100
show_missing = True
omit =
proto/marshal/containers.py
proto/marshal/compat.py
exclude_lines =
# Re-enable the standard pragma
pragma: NO COVER
Expand Down
4 changes: 2 additions & 2 deletions proto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .fields import Field
from .fields import MapField
from .fields import RepeatedField
from .marshal.marshal import marshal
from .marshal import Marshal
from .message import Message
from .primitives import ProtoType

Expand Down Expand Up @@ -43,7 +43,7 @@
'Field',
'MapField',
'RepeatedField',
'marshal',
'Marshal',
'Message',

# Expose the types directly.
Expand Down
7 changes: 7 additions & 0 deletions proto/marshal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .marshal import Marshal


__all__ = (
'Marshal',
)
File renamed without changes.
102 changes: 66 additions & 36 deletions proto/marshal/marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
from google.protobuf import timestamp_pb2
from google.protobuf import wrappers_pb2

from proto.marshal import containers
from proto.marshal import compat
from proto.marshal.collections import MapComposite
from proto.marshal.collections import Repeated
from proto.marshal.collections import RepeatedComposite
from proto.marshal.types import dates
from proto.marshal.types import wrappers
from proto.marshal.rules import dates
from proto.marshal.rules import wrappers


class Rule(abc.ABC):
Expand All @@ -37,8 +37,8 @@ def __subclasshook__(cls, C):
return NotImplemented


class MarshalRegistry:
"""A class to translate between protocol buffers and Python classes.
class BaseMarshal:
"""The base class to translate between protobuf and Python classes.
Protocol buffers defines many common types (e.g. Timestamp, Duration)
which also exist in the Python standard library. The marshal essentially
Expand All @@ -52,14 +52,12 @@ class MarshalRegistry:
the declared field type is still used. This means that, if appropriate,
multiple protocol buffer types may use the same Python type.
The marshal is intended to be a singleton; this module instantiates
and exports one marshal, which is imported throughout the rest of this
library. This allows for an advanced case where user code registers
additional types to be marshaled.
The primary implementation of this is :class:`Marshal`, which should
usually be used instead of this class directly.
"""
def __init__(self):
self._registry = {}
self._noop = NoopMarshal()
self._rules = {}
self._noop = NoopRule()
self.reset()

def register(self, proto_type: type, rule: Rule = None):
Expand All @@ -73,7 +71,7 @@ def register(self, proto_type: type, rule: Rule = None):
This function can also be used as a decorator::
@marshal.register(timestamp_pb2.Timestamp)
class TimestampMarshal:
class TimestampRule:
...
In this case, the class will be initialized for you with zero
Expand All @@ -97,7 +95,7 @@ class TimestampMarshal:
'`to_proto` and `to_python` methods.')

# Register the rule.
self._registry[proto_type] = rule
self._rules[proto_type] = rule
return

# Create an inner function that will register an instance of the
Expand All @@ -109,43 +107,43 @@ def register_rule_class(rule_class: type):
'`to_proto` and `to_python` methods.')

# Register the rule class.
self._registry[proto_type] = rule_class()
self._rules[proto_type] = rule_class()
return rule_class
return register_rule_class

def reset(self):
"""Reset the registry to its initial state."""
self._registry.clear()
self._rules.clear()

# Register date and time wrappers.
self.register(timestamp_pb2.Timestamp, dates.TimestampMarshal())
self.register(duration_pb2.Duration, dates.DurationMarshal())
self.register(timestamp_pb2.Timestamp, dates.TimestampRule())
self.register(duration_pb2.Duration, dates.DurationRule())

# Register nullable primitive wrappers.
self.register(wrappers_pb2.BoolValue, wrappers.BoolValueMarshal())
self.register(wrappers_pb2.BytesValue, wrappers.BytesValueMarshal())
self.register(wrappers_pb2.DoubleValue, wrappers.DoubleValueMarshal())
self.register(wrappers_pb2.FloatValue, wrappers.FloatValueMarshal())
self.register(wrappers_pb2.Int32Value, wrappers.Int32ValueMarshal())
self.register(wrappers_pb2.Int64Value, wrappers.Int64ValueMarshal())
self.register(wrappers_pb2.StringValue, wrappers.StringValueMarshal())
self.register(wrappers_pb2.UInt32Value, wrappers.UInt32ValueMarshal())
self.register(wrappers_pb2.UInt64Value, wrappers.UInt64ValueMarshal())
self.register(wrappers_pb2.BoolValue, wrappers.BoolValueRule())
self.register(wrappers_pb2.BytesValue, wrappers.BytesValueRule())
self.register(wrappers_pb2.DoubleValue, wrappers.DoubleValueRule())
self.register(wrappers_pb2.FloatValue, wrappers.FloatValueRule())
self.register(wrappers_pb2.Int32Value, wrappers.Int32ValueRule())
self.register(wrappers_pb2.Int64Value, wrappers.Int64ValueRule())
self.register(wrappers_pb2.StringValue, wrappers.StringValueRule())
self.register(wrappers_pb2.UInt32Value, wrappers.UInt32ValueRule())
self.register(wrappers_pb2.UInt64Value, wrappers.UInt64ValueRule())

def to_python(self, proto_type, value, *, absent: bool = None):
# Internal protobuf has its own special type for lists of values.
# Return a view around it that implements MutableSequence.
if isinstance(value, containers.repeated_composite_types):
if isinstance(value, compat.repeated_composite_types):
return RepeatedComposite(value, marshal=self)
if isinstance(value, containers.repeated_scalar_types):
if isinstance(value, compat.repeated_scalar_types):
return Repeated(value, marshal=self)

# Same thing for maps of messages.
if isinstance(value, containers.map_composite_types):
if isinstance(value, compat.map_composite_types):
return MapComposite(value, marshal=self)

# Convert ordinary values.
rule = self._registry.get(proto_type, self._noop)
rule = self._rules.get(proto_type, self._noop)
return rule.to_python(value, absent=absent)

def to_proto(self, proto_type, value, *, strict: bool = False):
Expand All @@ -172,7 +170,7 @@ def to_proto(self, proto_type, value, *, strict: bool = False):
for k, v in value.items()}

# Convert ordinary values.
rule = self._registry.get(proto_type, self._noop)
rule = self._rules.get(proto_type, self._noop)
pb_value = rule.to_proto(value)

# Sanity check: If we are in strict mode, did we get the value we want?
Expand All @@ -189,8 +187,42 @@ def to_proto(self, proto_type, value, *, strict: bool = False):
return pb_value


class NoopMarshal:
"""A catch-all marshal that does nothing."""
class Marshal(BaseMarshal):
"""The translator between protocol buffer and Python instances.
The bulk of the implementation is in :class:`BaseMarshal`. This class
adds identity tracking: multiple instantiations of :class:`Marshal` with
the same name will provide the same instance.
"""
_instances = {}

def __new__(cls, *, name: str):
"""Create a marshal instance.
Args:
name (str): The name of the marshal. Instantiating multiple
marshals with the same ``name`` argument will provide the
same marshal each time.
"""
if name not in cls._instances:
cls._instances[name] = super().__new__(cls)
return cls._instances[name]

def __init__(self, *, name: str):
"""Instantiate a marshal.
Args:
name (str): The name of the marshal. Instantiating multiple
marshals with the same ``name`` argument will provide the
same marshal each time.
"""
self._name = name
if not hasattr(self, '_rules'):
super().__init__()


class NoopRule:
"""A catch-all rule that does nothing."""

def to_python(self, pb_value, *, absent: bool = None):
return pb_value
Expand All @@ -199,8 +231,6 @@ def to_proto(self, value):
return value


marshal = MarshalRegistry()

__all__ = (
'marshal',
'Marshal',
)
File renamed without changes.
4 changes: 2 additions & 2 deletions proto/marshal/types/dates.py → proto/marshal/rules/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from google.protobuf import timestamp_pb2


class TimestampMarshal:
class TimestampRule:
"""A marshal between Python datetimes and protobuf timestamps.
Note: Python datetimes are less precise than protobuf datetimes
Expand All @@ -47,7 +47,7 @@ def to_proto(self, value) -> timestamp_pb2.Timestamp:
return value


class DurationMarshal:
class DurationRule:
"""A marshal between Python timedeltas and protobuf durations.
Note: Python timedeltas are less precise than protobuf durations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.


class MessageMarshal:
class MessageRule:
"""A marshal for converting between a descriptor and proto.Message."""

def __init__(self, descriptor: type, wrapper: type):
Expand Down
20 changes: 10 additions & 10 deletions proto/marshal/types/wrappers.py → proto/marshal/rules/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from google.protobuf import wrappers_pb2


class WrapperMarshal:
class WrapperRule:
"""A marshal for converting the protobuf wrapper classes to Python.
This class converts between ``google.protobuf.BoolValue``,
Expand All @@ -38,46 +38,46 @@ def to_proto(self, value):
return value


class DoubleValueMarshal(WrapperMarshal):
class DoubleValueRule(WrapperRule):
_proto_type = wrappers_pb2.DoubleValue
_python_type = float


class FloatValueMarshal(WrapperMarshal):
class FloatValueRule(WrapperRule):
_proto_type = wrappers_pb2.FloatValue
_python_type = float


class Int64ValueMarshal(WrapperMarshal):
class Int64ValueRule(WrapperRule):
_proto_type = wrappers_pb2.Int64Value
_python_type = int


class UInt64ValueMarshal(WrapperMarshal):
class UInt64ValueRule(WrapperRule):
_proto_type = wrappers_pb2.UInt64Value
_python_type = int


class Int32ValueMarshal(WrapperMarshal):
class Int32ValueRule(WrapperRule):
_proto_type = wrappers_pb2.Int32Value
_python_type = int


class UInt32ValueMarshal(WrapperMarshal):
class UInt32ValueRule(WrapperRule):
_proto_type = wrappers_pb2.UInt32Value
_python_type = int


class BoolValueMarshal(WrapperMarshal):
class BoolValueRule(WrapperRule):
_proto_type = wrappers_pb2.BoolValue
_python_type = bool


class StringValueMarshal(WrapperMarshal):
class StringValueRule(WrapperRule):
_proto_type = wrappers_pb2.StringValue
_python_type = str


class BytesValueMarshal(WrapperMarshal):
class BytesValueRule(WrapperRule):
_proto_type = wrappers_pb2.BytesValue
_python_type = bytes

0 comments on commit 84c2c28

Please sign in to comment.