From f4e3b4ad9827276ff20c2c3a86e0637a37b09019 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 14:55:42 -0700 Subject: [PATCH] Drop Python 3.7 --- changelog/11151.breaking.rst | 2 + doc/en/conf.py | 21 --------- setup.cfg | 4 +- src/_pytest/_code/code.py | 2 +- src/_pytest/_code/source.py | 3 +- src/_pytest/_io/terminalwriter.py | 2 +- src/_pytest/assertion/rewrite.py | 66 ++++++++++++----------------- src/_pytest/cacheprovider.py | 2 +- src/_pytest/capture.py | 2 +- src/_pytest/compat.py | 63 --------------------------- src/_pytest/config/__init__.py | 9 ++-- src/_pytest/config/argparsing.py | 2 +- src/_pytest/config/exceptions.py | 2 +- src/_pytest/fixtures.py | 11 ++--- src/_pytest/legacypath.py | 2 +- src/_pytest/logging.py | 2 +- src/_pytest/main.py | 4 +- src/_pytest/mark/expression.py | 9 +--- src/_pytest/mark/structures.py | 2 +- src/_pytest/monkeypatch.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/outcomes.py | 13 +----- src/_pytest/pytester.py | 2 +- src/_pytest/python.py | 2 +- src/_pytest/python_api.py | 13 +++--- src/_pytest/recwarn.py | 4 +- src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 2 +- src/_pytest/terminal.py | 2 +- src/_pytest/tmpdir.py | 24 ++++------- src/_pytest/warning_types.py | 3 +- testing/acceptance_test.py | 4 +- testing/code/test_source.py | 9 +--- testing/python/metafunc.py | 22 ---------- testing/test_assertion.py | 4 +- testing/test_assertrewrite.py | 17 +------- testing/test_compat.py | 2 +- testing/test_config.py | 14 +++--- testing/test_entry_points.py | 4 +- testing/test_skipping.py | 6 +-- testing/test_threadexception.py | 6 --- testing/test_unittest.py | 9 ---- testing/test_unraisableexception.py | 6 --- 43 files changed, 99 insertions(+), 285 deletions(-) create mode 100644 changelog/11151.breaking.rst diff --git a/changelog/11151.breaking.rst b/changelog/11151.breaking.rst new file mode 100644 index 000000000..d846fc94c --- /dev/null +++ b/changelog/11151.breaking.rst @@ -0,0 +1,2 @@ +Drop support for Python 3.7, which `reached end-of-life on 2023-06-27 +`__. diff --git a/doc/en/conf.py b/doc/en/conf.py index 32f508219..92607a15a 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -15,12 +15,10 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -import ast import os import shutil import sys from textwrap import dedent -from typing import List from typing import TYPE_CHECKING from _pytest import __version__ as version @@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None: configure_logging(app) - # Make Sphinx mark classes with "final" when decorated with @final. - # We need this because we import final from pytest._compat, not from - # typing (for Python < 3.8 compat), so Sphinx doesn't detect it. - # To keep things simple we accept any `@final` decorator. - # Ref: https://github.com/pytest-dev/pytest/pull/7780 - import sphinx.pycode.ast - import sphinx.pycode.parser - - original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final - - def patched_is_final(self, decorators: List[ast.expr]) -> bool: - if original_is_final(self, decorators): - return True - return any( - sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators - ) - - sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final - # legacypath.py monkey-patches pytest.Testdir in. Import the file so # that autodoc can discover references to it. import _pytest.legacypath # noqa: F401 diff --git a/setup.cfg b/setup.cfg index bc93b1c06..f80665f42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,6 @@ classifiers = Operating System :: POSIX Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -50,9 +49,8 @@ install_requires = pluggy>=0.12,<2.0 colorama;sys_platform=="win32" exceptiongroup>=1.0.0rc8;python_version<"3.11" - importlib-metadata>=0.12;python_version<"3.8" tomli>=1.0.0;python_version<"3.11" -python_requires = >=3.7 +python_requires = >=3.8 package_dir = =src setup_requires = diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 9b051332b..ddc03d752 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -17,6 +17,7 @@ from typing import Any from typing import Callable from typing import ClassVar from typing import Dict +from typing import final from typing import Generic from typing import Iterable from typing import List @@ -42,7 +43,6 @@ from _pytest._code.source import Source from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.deprecated import check_ispytest from _pytest.pathlib import absolutepath diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 208cfb800..cc7ac407e 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i values: List[int] = [] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): - # Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. - # Since Python 3.8, the lineno points to the class/def, so need to include the decorators. + # The lineno points to the class/def, so need to include the decorators. if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): for d in x.decorator_list: values.append(d.lineno - 1) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 379035d85..eb1b46939 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -2,12 +2,12 @@ import os import shutil import sys +from typing import final from typing import Optional from typing import Sequence from typing import TextIO from .wcwidth import wcswidth -from _pytest.compat import final # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ab83fee32..157903faf 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -44,17 +44,6 @@ from _pytest.stash import StashKey if TYPE_CHECKING: from _pytest.assertion import AssertionState -if sys.version_info >= (3, 8): - namedExpr = ast.NamedExpr - astNameConstant = ast.Constant - astStr = ast.Constant - astNum = ast.Constant -else: - namedExpr = ast.Expr - astNameConstant = ast.NameConstant - astStr = ast.Str - astNum = ast.Num - assertstate_key = StashKey["AssertionState"]() @@ -686,12 +675,9 @@ class AssertionRewriter(ast.NodeVisitor): if ( expect_docstring and isinstance(item, ast.Expr) - and isinstance(item.value, astStr) + and isinstance(item.value, ast.Constant) ): - if sys.version_info >= (3, 8): - doc = item.value.value - else: - doc = item.value.s + doc = item.value.value if self.is_rewrite_disabled(doc): return expect_docstring = False @@ -823,7 +809,7 @@ class AssertionRewriter(ast.NodeVisitor): current = self.stack.pop() if self.stack: self.explanation_specifiers = self.stack[-1] - keys = [astStr(key) for key in current.keys()] + keys = [ast.Constant(key) for key in current.keys()] format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) @@ -877,16 +863,16 @@ class AssertionRewriter(ast.NodeVisitor): negation = ast.UnaryOp(ast.Not(), top_condition) if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - msg = self.pop_format_context(astStr(explanation)) + msg = self.pop_format_context(ast.Constant(explanation)) # Failed if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) gluestr = "\n>assert " else: - assertmsg = astStr("") + assertmsg = ast.Constant("") gluestr = "assert " - err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg) + err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_name = ast.Name("AssertionError", ast.Load()) fmt = self.helper("_format_explanation", err_msg) @@ -902,8 +888,8 @@ class AssertionRewriter(ast.NodeVisitor): hook_call_pass = ast.Expr( self.helper( "_call_assertion_pass", - astNum(assert_.lineno), - astStr(orig), + ast.Constant(assert_.lineno), + ast.Constant(orig), fmt_pass, ) ) @@ -922,7 +908,7 @@ class AssertionRewriter(ast.NodeVisitor): variables = [ ast.Name(name, ast.Store()) for name in self.format_variables ] - clear_format = ast.Assign(variables, astNameConstant(None)) + clear_format = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear_format) else: # Original assertion rewriting @@ -933,9 +919,9 @@ class AssertionRewriter(ast.NodeVisitor): assertmsg = self.helper("_format_assertmsg", assert_.msg) explanation = "\n>assert " + explanation else: - assertmsg = astStr("") + assertmsg = ast.Constant("") explanation = "assert " + explanation - template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation)) + template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation)) msg = self.pop_format_context(template) fmt = self.helper("_format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) @@ -947,7 +933,7 @@ class AssertionRewriter(ast.NodeVisitor): # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] - clear = ast.Assign(variables, astNameConstant(None)) + clear = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear) # Fix locations (line numbers/column offsets). for stmt in self.statements: @@ -955,26 +941,26 @@ class AssertionRewriter(ast.NodeVisitor): ast.copy_location(node, assert_) return self.statements - def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]: + def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]: # This method handles the 'walrus operator' repr of the target # name if it's a local variable or _should_repr_global_name() # thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) target_id = name.target.id # type: ignore[attr-defined] - inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), astStr(target_id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(target_id)) return name, self.explanation_param(expr) def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: # Display the repr of the name if it's a local variable or # _should_repr_global_name() thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) - inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), astStr(name.id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(name.id)) return name, self.explanation_param(expr) def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: @@ -993,10 +979,10 @@ class AssertionRewriter(ast.NodeVisitor): # cond is set in a prior loop iteration below self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts = fail_inner - # Check if the left operand is a namedExpr and the value has already been visited + # Check if the left operand is a ast.NamedExpr and the value has already been visited if ( isinstance(v, ast.Compare) - and isinstance(v.left, namedExpr) + and isinstance(v.left, ast.NamedExpr) and v.left.target.id in [ ast_expr.id @@ -1012,7 +998,7 @@ class AssertionRewriter(ast.NodeVisitor): self.push_format_context() res, expl = self.visit(v) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) - expl_format = self.pop_format_context(astStr(expl)) + expl_format = self.pop_format_context(ast.Constant(expl)) call = ast.Call(app, [expl_format], []) self.expl_stmts.append(ast.Expr(call)) if i < levels: @@ -1024,7 +1010,7 @@ class AssertionRewriter(ast.NodeVisitor): self.statements = body = inner self.statements = save self.expl_stmts = fail_save - expl_template = self.helper("_format_boolop", expl_list, astNum(is_or)) + expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or)) expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) @@ -1098,7 +1084,7 @@ class AssertionRewriter(ast.NodeVisitor): comp.left = self.variables_overwrite[ comp.left.id ] # type:ignore[assignment] - if isinstance(comp.left, namedExpr): + if isinstance(comp.left, ast.NamedExpr): self.variables_overwrite[ comp.left.target.id ] = comp.left # type:ignore[assignment] @@ -1114,7 +1100,7 @@ class AssertionRewriter(ast.NodeVisitor): results = [left_res] for i, op, next_operand in it: if ( - isinstance(next_operand, namedExpr) + isinstance(next_operand, ast.NamedExpr) and isinstance(left_res, ast.Name) and next_operand.target.id == left_res.id ): @@ -1127,9 +1113,9 @@ class AssertionRewriter(ast.NodeVisitor): next_expl = f"({next_expl})" results.append(next_res) sym = BINOP_MAP[op.__class__] - syms.append(astStr(sym)) + syms.append(ast.Constant(sym)) expl = f"{left_expl} {sym} {next_expl}" - expls.append(astStr(expl)) + expls.append(ast.Constant(expl)) res_expr = ast.Compare(left_res, [op], [next_res]) self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl @@ -1173,7 +1159,7 @@ def try_makedirs(cache_dir: Path) -> bool: def get_cache_dir(file_path: Path) -> Path: """Return the cache directory to write .pyc files for the given .py file path.""" - if sys.version_info >= (3, 8) and sys.pycache_prefix: + if sys.pycache_prefix: # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 855716d81..a0029d6a0 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,6 +6,7 @@ import json import os from pathlib import Path from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import List @@ -18,7 +19,6 @@ from .pathlib import rm_rf from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index a8ca0869f..3e31e3cb7 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -11,6 +11,7 @@ from types import TracebackType from typing import Any from typing import AnyStr from typing import BinaryIO +from typing import final from typing import Generator from typing import Generic from typing import Iterable @@ -24,7 +25,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union -from _pytest.compat import final from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 352211de8..4736bf40d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -12,23 +12,12 @@ from inspect import signature from pathlib import Path from typing import Any from typing import Callable -from typing import Generic from typing import NoReturn from typing import TYPE_CHECKING from typing import TypeVar import py -# fmt: off -# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351. -# If `overload` is imported from `compat` instead of from `typing`, -# Sphinx doesn't recognize it as `overload` and the API docs for -# overloaded functions look good again. But type checkers handle -# it fine. -# fmt: on -if True: - from typing import overload as overload - if TYPE_CHECKING: from typing_extensions import Final @@ -58,17 +47,6 @@ class NotSetType(enum.Enum): NOTSET: Final = NotSetType.token # noqa: E305 # fmt: on -if sys.version_info >= (3, 8): - import importlib.metadata - - importlib_metadata = importlib.metadata -else: - import importlib_metadata as importlib_metadata # noqa: F401 - - -def _format_args(func: Callable[..., Any]) -> str: - return str(signature(func)) - def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) @@ -338,47 +316,6 @@ def safe_isclass(obj: object) -> bool: return False -if TYPE_CHECKING: - if sys.version_info >= (3, 8): - from typing import final as final - else: - from typing_extensions import final as final -elif sys.version_info >= (3, 8): - from typing import final as final -else: - - def final(f): - return f - - -if sys.version_info >= (3, 8): - from functools import cached_property as cached_property -else: - - class cached_property(Generic[_S, _T]): - __slots__ = ("func", "__doc__") - - def __init__(self, func: Callable[[_S], _T]) -> None: - self.func = func - self.__doc__ = func.__doc__ - - @overload - def __get__( - self, instance: None, owner: type[_S] | None = ... - ) -> cached_property[_S, _T]: - ... - - @overload - def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T: - ... - - def __get__(self, instance, owner=None): - if instance is None: - return self - value = instance.__dict__[self.func.__name__] = self.func(instance) - return value - - def get_user_id() -> int | None: """Return the current user id, or None if we cannot get it reliably on the current platform.""" # win32 does not have a getuid() function. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c9a4b7f63..47651ad9f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -5,6 +5,7 @@ import copy import dataclasses import enum import glob +import importlib.metadata import inspect import os import re @@ -21,6 +22,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generator from typing import IO from typing import Iterable @@ -48,8 +50,6 @@ from .findpaths import determine_setup from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter -from _pytest.compat import final -from _pytest.compat import importlib_metadata # type: ignore[attr-defined] from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath @@ -257,7 +257,8 @@ default_plugins = essential_plugins + ( "logging", "reports", "python_path", - *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), + "unraisableexception", + "threadexception", "faulthandler", ) @@ -1216,7 +1217,7 @@ class Config: package_files = ( str(file) - for dist in importlib_metadata.distributions() + for dist in importlib.metadata.distributions() if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] ) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d3f01916b..108349f10 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -7,6 +7,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import List from typing import Mapping from typing import NoReturn @@ -17,7 +18,6 @@ from typing import TYPE_CHECKING from typing import Union import _pytest._io -from _pytest.compat import final from _pytest.config.exceptions import UsageError from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT from _pytest.deprecated import ARGUMENT_TYPE_STR diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 4f1320e75..4031ea732 100644 --- a/src/_pytest/config/exceptions.py +++ b/src/_pytest/config/exceptions.py @@ -1,4 +1,4 @@ -from _pytest.compat import final +from typing import final @final diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 66722da27..6e1134928 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -13,6 +13,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generator from typing import Generic from typing import Iterable @@ -21,6 +22,7 @@ from typing import List from typing import MutableMapping from typing import NoReturn from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple @@ -35,10 +37,8 @@ from _pytest._code import getfslineno from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames @@ -47,7 +47,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import NotSetType -from _pytest.compat import overload from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config @@ -729,8 +728,10 @@ class FixtureRequest: p = bestrelpath(session.path, fs) else: p = fs - args = _format_args(factory) - lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) + lines.append( + "%s:%d: def %s%s" + % (p, lineno + 1, factory.__name__, inspect.signature(factory)) + ) return lines def __repr__(self) -> str: diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index af1d0c07e..7091f038a 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -3,6 +3,7 @@ import dataclasses import shlex import subprocess from pathlib import Path +from typing import final from typing import List from typing import Optional from typing import TYPE_CHECKING @@ -11,7 +12,6 @@ from typing import Union from iniconfig import SectionWrapper from _pytest.cacheprovider import Cache -from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.config import Config diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 838134660..ea856837c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -13,6 +13,7 @@ from logging import LogRecord from pathlib import Path from typing import AbstractSet from typing import Dict +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -25,7 +26,6 @@ from typing import Union from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager -from _pytest.compat import final from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 155d4300e..f2327ca68 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -9,10 +9,12 @@ import sys from pathlib import Path from typing import Callable from typing import Dict +from typing import final from typing import FrozenSet from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple @@ -22,8 +24,6 @@ from typing import Union import _pytest._code from _pytest import nodes -from _pytest.compat import final -from _pytest.compat import overload from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 9287bcee5..b995518bf 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -18,7 +18,6 @@ import ast import dataclasses import enum import re -import sys import types from typing import Callable from typing import Iterator @@ -27,12 +26,6 @@ from typing import NoReturn from typing import Optional from typing import Sequence -if sys.version_info >= (3, 8): - astNameConstant = ast.Constant -else: - astNameConstant = ast.NameConstant - - __all__ = [ "Expression", "ParseError", @@ -138,7 +131,7 @@ IDENT_PREFIX = "$" def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - ret: ast.expr = astNameConstant(False) + ret: ast.expr = ast.Constant(False) else: ret = expr(s) s.accept(TokenType.EOF, reject=True) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 406da5d9d..bc10d3b90 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -5,6 +5,7 @@ import warnings from typing import Any from typing import Callable from typing import Collection +from typing import final from typing import Iterable from typing import Iterator from typing import List @@ -23,7 +24,6 @@ from typing import Union from .._code import getfslineno from ..compat import ascii_escaped -from ..compat import final from ..compat import NOTSET from ..compat import NotSetType from _pytest.config import Config diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 9e51ff335..834700b1b 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -5,6 +5,7 @@ import sys import warnings from contextlib import contextmanager from typing import Any +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -15,7 +16,6 @@ from typing import Tuple from typing import TypeVar from typing import Union -from _pytest.compat import final from _pytest.fixtures import fixture from _pytest.warning_types import PytestWarning diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index dbd6b0a42..83180c126 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,5 +1,6 @@ import os import warnings +from functools import cached_property from inspect import signature from pathlib import Path from typing import Any @@ -23,7 +24,6 @@ from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest._code.code import Traceback -from _pytest.compat import cached_property from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 1be97dda4..7dab4499b 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -7,23 +7,12 @@ from typing import Callable from typing import cast from typing import NoReturn from typing import Optional +from typing import Protocol from typing import Type from typing import TypeVar from _pytest.deprecated import KEYWORD_MSG_ARG -TYPE_CHECKING = False # Avoid circular import through compat. - -if TYPE_CHECKING: - from typing_extensions import Protocol -else: - # typing.Protocol is only available starting from Python 3.8. It is also - # available from typing_extensions, but we don't want a runtime dependency - # on that. So use a dummy runtime implementation. - from typing import Generic - - Protocol = Generic - class OutcomeException(BaseException): """OutcomeException and its subclass instances indicate and contain info diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3df52ebe8..0634267ae 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -20,6 +20,7 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import final from typing import Generator from typing import IO from typing import Iterable @@ -40,7 +41,6 @@ from iniconfig import SectionWrapper from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture -from _pytest.compat import final from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 6ba568c0f..5f4ba3da6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -15,6 +15,7 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import Iterator @@ -40,7 +41,6 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func from _pytest.compat import getimfunc diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 4213bd098..0967ae8ec 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -9,9 +9,11 @@ from typing import Any from typing import Callable from typing import cast from typing import ContextManager +from typing import final from typing import List from typing import Mapping from typing import Optional +from typing import overload from typing import Pattern from typing import Sequence from typing import Tuple @@ -20,17 +22,14 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import _pytest._code +from _pytest.compat import STRING_TYPES +from _pytest.outcomes import fail + if TYPE_CHECKING: from numpy import ndarray -import _pytest._code -from _pytest.compat import final -from _pytest.compat import STRING_TYPES -from _pytest.compat import overload -from _pytest.outcomes import fail - - def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = f" at {at}" if at else "" return TypeError( diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index d76ea020f..b422f3627 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -5,18 +5,18 @@ from pprint import pformat from types import TracebackType from typing import Any from typing import Callable +from typing import final from typing import Generator from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Pattern from typing import Tuple from typing import Type from typing import TypeVar from typing import Union -from _pytest.compat import final -from _pytest.compat import overload from _pytest.deprecated import check_ispytest from _pytest.deprecated import WARNS_NONE_ARG from _pytest.fixtures import fixture diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 74e8794b2..0a4044ec6 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -5,6 +5,7 @@ from pprint import pprint from typing import Any from typing import cast from typing import Dict +from typing import final from typing import Iterable from typing import Iterator from typing import List @@ -29,7 +30,6 @@ from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f861c05a4..1f14ff944 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -6,6 +6,7 @@ import sys from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generic from typing import List from typing import Optional @@ -23,7 +24,6 @@ from _pytest import timing from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr -from _pytest.compat import final from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.nodes import Collector diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b0cdb58ce..089314c3e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -18,6 +18,7 @@ from typing import Callable from typing import cast from typing import ClassVar from typing import Dict +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -40,7 +41,6 @@ from _pytest._code.code import ExceptionRepr from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.assertion.util import running_on_ci -from _pytest.compat import final from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 3cc2bace5..fe0855c18 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -7,38 +7,32 @@ from pathlib import Path from shutil import rmtree from typing import Any from typing import Dict +from typing import final from typing import Generator +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING from typing import Union -from _pytest.nodes import Item -from _pytest.reports import CollectReport -from _pytest.stash import StashKey - -if TYPE_CHECKING: - from typing_extensions import Literal - - RetentionType = Literal["all", "failed", "none"] - - -from _pytest.config.argparsing import Parser - +from .pathlib import cleanup_dead_symlinks from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf -from .pathlib import cleanup_dead_symlinks -from _pytest.compat import final, get_user_id +from _pytest.compat import get_user_id from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item +from _pytest.reports import CollectReport +from _pytest.stash import StashKey tmppath_result_key = StashKey[Dict[str, bool]]() +RetentionType = Literal["all", "failed", "none"] @final diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index f6b0a3a69..31726e1ce 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -3,12 +3,11 @@ import inspect import warnings from types import FunctionType from typing import Any +from typing import final from typing import Generic from typing import Type from typing import TypeVar -from _pytest.compat import final - class PytestWarning(UserWarning): """Base class for all warnings emitted by pytest.""" diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index de9e92d00..453e26972 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,10 +1,10 @@ import dataclasses +import importlib.metadata import os import sys import types import pytest -from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester @@ -139,7 +139,7 @@ class TestGeneralUsage: def my_dists(): return (DummyDist(entry_points),) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) params = ("-p", "mycov") if load_cov_early else () pytester.runpytest_inprocess(*params) if load_cov_early: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index dc35c9496..260c80299 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -439,14 +439,9 @@ comment 4 ''' for line in range(2, 6): assert str(getstatement(line, source)) == " x = 1" - if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - tqs_start = 8 - else: - tqs_start = 10 - assert str(getstatement(10, source)) == '"""' - for line in range(6, tqs_start): + for line in range(6, 8): assert str(getstatement(line, source)) == " assert False" - for line in range(tqs_start, 10): + for line in range(8, 10): assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index a9e9b5269..08ea8f910 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -19,7 +19,6 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python -from _pytest.compat import _format_args from _pytest.compat import getfuncargnames from _pytest.compat import NOTSET from _pytest.outcomes import fail @@ -1036,27 +1035,6 @@ class TestMetafunc: """ ) - def test_format_args(self) -> None: - def function1(): - pass - - assert _format_args(function1) == "()" - - def function2(arg1): - pass - - assert _format_args(function2) == "(arg1)" - - def function3(arg1, arg2="qwe"): - pass - - assert _format_args(function3) == "(arg1, arg2='qwe')" - - def function4(arg1, *args, **kwargs): - pass - - assert _format_args(function4) == "(arg1, *args, **kwargs)" - class TestMetafuncFunctional: def test_attributes(self, pytester: Pytester) -> None: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 7119b3b5a..c04c31f31 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -199,8 +199,8 @@ class TestImportHookInstallation: return check """, "mainwrapper.py": """\ + import importlib.metadata import pytest - from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' @@ -220,7 +220,7 @@ class TestImportHookInstallation: def distributions(): return (DummyDistInfo(),) - importlib_metadata.distributions = distributions + importlib.metadata.distributions = distributions pytest.main() """, "test_foo.py": """\ diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 778f843e6..d01803204 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -131,9 +131,8 @@ class TestAssertionRewrite: for n in [node, *ast.iter_child_nodes(node)]: assert n.lineno == 3 assert n.col_offset == 0 - if sys.version_info >= (3, 8): - assert n.end_lineno == 6 - assert n.end_col_offset == 3 + assert n.end_lineno == 6 + assert n.end_col_offset == 3 def test_dont_rewrite(self) -> None: s = """'PYTEST_DONT_REWRITE'\nassert 14""" @@ -1270,9 +1269,6 @@ class TestIssue2121: result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="walrus operator not available in py<38" -) class TestIssue10743: def test_assertion_walrus_operator(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1441,9 +1437,6 @@ class TestIssue10743: assert result.ret == 0 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="walrus operator not available in py<38" -) class TestIssue11028: def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1957,16 +1950,10 @@ class TestPyCacheDir: ) def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None: monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - - if prefix is not None and sys.version_info < (3, 8): - pytest.skip("pycache_prefix not available in py<38") monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False) assert get_cache_dir(Path(source)) == Path(expected) - @pytest.mark.skipif( - sys.version_info < (3, 8), reason="pycache_prefix not available in py<38" - ) @pytest.mark.skipif( sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"), reason="#9298", diff --git a/testing/test_compat.py b/testing/test_compat.py index 8a80fd625..27c6db95b 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,5 +1,6 @@ import enum import sys +from functools import cached_property from functools import partial from functools import wraps from typing import TYPE_CHECKING @@ -8,7 +9,6 @@ from typing import Union import pytest from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr diff --git a/testing/test_config.py b/testing/test_config.py index 257d696fa..9b3fe4af0 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,4 +1,5 @@ import dataclasses +import importlib.metadata import os import re import sys @@ -13,7 +14,6 @@ from typing import Union import _pytest._code import pytest -from _pytest.compat import importlib_metadata from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool @@ -475,7 +475,7 @@ class TestParseIni: pytester.makepyfile(myplugin1_module="# my plugin module") pytester.syspathinsert() - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) pytester.makeini(ini_file_text) @@ -1003,7 +1003,7 @@ def test_preparse_ordering_with_setuptools( def my_dists(): return (Dist,) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) pytester.makeconftest( """ pytest_plugins = "mytestplugin", @@ -1036,7 +1036,7 @@ def test_setuptools_importerror_issue1479( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) with pytest.raises(ImportError): pytester.parseconfig() @@ -1063,7 +1063,7 @@ def test_importlib_metadata_broken_distribution( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) pytester.parseconfig() @@ -1091,7 +1091,7 @@ def test_plugin_preparse_prevents_setuptools_loading( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () config = pytester.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") @@ -1140,7 +1140,7 @@ def test_disable_plugin_autoload( return (Distribution(),) monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index 5d0031273..dfb3d57d2 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -1,7 +1,7 @@ -from _pytest.compat import importlib_metadata +import importlib.metadata def test_pytest_entry_points_are_identical(): - dist = importlib_metadata.distribution("pytest") + dist = importlib.metadata.distribution("pytest") entry_map = {ep.name: ep for ep in dist.entry_points} assert entry_map["pytest"].value == entry_map["py.test"].value diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 6b8034610..d8b22aa46 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1142,12 +1142,10 @@ def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None: """ ) result = pytester.runpytest() - markline = " ^" + markline = " ^" pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info is not None and pypy_version_info < (6,): - markline = markline[5:] - elif sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - markline = markline[4:] + markline = markline[1:] if sys.version_info[:2] >= (3, 10): expected = [ diff --git a/testing/test_threadexception.py b/testing/test_threadexception.py index 5b7519f27..fd9a091cc 100644 --- a/testing/test_threadexception.py +++ b/testing/test_threadexception.py @@ -1,13 +1,7 @@ -import sys - import pytest from _pytest.pytester import Pytester -if sys.version_info < (3, 8): - pytest.skip("threadexception plugin needs Python>=3.8", allow_module_level=True) - - @pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning") def test_unhandled_thread_exception(pytester: Pytester) -> None: pytester.makepyfile( diff --git a/testing/test_unittest.py b/testing/test_unittest.py index d917d331a..99a53e0a9 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1354,9 +1354,6 @@ def test_plain_unittest_does_not_support_async(pytester: Pytester) -> None: result.stdout.fnmatch_lines(expected_lines) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_success(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1382,9 +1379,6 @@ def test_do_class_cleanups_on_success(pytester: Pytester) -> None: assert passed == 3 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1409,9 +1403,6 @@ def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: assert passed == 1 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_teardownclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index f625833dc..72f620364 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -1,13 +1,7 @@ -import sys - import pytest from _pytest.pytester import Pytester -if sys.version_info < (3, 8): - pytest.skip("unraisableexception plugin needs Python>=3.8", allow_module_level=True) - - @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable(pytester: Pytester) -> None: pytester.makepyfile(