Skip to content

Commit 9173825

Browse files
committed
Fix inspection of built-in functions with >= 3.11
The built-in functions no longer have their signature in the docstring, but now inspect.signature can produce results. But as we have no source for built-in functions, we cannot replace the default values. Hence, we handle built-in functions in an extra step. This commit also changes the handling of default values slightly. They are now always put into a _Repr.
1 parent 3b89278 commit 9173825

File tree

2 files changed

+60
-36
lines changed

2 files changed

+60
-36
lines changed

bpython/inspection.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ class ArgSpec:
5757
args: List[str]
5858
varargs: Optional[str]
5959
varkwargs: Optional[str]
60-
defaults: Optional[List[Any]]
60+
defaults: Optional[List[_Repr]]
6161
kwonly: List[str]
62-
kwonly_defaults: Optional[Dict[str, Any]]
62+
kwonly_defaults: Optional[Dict[str, _Repr]]
6363
annotations: Optional[Dict[str, Any]]
6464

6565

@@ -169,31 +169,51 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]:
169169
return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3}
170170

171171

172-
def _fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec:
172+
def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec:
173173
"""Functions taking default arguments that are references to other objects
174-
whose str() is too big will cause breakage, so we swap out the object
175-
itself with the name it was referenced with in the source by parsing the
176-
source itself !"""
177-
if argspec.defaults is None:
174+
will cause breakage, so we swap out the object itself with the name it was
175+
referenced with in the source by parsing the source itself!"""
176+
177+
if argspec.defaults is None and argspec.kwonly_defaults is None:
178178
# No keyword args, no need to do anything
179179
return argspec
180-
values = list(argspec.defaults)
181-
if not values:
182-
return argspec
183-
keys = argspec.args[-len(values) :]
180+
184181
try:
185-
src = inspect.getsourcelines(f)
182+
src, _ = inspect.getsourcelines(f)
186183
except (OSError, IndexError):
187184
# IndexError is raised in inspect.findsource(), can happen in
188185
# some situations. See issue #94.
189186
return argspec
190-
kwparsed = parsekeywordpairs("".join(src[0]))
187+
except TypeError:
188+
# No source code is available (for Python >= 3.11)
189+
#
190+
# If the function is a builtin, we replace the default values.
191+
# Otherwise, let's bail out.
192+
if not inspect.isbuiltin(f):
193+
raise
194+
195+
if argspec.defaults is not None:
196+
argspec.defaults = [_Repr(str(value)) for value in argspec.defaults]
197+
if argspec.kwonly_defaults is not None:
198+
argspec.kwonly_defaults = {
199+
key: _Repr(str(value))
200+
for key, value in argspec.kwonly_defaults.items()
201+
}
202+
return argspec
191203

192-
for i, (key, value) in enumerate(zip(keys, values)):
193-
if len(repr(value)) != len(kwparsed[key]):
204+
kwparsed = parsekeywordpairs("".join(src))
205+
206+
if argspec.defaults is not None:
207+
values = list(argspec.defaults)
208+
keys = argspec.args[-len(values) :]
209+
for i, key in enumerate(keys):
194210
values[i] = _Repr(kwparsed[key])
195211

196-
argspec.defaults = values
212+
argspec.defaults = values
213+
if argspec.kwonly_defaults is not None:
214+
for key in argspec.kwonly_defaults.keys():
215+
argspec.kwonly_defaults[key] = _Repr(kwparsed[key])
216+
197217
return argspec
198218

199219

@@ -234,11 +254,11 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]:
234254
if varargs is not None:
235255
kwonly_args.append(arg)
236256
if default:
237-
kwonly_defaults[arg] = default
257+
kwonly_defaults[arg] = _Repr(default)
238258
else:
239259
args.append(arg)
240260
if default:
241-
defaults.append(default)
261+
defaults.append(_Repr(default))
242262

243263
return ArgSpec(
244264
args, varargs, varkwargs, defaults, kwonly_args, kwonly_defaults, None
@@ -267,7 +287,9 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]:
267287
return None
268288
try:
269289
argspec = _get_argspec_from_signature(f)
270-
fprops = FuncProps(func, _fixlongargs(f, argspec), is_bound_method)
290+
fprops = FuncProps(
291+
func, _fix_default_values(f, argspec), is_bound_method
292+
)
271293
except (TypeError, KeyError, ValueError):
272294
argspec_pydoc = _getpydocspec(f)
273295
if argspec_pydoc is None:

bpython/test/test_inspection.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from bpython.test.fodder import encoding_utf8
1212

1313
pypy = "PyPy" in sys.version
14+
_is_py311 = sys.version_info[:2] >= (3, 11)
1415

1516
try:
1617
import numpy
@@ -53,38 +54,32 @@ def test_parsekeywordpairs(self):
5354
def fails(spam=["-a", "-b"]):
5455
pass
5556

56-
default_arg_repr = "['-a', '-b']"
57-
self.assertEqual(
58-
str(["-a", "-b"]),
59-
default_arg_repr,
60-
"This test is broken (repr does not match), fix me.",
61-
)
62-
6357
argspec = inspection.getfuncprops("fails", fails)
58+
self.assertIsNotNone(argspec)
6459
defaults = argspec.argspec.defaults
65-
self.assertEqual(str(defaults[0]), default_arg_repr)
60+
self.assertEqual(str(defaults[0]), '["-a", "-b"]')
6661

6762
def test_pasekeywordpairs_string(self):
6863
def spam(eggs="foo, bar"):
6964
pass
7065

7166
defaults = inspection.getfuncprops("spam", spam).argspec.defaults
72-
self.assertEqual(repr(defaults[0]), "'foo, bar'")
67+
self.assertEqual(repr(defaults[0]), '"foo, bar"')
7368

7469
def test_parsekeywordpairs_multiple_keywords(self):
7570
def spam(eggs=23, foobar="yay"):
7671
pass
7772

7873
defaults = inspection.getfuncprops("spam", spam).argspec.defaults
7974
self.assertEqual(repr(defaults[0]), "23")
80-
self.assertEqual(repr(defaults[1]), "'yay'")
75+
self.assertEqual(repr(defaults[1]), '"yay"')
8176

8277
def test_pasekeywordpairs_annotation(self):
8378
def spam(eggs: str = "foo, bar"):
8479
pass
8580

8681
defaults = inspection.getfuncprops("spam", spam).argspec.defaults
87-
self.assertEqual(repr(defaults[0]), "'foo, bar'")
82+
self.assertEqual(repr(defaults[0]), '"foo, bar"')
8883

8984
def test_get_encoding_ascii(self):
9085
self.assertEqual(inspection.get_encoding(encoding_ascii), "ascii")
@@ -134,8 +129,15 @@ def test_getfuncprops_print(self):
134129
self.assertIn("file", props.argspec.kwonly)
135130
self.assertIn("flush", props.argspec.kwonly)
136131
self.assertIn("sep", props.argspec.kwonly)
137-
self.assertEqual(props.argspec.kwonly_defaults["file"], "sys.stdout")
138-
self.assertEqual(props.argspec.kwonly_defaults["flush"], "False")
132+
if _is_py311:
133+
self.assertEqual(
134+
repr(props.argspec.kwonly_defaults["file"]), "None"
135+
)
136+
else:
137+
self.assertEqual(
138+
repr(props.argspec.kwonly_defaults["file"]), "sys.stdout"
139+
)
140+
self.assertEqual(repr(props.argspec.kwonly_defaults["flush"]), "False")
139141

140142
@unittest.skipUnless(
141143
numpy is not None and numpy.__version__ >= "1.18",
@@ -173,12 +175,12 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]:
173175
props = inspection.getfuncprops("fun", fun)
174176
self.assertEqual(props.func, "fun")
175177
self.assertEqual(props.argspec.args, ["number", "lst"])
176-
self.assertEqual(props.argspec.defaults[0], [])
178+
self.assertEqual(repr(props.argspec.defaults[0]), "[]")
177179

178180
props = inspection.getfuncprops("fun_annotations", fun_annotations)
179181
self.assertEqual(props.func, "fun_annotations")
180182
self.assertEqual(props.argspec.args, ["number", "lst"])
181-
self.assertEqual(props.argspec.defaults[0], [])
183+
self.assertEqual(repr(props.argspec.defaults[0]), "[]")
182184

183185
def test_issue_966_class_method(self):
184186
class Issue966(Sequence):
@@ -215,7 +217,7 @@ def bmethod(cls, number, lst):
215217
)
216218
self.assertEqual(props.func, "cmethod")
217219
self.assertEqual(props.argspec.args, ["number", "lst"])
218-
self.assertEqual(props.argspec.defaults[0], [])
220+
self.assertEqual(repr(props.argspec.defaults[0]), "[]")
219221

220222
def test_issue_966_static_method(self):
221223
class Issue966(Sequence):
@@ -252,7 +254,7 @@ def bmethod(number, lst):
252254
)
253255
self.assertEqual(props.func, "cmethod")
254256
self.assertEqual(props.argspec.args, ["number", "lst"])
255-
self.assertEqual(props.argspec.defaults[0], [])
257+
self.assertEqual(repr(props.argspec.defaults[0]), "[]")
256258

257259

258260
class A:

0 commit comments

Comments
 (0)