diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 39caee4f89016b..654c1961d4ab55 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -750,10 +750,14 @@ Retrieving source code .. function:: getcomments(object) Return in a single string any lines of comments immediately preceding the - object's source code (for a class, function, or method), or at the top of the - Python source file (if the object is a module). If the object's source code - is unavailable, return ``None``. This could happen if the object has been - defined in C or the interactive shell. + object's source code (for a class, function, method, or type alias), or at the + top of the Python source file (if the object is a module). If the object's + source code is unavailable, return ``None``. This could happen if the object + has been defined in C or in an interactive shell that does not retain source + code. + + .. versionchanged:: next + Added support for type aliases created with the :keyword:`type` statement. .. function:: getfile(object) @@ -763,6 +767,9 @@ Retrieving source code This will fail with a :exc:`TypeError` if the object is a built-in module, class, or function. + .. versionchanged:: next + Added support for :class:`~typing.TypeAliasType` objects. + .. function:: getmodule(object) @@ -778,15 +785,18 @@ Retrieving source code This will fail with a :exc:`TypeError` if the object is a built-in module, class, or function. + .. versionchanged:: next + Added support for :class:`~typing.TypeAliasType` objects. + .. function:: getsourcelines(object) Return a list of source lines and starting line number for an object. The - argument may be a module, class, method, function, traceback, frame, or code - object. The source code is returned as a list of the lines corresponding to the - object and the line number indicates where in the original source file the first - line of code was found. An :exc:`OSError` is raised if the source code cannot - be retrieved. + argument may be a module, class, method, function, traceback, frame, code + object, or type alias. The source code is returned as a list of the lines + corresponding to the object and the line number indicates where in the + original source file the first line of code was found. An :exc:`OSError` is + raised if the source code cannot be retrieved. A :exc:`TypeError` is raised if the object is a built-in module, class, or function. @@ -794,13 +804,16 @@ Retrieving source code :exc:`OSError` is raised instead of :exc:`IOError`, now an alias of the former. + .. versionchanged:: next + Added support for type aliases created with the :keyword:`type` statement. + .. function:: getsource(object) Return the text of the source code for an object. The argument may be a module, - class, method, function, traceback, frame, or code object. The source code is - returned as a single string. An :exc:`OSError` is raised if the source code - cannot be retrieved. + class, method, function, traceback, frame, code object, or type alias. The + source code is returned as a single string. An :exc:`OSError` is raised if the + source code cannot be retrieved. A :exc:`TypeError` is raised if the object is a built-in module, class, or function. @@ -808,6 +821,9 @@ Retrieving source code :exc:`OSError` is raised instead of :exc:`IOError`, now an alias of the former. + .. versionchanged:: next + Added support for type aliases created with the :keyword:`type` statement. + .. function:: cleandoc(doc, *, dedent=True) diff --git a/Lib/inspect.py b/Lib/inspect.py index 3683f8c3fd5330..80eb9d0ca52d1d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -163,6 +163,7 @@ from keyword import iskeyword from operator import attrgetter from collections import namedtuple, OrderedDict +from _typing import TypeAliasType from _weakref import ref as make_weakref # Create constants for the compiler flags in Include/cpython/code.h @@ -872,8 +873,18 @@ def getfile(object): object = object.f_code if iscode(object): return object.co_filename - raise TypeError('module, class, method, function, traceback, frame, or ' - 'code object was expected, got {}'.format( + if isinstance(object, TypeAliasType): + evaluator = object.evaluate_value + if isfunction(evaluator): + return getfile(evaluator) + module = sys.modules.get(object.__module__) + if getattr(module, '__file__', None): + return module.__file__ + if object.__module__ == '__main__': + raise OSError('source code not available') + raise TypeError(f'module not available for {object!r}') + raise TypeError('module, class, method, function, traceback, frame, ' + 'code object, or type alias was expected, got {}'.format( type(object).__name__)) def getmodulename(path): @@ -984,9 +995,14 @@ def findsource(object): """Return the entire source file and starting line number for an object. The argument may be a module, class, method, function, traceback, frame, - or code object. The source code is returned as a list of all the lines - in the file and the line number indexes a line in that list. An OSError - is raised if the source code cannot be retrieved.""" + code object, or type alias. The source code is returned as a list of all + the lines in the file and the line number indexes a line in that list. + An OSError is raised if the source code cannot be retrieved.""" + + if isinstance(object, TypeAliasType): + evaluator = object.evaluate_value + if isfunction(evaluator): + object = evaluator file = getsourcefile(object) if file: @@ -1168,7 +1184,7 @@ def getsourcelines(object): """Return a list of source lines and starting line number for an object. The argument may be a module, class, method, function, traceback, frame, - or code object. The source code is returned as a list of the lines + code object, or type alias. The source code is returned as a list of the lines corresponding to the object and the line number indicates where in the original source file the first line of code was found. An OSError is raised if the source code cannot be retrieved.""" @@ -1189,7 +1205,7 @@ def getsource(object): """Return the text of the source code for an object. The argument may be a module, class, method, function, traceback, frame, - or code object. The source code is returned as a single string. An + code object, or type alias. The source code is returned as a single string. An OSError is raised if the source code cannot be retrieved.""" lines, lnum = getsourcelines(object) return ''.join(lines) diff --git a/Lib/test/test_inspect/inspect_fodder.py b/Lib/test/test_inspect/inspect_fodder.py index febd54c86fe1d1..469d2f9fdcb5d0 100644 --- a/Lib/test/test_inspect/inspect_fodder.py +++ b/Lib/test/test_inspect/inspect_fodder.py @@ -118,3 +118,6 @@ async def asyncf(self): # a closing parenthesis with the opening paren being in another line ( ); after_closing = lambda: 1 + +# What is their airspeed? +type Sparrow = African | European diff --git a/Lib/test/test_inspect/inspect_fodder2.py b/Lib/test/test_inspect/inspect_fodder2.py index 157e12167b5d27..e7d0c4ca225d58 100644 --- a/Lib/test/test_inspect/inspect_fodder2.py +++ b/Lib/test/test_inspect/inspect_fodder2.py @@ -400,4 +400,21 @@ def func394(): def func400(): return 401 +# A multiline generic alias. +type GenericAlias[ + T, +] = ( + list[T] + | tuple[T, ...] +) + +class TypeAliases: + # A nested alias. + type Nested = MissingName + +def make_type_alias(): + # A local alias. + type Local = MissingName + return Local + pass # end of file diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index a4381f2c023395..002795adc260ae 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -22,6 +22,7 @@ import subprocess import time import types +import typing import tempfile import textwrap import unicodedata @@ -812,6 +813,7 @@ def test_getcomments(self): self.assertEqual(inspect.getcomments(mod), '# line 1\n') self.assertEqual(inspect.getcomments(mod.StupidGit), '# line 20\n') self.assertEqual(inspect.getcomments(mod2.cls160), '# line 159\n') + self.assertEqual(inspect.getcomments(mod.Sparrow), '# What is their airspeed?\n') # If the object source file is not available, return None. co = compile('x=1', '_non_existing_filename.py', 'exec') self.assertIsNone(inspect.getcomments(co)) @@ -851,6 +853,7 @@ def test_getsource(self): self.assertSourceEqual(mod.StupidGit, 21, 51) self.assertSourceEqual(mod.lobbest, 75, 76) self.assertSourceEqual(mod.after_closing, 120, 120) + self.assertSourceEqual(mod.Sparrow, 123, 123) def test_getsourcefile(self): self.assertEqual(normcase(inspect.getsourcefile(mod.spam)), modfile) @@ -864,6 +867,31 @@ def test_getsourcefile(self): finally: del linecache.cache[co.co_filename] + def test_getsourcefile_type_alias(self): + self.assertEqual(normcase(inspect.getsourcefile(mod.Sparrow)), + normcase(mod.__file__)) + self.assertEqual(normcase(inspect.getfile(mod.Sparrow)), + normcase(mod.__file__)) + + WrongModule = typing.TypeAliasType("WrongModule", int) + WrongModule.__module__ = "types" + self.assertEqual(normcase(inspect.getsourcefile(WrongModule)), normcase(types.__file__)) + self.assertEqual(normcase(inspect.getfile(WrongModule)), normcase(types.__file__)) + + WrongModule.__module__ = "non-existing module" + with self.assertRaises(TypeError): + inspect.getsourcefile(WrongModule) + with self.assertRaises(TypeError): + inspect.getfile(WrongModule) + + def test_getsource_on_generated_type_alias(self): + Alias = typing.TypeAliasType("Alias", int) + self.assertEqual(inspect.getsourcefile(Alias), __file__) + self.assertEqual(inspect.getfile(Alias), __file__) + self.assertRaises(OSError, inspect.getsource, Alias) + self.assertRaises(OSError, inspect.getsourcelines, Alias) + self.assertIsNone(inspect.getcomments(Alias)) + def test_getsource_empty_file(self): with temp_cwd() as cwd: with open('empty_file.py', 'w'): @@ -1135,6 +1163,20 @@ def test_class_async_method(self): class TestBuggyCases(GetSourceBase): fodderModule = mod2 + def test_type_aliases(self): + cases = ( + (mod2.GenericAlias, 404, 409, '# A multiline generic alias.\n'), + (mod2.TypeAliases.Nested, 413, 413, '# A nested alias.\n'), + (mod2.make_type_alias(), 417, 417, '# A local alias.\n'), + ) + for alias, start, end, comments in cases: + with self.subTest(alias=alias): + self.assertSourceEqual(alias, start, end) + self.assertEqual(inspect.getsourcelines(alias), + (self.sourcerange(start, end).splitlines(True), + start)) + self.assertEqual(inspect.getcomments(alias), comments) + def test_with_comment(self): self.assertSourceEqual(mod2.with_comment, 58, 59) @@ -6920,6 +6962,32 @@ def f(): expected = "The source is: <<>>" self.assertIn(expected, output) + @unittest.skipIf(not has_subprocess_support, "test requires subprocess") + def test_getsource_type_alias(self): + output = self.run_on_interactive_mode(textwrap.dedent("""\ + type Alias = MissingName + import inspect + print(f"The source is: <<<{inspect.getsource(Alias)}>>>") + print(f"The lines are: {inspect.getsourcelines(Alias)!r}") + """)) + + self.assertIn("The source is: <<>>", output) + self.assertIn("The lines are: (['type Alias = MissingName\\n'], 1)", output) + + @unittest.skipIf(not has_subprocess_support, "test requires subprocess") + def test_getcomments_type_alias(self): + output = self.run_on_interactive_mode(textwrap.dedent("""\ + def f(): + # A local alias. + type Alias = MissingName + return Alias + + import inspect + print(f"The comments are: <<<{inspect.getcomments(f())}>>>") + """)) + + self.assertIn("The comments are: <<<# A local alias.\n>>>", output) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-09-22-20-26-13.gh-issue-157955.DhpVD8.rst b/Misc/NEWS.d/next/Library/2026-09-22-20-26-13.gh-issue-157955.DhpVD8.rst new file mode 100644 index 00000000000000..a8d8e8a25e5abf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-22-20-26-13.gh-issue-157955.DhpVD8.rst @@ -0,0 +1,3 @@ +Support type alias objects in :func:`inspect.getcomments`, +:func:`inspect.getfile`, :func:`inspect.getsourcefile`, +:func:`inspect.getsourcelines`, and :func:`inspect.getsource`.