Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix creating TypeVar and others in non-executed code #746

Merged
merged 1 commit into from
Mar 10, 2024
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
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Support calls to `TypeVar` and several other typing constructs in
code that is not executed (e.g., under `if TYPE_CHECKING`) (#746)
- Fix spurious errors for the class-based syntax for creating
`NamedTuple` classes (#746)
- Make error registry into a custom class instead of an enum, removing
dependency on `aenum` (#739)
- Treat subclasses of `int` as subclasses of `float` and `complex` too (#738)
Expand Down
28 changes: 27 additions & 1 deletion pyanalyze/arg_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import ast
import asyncio
import collections
import contextlib
import enum
import inspect
Expand Down Expand Up @@ -34,6 +35,7 @@

import asynq
import qcore
import typing_extensions
from typing_extensions import is_typeddict

import pyanalyze
Expand Down Expand Up @@ -208,6 +210,24 @@ class IgnoredCallees(PyObjectSequenceOption[object]):
name = "ignored_callees"


TYPING_OBJECTS_SAFE_TO_CALL = [
getattr(mod, name)
for mod in (typing, typing_extensions)
for name in (
"TypeVar",
"TypeVarTuple",
"ParamSpec",
"NewType",
"TypeAliasType",
"NamedTuple",
"TypedDict",
"deprecated",
"dataclass_transform",
)
if hasattr(mod, name)
]


class ClassesSafeToInstantiate(PyObjectSequenceOption[type]):
"""We will instantiate instances of these classes if we can infer the value of all of
their arguments. This is useful mostly for classes that are commonly instantiated with static
Expand All @@ -223,6 +243,7 @@ class ClassesSafeToInstantiate(PyObjectSequenceOption[type]):
asynq.ConstFuture,
range,
tuple,
*[obj for obj in TYPING_OBJECTS_SAFE_TO_CALL if safe_isinstance(obj, type)],
]


Expand All @@ -232,7 +253,12 @@ class FunctionsSafeToCall(PyObjectSequenceOption[object]):
arguments."""

name = "functions_safe_to_call"
default_value = [sorted, asynq.asynq]
default_value = [
sorted,
asynq.asynq,
collections.namedtuple,
*[obj for obj in TYPING_OBJECTS_SAFE_TO_CALL if not safe_isinstance(obj, type)],
]


_HookReturn = Union[None, ConcreteSignature, inspect.Signature, Callable[..., Any]]
Expand Down
85 changes: 83 additions & 2 deletions pyanalyze/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,55 @@ def _any_impl(ctx: CallContext) -> Value:
return AnyValue(AnySource.error)


# Should not be necessary, but by default we pick up a wrong signature for
# typing.NamedTuple
def _namedtuple_impl(ctx: CallContext) -> Value:
has_kwargs = (
isinstance(ctx.vars["kwargs"], TypedDictValue)
and len(ctx.vars["kwargs"].items) > 0
)
# Mirrors the runtime logic in typing.NamedTuple in 3.13
if ctx.vars["fields"] is _NO_ARG_SENTINEL:
if has_kwargs:
ctx.show_error(
'Creating "NamedTuple" classes using keyword arguments'
" is deprecated and will be disallowed in Python 3.15. "
"Use the class-based or functional syntax instead.",
ErrorCode.deprecated,
arg="kwargs",
)
else:
ctx.show_error(
'Failing to pass a value for the "fields" parameter'
" is deprecated and will be disallowed in Python 3.15.",
ErrorCode.deprecated,
)
elif ctx.vars["fields"] == KnownValue(None):
if has_kwargs:
ctx.show_error(
'Cannot pass "None" as the "fields" parameter '
"and also specify fields using keyword arguments",
ErrorCode.incompatible_call,
arg="fields",
)
else:
ctx.show_error(
'Passing "None" as the "fields" parameter '
" is deprecated and will be disallowed in Python 3.15.",
ErrorCode.deprecated,
arg="fields",
)
elif has_kwargs:
ctx.show_error(
"Either list of fields or keywords"
' can be provided to "NamedTuple", not both',
ErrorCode.incompatible_call,
arg="kwargs",
)

return AnyValue(AnySource.inference)


_POS_ONLY = ParameterKind.POSITIONAL_ONLY
_ENCODING_PARAMETER = SigParameter(
"encoding", annotation=TypedValue(str), default=KnownValue("")
Expand Down Expand Up @@ -2090,15 +2139,13 @@ def get_default_argspecs() -> Dict[object, Signature]:
except AttributeError:
pass
else:
# Anticipating https://bugs.python.org/issue46414
sig = Signature.make(
[SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))],
return_annotation=TypeVarValue(T),
impl=_reveal_type_impl,
callable=reveal_type_func,
)
signatures.append(sig)
# Anticipating that this will be added to the stdlib
try:
assert_type_func = getattr(mod, "assert_type")
except AttributeError:
Expand All @@ -2114,4 +2161,38 @@ def get_default_argspecs() -> Dict[object, Signature]:
impl=_assert_type_impl,
)
signatures.append(sig)
try:
namedtuple_func = getattr(mod, "NamedTuple")
except AttributeError:
pass
else:
sig = Signature.make(
[
SigParameter("typename", _POS_ONLY, annotation=TypedValue(str)),
SigParameter(
"fields",
_POS_ONLY,
annotation=GenericValue(
collections.abc.Iterable,
[
SequenceValue(
tuple,
[
(False, TypedValue(str)),
(False, AnyValue(AnySource.inference)),
],
)
],
)
| KnownValue(None),
default=_NO_ARG_SENTINEL,
),
SigParameter("kwargs", ParameterKind.VAR_KEYWORD),
],
return_annotation=TypedValue(type),
callable=namedtuple_func,
impl=_namedtuple_impl,
allow_call=True,
)
signatures.append(sig)
return {sig.callable: sig for sig in signatures}
35 changes: 35 additions & 0 deletions pyanalyze/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1900,3 +1900,38 @@ def capybara(
z: float | bool | set, # E: missing_generic_parameters
) -> None:
pass


class TestIfTypeChecking(TestNameCheckVisitorBase):
@assert_passes()
def test_typevar(self):
from typing import TYPE_CHECKING, TypeVar

from typing_extensions import Literal, assert_type

if TYPE_CHECKING:

T = TypeVar("T")

def capybara(x: T) -> T:
return x

assert_type(capybara(1), Literal[1])

@assert_passes()
def test_namedtuple(self):
from collections import namedtuple
from typing import TYPE_CHECKING, Any, NamedTuple

from typing_extensions import assert_type

if TYPE_CHECKING:
TypedCapybara = NamedTuple("TypedCapybara", [("x", int), ("y", str)])
UntypedCapybara = namedtuple("UntypedCapybara", ["x", "y"])

def capybara(t: "TypedCapybara", u: "UntypedCapybara") -> None:
assert_type(t.x, int)
assert_type(t.y, str)

assert_type(u.x, Any)
assert_type(u.y, Any)
15 changes: 15 additions & 0 deletions pyanalyze/test_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,3 +1445,18 @@ def test_call(self):

def capybara():
Any(42) # E: incompatible_call


class TestNamedTuple(TestNameCheckVisitorBase):
@assert_passes()
def test_namedtuple(self):
from typing import NamedTuple

def capybara() -> None:
NamedTuple("x", y=int) # E: deprecated
NamedTuple("x") # E: deprecated
NamedTuple("x", None, y=int) # E: incompatible_call
NamedTuple("x", None) # E: deprecated
NamedTuple("x", [("y", int)], z=str) # E: incompatible_call

NamedTuple("x", [("y", int)]) # ok
Loading