Skip to content

Commit

Permalink
Fix creating TypeVar and others in non-executed code (#746)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Mar 10, 2024
1 parent 972a5d9 commit a0a9b70
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 3 deletions.
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

0 comments on commit a0a9b70

Please sign in to comment.