Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AliasNewType,
AliasUnion,
ClassWithDecorators,
ClassWithForwardRefBase,
ClassWithIvarsAndCvars,
DocstringClass,
FakeError,
Expand All @@ -35,18 +36,21 @@
SomethingWithLiterals,
SomeEnum
)
import sys
from ._dataclasses import (
DataClassSimple,
DataClassWithFields,
DataClassDynamic,
DataClassWithKeywordOnly,
DataClassWithPostInit
)
if sys.version_info >= (3, 10):
from ._dataclasses import DataClassWithKeywordOnly

__all__ = (
"AliasNewType",
"AliasUnion",
"ClassWithDecorators",
"ClassWithForwardRefBase",
"ClassWithIvarsAndCvars",
"DataClassSimple",
"DataClassWithFields",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
# --------------------------------------------------------------------------

from dataclasses import dataclass, make_dataclass, KW_ONLY, field
import sys
from dataclasses import dataclass, make_dataclass, field
from typing import Any

if sys.version_info >= (3, 10):
from dataclasses import KW_ONLY


@dataclass
class DataClassSimple:
Expand Down Expand Up @@ -40,13 +44,14 @@ class DataClassWithFields:
)


@dataclass
class DataClassWithKeywordOnly:
""" Class for testing @dataclass with KW_ONLY. """
x: float
_: KW_ONLY
y: float
z: float
if sys.version_info >= (3, 10):
@dataclass
class DataClassWithKeywordOnly:
""" Class for testing @dataclass with KW_ONLY. """
x: float
_: KW_ONLY
y: float
z: float


@dataclass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
# --------------------------------------------------------------------------

from typing import overload
from typing import Union, overload


class MixinWithOverloads:
Expand All @@ -23,5 +23,5 @@ def do_thing(val: int) -> int:
def do_thing(val: bool) -> bool:
...

def do_thing(val: str | int | bool) -> str | int | bool:
def do_thing(val: Union[str, int, bool]) -> Union[str, int, bool]:
return val
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import datetime
from enum import Enum, EnumMeta
import functools
from typing import Any, overload, Dict, TypedDict, Union, Optional, Generic, TypeVar, NewType, ClassVar, Protocol, Literal
from typing import Any, overload, Dict, List, TypedDict, Union, Optional, Generic, TypeVar, NewType, ClassVar, Protocol, Literal
from typing_extensions import runtime_checkable

from ._mixin import MixinWithOverloads
Expand Down Expand Up @@ -48,6 +48,11 @@ def __init__(self, id, *args, **kwargs):
return cls


class ClassWithForwardRefBase(List["ClassWithForwardRefBase"]):
"""Class with a forward-reference string in its base class."""
pass


@add_id
class ClassWithDecorators:
pass
Expand Down Expand Up @@ -132,9 +137,7 @@ class FakeError(object):

FakeTypedDict = TypedDict(
'FakeTypedDict',
name=str,
age=int,
union=Union[bool, FakeObject, PetEnumPy3MetaclassAlt]
{'name': str, 'age': int, 'union': Union[bool, FakeObject, PetEnumPy3MetaclassAlt]}
)


Expand Down Expand Up @@ -204,7 +207,7 @@ def double(self, input: int = 1, *, test: bool = False, **kwargs) -> int:
def double(self, input: Sequence[int] = [1], *, test: bool = False, **kwargs) -> list[int]:
...

def double(self, input: int | Sequence[int], *, test: bool = False, **kwargs) -> int | list[int]:
def double(self, input: Union[int, Sequence[int]], *, test: bool = False, **kwargs) -> Union[int, List[int]]:
if isinstance(input, Sequence):
return [i * 2 for i in input]
return input * 2
Expand All @@ -217,7 +220,7 @@ def something(self, id: str, *args, **kwargs) -> str:
def something(self, id: int, *args, **kwargs) -> str:
...

def something(self, id: int | str, *args, **kwargs) -> str:
def something(self, id: Union[int, str], *args, **kwargs) -> str:
return str(id)


Expand All @@ -231,7 +234,7 @@ async def double(self, input: int = 1, *, test: bool = False, **kwargs) -> int:
async def double(self, input: Sequence[int] = [1], *, test: bool = False, **kwargs) -> list[int]:
...

async def double(self, input: int | Sequence[int], *, test: bool = False, **kwargs) -> int | list[int]:
async def double(self, input: Union[int, Sequence[int]], *, test: bool = False, **kwargs) -> Union[int, List[int]]:
if isinstance(input, Sequence):
return [i * 2 for i in input]
return input * 2
Expand All @@ -244,7 +247,7 @@ async def something(self, id: str, *args, **kwargs) -> str:
async def something(self, id: int, *args, **kwargs) -> str:
...

async def something(self, id: int | str, *args, **kwargs) -> str:
async def something(self, id: Union[int, str], *args, **kwargs) -> str:
return str(id)


Expand Down
8 changes: 4 additions & 4 deletions packages/python-packages/apistubgentest/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@
'Programming Language :: Python',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'License :: OSI Approved :: MIT License',
],
zip_safe=False,
Expand All @@ -70,5 +70,5 @@
"pandas>1.0.0,<3.0",
]
},
python_requires=">=3.8"
python_requires=">=3.9"
)
2 changes: 2 additions & 0 deletions packages/python-packages/apiview-stub-generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Release History

## Version 0.3.28 (Unreleased)
Dropped 3.7/3.8 support and added 3.11/3.12/3.13/3.14.
Added `crossLanguageVersion` to the `CrossLanguageMetadata` model.
Fixed a bug where class supertypes were resolving as runtime internal name rather than source code representation.

## Version 0.3.27 (2026-03-17)
Removed `ivar`/`cvar` keywords from token file in favor of the `ClassVar` type annotation to distinguish then.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import inspect
from inspect import Parameter
import re
import sys
import types as _builtin_types

from ._pylint_parser import PylintParser

Expand Down Expand Up @@ -30,7 +32,7 @@ def normalize_literal_match(match):
return literal_regex.sub(normalize_literal_match, value)


def process_literal_args(obj, args):
def process_literal_args(obj, args, namespace=""):
"""Unified function to handle Literal type arguments with proper quote normalization.
"""
processed_args = []
Expand All @@ -41,7 +43,15 @@ def process_literal_args(obj, args):
"Literal[" in obj_str)

for arg in args:
arg_string = str(arg)
# Call get_qualified_name for any parameterised generic (has __origin__) or
# PEP 604 union (types.UnionType, Python 3.10+). On Python 3.14, str() on
# these types emits X | Y notation; get_qualified_name normalises to Union[X, Y].
# TODO: When Python 3.9 support is dropped, emit X | Y (PEP 604)
# style instead of Union[X, Y] to align with the 3.10+ preferred syntax.
if hasattr(arg, '__origin__') or (sys.version_info >= (3, 10) and isinstance(arg, _builtin_types.UnionType)):
arg_string = get_qualified_name(arg, namespace)
else:
arg_string = str(arg)

if is_literal and obj_str.startswith(("typing.Literal[")):
# If individual string literal arg and not enum, return normalized string
Expand Down Expand Up @@ -158,6 +168,17 @@ def get_qualified_name(obj, namespace: str) -> str:
if module_name.startswith("astroid"):
return obj.as_string()
elif module_name == "types":
# PEP 604 (Python 3.10+): X | Y produces a types.UnionType. Normalise to
# Union[X, Y] / Optional[X] for consistent rendering across all supported versions.
# TODO: when Python 3.9 support is dropped, emit X | Y (PEP 604) style instead.
if sys.version_info >= (3, 10) and isinstance(obj, _builtin_types.UnionType) and hasattr(obj, '__args__'):
union_args = [get_qualified_name(a, namespace) for a in obj.__args__]
non_none = [a for a in union_args if a not in ("NoneType", "None")]
if len(non_none) < len(union_args):
if len(non_none) == 1:
return f"Optional[{non_none[0]}]"
return f"Optional[Union[{', '.join(non_none)}]]"
return f"Union[{', '.join(union_args)}]"
return str(obj)

if obj is Parameter.empty:
Expand All @@ -175,15 +196,18 @@ def get_qualified_name(obj, namespace: str) -> str:
# and are no longer part of the name
if hasattr(obj, "__args__"):
# Process all arguments with unified Literal handling
processed_args = process_literal_args(obj, obj.__args__ or [])
processed_args = process_literal_args(obj, obj.__args__ or [], namespace)

for arg_string in processed_args:
if keyword_regex.match(arg_string):
value = keyword_regex.search(arg_string).group(2)
if value == "NoneType":
# we ignore NoneType since Optional implies that NoneType is
# acceptable
if not name.startswith("Optional"):
# NoneType means the type is nullable; track it via wrap_optional.
# Extract the bare name (strip module prefix and brackets) to avoid
# double-wrapping when the name is already "Optional" (Python 3.13).
base = name_regex.search(name).group(0)
bare_name = base.split(".")[-1] if "." in base else base
if not bare_name.startswith("Optional"):
wrap_optional = True
else:
args.append(value)
Expand All @@ -202,8 +226,16 @@ def get_qualified_name(obj, namespace: str) -> str:
value = value[len(module_name) + 1 :]

if args and "[" not in value:
arg_string = ", ".join(args)
value = f"{value}[{arg_string}]"
bare_value = value.split(".")[-1] if "." in value else value
if wrap_optional and bare_value == "Union" and len(args) == 1:
# Union[X, None] with a single non-None arg collapses to Optional[X].
# Use bare_value (module prefix stripped) because value may be "typing.Union"
# when namespace is empty. 3.9 and 3.14 both yield __name__ == "Union" here;
# 3.13 yields "Optional" and skips this branch entirely.
value = args[0]
else:
arg_string = ", ".join(args)
value = f"{value}[{arg_string}]"
if wrap_optional:
value = f"Optional[{value}]"
return value
Loading