def prepare_item(self, new_item: Any) -> Any: """ This method when an item in this collection is mutated in the `mutate_value` method. It: (1) Transforms the new item under the attribute-specific item-preparer. (2) Automatically casts stand-alone spec-class keys to a spec-class instance with that key. Args: new_item: The incoming item. Returns: The transformed item ready for further mutation/transformation. Note: This is called *after* we know which base value we are using but before we fallback to constructors and/or apply passed attributes and transforms. """ if self.attr_spec.prepare_item: new_item = self.attr_spec.prepare_item(self.instance, new_item) if ( # Convert to spec-class if key was provided. self.attr_spec.item_spec_key_type and new_item is not MISSING and not check_type(new_item, self.attr_spec.item_type) and check_type(new_item, self.attr_spec.item_spec_key_type) ): new_item = self.attr_spec.item_spec_type(new_item) return new_item
def _validate_item(self, item: ItemType) -> Tuple[ItemType, KeyType]: key = self.key(item) if hasattr(self._type, "__args__"): item_type, key_type = self._type.__args__ if not check_type(item, item_type): raise TypeError( f"Invalid item type. Got: `{repr(item)}`; Expected instance of: `{type_label(item_type)}`." ) if not check_type(key, key_type): raise TypeError( f"Invalid key type. Got: `{repr(key)}`; Expected instance of: `{type_label(key_type)}`." ) return item, key
def _extractor( # pylint: disable=arguments-differ self, value_or_index, raise_if_missing=False, by_index=MISSING, ) -> IndexedItem: if self.collection is MISSING or value_or_index is MISSING: return None, MISSING if by_index is MISSING: by_index = not check_type(value_or_index, self.attr_spec.item_type) if by_index: try: return (value_or_index, self.collection[value_or_index]) except IndexError: if raise_if_missing: raise IndexError( f"Index `{repr(value_or_index)}` not found in collection `{self.attr_spec.qualified_name}`." ) from None return (value_or_index, MISSING) try: value_index = self.collection.index(value_or_index) except ValueError: value_index = None if raise_if_missing and value_index is None: raise ValueError( f"Item `{repr(value_or_index)}` not found in collection `{self.attr_spec.qualified_name}`." ) return (value_index, value_or_index)
def add_items(self, items: Iterable): if not check_type(items, Iterable): raise TypeError( f"Incoming collection for `{self.attr_spec.qualified_name}` is not iterable." ) for item in items: self.add_item(item) return self
def _inserter(self, index, item, replace=True): # pylint: disable=arguments-differ if not check_type(item, self.attr_spec.item_type): raise ValueError( f"Attempted to add an invalid item `{repr(item)}` to `{self.attr_spec.qualified_name}`. Expected item of type `{type_label(self.attr_spec.item_type)}`." ) if index and replace: self.collection.discard(index) self.collection.add(item)
def add_items(self, items: Mapping): if not check_type(items, Mapping): raise TypeError( f"Incoming collection for `{self.attr_spec.qualified_name}` is not a mapping." ) for k, v in items.items(): self.add_item(k, v) return self
def _inserter(self, index, item, insert=False): # pylint: disable=arguments-differ if not check_type(item, self.attr_spec.item_type): raise ValueError( f"Attempted to add an invalid item `{repr(item)}` to `{self.attr_spec.qualified_name}`. Expected item of type `{type_label(self.attr_spec.item_type)}`." ) if index is None: self.collection.append(item) elif insert: self.collection.insert(index, item) else: self.collection[index] = item
def prepare(self): if self.collection in (MISSING, None): self.collection = self._create_collection() if not check_type(self.collection, self.attr_spec.type): items = self.collection self.collection = self._create_collection() self.add_items(items) return self if self.collection and self.prepare_item: self._prepare_items() return self
def __spec_class_check_type__(cls, instance: Any, type_: Type) -> bool: """ Returns `True` if this instance is a valid instance of `type_` and this class. """ if not isinstance(instance, cls): return False if hasattr(type_, "__args__"): item_type, key_type = type_.__args__ for ( key, value, ) in instance._dict.items(): # pylint: disable=protected-access if not check_type(key, key_type): return False if not check_type(value, item_type): return False return True
def validator(obj): if not check_type(obj, numeric_type): return False if ge is not None and obj < ge: return False if gt is not None and obj <= gt: return False if le is not None and obj > le: return False if lt is not None and obj >= lt: return False return True
def __get__(self, instance, owner=None): # If lookup occuring on owner class. if instance is None: return self # If value exists in cache or has been overridden if (self.overridable or self.cache) and self.attr_name in instance.__dict__: return instance.__dict__[self.attr_name] # If there is no assigned getter if self.fget is None: raise AttributeError( f"Property override for `{self._qualified_name}` does not have a getter method." ) # Get value from getter try: value = self.fget(instance) except AttributeError as e: if self.allow_attribute_error: raise raise NestedAttributeError(e) from e # If attribute is annotated with a `spec_class` type, apply any # transforms using `_prepare_foo()` methods, and then check that the # attribute type is correct. spec_metadata = getattr(instance, "__spec_class__", None) if spec_metadata and self.attr_name in spec_metadata.attrs: attr_spec = spec_metadata.attrs[self.attr_name] value = prepare_attr_value(attr_spec, instance, value) if not check_type(value, attr_spec.type): raise ValueError( f"Property override for `{owner.__name__ if owner else ''}.{self.attr_name or ''}` returned an invalid type [got `{repr(value)}`; expecting `{type_label(attr_spec.type)}`]." ) # Store value in cache is cache is enabled if self.cache: instance.__dict__[self.attr_name] = value return value
def test_type_checking(self): assert check_type("string", str) assert check_type([], list) assert check_type([], List) assert not check_type("a", List) assert check_type(["a", "b"], List[str]) assert not check_type([1, 2], List[str]) assert check_type({}, Dict) assert not check_type("a", Dict) assert check_type({"a": 1, "b": 2}, Dict[str, int]) assert not check_type({"a": "1", "b": "2"}, Dict[str, int]) assert not check_type({1: "a", 2: "b"}, Dict[str, int]) assert check_type(set(), Set) assert not check_type("a", Set) assert check_type({"a", "b"}, Set[str]) assert not check_type({1, 2}, Set[str]) assert check_type(lambda x: x, Callable) assert check_type([1, "a"], List[Union[str, int]])
def _inserter(self, index, item): if not check_type(item, self.attr_spec.item_type): raise ValueError( f"Attempted to add an invalid item `{repr(item)}` to `{self.attr_spec.qualified_name}`. Expected item of type `{type_label(self.attr_spec.item_type)}`." ) self.collection[index] = item