diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index aabf85d5113..40e0158b49a 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -1006,3 +1006,20 @@ updated as expected: 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 60dd94d7857..0690e70f625 100644 --- a/Doc/whatsnew/3.4.rst +++ b/Doc/whatsnew/3.4.rst @@ -264,11 +264,15 @@ New :func:`functools.singledispatch` decorator: see the :pep:`443`. 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 371bb355ba2..5feef8f9caa 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2109,3 +2109,64 @@ class Signature: 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 be55fe42761..bcb7d8a98b5 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -9,10 +9,11 @@ import collections 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 @@ class TestUnwrap(unittest.TestCase): __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 @@ def test_main(): 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 2fd27a43301..456307a25cf 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,9 @@ Core and Builtins 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.