def parse(self, filepath, filecontent): parse_globals = self._globals python = filecontent symbols = {} exec(python, parse_globals, symbols) objects = [] for name, obj in symbols.items(): if isinstance(obj, type): # Allow type imports continue if not Serializable.is_serializable(obj): raise ParseError( f"Found a non-serializable top-level object: {obj}") attributes = obj._asdict() if "name" in attributes: attributes = attributes.copy() redundant_name = attributes.pop("name", None) if redundant_name and redundant_name != name: raise ParseError( "The object named {!r} is assigned to a mismatching name {!r}" .format(redundant_name, name)) obj_type = type(obj) named_obj = obj_type(name=name, **attributes) objects.append(named_obj) return objects
def parse(cls, filepath: str, filecontent: bytes, parser: Parser) -> "AddressMap": """Parses a source for addressable Serializable objects. No matter the parser used, the parsed and mapped addressable objects are all 'thin'; ie: any objects they point to in other namespaces or even in the same namespace but from a separate source are left as unresolved pointers. :param filepath: The path to the byte source containing serialized objects. :param filecontent: The content of byte source containing serialized objects to be parsed. :param parser: The parser cls to use. """ try: objects = parser.parse(filepath, filecontent) except Exception as e: raise MappingError(f"Failed to parse {filepath}:\n{e!r}") objects_by_name: Dict[str, ThinAddressableObject] = {} for obj in objects: if not Serializable.is_serializable(obj): raise UnaddressableObjectError("Parsed a non-serializable object: {!r}".format(obj)) attributes = obj._asdict() name = attributes.get("name") if not name: raise UnaddressableObjectError("Parsed a non-addressable object: {!r}".format(obj)) if name in objects_by_name: raise DuplicateNameError( "An object already exists at {!r} with name {!r}: {!r}. Cannot " "map {!r}".format(filepath, name, objects_by_name[name], obj) ) objects_by_name[name] = obj return cls(filepath, dict(sorted(objects_by_name.items())))
def _checked_value(self, instance, value): # We allow five forms of value: # 0. None. # 1. An opaque (to us) address pointing to a value that can be resolved by external # means. # 2. A `Resolvable` value that we can lazily resolve and type-check in `__get__`. # 3. A concrete instance that meets our type constraint. # 4. A dict when our type constraint has exactly one Serializable subject type - we convert the # dict into an instance of that type. if value is None: return None if isinstance(value, (str, Address, Resolvable)): return value # Support untyped dicts that we deserialize on-demand here into the required type. # This feature allows for more brevity in the JSON form (local type inference) and an alternate # construction style in the python forms. type_constraint = self._get_type_constraint(instance) if (isinstance(value, dict) and len(type_constraint.types) == 1 and Serializable.is_serializable_type(type_constraint.types[0])): if not value: # TODO(John Sirois): Is this the right thing to do? Or should an empty serializable_type # be constructed? return None # {} -> None. else: serializable_type = type_constraint.types[0] return serializable_type(**value) try: return type_constraint.validate_satisfied_by(value) except TypeConstraintError as e: raise AddressableTypeValidationError( "The value for the {} attribute of {} was invalid".format( self._name, instance), e)
def registered(type_name, object_type, name=None, **kwargs): if name: obj = object_type(name=name, type_alias=type_name, **kwargs) if Serializable.is_serializable(obj): objects.append(obj) return obj else: return object_type(type_alias=type_name, **kwargs)
def _object_encoder(obj, inline): if isinstance(obj, Resolvable): return obj.resolve() if inline else obj.address if isinstance(obj, Address): return obj.reference() if not Serializable.is_serializable(obj): raise ParseError( "Can only encode Serializable objects in JSON, given {!r} of type {}" .format(obj, type(obj).__name__)) encoded = obj._asdict() if "type_alias" not in encoded: encoded = encoded.copy() encoded[ "type_alias"] = f"{inspect.getmodule(obj).__name__}.{type(obj).__name__}" return {k: v for k, v in encoded.items() if v}
def __set__(self, instance, value): if not Serializable.is_serializable(instance): raise NotSerializableError( "The addressable descriptor {} can only be applied to methods or " "properties of Serializable objects, applied to method {} of " "type {}".format(type(self).__name__, self._name, type(instance).__name__) ) instance_dict = instance._asdict() if self._name in instance_dict: raise MutationError( "Attribute {} of {} has already been set to {}, rejecting attempt to " "re-set with {}".format(self._name, instance, instance_dict[self._name], value) ) value = self._checked_value(instance, value) self._register(instance, self) # We mutate the instance dict, which is only OK if used in the conventional idiom of setting # the value via this data descriptor in the instance's constructor. instance_dict[self._name] = value
def __init__(self, parse_context, type_alias, object_type): self._parse_context = parse_context self._type_alias = type_alias self._object_type = object_type self._serializable = Serializable.is_serializable_type(self._object_type)