From 9b24443787c24e35cffecca9a081b30736698072 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Mon, 9 Mar 2026 15:38:12 +0100 Subject: [PATCH 1/2] 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'}, From 04737b26372722b19292e82b167123ff29182496 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Tue, 10 Mar 2026 09:12:32 +0100 Subject: [PATCH 2/2] Fix Python 3.15 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix getattr_static for Python 3.15 __dict__ GetSetDescriptorType - Accept abs() parameter name change ('x' → 'number') - Add Python 3.15 os module constants to test expectations Fixes instance attribute introspection and stdlib changes in Python 3.15. --- jedi/inference/compiled/getattr_static.py | 5 ++++- test/test_api/test_interpreter.py | 3 ++- test/test_utils.py | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/jedi/inference/compiled/getattr_static.py b/jedi/inference/compiled/getattr_static.py index 03c199ef..770a471e 100644 --- a/jedi/inference/compiled/getattr_static.py +++ b/jedi/inference/compiled/getattr_static.py @@ -90,7 +90,10 @@ def getattr_static(obj, attr, default=_sentinel): if not _is_type(obj): klass = type(obj) dict_attr = _shadowed_dict(klass) - if (dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType): + # In Python 3.15+, __dict__ is a GetSetDescriptorType instead of being _sentinel + if (dict_attr is _sentinel + or type(dict_attr) is types.MemberDescriptorType + or type(dict_attr) is types.GetSetDescriptorType): instance_result = _check_instance(obj, attr) else: klass = obj diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 42193482..b87b0410 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -746,7 +746,8 @@ def test_complete_not_findable_class_source(): def test_param_infer_default(): abs_sig, = jedi.Interpreter('abs(', [{'abs': abs}]).get_signatures() param, = abs_sig.params - assert param.name == 'x' + # Parameter name changed from 'x' to 'number' in Python 3.15 + assert param.name in ('x', 'number') assert param.infer_default() == [] diff --git a/test/test_utils.py b/test/test_utils.py index 4fc19878..a212eec5 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -81,6 +81,9 @@ class TestSetupReadline(unittest.TestCase): '_', 'O_', 'EX_', 'EFD_', 'MFD_', 'TFD_', 'SF_', 'ST_', 'CLD_', 'POSIX_SPAWN_', 'P_', 'RWF_', 'CLONE_', 'SCHED_', 'SPLICE_', + # Python 3.15+ new constants + 'AT_', 'PIDFD_', 'STATX_', 'GRND_', 'XATTR_', + 'RTLD_', 'PRIO_', 'F_', 'SEEK_', 'NODEV', ] difference = { x for x in difference