Skip to content

Commit

Permalink
Clean up Python 3.8 remnants (#822)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Oct 11, 2024
1 parent 457cb55 commit 63a8cc4
Show file tree
Hide file tree
Showing 11 changed files with 42 additions and 128 deletions.
20 changes: 0 additions & 20 deletions pyanalyze/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,6 @@ def _type_from_runtime(
elif is_instance_of_typing_name(val, "_TypedDictMeta"):
required_keys = getattr(val, "__required_keys__", None)
readonly_keys = getattr(val, "__readonly_keys__", None)
# 3.8's typing.TypedDict doesn't have __required_keys__. With
# inheritance, this makes it apparently impossible to figure out which
# keys are required at runtime.
total = getattr(val, "__total__", True)
extra_keys = None
if hasattr(val, "__extra_keys__"):
Expand All @@ -447,14 +444,7 @@ def _type_from_runtime(
extra_keys=extra_keys,
extra_keys_readonly=extra_readonly,
)
elif val is InitVar:
# On 3.6 and 3.7, InitVar[T] just returns InitVar at runtime, so we can't
# get the actual type out.
return AnyValue(AnySource.inference)
elif isinstance(val, InitVar):
# val.type exists only on 3.8+, but on earlier versions
# InitVar instances aren't being created
# static analysis: ignore[undefined_attribute]
return type_from_runtime(val.type)
elif val is AsynqCallable:
return CallableValue(Signature.make([ELLIPSIS_PARAM], is_asynq=True))
Expand Down Expand Up @@ -759,10 +749,6 @@ def _type_from_subscripted_value(
if root is typing.Union:
return unite_values(*[_type_from_value(elt, ctx) for elt in members])
elif is_typing_name(root, "Literal"):
# Note that in Python 3.8, the way typing's internal cache works means that
# Literal[1] and Literal[True] are cached to the same value, so if you use
# both, you'll get whichever one was used first in later calls. There's nothing
# we can do about that.
if all(isinstance(elt, KnownValue) for elt in members):
return unite_values(*members)
else:
Expand Down Expand Up @@ -860,8 +846,6 @@ def _type_from_subscripted_value(
elif isinstance(root, type):
return GenericValue(root, [_type_from_value(elt, ctx) for elt in members])
else:
# In Python 3.9, generics are implemented differently and typing.get_origin
# can help.
origin = get_origin(root)
if isinstance(origin, type):
return GenericValue(origin, [_type_from_value(elt, ctx) for elt in members])
Expand Down Expand Up @@ -1014,10 +998,6 @@ def visit_Dict(self, node: ast.Dict) -> Any:
kvpairs.append(KVPair(key, value))
return DictIncompleteValue(dict, kvpairs)

def visit_Index(self, node: ast.Index) -> Value:
# class is unused in 3.9
return self.visit(node.value) # static analysis: ignore[undefined_attribute]

def visit_Constant(self, node: ast.Constant) -> Value:
return KnownValue(node.value)

Expand Down
8 changes: 2 additions & 6 deletions pyanalyze/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,17 +631,13 @@ def decorator(cls: _T) -> _T:
return decorator


class _EnumName:
class EnumName:
"""A type representing the names of members of an enum.
Equivalent to a Literal type, but using this will produce nicer error messages
for users.
"""

# TODO after dropping 3.8: switch to a single class with __class_getitem__
def __getitem__(self, enum_cls: type[enum.Enum]) -> Any:
def __class_getitem__(self, enum_cls: type[enum.Enum]) -> Any:
return Annotated[str, pyanalyze.annotated_types.EnumName(enum_cls)]


EnumName = _EnumName()
29 changes: 6 additions & 23 deletions pyanalyze/name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from dataclasses import dataclass
from itertools import chain
from pathlib import Path
from types import GenericAlias
from typing import Annotated, Any, Callable, ClassVar, Optional, TypeVar, Union
from unittest.mock import ANY

Expand Down Expand Up @@ -220,13 +221,6 @@
TryNode = ast.Try


try:
from types import GenericAlias
except ImportError:
# 3.8 and lower
GenericAlias = None


T = TypeVar("T")
U = TypeVar("U")
AwaitableValue = GenericValue(collections.abc.Awaitable, [TypeVarValue(T)])
Expand Down Expand Up @@ -2514,18 +2508,11 @@ def check_for_missing_generic_params(self, node: ast.AST, value: Value) -> None:
if val is tuple and value.val is not tuple:
# tuple[()]
return
if GenericAlias is not None and isinstance(val, GenericAlias):
if isinstance(val, GenericAlias):
return
generic_params = self.arg_spec_cache.get_type_parameters(val)
if not generic_params:
return
# On 3.8, ast.Index has no lineno
if sys.version_info < (3, 9) and not hasattr(node, "lineno"):
if isinstance(node, ast.Index):
node = node.value
else:
# Slice or ExtSlice, shouldn't happen
return
self.show_error(
node,
f"Missing type parameters for generic type {stringify_object(value.val)}",
Expand Down Expand Up @@ -4993,8 +4980,8 @@ def _composite_from_subscript_no_mvv(
)
return self.being_assigned
elif isinstance(node.ctx, ast.Load):
if sys.version_info >= (3, 9) and value == KnownValue(type):
# In Python 3.9+ "type[int]" is legal, but neither
if value == KnownValue(type):
# "type[int]" is legal, but neither
# type.__getitem__ nor type.__class_getitem__ exists at runtime. Support
# it directly instead.
if isinstance(index, KnownValue):
Expand Down Expand Up @@ -5029,9 +5016,7 @@ def _composite_from_subscript_no_mvv(
)
# Special case to avoid "Unrecognized annotation types.GenericAlias" later;
# ideally we'd be more precise.
if GenericAlias is not None and return_value == TypedValue(
GenericAlias
):
if return_value == TypedValue(GenericAlias):
return_value = self.check_call(
node.value,
cgi,
Expand Down Expand Up @@ -5398,9 +5383,7 @@ def composite_from_node(self, node: ast.AST) -> Composite:
composite = self.composite_from_name(node)
elif isinstance(node, ast.Subscript):
composite = self.composite_from_subscript(node)
elif sys.version_info < (3, 9) and isinstance(node, ast.Index):
composite = self.composite_from_node(node.value)
elif isinstance(node, (ast.ExtSlice, ast.Slice)):
elif isinstance(node, ast.Slice):
# These don't have a .lineno attribute, which would otherwise cause trouble.
composite = Composite(self.visit(node), None, None)
elif isinstance(node, ast.NamedExpr):
Expand Down
2 changes: 1 addition & 1 deletion pyanalyze/patma.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
MatchAs = MatchClass = MatchMapping = Any
MatchOr = MatchSequence = MatchSingleton = MatchValue = Any

# Avoid false positive errors on isinstance() in 3.8/3.9 self check
# Avoid false positive errors on isinstance() in 3.9 self check
class MatchStar(ast.AST):
pass

Expand Down
26 changes: 7 additions & 19 deletions pyanalyze/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,6 @@ def capybara(
assert_is_value(omega, t_str_int | KnownValue(None))
assert_is_value(empty, SequenceValue(tuple, []))

@skip_before((3, 9))
@assert_passes()
def test_builtin_tuples(self):
from collections.abc import Iterable
Expand Down Expand Up @@ -412,7 +411,6 @@ def capybara(
for elt in returner():
assert_is_value(elt, t_str_int)

@skip_before((3, 9))
def test_builtin_tuples_string(self):
self.assert_passes(
"""
Expand Down Expand Up @@ -536,14 +534,12 @@ def capybara(x: Type[str], y: "Type[int]"):
assert_is_value(x, SubclassValue(TypedValue(str)))
assert_is_value(y, SubclassValue(TypedValue(int)))

@skip_before((3, 9))
@assert_passes()
def test_lowercase_type(self):
def capybara(x: type[str], y: "type[int]"):
assert_is_value(x, SubclassValue(TypedValue(str)))
assert_is_value(y, SubclassValue(TypedValue(int)))

@skip_before((3, 9))
@assert_passes()
def test_generic_alias(self):
from queue import Queue
Expand All @@ -564,7 +560,6 @@ def capybara(x: list[int], y: tuple[int, str], z: tuple[int, ...]) -> None:
)
assert_is_value(z, GenericValue(tuple, [TypedValue(int)]))

@skip_before((3, 9))
def test_pep604(self):
self.assert_passes(
"""
Expand All @@ -581,18 +576,15 @@ def caller():
)

@skip_before((3, 10))
@assert_passes()
def test_pep604_runtime(self):
self.assert_passes(
"""
def capybara(x: int | None, y: int | str) -> None:
assert_is_value(x, MultiValuedValue([TypedValue(int), KnownValue(None)]))
assert_is_value(y, MultiValuedValue([TypedValue(int), TypedValue(str)]))
def capybara(x: int | None, y: int | str) -> None:
assert_is_value(x, MultiValuedValue([TypedValue(int), KnownValue(None)]))
assert_is_value(y, MultiValuedValue([TypedValue(int), TypedValue(str)]))

def caller():
capybara(1, 2)
capybara(None, "x")
"""
)
def caller():
capybara(1, 2)
capybara(None, "x")

@assert_passes()
def test_stringified_ops(self):
Expand Down Expand Up @@ -688,7 +680,6 @@ def capybara(
),
)

@skip_before((3, 9))
@assert_passes()
def test_typing(self):
import collections.abc
Expand Down Expand Up @@ -730,7 +721,6 @@ def capybara(
),
)

@skip_before((3, 9))
@assert_passes()
def test_genericalias_nested_class(self):
def capybara():
Expand Down Expand Up @@ -785,7 +775,6 @@ def capybara(
assert_is_value(takes_seq([int("1")]), TypedValue(int))
assert_is_value(two_args(1, "x"), TypedValue(float))

@skip_before((3, 9))
@assert_passes()
def test_abc_callable(self):
from collections.abc import Callable, Sequence
Expand Down Expand Up @@ -1961,7 +1950,6 @@ def capybara(
) -> set: # E: missing_generic_parameters
return {1}

@skip_before((3, 9))
@assert_passes()
def test_with_pep_585(self):
def capybara(
Expand Down
9 changes: 1 addition & 8 deletions pyanalyze/test_arg_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,17 +210,10 @@ def test_get_argspec():
Composite(KnownValue(ClassWithCall)),
) == asc.get_argspec(ClassWithCall.pure_async_classmethod)

# This behaves differently in 3.9 (decorator) than in 3.6-3.8 (__func__). Not
# sure why.
if hasattr(ClassWithCall.classmethod_before_async, "decorator"):
callable = ClassWithCall.classmethod_before_async.decorator.fn
else:
callable = ClassWithCall.classmethod_before_async.__func__.fn

assert BoundMethodSignature(
Signature.make(
[SigParameter("cls"), SigParameter("ac")],
callable=callable,
callable=ClassWithCall.classmethod_before_async.decorator.fn,
is_asynq=True,
),
Composite(KnownValue(ClassWithCall)),
Expand Down
32 changes: 13 additions & 19 deletions pyanalyze/test_asynq.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# static analysis: ignore
from .error_code import ErrorCode
from .implementation import assert_is_value
from .test_name_check_visitor import TestNameCheckVisitorBase
from .test_node_visitor import assert_fails, assert_passes
from .test_node_visitor import assert_passes
from .tests import make_simple_sequence
from .value import (
AnySource,
Expand Down Expand Up @@ -106,17 +105,15 @@ def caller(ints: Sequence[Literal[0, 1, 2]]):


class TestTaskNeedsYield(TestNameCheckVisitorBase):
# couldn't change assert_fails to assert_passes for
# constfuture, async, and yielded because changes between Python 3.7 and 3.8
@assert_fails(ErrorCode.task_needs_yield)
@assert_passes()
def test_constfuture(self):
from asynq import ConstFuture, asynq

@asynq()
def bad_async_fn():
def bad_async_fn(): # E: task_needs_yield
return ConstFuture(3)

@assert_fails(ErrorCode.task_needs_yield)
@assert_passes()
def test_async(self):
from asynq import asynq

Expand All @@ -125,17 +122,17 @@ def async_fn():
pass

@asynq()
def bad_async_fn():
def bad_async_fn(): # E: task_needs_yield
return async_fn.asynq()

@assert_fails(ErrorCode.task_needs_yield)
@assert_passes()
def test_not_yielded(self):
from asynq import asynq

from pyanalyze.tests import async_fn

@asynq()
def capybara(oid):
def capybara(oid): # E: task_needs_yield
return async_fn.asynq(oid)

def test_not_yielded_replacement(self):
Expand Down Expand Up @@ -205,23 +202,20 @@ def capybara(oid):
AsyncTaskIncompleteValue(FutureBase, AnyValue(AnySource.unannotated)),
)

# Can't use assert_passes for those two because the location of the error
# changes between 3.7 and 3.8. Maybe we should hack the error code to
# always show the error for a function on the def line, not the decorator line.
@assert_fails(ErrorCode.missing_return)
@assert_passes()
def test_asynq_missing_return(self):
from asynq import asynq

@asynq() # E: missing_return
def f() -> int:
@asynq()
def f() -> int: # E: missing_return
yield f.asynq()

@assert_fails(ErrorCode.missing_return)
@assert_passes()
def test_asynq_missing_branch(self):
from asynq import asynq

@asynq() # E: missing_return
def capybara(cond: bool) -> int:
@asynq()
def capybara(cond: bool) -> int: # E: missing_return
if cond:
return 3
yield capybara.asynq(False)
Expand Down
Loading

0 comments on commit 63a8cc4

Please sign in to comment.