Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Support `P.args`, `P.kwargs`, and generic specialization with PEP 695 parameter specifications without crashing, and exclude `__type_params__` metadata from protocol requirements.
- Narrow negative `TypeIs` checks against covariant generic `Any` arms more precisely when the checked type uses `object`.
- Normalize `Not[...]` inside expected-type expressions such as `assert_type(x, Intersection[A, Not[B]])`.
- Implement call checking for callable intersection types.
Expand Down
19 changes: 19 additions & 0 deletions pycroscope/arg_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,25 @@ def _specialize_generic_type_params(
for arg in generic_args
)

if (
len(type_params) == 1
and isinstance(type_params[0], ParamSpecParam)
and generic_args
):
if len(generic_args) == 1:
value = coerce_paramspec_specialization_to_input_sig(generic_args[0])
if not isinstance(value, InputSigValue):
value = coerce_paramspec_specialization_to_input_sig(
SequenceValue(tuple, [(False, value)])
)
else:
value = coerce_paramspec_specialization_to_input_sig(
SequenceValue(
tuple, [(False, argument) for argument in generic_args]
)
)
return [value]

def _coerce_specialized_arg(type_param: TypeParam, value: Value) -> Value:
if isinstance(type_param, ParamSpecParam):
return coerce_paramspec_specialization_to_input_sig(value)
Expand Down
34 changes: 23 additions & 11 deletions pycroscope/name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13147,7 +13147,9 @@ def _composite_from_subscript_no_mvv(
) -> Value:
value = root_composite.value
index = index_composite.value
if not TypedValue(slice).is_assignable(index, self):
if isinstance(
index, (InputSigValue, TypeVarTupleBindingValue)
) or not TypedValue(slice).is_assignable(index, self):
value = self._maybe_replace_tuple_subtype_with_tuple_sequence(value)
root_composite = Composite(value, root_composite.varname, root_composite.node)

Expand Down Expand Up @@ -13879,11 +13881,11 @@ def composite_from_attribute(self, node: ast.Attribute) -> Composite:
return Composite(self.being_assigned, composite, node)
elif isinstance(node.ctx, ast.Load):
root_composite = self._get_locally_narrowed_composite(root_composite, node)
partial_paramspec_component = self._partial_paramspec_component(
paramspec_component = self._paramspec_component(
root_composite.value, node.attr
)
if partial_paramspec_component is not None:
return Composite(partial_paramspec_component, composite, node)
if paramspec_component is not None:
return Composite(paramspec_component, composite, node)
if self.in_annotation and isinstance(root_composite.value, KnownValue):
try:
attr_value = getattr(root_composite.value.val, node.attr)
Expand Down Expand Up @@ -13940,16 +13942,23 @@ def composite_from_attribute(self, node: ast.Attribute) -> Composite:
self.show_error(node, "Unknown context", ErrorCode.unexpected_node)
return Composite(AnyValue(AnySource.error), composite, node)

def _partial_paramspec_component(self, value: Value, attr: str) -> Value | None:
if attr not in ("args", "kwargs") or not isinstance(value, PartialCallValue):
def _paramspec_component(self, value: Value, attr: str) -> Value | None:
if attr not in ("args", "kwargs"):
return None
runtime_value = replace_fallback(value.runtime_value)
if not (
isinstance(runtime_value, TypedValue)
and is_typing_name(runtime_value.typ, "ParamSpec")
if isinstance(value, InputSigValue) and isinstance(
value.input_sig, ParamSpecParam
):
type_param = value.input_sig
elif isinstance(value, PartialCallValue):
runtime_value = replace_fallback(value.runtime_value)
if not (
isinstance(runtime_value, TypedValue)
and is_typing_name(runtime_value.typ, "ParamSpec")
):
return None
type_param = make_type_param_from_value(value, visitor=self)
else:
return None
type_param = make_type_param_from_value(value, visitor=self)
if not isinstance(type_param, ParamSpecParam):
return None
if attr == "args":
Expand Down Expand Up @@ -14486,6 +14495,9 @@ def get_attribute(
is_special_lookup = (
self_value is not None and attr.startswith("__") and attr.endswith("__")
)
paramspec_component = self._paramspec_component(root_composite.value, attr)
if paramspec_component is not None:
return paramspec_component
if (
isinstance(root_composite.value, PartialValue)
and root_composite.value.operation is PartialValueOperation.SUBSCRIPT
Expand Down
14 changes: 14 additions & 0 deletions pycroscope/test_name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3204,6 +3204,20 @@ def f(p3: Proto3, p4: Proto4[...]) -> None:
takes_proto4(p3)
takes_proto3(p4)

@skip_before((3, 12))
def test_pep695_paramspec_components_do_not_internal_error(self):
self.assert_passes(
"""
from typing import Protocol

class Proto[**P](Protocol):
def __call__(
self, *args: P.args, **kwargs: P.kwargs
) -> None: ...
""",
run_in_both_module_modes=True,
)

@assert_passes(run_in_both_module_modes=True)
def test_bound_typeguard_methods_preserve_narrowing(self):
from typing import TypeGuard
Expand Down
23 changes: 22 additions & 1 deletion pycroscope/test_protocol.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
# static analysis: ignore
from .test_name_check_visitor import TestNameCheckVisitorBase
from .test_node_visitor import assert_passes
from .test_node_visitor import assert_passes, skip_before


class TestProtocol(TestNameCheckVisitorBase):
@skip_before((3, 12))
def test_pep695_type_params_are_not_protocol_members(self):
self.assert_passes(
"""
from collections.abc import Callable
from typing import Protocol

class ProtocolWithP[**P](Protocol):
def __call__(
self, *args: P.args, **kwargs: P.kwargs
) -> None: ...

type TypeAliasWithP[**P] = Callable[P, None]

def capybara[**P](value: TypeAliasWithP[P]) -> None:
protocol: ProtocolWithP[P] = value
print(protocol)
""",
run_in_both_module_modes=True,
)

@assert_passes()
def test_generic_constructor_accepts_known_protocol_value(self):
import logging
Expand Down
22 changes: 22 additions & 0 deletions pycroscope/test_typevar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,28 @@ def capybara(s: Sequence[int], t: str):


class TestGenericClasses(TestNameCheckVisitorBase):
@skip_before((3, 12))
def test_paramspec_flat_specialization_constructor_does_not_crash(self):
self.assert_passes(
"""
from typing import Generic, ParamSpec

class New[**P]:
pass

P = ParamSpec("P")

class Old(Generic[P]):
pass

new_one: New[[int]] = New[int]()
new_two: New[[int, str]] = New[int, str]()
old_one: Old[[int]] = Old[int]()
old_two: Old[[int, str]] = Old[int, str]()
""",
run_in_both_module_modes=True,
)

@skip_before((3, 12))
def test_generic(self):
self.assert_passes("""
Expand Down
4 changes: 3 additions & 1 deletion pycroscope/type_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"__new__",
"__module__",
"__parameters__",
"__type_params__",
"__slots__",
"__subclasshook__",
"__weakref__",
Expand Down Expand Up @@ -708,10 +709,11 @@ def _compute_is_protocol(self) -> bool:
def _compute_protocol_members(self) -> set[str]:
if not self.is_protocol():
return set()
return (
members = (
self._get_protocol_members_contributed_by_self()
| self._get_protocol_members_contributed_by_protocol_bases()
)
return members - EXCLUDED_PROTOCOL_MEMBERS

def _compute_dataclass_fields(self) -> tuple[DataclassFieldRecord, ...]:
import pycroscope.type_object_builder as type_object_builder
Expand Down
10 changes: 10 additions & 0 deletions tools/conformance_known_failures.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,13 @@ dataclasses_descriptors

# Newly added on typing; pycroscope doesn't fully implement disjoint_base yet
directives_disjoint_base

# Added or changed by https://github.com/python/typing/pull/2215.
# ParamSpec and TypeVarTuple variance is not fully implemented yet; the other
# cases changed because they now use PEP 695 ParamSpec syntax.
classes_classvar
classes_override
generics_mixed_variance_inference
generics_paramspec_variance
generics_typevartuple_basic
generics_typevartuple_variance
Loading