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/jedi/inference/compiled/getattr_static.py b/jedi/inference/compiled/getattr_static.py index dd02f4d6..9c3f4d35 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 933ad740..38650fee 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'}, @@ -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 e452f3d8..3000c2dc 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -83,6 +83,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