From 63a8cc4ef113423ad4f602d03cc8bcf5e22373ed Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 10 Oct 2024 18:26:40 -0700 Subject: [PATCH] Clean up Python 3.8 remnants (#822) --- pyanalyze/annotations.py | 20 ------------------- pyanalyze/extensions.py | 8 ++------ pyanalyze/name_check_visitor.py | 29 ++++++---------------------- pyanalyze/patma.py | 2 +- pyanalyze/test_annotations.py | 26 +++++++------------------ pyanalyze/test_arg_spec.py | 9 +-------- pyanalyze/test_asynq.py | 32 +++++++++++++------------------ pyanalyze/test_typeshed.py | 34 ++++++++++----------------------- pyanalyze/test_typevar.py | 1 - pyanalyze/type_evaluation.py | 8 ++------ setup.py | 1 - 11 files changed, 42 insertions(+), 128 deletions(-) diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py index f71ab76d..842a9212 100644 --- a/pyanalyze/annotations.py +++ b/pyanalyze/annotations.py @@ -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__"): @@ -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)) @@ -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: @@ -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]) @@ -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) diff --git a/pyanalyze/extensions.py b/pyanalyze/extensions.py index d47acb4a..0f3cd4ac 100644 --- a/pyanalyze/extensions.py +++ b/pyanalyze/extensions.py @@ -631,7 +631,7 @@ 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 @@ -639,9 +639,5 @@ class _EnumName: """ - # 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() diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py index 448610df..c56c5527 100644 --- a/pyanalyze/name_check_visitor.py +++ b/pyanalyze/name_check_visitor.py @@ -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 @@ -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)]) @@ -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)}", @@ -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): @@ -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, @@ -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): diff --git a/pyanalyze/patma.py b/pyanalyze/patma.py index db448e14..458ae6e6 100644 --- a/pyanalyze/patma.py +++ b/pyanalyze/patma.py @@ -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 diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py index cc88d9f2..44442a1b 100644 --- a/pyanalyze/test_annotations.py +++ b/pyanalyze/test_annotations.py @@ -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 @@ -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( """ @@ -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 @@ -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( """ @@ -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): @@ -688,7 +680,6 @@ def capybara( ), ) - @skip_before((3, 9)) @assert_passes() def test_typing(self): import collections.abc @@ -730,7 +721,6 @@ def capybara( ), ) - @skip_before((3, 9)) @assert_passes() def test_genericalias_nested_class(self): def capybara(): @@ -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 @@ -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( diff --git a/pyanalyze/test_arg_spec.py b/pyanalyze/test_arg_spec.py index a1c44a5a..7abf0bd1 100644 --- a/pyanalyze/test_arg_spec.py +++ b/pyanalyze/test_arg_spec.py @@ -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)), diff --git a/pyanalyze/test_asynq.py b/pyanalyze/test_asynq.py index 36bd147c..891a1b26 100644 --- a/pyanalyze/test_asynq.py +++ b/pyanalyze/test_asynq.py @@ -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, @@ -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 @@ -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): @@ -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) diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py index 041342e0..b86b87b2 100644 --- a/pyanalyze/test_typeshed.py +++ b/pyanalyze/test_typeshed.py @@ -3,7 +3,6 @@ import collections.abc import contextlib import io -import sys import tempfile import textwrap import time @@ -562,29 +561,16 @@ def test_dict_items(self): ) def test_struct_time(self): - if sys.version_info < (3, 9): - # Until 3.8 NamedTuple is actually a class. - expected = { - time.struct_time: [], - "_typeshed.structseq": [AnyValue(AnySource.explicit) | TypedValue(int)], - tuple: [TypedValue(int)], - collections.abc.Collection: [TypedValue(int)], - collections.abc.Reversible: [TypedValue(int)], - collections.abc.Iterable: [TypedValue(int)], - collections.abc.Sequence: [TypedValue(int)], - collections.abc.Container: [TypedValue(int)], - } - else: - expected = { - time.struct_time: [], - "_typeshed.structseq": [AnyValue(AnySource.explicit) | TypedValue(int)], - tuple: [TypedValue(int)], - collections.abc.Collection: [TypedValue(int)], - collections.abc.Reversible: [TypedValue(int)], - collections.abc.Iterable: [TypedValue(int)], - collections.abc.Sequence: [TypedValue(int)], - collections.abc.Container: [TypedValue(int)], - } + expected = { + time.struct_time: [], + "_typeshed.structseq": [AnyValue(AnySource.explicit) | TypedValue(int)], + tuple: [TypedValue(int)], + collections.abc.Collection: [TypedValue(int)], + collections.abc.Reversible: [TypedValue(int)], + collections.abc.Iterable: [TypedValue(int)], + collections.abc.Sequence: [TypedValue(int)], + collections.abc.Container: [TypedValue(int)], + } self.check(expected, time.struct_time) def test_context_manager(self): diff --git a/pyanalyze/test_typevar.py b/pyanalyze/test_typevar.py index cf32730a..d3949c65 100644 --- a/pyanalyze/test_typevar.py +++ b/pyanalyze/test_typevar.py @@ -91,7 +91,6 @@ def capybara() -> None: assert_is_value(mktemp(suffix="s"), KnownValue("s")) assert_is_value(mktemp("p", "s"), KnownValue("p") | KnownValue("s")) - @skip_before((3, 9)) @assert_passes() def test_generic_constructor(self): from typing import Generic, TypeVar diff --git a/pyanalyze/type_evaluation.py b/pyanalyze/type_evaluation.py index 009adb7e..b7bec930 100644 --- a/pyanalyze/type_evaluation.py +++ b/pyanalyze/type_evaluation.py @@ -395,10 +395,8 @@ def visit_Call(self, node: ast.Call) -> ConditionReturn: "exclude_any argument must be a literal bool", keyword.value ) else: - # Before 3.9 keyword nodes don't have a lineno - error_node = keyword return self.return_invalid( - "Invalid keyword argument to is_of_type()", error_node + "Invalid keyword argument to is_of_type()", keyword ) return self.visit_is_of_type( varname_node, typ, "is of type", exclude_any=exclude_any @@ -723,9 +721,7 @@ def visit_show_error(self, call: ast.Call) -> EvalReturn: ) return None else: - # Before 3.9 keyword nodes don't have a lineno - error_node = keyword - self.add_invalid("Invalid keyword argument to show_error()", error_node) + self.add_invalid("Invalid keyword argument to show_error()", keyword) return None self.errors.append( UserRaisedError(message, list(self.active_conditions), argument) diff --git a/setup.py b/setup.py index 845bd2b4..a11d085c 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ classifiers=[ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11",