ll change the state of # dct. for key, field in dct.items(): if key in _RESERVED_ATTRIBUTE_NAMES: continue if isinstance(field, type) and issubclass(field, Enum): enums.append(key) continue if (isinstance(field, type) and issubclass(field, Message) and field is not Message): messages.append(key) continue # Reject anything that is not a field. # pylint:disable=unidiomatic-typecheck if type(field) is Field or not isinstance(field, Field): raise MessageDefinitionError( 'May only use fields in message definitions. ' 'Found: %s = %s' % (key, field)) if field.number in by_number: raise DuplicateNumberError( 'Field with number %d declared more than once in %s' % (field.number, name)) field.name = key # Place in name and number maps. by_name[key] = field by_number[field.number] = field # Add enums if any exist. if enums: dct['__enums__'] = sorted(enums) # Add messages if any exist. if messages: dct['__messages__'] = sorted(messages) dct['_Message__by_number'] = by_number dct['_Message__by_name'] = by_name return _DefinitionClass.__new__(cls, name, bases, dct) def __init__(cls, name, bases, dct): """Initializer required to assign references to new class.""" if bases != (object,): for v in dct.values(): if isinstance(v, _DefinitionClass) and v is not Message: v._message_definition = weakref.ref(cls) for field in cls.all_fields(): field._message_definition = weakref.ref(cls) _DefinitionClass.__init__(cls, name, bases, dct) class Message(six.with_metaclass(_MessageClass, object)): """Base class for user defined message objects. Used to define messages for efficient transmission across network or process space. Messages are defined using the field classes (IntegerField, FloatField, EnumField, etc.). Messages are more restricted than normal classes in that they may only contain field attributes and other Message and Enum definitions. These restrictions are in place because the structure of the Message class is intentended to itself be transmitted across network or process space and used directly by clients or even other servers. As such methods and non-field attributes could not be transmitted with the structural information causing discrepancies between different languages and implementations. Initialization and validation: A Message object is considered to be initialized if it has all required fields and any nested messages are also initialized. Calling 'check_initialized' will raise a ValidationException if it is not initialized; 'is_initialized' returns a boolean value indicating if it is valid. Validation automatically occurs when Message objects are created and populated. Validation that a given value will be compatible with a field that it is assigned to can be done through the Field instances validate() method. The validate method used on a message will check that all values of a message and its sub-messages are valid. Assingning an invalid value to a field will raise a ValidationException. Example: # Trade type. class TradeType(Enum): BUY = 1 SELL = 2 SHORT = 3 CALL = 4 class Lot(Message): price = IntegerField(1, required=True) quantity = IntegerField(2, required=True) class Order(Message): symbol = StringField(1, required=True) total_quantity = IntegerField(2, required=True) trade_type = EnumField(TradeType, 3, required=True) lots = MessageField(Lot, 4, repeated=True) limit = IntegerField(5) order = Order(symbol='GOOG', total_quantity=10, trade_type=TradeType.BUY) lot1 = Lot(price=304, quantity=7) lot2 = Lot(price = 305, quantity=3) order.lots = [lot1, lot2] # Now object is initialized! order.check_initialized() """ def __init__(self, **kwargs): """Initialize internal messages state. Args: A message can be initialized via the constructor by passing in keyword arguments corresponding to fields. For example: class Date(Message): day = IntegerField(1) month = IntegerField(2) year = IntegerField(3) Invoking: date = Date(day=6, month=6, year=1911) is the same as doing: date = Date() date.day = 6 date.month = 6 date.year = 1911 """ # Tag being an essential implementation detail must be private. self.__tags = {} self.__unrecognized_fields = {} assigned = set() for name, value in kwargs.items(): setattr(self, name, value) assigned.add(name) # initialize repeated fields. for field in self.all_fields(): if field.repeated and field.name not in assigned: setattr(self, field.name, []) def check_initialized(self): """Check class for initialization status. Check that all required fields are initialized Raises: ValidationError: If message is not initialized. """ for name, field in self.__by_name.items(): value = getattr(self, name) if value is None: if field.required: raise ValidationError( "Message %s is missing required field %s" % (type(self).__name__, name)) else: try: if (isinstance(field, MessageField) and issubclass(field.message_type, Message)): if field.repeated: for item in value: item_message_value = field.value_to_message( item) item_message_value.check_initialized() else: message_value = field.value_to_message(value) message_value.check_initialized() except ValidationError as err: if not hasattr(err, 'message_name'): err.message_name = type(self).__name__ raise def is_initialized(self): """Get initialization status. Returns: True if message is valid, else False. """ try: self.check_initialized() except ValidationError: return False else: return True @classmethod def all_fields(cls): """Get all field definition objects. Ordering is arbitrary. Returns: Iterator over all values in arbitrary order. """ return cls.__by_name.values() @classmethod def field_by_name(cls, name): """Get field by name. Returns: Field object associated with name. Raises: KeyError if no field found by that name. """ return cls.__by_name[name] @classmethod def field_by_number(cls, number): """Get field by number. Returns: Field object associated with number. Raises: KeyError if no field found by that number. """ return cls.__by_number[number] def get_assigned_value(self, name): """Get the assigned value of an attribute. Get the underlying value of an attribute. If value has not been set, will not return the default for the field. Args: name: Name of attribute to get. Returns: Value of attribute, None if it has not been set. """ message_type = type(self) try: field = message_type.field_by_name(name) except KeyError: raise AttributeError('Message %s has no field %s' % ( message_type.__name__, name)) return self.__tags.get(field.number) def reset(self, name): """Reset assigned value for field. Resetting a field will return it to its default value or None. Args: name: Name of field to reset. """ message_type = type(self) try: field = message_type.field_by_name(name) except KeyError: if name not in message_type.__by_name: raise AttributeError('Message %s has no field %s' % ( message_type.__name__, name)) if field.repeated: self.__tags[field.number] = FieldList(field, []) else: self.__tags.pop(field.number, None) def all_unrecognized_fields(self): """Get the names of all unrecognized fields in this message.""" return list(self.__unrecognized_fields.keys()) def get_unrecognized_field_info(self, key, value_default=None, variant_default=None): """Get the value and variant of an unknown field in this message. Args: key: The name or number of the field to retrieve. value_default: Value to be returned if the key isn't found. variant_default: Value to be returned as variant if the key isn't found. Returns: (value, variant), where value and variant are whatever was passed to set_unrecognized_field. """ value, variant = self.__unrecognized_fields.get(key, (value_default, variant_default)) return value, variant def set_unrecognized_field(self, key, value, variant): """Set an unrecognized field, used when decoding a message. Args: key: The name or number used to refer to this unknown value. value: The value of the field. variant: Type information needed to interpret the value or re-encode it. Raises: TypeError: If the variant is not an instance of messages.Variant. """ if not isinstance(variant, Variant): raise TypeError('Variant type %s is not valid.' % variant) self.__unrecognized_fields[key] = value, variant def __setattr__(self, name, value): """Change set behavior for messages. Messages may only be assigned values that are fields. Does not try to validate field when set. Args: name: Name of field to assign to. value: Value to assign to field. Raises: AttributeError when trying to assign value that is not a field. """ if name in self.__by_name or name.startswith('_Message__'): object.__setattr__(self, name, value) else: raise AttributeError("May not assign arbitrary value %s " "to message %s" % (name, type(self).__name__)) def __repr__(self): """Make string representation of message. Example: class MyMessage(messages.Message): integer_value = messages.IntegerField(1) string_value = messages.StringField(2) my_message = MyMessage() my_message.integer_value = 42 my_message.string_value = u'A string' print my_message >>> Returns: String representation of message, including the values of all fields and repr of all sub-messages. """ body = ['<', type(self).__name__] for field in sorted(self.all_fields(), key=lambda f: f.number): attribute = field.name value = self.get_assigned_value(field.name) if value is not None: body.append('\n %s: %s' % (attribute, repr(value))) body.append('>') return ''.join(body) def __eq__(self, other): """Equality operator. Does field by field comparison with other message. For equality, must be same type and values of all fields must be equal. Messages not required to be initialized for comparison. Does not attempt to determine equality for values that have default values that are not set. In other words: class HasDefault(Message): attr1 = StringField(1, default='default value') message1 = HasDefault() message2 = HasDefault() message2.attr1 = 'default value' message1 != message2 Does not compare unknown values. Args: other: Other message to compare with. """ # TODO(user): Implement "equivalent" which does comparisons # taking default values in to consideration. if self is other: return True if type(self) is not type(other): return False return self.__tags == other.__tags def __ne__(self, other): """Not equals operator. Does field by field comparison with other message. For non-equality, must be different type or any value of a field must be non-equal to the same field in the other instance. Messages not required to be initialized for comparison. Args: other: Other message to compare with. """ return not self.__eq__(other) class FieldList(list): """List implementation that validates field values. This list implementation overrides all methods that add values in to a list in order to validate those new elements. Attempting to add or set list values that are not of the correct type will raise ValidationError. """ def __init__(self, field_instance, sequence): """Constructor. Args: field_instance: Instance of field that validates the list. sequence: List or tuple to construct list from. """ if not field_instance.repeated: raise FieldDefinitionError( 'FieldList may only accept repeated fields') self.__field = field_instance self.__field.validate(sequence) list.__init__(self, sequence) def __getstate__(self): """Enable pickling. The assigned field instance can't be pickled if it belongs to a Message definition (message_definition uses a weakref), so the Message class and field number are returned in that case. Returns: A 3-tuple containing: - The field instance, or None if it belongs to a Message class. - The Message class that the field instance belongs to, or None. - The field instance number of the Message class it belongs to, or None. """ message_class = self.__field.message_definition() if message_class is None: return self.__field, None, None return None, message_class, self.__field.number def __setstate__(self, state): """Enable unpickling. Args: state: A 3-tuple containing: - The field instance, or None if it belongs to a Message class. - The Message class that the field instance belongs to, or None. - The field instance number of the Message class it belongs to, or None. """ field_instance, message_class, number = state if field_instance is None: self.__field = message_class.field_by_number(number) else: self.__field = field_instance @property def field(self): """Field that validates list.""" return self.__field def __setslice__(self, i, j, sequence): """Validate slice assignment to list.""" self.__field.validate(sequence) list.__setslice__(self, i, j, sequence) def __setitem__(self, index, value): """Validate item assignment to list.""" if isinstance(index, slice): self.__field.validate(value) else: self.__field.validate_element(value) list.__setitem__(self, index, value) def append(self, value): """Validate item appending to list.""" if getattr(self, '_FieldList__field', None): self.__field.validate_element(value) return list.append(self, value) def extend(self, sequence): """Validate extension of list.""" if getattr(self, '_FieldList__field', None): self.__field.validate(sequence) return list.extend(self, sequence) def insert(self, index, value): """Validate item insertion to list.""" self.__field.validate_element(value) return list.insert(self, index, value) class _FieldMeta(type): def __init__(cls, name, bases, dct): getattr(cls, '_Field__variant_to_type').update( (variant, cls) for variant in dct.get('VARIANTS', [])) type.__init__(cls, name, bases, dct) # TODO(user): Prevent additional field subclasses. class Field(six.with_metaclass(_FieldMeta, object)): """Definition for message field.""" __initialized = False # pylint:disable=invalid-name __variant_to_type = {} # pylint:disable=invalid-name @util.positional(2) def __init__(self, number, required=False, repeated=False, variant=None, default=None): """Constructor. The required and repeated parameters are mutually exclusive. Setting both to True will raise a FieldDefinitionError. Sub-class Attributes: Each sub-class of Field must define the following: VARIANTS: Set of variant types accepted by that field. DEFAULT_VARIANT: Default variant type if not specified in constructor. Args: number: Number of field. Must be unique per message class. required: Whether or not field is required. Mutually exclusive with 'repeated'. repeated: Whether or not field is repeated. Mutually exclusive with 'required'. variant: Wire-format variant hint. default: Default value for field if not found in stream. Raises: InvalidVariantError when invalid variant for field is provided. InvalidDefaultError when invalid default for field is provided. FieldDefinitionError when invalid number provided or mutually exclusive fields are used. InvalidNumberError when the field number is out of range or reserved. """ if not isinstance(number, int) or not 1 <= number <= MAX_FIELD_NUMBER: raise InvalidNumberError( 'Invalid number for field: %s\n' 'Number must be 1 or greater and %d or less' % (number, MAX_FIELD_NUMBER)) if FIRST_RESERVED_FIELD_NUMBER <= number <= LAST_RESERVED_FIELD_NUMBER: raise InvalidNumberError('Tag number %d is a reserved number.\n' 'Numbers %d to %d are reserved' % (number, FIRST_RESERVED_FIELD_NUMBER, LAST_RESERVED_FIELD_NUMBER)) if repeated and required: raise FieldDefinitionError('Cannot set both repeated and required') if variant is None: variant = self.DEFAULT_VARIANT if repeated and default is not None: raise FieldDefinitionError('Repeated fields may not have defaults') if variant not in self.VARIANTS: raise InvalidVariantError( 'Invalid variant: %s\nValid variants for %s are %r' % (variant, type(self).__name__, sorted(self.VARIANTS))) self.number = number self.required = required self.repeated = repeated self.variant = variant if default is not None: try: self.validate_default(default) except ValidationError as err: try: name = self.name except AttributeError: # For when raising error before name initialization. raise InvalidDefaultError( 'Invalid default value for %s: %r: %s' % (self.__class__.__name__, default, err)) else: raise InvalidDefaultError( 'Invalid default value for field %s: ' '%r: %s' % (name, default, err)) self.__default = default self.__initialized = True def __setattr__(self, name, value): """Setter overidden to prevent assignment to fields after creation. Args: name: Name of attribute to set. value: Value to assign. """ # Special case post-init names. They need to be set after constructor. if name in _POST_INIT_FIELD_ATTRIBUTE_NAMES: object.__setattr__(self, name, value) return # All other attributes must be set before __initialized. if not self.__initialized: # Not initialized yet, allow assignment. object.__setattr__(self, name, value) else: raise AttributeError('Field objects are read-only') def __set__(self, message_instance, value): """Set value on message. Args: message_instance: Message instance to set value on. value: Value to set on message. """ # Reaches in to message instance directly to assign to private tags. if value is None: if self.repeated: raise ValidationError( 'May not assign None to repeated field %s' % self.name) else: message_instance._Message__tags.pop(self.number, None) else: if self.repeated: value = FieldList(self, value) else: value = self.validate(value) message_instance._Message__tags[self.number] = value def __get__(self, message_instance, message_class): if message_instance is None: return self result = message_instance._Message__tags.get(self.number) if result is None: return self.default return result def validate_element(self, value): """Validate single element of field. This is different from validate in that it is used on individual values of repeated fields. Args: value: Value to validate. Returns: The value casted in the expected type. Raises: ValidationError if value is not expected type. """ if not isinstance(value, self.type): # Authorize int values as float. if isinstance(value, six.integer_types) and self.type == float: return float(value) if value is None: if self.required: raise ValidationError('Required field is missing') else: try: name = self.name except AttributeError: raise ValidationError('Expected type %s for %s, ' 'found %s (type %s)' % (self.type, self.__class__.__name__, value, type(value))) else: raise ValidationError( 'Expected type %s for field %s, found %s (type %s)' % (self.type, name, value, type(value))) return value def __validate(self, value, validate_element): """Internal validation function. Validate an internal value using a function to validate individual elements. Args: value: Value to validate. validate_element: Function to use to validate individual elements. Raises: ValidationError if value is not expected type. """ if not self.repeated: return validate_element(value) else: # Must be a list or tuple, may not be a string. if isinstance(value, (list, tuple)): result = [] for element in value: if element is None: try: name = self.name except AttributeError: raise ValidationError( 'Repeated values for %s ' 'may not be None' % self.__class__.__name__) else: raise ValidationError( 'Repeated values for field %s ' 'may not be None' % name) result.append(validate_element(element)) return result elif value is not None: try: name = self.name except AttributeError: raise ValidationError('%s is repeated. Found: %s' % ( self.__class__.__name__, value)) else: raise ValidationError( 'Field %s is repeated. Found: %s' % (name, value)) return value def validate(self, value): """Validate value assigned to field. Args: value: Value to validate. Returns: the value in casted in the correct type. Raises: ValidationError if value is not expected type. """ return self.__validate(value, self.validate_element) def validate_default_element(self, value): """Validate value as assigned to field default field. Some fields may allow for delayed resolution of default types necessary in the case of circular definition references. In this case, the default value might be a place holder that is resolved when needed after all the message classes are defined. Args: value: Default value to validate. Returns: the value in casted in the correct type. Raises: ValidationError if value is not expected type. """ return self.validate_element(value) def validate_default(self, value): """Validate default value assigned to field. Args: value: Value to validate. Returns: the value in casted in the correct type. Raises: ValidationError if value is not expected type. """ return self.__validate(value, self.validate_default_element) def message_definition(self): """Get Message definition that contains this Field definition. Returns: Containing Message definition for Field. Will return None if for some reason Field is defined outside of a Message class. """ try: return self._message_definition() except AttributeError: return None @property def default(self): """Get default value for field.""" return self.__default @classmethod def lookup_field_type_by_variant(cls, variant): return cls.__variant_to_type[variant] class IntegerField(Field): """Field definition for integer values.""" VARIANTS = frozenset([ Variant.INT32, Variant.INT64, Variant.UINT32, Variant.UINT64, Variant.SINT32, Variant.SINT64, ]) DEFAULT_VARIANT = Variant.INT64 type = six.integer_types class FloatField(Field): """Field definition for float values.""" VARIANTS = frozenset([ Variant.FLOAT, Variant.DOUBLE, ]) DEFAULT_VARIANT = Variant.DOUBLE type = float class BooleanField(Field): """Field definition for boolean values.""" VARIANTS = frozenset([Variant.BOOL]) DEFAULT_VARIANT = Variant.BOOL type = bool class BytesField(Field): """Field definition for byte string values.""" VARIANTS = frozenset([Variant.BYTES]) DEFAULT_VARIANT = Variant.BYTES type = bytes class StringField(Field): """Field definition for unicode string values.""" VARIANTS = frozenset([Variant.STRING]) DEFAULT_VARIANT = Variant.STRING type = six.text_type def validate_element(self, value): """Validate StringField allowing for str and unicode. Raises: ValidationError if a str value is not UTF-8. """ # If value is str is it considered valid. Satisfies "required=True". if isinstance(value, bytes): try: six.text_type(value, 'UTF-8') except UnicodeDecodeError as err: try: _ = self.name except AttributeError: validation_error = ValidationError( 'Field encountered non-UTF-8 string %r: %s' % (value, err)) else: validation_error = ValidationError( 'Field %s encountered non-UTF-8 string %r: %s' % ( self.name, value, err)) validation_error.field_name = self.name raise validation_error else: return super(StringField, self).validate_element(value) return value class MessageField(Field): """Field definition for sub-message values. Message fields contain instance of other messages. Instances stored on messages stored on message fields are considered to be owned by the containing message instance and should not be shared between owning instances. Message fields must be defined to reference a single type of message. Normally message field are defined by passing the referenced message class in to the constructor. It is possible to define a message field for a type that does not yet exist by passing the name of the message in to the constructor instead of a message class. Resolution of the actual type of the message is deferred until it is needed, for example, during message verification. Names provided to the constructor must refer to a class within the same python module as the class that is using it. Names refer to messages relative to the containing messages scope. For example, the two fields of OuterMessage refer to the same message type: class Outer(Message): inner_relative = MessageField('Inner', 1) inner_absolute = MessageField('Outer.Inner', 2) class Inner(Message): ... When resolving an actual type, MessageField will traverse the entire scope of nested messages to match a message name. This makes it easy for siblings to reference siblings: class Outer(Message): class Inner(Message): sibling = MessageField('Sibling', 1) class Sibling(Message): ... """ VARIANTS = frozenset([Variant.MESSAGE]) DEFAULT_VARIANT = Variant.MESSAGE @util.positional(3) def __init__(self, message_type, number, required=False, repeated=False, variant=None): """Constructor. Args: message_type: Message type for field. Must be subclass of Message. number: Number of field. Must be unique per message class. required: Whether or not field is required. Mutually exclusive to 'repeated'. repeated: Whether or not field is rep