Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit 5746a77

Browse files
committed
start of pb performance refactor
1 parent e25dd7d commit 5746a77

2 files changed

Lines changed: 114 additions & 24 deletions

File tree

proto/fields.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ def descriptor(self):
114114
# Return the descriptor.
115115
return self._descriptor
116116

117+
def contribute_to_class(self, cls, name: str):
118+
119+
class _Descriptor:
120+
def __init__(self, name: str):
121+
self.name = f'_{name}'
122+
123+
def __set__(self, instance, value):
124+
setattr(instance, self.name, value)
125+
instance._mark_dirty()
126+
127+
def __get__(self, instance, owner):
128+
return getattr(instance, self.name, None)
129+
130+
setattr(cls, name, _Descriptor(name))
131+
117132
@property
118133
def name(self) -> str:
119134
"""Return the name of the field."""

proto/message.py

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import collections
1616
import collections.abc
1717
import copy
18+
import inspect
1819
import re
1920
from typing import List, Type
2021

@@ -31,6 +32,11 @@
3132
from proto.primitives import ProtoType
3233

3334

35+
def _has_contribute_to_class(value):
36+
# Only call contribute_to_class() if it's bound.
37+
return not inspect.isclass(value) and hasattr(value, 'contribute_to_class')
38+
39+
3440
class MessageMeta(type):
3541
"""A metaclass for building and registering Message subclasses."""
3642

@@ -105,6 +111,7 @@ def __new__(mcls, name, bases, attrs):
105111
# Okay, now we deal with all the rest of the fields.
106112
# Iterate over all the attributes and separate the fields into
107113
# their own sequence.
114+
contributable_attrs = {}
108115
fields = []
109116
new_attrs = {}
110117
oneofs = collections.OrderedDict()
@@ -127,6 +134,9 @@ def __new__(mcls, name, bases, attrs):
127134
"package": package,
128135
}
129136

137+
if _has_contribute_to_class(field):
138+
contributable_attrs[key] = field
139+
130140
# Add the field to the list of fields.
131141
fields.append(field)
132142
# If this field is part of a "oneof", ensure the oneof itself
@@ -248,6 +258,9 @@ def __new__(mcls, name, bases, attrs):
248258
# Run the superclass constructor.
249259
cls = super().__new__(mcls, name, bases, new_attrs)
250260

261+
for field_name, field in contributable_attrs.items():
262+
cls.add_to_class(field_name, field)
263+
251264
# The info class and fields need a reference to the class just created.
252265
cls._meta.parent = cls
253266
for field in cls._meta.fields.values():
@@ -269,6 +282,12 @@ def __new__(mcls, name, bases, attrs):
269282
def __prepare__(mcls, name, bases, **kwargs):
270283
return collections.OrderedDict()
271284

285+
def add_to_class(cls, name, value):
286+
if _has_contribute_to_class(value):
287+
value.contribute_to_class(cls, name)
288+
else:
289+
setattr(cls, name, value)
290+
272291
@property
273292
def meta(cls):
274293
return cls._meta
@@ -313,6 +332,9 @@ def serialize(cls, instance) -> bytes:
313332
Returns:
314333
bytes: The serialized representation of the protocol buffer.
315334
"""
335+
if instance.is_dirty:
336+
new_pb_values = instance._map_from_fields()
337+
instance._update_pb(new_pb_values)
316338
return cls.pb(instance, coerce=True).SerializeToString()
317339

318340
def deserialize(cls, payload: bytes) -> "Message":
@@ -445,6 +467,13 @@ class Message(metaclass=MessageMeta):
445467
message.
446468
"""
447469

470+
CLEAN_ATTRS = '_clean_attrs'
471+
472+
@property
473+
def is_dirty(self) -> bool:
474+
"""Default state is "clean", so the attr not being set indicates no problems."""
475+
return not getattr(self, self.CLEAN_ATTRS, True)
476+
448477
def __init__(self, mapping=None, *, ignore_unknown_fields=False, **kwargs):
449478
# We accept several things for `mapping`:
450479
# * An instance of this class.
@@ -454,7 +483,7 @@ def __init__(self, mapping=None, *, ignore_unknown_fields=False, **kwargs):
454483
if mapping is None:
455484
if not kwargs:
456485
# Special fast path for empty construction.
457-
super().__setattr__("_pb", self._meta.pb())
486+
# super().__setattr__("_pb", self._meta.pb())
458487
return
459488

460489
mapping = kwargs
@@ -488,10 +517,10 @@ def __init__(self, mapping=None, *, ignore_unknown_fields=False, **kwargs):
488517
% (self.__class__.__name__, mapping,)
489518
)
490519

491-
params = {}
520+
# params = {}
492521
# Update the mapping to address any values that need to be
493522
# coerced.
494-
marshal = self._meta.marshal
523+
# marshal = self._meta.marshal
495524
for key, value in mapping.items():
496525
try:
497526
pb_type = self._meta.fields[key].pb_type
@@ -503,12 +532,14 @@ def __init__(self, mapping=None, *, ignore_unknown_fields=False, **kwargs):
503532
"Unknown field for {}: {}".format(self.__class__.__name__, key)
504533
)
505534

506-
pb_value = marshal.to_proto(pb_type, value)
507-
if pb_value is not None:
508-
params[key] = pb_value
535+
# pb_value = marshal.to_proto(pb_type, value)
536+
# if pb_value is not None:
537+
# params[key] = pb_value
538+
setattr(self, key, value)
509539

510540
# Create the internal protocol buffer.
511-
super().__setattr__("_pb", self._meta.pb(**params))
541+
super().__setattr__("_pb", self._meta.pb())
542+
self._mark_dirty()
512543

513544
def __bool__(self):
514545
"""Return True if any field is truthy, False otherwise."""
@@ -609,28 +640,72 @@ def __ne__(self, other):
609640
return not self == other
610641

611642
def __repr__(self):
612-
return repr(self._pb)
643+
if (getattr(self, '_pb'), None) is not None:
644+
return repr(self._pb)
645+
return repr(self)
646+
647+
# def __setattr__(self, key, value):
648+
# """Set the value on the given field.
649+
650+
# For well-known protocol buffer types which are marshalled, either
651+
# the protocol buffer object or the Python equivalent is accepted.
652+
# """
653+
# if key[0] == "_" or key in self.__dict__:
654+
# return super().__setattr__(key, value)
655+
# marshal = self._meta.marshal
656+
# pb_type = self._meta.fields[key].pb_type
657+
# pb_value = marshal.to_proto(pb_type, value)
658+
659+
# # Clear the existing field.
660+
# # This is the only way to successfully write nested falsy values,
661+
# # because otherwise MergeFrom will no-op on them.
662+
# self._pb.ClearField(key)
663+
664+
# # Merge in the value being set.
665+
# if pb_value is not None:
666+
# self._pb.MergeFrom(self._meta.pb(**{key: pb_value}))
613667

614-
def __setattr__(self, key, value):
615-
"""Set the value on the given field.
616-
617-
For well-known protocol buffer types which are marshalled, either
618-
the protocol buffer object or the Python equivalent is accepted.
668+
@property
669+
def _pb(self):
670+
if not hasattr(self, '_cached_pb'):
671+
self._cached_pb = self._meta.pb()
672+
return self._cached_pb
673+
674+
@_pb.setter
675+
def _pb(self, value):
676+
self._cached_pb = value
677+
678+
def _map_from_fields(self) -> dict:
679+
_map = {}
680+
for field_name in self._meta.fields.keys():
681+
_map[field_name] = getattr(self, field_name)
682+
return _map
683+
684+
def _update_pb(self, values: dict):
685+
"""Batch update of inner _pb, used before serialization
619686
"""
620-
if key[0] == "_":
621-
return super().__setattr__(key, value)
622-
marshal = self._meta.marshal
623-
pb_type = self._meta.fields[key].pb_type
624-
pb_value = marshal.to_proto(pb_type, value)
625-
626-
# Clear the existing field.
627-
# This is the only way to successfully write nested falsy values,
628-
# because otherwise MergeFrom will no-op on them.
629-
self._pb.ClearField(key)
687+
_marshalled = {}
688+
for key, value in values.items():
689+
marshal = self._meta.marshal
690+
pb_type = self._meta.fields[key].pb_type
691+
pb_value = marshal.to_proto(pb_type, value)
692+
_marshalled[key] = pb_value
693+
694+
# Clear the existing field.
695+
# This is the only way to successfully write nested falsy values,
696+
# because otherwise MergeFrom will no-op on them.
697+
self._pb.ClearField(key)
630698

631699
# Merge in the value being set.
632700
if pb_value is not None:
633-
self._pb.MergeFrom(self._meta.pb(**{key: pb_value}))
701+
self._pb.MergeFrom(self._meta.pb(**_marshalled))
702+
self._mark_clean()
703+
704+
def _mark_clean(self):
705+
self._clean_attrs = True
706+
707+
def _mark_dirty(self):
708+
self._clean_attrs = False
634709

635710

636711
class _MessageInfo:

0 commit comments

Comments
 (0)