From dccb5dfc66e3b9f83ecffe7364a37c67f3c69129 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 22 Sep 2026 19:58:25 -0700 Subject: [PATCH 1/2] gh-157955: Support type aliases for inspect source inspection functions --- Lib/inspect.py | 17 ++++++++++++++-- Lib/test/test_inspect/inspect_fodder.py | 3 +++ Lib/test/test_inspect/test_inspect.py | 20 +++++++++++++++++++ Lib/test/typinganndata/ann_module9.py | 3 +++ ...-09-22-20-26-13.gh-issue-157955.DhpVD8.rst | 3 +++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-22-20-26-13.gh-issue-157955.DhpVD8.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 3683f8c3fd5330..d6ffbadfe70591 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): @@ -1022,6 +1033,8 @@ def findsource(object): raise OSError('lineno is out of bounds') return lines, lnum + if isinstance(object, TypeAliasType): + object = object.evaluate_value if ismethod(object): object = object.__func__ if isfunction(object): 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/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index a4381f2c023395..bb78e1ce96ef64 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,23 @@ 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_empty_file(self): with temp_cwd() as cwd: with open('empty_file.py', 'w'): diff --git a/Lib/test/typinganndata/ann_module9.py b/Lib/test/typinganndata/ann_module9.py index 952217393e1ff7..fe53ada2954e02 100644 --- a/Lib/test/typinganndata/ann_module9.py +++ b/Lib/test/typinganndata/ann_module9.py @@ -12,3 +12,6 @@ class A: ... A.__qualname__ = 'A' ann1 = Union[List[A], int] + +type T = int +class C: pass 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`. From 5a94de32126bdfed1e1101ef3ce20575847dbbfc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 22 Sep 2026 20:35:06 -0700 Subject: [PATCH 2/2] fixes --- Doc/library/inspect.rst | 40 ++++++++++++++------ Lib/inspect.py | 17 +++++---- Lib/test/test_inspect/inspect_fodder2.py | 17 +++++++++ Lib/test/test_inspect/test_inspect.py | 48 ++++++++++++++++++++++++ Lib/test/typinganndata/ann_module9.py | 3 -- 5 files changed, 103 insertions(+), 22 deletions(-) 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 d6ffbadfe70591..80eb9d0ca52d1d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -995,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: @@ -1033,8 +1038,6 @@ def findsource(object): raise OSError('lineno is out of bounds') return lines, lnum - if isinstance(object, TypeAliasType): - object = object.evaluate_value if ismethod(object): object = object.__func__ if isfunction(object): @@ -1181,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.""" @@ -1202,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_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 bb78e1ce96ef64..002795adc260ae 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -884,6 +884,14 @@ def test_getsourcefile_type_alias(self): 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'): @@ -1155,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) @@ -6940,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/Lib/test/typinganndata/ann_module9.py b/Lib/test/typinganndata/ann_module9.py index fe53ada2954e02..952217393e1ff7 100644 --- a/Lib/test/typinganndata/ann_module9.py +++ b/Lib/test/typinganndata/ann_module9.py @@ -12,6 +12,3 @@ class A: ... A.__qualname__ = 'A' ann1 = Union[List[A], int] - -type T = int -class C: pass