1515import collections
1616import collections .abc
1717import copy
18+ import inspect
1819import re
1920from typing import List , Type
2021
3132from 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+
3440class 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
636711class _MessageInfo :
0 commit comments