def __getattr__(self, name: str) -> _PathCapture: # We only allow diving into sub-objects if we are a dataclass. if not self._is_dataclass: raise TypeError( f"Field path cannot include attribute '{name}' " f'under parent {self._cls}; parent types must be dataclasses.') prep = PrepSession(explicit=False).prep_dataclass(self._cls, recursion_level=0) assert prep is not None try: anntype = prep.annotations[name] except KeyError as exc: raise AttributeError(f'{type(self)} has no {name} field.') from exc anntype, ioattrs = _parse_annotated(anntype) storagename = (name if (ioattrs is None or ioattrs.storagename is None) else ioattrs.storagename) origin = _get_origin(anntype) return _PathCapture(origin, pathparts=self._pathparts + [storagename])
def _value_from_input(self, cls: type, fieldpath: str, anntype: Any, value: Any, ioattrs: Optional[IOAttrs]) -> Any: """Convert an assigned value to what a dataclass field expects.""" # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches origin = _get_origin(anntype) if origin is typing.Any: if not _is_valid_for_codec(value, self._codec): raise TypeError(f'Invalid value type for \'{fieldpath}\';' f' \'Any\' typed values must contain only' f' types directly supported by the specified' f' codec ({self._codec.name}); found' f' \'{type(value).__name__}\' which is not.') return value if origin is typing.Union or origin is types.UnionType: # Currently, the only unions we support are None/Value # (translated from Optional), which we verified on prep. # So let's treat this as a simple optional case. if value is None: return None childanntypes_l = [ c for c in typing.get_args(anntype) if c is not type(None) ] # noqa (pycodestyle complains about *is* with type) assert len(childanntypes_l) == 1 return self._value_from_input(cls, fieldpath, childanntypes_l[0], value, ioattrs) # Everything below this point assumes the annotation type resolves # to a concrete type. (This should have been verified at prep time). assert isinstance(origin, type) if origin in SIMPLE_TYPES: if type(value) is not origin: # Special case: if they want to coerce ints to floats, do so. if (self._coerce_to_float and origin is float and type(value) is int): return float(value) _raise_type_error(fieldpath, type(value), (origin, )) return value if origin in {list, set}: return self._sequence_from_input(cls, fieldpath, anntype, value, origin, ioattrs) if origin is tuple: return self._tuple_from_input(cls, fieldpath, anntype, value, ioattrs) if origin is dict: return self._dict_from_input(cls, fieldpath, anntype, value, ioattrs) if dataclasses.is_dataclass(origin): return self._dataclass_from_input(origin, fieldpath, value) if issubclass(origin, Enum): return enum_by_value(origin, value) if issubclass(origin, datetime.datetime): return self._datetime_from_input(cls, fieldpath, value, ioattrs) if origin is bytes: return self._bytes_from_input(origin, fieldpath, value) raise TypeError( f"Field '{fieldpath}' of type '{anntype}' is unsupported here.")
def prep_type(self, cls: type, attrname: str, anntype: Any, recursion_level: int) -> None: """Run prep on a dataclass.""" # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches # If we run into classes containing themselves, we may have # to do something smarter to handle it. if recursion_level > MAX_RECURSION: raise RuntimeError('Max recursion exceeded.') origin = _get_origin(anntype) if origin is typing.Union: self.prep_union(cls, attrname, anntype, recursion_level=recursion_level + 1) return if anntype is typing.Any: return # Everything below this point assumes the annotation type resolves # to a concrete type. if not isinstance(origin, type): raise TypeError( f'Unsupported type found for \'{attrname}\' on {cls}:' f' {anntype}') if origin in SIMPLE_TYPES: return # For sets and lists, check out their single contained type (if any). if origin in (list, set): childtypes = typing.get_args(anntype) if len(childtypes) == 0: # This is equivalent to Any; nothing else needs checking. return if len(childtypes) > 1: raise TypeError( f'Unrecognized typing arg count {len(childtypes)}' f" for {anntype} attr '{attrname}' on {cls}") self.prep_type(cls, attrname, childtypes[0], recursion_level=recursion_level + 1) return if origin is dict: childtypes = typing.get_args(anntype) assert len(childtypes) in (0, 2) # For key types we support Any, str, int, # and Enums with uniform str/int values. if not childtypes or childtypes[0] is typing.Any: # 'Any' needs no further checks (just checked per-instance). pass elif childtypes[0] in (str, int): # str and int are all good as keys. pass elif issubclass(childtypes[0], Enum): # Allow our usual str or int enum types as keys. self.prep_enum(childtypes[0]) else: raise TypeError( f'Dict key type {childtypes[0]} for \'{attrname}\'' f' on {cls.__name__} is not supported by dataclassio.') # For value types we support any of our normal types. if not childtypes or _get_origin(childtypes[1]) is typing.Any: # 'Any' needs no further checks (just checked per-instance). pass else: self.prep_type(cls, attrname, childtypes[1], recursion_level=recursion_level + 1) return # For Tuples, simply check individual member types. # (and, for now, explicitly disallow zero member types or usage # of ellipsis) if origin is tuple: childtypes = typing.get_args(anntype) if not childtypes: raise TypeError( f'Tuple at \'{attrname}\'' f' has no type args; dataclassio requires type args.') if childtypes[-1] is ...: raise TypeError(f'Found ellipsis as part of type for' f' \'{attrname}\' on {cls.__name__};' f' these are not' f' supported by dataclassio.') for childtype in childtypes: self.prep_type(cls, attrname, childtype, recursion_level=recursion_level + 1) return if issubclass(origin, Enum): self.prep_enum(origin) return # We allow datetime objects (and google's extended subclass of them # used in firestore, which is why we don't look for exact type here). if issubclass(origin, datetime.datetime): return if dataclasses.is_dataclass(origin): self.prep_dataclass(origin, recursion_level=recursion_level + 1) return if origin is bytes: return raise TypeError(f"Attr '{attrname}' on {cls.__name__} contains" f" type '{anntype}'" f' which is not supported by dataclassio.')
def _process_value(self, cls: type, fieldpath: str, anntype: Any, value: Any, ioattrs: Optional[IOAttrs]) -> Any: # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches # pylint: disable=too-many-statements origin = _get_origin(anntype) if origin is typing.Any: if not _is_valid_for_codec(value, self._codec): raise TypeError( f'Invalid value type for \'{fieldpath}\';' f" 'Any' typed values must contain types directly" f' supported by the specified codec ({self._codec.name});' f' found \'{type(value).__name__}\' which is not.') return value if self._create else None if origin is typing.Union or origin is types.UnionType: # Currently, the only unions we support are None/Value # (translated from Optional), which we verified on prep. # So let's treat this as a simple optional case. if value is None: return None childanntypes_l = [ c for c in typing.get_args(anntype) if c is not type(None) ] # noqa (pycodestyle complains about *is* with type) assert len(childanntypes_l) == 1 return self._process_value(cls, fieldpath, childanntypes_l[0], value, ioattrs) # Everything below this point assumes the annotation type resolves # to a concrete type. (This should have been verified at prep time). assert isinstance(origin, type) # For simple flat types, look for exact matches: if origin in SIMPLE_TYPES: if type(value) is not origin: # Special case: if they want to coerce ints to floats, do so. if (self._coerce_to_float and origin is float and type(value) is int): return float(value) if self._create else None _raise_type_error(fieldpath, type(value), (origin, )) return value if self._create else None if origin is tuple: if not isinstance(value, tuple): raise TypeError(f'Expected a tuple for {fieldpath};' f' found a {type(value)}') childanntypes = typing.get_args(anntype) # We should have verified this was non-zero at prep-time assert childanntypes if len(value) != len(childanntypes): raise TypeError(f'Tuple at {fieldpath} contains' f' {len(value)} values; type specifies' f' {len(childanntypes)}.') if self._create: return [ self._process_value(cls, fieldpath, childanntypes[i], x, ioattrs) for i, x in enumerate(value) ] for i, x in enumerate(value): self._process_value(cls, fieldpath, childanntypes[i], x, ioattrs) return None if origin is list: if not isinstance(value, list): raise TypeError(f'Expected a list for {fieldpath};' f' found a {type(value)}') childanntypes = typing.get_args(anntype) # 'Any' type children; make sure they are valid values for # the specified codec. if len(childanntypes) == 0 or childanntypes[0] is typing.Any: for i, child in enumerate(value): if not _is_valid_for_codec(child, self._codec): raise TypeError( f'Item {i} of {fieldpath} contains' f' data type(s) not supported by the specified' f' codec ({self._codec.name}).') # Hmm; should we do a copy here? return value if self._create else None # We contain elements of some specified type. assert len(childanntypes) == 1 if self._create: return [ self._process_value(cls, fieldpath, childanntypes[0], x, ioattrs) for x in value ] for x in value: self._process_value(cls, fieldpath, childanntypes[0], x, ioattrs) return None if origin is set: if not isinstance(value, set): raise TypeError(f'Expected a set for {fieldpath};' f' found a {type(value)}') childanntypes = typing.get_args(anntype) # 'Any' type children; make sure they are valid Any values. if len(childanntypes) == 0 or childanntypes[0] is typing.Any: for child in value: if not _is_valid_for_codec(child, self._codec): raise TypeError( f'Set at {fieldpath} contains' f' data type(s) not supported by the' f' specified codec ({self._codec.name}).') return list(value) if self._create else None # We contain elements of some specified type. assert len(childanntypes) == 1 if self._create: # Note: we output json-friendly values so this becomes # a list. return [ self._process_value(cls, fieldpath, childanntypes[0], x, ioattrs) for x in value ] for x in value: self._process_value(cls, fieldpath, childanntypes[0], x, ioattrs) return None if origin is dict: return self._process_dict(cls, fieldpath, anntype, value, ioattrs) if dataclasses.is_dataclass(origin): if not isinstance(value, origin): raise TypeError(f'Expected a {origin} for {fieldpath};' f' found a {type(value)}.') return self._process_dataclass(cls, value, fieldpath) if issubclass(origin, Enum): if not isinstance(value, origin): raise TypeError(f'Expected a {origin} for {fieldpath};' f' found a {type(value)}.') # At prep-time we verified that these enums had valid value # types, so we can blindly return it here. return value.value if self._create else None if issubclass(origin, datetime.datetime): if not isinstance(value, origin): raise TypeError(f'Expected a {origin} for {fieldpath};' f' found a {type(value)}.') check_utc(value) if ioattrs is not None: ioattrs.validate_datetime(value, fieldpath) if self._codec is Codec.FIRESTORE: return value assert self._codec is Codec.JSON return [ value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond ] if self._create else None if origin is bytes: return self._process_bytes(cls, fieldpath, value) raise TypeError( f"Field '{fieldpath}' of type '{anntype}' is unsupported here.")
def prep_type(self, cls: type, attrname: str, anntype: Any, ioattrs: Optional[IOAttrs], recursion_level: int) -> None: """Run prep on a dataclass.""" # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches # pylint: disable=too-many-statements if recursion_level > MAX_RECURSION: raise RuntimeError('Max recursion exceeded.') origin = _get_origin(anntype) if origin is typing.Union or origin is types.UnionType: self.prep_union(cls, attrname, anntype, recursion_level=recursion_level + 1) return if anntype is typing.Any: return # Everything below this point assumes the annotation type resolves # to a concrete type. if not isinstance(origin, type): raise TypeError( f'Unsupported type found for \'{attrname}\' on {cls}:' f' {anntype}') # If a soft_default value/factory was passed, we do some basic # type checking on the top-level value here. We also run full # recursive validation on values later during inputting, but this # should catch at least some errors early on, which can be # useful since soft_defaults are not static type checked. if ioattrs is not None: have_soft_default = False soft_default: Any = None if ioattrs.soft_default is not ioattrs.MISSING: have_soft_default = True soft_default = ioattrs.soft_default elif ioattrs.soft_default_factory is not ioattrs.MISSING: assert callable(ioattrs.soft_default_factory) have_soft_default = True soft_default = ioattrs.soft_default_factory() # Do a simple type check for the top level to catch basic # soft_default mismatches early; full check will happen at # input time. if have_soft_default: if not isinstance(soft_default, origin): raise TypeError( f'{cls} attr {attrname} has type {origin}' f' but soft_default value is type {type(soft_default)}' ) if origin in SIMPLE_TYPES: return # For sets and lists, check out their single contained type (if any). if origin in (list, set): childtypes = typing.get_args(anntype) if len(childtypes) == 0: # This is equivalent to Any; nothing else needs checking. return if len(childtypes) > 1: raise TypeError( f'Unrecognized typing arg count {len(childtypes)}' f" for {anntype} attr '{attrname}' on {cls}") self.prep_type(cls, attrname, childtypes[0], ioattrs=None, recursion_level=recursion_level + 1) return if origin is dict: childtypes = typing.get_args(anntype) assert len(childtypes) in (0, 2) # For key types we support Any, str, int, # and Enums with uniform str/int values. if not childtypes or childtypes[0] is typing.Any: # 'Any' needs no further checks (just checked per-instance). pass elif childtypes[0] in (str, int): # str and int are all good as keys. pass elif issubclass(childtypes[0], Enum): # Allow our usual str or int enum types as keys. self.prep_enum(childtypes[0]) else: raise TypeError( f'Dict key type {childtypes[0]} for \'{attrname}\'' f' on {cls.__name__} is not supported by dataclassio.') # For value types we support any of our normal types. if not childtypes or _get_origin(childtypes[1]) is typing.Any: # 'Any' needs no further checks (just checked per-instance). pass else: self.prep_type(cls, attrname, childtypes[1], ioattrs=None, recursion_level=recursion_level + 1) return # For Tuples, simply check individual member types. # (and, for now, explicitly disallow zero member types or usage # of ellipsis) if origin is tuple: childtypes = typing.get_args(anntype) if not childtypes: raise TypeError( f'Tuple at \'{attrname}\'' f' has no type args; dataclassio requires type args.') if childtypes[-1] is ...: raise TypeError(f'Found ellipsis as part of type for' f' \'{attrname}\' on {cls.__name__};' f' these are not' f' supported by dataclassio.') for childtype in childtypes: self.prep_type(cls, attrname, childtype, ioattrs=None, recursion_level=recursion_level + 1) return if issubclass(origin, Enum): self.prep_enum(origin) return # We allow datetime objects (and google's extended subclass of them # used in firestore, which is why we don't look for exact type here). if issubclass(origin, datetime.datetime): return if dataclasses.is_dataclass(origin): self.prep_dataclass(origin, recursion_level=recursion_level + 1) return if origin is bytes: return raise TypeError(f"Attr '{attrname}' on {cls.__name__} contains" f" type '{anntype}'" f' which is not supported by dataclassio.')