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

- Fix various issues with Python 3.13 and 3.14 support (#773)
- Improve `ParamSpec` support (#772)
- Fix handling of stub functions with positional-only parameters with
defaults (#769)
Expand Down
6 changes: 5 additions & 1 deletion pyanalyze/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,11 @@ def _get_attribute_from_mro(
try:
# Make sure to use only __annotations__ that are actually on this
# class, not ones inherited from a base class.
annotations = base_dict["__annotations__"]
# Starting in 3.10, __annotations__ is not inherited.
if sys.version_info >= (3, 10):
annotations = base_cls.__annotations__
else:
annotations = base_dict["__annotations__"]
except Exception:
pass
else:
Expand Down
3 changes: 3 additions & 0 deletions pyanalyze/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ def get_attribute_from_value(

EXCLUDED_PROTOCOL_MEMBERS = {
"__abstractmethods__",
"__annotate__",
"__annotations__",
"__dict__",
"__doc__",
Expand All @@ -447,6 +448,8 @@ def get_attribute_from_value(
"__protocol_attrs__",
"__callable_proto_members_only__",
"__non_callable_proto_members__",
"__static_attributes__",
"__firstlineno__",
}


Expand Down
9 changes: 7 additions & 2 deletions pyanalyze/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,20 @@ def post_capybara() -> Iterator[int]:

@assert_passes()
def test_contextmanager_class(self):
import sys
from typing import ContextManager

def f() -> ContextManager[int]:
raise NotImplementedError

if sys.version_info >= (3, 13):
expected_args = [TypedValue(int), TypedValue(bool) | KnownValue(None)]
else:
expected_args = [TypedValue(int)]

def capybara():
assert_is_value(
f(),
GenericValue("contextlib.AbstractContextManager", [TypedValue(int)]),
f(), GenericValue("contextlib.AbstractContextManager", expected_args)
)
with f() as x:
assert_is_value(x, TypedValue(int))
Expand Down
20 changes: 18 additions & 2 deletions pyanalyze/test_implementation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# static analysis: ignore
from .test_name_check_visitor import TestNameCheckVisitorBase
from .test_node_visitor import assert_passes
from .test_node_visitor import assert_passes, only_before, skip_before
from .tests import make_simple_sequence
from .value import (
NO_RETURN_VALUE,
Expand Down Expand Up @@ -1455,8 +1455,24 @@ def test_namedtuple(self):
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

@only_before((3, 13))
@assert_passes()
def test_namedtuple_before_3_13(self):
from typing import NamedTuple

def capybara() -> None:
NamedTuple("x", None, y=int) # E: incompatible_call

@skip_before((3, 13))
@assert_passes()
def test_namedtuple_after_3_13(self):
from typing import NamedTuple

def capybara() -> None:
# on 3.13+ we get a second error from calling the runtime
NamedTuple("x", None, y=int) # E: incompatible_call # E: incompatible_call
22 changes: 5 additions & 17 deletions pyanalyze/test_type_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ def capybara():

@assert_passes()
def test_protocol_inheritance(self):
import cgi
import operator

# cgi.parse requires SupportsItemAccess[str, str]
# operator.getitem requires SupportsGetItem[K, V]

class Good:
def __contains__(self, obj: object) -> bool:
Expand All @@ -177,29 +177,17 @@ def __contains__(self, obj: object) -> bool:
def __getitem__(self, k: str) -> str:
raise KeyError(k)

def __setitem__(self, k: str, v: str) -> None:
pass

def __delitem__(self, v: str) -> None:
pass

class Bad:
def __contains__(self, obj: object) -> bool:
return False

def __getitem__(self, k: bytes) -> str:
raise KeyError(k)

def __setitem__(self, k: str, v: str) -> None:
pass

def __delitem__(self, v: str) -> None:
pass

def capybara():
cgi.parse(environ=Good())
cgi.parse(environ=Bad()) # E: incompatible_argument
cgi.parse(environ=1) # E: incompatible_argument
operator.getitem(Good(), "hello")
operator.getitem(Bad(), "hello") # E: incompatible_call
operator.getitem(1, "hello") # E: incompatible_argument

@assert_passes()
def test_iterable(self):
Expand Down