From 8d0a45ac3ea1fa1d847d1ebcaf7edb701a90d399 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Tue, 15 Sep 2026 23:24:13 +0530 Subject: [PATCH] Preserve defaults for positional-only parameters Collect default values for both positional-only and positional-or-keyword parameters. Add six regression tests covering built-ins, bound methods, mixed signatures, omitted defaults, overrides, and required arguments. Prepared with Codex assistance. Local validation on Python 3.12.14: 279 tests passed; pylint on changed modules passed with 10.00/10. --- fire/core_test.py | 12 ++++++++++++ fire/inspectutils.py | 7 ++++--- fire/inspectutils_test.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/fire/core_test.py b/fire/core_test.py index f48d6e2d..d5ccf236 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -24,6 +24,18 @@ class CoreTest(testutils.BaseTestCase): + def testBuiltinPositionalOnlyDefault(self): + with self.assertOutputMatches(stdout=' hi', stderr=None): + self.assertEqual(core.Fire('hi'.rjust, command=['5']), ' hi') + + def testBuiltinPositionalOnlyDefaultOverride(self): + with self.assertOutputMatches(stdout='___hi', stderr=None): + self.assertEqual(core.Fire('hi'.rjust, command=['5', '_']), '___hi') + + def testBuiltinPositionalOnlyRequiredArgument(self): + with self.assertRaisesFireExit(2, 'required argument: width'): + core.Fire('hi'.rjust, command=[]) + def testOneLineResult(self): self.assertEqual(core._OneLineResult(1), '1') # pylint: disable=protected-access self.assertEqual(core._OneLineResult('hello'), 'hello') # pylint: disable=protected-access diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 17508e30..3b4181fd 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -128,9 +128,10 @@ def Py3GetFullArgSpec(fn): name = param.name # pylint: disable=protected-access - if kind is inspect._POSITIONAL_ONLY: # type: ignore - args.append(name) - elif kind is inspect._POSITIONAL_OR_KEYWORD: # type: ignore + if kind in ( + inspect._POSITIONAL_ONLY, + inspect._POSITIONAL_OR_KEYWORD, + ): # type: ignore args.append(name) if param.default is not param.empty: defaults += (param.default,) diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py index 47de7e72..11e60b71 100644 --- a/fire/inspectutils_test.py +++ b/fire/inspectutils_test.py @@ -14,6 +14,7 @@ """Tests for the inspectutils module.""" +import inspect import os from fire import inspectutils @@ -52,6 +53,35 @@ def testGetFullArgSpecFromBuiltin(self): self.assertEqual(spec.kwonlydefaults, {}) self.assertEqual(spec.annotations, {}) + def testGetFullArgSpecPositionalOnlyDefault(self): + spec = inspectutils.GetFullArgSpec(dict.get) + self.assertEqual(spec.args, ['self', 'key', 'default']) + self.assertEqual(spec.defaults, (None,)) + + def testGetFullArgSpecBoundPositionalOnlyDefault(self): + spec = inspectutils.GetFullArgSpec({}.get) + self.assertEqual(spec.args, ['key', 'default']) + self.assertEqual(spec.defaults, (None,)) + + def testGetFullArgSpecMixedPositionalDefaults(self): + def function(*args, **kwargs): + del args, kwargs + + # Use a signature object so this test also parses on Python 3.7. + function.__signature__ = inspect.Signature([ + inspect.Parameter('required', inspect.Parameter.POSITIONAL_ONLY), + inspect.Parameter('first', inspect.Parameter.POSITIONAL_ONLY, + default=1), + inspect.Parameter('second', inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=2), + inspect.Parameter('third', inspect.Parameter.KEYWORD_ONLY, default=3), + ]) + spec = inspectutils.GetFullArgSpec(function) + self.assertEqual(spec.args, ['required', 'first', 'second']) + self.assertEqual(spec.defaults, (1, 2)) + self.assertEqual(spec.kwonlyargs, ['third']) + self.assertEqual(spec.kwonlydefaults, {'third': 3}) + def testGetFullArgSpecFromSlotWrapper(self): spec = inspectutils.GetFullArgSpec(tc.NoDefaults) self.assertEqual(spec.args, [])