diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4f4cdb6c564..43e5d69cb1c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -33,3 +33,5 @@ c9df77cbd6a365dcb73c39618e4842711817e871 4588653b2497ed25976b7aaff225b889fb476756 # Use format specifiers instead of percent format 4788165e69d08e10fc6b9c0124083fb358e2e9b0 +# pyproject-fmt big upgrade from 2.12 to 2.16 +690c2dc208704d34117fea8600f341d49f9abef1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d53253810c5..f34003a29e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ minimum_pre_commit_version: "4.4.0" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.14.14" + rev: "v0.15.0" hooks: - id: ruff-check args: ["--fix"] @@ -68,7 +68,7 @@ repos: # Manual because passing pyright is a work in progress. stages: [manual] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.12.1" + rev: "v2.16.0" hooks: - id: pyproject-fmt # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version diff --git a/pyproject.toml b/pyproject.toml index 7ae76a3c8dd..3e9cbf04be3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,11 +72,11 @@ urls.Tracker = "https://github.com/pytest-dev/pytest/issues" scripts."py.test" = "pytest:console_main" scripts.pytest = "pytest:console_main" -[tool.setuptools.package-data] -"_pytest" = [ +[tool.setuptools] +package-data."_pytest" = [ "py.typed", ] -"pytest" = [ +package-data."pytest" = [ "py.typed", ] @@ -116,31 +116,31 @@ lint.select = [ ] lint.ignore = [ # bugbear ignore - "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. - "B007", # Loop control variable `i` not used within loop body - "B009", # Do not call `getattr` with a constant attribute value - "B010", # [*] Do not call `setattr` with a constant attribute value. - "B011", # Do not `assert False` (`python -O` removes these calls) - "B028", # No explicit `stacklevel` keyword argument found + "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. + "B007", # Loop control variable `i` not used within loop body + "B009", # Do not call `getattr` with a constant attribute value + "B010", # [*] Do not call `setattr` with a constant attribute value. + "B011", # Do not `assert False` (`python -O` removes these calls) + "B028", # No explicit `stacklevel` keyword argument found # pydocstyle ignore - "D100", # Missing docstring in public module - "D101", # Missing docstring in public class - "D102", # Missing docstring in public method - "D103", # Missing docstring in public function - "D104", # Missing docstring in public package - "D105", # Missing docstring in magic method - "D106", # Missing docstring in public nested class - "D107", # Missing docstring in `__init__` - "D205", # 1 blank line required between summary line and description - "D209", # [*] Multi-line docstring closing quotes should be on a separate line - "D400", # First line should end with a period - "D401", # First line of docstring should be in imperative mood - "D402", # First line should not be the function's signature - "D404", # First word of the docstring should not be "This" - "D415", # First line should end with a period, question mark, or exclamation point + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D205", # 1 blank line required between summary line and description + "D209", # [*] Multi-line docstring closing quotes should be on a separate line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D402", # First line should not be the function's signature + "D404", # First word of the docstring should not be "This" + "D415", # First line should end with a period, question mark, or exclamation point # pytest can do weird low-level things, and we usually know # what we're doing when we use type(..) is ... - "E721", # Do not compare types, use `isinstance()` + "E721", # Do not compare types, use `isinstance()` # pylint ignore "PLC0105", # `TypeVar` name "E" does not reflect its covariance; "PLC0414", # Import alias does not rename original package @@ -160,7 +160,8 @@ lint.ignore = [ "PLW1641", # Does not implement the __hash__ method "PLW2901", # for loop variable overwritten by assignment target # ruff ignore - "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "RUF061", # Use context-manager form of `pytest.raises()` ] lint.per-file-ignores."src/_pytest/_py/**/*.py" = [ "B", @@ -169,6 +170,7 @@ lint.per-file-ignores."src/_pytest/_py/**/*.py" = [ lint.per-file-ignores."src/_pytest/_version.py" = [ "I001", ] +lint.per-file-ignores."testing/**/*.py" = [ "RUF060" ] # can't be disabled on a line-by-line basis in file lint.per-file-ignores."testing/code/test_source.py" = [ "F841", @@ -197,10 +199,10 @@ lint.pycodestyle.max-line-length = 120 lint.pydocstyle.convention = "pep257" lint.pyupgrade.keep-runtime-typing = false -[tool.pylint.main] +[tool.pylint] # Maximum number of characters on a single line. -max-line-length = 120 -disable = [ +main.max-line-length = 120 +main.disable = [ "abstract-method", "arguments-differ", "arguments-renamed", @@ -217,31 +219,30 @@ disable = [ "comparison-with-callable", "comparison-with-itself", # PLR0124 from ruff "condition-evals-to-constant", - "consider-alternative-union-syntax", "confusing-consecutive-elif", + "consider-alternative-union-syntax", + "consider-ternary-expression", "consider-using-assignment-expr", "consider-using-dict-items", - "consider-using-from-import", + "consider-using-from-import", # not activated by default, PLR0402 disabled in ruff "consider-using-f-string", "consider-using-in", "consider-using-namedtuple-or-dataclass", "consider-using-ternary", "consider-using-tuple", "consider-using-with", - "consider-using-from-import", # not activated by default, PLR0402 disabled in ruff - "consider-ternary-expression", "cyclic-import", - "differing-param-doc", - "docstring-first-line-empty", "deprecated-argument", "deprecated-attribute", "deprecated-class", + "differing-param-doc", "disallowed-name", # foo / bar are used often in tests + "docstring-first-line-empty", "duplicate-code", "else-if-used", # not activated by default, PLR5501 disabled in ruff "empty-comment", # not activated by default, PLR2044 disabled in ruff - "eval-used", "eq-without-hash", # PLW1641 disabled in ruff + "eval-used", "exec-used", "expression-not-assigned", "fixme", @@ -258,13 +259,13 @@ disable = [ "line-too-long", "magic-value-comparison", # not activated by default, PLR2004 disabled in ruff "method-hidden", + "misplaced-bare-raise", # PLE0704 from ruff + "misplaced-comparison-constant", "missing-docstring", "missing-param-doc", "missing-raises-doc", "missing-timeout", "missing-type-doc", - "misplaced-bare-raise", # PLE0704 from ruff - "misplaced-comparison-constant", "multiple-statements", # multiple-statements-on-one-line-colon (E701) from ruff "no-else-break", "no-else-continue", @@ -329,10 +330,10 @@ disable = [ "use-dict-literal", "use-implicit-booleaness-not-comparison", "use-implicit-booleaness-not-len", - "use-set-for-membership", "useless-else-on-loop", # PLC0414 disabled in ruff "useless-import-alias", "useless-return", + "use-set-for-membership", "using-constant-test", "while-used", "wrong-import-order", # handled by isort / ruff @@ -380,39 +381,39 @@ norecursedirs = [ ] strict = true filterwarnings = [ - 'error', - 'default:Using or importing the ABCs:DeprecationWarning:unittest2.*', + "error", + "default:Using or importing the ABCs:DeprecationWarning:unittest2.*", # produced by older pyparsing<=2.2.0. - 'default:Using or importing the ABCs:DeprecationWarning:pyparsing.*', - 'default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*', + "default:Using or importing the ABCs:DeprecationWarning:pyparsing.*", + "default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*", # distutils is deprecated in 3.10, scheduled for removal in 3.12 - 'ignore:The distutils package is deprecated:DeprecationWarning', + "ignore:The distutils package is deprecated:DeprecationWarning", # produced by pytest-xdist - 'ignore:.*type argument to addoption.*:DeprecationWarning', + "ignore:.*type argument to addoption.*:DeprecationWarning", # produced on execnet (pytest-xdist) - 'ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning', + "ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning", # pytest's own futurewarnings - 'ignore::pytest.PytestExperimentalApiWarning', + "ignore::pytest.PytestExperimentalApiWarning", # Do not cause SyntaxError for invalid escape sequences in py37. # Those are caught/handled by pyupgrade, and not easy to filter with the # module being the filename (with .py removed). - 'default:invalid escape sequence:DeprecationWarning', + "default:invalid escape sequence:DeprecationWarning", # ignore not yet fixed warnings for hook markers - 'default:.*not marked using pytest.hook.*', - 'ignore:.*not marked using pytest.hook.*::xdist.*', + "default:.*not marked using pytest.hook.*", + "ignore:.*not marked using pytest.hook.*::xdist.*", # ignore use of unregistered marks, because we use many to test the implementation - 'ignore::_pytest.warning_types.PytestUnknownMarkWarning', + "ignore::_pytest.warning_types.PytestUnknownMarkWarning", # https://github.com/benjaminp/six/issues/341 - 'ignore:_SixMetaPathImporter\.exec_module\(\) not found; falling back to load_module\(\):ImportWarning', + "ignore:_SixMetaPathImporter\\.exec_module\\(\\) not found; falling back to load_module\\(\\):ImportWarning", # https://github.com/benjaminp/six/pull/352 - 'ignore:_SixMetaPathImporter\.find_spec\(\) not found; falling back to find_module\(\):ImportWarning', + "ignore:_SixMetaPathImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning", # https://github.com/pypa/setuptools/pull/2517 - 'ignore:VendorImporter\.find_spec\(\) not found; falling back to find_module\(\):ImportWarning', + "ignore:VendorImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning", # https://github.com/pytest-dev/execnet/pull/127 - 'ignore:isSet\(\) is deprecated, use is_set\(\) instead:DeprecationWarning', + "ignore:isSet\\(\\) is deprecated, use is_set\\(\\) instead:DeprecationWarning", # https://github.com/pytest-dev/pytest/issues/2366 # https://github.com/pytest-dev/pytest/pull/13057 - 'default::pytest.PytestFDWarning', + "default::pytest.PytestFDWarning", ] pytester_example_dir = "testing/example_scripts" markers = [ @@ -432,47 +433,43 @@ markers = [ "keep_ci_var", ] -[tool.coverage.paths] -source = [ - 'src/', - '*/lib/python*/site-packages/', - '*/pypy*/site-packages/', - '*\Lib\site-packages\', -] - -[tool.coverage.report] -skip_covered = true -show_missing = true -exclude_lines = [ - '\#\s*pragma: no cover', - '^\s*raise NotImplementedError\b', - '^\s*return NotImplemented\b', - '^\s*assert False(,|$)', - '^\s*case unreachable:', - '^\s*assert_never\(', - '^\s*if TYPE_CHECKING:', - '^\s*@overload( |$)', - '^\s*def .+: \.\.\.$', - '^\s*@pytest\.mark\.xfail', -] - -[tool.coverage.run] -include = [ - 'src/*', - 'testing/*', - '*/lib/python*/site-packages/_pytest/*', - '*/lib/python*/site-packages/pytest.py', - '*/pypy*/site-packages/_pytest/*', - '*/pypy*/site-packages/pytest.py', - '*\Lib\site-packages\_pytest\*', - '*\Lib\site-packages\pytest.py', -] -parallel = true -branch = true -patch = [ "subprocess" ] +[tool.coverage] +run.branch = true # The sysmon core (default since Python 3.14) is much slower. # Perhaps: https://github.com/coveragepy/coveragepy/issues/2082 -core = "ctrace" +run.core = "ctrace" +run.include = [ + "*/lib/python*/site-packages/_pytest/*", + "*/lib/python*/site-packages/pytest.py", + "*/pypy*/site-packages/_pytest/*", + "*/pypy*/site-packages/pytest.py", + "*\\Lib\\site-packages\\_pytest\\*", + "*\\Lib\\site-packages\\pytest.py", + "src/*", + "testing/*", +] +run.parallel = true +run.patch = [ "subprocess" ] +paths.source = [ + "src/", + "*/lib/python*/site-packages/", + "*/pypy*/site-packages/", + "*\\Lib\\site-packages\\", +] +report.exclude_lines = [ + "\\#\\s*pragma: no cover", + "^\\s*@overload( |$)", + "^\\s*@pytest\\.mark\\.xfail", + "^\\s*assert False(,|$)", + "^\\s*assert_never\\(", + "^\\s*case unreachable:", + "^\\s*def .+: \\.\\.\\.$", + "^\\s*if TYPE_CHECKING:", + "^\\s*raise NotImplementedError\\b", + "^\\s*return NotImplemented\\b", +] +report.show_missing = true +report.skip_covered = true [tool.towncrier] package = "pytest" @@ -481,76 +478,37 @@ filename = "doc/en/changelog.rst" directory = "changelog/" title_format = "pytest {version} ({project_date})" template = "changelog/_template.rst" - -# NOTE: The types are declared because: -# NOTE: - there is no mechanism to override just the value of -# NOTE: `tool.towncrier.type.misc.showcontent`; -# NOTE: - and, we want to declare extra non-default types for -# NOTE: clarity and flexibility. - -[[tool.towncrier.type]] -# When something public gets removed in a breaking way. Could be -# deprecated in an earlier release. -directory = "breaking" -name = "Removals and backward incompatible breaking changes" -showcontent = true - -[[tool.towncrier.type]] -# Declarations of future API removals and breaking changes in behavior. -directory = "deprecation" -name = "Deprecations (removal in next major release)" -showcontent = true - -[[tool.towncrier.type]] -# New behaviors, public APIs. That sort of stuff. -directory = "feature" -name = "New features" -showcontent = true - -[[tool.towncrier.type]] -# New behaviors in existing features. -directory = "improvement" -name = "Improvements in existing functionality" -showcontent = true - -[[tool.towncrier.type]] -# Something we deemed an improper undesired behavior that got corrected -# in the release to match pre-agreed expectations. -directory = "bugfix" -name = "Bug fixes" -showcontent = true - -[[tool.towncrier.type]] -# Updates regarding bundling dependencies. -directory = "vendor" -name = "Vendored libraries" -showcontent = true - -[[tool.towncrier.type]] -# Notable updates to the documentation structure or build process. -directory = "doc" -name = "Improved documentation" -showcontent = true - -[[tool.towncrier.type]] -# Notes for downstreams about unobvious side effects and tooling. Changes -# in the test invocation considerations and runtime assumptions. -directory = "packaging" -name = "Packaging updates and notes for downstreams" -showcontent = true - -[[tool.towncrier.type]] -# Stuff that affects the contributor experience. e.g. Running tests, -# building the docs, setting up the development environment. -directory = "contrib" -name = "Contributor-facing changes" -showcontent = true - -[[tool.towncrier.type]] -# Changes that are hard to assign to any of the above categories. -directory = "misc" -name = "Miscellaneous internal changes" -showcontent = true +type = [ + # NOTE: The types are declared because: + # NOTE: - there is no mechanism to override just the value of + # NOTE: `tool.towncrier.type.misc.showcontent`; + # NOTE: - and, we want to declare extra non-default types for + # NOTE: clarity and flexibility. + # When something public gets removed in a breaking way. Could be + # deprecated in an earlier release. + { directory = "breaking", name = "Removals and backward incompatible breaking changes", showcontent = true }, + # Declarations of future API removals and breaking changes in behavior. + { directory = "deprecation", name = "Deprecations (removal in next major release)", showcontent = true }, + # New behaviors, public APIs. That sort of stuff. + { directory = "feature", name = "New features", showcontent = true }, + # New behaviors in existing features. + { directory = "improvement", name = "Improvements in existing functionality", showcontent = true }, + # Something we deemed an improper undesired behavior that got corrected + # in the release to match pre-agreed expectations. + { directory = "bugfix", name = "Bug fixes", showcontent = true }, + # Updates regarding bundling dependencies. + { directory = "vendor", name = "Vendored libraries", showcontent = true }, + # Notable updates to the documentation structure or build process. + { directory = "doc", name = "Improved documentation", showcontent = true }, + # Notes for downstreams about unobvious side effects and tooling. Changes + # in the test invocation considerations and runtime assumptions. + { directory = "packaging", name = "Packaging updates and notes for downstreams", showcontent = true }, + # Stuff that affects the contributor experience. e.g. Running tests, + # building the docs, setting up the development environment. + { directory = "contrib", name = "Contributor-facing changes", showcontent = true }, + # Changes that are hard to assign to any of the above categories. + { directory = "misc", name = "Miscellaneous internal changes", showcontent = true }, +] [tool.mypy] files = [ diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 3c628a09c2d..e74546c6e28 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -233,7 +233,7 @@ def is_option(x: str) -> bool: return x.startswith("-") def get_file_part_from_node_id(x: str) -> str: - return x.split("::")[0] + return x.split("::", maxsplit=1)[0] def get_dir_from_path(path: Path) -> Path: if path.is_dir(): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bb6f35633b9..e9049bb82ec 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -476,7 +476,7 @@ def hasopt(self, char: str) -> bool: return char in self.reportchars def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None: - fspath = self.config.rootpath / nodeid.split("::")[0] + fspath = self.config.rootpath / nodeid.split("::", maxsplit=1)[0] if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: self._write_progress_information_filling_space() @@ -1034,7 +1034,9 @@ def mkrel(nodeid: str) -> str: # fspath comes from testid which has a "/"-normalized path. if fspath: res = mkrel(nodeid) - if self.verbosity >= 2 and nodeid.split("::")[0] != nodes.norm_sep(fspath): + if self.verbosity >= 2 and ( + nodeid.split("::", maxsplit=1)[0] != nodes.norm_sep(fspath) + ): res += " <- " + bestrelpath(self.startpath, Path(fspath)) else: res = "[location]" diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py index 51c3db23f6f..232e2e38464 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -32,12 +32,13 @@ class Instant: # Creation time of this instant, using time.time(), to measure actual time. # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. - time: float = dataclasses.field(default_factory=lambda: time(), init=False) + time: float = dataclasses.field(default_factory=lambda: time(), init=False) # noqa: PLW0108 # Performance counter tick of the instant, used to measure precise elapsed time. # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. perf_count: float = dataclasses.field( - default_factory=lambda: perf_counter(), init=False + default_factory=lambda: perf_counter(), # noqa: PLW0108 + init=False, ) def elapsed(self) -> Duration: diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 6b7d756a45c..c32e71f6542 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -1388,7 +1388,7 @@ def test_stat_helpers(self, tmpdir, monkeypatch): def test_stat_non_raising(self, tmpdir): path1 = tmpdir.join("file") - pytest.raises(error.ENOENT, lambda: path1.stat()) + pytest.raises(error.ENOENT, path1.stat) res = path1.stat(raising=False) assert res is None