def build_method(self) -> Callable: fn_key_type, _ = _get_mapping_key_and_value_annotations(self.attr_spec) return ( MethodBuilder( f"without_{self.attr_spec.item_name}", functools.partial(self.without_mapping_item, self.attr_spec), ) .with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with an item removed from `{self.attr_spec.name}`." ) .with_arg( "_key", desc="The key of the item to remove.", annotation=fn_key_type ) .with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ) .with_arg( "_if", desc="This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ) .with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ) .build() )
def build_method(self) -> Callable: fn_item_type = _get_set_item_type(self.attr_spec) return (MethodBuilder( self.name, functools.partial(self.with_set_item, self.attr_spec), ).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with an item added to `{self.attr_spec.name}`." ).with_arg( "_item", desc= f"A new `{type_label(self.attr_spec.item_type)}` instance for {self.attr_spec.name}.", default=MISSING if self.attr_spec.item_spec_type else Parameter.empty, annotation=fn_item_type, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_spec_attrs_for( self.attr_spec.item_spec_type, desc_template= f"An optional new value for `{self.attr_spec.item_name}.{{}}`.", ).with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ).build())
def build_method(self) -> Callable: fn_index_type, fn_item_type = _get_sequence_index_and_item_annotations( self.attr_spec) return (MethodBuilder( self.name, functools.partial(self.without_sequence_item, self.attr_spec), ).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with an item removed from `{self.attr_spec.name}`." ).with_arg( "_value_or_index", desc="The value to remove, or (if `by_index=True`) its index.", annotation=Union[fn_item_type, fn_index_type], ).with_arg( "_by_index", desc="If True, value_or_index is the index of the item to remove.", default=MISSING, kind="keyword_only", annotation=bool, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ).build())
def build_method(self) -> Callable: attr_spec_type = self.attr_spec.spec_type or_its_attributes = " or its attributes" if attr_spec_type else "" return (MethodBuilder( self.name, functools.partial(self.with_attr, self.attr_spec) ).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with `{self.attr_spec.name}`{or_its_attributes} mutated." ).with_arg( "_new_value", desc=f"The new value for `{self.attr_spec.name}`.", default=MISSING, annotation=self.attr_spec.type, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_spec_attrs_for( self.attr_spec.type, desc_template= f"An optional new value for {self.attr_spec.name}.{{}}.", ).with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ).build())
def build_method(self) -> Callable: return (MethodBuilder(self.name, self.transform).with_preamble( f"Return a transformed `{self.spec_cls.__name__}` instance." ).with_arg( "_transform", desc= f"A function that takes the current `{type_label(self.spec_cls)}` instance, and returns the new value.", default=MISSING, annotation=Callable, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_spec_attrs_for( self.spec_cls, desc_template= f"An optional transformer for {type_label(self.spec_cls)}.{{}}.", ).with_returns( f"The output of `_transform(self)` or a reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=Any, ).build())
def build_method(self) -> Callable: spec_class_key = self.spec_cls.__spec_class__.key key_default = inspect.Parameter.empty if spec_class_key: spec_class_key_spec = ( self.spec_cls.__spec_class__.attrs.get(spec_class_key) or Attr()) # If the key has a default, don't require it to be set during # construction. key_default = (MISSING if spec_class_key_spec.has_default else inspect.Parameter.empty) return (MethodBuilder( "__init__", functools.partial(self.init, self.spec_cls) ).with_preamble( f"Initialise this `{self.spec_cls.__name__}` instance." ).with_arg( spec_class_key, desc=f"The value to use for the `{spec_class_key}` key attribute.", default=key_default, annotation=self.spec_cls.__spec_class__.annotations.get( spec_class_key), only_if=spec_class_key, ).with_spec_attrs_for( self.spec_cls, desc_template=f"Initial value for `{self.spec_cls.__name__}.{{}}`.", ).build())
def build_method(self) -> Callable: return (MethodBuilder(self.name, self.update).with_preamble( f"Return `_new_value`, or an `{self.spec_cls.__name__}` instance identical to this one except with nominated attributes mutated." ).with_arg( "_new_value", desc="A complete replacement for this instance.", default=MISSING, annotation=self.spec_cls, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_spec_attrs_for( self.spec_cls, desc_template= f"An optional new value for {type_label(self.spec_cls)}.{{}}.", ).with_returns( f"`_new_value` or a reference to the mutated `{type_label(self.spec_cls)}` instance.", annotation=Any, ).build())
def test__method_signature_to_implementation_call(self): assert (MethodBuilder._method_signature_to_implementation_call( Signature([ Parameter("a", kind=Parameter.POSITIONAL_ONLY), Parameter("b", kind=Parameter.POSITIONAL_OR_KEYWORD), Parameter("c", kind=Parameter.VAR_POSITIONAL), Parameter("d", kind=Parameter.KEYWORD_ONLY, default=None), Parameter("e", kind=Parameter.VAR_KEYWORD), ])) == "a, b=b, *c, d=d, **e")
def test_with_spec_attrs_for(self): from spec_classes import spec_class @spec_class(init_overflow_attr="overflow") class Spec: a: int b: float m = MethodBuilder("basic_wrapper", None).with_spec_attrs_for(Spec) assert len(m.method_args) == 2 assert len(m.method_args_virtual) == 3 assert {arg.name for arg in m.method_args_virtual} == {"a", "b", "overflow"} m = MethodBuilder("basic_wrapper", None).with_spec_attrs_for(Spec, only_if=False) assert len(m.method_args_virtual) == 0
def test_with_args(self): m = MethodBuilder("basic_wrapper", None) m.with_args({"a": "A value."}) m.with_args(["b"]) assert len(m.method_args) == 3 assert m.method_args[0].name == "self" assert m.method_args[1].name == "a" assert m.method_args[2].name == "b" m.with_args({"c": "Another value."}, only_if=False) assert len(m.method_args) == 3 with pytest.raises( RuntimeError, match=re.escape( "Method already has some incoming arguments: {'a'}"), ): m.with_args({"a": "A value."})
def test__method_signature_to_definition_str(self): assert (MethodBuilder._method_signature_to_definition_str( Signature([ Parameter("a", kind=Parameter.POSITIONAL_ONLY), Parameter("b", kind=Parameter.POSITIONAL_OR_KEYWORD), Parameter("c", kind=Parameter.VAR_POSITIONAL), Parameter("d", kind=Parameter.KEYWORD_ONLY, default=None), Parameter("e", kind=Parameter.VAR_KEYWORD), ])) == ('(a, b, *c, d=DEFAULTS["d"], **e)', { "d": None }))
def test_with_notes(self): m = MethodBuilder("basic_wrapper", None) m.with_notes("A note", "Another note") assert m.doc_notes == ["A note", "Another note"] m.with_notes("Anecdote", only_if=False) assert m.doc_notes == ["A note", "Another note"]
def test_with_preamble(self): m = MethodBuilder("basic_wrapper", None) m.with_preamble("A value") assert m.doc_preamble == "A value" m.with_preamble("A different value", only_if=False) assert m.doc_preamble == "A value"
def build_method(self) -> Callable: fn_index_type, fn_item_type = _get_sequence_index_and_item_annotations( self.attr_spec) return (MethodBuilder( self.name, functools.partial(self.with_sequence_item, self.attr_spec), ).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with an item added to or updated in `{self.attr_spec.name}`." ).with_arg( "_item", desc= f"A new `{type_label(self.attr_spec.item_type)}` instance for {self.attr_spec.name}.", default=MISSING, annotation=fn_item_type, ).with_arg( "_index", desc= "Index for which to insert or replace, depending on `insert`; if not provided, append.", default=MISSING, kind="keyword_only", annotation=fn_index_type, ).with_arg( "_insert", desc= f"Insert item before {self.attr_spec.name}[index], otherwise replace this index.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_spec_attrs_for( self.attr_spec.item_spec_type, desc_template= f"An optional new value for `{self.attr_spec.item_name}.{{}}`.", ).with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ).build())
def test_with_returns(self): m = MethodBuilder("basic_wrapper", None) m.with_returns("A value", annotation=str) assert m.doc_returns == "A value" assert m.method_return_type is str m.with_returns("A different value", annotation=int, only_if=False) assert m.doc_returns == "A value" assert m.method_return_type is str
def build_method(self) -> Callable: fn_index_type, fn_item_type = _get_sequence_index_and_item_annotations( self.attr_spec) return (MethodBuilder( self.name, functools.partial(self.transform_sequence_item, self.attr_spec), ).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with an item transformed in `{self.attr_spec.name}`." ).with_arg( "_value_or_index", desc="The value to transform, or (if `by_index=True`) its index.", annotation=Union[fn_item_type, fn_index_type], ).with_arg( "_transform", desc= "A function that takes the old item as input, and returns the new item.", default=MISSING if self.attr_spec.item_spec_type else Parameter.empty, annotation=Callable[[fn_item_type], fn_item_type], ).with_arg( "_by_index", desc= "If True, value_or_index is the index of the item to transform.", kind="keyword_only", default=MISSING, annotation=bool, ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_spec_attrs_for( self.attr_spec.item_spec_type, desc_template= f"An optional transformer for `{self.attr_spec.item_name}.{{}}`.", ).with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ).build())
def build_method(self) -> Callable: fn_key_type, fn_value_type = _get_mapping_key_and_value_annotations( self.attr_spec ) return ( MethodBuilder( self.name, functools.partial(self.update_mapping_item, self.attr_spec), ) .with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with an item updated in `{self.attr_spec.name}`." ) .with_arg( "_key", desc="The key for the item to be updated.", annotation=fn_key_type, ) .with_arg( "_new_item", desc="A new value for the nominated key.", default=MISSING if self.attr_spec.item_spec_type else Parameter.empty, annotation=Callable[[fn_value_type], fn_value_type], ) .with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ) .with_arg( "_if", desc="This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ) .with_spec_attrs_for( self.attr_spec.item_spec_type, desc_template=f"Optional new value for `{self.attr_spec.item_name}.{{}}`.", ) .with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ) .build() )
def build_method(self) -> Callable: return (MethodBuilder(self.name, self.reset).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical but with all attributes reset to their default values." ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_returns( f"A reference to the mutated `{type_label(self.spec_cls)}` instance.", annotation=self.spec_cls, ).build())
def build_method(self) -> Callable: return (MethodBuilder( self.name, functools.partial(self.reset_attr, self.attr_spec) ).with_preamble( f"Return a `{self.spec_cls.__name__}` instance identical to this one except with `{self.attr_spec.name}` reset to its default value." ).with_arg( "_inplace", desc="Whether to perform change without first copying.", default=False, kind="keyword_only", annotation=bool, ).with_arg( "_if", desc= "This action is only taken when `_if` is `True`. If it is `False`, this is a no-op.", default=True, kind="keyword_only", annotation=bool, ).with_returns( f"A reference to the mutated `{self.spec_cls.__name__}` instance.", annotation=self.spec_cls, ).build())
def test__check_signature_compatible_with_implementation(self): assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x: None), inspect.signature(lambda x: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x: None), inspect.signature(lambda *x: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda *x: None), inspect.signature(lambda *x: None), ) assert not MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda *x: None), inspect.signature(lambda x: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x, y: None), inspect.signature(lambda x, y: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda y, x: None), inspect.signature(lambda x, y: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x: None), inspect.signature(lambda x, y=False: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x, y: None), inspect.signature(lambda x, y=False: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x, y: None), inspect.signature(lambda x, **y: None), ) assert not MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x, **y: None), inspect.signature(lambda x, y: None), ) assert MethodBuilder._check_signature_compatible_with_implementation( inspect.signature(lambda x, **y: None), inspect.signature(lambda x, **y: None), )
def test_with_arg(self): m = MethodBuilder("basic_wrapper", None) m.with_arg("a", desc="A value.") assert len(m.method_args) == 2 assert m.method_args[0].name == "self" assert m.method_args[1].name == "a" m.with_arg("c", desc="Another value.", kind="keyword_only", virtual=True) assert len(m.method_args) == 3 assert m.method_args[2].name == "kwargs" assert m.method_args[2].kind == Parameter.VAR_KEYWORD assert len(m.method_args_virtual) == 1 assert m.method_args_virtual[0].kind == Parameter.KEYWORD_ONLY m.with_arg("d", desc="Another value.", only_if=False) assert len(m.method_args) == 3 with pytest.raises( RuntimeError, match=re.escape( "Virtual arguments can only be `KEYWORD_ONLY` or `VAR_KEYWORD`, not `POSITIONAL_OR_KEYWORD`." ), ): m.with_arg("d", desc="Another value.", virtual=True) with pytest.raises( RuntimeError, match=re.escape( "Arguments of kind `POSITIONAL_OR_KEYWORD` cannot be added after `VAR_KEYWORD` arguments." ), ): m.with_arg("d", desc="Another value.") with pytest.raises( RuntimeError, match=re.escape( "Arguments of kind `POSITIONAL_ONLY` cannot be added after `VAR_KEYWORD` arguments." ), ): m.with_arg("d", desc="Another value.", kind="positional_only") m.with_arg("e", desc="Collect all.", kind="var_keyword", virtual=True) with pytest.raises( RuntimeError, match=re.escape( "Virtual arguments of kind `KEYWORD_ONLY` cannot be added after `VAR_KEYWORD` arguments." ), ): m.with_arg("f", desc="Another value.", virtual=True, kind="keyword_only")
def test_build(self): def basic_implementation(self, a, b, **kwargs): return (a, b, kwargs) m = MethodBuilder("basic_wrapper", basic_implementation) m.with_preamble("Hello World!") m.with_returns("All the arguments.", annotation=Tuple) m.with_notes("This method doesn't do a whole lot.") m.with_arg("a", desc="A value.", annotation=int) with pytest.raises( RuntimeError, match=re.escape( "Proposed method signature `basic_wrapper(self, a: int)` is not compatible with implementation signature `implementation(self, a, b, **kwargs)`" ), ): m.build() m.with_arg("b", desc="Another value.", annotation=str) m.with_arg( "c", desc="Yet another value.", annotation=float, virtual=True, kind="keyword_only", ) c = m.build() assert (c.__doc__ == textwrap.dedent(""" Hello World! Args: a: A value. b: Another value. c: Yet another value. Returns: All the arguments. Notes: This method doesn't do a whole lot. """).strip()) assert str( inspect.signature(c)) == "(self, a: int, b: str, *, c: float)" assert c(None, 1, "two", c=3.0) == (1, "two", {"c": 3.0}) with pytest.raises( TypeError, match=re.escape( "basic_wrapper() got unexpected keyword arguments: {'d'}." ), ): c(None, 1, "two", c=3.0, d=None)