Пример #1
0
 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()
     )
Пример #2
0
 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())
Пример #3
0
 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())
Пример #4
0
 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())
Пример #5
0
 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())
Пример #6
0
    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())
Пример #7
0
 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"
Пример #14
0
 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
Пример #16
0
 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())
Пример #17
0
 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()
     )
Пример #18
0
 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())
Пример #19
0
 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)