Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 654f72c

Browse files
[codex] Simplify type alias representation (#474)
## Summary - represent explicit TypeAlias declarations and PEP 695 aliases as dedicated PartialValue operations instead of overloading TypeAliasValue with runtime/object-semantics flags - remove the redundant is_specialized state and rely on stored type arguments, including packed empty TypeVarTuple arguments - simplify alias attribute lookup so PEP 695 alias partials only synthesize alias-defined attributes and otherwise defer to their runtime value ## Why TypeAliasValue had drifted into representing two different things at once: the typing-level alias metadata and the runtime alias object/value behavior. That forced several boolean flags and special cases to keep the two roles in sync. This change separates those roles. TypeAliasValue now stays focused on the typing alias, while PartialValue PEP_613_ALIAS and PartialValue PEP_695_ALIAS carry the runtime-facing behavior. ## Impact - the internal representation of type aliases is simpler and more consistent - PEP 613 aliases keep runtime callability without pretending to be runtime alias objects - PEP 695 alias symbols preserve runtime alias attributes like __value__ and __type_params__ while ordinary attributes follow normal runtime lookup ## Root Cause The old model mixed alias types and alias values in one object, so downstream code had to inspect flags like runtime_allows_value_call and uses_type_alias_object_semantics to guess which behavior it should use. ## Validation - uv run --python 3.12 --extra tests pytest pycroscope/test_type_aliases.py pycroscope/test_name_check_visitor.py pycroscope/test_annotations.py pycroscope/test_attributes.py -q
1 parent 59b8020 commit 654f72c

8 files changed

Lines changed: 386 additions & 180 deletions

File tree

pycroscope/annotations.py

Lines changed: 124 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@
154154
bound_self_type_from_class_key,
155155
class_owner_from_key,
156156
get_single_typevartuple_param,
157+
get_type_alias_root,
157158
get_typevar_variance,
159+
is_type_alias_partial_operation,
158160
iter_type_params_in_value,
159161
match_typevar_arguments,
160162
replace_fallback,
@@ -1137,9 +1139,7 @@ def _make_runtime_type_alias_value(
11371139
normalized_type_params
11381140
),
11391141
)
1140-
return TypeAliasValue(
1141-
"<runtime_generic_alias>", module, alias, runtime_allows_value_call=True
1142-
)
1142+
return TypeAliasValue("<runtime_generic_alias>", module, alias)
11431143

11441144

11451145
def _infer_alias_type_params_from_value(alias_value: Value) -> tuple[TypeParam, ...]:
@@ -1641,7 +1641,11 @@ def _pack_typevartuple_runtime_args(
16411641

16421642

16431643
def _validate_type_alias_arg_values(
1644-
type_params: Sequence[TypeParam], args_vals: Sequence[Value], ctx: Context
1644+
type_params: Sequence[TypeParam],
1645+
args_vals: Sequence[Value],
1646+
ctx: Context,
1647+
*,
1648+
node: ast.AST | None = None,
16451649
) -> list[Value]:
16461650
normalized_args = list(args_vals)
16471651
matched = match_typevar_arguments(type_params, args_vals)
@@ -1655,6 +1659,7 @@ def _validate_type_alias_arg_values(
16551659
f"Expected {len(type_params)} type arguments for type alias,"
16561660
f" got {len(args_vals)}",
16571661
error_code=ErrorCode.invalid_specialization,
1662+
node=node,
16581663
)
16591664
return normalized_args
16601665
for i, (type_param, arg) in enumerate(matched_args):
@@ -1673,6 +1678,7 @@ def _validate_type_alias_arg_values(
16731678
ctx.show_error(
16741679
f"Type argument {arg} is not compatible with {type_param}",
16751680
error_code=ErrorCode.invalid_specialization,
1681+
node=node,
16761682
)
16771683
elif type_param.constraints and not _is_alias_arg_compatible_with_constraints(
16781684
type_param.constraints, arg, ctx
@@ -1683,6 +1689,7 @@ def _validate_type_alias_arg_values(
16831689
ctx.show_error(
16841690
f"Type argument {arg} is not compatible with constraints ({constraint_list})",
16851691
error_code=ErrorCode.invalid_specialization,
1692+
node=node,
16861693
)
16871694
return normalized_args
16881695

@@ -1973,6 +1980,12 @@ def _type_from_value(value: Value, ctx: Context) -> Value:
19731980
return _type_from_subscripted_value(value.root, value.members, ctx)
19741981
if value.operation is PartialValueOperation.BITOR:
19751982
return _type_from_bitor_value(value.root, value.members, ctx)
1983+
if value.operation is PartialValueOperation.PEP_613_ALIAS:
1984+
assert isinstance(value.root, TypeAliasValue)
1985+
return value.root.get_value()
1986+
if value.operation is PartialValueOperation.PEP_695_ALIAS:
1987+
assert isinstance(value.root, TypeAliasValue)
1988+
return value.root
19761989
return value.get_fallback_value()
19771990
elif isinstance(value, PartialCallValue):
19781991
type_param = make_type_param_from_value(value, ctx=ctx)
@@ -2095,6 +2108,14 @@ def _annotation_expr_from_subscripted_value(
20952108
def _type_from_subscripted_value(
20962109
root: Value, members: Sequence[Value], ctx: Context
20972110
) -> Value:
2111+
if isinstance(root, PartialValue) and is_type_alias_partial_operation(
2112+
root.operation
2113+
):
2114+
specialized = _specialize_type_alias_partial(root, members, ctx)
2115+
if root.operation is PartialValueOperation.PEP_613_ALIAS:
2116+
return specialized.runtime_value
2117+
return specialized.root
2118+
20982119
if isinstance(root, AnnotatedValue):
20992120
self_owner = next(root.get_metadata_of_type(SelfOwnerExtension), None)
21002121
if self_owner is not None:
@@ -2310,68 +2331,7 @@ def _type_from_subscripted_value(
23102331
synthetic_typ, _canonicalize_generic_args_for_value(typed_members)
23112332
)
23122333
if isinstance(root, TypeAliasValue):
2313-
type_params = tuple(root.alias.get_type_params())
2314-
type_arguments_are_packed = False
2315-
if any(_is_unpack_annotation_member(member) for member in members):
2316-
normalized_unpack_members = _normalize_generic_unpack_members(members, ctx)
2317-
else:
2318-
normalized_unpack_members = None
2319-
saw_unpack = normalized_unpack_members is not None
2320-
has_unbounded_unpack = (
2321-
saw_unpack
2322-
and normalized_unpack_members is not None
2323-
and any(is_many for is_many, _ in normalized_unpack_members)
2324-
)
2325-
packed_variadic_members = _pack_typevartuple_args_from_unpack_members(
2326-
type_params, members, ctx
2327-
)
2328-
if packed_variadic_members is not None:
2329-
args_vals = packed_variadic_members
2330-
type_arguments_are_packed = True
2331-
elif (
2332-
saw_unpack
2333-
and normalized_unpack_members is not None
2334-
and not has_unbounded_unpack
2335-
):
2336-
unpacked_members = [member for _, member in normalized_unpack_members]
2337-
if len(unpacked_members) == len(type_params):
2338-
args_vals = [
2339-
_type_from_value_type_alias_arg(member, type_param, ctx)
2340-
for member, type_param in zip(unpacked_members, type_params)
2341-
]
2342-
else:
2343-
args_vals = [
2344-
_type_from_alias_argument_value(member, ctx)
2345-
for member in unpacked_members
2346-
]
2347-
elif len(members) == len(type_params):
2348-
args_vals = [
2349-
_type_from_value_type_alias_arg(member, type_param, ctx)
2350-
for member, type_param in zip(members, type_params)
2351-
]
2352-
else:
2353-
args_vals = [
2354-
_type_from_alias_argument_value(member, ctx) for member in members
2355-
]
2356-
if has_unbounded_unpack and packed_variadic_members is None:
2357-
ctx.show_error("Unpacked TypeVarTuple cannot specialize this type alias")
2358-
args_vals = _validate_type_alias_arg_values(type_params, args_vals, ctx)
2359-
alias_value = TypeAliasValue(
2360-
root.name,
2361-
root.module,
2362-
root.alias,
2363-
tuple(args_vals),
2364-
runtime_allows_value_call=root.runtime_allows_value_call,
2365-
uses_type_alias_object_semantics=root.uses_type_alias_object_semantics,
2366-
is_specialized=True,
2367-
type_arguments_are_packed=type_arguments_are_packed,
2368-
)
2369-
if root.runtime_allows_value_call:
2370-
# Explicit `TypeAlias` declarations should behave like expanded types in
2371-
# annotation contexts so generic solving can bind function/class type
2372-
# variables precisely even when runtime module loading fails.
2373-
return alias_value.get_value()
2374-
return alias_value
2334+
return _specialize_type_alias_value(root, members, ctx)
23752335

23762336
assert isinstance(root, Value)
23772337
if not isinstance(root, KnownValue):
@@ -2386,7 +2346,15 @@ def _type_from_subscripted_value(
23862346
alias_object = root
23872347
runtime_type_params = tuple(alias_object.__type_params__)
23882348
type_params = tuple(make_type_param(tp, ctx=ctx) for tp in runtime_type_params)
2389-
if len(members) == len(type_params):
2349+
type_arguments_are_packed = False
2350+
if (
2351+
not members
2352+
and len(type_params) == 1
2353+
and isinstance(type_params[0], TypeVarTupleParam)
2354+
):
2355+
args_vals = [TypeVarTupleBindingValue(())]
2356+
type_arguments_are_packed = True
2357+
elif len(members) == len(type_params):
23902358
args_vals = [
23912359
_type_from_value_type_alias_arg(member, type_param, ctx)
23922360
for member, type_param in zip(members, type_params)
@@ -2406,7 +2374,7 @@ def _type_from_subscripted_value(
24062374
alias_object.__module__,
24072375
alias,
24082376
tuple(args_vals),
2409-
is_specialized=True,
2377+
type_arguments_are_packed=type_arguments_are_packed,
24102378
)
24112379
if root is typing.Union:
24122380
return unite_values(*[_type_from_value(elt, ctx) for elt in members])
@@ -2592,6 +2560,94 @@ def _evaluate_runtime_generic_alias_type_params(
25922560
return AnyValue(AnySource.error)
25932561

25942562

2563+
def _specialize_type_alias_partial(
2564+
root: PartialValue,
2565+
members: Sequence[Value],
2566+
ctx: Context,
2567+
*,
2568+
node: ast.AST | None = None,
2569+
) -> PartialValue:
2570+
assert is_type_alias_partial_operation(root.operation)
2571+
alias_root = get_type_alias_root(root)
2572+
assert alias_root is not None
2573+
specialized_root = _specialize_type_alias_value(alias_root, members, ctx, node=node)
2574+
runtime_value = (
2575+
specialized_root.get_value()
2576+
if root.operation is PartialValueOperation.PEP_613_ALIAS
2577+
else root.runtime_value
2578+
)
2579+
return PartialValue(root.operation, specialized_root, root.node, (), runtime_value)
2580+
2581+
2582+
def _specialize_type_alias_value(
2583+
root: TypeAliasValue,
2584+
members: Sequence[Value],
2585+
ctx: Context,
2586+
*,
2587+
node: ast.AST | None = None,
2588+
) -> TypeAliasValue:
2589+
type_params = tuple(root.alias.get_type_params())
2590+
type_arguments_are_packed = False
2591+
if any(_is_unpack_annotation_member(member) for member in members):
2592+
normalized_unpack_members = _normalize_generic_unpack_members(members, ctx)
2593+
else:
2594+
normalized_unpack_members = None
2595+
saw_unpack = normalized_unpack_members is not None
2596+
has_unbounded_unpack = (
2597+
saw_unpack
2598+
and normalized_unpack_members is not None
2599+
and any(is_many for is_many, _ in normalized_unpack_members)
2600+
)
2601+
packed_variadic_members = _pack_typevartuple_args_from_unpack_members(
2602+
type_params, members, ctx
2603+
)
2604+
if packed_variadic_members is not None:
2605+
args_vals = packed_variadic_members
2606+
type_arguments_are_packed = True
2607+
elif (
2608+
not members
2609+
and len(type_params) == 1
2610+
and isinstance(type_params[0], TypeVarTupleParam)
2611+
):
2612+
args_vals = [TypeVarTupleBindingValue(())]
2613+
type_arguments_are_packed = True
2614+
elif (
2615+
saw_unpack
2616+
and normalized_unpack_members is not None
2617+
and not has_unbounded_unpack
2618+
):
2619+
unpacked_members = [member for _, member in normalized_unpack_members]
2620+
if len(unpacked_members) == len(type_params):
2621+
args_vals = [
2622+
_type_from_value_type_alias_arg(member, type_param, ctx)
2623+
for member, type_param in zip(unpacked_members, type_params)
2624+
]
2625+
else:
2626+
args_vals = [
2627+
_type_from_alias_argument_value(member, ctx)
2628+
for member in unpacked_members
2629+
]
2630+
elif len(members) == len(type_params):
2631+
args_vals = [
2632+
_type_from_value_type_alias_arg(member, type_param, ctx)
2633+
for member, type_param in zip(members, type_params)
2634+
]
2635+
else:
2636+
args_vals = [_type_from_alias_argument_value(member, ctx) for member in members]
2637+
if has_unbounded_unpack and packed_variadic_members is None:
2638+
ctx.show_error(
2639+
"Unpacked TypeVarTuple cannot specialize this type alias", node=node
2640+
)
2641+
args_vals = _validate_type_alias_arg_values(type_params, args_vals, ctx, node=node)
2642+
return TypeAliasValue(
2643+
root.name,
2644+
root.module,
2645+
root.alias,
2646+
tuple(args_vals),
2647+
type_arguments_are_packed=type_arguments_are_packed,
2648+
)
2649+
2650+
25952651
def _maybe_get_extra(origin: type) -> ClassKey:
25962652
# ContextManager is defined oddly and we lose the Protocol if we don't use
25972653
# synthetic types.

pycroscope/attributes.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,19 @@ def _get_type_object_attribute(
192192

193193

194194
def get_attribute(ctx: AttrContext) -> Value:
195-
if (
196-
isinstance(ctx.root_value, TypeAliasValue)
197-
and ctx.root_value.uses_type_alias_object_semantics
198-
):
199-
return _get_attribute_from_type_alias(ctx.root_value, ctx)
200195
lookup_root_value = (
201196
ctx.root_value if ctx.lookup_root_value is None else ctx.lookup_root_value
202197
)
198+
if (
199+
isinstance(ctx.root_value, PartialValue)
200+
and ctx.root_value.operation is PartialValueOperation.PEP_695_ALIAS
201+
):
202+
assert isinstance(ctx.root_value.root, TypeAliasValue)
203+
attribute_value = _get_attribute_from_type_alias(ctx.root_value.root, ctx)
204+
if attribute_value is not UNINITIALIZED_VALUE:
205+
return attribute_value
206+
if ctx.lookup_root_value is None:
207+
lookup_root_value = ctx.root_value.runtime_value
203208
if (
204209
isinstance(lookup_root_value, TypeVarValue)
205210
and lookup_root_value.typevar_param.bound is not None
@@ -256,12 +261,14 @@ def get_attribute(ctx: AttrContext) -> Value:
256261
return ctx.root_value.predicate.value
257262
return attribute_value
258263
root_value = replace_fallback(lookup_root_value)
259-
if isinstance(root_value, KnownValue) and is_typing_name(
260-
type(root_value.val), "TypeAliasType"
261-
):
262-
return _get_attribute_from_runtime_type_alias(root_value.val, ctx)
263264
attribute_value: Value = UNINITIALIZED_VALUE
264265
if isinstance(root_value, KnownValue):
266+
if is_typing_name(type(root_value.val), "TypeAliasType"):
267+
attribute_value = _get_attribute_from_runtime_type_alias(
268+
root_value.val, ctx
269+
)
270+
if attribute_value is not UNINITIALIZED_VALUE:
271+
return attribute_value
265272
attribute_value = _get_attribute_from_known(root_value.val, ctx)
266273
elif isinstance(root_value, TypedValue):
267274
if (

pycroscope/checker.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
safe_isinstance,
3535
safe_issubclass,
3636
)
37+
from .safe import is_union as is_runtime_union
3738
from .shared_options import EnforceNoUnusedCallPatterns, VariableNameValues
3839
from .signature import (
3940
ANY_SIGNATURE,
@@ -115,6 +116,18 @@
115116
_SyntheticGenericBases = dict[ClassKey, TypeVarMap]
116117

117118

119+
def _runtime_value_is_union(value: Value) -> bool:
120+
value = replace_fallback(value)
121+
if isinstance(value, AnnotatedValue):
122+
return _runtime_value_is_union(value.value)
123+
if is_union(value):
124+
return True
125+
if not isinstance(value, KnownValue):
126+
return False
127+
origin = safe_getattr(value.val, "__origin__", None)
128+
return is_runtime_union(value.val) or is_runtime_union(origin)
129+
130+
118131
@dataclass(frozen=True)
119132
class _DataclassFieldEntry:
120133
parameter: SigParameter
@@ -2059,28 +2072,17 @@ def signature_from_value(
20592072
)
20602073
if sig is not None:
20612074
return sig
2062-
if (
2063-
isinstance(value, TypeAliasValue)
2064-
and value.runtime_allows_value_call
2065-
and not value.type_arguments
2066-
and not value.alias.get_type_params()
2067-
):
2068-
alias_value = value.get_value()
2069-
# Explicit TypeAlias declarations can denote class objects (e.g.
2070-
# `Alias: TypeAlias = list`) that should remain callable.
2071-
if isinstance(alias_value, KnownValue) and isinstance(
2072-
alias_value.val, type
2073-
):
2074-
return self.signature_from_value(
2075-
alias_value,
2076-
get_return_override=get_return_override,
2077-
get_call_attribute=get_call_attribute,
2078-
)
2079-
if isinstance(alias_value, TypedValue) and isinstance(
2080-
alias_value.typ, type
2075+
if value.operation in (
2076+
PartialValueOperation.PEP_613_ALIAS,
2077+
PartialValueOperation.PEP_695_ALIAS,
20812078
):
2079+
if (
2080+
value.operation is PartialValueOperation.PEP_613_ALIAS
2081+
and _runtime_value_is_union(value.runtime_value)
2082+
):
2083+
return None
20822084
return self.signature_from_value(
2083-
KnownValue(alias_value.typ),
2085+
value.runtime_value,
20842086
get_return_override=get_return_override,
20852087
get_call_attribute=get_call_attribute,
20862088
)

pycroscope/implementation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ def _invalid_classinfo_kind(
338338
return _invalid_classinfo_kind(
339339
value.value, ctx, is_subclass_check=is_subclass_check
340340
)
341+
if (
342+
isinstance(value, PartialValue)
343+
and value.operation is PartialValueOperation.PEP_695_ALIAS
344+
):
345+
return "a type alias"
341346
if isinstance(value, TypeAliasValue):
342347
return "a type alias"
343348
if isinstance(value, SyntheticClassObjectValue):

0 commit comments

Comments
 (0)