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
This commit is contained in:
Lumir Balhar
2026-03-09 15:38:12 +01:00
parent 76c1e03f07
commit 9b24443787
2 changed files with 34 additions and 7 deletions
+33 -6
View File
@@ -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):
+1 -1
View File
@@ -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'},