From 9b24443787c24e35cffecca9a081b30736698072 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 9 Mar 2026 15:38:12 +0100 Subject: [PATCH] Fix Python 3.14 compatibility for typing.Union annotations In Python 3.14, typing.Union changed its repr from 'typing.Union[X, Y]' to 'X | Y' (PEP 604), breaking annotation inference. Changes: - Use getattr() instead of safe_getattr() for __module__ retrieval (getattr_static fails on Union types in Python 3.14) - Add fallback to typing.get_origin() when regex fails to match - Normalize Union display back to 'Union[X, Y]' format for consistency - Update test expectations for invalid annotation edge case in 3.14 Fixes: https://github.com/davidhalter/jedi/issues/2064 --- jedi/inference/compiled/access.py | 39 ++++++++++++++++++++++++++----- test/test_api/test_interpreter.py | 2 +- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/jedi/inference/compiled/access.py b/jedi/inference/compiled/access.py index 5a8e68fa..7b1fc5b0 100644 --- a/jedi/inference/compiled/access.py +++ b/jedi/inference/compiled/access.py @@ -477,22 +477,49 @@ class DirectObjectAccess: """ name = None args = () - if safe_getattr(self._obj, '__module__', default='') == 'typing': + # Use getattr instead of safe_getattr for __module__ as getattr_static + # fails on typing types in Python 3.14+ + module = getattr(self._obj, '__module__', '') + if module == 'typing': + import typing + # Try regex first (works for most types) m = re.match(r'typing.(\w+)\[', repr(self._obj)) if m is not None: name = m.group(1) + elif sys.version_info >= (3, 8): + # Fallback to get_origin() for Python 3.8+ when regex fails + # In Python 3.14+, Union/Optional repr changed to use | syntax + origin = typing.get_origin(self._obj) + if origin is typing.Union: + name = 'Union' - import typing - if sys.version_info >= (3, 8): - args = typing.get_args(self._obj) - else: - args = safe_getattr(self._obj, '__args__', default=None) + # Get args + if sys.version_info >= (3, 8): + args = typing.get_args(self._obj) + else: + args = safe_getattr(self._obj, '__args__', default=None) + if args is None: + args = () return name, tuple(self._create_access_path(arg) for arg in args) def needs_type_completions(self): return inspect.isclass(self._obj) and self._obj != type def _annotation_to_str(self, annotation): + # In Python 3.14+, Union types are displayed as X | Y instead of Union[X, Y] + # We normalize to Union[X, Y] for consistency + if sys.version_info >= (3, 8): + import typing + origin = typing.get_origin(annotation) + if origin is typing.Union: + # Get the args and format them as Union[...] + args = typing.get_args(annotation) + formatted_args = ', '.join( + self._annotation_to_str(arg) if hasattr(arg, '__origin__') + else getattr(arg, '__name__', str(arg)) + for arg in args + ) + return f'Union[{formatted_args}]' return inspect.formatannotation(annotation) def get_signature_params(self): diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 1aa027bf..42193482 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -664,7 +664,7 @@ def bar(): ({'return': 'typing.Union["str", int]'}, ['int', 'str'] if sys.version_info >= (3, 9) else ['int'], ''), ({'return': 'typing.Union["str", 1]'}, - ['str'] if sys.version_info >= (3, 11) else [], ''), + [] if sys.version_info >= (3, 14) else (['str'] if sys.version_info >= (3, 11) else []), ''), ({'return': 'typing.Optional[str]'}, ['NoneType', 'str'], ''), ({'return': 'typing.Optional[str, int]'}, [], ''), # Takes only one arg ({'return': 'typing.Any'},