@@ -112,14 +112,16 @@ def example_method(self, x, y=1) -> None:
112112 ex_inst = Example ()
113113
114114 assert varnames (example ) == (("a" ,), ("b" ,))
115+ # Unbound: self is stripped because it's in _IMPLICIT_NAMES and qualname is dotted.
115116 assert varnames (Example .example_method ) == (("x" ,), ("y" ,))
117+ # Bound: self is already consumed.
116118 assert varnames (ex_inst .example_method ) == (("x" ,), ("y" ,))
117119
118120
119121def test_varnames_bound_method_from_module_function () -> None :
120122 """A module-level function assigned to a class attribute becomes a bound
121123 method when accessed on an instance, but its __qualname__ has no dot.
122- varnames must still strip ``self`` ."""
124+ varnames must still strip the first parameter ."""
123125
124126 def standalone (self , x ) -> None :
125127 pass # pragma: no cover
@@ -130,6 +132,109 @@ class MyClass:
130132 assert varnames (MyClass ().method ) == (("x" ,), ())
131133
132134
135+ def test_varnames_unconventional_first_param_name () -> None :
136+ """Bound methods strip unconditionally, but unbound methods with
137+ non-standard first parameter names preserve all arguments."""
138+
139+ class MyClass :
140+ def method (this , x ) -> None :
141+ pass # pragma: no cover
142+
143+ # Bound: stripped regardless of name.
144+ assert varnames (MyClass ().method ) == (("x" ,), ())
145+ # Unbound with dotted qualname but non-implicit name: NOT stripped.
146+ assert varnames (MyClass .method ) == (("this" , "x" ), ())
147+
148+
149+ def test_varnames_classmethod () -> None :
150+ class MyClass :
151+ @classmethod
152+ def cm (cls , x , y = 1 ) -> None :
153+ pass # pragma: no cover
154+
155+ # Classmethods are always bound (even from the class).
156+ assert varnames (MyClass .cm ) == (("x" ,), ("y" ,))
157+ assert varnames (MyClass ().cm ) == (("x" ,), ("y" ,))
158+
159+
160+ def test_varnames_staticmethod () -> None :
161+ class MyClass :
162+ @staticmethod
163+ def sm (x , y = 1 ) -> None :
164+ pass # pragma: no cover
165+
166+ # Staticmethods have no implicit first arg.
167+ assert varnames (MyClass .sm ) == (("x" ,), ("y" ,))
168+ assert varnames (MyClass ().sm ) == (("x" ,), ("y" ,))
169+
170+
171+ def test_varnames_hookspec_without_self () -> None :
172+ """Hookspec-style class methods without self/cls preserve all parameters.
173+
174+ This is the convention used by projects like pytest-timeout where hookspec
175+ classes define methods without ``self`` since they serve as pure signatures.
176+ By default varnames does not warn; the warning is emitted when
177+ ``legacy_noself=True`` is passed (as HookSpec.__init__ does).
178+ """
179+
180+ class MySpecs :
181+ def my_hook (item , extra ) -> None :
182+ pass # pragma: no cover
183+
184+ # Accessed as unbound: first arg is not an implicit name, keep it.
185+ assert varnames (MySpecs .my_hook ) == (("item" , "extra" ), ())
186+ # Accessed as bound (via instance): first arg is stripped.
187+ assert varnames (MySpecs ().my_hook ) == (("extra" ,), ())
188+
189+
190+ def test_varnames_legacy_noself_warns () -> None :
191+ """With ``legacy_noself=True``, varnames warns when it encounters a
192+ class method whose first parameter is not an implicit name."""
193+ import warnings
194+
195+ class MySpecs :
196+ def my_hook (item , extra ) -> None :
197+ pass # pragma: no cover
198+
199+ with warnings .catch_warnings (record = True ) as w :
200+ warnings .simplefilter ("always" )
201+ result = varnames (MySpecs .my_hook , legacy_noself = True )
202+ assert result == (("item" , "extra" ), ())
203+ assert len (w ) == 1
204+ assert issubclass (w [0 ].category , FutureWarning )
205+ assert "'item' is not 'self'" in str (w [0 ].message )
206+
207+
208+ def test_varnames_legacy_noself_no_warn_with_self () -> None :
209+ """With ``legacy_noself=True``, no warning when the method has ``self``."""
210+ import warnings
211+
212+ class MySpecs :
213+ def my_hook (self , item , extra ) -> None :
214+ pass # pragma: no cover
215+
216+ with warnings .catch_warnings (record = True ) as w :
217+ warnings .simplefilter ("always" )
218+ result = varnames (MySpecs .my_hook , legacy_noself = True )
219+ assert result == (("item" , "extra" ), ())
220+ assert len (w ) == 0
221+
222+
223+ def test_varnames_no_legacy_noself_no_warn () -> None :
224+ """Without ``legacy_noself``, no warning even for class methods without self."""
225+ import warnings
226+
227+ class MySpecs :
228+ def my_hook (item , extra ) -> None :
229+ pass # pragma: no cover
230+
231+ with warnings .catch_warnings (record = True ) as w :
232+ warnings .simplefilter ("always" )
233+ result = varnames (MySpecs .my_hook )
234+ assert result == (("item" , "extra" ), ())
235+ assert len (w ) == 0
236+
237+
133238def test_varnames_unresolvable_annotation () -> None :
134239 """Test that varnames works with annotations that cannot be resolved.
135240
0 commit comments