Close #18626: add a basic CLI for the inspect module
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index aabf85d..40e0158 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -1006,3 +1006,20 @@
return an empty dictionary.
.. versionadded:: 3.3
+
+
+Command Line Interface
+----------------------
+
+The :mod:`inspect` module also provides a basic introspection capability
+from the command line.
+
+.. program:: inspect
+
+By default, accepts the name of a module and prints the source of that
+module. A class or function within the module can be printed instead by
+appended a colon and the qualified name of the target object.
+
+.. cmdoption:: --details
+
+ Print information about the specified object rather than the source code
diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst
index 60dd94d..0690e70 100644
--- a/Doc/whatsnew/3.4.rst
+++ b/Doc/whatsnew/3.4.rst
@@ -264,11 +264,15 @@
inspect
-------
+
+The inspect module now offers a basic command line interface to quickly
+display source code and other information for modules, classes and
+functions.
+
:func:`~inspect.unwrap` makes it easy to unravel wrapper function chains
created by :func:`functools.wraps` (and any other API that sets the
``__wrapped__`` attribute on a wrapper function).
-
mmap
----
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 371bb35..5feef8f 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -2109,3 +2109,64 @@
rendered += ' -> {}'.format(anno)
return rendered
+
+def _main():
+ """ Logic for inspecting an object given at command line """
+ import argparse
+ import importlib
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'object',
+ help="The object to be analysed. "
+ "It supports the 'module:qualname' syntax")
+ parser.add_argument(
+ '-d', '--details', action='store_true',
+ help='Display info about the module rather than its source code')
+
+ args = parser.parse_args()
+
+ target = args.object
+ mod_name, has_attrs, attrs = target.partition(":")
+ try:
+ obj = module = importlib.import_module(mod_name)
+ except Exception as exc:
+ msg = "Failed to import {} ({}: {})".format(mod_name,
+ type(exc).__name__,
+ exc)
+ print(msg, file=sys.stderr)
+ exit(2)
+
+ if has_attrs:
+ parts = attrs.split(".")
+ obj = module
+ for part in parts:
+ obj = getattr(obj, part)
+
+ if module.__name__ in sys.builtin_module_names:
+ print("Can't get info for builtin modules.", file=sys.stderr)
+ exit(1)
+
+ if args.details:
+ print('Target: {}'.format(target))
+ print('Origin: {}'.format(getsourcefile(module)))
+ print('Cached: {}'.format(module.__cached__))
+ if obj is module:
+ print('Loader: {}'.format(repr(module.__loader__)))
+ if hasattr(module, '__path__'):
+ print('Submodule search path: {}'.format(module.__path__))
+ else:
+ try:
+ __, lineno = findsource(obj)
+ except Exception:
+ pass
+ else:
+ print('Line: {}'.format(lineno))
+
+ print('\n')
+ else:
+ print(getsource(obj))
+
+
+if __name__ == "__main__":
+ _main()
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index be55fe4..bcb7d8a 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -9,10 +9,11 @@
import os
import shutil
import functools
+import importlib
from os.path import normcase
from test.support import run_unittest, TESTFN, DirsOnSysPath
-
+from test.script_helper import assert_python_ok, assert_python_failure
from test import inspect_fodder as mod
from test import inspect_fodder2 as mod2
@@ -2372,6 +2373,47 @@
__wrapped__ = func
self.assertIsNone(inspect.unwrap(C()))
+class TestMain(unittest.TestCase):
+ def test_only_source(self):
+ module = importlib.import_module('unittest')
+ rc, out, err = assert_python_ok('-m', 'inspect',
+ 'unittest')
+ lines = out.decode().splitlines()
+ # ignore the final newline
+ self.assertEqual(lines[:-1], inspect.getsource(module).splitlines())
+ self.assertEqual(err, b'')
+
+ def test_qualname_source(self):
+ module = importlib.import_module('concurrent.futures')
+ member = getattr(module, 'ThreadPoolExecutor')
+ rc, out, err = assert_python_ok('-m', 'inspect',
+ 'concurrent.futures:ThreadPoolExecutor')
+ lines = out.decode().splitlines()
+ # ignore the final newline
+ self.assertEqual(lines[:-1],
+ inspect.getsource(member).splitlines())
+ self.assertEqual(err, b'')
+
+ def test_builtins(self):
+ module = importlib.import_module('unittest')
+ _, out, err = assert_python_failure('-m', 'inspect',
+ 'sys')
+ lines = err.decode().splitlines()
+ self.assertEqual(lines, ["Can't get info for builtin modules."])
+
+ def test_details(self):
+ module = importlib.import_module('unittest')
+ rc, out, err = assert_python_ok('-m', 'inspect',
+ 'unittest', '--details')
+ output = out.decode()
+ # Just a quick sanity check on the output
+ self.assertIn(module.__name__, output)
+ self.assertIn(module.__file__, output)
+ self.assertIn(module.__cached__, output)
+ self.assertEqual(err, b'')
+
+
+
def test_main():
run_unittest(
@@ -2380,7 +2422,7 @@
TestGetcallargsFunctions, TestGetcallargsMethods,
TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState,
TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject,
- TestBoundArguments, TestGetClosureVars, TestUnwrap
+ TestBoundArguments, TestGetClosureVars, TestUnwrap, TestMain
)
if __name__ == "__main__":
diff --git a/Misc/NEWS b/Misc/NEWS
index 2fd27a4..456307a 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -12,6 +12,9 @@
Library
-------
+- Issue #18626: the inspect module now offers a basic command line
+ introspection interface (Initial patch by Claudiu Popa)
+
- Issue #3015: Fixed tkinter with wantobject=False. Any Tcl command call
returned empty string.