Skip to content

Commit

Permalink
[refactor] Make collection wrappers take their marshal object. (#8)
Browse files Browse the repository at this point in the history
This commit makes collections accept their marshal object, instead of
referencing a singleton. This will make it easier to make marshals
have a defined scope.
  • Loading branch information
lukesneeringer committed Dec 27, 2018
1 parent c1e20e1 commit 33da283
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 164 deletions.
24 changes: 24 additions & 0 deletions proto/marshal/collections/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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 .map import MapComposite
from .repeated import Repeated
from .repeated import RepeatedComposite


__all__ = (
'MapComposite',
'Repeated',
'RepeatedComposite',
)
81 changes: 81 additions & 0 deletions proto/marshal/collections/map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.

import collections

from proto.utils import cached_property


class MapComposite(collections.MutableMapping):
"""A view around a mutable sequence in protocol buffers.
This implements the full Python MutableMapping interface, but all methods
modify the underlying field container directly.
"""
@cached_property
def _pb_type(self):
"""Return the protocol buffer type for this sequence."""
# Huzzah, another hack. Still less bad than RepeatedComposite.
return type(self.pb.GetEntryClass()().value)

def __init__(self, sequence, *, marshal):
"""Initialize a wrapper around a protobuf map.
Args:
sequence: A protocol buffers map.
marshal (~.MarshalRegistry): An instantiated marshal, used to
convert values going to and from this map.
"""
self._pb = sequence
self._marshal = marshal

def __contains__(self, key):
# Protocol buffers is so permissive that querying for the existence
# of a key will in of itself create it.
#
# By taking a tuple of the keys and querying that, we avoid sending
# the lookup to protocol buffers and therefore avoid creating the key.
return key in tuple(self.keys())

def __getitem__(self, key):
# We handle raising KeyError ourselves, because otherwise protocol
# buffers will create the key if it does not exist.
if key not in self:
raise KeyError(key)
return self._marshal.to_python(self._pb_type, self.pb[key])

def __setitem__(self, key, value):
pb_value = self._marshal.to_proto(self._pb_type, value, strict=True)

# Directly setting a key is not allowed; however, protocol buffers
# is so permissive that querying for the existence of a key will in
# of itself create it.
#
# Therefore, we create a key that way (clearing any fields that may
# be set) and then merge in our values.
self.pb[key].Clear()
self.pb[key].MergeFrom(pb_value)

def __delitem__(self, key):
self.pb.pop(key)

def __len__(self):
return len(self.pb)

def __iter__(self):
return iter(self.pb)

@property
def pb(self):
return self._pb
127 changes: 127 additions & 0 deletions proto/marshal/collections/repeated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.

import collections
import copy

from proto.utils import cached_property


class Repeated(collections.MutableSequence):
"""A view around a mutable sequence in protocol buffers.
This implements the full Python MutableSequence interface, but all methods
modify the underlying field container directly.
"""
def __init__(self, sequence, *, marshal):
"""Initialize a wrapper around a protobuf repeated field.
Args:
sequence: A protocol buffers repeated field.
marshal (~.MarshalRegistry): An instantiated marshal, used to
convert values going to and from this map.
"""
self._pb = sequence
self._marshal = marshal

def __copy__(self):
"""Copy this object and return the copy."""
return type(self)(self.pb[:], marshal=self._marshal)

def __delitem__(self, key):
"""Delete the given item."""
del self.pb[key]

def __eq__(self, other):
if hasattr(other, 'pb'):
return tuple(self.pb) == tuple(other.pb)
return tuple(self.pb) == tuple(other)

def __getitem__(self, key):
"""Return the given item."""
return self.pb[key]

def __len__(self):
"""Return the length of the sequence."""
return len(self.pb)

def __ne__(self, other):
return not self == other

def __repr__(self):
return repr(self.pb)

def __setitem__(self, key, value):
self.pb[key] = value

def insert(self, index: int, value):
"""Insert ``value`` in the sequence before ``index``."""
self.pb.insert(index, value)

def sort(self, *, key: str = None, reverse: bool = False):
"""Stable sort *IN PLACE*."""
self.pb.sort(key=key, reverse=reverse)

@property
def pb(self):
return self._pb


class RepeatedComposite(Repeated):
"""A view around a mutable sequence of messages in protocol buffers.
This implements the full Python MutableSequence interface, but all methods
modify the underlying field container directly.
"""
@cached_property
def _pb_type(self):
"""Return the protocol buffer type for this sequence."""
# There is no public-interface mechanism to determine the type
# of what should go in the list (and the C implementation seems to
# have no exposed mechanism at all).
#
# If the list has members, use the existing list members to
# determine the type.
if len(self.pb) > 0:
return type(self.pb[0])

# We have no members in the list.
# In order to get the type, we create a throw-away copy and add a
# blank member to it.
canary = copy.deepcopy(self.pb).add()
return type(canary)

def __getitem__(self, key):
return self._marshal.to_python(self._pb_type, self.pb[key])

def __setitem__(self, key, value):
pb_value = self._marshal.to_proto(self._pb_type, value, strict=True)

# Protocol buffers does not define a useful __setitem__, so we
# have to pop everything after this point off the list and reload it.
after = [pb_value]
while self.pb[key:]:
after.append(self.pb.pop(key))
self.pb.extend(after)

def insert(self, index: int, value):
"""Insert ``value`` in the sequence before ``index``."""
pb_value = self._marshal.to_proto(self._pb_type, value, strict=True)

# Protocol buffers does not define a useful insert, so we have
# to pop everything after this point off the list and reload it.
after = [pb_value]
while self.pb[index:]:
after.append(self.pb.pop(index))
self.pb.extend(after)

0 comments on commit 33da283

Please sign in to comment.