Skip to content

Commit 5235467

Browse files
authored
gh-149010: Improve reliability of inspect CLI (#149357)
* Handle non-source modules more gracefully (and consistently) * Improve handling of frozen modules (which may or may not have source) * Avoid reporting misleading info when looking up objects via aliases * Refactor CLI implementation to improve testability * Add several more test cases Closes #149010
1 parent c3972f2 commit 5235467

4 files changed

Lines changed: 334 additions & 38 deletions

File tree

Doc/library/inspect.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1833,8 +1833,15 @@ from the command line.
18331833

18341834
By default, accepts the name of a module and prints the source of that
18351835
module. A class or function within the module can be printed instead by
1836-
appended a colon and the qualified name of the target object.
1836+
appending a colon and the qualified name of the target object.
18371837

18381838
.. option:: --details
18391839

18401840
Print information about the specified object rather than the source code
1841+
1842+
.. versionchanged:: next
1843+
1844+
The ``--details`` option now supports basic introspection for modules
1845+
without available source code and indicates when modules are frozen.
1846+
It also indicates when the given target reference is not the canonical
1847+
name of the referenced object.

Lib/inspect.py

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,6 +3351,95 @@ class BufferFlags(enum.IntFlag):
33513351
WRITE = 0x200
33523352

33533353

3354+
def _get_details_for_cli(module, nominal_target, resolved_target):
3355+
# Determine if the given module name is an alias for another module,
3356+
# or if it is reexporting a name that is actually defined elsewhere
3357+
resolved_module = getmodule(resolved_target)
3358+
if resolved_module is not None and resolved_module is not module:
3359+
# Referenced target indicates it was defined somewhere else,
3360+
# so report the details of that module rather than the lookup module
3361+
module = resolved_module
3362+
reported_module_name = module.__name__
3363+
# Ensure the reported source file reflects the actual defining location
3364+
try:
3365+
source_file = getsourcefile(resolved_target)
3366+
except Exception:
3367+
try:
3368+
source_file = getsourcefile(module)
3369+
except Exception:
3370+
source_file = None
3371+
# Determine if the nominal target location is its defining location
3372+
if resolved_target is module:
3373+
reported_target = reported_module_name
3374+
else:
3375+
reported_qualname = getattr(resolved_target, "__qualname__", None)
3376+
if not reported_qualname:
3377+
reported_qualname = nominal_target.partition(":")[2]
3378+
reported_target = f"{reported_module_name}:{reported_qualname}"
3379+
# Special case for looking up functions in frozen modules
3380+
if source_file == f"<frozen {reported_module_name}>":
3381+
source_file = module.__file__
3382+
# Populate the actual details to be reported
3383+
details = {
3384+
"target": reported_target,
3385+
"origin": module.__spec__.origin,
3386+
"cached": module.__spec__.cached,
3387+
"source": source_file,
3388+
}
3389+
if reported_target != nominal_target:
3390+
details["alias"] = nominal_target
3391+
error = None
3392+
if not source_file:
3393+
if module.__name__ in sys.builtin_module_names:
3394+
error = "No source code available for builtin module"
3395+
else:
3396+
error = "No source code available for defining module"
3397+
if resolved_target is module:
3398+
details["loader"] = repr(module.__spec__.loader)
3399+
if hasattr(module, '__path__'):
3400+
details["submodule_paths"] = str(module.__path__)
3401+
elif source_file:
3402+
try:
3403+
__, lineno = findsource(resolved_target)
3404+
except Exception:
3405+
error = "Failed to retrieve source code for given target"
3406+
else:
3407+
details["lineno"] = lineno
3408+
if error:
3409+
details["error"] = error
3410+
return details
3411+
3412+
def _render_details_for_cli(details):
3413+
resolved_target = details["target"]
3414+
alias = details.get("alias")
3415+
if alias:
3416+
rendered_target = f'{resolved_target} (looked up as "{alias}")'
3417+
else:
3418+
rendered_target = resolved_target
3419+
lines = [
3420+
f'Target: {rendered_target}',
3421+
f'Origin: {details["origin"]}',
3422+
f'Source: {details["source"]}',
3423+
f'Cached: {details["cached"]}',
3424+
]
3425+
loader = details.get("loader")
3426+
if loader:
3427+
lines.append(f'Loader: {loader}')
3428+
submodule_paths = details.get("submodule_paths")
3429+
if submodule_paths:
3430+
lines.append(f'Submodule search paths: {submodule_paths}')
3431+
else:
3432+
error = details.get("error")
3433+
if error:
3434+
# The error is only informational when retrieving object details
3435+
lines.append(error)
3436+
else:
3437+
lines.append(f'Line: {details["lineno"]}')
3438+
3439+
lines.append("")
3440+
return "\n".join(lines)
3441+
3442+
33543443
def _main():
33553444
""" Logic for inspecting an object given at command line """
33563445
import argparse
@@ -3367,6 +3456,8 @@ def _main():
33673456

33683457
args = parser.parse_args()
33693458

3459+
# We don't use `pkgutil.resolve_name` here because we want to obtain
3460+
# references to both the module *and* the fully resolved target object
33703461
target = args.object
33713462
mod_name, has_attrs, attrs = target.partition(":")
33723463
try:
@@ -3384,29 +3475,16 @@ def _main():
33843475
for part in parts:
33853476
obj = getattr(obj, part)
33863477

3387-
if module.__name__ in sys.builtin_module_names:
3388-
print("Can't get info for builtin modules.", file=sys.stderr)
3389-
sys.exit(1)
3390-
3478+
details = _get_details_for_cli(module, target, obj)
33913479
if args.details:
3392-
print(f'Target: {target}')
3393-
print(f'Origin: {getsourcefile(module)}')
3394-
print(f'Cached: {module.__spec__.cached}')
3395-
if obj is module:
3396-
print(f'Loader: {module.__loader__!r}')
3397-
if hasattr(module, '__path__'):
3398-
print(f'Submodule search path: {module.__path__}')
3399-
else:
3400-
try:
3401-
__, lineno = findsource(obj)
3402-
except Exception:
3403-
pass
3404-
else:
3405-
print(f'Line: {lineno}')
3406-
3407-
print()
3480+
print(_render_details_for_cli(details))
34083481
else:
3409-
print(getsource(obj))
3482+
# Attempt to render target source details
3483+
error = details.get("error")
3484+
if error:
3485+
sys.exit(error)
3486+
else:
3487+
print(getsource(obj))
34103488

34113489

34123490
if __name__ == "__main__":

0 commit comments

Comments
 (0)