From efd3f091d7fd5105888d5fd786e86e5eeacd5b45 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:51:21 +0000 Subject: [PATCH] methodize: keep self before position-only params (#118) func_to_method_func prepended the instance param with Sig addition, which sorts by kind and so put `self` after the `/` when the method kept a position-only param. The instance param is now position-only (and first) whenever the method signature has position-only params. Co-Authored-By: Claude Opus 5.5 --- i2/tests/test_partials_and_methodize.py | 23 ++++++++++++++++++++++- i2/wrapper.py | 22 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/i2/tests/test_partials_and_methodize.py b/i2/tests/test_partials_and_methodize.py index 867d1133..9f3b50b7 100644 --- a/i2/tests/test_partials_and_methodize.py +++ b/i2/tests/test_partials_and_methodize.py @@ -1,5 +1,5 @@ """Tests for partialx(_rm_partialize), curried, methodize(default_from) and the -docstring parser fixes of i2mint/i2#111, #112, #113 and #116.""" +docstring parser fixes of i2mint/i2#111, #112, #113, #116 and #118.""" import asyncio import inspect @@ -82,6 +82,27 @@ class K: assert (k.bar(3), k.bar(3, 10), k.bar(3, z=0)) == (7, 31, 1) +def test_methodize_keeps_self_before_position_only_params(): + """i2mint/i2#118: ``self`` used to land after the ``/``.""" + + def foo(a, /, b, *, c=3): + return (a, b, c) + + @dataclass + class K: + a: int = 1 + b: int = 7 + bound = methodize(foo, bind="b") + defaulted = methodize(foo, default_from="b") + + assert str(inspect.signature(K.bound)) == "(self, a, /, *, c=3)" + assert str(inspect.signature(K.defaulted)) == "(self, a, /, b=self.b, *, c=3)" + k = K() + assert k.bound(5) == (5, 7, 3) + assert (k.defaulted(5), k.defaulted(5, 0)) == ((5, 7, 3), (5, 0, 3)) + assert methodize(foo, bind="a")(k, 2) == (1, 2, 3) + + def test_methodize_default_from_rejects_variadic_and_unknown_params(): def foo(x, *args, **kwargs): return x diff --git a/i2/wrapper.py b/i2/wrapper.py index 508f8b0d..198bd834 100644 --- a/i2/wrapper.py +++ b/i2/wrapper.py @@ -2042,7 +2042,7 @@ def func_to_method_func( method_sig = _with_instance_attr_defaults( method_sig, default_from, instance_arg_name=instance_arg_name ) - method_sig = instance_arg_name + method_sig + method_sig = _prepend_instance_param(method_sig, instance_arg_name) # make the ingress function that will map method_sig's interface to sig's. ingress = Ingress( inner_sig=sig, @@ -2062,6 +2062,26 @@ def func_to_method_func( return method_func +def _prepend_instance_param(sig: Sig, instance_arg_name: str = "self") -> Sig: + """Put the instance param first in ``sig``. + + If ``sig`` has position-only params, the instance param is position-only too, so + it stays before the ``/`` (i2mint/i2#118). + + >>> def f(a, /, b): ... + >>> _prepend_instance_param(Sig(f)) + + >>> _prepend_instance_param(Sig(lambda b, *, c: 0)) + + """ + if PO not in sig.kinds.values(): + return instance_arg_name + sig + return Sig( + [Parameter(instance_arg_name, PO), *sig.params], + return_annotation=sig.return_annotation, + ) + + def _with_instance_attr_defaults( sig: Sig, default_from: dict, *, instance_arg_name: str = "self" ) -> Sig: