Compare commits

..

232 Commits
8.2.x ... main

Author SHA1 Message Date
Sviatoslav Sydorenko (Святослав Сидоренко) ac41898755
Merge pull request #12563 from webknjaz/maintenance/hotfixes/note/12264--reraise-with-original-tb
📝🚑 Polish the PR #12264 changelog entry
2024-07-02 21:30:19 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 07ed62ae27
Merge pull request #12561 from webknjaz/maintenance/hotfixes/note/12502--ci-plugin-update-draft-ux
📝💄 Drop trailing period from #12502 byline
2024-07-02 21:28:41 +02:00
Virendra Patil 49bb5c89a6
Improved handling of invalid regex pattern in pytest.raises (#12526) 2024-07-02 21:58:08 +03:00
Sviatoslav Sydorenko (Святослав Сидоренко) e8aee21384
Merge pull request #12562 from webknjaz/maintenance/docs/user-role-validation 2024-07-02 20:52:05 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 4a75f65c73
Merge pull request #12560 from webknjaz/maintenance/hotfixes/note/12531--xfail-no-cover 2024-07-02 20:50:16 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 2719fd6825
Merge pull request #12559 from webknjaz/maintenance/hotfixes/note/12545--venv-detection-mingw 2024-07-02 20:49:57 +02:00
Sviatoslav Sydorenko b62974e63f
📝💅 Link #12264 change note to #12204 2024-07-02 15:54:35 +02:00
Sviatoslav Sydorenko ffcc001562
📝 Add a byline to #12264 change note 2024-07-02 15:54:35 +02:00
Sviatoslav Sydorenko 94c0122a17
📝 Use past tense in #12204 change note 2024-07-02 15:54:34 +02:00
Sviatoslav Sydorenko 8398609f08
📝🚑 Fix RST list in #12264 changelog entry
It was being rendered inline and now it's not.
2024-07-02 15:54:34 +02:00
Sviatoslav Sydorenko 57fe9f53c6
📝 Add a change note for PR #12562 2024-07-02 15:26:35 +02:00
Sviatoslav Sydorenko 9c319d6605
🧪 Lint for typos in `:user:` RST role
It is easy to forget backticks in change note bylines. It's happened
in #12531 already, requiring a hotfix in #12560.

The pre-commit based check idea is coming from the Tox project and
have been battle-tested in aiohttp, CherryPy, and other ecosystems.
2024-07-02 15:21:24 +02:00
Sviatoslav Sydorenko a34f2f06db
📝💄 Drop trailing period from #12502 byline 2024-07-02 15:15:31 +02:00
Sviatoslav Sydorenko 388cf8f381
Correct the `:user:` role @ PR #12531 change note 2024-07-02 15:12:27 +02:00
Sviatoslav Sydorenko 2455e99ea3
📝 Add markup to the PR #12545 changelog entry 2024-07-02 15:09:32 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 51ee3880c7
Merge pull request #12553 from pytest-dev/revert-12516-maintenance/hotfix/gha-codecov-token 2024-07-01 17:20:45 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 90459a8fd3
Merge pull request #12472 from pbrezina/testresult-markup 2024-07-01 17:06:07 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) de4e3cffc2
Revert "🚑🧪 Set the Codecov token directly in GHA" 2024-07-01 16:47:47 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 1a8394ed89
Merge pull request #12531 from webknjaz/maintenance/xfail-no-cover 2024-07-01 16:21:35 +02:00
Bruno Oliveira f502f1db31 Update src/_pytest/reports.py 2024-07-01 13:23:53 +02:00
Bruno Oliveira 201ed9f6a5 Update src/_pytest/reports.py 2024-07-01 13:23:53 +02:00
pre-commit-ci[bot] 2c1ed505d9 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-07-01 13:23:53 +02:00
Bruno Oliveira 269ded17a6 Update src/_pytest/reports.py 2024-07-01 13:23:53 +02:00
Pavel Březina 41ca4dd44e testresult: correctly apply verbose word markup and avoid crash
The following snippet would have resulted in crash on multiple places since
`_get_verbose_word` expects only string, not a tuple.

```python
    @pytest.hookimpl(tryfirst=True)
    def pytest_report_teststatus(report: pytest.CollectReport | pytest.TestReport, config: pytest.Config):
        if report.when == "call":
            return ("error", "A",  ("AVC", {"bold": True, "red": True}))

        return None
```

```
Traceback (most recent call last):
  File "/home/pbrezina/workspace/sssd/.venv/bin/pytest", line 8, in <module>
    sys.exit(console_main())
             ^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/config/__init__.py", line 207, in console_main
    code = main()
           ^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/config/__init__.py", line 179, in main
    ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/main.py", line 333, in pytest_cmdline_main
    return wrap_session(config, _main)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/main.py", line 321, in wrap_session
    config.hook.pytest_sessionfinish(
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/logging.py", line 872, in pytest_sessionfinish
    return (yield)
            ^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 124, in _multicall
    teardown.send(result)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 899, in pytest_sessionfinish
    self.config.hook.pytest_terminal_summary(
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 124, in _multicall
    teardown.send(result)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 923, in pytest_terminal_summary
    self.short_test_summary()
  File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 1272, in short_test_summary
    action(lines)
  File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 1205, in show_simple
    line = _get_line_with_reprcrash_message(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 1429, in _get_line_with_reprcrash_message
    word = tw.markup(verbose_word, **word_markup)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pbrezina/workspace/pytest/src/_pytest/_io/terminalwriter.py", line 114, in markup
    text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m"
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
TypeError: can only concatenate str (not "tuple") to str
```

Signed-off-by: Pavel Březina <pbrezina@redhat.com>
2024-07-01 13:23:53 +02:00
github-actions[bot] dbf7dee8c8
[automated] Update plugin list (#12547)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-06-30 12:05:11 +00:00
Zach Snicker 0adcc21f48
Support venv detection on Windows with mingw Python (#12545)
Closes #12544
2024-06-29 00:30:51 +03:00
joseph-sentry 0ed2d79457
junitxml: add timezone to testsuite timestamp (#12491)
Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2024-06-27 09:41:02 -03:00
Sviatoslav Sydorenko (Святослав Сидоренко) f74e947c1f
Merge pull request #12533 from webknjaz/docs/drop-extlinks-bpo 2024-06-26 14:39:31 +02:00
Sviatoslav Sydorenko d75fa9f9b4
📝 Add a change note for PE #12533 2024-06-26 14:22:56 +02:00
Sviatoslav Sydorenko 29e4b6b9f0
📝🔥 Remove the `:bpo:` RST role declaration
BPO is in read-only mode and all issues have been migrated to GitHub.
This patch replaces the use of that role with `:issue:`, recently
integrated via #12522. It also disables the built-in `extlinks` Sphinx
extension, as it's no longer in use.
2024-06-26 14:19:13 +02:00
Sviatoslav Sydorenko f2acc65485
📝 Add a change note for PR #12531 2024-06-25 23:33:43 +02:00
Sviatoslav Sydorenko 634a83df94
🧪 Unmeasure xfail tests
These tests are known to only be executed partially or not at all. So
we always get incomplete, missing, and sometimes flaky, coverage in
the test functions that are expected to fail.

This change updates the ``coverage.py`` config to prevent said tests
from influencing the coverage level measurement.
2024-06-25 23:25:04 +02:00
Florian Bruhin 53bf188999
Merge pull request #12523 from lovetheguitar/fix/follow_up_to_pr_#12500
Improve documentation & other kinks of marker keyword expression PR #12500
2024-06-25 10:17:07 +02:00
lovetheguitar 36b384afc7 docs(expression.py): simplify grammar documentation by defining `kwargs` separately 2024-06-25 09:18:18 +02:00
lovetheguitar 3d07791c36 chore: remove obsolete `TODO`s 2024-06-25 09:18:18 +02:00
lovetheguitar dd57196953 perf(expression): define `TokenType.STRING` as "string literal" for clearer errors 2024-06-25 09:18:18 +02:00
lovetheguitar 540ede3439 docs(expression.py): describe new `name` & `value` productions 2024-06-25 09:18:18 +02:00
Florian Bruhin 2b7eadf090
Update trainings (#12514) 2024-06-25 07:24:50 +02:00
pre-commit-ci[bot] 77416d64f5
[pre-commit.ci] pre-commit autoupdate (#12528)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.9 → v0.4.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.9...v0.4.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-25 04:26:26 +00:00
dependabot[bot] d582dcfc16
build(deps): Bump peter-evans/create-pull-request from 6.0.5 to 6.1.0 (#12527)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6.0.5 to 6.1.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](6d6857d369...c5a7806660)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 18:21:02 -03:00
Sviatoslav Sydorenko (Святослав Сидоренко) 237152552e
Merge pull request #12522 from webknjaz/docs/sphinx-issues-ext
📝 Replace GH/PyPI `extlinks` w/ `sphinx-issues`
2024-06-23 16:55:51 +02:00
github-actions[bot] 175340e06b
[automated] Update plugin list (#12524)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-06-23 06:22:44 +00:00
Sviatoslav Sydorenko 8f2ef30f4a
🚑 Stop setting PR/issue prefix @ `sphinx-issues`
Apparently, the extension only supports one of the pre-defined
prefixes.
2024-06-22 22:16:40 +02:00
Sviatoslav Sydorenko 0971a95902
📝 Add a change note for PR #12522 2024-06-22 22:14:33 +02:00
Sviatoslav Sydorenko b919a711a4
📝 Replace GH/PyPI `extlinks` w/ `sphinx_issues`
This extension implements more generic roles that can also be used
more flexibly. Relying on an external extension allows to stop
maintaining an in-repo copy of the commonly used behavior.
2024-06-22 22:11:43 +02:00
Sviatoslav Sydorenko 66dbab697b
📝 Rename `:pull:` RST role to `:pr:`
This is a preparatory patch for integrating the third party
`sphinx-issues` extension.
2024-06-22 21:54:45 +02:00
lovetheguitar 3cce243774 docs(expression.py): fix typo 2024-06-22 20:16:24 +02:00
lovetheguitar 73bc35ce2b docs(expression.py): correct grammar definition of lexer 2024-06-22 20:16:24 +02:00
lovetheguitar 66eff85e54 docs: use double quotes for cross-platform compatibility in example code 2024-06-22 19:48:15 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) f75e3fe63f
Merge pull request #12516 from webknjaz/maintenance/hotfix/gha-codecov-token 2024-06-22 12:32:18 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 6448d30fa1
Merge pull request #12517 from webknjaz/maintenance/gha-check-223e4bb7 2024-06-22 11:58:41 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) fbdf9c887c
Merge pull request #12455 from stdedos/patch-1
Update `contact.rst`: Update Matrix link
2024-06-21 23:26:32 +02:00
Sviatoslav Sydorenko c716e0baef
🧪 Bump the `alls-green` action to 223erbb7
This version drops the use of the outdated GHA syntax for setting
action output values.
2024-06-21 23:06:26 +02:00
Sviatoslav Sydorenko 1b85ac126e
🚑🧪 Set the Codecov token directly in GHA
It's necessary since it seems that the currently used Codecov uploader
doesn't read the token from config sometimes.

This is a follow-up for #12508 which wasn't enough.
2024-06-21 22:52:17 +02:00
Ronny Pfannschmidt 9f134fc6ad
Merge pull request #12500 from lovetheguitar/feat/support_marker_kwarg_in_marker_expressions 2024-06-21 22:24:34 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 75a2225ed1
🔥 Drop the missing kwargs case 2024-06-21 22:09:45 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 329662e2ec
📝 Drop stray trailing period from the change note byline 2024-06-21 22:06:33 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 24450e33e3
📝 Use explicit RST roles for built-in types in docs 2024-06-21 22:05:43 +02:00
lovetheguitar b1255a9aae style(mark): type hint `**kwargs` as `str | int | bool | None` 2024-06-21 20:51:01 +02:00
lovetheguitar c3e898353b docs(AUTHORS): add myself as contributor 2024-06-21 20:51:01 +02:00
lovetheguitar 9cf9cfabcb docs(12281.feature.rst): add changelog fragment 2024-06-21 20:51:01 +02:00
lovetheguitar 598d881c9c docs: document keyword argument support in marker expressions 2024-06-21 20:51:01 +02:00
lovetheguitar 7c7c36d7e0 test(test_mark.py): add sad case that `-k` doesn't support keyword expressions 2024-06-21 20:51:01 +02:00
lovetheguitar 3921d94316 test: use new `MarkMatcher.from_markers` method & register test markers 2024-06-21 20:51:01 +02:00
lovetheguitar 6dd8ad60a4 refactor(MarkMatcher): replace `from_item` with `from_markers` method 2024-06-21 20:51:01 +02:00
lovetheguitar 1e7eb20347 perf(expression): improve string lexing & error messages 2024-06-21 20:51:01 +02:00
lovetheguitar f4897391ec refactor(mark): use existing `NOT_SET` sentinel 2024-06-21 20:51:01 +02:00
lovetheguitar 1cc35ecc3f test: add empty string keyword argument marker test cases 2024-06-21 20:51:01 +02:00
lovetheguitar 04f457c4f4 style: use `@overload` to get rid of mypy only assertions 2024-06-21 20:51:01 +02:00
lovetheguitar 15c33fbaa3 feat: support keyword arguments in marker expressions
Fixes #12281
2024-06-21 20:51:01 +02:00
Ronny Pfannschmidt e8fa8dd31c
Merge pull request #12509 from pytest-dev/update-plugin-list/patch-c8948fca6
[automated] Update plugin list
2024-06-21 20:34:00 +02:00
pytest bot 27b2550283 [automated] Update plugin list 2024-06-21 20:19:10 +02:00
Farbod Ahmadian 34e28295a7
refactor: simplify bound method representation (#12492)
Co-authored-by: Sviatoslav Sydorenko <webknjaz@redhat.com>
Co-authored-by: Farbod Ahmadian <farbod@datachef.com>
2024-06-21 18:20:44 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) 9947ec3ad1
🧪🚑 Pass a Codecov config to the action @ GHA (#12508)
The #11921 update broke uploading coverage of the `main` branch (or
any in-repo pushes for that matter) to Codecov 4 months ago.
Version 4 requires an upload token to be provided and since there was
no configuration for it, the upload was failing. But the step itself
was showing up as successful due to `fail_ci_if_error: true` being
set. The error is visible in the console output, though.

This patch flips the setting to `fail_ci_if_error: false` and sets the
Codecov upload token in the config in clear text. The non-secret part
allows the PRs uploads to be more stable.

Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2024-06-21 17:47:23 +02:00
Ronny Pfannschmidt c8948fca65
Merge pull request #12502 from webknjaz/maintenance/ci-plugin-update-draft-ux
🧪 Make a draft based plugin bump PR CI trigger
2024-06-21 15:48:49 +02:00
Ronny Pfannschmidt dab29d39d7
Merge pull request #12507 from webknjaz/maintenance/hotfixes/note/sphinx-towncrier-draft
📝 Add a change note for PR #12493
2024-06-21 15:26:11 +02:00
Oliver Bestwalter b864e50138
Merge pull request #12498 from webknjaz/maintenance/tox-descriptions 2024-06-21 15:17:40 +02:00
Sviatoslav Sydorenko 5d95f09b2a
📝 Add a change note for PR #12493 2024-06-21 15:11:12 +02:00
Sviatoslav Sydorenko a67327ac8e
📝 Add a change note to PR #12498 2024-06-21 14:14:28 +02:00
Sviatoslav Sydorenko a69230ea5f
🎨 Add descriptions to all `tox` environments
Previously, a part of the environments weren't documented in the
config, making it difficult for the newbies to figure out what
their purposes are. This patch sets the descriptions for all the
envs listed with the `tox -av` command, leveraging the dynamic
factor-dependent explanation fragments.

```console
default environments:
linting                   -> run pre-commit-defined linters under `python3`
py38                      -> run the tests under `py38`
py39                      -> run the tests under `py39`
py310                     -> run the tests under `py310`
py311                     -> run the tests under `py311`
py312                     -> run the tests under `py312`
py313                     -> run the tests under `py313`
pypy3                     -> run the tests under `pypy3`
py38-pexpect              -> run the tests against `pexpect` under `py38`
py38-xdist                -> run the tests with pytest in parallel mode under `py38`
py38-unittestextras       -> run the tests against the unit test extras under `py38`
py38-numpy                -> run the tests against `numpy` under `py38`
py38-pluggymain           -> run the tests against the bleeding edge `pluggy` from Git under `py38`
py38-pylib                -> run the tests against `py` lib under `py38`
doctesting                -> run the tests under `~/.pyenv/versions/3.12.3/envs/pytest-pyenv-py3.12.3/bin/python` including doctests
doctesting-coverage       -> run the tests collecting coverage under `~/.pyenv/versions/3.12.3/envs/pytest-pyenv-py3.12.3/bin/python` including doctests
plugins                   -> run reverse dependency testing against pytest plugins under `~/.pyenv/versions/3.12.3/envs/pytest-pyenv-py3.12.3/bin/python`
py38-freeze               -> test pytest frozen with `pyinstaller` under `py38`
docs                      -> build the documentation site under `~/src/github/pytest-dev/pytest/doc/en/_build/html` with `python3`
docs-checklinks           -> check the links in the documentation with `python3`
py311-exceptiongroup      -> run the tests against `exceptiongroup` under `py311`

additional environments:
regen                     -> regenerate documentation examples under `python3`
release                   -> do a release, required posarg of the version number
prepare-release-pr        -> prepare a release PR from a manual trigger in GitHub actions
generate-gh-release-notes -> generate release notes that can be published as GitHub Release
nobyte                    -> run the tests in no-bytecode mode under `~/.pyenv/versions/3.12.3/envs/pytest-pyenv-py3.12.3/bin/python`
lsof                      -> run the tests with `--lsof` pytest CLI option under `~/.pyenv/versions/3.12.3/envs/pytest-pyenv-py3.12.3/bin/python`
```
2024-06-21 14:14:26 +02:00
Sviatoslav Sydorenko 072cb5250e
📝 Add a change note for PR #12502 2024-06-21 14:11:07 +02:00
Sviatoslav Sydorenko 323b0bd853
🧪 Make a draft based plugin bump PR CI trigger
Normally, PRs/commits published using the default GitHub Actions CI/CD
API token are not propagated to any integrations. This patch marks the
plugin update PR as a draft and leaves a comment asking the maintainers to
mark it as ready for review in order to actually trigger a CI run.

This idea is found in GitHub's own repos:
* https://github.com/github/codeql-action/pull/2263#issuecomment-2078311173
* https://github.com/github/codeql-action/blob/4ebadbc7/.github/workflows/update-dependencies.yml#L38-L41
* https://github.com/github/codeql-action/pull/1868
* https://github.com/github/codeql-action/pull/679
2024-06-21 14:11:06 +02:00
Ronny Pfannschmidt b08b6d122f
Merge pull request #12501 from webknjaz/maintenance/changelog-categories
📝💅 Split trivial change log category into 3
2024-06-21 13:50:07 +02:00
Sviatoslav Sydorenko e73db68642
📝 Add a change note for PR #12501 2024-06-21 13:35:47 +02:00
Sviatoslav Sydorenko 3ce1d658bd
🎨 Set up Git to only allow certain change notes 2024-06-21 13:35:47 +02:00
Sviatoslav Sydorenko 03be1ed8b1
📝💅 Split `trivial` change log category into 3
The new change note types are `packaging`, `contrib` and `misc`.
`packaging` is intended for the audience of downstream redistributors.
The `contrib` notes are meant to be documenting news affecting the
project contributors, their development, and processes.
Finally, `misc` is for things that don't fit anywhere but are still
desired to be documented for some reason.
2024-06-21 13:35:47 +02:00
Sviatoslav Sydorenko 45a89ec61f
📝 Extend change log title meanings
This patch makes them more verbose so that they are clearer to the
readers.
2024-06-21 13:34:38 +02:00
Sviatoslav Sydorenko 0cf92cfa4c
📝 Add comments to change note categories 2024-06-21 13:34:38 +02:00
Ronny Pfannschmidt bbe6b4a218
Merge pull request #12467 from RonnyPfannschmidt/ronny/new-annotations-try-2
from __future__ import annotations + migrate
2024-06-21 10:24:56 +02:00
Oliver Bestwalter 2f92da9190
Merge pull request #12503 from webknjaz/maintenance/hotfixes/sphinx-towncrier-draft 2024-06-21 10:08:50 +02:00
Ronny Pfannschmidt 6a8e9ed43a fixup: Config.cache cannot be None 2024-06-21 09:42:24 +02:00
Sviatoslav Sydorenko dc2568a683
🚑🧪📝 Stop capping Towncrier @ local docs builds
It is uncapped in RTD. Apparently, `tox -e docs` is not invoked in CI,
neither is it called in RTD, resulting in the regression having been
caught only in local development environments.

This is a follow-up for #12493.
2024-06-20 22:13:43 +02:00
Sviatoslav Sydorenko 43815b6fc2
🚑🧪📝 Allow invoking `git` in tox
It is called when building the docs. Apparently, `tox -e docs` is not
invoked in CI, neither is it called in RTD, resulting in the
regression having been caught only in local development environments.

This is a follow-up for #12493.
2024-06-20 22:13:43 +02:00
Ronny Pfannschmidt f426c0b35a
Merge pull request #12469 from webknjaz/maintenance/non-setuptools-entry-points
📝 Make "setuptools entrypoint" term generic
2024-06-20 11:31:13 +02:00
Ronny Pfannschmidt 85e451a2cc add changelog entry 2024-06-20 11:10:04 +02:00
Ronny Pfannschmidt 4e54f19be1 update tox:docs python to rtd python 2024-06-20 11:04:38 +02:00
Ronny Pfannschmidt 5e1649f59a resolve most sphinx lookup errors
add the extra sphinx annotations to refer to Path instances

add Path to nitpicky ignore
2024-06-20 11:04:33 +02:00
Ronny Pfannschmidt b7c0295e1a use runtime union for EXCEPTION_OR_MORE 2024-06-20 11:03:07 +02:00
Ronny Pfannschmidt 9295f9ffff RFC: from __future__ import annotations + migrate 2024-06-20 11:03:03 +02:00
Sviatoslav Sydorenko cb179472bb
📝 Add change notes for PR #12469 2024-06-20 11:01:10 +02:00
Sviatoslav Sydorenko 39b548e6ea
📝 Make "setuptools entrypoint" term generic
This feature grew out of `setuptools` but the modern interface for
extracting this information from the distribution package metadata
is `importlib.metadata`. So the patch attempts to reflect this in
the documentation messaging.

Refs:
* https://docs.python.org/3/library/importlib.metadata.html#entry-points
* https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata
* https://packaging.python.org/en/latest/specifications/entry-points/#entry-points
2024-06-20 11:01:10 +02:00
Ronny Pfannschmidt 20dd1d6738
Merge pull request #12493 from webknjaz/maintenance/sphinx-towncrier-draft
📝💅 Always render changelog draft @ Sphinx docs
2024-06-20 10:59:04 +02:00
Sviatoslav Sydorenko e702079fd5
📝💅 Always render changelog draft @ Sphinx docs
The earlier implementation was generating a temporary file, when
the docs site was being built with `tox`. However, this was not
enabled in RTD and is hackish.
This patch integrates the `sphinxcontrib-towncrier` extension to
make it work in any environment where Sphinx docs are being built.
2024-06-20 10:44:32 +02:00
Ronny Pfannschmidt 63dfa4bb84
Merge pull request #12494 from webknjaz/maintenance/rtd-conf-name
📝 Rename the RTD config to canonical filename
2024-06-20 10:39:34 +02:00
Sviatoslav Sydorenko 0470c387a9
📝 Rename the RTD config to canonical filename
Previously, it was possible to use several different config file name
variants. However, there is one canonical name now and the others are
being deprecated [[1]].

[1]: https://docs.rtfd.io/en/stable/config-file/index.html
2024-06-20 10:22:17 +02:00
Ronny Pfannschmidt 57bc6df510
Merge pull request #12488 from webknjaz/maintenance/gha-check-allowed-failures
🧪 Make required CI jobs match branch protection
2024-06-19 12:42:36 +02:00
Sviatoslav Sydorenko 6a95bcaa58
🧪 Make required CI jobs match branch protection 2024-06-19 12:10:20 +02:00
Ronny Pfannschmidt ff133f6baa
Merge pull request #12486 from webknjaz/maintenance/rm-legacy-backport-gha-workflow
🔥 Exterminate legacy backport GHA workflow
2024-06-19 11:59:28 +02:00
Sviatoslav Sydorenko bab3c2c291
📝 Add myself to the authors file 2024-06-19 11:31:55 +02:00
Ronny Pfannschmidt 08a39bf9b7
Merge pull request #11818 from RonnyPfannschmidt/ronny/issue-11797-approx-sequence-like
fix #11797: be more lenient on SequenceLike approx
2024-06-19 11:25:55 +02:00
Sviatoslav Sydorenko 19715bf313
🔥 Exterminate legacy `backport` GHA workflow
Previously, this workflow was being used to cherry-pick PRs made
against `main` into older stable branches. Now that #12475 integrated
the Patchback GitHub App, it's no longer needed as it's making
duplicate pull requests which don't even trigger CI runs automatically
due to #10354. So this patch removes said workflow to address the
problem.
2024-06-19 11:22:55 +02:00
Ronny Pfannschmidt c46a3a9920
Merge pull request #12477 from webknjaz/maintenance/chronographer-config
💅 Add a config for the Chronographer GitHub App
2024-06-19 09:49:11 +02:00
Sviatoslav Sydorenko (Святослав Сидоренко) d7b4010638
💅 Add a config for the Patchback GitHub App (#12475)
This patch prepares the project's backporting process to start being
handled by the Patchback GitHub App [[1]].

Ref #9384
Resolves #9385
Resolves #9553
Resolves #9554
Resolves #9555

[1]: https://github.com/apps/patchback
2024-06-19 00:41:33 +00:00
Sviatoslav Sydorenko f479afc5c0
💅 Add a config for the Chronographer GitHub App
This app allows requiring changelog fragments to be included with
each pull request.
2024-06-19 00:40:11 +02:00
Ronny Pfannschmidt ea87bd6302
Merge pull request #12476 from webknjaz/maintenance/rtd-latest-env
🧪 Bump RTD env to the latest LTS Ubuntu & Python
2024-06-19 00:18:12 +02:00
Sviatoslav Sydorenko 9e98b6db5b
🧪 Bump RTD env to the latest LTS Ubuntu & Python 2024-06-18 18:39:13 +02:00
Ronny Pfannschmidt 76f3f3da00 fix #11797: be more lenient on SequenceLike approx
this needs a validation as it allows partially implemented sequences
2024-06-18 17:01:24 +02:00
Ronny Pfannschmidt dc65bb6a66
Merge pull request #10315 from webknjaz/maintenance/gha-check
Introduce a gate/check GHA job
2024-06-18 16:30:54 +02:00
pre-commit-ci[bot] 807d16abfd
[pre-commit.ci] pre-commit autoupdate (#12470)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-18 08:29:53 -03:00
Sviatoslav Sydorenko (Святослав Сидоренко) 49374ec7a0
🚑 Clarify patch condition for doctest finder hack (#12471)
There was a regression caused by #12431 that excluded the patch on
broken CPython patch version ranges 3.11.0–3.11.8 and 3.12.0–3.12.2.

This change fixes that by adjusting the patching conditional clause
to include said versions.

Closes #12430.

Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-06-18 09:21:25 +00:00
Sviatoslav Sydorenko (Святослав Сидоренко) fe4961afae
Modernize the skipped `test_issue_9765` regression test (#12468)
* 🚑 Explicitly set encoding @ `subprocess.run()`

This invocation is present in the `test_issue_9765` regression test
that is always skipped unconditionally, resulting in the
`EncodingWarning` never manifesting itself in CI or development
environments of the contributors.

* Change `setup.py` call w/ `pip install` @ tests

Using this CLI interface has been deprecated in `setuptools` [[1]].

[1]: https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html
2024-06-17 15:44:14 +00:00
dependabot[bot] 80b7657b2a
build(deps): Bump pypa/gh-action-pypi-publish from 1.8.14 to 1.9.0 (#12464)
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.14 to 1.9.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.14...v1.9.0)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 09:58:23 +02:00
github-actions[bot] 5037f8d114
[automated] Update plugin list (#12462)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-06-16 05:24:06 +00:00
neutraljump 2effd8cb2c
Docs: clean up various documentation pages (#12451)
* Change Contribution doc title to match sidebar

* Rearrange sentence for clarity

* Update backwards-compatibility.rst

some minor grammar changes

* Update pythonpath.rst

fixed some gramatical errors

* Update AUTHORS

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update doc/en/explanation/pythonpath.rst

From a quick overview it looks like lowercase is more consistent, although some pages do use `pytest` in code blocks

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>

---------

Co-authored-by: Mackerello <82668740+Mackerello@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-06-13 09:09:02 -03:00
Stavros Ntentos 037aaa7d45
Update `contact.rst`: Update Matrix link 2024-06-13 11:18:40 +03:00
pre-commit-ci[bot] ff75980135
[pre-commit.ci] pre-commit autoupdate (#12447)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.4.8)
- [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0)

* Apply pyupgrade automatically

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-06-11 12:36:39 -03:00
Ran Benita 23ca9798f7
doc: fix broken code blocks (#12449)
Caused by 4588653b24.

The issue was fixed in https://github.com/astral-sh/ruff/issues/11577,
so won't trigger again.

Fix #12437.
2024-06-11 12:36:20 -03:00
Ran Benita f3946dce07
Merge pull request #12443 from bluetech/pygments-refactor
terminalwriter: small refactor of pygments code, improve usage errors
2024-06-10 14:13:46 +03:00
Ran Benita 7ef9da1f02 terminalwriter: improve `PYTEST_THEME`, `PYTEST_THEME_MODE` usage errors 2024-06-10 11:50:08 +03:00
Ran Benita 67a570aea9 terminalwriter: factor out pygments lexer & formatter selection to own functions
We intend to add some more logic here.
2024-06-10 11:48:19 +03:00
dependabot[bot] 3d91e42229
build(deps): Bump pytest-bdd in /testing/plugins_integration (#12442)
Bumps [pytest-bdd](https://github.com/pytest-dev/pytest-bdd) from 7.1.2 to 7.2.0.
- [Release notes](https://github.com/pytest-dev/pytest-bdd/releases)
- [Changelog](https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-bdd/compare/7.1.2...7.2.0)

---
updated-dependencies:
- dependency-name: pytest-bdd
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 08:28:57 +02:00
github-actions[bot] 9bfcca6f48
[automated] Update plugin list (#12441)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-06-09 22:49:33 -03:00
Ran Benita f85289ba87
Merge pull request #12436 from bluetech/unittest-rerun-assertion
unittest: fix assertion errors on unittest reruns
2024-06-08 02:11:20 +03:00
Ran Benita 18f15a38fc
Merge pull request #12435 from bluetech/avoid-type-checking
Avoid some `TYPE_CHECKING`
2024-06-08 02:11:06 +03:00
Ran Benita db67107090
Merge pull request #12434 from bluetech/register-fixture-scope-none
fixtures: change `register_fixture` to not accept `scope=None`
2024-06-07 12:11:56 +03:00
Ran Benita de47b73520 unittest: fix assertion errors on unittest reruns
This fixes unittest test reruns when using plugins like
pytest-rerunfailures.

The `instance` property uses AttributeError to check if the instance
needs to be initialized, so `del` is the correct way to clear it, not
setting to `None`.

Regressed in 8.2.2.
2024-06-07 10:21:15 +03:00
Ran Benita 8585c58826
Merge pull request #10470 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-trio-0.8.0
build(deps): Bump pytest-trio from 0.7.0 to 0.8.0 in /testing/plugins_integration
2024-06-07 10:20:42 +03:00
dependabot[bot] 13f97632c6
build(deps): Bump pytest-trio in /testing/plugins_integration
Bumps [pytest-trio](https://github.com/python-trio/pytest-trio) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/python-trio/pytest-trio/releases)
- [Commits](https://github.com/python-trio/pytest-trio/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: pytest-trio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-07 07:03:09 +00:00
Ran Benita c07bbdfa5b Avoid some TYPE_CHECKING
It's better not to use it when possible.
2024-06-07 09:36:55 +03:00
Jason R. Coombs f941099371
Cleanup MockAwareDocTestFinder. (#12431)
* Only rely on _find_lineno on Python 3.10 and earlier.

* Update docstring to reflect current expectation.

* Only rely on _find on Python 3.9 and earlier.

* Mark line as uncovered.

* Remove empty else block (implicit is better than explicit).

Closes #12430
Closes #12432
2024-06-07 09:32:33 +03:00
Tomasz Kłoczko 6b2daaa2e9
[pre-commit] Add pyupgrade back as a manual stage (#12418)
Launchable with ``pre-commit run --hook-stage manual pyupgrade -a``

---------

Signed-off-by: Tomasz Kłoczko <kloczek@github.com>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-06-06 23:52:29 +02:00
holger krekel 043ff9abc6
Remove hp42 contact details from the documentation (#12427) 2024-06-06 10:24:31 +00:00
Ran Benita c9d8765381 fixtures: change `register_fixture` to not accept `scope=None`
There is no reason to allow this.
2024-06-04 23:22:18 +03:00
Ran Benita 0070151c63
Merge pull request #12416 from bluetech/cherry-pick-release
Cherry-pick 8.2.2 release notes
2024-06-04 17:22:29 +03:00
Ran Benita 4cd80e19c5 Merge pull request #12415 from pytest-dev/release-8.2.2
Prepare release 8.2.2

(cherry picked from commit f3a494cca3)
2024-06-04 16:52:15 +03:00
Ran Benita d4dbe771f0
Merge pull request #12409 from bluetech/reorder-items-perf
fixtures: fix catastrophic performance problem in `reorder_items`
2024-06-04 10:16:19 +03:00
Ran Benita e89d23b247 fixtures: fix catastrophic performance problem in `reorder_items`
Fix #12355.

In the issue, it was reported that the `reorder_items` has quadratic (or
worse...) behavior with certain simple parametrizations. After some
debugging I found that the problem happens because the "Fix
items_by_argkey order" loop keeps adding the same item to the deque,
and it reaches epic sizes which causes the slowdown.

I don't claim to understand how the `reorder_items` algorithm works, but
if as far as I understand, if an item already exists in the deque, the
correct thing to do is to move it to the front. Since a deque doesn't
have such an (efficient) operation, this switches to `OrderedDict` which
can efficiently append from both sides, deduplicate and move to front.
2024-06-04 10:15:50 +03:00
pre-commit-ci[bot] 3433c7adf5
[pre-commit.ci] pre-commit autoupdate (#12413)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.5 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.5...v0.4.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-04 09:06:33 +02:00
Michael Vogt 7be95f9b30
code: do not truncate args when running with -vvv (#12241)
Related to #2871.
2024-06-03 12:11:41 +00:00
Ran Benita 177f2ae6c4
Merge pull request #12408 from bluetech/cache-race
cacheprovider: fix "Directory not empty" crash from cache directory creation
2024-06-03 12:43:59 +03:00
Ran Benita 1eee63a891 fixtures: minor cleanups to reorder_items
This makes some minor clarity and performance improvements to the code.
2024-06-02 18:31:54 +03:00
Ran Benita 17065cb008 cacheprovider: fix "Directory not empty" crash from cache directory creation
Fix #12381

Test plan:
It's possible to write a deterministic test case for this, but somewhat
of a hassle so I tested it manually. I reproduced by removing existing
`.pytest_cache`, adding a sleep before the rename and running two
pytests. I verified that it doesn't reproduce after the fix.
2024-06-02 17:05:36 +03:00
github-actions[bot] 98021838fd
[automated] Update plugin list (#12405)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-06-02 09:31:40 -03:00
Florian Bruhin 10c6db2df2
doc: Update trainings/events (#12401) 2024-05-30 16:48:37 +02:00
Pierre Sassoulas 0ba3e91fbd
Merge pull request #12379 from Pierre-Sassoulas/more-pylint-fixes
[pylint] Fix ``consider-using-sys-exit``, ``use-yield-from``, and ``implicit-str-concat``
2024-05-29 15:07:04 +02:00
Pierre Sassoulas 908e112999 [pylint 'implicit-str-concat'] fix existing unwanted implicit str concat 2024-05-29 14:36:01 +02:00
Pierre Sassoulas 0d33cdf02a [pylint] Disable the only 'misplaced-bare-raise' 2024-05-29 14:36:01 +02:00
Pierre Sassoulas c45afde35d [pylint 'consider-using-sys-exit'] Fix all occurences in existing code 2024-05-29 14:36:01 +02:00
Pierre Sassoulas db67d5c874 [pylint 'use-yield-from'] Fix all occurences in existing code 2024-05-29 14:36:01 +02:00
dependabot[bot] 383659d0be
build(deps): Bump hynek/build-and-inspect-python-package (#12373)
Bumps [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.5.0...v2.6.0)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 13:39:29 +02:00
Bruno Oliveira 9f121e85a7
Clarify pytest_ignore_collect docs (#12385)
Fixes #12383

Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-05-28 13:47:00 -03:00
pre-commit-ci[bot] 48cb8a2b32
[pre-commit.ci] pre-commit autoupdate (#12380)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.5)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-27 22:31:18 +00:00
Pierre Sassoulas 88fae23bdd
[pylint] Fixes all ``use-maxplit-args``, ``consider-using-enumerate`` (#12172)
* [pylint 'use-maxsplit-arg'] Do not split more than necessary when using the first element

* [pylint 'consider-using-enumerate'] Use zip when iterating on two iterators

* [pylint] 'cell-var-from-loop' and 'disallowed-name' permanent disable

* [pylint] Disable 'possibly-used-before-assignment' following 3.2.0 release
2024-05-27 12:18:03 -07:00
dependabot[bot] 24abe4eb03
build(deps): Bump anyio[curio,trio] in /testing/plugins_integration (#12374)
Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/agronholm/anyio/releases)
- [Changelog](https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/anyio/compare/4.3.0...4.4.0)

---
updated-dependencies:
- dependency-name: anyio[curio,trio]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 06:43:30 +02:00
James Frost 020db7ec04
Add html_baseurl to sphinx conf.py (#12364)
This is used to set the <link rel="canonical" href="X"> tag that points to the canonical version of the webpage. Including this indicates to search engines which version to include in their indexes, and should prevent older versions showing up.

Fixes #12363
2024-05-26 07:21:30 -03:00
Ran Benita f8dfc1d5f0
Merge pull request #12368 from bluetech/unittest-clear-instance
unittest: fix class instances no longer released on test teardown since pytest 8.2.0
2024-05-26 10:34:11 +03:00
Ran Benita c3c51037f0 unittest: fix class instances no longer released on test teardown since pytest 8.2.0
Fix #12367.
2024-05-26 10:18:42 +03:00
Ran Benita 3fbf4d3cbd
Merge pull request #12369 from pytest-dev/update-plugin-list/patch-889d9b28d
[automated] Update plugin list
2024-05-26 10:14:35 +03:00
pytest bot b83dd34ce1 [automated] Update plugin list 2024-05-26 00:21:28 +00:00
Nathan Goldbaum 889d9b28d7
Add thread safety section to flaky test docs (#12359)
Closes #12356
2024-05-24 08:16:44 -03:00
Sam Jirovec cbf6bd9dd2
Issue #12290 - Docs using Furo Theme W/ Dark Mode (#12326)
* furo theme for docs site

* removing duplicate tocs from deprecations and reference pages

* removing pallets references in code and config

* reverting trainings to sidebar

* removed sphinx style and unpinned packaging version

* updated styles
2024-05-21 13:56:18 +00:00
pre-commit-ci[bot] 5d5c9dc858
[pre-commit.ci] pre-commit autoupdate (#12321)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.4)
- [github.com/tox-dev/pyproject-fmt: 1.8.0 → 2.1.3](https://github.com/tox-dev/pyproject-fmt/compare/1.8.0...2.1.3)

Also fix the comment following autofix

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-05-21 07:04:09 +00:00
dependabot[bot] 00be7c0739
build(deps): Bump pytest-asyncio in /testing/plugins_integration (#12345)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.23.6 to 0.23.7.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.6...v0.23.7)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 07:11:04 +02:00
Hynek Schlawack d4f827d86b
Fix link in changelog (#12343) 2024-05-19 21:14:45 +00:00
Ran Benita 69fa9bae43
Merge pull request #12342 from bluetech/cherry-pick-release
Cherry pick 8.2.1 release notes
2024-05-19 22:37:32 +03:00
pytest bot 32baa0b93d Prepare release version 8.2.1
(cherry picked from commit 66ff8dffdf)
2024-05-19 22:11:09 +03:00
Ran Benita d5b6d44d0e
Merge pull request #12339 from pytest-dev/update-plugin-list/patch-ee9ea703f
[automated] Update plugin list
2024-05-19 12:47:55 +03:00
Ran Benita c1d623cbff
Merge pull request #12334 from bluetech/py313
Add Python 3.13 (beta) support
2024-05-19 09:45:09 +03:00
Ran Benita 1cb704ff2c Add Python 3.13 (beta1) support 2024-05-19 09:25:13 +03:00
pytest bot cb732bbfb0 [automated] Update plugin list 2024-05-19 00:21:06 +00:00
Ran Benita dbee3fa34a testing: remove conditionals for Python 3.11 beta releases
No need to support beta releases of an older version anymore.

Ref: 09b2c95320
2024-05-18 19:53:50 +03:00
Jelle Zijlstra ee9ea703f9
Fix new typing issues in AST code (#12337)
python/typeshed#11880 adds more precise types for AST nodes. I'm submitting some changes to adapt pytest to these changes.
2024-05-18 19:07:06 +03:00
Bruno Oliveira 635fbe2bff
Attest package provenance (#12333)
Use the new build provenance support added in [build-and-inspect-python-package 2.5.0](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md#250---2024-05-13).

More information: https://github.blog/2024-05-02-introducing-artifact-attestations-now-in-public-beta/

Tested also in https://github.com/pytest-dev/pytest-mock/pull/431.

Note: even though it is technically necessary only for the `deploy` workflow, as the `test` workflow does not publish its packages, decided to always attest the provenance in both cases to avoid any surprises related to this (say a misconfiguration) when deploying.
2024-05-17 08:19:13 -03:00
Ran Benita fdf3aa3fc3
Merge pull request #12329 from pytest-dev/fix-package-scope-reorder
fixtures: fix non-working package-scope parametrization reordering
2024-05-16 10:30:34 +03:00
Josh Soref 8d00811822
Spelling and minor changes (#12122) 2024-05-15 13:49:34 -03:00
Ran Benita 1acf56d033 fixtures: fix non-working package-scope parametrization reordering
The `.parent` was incorrectly removed in
a21fb87a90, but it was actually correct
(well, it assumes that Module.path.parent == Package.path, which I'm not
sure is always correct, but that's a different matter). Restore it.

Fix #12328.
2024-05-15 17:29:43 +03:00
Ran Benita fa1d5be3e8
Merge pull request #12325 from bluetech/pytest-cache-755
cacheprovider: fix `.pytest_cache` not being world-readable
2024-05-15 10:57:10 +03:00
Ran Benita 3a64c47f1f cacheprovider: fix `.pytest_cache` not being world-readable
Fix #12308.
2024-05-14 23:47:12 +03:00
Ran Benita ae1a47e050
Merge pull request #12320 from pytest-dev/ut-breaking-change
changelog: document unittest 8.2 change as breaking
2024-05-14 23:06:48 +03:00
Ran Benita ebe2e8eac9 changelog: document unittest 8.2 change as breaking 2024-05-13 22:34:35 +03:00
Ran Benita 93dd34e76d
Merge pull request #12310 from pytest-dev/update-plugin-list/patch-5af46f3d4
[automated] Update plugin list
2024-05-13 20:34:35 +03:00
Ran Benita 704792ecbd
Merge pull request #12311 from bluetech/pkg-collect-perm-error
python: add workaround for permission error crashes from non-selected directories
2024-05-13 20:32:23 +03:00
Ran Benita d4223b9ae2
Merge pull request #12318 from bluetech/unittest-abc
python,unittest: don't collect abstract classes
2024-05-13 20:22:10 +03:00
Ran Benita 37489d3c6c python,unittest: don't collect abstract classes
Fix #12275.
2024-05-13 13:00:47 +03:00
dependabot[bot] eea04c2891
build(deps): Bump django in /testing/plugins_integration (#12314)
Bumps [django](https://github.com/django/django) from 5.0.4 to 5.0.6.
- [Commits](https://github.com/django/django/compare/5.0.4...5.0.6)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 05:56:16 +02:00
Ran Benita 90a3ef2f36 python: add workaround for permission error crashes from non-selected directories
Fix #12120.
2024-05-12 22:38:49 +03:00
pytest bot e20a08565f [automated] Update plugin list 2024-05-12 00:21:02 +00:00
Yutian Li 5af46f3d4e
Fix crashing under squashfuse_ll read-only mounts (#12301)
Fixes #12300
2024-05-09 15:04:58 -03:00
Florian Bruhin d49f1fc4db
doc: update sprint repo link (#12296) 2024-05-08 10:13:30 -03:00
Brian Okken 8efbefb2d7
New --xfail-tb flag (#12280)
Fix #12231
2024-05-07 14:37:00 -03:00
pre-commit-ci[bot] 4080459f04
[pre-commit.ci] pre-commit autoupdate (#12292)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-07 07:28:34 +02:00
Bruno Oliveira 84e59af8c8
Document exceptions raised by exit, skip, xfail, fail, and importorskip (#12285)
As commented on: https://github.com/pytest-dev/pytest/issues/7469#issuecomment-2094104215
2024-05-05 22:53:16 +00:00
github-actions[bot] 3d09c85df0
[automated] Update plugin list (#12286)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-05-04 21:26:09 -03:00
Anita Hammer 97610067ac
Handle KeyboardInterrupt and SystemExit at collection time (#12191) 2024-05-02 11:59:09 +00:00
Ben Brown 4c5298c395
Add back "Fix teardown error reporting when --maxfail=1 (#11721)" (#12279)
Closes #11706.

Originally fixed in #11721, but then reverted in #12022 due to a regression in pytest-xdist.

The regression was fixed on the pytest-xdist side in pytest-dev/pytest-xdist#1026.
2024-05-02 10:42:31 +03:00
Ran Benita c3e9bd4518
Merge pull request #12271 from bluetech/restore-bdd
testing: restore integration testing with pytest-bdd
2024-05-02 10:27:08 +03:00
Pierre Sassoulas 1385ec117d
Merge pull request #12274 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-30 18:32:12 +02:00
Pierre Sassoulas 65d7d6135e [ruff] Add change to format in the git blame ignore file 2024-04-30 18:13:04 +02:00
Pierre Sassoulas 2b6c946a2b [mypy 1.10.0] Remove 'type: ignore' that became useless 2024-04-30 18:11:06 +02:00
Pierre Sassoulas 4788165e69 [ruff UP031] Fix to use format specifiers instead of percent format 2024-04-30 18:06:26 +02:00
pre-commit-ci[bot] da53e29780
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.2)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)
2024-04-29 22:30:08 +00:00
Ran Benita b660596f61 testing: restore integration testing with pytest-bdd
The problem was fixed.
2024-04-29 20:52:35 +03:00
Ran Benita feaae2fb35
Merge pull request #12264 from bluetech/reraise-with-original-tb
fixtures,runner: fix tracebacks getting longer and longer
2024-04-29 20:02:44 +03:00
dependabot[bot] cf90008a1a
build(deps): Bump peter-evans/create-pull-request from 6.0.4 to 6.0.5 (#12268)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6.0.4 to 6.0.5.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](9153d834b6...6d6857d369)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 08:38:05 -03:00
dj 2ede8778d0
Document using PYTEST_VERSION to detect if a code is running inside pytest (#12153)
Related to #9502
2024-04-28 15:52:29 -03:00
Ran Benita 2e1dfb0c16
Merge pull request #12265 from bluetech/pre-commit-tweaks
pre-commit tweaks
2024-04-28 17:52:38 +03:00
Ran Benita 940b78232e pre-commit: fix misleading indentation 2024-04-28 17:11:11 +03:00
Ran Benita e847b2a5a9 pre-commit: replace `debug-statements` hook with ruff
Ruff supports this functionality so we can use it.
2024-04-28 17:06:07 +03:00
Ran Benita 85bc9ca954 pre-commit: remove deprecated `fix-encoding-pragma` hook
Deprecated by pre-commit.
2024-04-28 17:06:07 +03:00
Ran Benita ebc4540436
Merge pull request #12258 from bluetech/xdist-align
terminal: fix progress percentages not aligning correctly in xdist
2024-04-28 16:52:40 +03:00
Ran Benita 3e81cb2f45 runner: fix tracebacks for failed collectors getting longer and longer
Refs https://github.com/pytest-dev/pytest/issues/12204#issuecomment-2081239376
2024-04-28 13:30:05 +03:00
Ran Benita 0b91d5e3e8 fixtures: fix tracebacks for higher-scoped failed fixtures getting longer and longer
Fix #12204.
2024-04-28 13:06:32 +03:00
Ran Benita 50d1e81713 terminal: fix progress percentages not aligning correctly in xdist
Fix #7166
2024-04-28 11:33:18 +03:00
Ran Benita 1a84d233f3 terminal: some minor code cleanups
No logical changed intended.
2024-04-28 11:33:18 +03:00
Ran Benita 127a372928
Merge pull request #12259 from bluetech/plain-raise
runner: avoid adding uninteresting entry to tracebacks
2024-04-28 11:21:15 +03:00
Bruno Oliveira 2a809e6f8b
Merge pull request #12261 from pytest-dev/update-plugin-list/patch-9eecdc7da
[automated] Update plugin list
2024-04-27 22:09:19 -03:00
pytest bot 852df0dc3e [automated] Update plugin list 2024-04-28 00:20:56 +00:00
Bruno Oliveira 9eecdc7dae
Merge pull request #12260 from nicoddemus/cherry-pick-release
Cherry-pick release pytest-dev/release-8.2.0
2024-04-27 20:41:36 -03:00
Bruno Oliveira 79ca819e59 Merge pull request #12257 from pytest-dev/release-8.2.0
Prepare release 8.2.0

(cherry picked from commit 69c3bcea36)
2024-04-27 20:36:02 -03:00
Ran Benita fc5c960304 runner: avoid adding uninteresting entry to tracebacks
In these two cases which re-raise an immediately-caught exception, do
the re-raising with `raise` instead of `raise exc`, because the `raise
exc` adds an uninteresting entry to the exception's traceback (that of
itself), while `raise` doesn't.
2024-04-28 01:42:43 +03:00
Sviatoslav Sydorenko 30ce631fb9
Securely pin re-actors/alls-green to v1.2 commit 2023-12-07 01:29:02 +01:00
Sviatoslav Sydorenko b92fde37ce
Introduce a gate/check GHA job
This adds a GHA job that reliably determines if all the required
dependencies have succeeded or not.

It also allows to reduce the list of required branch protection CI
statuses to just one — `check`. This reduces the maintenance burden
by a lot and have been battle-tested across a small bunch of projects
in its action form and in-house implementations of other people.

I was curious about the spread of use. And I've just discovered that
it is now in use in aiohttp (and other aio-libs projects), CherryPy,
some of the Ansible repositories, all of the jaraco's projects (like
`setuptools`, `importlib_metadata`), some PyCQA, PyCA and pytest
projects, a few AWS Labs projects. Admittedly, I maintain a few of
these but it seems to address some of the pain folks have:
https://github.com/jaraco/skeleton/pull/55#issuecomment-1106638475.

The story behind this is explained in more detail at
https://github.com/marketplace/actions/alls-green#why.
2023-12-07 01:29:02 +01:00
108 changed files with 2286 additions and 1094 deletions

View File

@ -31,3 +31,5 @@ c9df77cbd6a365dcb73c39618e4842711817e871
4546d5445aaefe6a03957db028c263521dfb5c4b
# Migration to ruff / ruff format
4588653b2497ed25976b7aaff225b889fb476756
# Use format specifiers instead of percent format
4788165e69d08e10fc6b9c0124083fb358e2e9b0

View File

@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.5.0
uses: hynek/build-and-inspect-python-package@v2.6.0
with:
attest-build-provenance-github: 'true'
@ -54,7 +54,7 @@ jobs:
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.14
uses: pypa/gh-action-pypi-publish@v1.9.0
- name: Push tag
run: |

View File

@ -14,6 +14,11 @@ on:
branches:
- main
- "[0-9]+.[0-9]+.x"
types:
- opened # default
- synchronize # default
- reopened # default
- ready_for_review # used in PRs created from the release workflow
env:
PYTEST_ADDOPTS: "--color=yes"
@ -35,7 +40,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.5.0
uses: hynek/build-and-inspect-python-package@v2.6.0
build:
needs: [package]

View File

@ -46,7 +46,8 @@ jobs:
run: python scripts/update-plugin-list.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
id: pr
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c
with:
commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>'
@ -55,3 +56,13 @@ jobs:
branch-suffix: short-commit-hash
title: '[automated] Update plugin list'
body: '[automated] Update plugin list'
draft: true
- name: Instruct the maintainers to trigger CI by undrafting the PR
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh pr comment
--body 'Please mark the PR as ready for review to trigger PR checks.'
--repo '${{ github.repository }}'
'${{ steps.pr.outputs.pull-request-number }}'

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.4.1"
rev: "v0.4.10"
hooks:
- id: ruff
args: ["--fix"]
@ -10,12 +10,7 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: fix-encoding-pragma
args: [--remove]
- id: check-yaml
- id: debug-statements
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.16.0
hooks:
@ -26,7 +21,7 @@ repos:
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
rev: v1.10.0
hooks:
- id: mypy
files: ^(src/|testing/|scripts/)
@ -43,20 +38,25 @@ repos:
# on <3.11
- exceptiongroup>=1.0.0rc8
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.8.0"
rev: "2.1.3"
hooks:
- id: pyproject-fmt
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
additional_dependencies: ["tox>=4.9"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.16.0
hooks:
- id: pyupgrade
stages: [manual]
- repo: local
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
args: ["-rn", "-sn", "--fail-on=I"]
stages: [manual]
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
args: ["-rn", "-sn", "--fail-on=I"]
stages: [manual]
- id: rst
name: rst
entry: rst-lint --encoding utf-8

View File

@ -149,6 +149,7 @@ Evgeny Seliverstov
Fabian Sturm
Fabien Zarifian
Fabio Zadrozny
Farbod Ahmadian
faph
Felix Hofstätter
Felix Nieuwenhuizen
@ -212,6 +213,7 @@ Jordan Guymon
Jordan Moldow
Jordan Speicher
Joseph Hunkeler
Joseph Sawaya
Josh Karpel
Joshua Bronson
Jurko Gospodnetić
@ -244,6 +246,7 @@ Levon Saldamli
Lewis Cowles
Llandy Riveron Del Risco
Loic Esteve
lovetheguitar
Lukas Bednar
Luke Murphy
Maciek Fijalkowski
@ -280,6 +283,7 @@ Michael Droettboom
Michael Goerz
Michael Krebs
Michael Seifert
Michael Vogt
Michal Wajszczuk
Michał Górny
Michał Zięba
@ -426,6 +430,7 @@ Victor Rodriguez
Victor Uriarte
Vidar T. Fauske
Vijay Arora
Virendra Patil
Virgil Dupras
Vitaly Lashmanov
Vivaan Verma
@ -450,6 +455,7 @@ Yusuke Kadowaki
Yutian Li
Yuval Shimon
Zac Hatfield-Dodds
Zach Snicker
Zachary Kneupper
Zachary OBrien
Zhouxin Qiu

View File

@ -10,7 +10,7 @@ if __name__ == "__main__":
import pytest # noqa: F401
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
cProfile.run(f"pytest.cmdline.main({script!r})", "prof")
p = pstats.Stats("prof")
p.strip_dirs()
p.sort_stats("cumulative")

View File

@ -0,0 +1,4 @@
Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
Originally added in pytest 8.0.0, but reverted in 8.0.2 due to a regression in pytest-xdist.
This regression was fixed in pytest-xdist 3.6.1.

View File

@ -0,0 +1 @@
:func:`pytest.approx` now correctly handles :class:`Sequence <collections.abc.Sequence>`-like objects.

1
changelog/12153.doc.rst Normal file
View File

@ -0,0 +1 @@
Documented using :envvar:`PYTEST_VERSION` to detect if code is running from within a pytest run.

View File

@ -0,0 +1,11 @@
Fixed a regression in pytest 8.0 where tracebacks get longer and longer when multiple
tests fail due to a shared higher-scope fixture which raised -- by :user:`bluetech`.
Also fixed a similar regression in pytest 5.4 for collectors which raise during setup.
The fix necessitated internal changes which may affect some plugins:
* ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)``
instead of ``exc``.
* ``SetupState.stack`` failures are now a tuple ``(exc, tb)``
instead of ``exc``.

View File

@ -0,0 +1,11 @@
Added `--xfail-tb` flag, which turns on traceback output for XFAIL results.
* If the `--xfail-tb` flag is not sent, tracebacks for XFAIL results are NOT shown.
* The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`.
* Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`.
Some history:
With pytest 8.0, `-rx` or `-ra` would not only turn on summary reports for xfail, but also report the tracebacks for xfail results. This caused issues with some projects that utilize xfail, but don't want to see all of the xfail tracebacks.
This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. With this, the default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. While this is a behavior change, it brings default behavior back to pre-8.0.0 behavior, which ultimately was considered the better course of action.

1
changelog/12264.bugfix.rst Symbolic link
View File

@ -0,0 +1 @@
12204.bugfix.rst

View File

@ -0,0 +1 @@
Fix collection error upon encountering an :mod:`abstract <abc>` class, including abstract `unittest.TestCase` subclasses.

View File

@ -0,0 +1,8 @@
Added support for keyword matching in marker expressions.
Now tests can be selected by marker keyword arguments.
Supported values are :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None`.
See :ref:`marker examples <marker_keyword_expression_example>` for more information.
-- by :user:`lovetheguitar`

View File

@ -0,0 +1 @@
Fix a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases.

6
changelog/12469.doc.rst Normal file
View File

@ -0,0 +1,6 @@
The external plugin mentions in the documentation now avoid mentioning
:std:doc:`setuptools entry-points <setuptools:index>` as the concept is
much more generic nowadays. Instead, the terminology of "external",
"installed", or "third-party" plugins (or packages) replaces that.
-- by :user:`webknjaz`

View File

@ -0,0 +1,4 @@
The console output now uses the "third-party plugins" terminology,
replacing the previously established but confusing and outdated
reference to :std:doc:`setuptools <setuptools:index>`
-- by :user:`webknjaz`.

View File

@ -0,0 +1 @@
Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`.

View File

@ -0,0 +1,7 @@
The UX of the GitHub automation making pull requests to update the
plugin list has been updated. Previously, the maintainers had to close
the automatically created pull requests and re-open them to trigger the
CI runs. From now on, they only need to click the `Ready for review`
button instead.
-- by :user:`webknjaz`

View File

@ -0,0 +1 @@
Improve handling of invalid regex patterns in :func:`pytest.raises(match=r'...') <pytest.raises>` by providing a clear error message.

View File

@ -0,0 +1,3 @@
The ``_in_venv()`` function now detects Python virtual environments by
checking for a :file:`pyvenv.cfg` file, ensuring reliable detection on
various platforms -- by :user:`zachsnickers`.

View File

@ -0,0 +1 @@
12544.improvement.rst

View File

@ -0,0 +1 @@
Do not truncate arguments to functions in output when running with `-vvv`.

View File

@ -0,0 +1,38 @@
The readability of assertion introspection of bound methods has been enhanced
-- by :user:`farbodahm`, :user:`webknjaz`, :user:`obestwalter`, :user:`flub`
and :user:`glyphack`.
Earlier, it was like:
.. code-block:: console
=================================== FAILURES ===================================
_____________________________________ test _____________________________________
def test():
> assert Help().fun() == 2
E assert 1 == 2
E + where 1 = <bound method Help.fun of <example.Help instance at 0x256a830>>()
E + where <bound method Help.fun of <example.Help instance at 0x256a830>> = <example.Help instance at 0x256a830>.fun
E + where <example.Help instance at 0x256a830> = Help()
example.py:7: AssertionError
=========================== 1 failed in 0.03 seconds ===========================
And now it's like:
.. code-block:: console
=================================== FAILURES ===================================
_____________________________________ test _____________________________________
def test():
> assert Help().fun() == 2
E assert 1 == 2
E + where 1 = fun()
E + where fun = <test_local.Help object at 0x1074be230>.fun
E + where <test_local.Help object at 0x1074be230> = Help()
test_local.py:13: AssertionError
=========================== 1 failed in 0.03 seconds ===========================

View File

@ -0,0 +1 @@
Fixed progress percentages (the ``[ 87%]`` at the edge of the screen) sometimes not aligning correctly when running with pytest-xdist ``-n``.

View File

@ -0,0 +1 @@
Added timezone information to the testsuite timestamp in the JUnit XML report.

View File

@ -237,7 +237,7 @@ html_theme = "furo"
html_title = "pytest documentation"
# A shorter title for the navigation bar. Default is the same as html_title.
html_short_title = "pytest-%s" % release
html_short_title = f"pytest-{release}"
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.

View File

@ -25,10 +25,12 @@ You can "mark" a test function with custom metadata like this:
pass # perform some webtest test for your app
@pytest.mark.device(serial="123")
def test_something_quick():
pass
@pytest.mark.device(serial="abc")
def test_another():
pass
@ -71,6 +73,28 @@ Or the inverse, running all tests except the webtest ones:
===================== 3 passed, 1 deselected in 0.12s ======================
.. _`marker_keyword_expression_example`:
Additionally, you can restrict a test run to only run tests matching one or multiple marker
keyword arguments, e.g. to run only tests marked with ``device`` and the specific ``serial="123"``:
.. code-block:: pytest
$ pytest -v -m "device(serial='123')"
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selected
test_server.py::test_something_quick PASSED [100%]
===================== 1 passed, 3 deselected in 0.12s ======================
.. note:: Only keyword argument matching is supported in marker expressions.
.. note:: Only :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None` values are supported in marker expressions.
Selecting tests based on their node ID
--------------------------------------

View File

@ -212,7 +212,7 @@ the command line arguments before they get processed:
.. code-block:: python
# setuptools plugin
# installable external plugin
import sys
@ -405,35 +405,20 @@ Detect if running from within a pytest run
Usually it is a bad idea to make application code
behave differently if called from a test. But if you
absolutely must find out if your application code is
running from a test you can do something like this:
running from a test you can do this:
.. code-block:: python
# content of your_module.py
import os
_called_from_test = False
.. code-block:: python
# content of conftest.py
def pytest_configure(config):
your_module._called_from_test = True
and then check for the ``your_module._called_from_test`` flag:
.. code-block:: python
if your_module._called_from_test:
# called from within a test run
if os.environ.get("PYTEST_VERSION") is not None:
# Things you want to to do if your code is called by pytest.
...
else:
# called "normally"
# Things you want to to do if your code is not called by pytest.
...
accordingly in your application.
Adding info to test report header
--------------------------------------------------------------
@ -1088,8 +1073,8 @@ Instead of freezing the pytest runner as a separate executable, you can make
your frozen program work as the pytest runner by some clever
argument handling during program startup. This allows you to
have a single executable, which is usually more convenient.
Please note that the mechanism for plugin discovery used by pytest
(setuptools entry points) doesn't work with frozen executables so pytest
Please note that the mechanism for plugin discovery used by pytest (:ref:`entry
points <pip-installable plugins>`) doesn't work with frozen executables so pytest
can't find any third party plugins automatically. To include third party plugins
like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.

View File

@ -1931,7 +1931,7 @@ The same applies for the test folder level obviously.
Using fixtures from other projects
----------------------------------
Usually projects that provide pytest support will use :ref:`entry points <setuptools entry points>`,
Usually projects that provide pytest support will use :ref:`entry points <pip-installable plugins>`,
so just installing those projects into an environment will make those fixtures available for use.
In case you want to use fixtures from a project that does not use entry points, you can

View File

@ -76,11 +76,19 @@ Specifying a specific parametrization of a test:
**Run tests by marker expressions**
To run all tests which are decorated with the ``@pytest.mark.slow`` decorator:
.. code-block:: bash
pytest -m slow
Will run all tests which are decorated with the ``@pytest.mark.slow`` decorator.
To run all tests which are decorated with the annotated ``@pytest.mark.slow(phase=1)`` decorator,
with the ``phase`` keyword argument set to ``1``:
.. code-block:: bash
pytest -m "slow(phase=1)"
For more information see :ref:`marks <mark>`.
@ -154,7 +162,7 @@ You can early-load plugins (internal and external) explicitly in the command-lin
The option receives a ``name`` parameter, which can be:
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is
* The entry-point name of a plugin. This is the name passed to ``importlib`` when the plugin is
registered. For example to early-load the :pypi:`pytest-cov` plugin you can use::
pytest -p pytest_cov

View File

@ -16,8 +16,8 @@ reporting by calling :ref:`well specified hooks <hook-reference>` of the followi
* builtin plugins: loaded from pytest's internal ``_pytest`` directory.
* :ref:`external plugins <extplugins>`: modules discovered through
`setuptools entry points`_
* :ref:`external plugins <extplugins>`: installed third-party modules discovered
through :ref:`entry points <pip-installable plugins>` in their packaging metadata
* `conftest.py plugins`_: modules auto-discovered in test directories
@ -42,7 +42,8 @@ Plugin discovery order at tool startup
3. by scanning the command line for the ``-p name`` option
and loading the specified plugin. This happens before normal command-line parsing.
4. by loading all plugins registered through `setuptools entry points`_.
4. by loading all plugins registered through installed third-party package
:ref:`entry points <pip-installable plugins>`.
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
@ -142,7 +143,8 @@ Making your plugin installable by others
If you want to make your plugin externally available, you
may define a so-called entry point for your distribution so
that ``pytest`` finds your plugin module. Entry points are
a feature that is provided by :std:doc:`setuptools <setuptools:index>`.
a feature that is provided by :std:doc:`packaging tools
<packaging:specifications/entry-points>`.
pytest looks up the ``pytest11`` entrypoint to discover its
plugins, thus you can make your plugin available by defining
@ -265,8 +267,9 @@ of the variable will also be loaded as plugins, and so on.
tests root directory is deprecated, and will raise a warning.
This mechanism makes it easy to share fixtures within applications or even
external applications without the need to create external plugins using
the ``setuptools``'s entry point technique.
external applications without the need to create external plugins using the
:std:doc:`entry point packaging metadata
<packaging:guides/creating-and-discovering-plugins>` technique.
Plugins imported by :globalvar:`pytest_plugins` will also automatically be marked
for assertion rewriting (see :func:`pytest.register_assert_rewrite`).

View File

@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
setup.cfg
~~~~~~~~~
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and :std:doc:`setuptools <setuptools:userguide/declarative_config>`, and can also be used to hold pytest configuration
if they have a ``[tool:pytest]`` section.
.. code-block:: ini

File diff suppressed because it is too large Load Diff

View File

@ -650,7 +650,7 @@ Reference to all hooks which can be implemented by :ref:`conftest.py files <loca
Bootstrapping hooks
~~~~~~~~~~~~~~~~~~~
Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
Bootstrapping hooks called for plugins registered early enough (internal and third-party plugins).
.. hook:: pytest_load_initial_conftests
.. autofunction:: pytest_load_initial_conftests
@ -1147,8 +1147,9 @@ When set, pytest will print tracing and debug information.
.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD
When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be
loaded.
When set, disables plugin auto-loading through :std:doc:`entry point packaging
metadata <packaging:guides/creating-and-discovering-plugins>`. Only explicitly
specified plugins will be loaded.
.. envvar:: PYTEST_PLUGINS
@ -1701,13 +1702,13 @@ passed multiple times. The expected format is ``name=value``. For example::
This would tell ``pytest`` to not look into typical subversion or
sphinx-build directories or into any ``tmp`` prefixed directory.
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
virtualenv by the presence of an activation script. Any directory deemed to
be the root of a virtual environment will not be considered during test
collection unless ``--collect-in-virtualenv`` is given. Note also that
``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
you intend to run tests in a virtualenv with a base directory that matches
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
Additionally, ``pytest`` will attempt to intelligently identify and ignore
a virtualenv. Any directory deemed to be the root of a virtual environment
will not be considered during test collection unless
``--collect-in-virtualenv`` is given. Note also that ``norecursedirs``
takes precedence over ``--collect-in-virtualenv``; e.g. if you intend to
run tests in a virtualenv with a base directory that matches ``'.*'`` you
*must* override ``norecursedirs`` in addition to using the
``--collect-in-virtualenv`` flag.

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import json
from pathlib import Path
import sys
import requests
@ -19,7 +20,7 @@ def get_issues():
if r.status_code == 403:
# API request limit exceeded
print(data["message"])
exit(1)
sys.exit(1)
issues.extend(data)
# Look for next page
@ -62,7 +63,7 @@ def report(issues):
kind = _get_kind(issue)
status = issue["state"]
number = issue["number"]
link = "https://github.com/pytest-dev/pytest/issues/%s/" % number
link = f"https://github.com/pytest-dev/pytest/issues/{number}/"
print("----")
print(status, kind, link)
print(title)
@ -71,7 +72,7 @@ def report(issues):
# print("\n".join(lines[:3]))
# if len(lines) > 3 or len(body) > 240:
# print("...")
print("\n\nFound %s open issues" % len(issues))
print(f"\n\nFound {len(issues)} open issues")
if __name__ == "__main__":

View File

@ -1,3 +1,10 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=61",
"setuptools-scm[toml]>=6.2.3",
]
[project]
name = "pytest"
description = "pytest: simple powerful testing with Python"
@ -31,7 +38,6 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Testing",
"Topic :: Utilities",
@ -40,15 +46,14 @@ dynamic = [
"version",
]
dependencies = [
'colorama; sys_platform == "win32"',
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
"colorama; sys_platform=='win32'",
"exceptiongroup>=1.0.0rc8; python_version<'3.11'",
"iniconfig",
"packaging",
"pluggy<2.0,>=1.5",
'tomli>=1; python_version < "3.11"',
"pluggy<2,>=1.5",
"tomli>=1; python_version<'3.11'",
]
[project.optional-dependencies]
dev = [
optional-dependencies.dev = [
"argcomplete",
"attrs>=19.2",
"hypothesis>=3.56",
@ -58,59 +63,55 @@ dev = [
"setuptools",
"xmlschema",
]
[project.urls]
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
Homepage = "https://docs.pytest.org/en/latest/"
Source = "https://github.com/pytest-dev/pytest"
Tracker = "https://github.com/pytest-dev/pytest/issues"
Twitter = "https://twitter.com/pytestdotorg"
[project.scripts]
"py.test" = "pytest:console_main"
pytest = "pytest:console_main"
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=61",
"setuptools-scm[toml]>=6.2.3",
]
urls.Changelog = "https://docs.pytest.org/en/stable/changelog.html"
urls.Homepage = "https://docs.pytest.org/en/latest/"
urls.Source = "https://github.com/pytest-dev/pytest"
urls.Tracker = "https://github.com/pytest-dev/pytest/issues"
urls.Twitter = "https://twitter.com/pytestdotorg"
scripts."py.test" = "pytest:console_main"
scripts.pytest = "pytest:console_main"
[tool.setuptools.package-data]
"_pytest" = ["py.typed"]
"pytest" = ["py.typed"]
"_pytest" = [
"py.typed",
]
"pytest" = [
"py.typed",
]
[tool.setuptools_scm]
write_to = "src/_pytest/_version.py"
[tool.black]
target-version = ['py38']
target-version = [
'py38',
]
[tool.ruff]
src = ["src"]
line-length = 88
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = [
"B", # bugbear
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"FA100", # add future annotations
"PYI", # flake8-pyi
"UP", # pyupgrade
"RUF", # ruff
"W", # pycodestyle
"PIE", # flake8-pie
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
"PLE", # pylint error
"PLW", # pylint warning
"PLR1714", # Consider merging multiple comparisons
src = [
"src",
]
ignore = [
format.docstring-code-format = true
lint.select = [
"B", # bugbear
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"FA100", # add future annotations
"I", # isort
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
"PIE", # flake8-pie
"PLE", # pylint error
"PLR1714", # Consider merging multiple comparisons
"PLW", # pylint warning
"PYI", # flake8-pyi
"RUF", # ruff
"T100", # flake8-debugger
"UP", # pyupgrade
"W", # pycodestyle
]
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
@ -118,10 +119,6 @@ ignore = [
"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
# pycodestyle ignore
# 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()`
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
@ -131,41 +128,54 @@ ignore = [
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
"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()`
# pylint ignore
"PLR5501", # Use `elif` instead of `else` then `if`
"PLW0120", # remove the else and dedent its contents
"PLW0603", # Using the global statement
"PLW2901", # for loop variable overwritten by assignment target
# ruff ignore
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
# pylint ignore
"PLW0603", # Using the global statement
"PLW0120", # remove the else and dedent its contents
"PLW2901", # for loop variable overwritten by assignment target
"PLR5501", # Use `elif` instead of `else` then `if`
]
[tool.ruff.lint.pycodestyle]
lint.per-file-ignores."src/_pytest/_py/**/*.py" = [
"B",
"PYI",
]
lint.per-file-ignores."src/_pytest/_version.py" = [
"I001",
]
lint.per-file-ignores."testing/python/approx.py" = [
"B015",
]
lint.extend-safe-fixes = [
"UP006",
"UP007",
]
lint.isort.combine-as-imports = true
lint.isort.force-single-line = true
lint.isort.force-sort-within-sections = true
lint.isort.known-local-folder = [
"pytest",
"_pytest",
]
lint.isort.lines-after-imports = 2
lint.isort.order-by-type = false
lint.isort.required-imports = [
"from __future__ import annotations",
]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
known-local-folder = ["pytest", "_pytest"]
lines-after-imports = 2
[tool.ruff.lint.per-file-ignores]
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
"src/_pytest/_version.py" = ["I001"]
"testing/python/approx.py" = ["B015"]
lint.pycodestyle.max-line-length = 120
lint.pydocstyle.convention = "pep257"
lint.pyupgrade.keep-runtime-typing = false
[tool.pylint.main]
# Maximum number of characters on a single line.
@ -180,28 +190,25 @@ disable = [
"bad-mcs-method-argument",
"broad-exception-caught",
"broad-exception-raised",
"cell-var-from-loop",
"cell-var-from-loop", # B023 from ruff / flake8-bugbear
"comparison-of-constants",
"comparison-with-callable",
"comparison-with-itself",
"condition-evals-to-constant",
"consider-using-dict-items",
"consider-using-enumerate",
"consider-using-from-import",
"consider-using-f-string",
"consider-using-in",
"consider-using-sys-exit",
"consider-using-ternary",
"consider-using-with",
"cyclic-import",
"disallowed-name",
"disallowed-name", # foo / bar are used often in tests
"duplicate-code",
"eval-used",
"exec-used",
"expression-not-assigned",
"fixme",
"global-statement",
"implicit-str-concat",
"import-error",
"import-outside-toplevel",
"inconsistent-return-statements",
@ -212,10 +219,9 @@ disable = [
"keyword-arg-before-vararg",
"line-too-long",
"method-hidden",
"misplaced-bare-raise",
"missing-docstring",
"missing-timeout",
"multiple-statements",
"multiple-statements", # multiple-statements-on-one-line-colon (E701) from ruff
"no-else-break",
"no-else-continue",
"no-else-raise",
@ -228,6 +234,7 @@ disable = [
"pointless-exception-statement",
"pointless-statement",
"pointless-string-statement",
"possibly-used-before-assignment",
"protected-access",
"raise-missing-from",
"redefined-argument-from-local",
@ -275,7 +282,6 @@ disable = [
"useless-else-on-loop",
"useless-import-alias",
"useless-return",
"use-maxsplit-arg",
"using-constant-test",
"wrong-import-order",
]
@ -291,16 +297,27 @@ indent = 4
[tool.pytest.ini_options]
minversion = "2.0"
addopts = "-rfEX -p pytester --strict-markers"
python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
python_classes = ["Test", "Acceptance"]
python_functions = ["test"]
python_files = [
"test_*.py",
"*_test.py",
"testing/python/*.py",
]
python_classes = [
"Test",
"Acceptance",
]
python_functions = [
"test",
]
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
testpaths = ["testing"]
testpaths = [
"testing",
]
norecursedirs = [
"testing/example_scripts",
".*",
"build",
"dist",
"testing/example_scripts",
".*",
"build",
"dist",
]
xfail_strict = true
filterwarnings = [
@ -341,6 +358,9 @@ markers = [
"foo",
"bar",
"baz",
"number_mark",
"builtin_matchers_mark",
"str_mark",
# conftest.py reorders tests moving slow ones to the end of the list
"slow",
# experimental mark for all tests using pexpect
@ -426,8 +446,14 @@ name = "Miscellaneous internal changes"
showcontent = true
[tool.mypy]
files = ["src", "testing", "scripts"]
mypy_path = ["src"]
files = [
"src",
"testing",
"scripts",
]
mypy_path = [
"src",
]
check_untyped_defs = true
disallow_any_generics = true
disallow_untyped_defs = true

View File

@ -54,7 +54,7 @@ from _pytest.pathlib import bestrelpath
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]]
@ -618,12 +618,13 @@ class ExceptionInfo(Generic[E]):
def getrepr(
self,
showlocals: bool = False,
style: _TracebackStyle = "long",
style: TracebackStyle = "long",
abspath: bool = False,
tbfilter: bool
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
funcargs: bool = False,
truncate_locals: bool = True,
truncate_args: bool = True,
chain: bool = True,
) -> ReprExceptionInfo | ExceptionChainRepr:
"""Return str()able representation of this exception info.
@ -654,6 +655,9 @@ class ExceptionInfo(Generic[E]):
:param bool truncate_locals:
With ``showlocals==True``, make sure locals can be safely represented as strings.
:param bool truncate_args:
With ``showargs==True``, make sure args can be safely represented as strings.
:param bool chain:
If chained exceptions in Python 3 should be shown.
@ -680,6 +684,7 @@ class ExceptionInfo(Generic[E]):
tbfilter=tbfilter,
funcargs=funcargs,
truncate_locals=truncate_locals,
truncate_args=truncate_args,
chain=chain,
)
return fmt.repr_excinfo(self)
@ -793,11 +798,12 @@ class FormattedExcinfo:
fail_marker: ClassVar = "E"
showlocals: bool = False
style: _TracebackStyle = "long"
style: TracebackStyle = "long"
abspath: bool = True
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
funcargs: bool = False
truncate_locals: bool = True
truncate_args: bool = True
chain: bool = True
astcache: dict[str | Path, ast.AST] = dataclasses.field(
default_factory=dict, init=False, repr=False
@ -828,7 +834,11 @@ class FormattedExcinfo:
if self.funcargs:
args = []
for argname, argvalue in entry.frame.getargs(var=True):
args.append((argname, saferepr(argvalue)))
if self.truncate_args:
str_repr = saferepr(argvalue)
else:
str_repr = saferepr(argvalue, maxsize=None)
args.append((argname, str_repr))
return ReprFuncArgs(args)
return None
@ -928,7 +938,7 @@ class FormattedExcinfo:
s = self.get_source(source, line_index, excinfo, short=short)
lines.extend(s)
if short:
message = "in %s" % (entry.name)
message = f"in {entry.name}"
else:
message = excinfo and excinfo.typename or ""
entry_path = entry.path
@ -1147,7 +1157,7 @@ class ReprExceptionInfo(ExceptionRepr):
class ReprTraceback(TerminalRepr):
reprentries: Sequence[ReprEntry | ReprEntryNative]
extraline: str | None
style: _TracebackStyle
style: TracebackStyle
entrysep: ClassVar = "_ "
@ -1181,7 +1191,7 @@ class ReprTracebackNative(ReprTraceback):
class ReprEntryNative(TerminalRepr):
lines: Sequence[str]
style: ClassVar[_TracebackStyle] = "native"
style: ClassVar[TracebackStyle] = "native"
def toterminal(self, tw: TerminalWriter) -> None:
tw.write("".join(self.lines))
@ -1193,7 +1203,7 @@ class ReprEntry(TerminalRepr):
reprfuncargs: ReprFuncArgs | None
reprlocals: ReprLocals | None
reprfileloc: ReprFileLocation | None
style: _TracebackStyle
style: TracebackStyle
def _write_entry_lines(self, tw: TerminalWriter) -> None:
"""Write the source code portions of a list of traceback entries with syntax highlighting.

View File

@ -613,7 +613,7 @@ class PrettyPrinter:
vrepr = self._safe_repr(v, context, maxlevels, level)
append(f"{krepr}: {vrepr}")
context.remove(objid)
return "{%s}" % ", ".join(components)
return "{{{}}}".format(", ".join(components))
if (issubclass(typ, list) and r is list.__repr__) or (
issubclass(typ, tuple) and r is tuple.__repr__

View File

@ -60,7 +60,6 @@ class SafeRepr(reprlib.Repr):
s = ascii(x)
else:
s = super().repr(x)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:

View File

@ -9,11 +9,17 @@ from typing import final
from typing import Literal
from typing import Sequence
from typing import TextIO
from typing import TYPE_CHECKING
from ..compat import assert_never
from .wcwidth import wcswidth
if TYPE_CHECKING:
from pygments.formatter import Formatter
from pygments.lexer import Lexer
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
@ -105,7 +111,7 @@ class TerminalWriter:
if self.hasmarkup:
esc = [self._esctable[name] for name, on in markup.items() if on]
if esc:
text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m"
text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m"
return text
def sep(
@ -195,58 +201,74 @@ class TerminalWriter:
for indent, new_line in zip(indents, new_lines):
self.line(indent + new_line)
def _highlight(
self, source: str, lexer: Literal["diff", "python"] = "python"
) -> str:
"""Highlight the given source if we have markup support."""
def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None:
try:
if lexer == "python":
from pygments.lexers.python import PythonLexer
return PythonLexer()
elif lexer == "diff":
from pygments.lexers.diff import DiffLexer
return DiffLexer()
else:
assert_never(lexer)
except ModuleNotFoundError:
return None
def _get_pygments_formatter(self) -> Formatter | None:
try:
import pygments.util
except ModuleNotFoundError:
return None
from _pytest.config.exceptions import UsageError
if not source or not self.hasmarkup or not self.code_highlight:
return source
theme = os.getenv("PYTEST_THEME")
theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
try:
from pygments.formatters.terminal import TerminalFormatter
if lexer == "python":
from pygments.lexers.python import PythonLexer as Lexer
elif lexer == "diff":
from pygments.lexers.diff import DiffLexer as Lexer
else:
assert_never(lexer)
from pygments import highlight
import pygments.util
except ImportError:
return source
else:
try:
highlighted: str = highlight(
source,
Lexer(),
TerminalFormatter(
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
style=os.getenv("PYTEST_THEME"),
),
)
# pygments terminal formatter may add a newline when there wasn't one.
# We don't want this, remove.
if highlighted[-1] == "\n" and source[-1] != "\n":
highlighted = highlighted[:-1]
return TerminalFormatter(bg=theme_mode, style=theme)
# Some lexers will not set the initial color explicitly
# which may lead to the previous color being propagated to the
# start of the expression, so reset first.
return "\x1b[0m" + highlighted
except pygments.util.ClassNotFound as e:
raise UsageError(
"PYTEST_THEME environment variable had an invalid value: '{}'. "
"Only valid pygment styles are allowed.".format(
os.getenv("PYTEST_THEME")
)
) from e
except pygments.util.OptionError as e:
raise UsageError(
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
"The only allowed values are 'dark' and 'light'.".format(
os.getenv("PYTEST_THEME_MODE")
)
) from e
except pygments.util.ClassNotFound as e:
raise UsageError(
f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
"Hint: See available pygments styles with `pygmentize -L styles`."
) from e
except pygments.util.OptionError as e:
raise UsageError(
f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. "
"The allowed values are 'dark' (default) and 'light'."
) from e
def _highlight(
self, source: str, lexer: Literal["diff", "python"] = "python"
) -> str:
"""Highlight the given source if we have markup support."""
if not source or not self.hasmarkup or not self.code_highlight:
return source
pygments_lexer = self._get_pygments_lexer(lexer)
if pygments_lexer is None:
return source
pygments_formatter = self._get_pygments_formatter()
if pygments_formatter is None:
return source
from pygments import highlight
highlighted: str = highlight(source, pygments_lexer, pygments_formatter)
# pygments terminal formatter may add a newline when there wasn't one.
# We don't want this, remove.
if highlighted[-1] == "\n" and source[-1] != "\n":
highlighted = highlighted[:-1]
# Some lexers will not set the initial color explicitly
# which may lead to the previous color being propagated to the
# start of the expression, so reset first.
highlighted = "\x1b[0m" + highlighted
return highlighted

View File

@ -161,15 +161,13 @@ class Visitor:
)
if not self.breadthfirst:
for subdir in dirs:
for p in self.gen(subdir):
yield p
yield from self.gen(subdir)
for p in self.optsort(entries):
if self.fil is None or self.fil(p):
yield p
if self.breadthfirst:
for subdir in dirs:
for p in self.gen(subdir):
yield p
yield from self.gen(subdir)
class FNMatcher:
@ -659,7 +657,7 @@ class LocalPath:
)
if "basename" in kw:
if "purebasename" in kw or "ext" in kw:
raise ValueError("invalid specification %r" % kw)
raise ValueError(f"invalid specification {kw!r}")
else:
pb = kw.setdefault("purebasename", purebasename)
try:
@ -705,7 +703,7 @@ class LocalPath:
elif name == "ext":
res.append(ext)
else:
raise ValueError("invalid part specification %r" % name)
raise ValueError(f"invalid part specification {name!r}")
return res
def dirpath(self, *args, **kwargs):
@ -1026,7 +1024,7 @@ class LocalPath:
return self.stat().atime
def __repr__(self):
return "local(%r)" % self.strpath
return f"local({self.strpath!r})"
def __str__(self):
"""Return string representation of the Path."""

View File

@ -97,7 +97,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
state = self.config.stash[assertstate_key]
if self._early_rewrite_bailout(name, state):
return None
state.trace("find_module called for: %s" % name)
state.trace(f"find_module called for: {name}")
# Type ignored because mypy is confused about the `self` binding here.
spec = self._find_spec(name, path) # type: ignore
@ -269,7 +269,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
self.config.issue_config_time_warning(
PytestAssertRewriteWarning(
"Module already imported so cannot be rewritten: %s" % name
f"Module already imported so cannot be rewritten: {name}"
),
stacklevel=5,
)
@ -370,21 +370,21 @@ def _read_pyc(
return None
# Check for invalid or out of date pyc file.
if len(data) != (16):
trace("_read_pyc(%s): invalid pyc (too short)" % source)
trace(f"_read_pyc({source}): invalid pyc (too short)")
return None
if data[:4] != importlib.util.MAGIC_NUMBER:
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
return None
if data[4:8] != b"\x00\x00\x00\x00":
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
return None
mtime_data = data[8:12]
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
trace("_read_pyc(%s): out of date" % source)
trace(f"_read_pyc({source}): out of date")
return None
size_data = data[12:16]
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
return None
try:
co = marshal.load(fp)
@ -392,7 +392,7 @@ def _read_pyc(
trace(f"_read_pyc({source}): marshal.load error {e}")
return None
if not isinstance(co, types.CodeType):
trace("_read_pyc(%s): not a code object" % source)
trace(f"_read_pyc({source}): not a code object")
return None
return co
@ -417,6 +417,10 @@ def _saferepr(obj: object) -> str:
sequences, especially '\n{' and '\n}' are likely to be present in
JSON reprs.
"""
if isinstance(obj, types.MethodType):
# for bound methods, skip redundant <bound method ...> information
return obj.__name__
maxsize = _get_maxsize_for_saferepr(util._config)
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")

View File

@ -292,7 +292,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
if i > 42:
i -= 10 # Provide some context
explanation = [
"Skipping %s identical leading characters in diff, use -v to show" % i
f"Skipping {i} identical leading characters in diff, use -v to show"
]
left = left[i:]
right = right[i:]
@ -493,7 +493,7 @@ def _compare_eq_dict(
common = set_left.intersection(set_right)
same = {k: left[k] for k in common if left[k] == right[k]}
if same and verbose < 2:
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
explanation += [f"Omitting {len(same)} identical items, use -vv to show"]
elif same:
explanation += ["Common items:"]
explanation += highlighter(pprint.pformat(same)).splitlines()
@ -560,7 +560,7 @@ def _compare_eq_cls(
if same or diff:
explanation += [""]
if same and verbose < 2:
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
explanation.append(f"Omitting {len(same)} identical items, use -vv to show")
elif same:
explanation += ["Matching attributes:"]
explanation += highlighter(pprint.pformat(same)).splitlines()
@ -590,7 +590,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
tail = text[index + len(term) :]
correct_text = head + tail
diff = _diff_text(text, correct_text, verbose)
newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"]
for line in diff:
if line.startswith("Skipping"):
continue

View File

@ -218,9 +218,9 @@ class Cache:
os.umask(umask)
path.chmod(0o777 - umask)
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f:
f.write(README_CONTENT)
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f:
f.write("# Created by pytest automatically.\n*\n")
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
f.write(CACHEDIR_TAG_CONTENT)
@ -347,7 +347,7 @@ class LFPlugin:
def pytest_report_collectionfinish(self) -> str | None:
if self.active and self.config.getoption("verbose") >= 0:
return "run-last-failure: %s" % self._report_status
return f"run-last-failure: {self._report_status}"
return None
def pytest_runtest_logreport(self, report: TestReport) -> None:
@ -603,21 +603,21 @@ def cacheshow(config: Config, session: Session) -> int:
dummy = object()
basedir = config.cache._cachedir
vdir = basedir / Cache._CACHE_PREFIX_VALUES
tw.sep("-", "cache values for %r" % glob)
tw.sep("-", f"cache values for {glob!r}")
for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
key = str(valpath.relative_to(vdir))
val = config.cache.get(key, dummy)
if val is dummy:
tw.line("%s contains unreadable content, will be ignored" % key)
tw.line(f"{key} contains unreadable content, will be ignored")
else:
tw.line("%s contains:" % key)
tw.line(f"{key} contains:")
for line in pformat(val).splitlines():
tw.line(" " + line)
ddir = basedir / Cache._CACHE_PREFIX_DIRS
if ddir.is_dir():
contents = sorted(ddir.rglob(glob))
tw.sep("-", "cache directories for %r" % glob)
tw.sep("-", f"cache directories for {glob!r}")
for p in contents:
# if p.is_dir():
# print("%s/" % p.relative_to(basedir))

View File

@ -739,7 +739,7 @@ class CaptureManager:
if self.is_globally_capturing():
return "global"
if self._capture_fixture:
return "fixture %s" % self._capture_fixture.request.fixturename
return f"fixture {self._capture_fixture.request.fixturename}"
return False
# Global capturing control
@ -984,6 +984,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
@ -1011,6 +1012,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
@ -1038,6 +1040,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfd):
@ -1065,6 +1068,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):

View File

@ -50,7 +50,10 @@ from _pytest import __version__
import _pytest._code
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._code.code import TracebackStyle
from _pytest._io import TerminalWriter
from _pytest.config.argparsing import Argument
from _pytest.config.argparsing import Parser
import _pytest.deprecated
import _pytest.hookspec
from _pytest.outcomes import fail
@ -67,9 +70,6 @@ from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from .argparsing import Argument
from .argparsing import Parser
from _pytest._code.code import _TracebackStyle
from _pytest.cacheprovider import Cache
from _pytest.terminal import TerminalReporter
@ -791,7 +791,7 @@ class PytestPluginManager(PluginManager):
if arg.startswith("no:"):
name = arg[3:]
if name in essential_plugins:
raise UsageError("plugin %s cannot be disabled" % name)
raise UsageError(f"plugin {name} cannot be disabled")
# PR #4304: remove stepwise if cacheprovider is blocked.
if name == "cacheprovider":
@ -840,9 +840,9 @@ class PytestPluginManager(PluginManager):
# "terminal" or "capture". Those plugins are registered under their
# basename for historic purposes but must be imported with the
# _pytest prefix.
assert isinstance(modname, str), (
"module name as text required, got %r" % modname
)
assert isinstance(
modname, str
), f"module name as text required, got {modname!r}"
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
return
@ -885,8 +885,7 @@ def _get_plugin_specs_as_list(
if isinstance(specs, collections.abc.Sequence):
return list(specs)
raise UsageError(
"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r"
% specs
f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
)
@ -1010,9 +1009,6 @@ class Config:
object.__setattr__(self, "plugins", plugins)
object.__setattr__(self, "dir", dir)
# Set by cacheprovider plugin.
cache: Cache
class ArgsSource(enum.Enum):
"""Indicates the source of the test arguments.
@ -1027,6 +1023,9 @@ class Config:
#: 'testpaths' configuration value.
TESTPATHS = enum.auto()
# Set by cacheprovider plugin.
cache: Cache
def __init__(
self,
pluginmanager: PytestPluginManager,
@ -1165,7 +1164,7 @@ class Config:
option: argparse.Namespace | None = None,
) -> None:
if option and getattr(option, "fulltrace", False):
style: _TracebackStyle = "long"
style: TracebackStyle = "long"
else:
style = "native"
excrepr = excinfo.getrepr(
@ -1174,7 +1173,7 @@ class Config:
res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
if not any(res):
for line in str(excrepr).split("\n"):
sys.stderr.write("INTERNALERROR> %s\n" % line)
sys.stderr.write(f"INTERNALERROR> {line}\n")
sys.stderr.flush()
def cwd_relative_nodeid(self, nodeid: str) -> str:
@ -1281,7 +1280,8 @@ class Config:
self.pluginmanager.rewrite_hook = hook
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# We don't autoload from setuptools entry points, no need to continue.
# We don't autoload from distribution package entry points,
# no need to continue.
return
package_files = (
@ -1372,8 +1372,8 @@ class Config:
self._consider_importhook(args)
self.pluginmanager.consider_preparse(args, exclude_only=False)
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# Don't autoload from setuptools entry point. Only explicitly specified
# plugins are going to be loaded.
# Don't autoload from distribution package entry point. Only
# explicitly specified plugins are going to be loaded.
self.pluginmanager.load_setuptools_entrypoints("pytest11")
self.pluginmanager.consider_env()
@ -1424,7 +1424,7 @@ class Config:
if not isinstance(minver, str):
raise pytest.UsageError(
"%s: 'minversion' must be a single value" % self.inipath
f"{self.inipath}: 'minversion' must be a single value"
)
if Version(minver) > Version(pytest.__version__):
@ -1728,6 +1728,7 @@ class Config:
can be used to explicitly use the global verbosity level.
Example:
.. code-block:: ini
# content of pytest.ini

View File

@ -310,23 +310,23 @@ class Argument:
for opt in opts:
if len(opt) < 2:
raise ArgumentError(
"invalid option string %r: "
"must be at least two characters long" % opt,
f"invalid option string {opt!r}: "
"must be at least two characters long",
self,
)
elif len(opt) == 2:
if not (opt[0] == "-" and opt[1] != "-"):
raise ArgumentError(
"invalid short option string %r: "
"must be of the form -x, (x any non-dash char)" % opt,
f"invalid short option string {opt!r}: "
"must be of the form -x, (x any non-dash char)",
self,
)
self._short_opts.append(opt)
else:
if not (opt[0:2] == "--" and opt[2] != "-"):
raise ArgumentError(
"invalid long option string %r: "
"must start with --, followed by non-dash" % opt,
f"invalid long option string {opt!r}: "
"must start with --, followed by non-dash",
self,
)
self._long_opts.append(opt)
@ -380,7 +380,7 @@ class OptionGroup:
name for opt in self.options for name in opt.names()
)
if conflict:
raise ValueError("option names %s already added" % conflict)
raise ValueError(f"option names {conflict} already added")
option = Argument(*opts, **attrs)
self._addoption_instance(option, shortupper=False)
@ -438,7 +438,9 @@ class MyOptionParser(argparse.ArgumentParser):
if unrecognized:
for arg in unrecognized:
if arg and arg[0] == "-":
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
lines = [
"unrecognized arguments: {}".format(" ".join(unrecognized))
]
for k, v in sorted(self.extra_info.items()):
lines.append(f" {k}: {v}")
self.error("\n".join(lines))
@ -517,7 +519,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
continue
if not option.startswith("--"):
raise ArgumentError(
'long optional argument without "--": [%s]' % (option), option
f'long optional argument without "--": [{option}]', option
)
xxoption = option[2:]
shortened = xxoption.replace("-", "")

View File

@ -1,4 +1,5 @@
# mypy: allow-untyped-defs
# ruff: noqa: T100
"""Interactive debugging with PDB, the Python Debugger."""
from __future__ import annotations
@ -10,11 +11,11 @@ import types
from typing import Any
from typing import Callable
from typing import Generator
from typing import TYPE_CHECKING
import unittest
from _pytest import outcomes
from _pytest._code import ExceptionInfo
from _pytest.capture import CaptureManager
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import hookimpl
@ -23,11 +24,7 @@ from _pytest.config.argparsing import Parser
from _pytest.config.exceptions import UsageError
from _pytest.nodes import Node
from _pytest.reports import BaseReport
if TYPE_CHECKING:
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
from _pytest.runner import CallInfo
def _validate_usepdb_cls(value: str) -> tuple[str, str]:
@ -177,8 +174,7 @@ class pytestPDB:
else:
tw.sep(
">",
"PDB continue (IO-capturing resumed for %s)"
% capturing,
f"PDB continue (IO-capturing resumed for {capturing})",
)
assert capman is not None
capman.resume()
@ -307,7 +303,7 @@ class PdbTrace:
return (yield)
def wrap_pytest_function_for_tracing(pyfuncitem):
def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
"""Change the Python function object of the given Function item by a
wrapper which actually enters pdb before calling the python function
itself, effectively leaving the user in the pdb prompt in the first
@ -319,14 +315,14 @@ def wrap_pytest_function_for_tracing(pyfuncitem):
# python < 3.7.4) runcall's first param is `func`, which means we'd get
# an exception if one of the kwargs to testfunction was called `func`.
@functools.wraps(testfunction)
def wrapper(*args, **kwargs):
def wrapper(*args, **kwargs) -> None:
func = functools.partial(testfunction, *args, **kwargs)
_pdb.runcall(func)
pyfuncitem.obj = wrapper
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
"""Wrap the given pytestfunct item for tracing support if --trace was given in
the command line."""
if pyfuncitem.config.getvalue("trace"):

View File

@ -370,7 +370,7 @@ class DoctestItem(Item):
).split("\n")
else:
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"]
lines += [
x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
]
@ -378,7 +378,7 @@ class DoctestItem(Item):
return ReprFailDoctest(reprlocation_lines)
def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
return self.path, self.dtest.lineno, f"[doctest] {self.name}"
def _get_flag_lookup() -> dict[str, int]:
@ -501,43 +501,52 @@ class DoctestModule(Module):
import doctest
class MockAwareDocTestFinder(doctest.DocTestFinder):
"""A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
py_ver_info_minor = sys.version_info[:2]
is_find_lineno_broken = (
py_ver_info_minor < (3, 11)
or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9)
or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3)
)
if is_find_lineno_broken:
https://github.com/pytest-dev/pytest/issues/3456
https://bugs.python.org/issue25532
"""
def _find_lineno(self, obj, source_lines):
"""On older Pythons, doctest code does not take into account
`@property`. https://github.com/python/cpython/issues/61648
def _find_lineno(self, obj, source_lines):
"""Doctest code does not take into account `@property`, this
is a hackish way to fix it. https://bugs.python.org/issue17446
Moreover, wrapped Doctests need to be unwrapped so the correct
line number is returned. #8796
"""
if isinstance(obj, property):
obj = getattr(obj, "fget", obj)
Wrapped Doctests will need to be unwrapped so the correct
line number is returned. This will be reported upstream. #8796
"""
if isinstance(obj, property):
obj = getattr(obj, "fget", obj)
if hasattr(obj, "__wrapped__"):
# Get the main obj in case of it being wrapped
obj = inspect.unwrap(obj)
if hasattr(obj, "__wrapped__"):
# Get the main obj in case of it being wrapped
obj = inspect.unwrap(obj)
# Type ignored because this is a private function.
return super()._find_lineno( # type:ignore[misc]
obj,
source_lines,
)
def _find(
self, tests, obj, name, module, source_lines, globs, seen
) -> None:
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
return super()._find_lineno( # type:ignore[misc]
obj,
source_lines,
)
if sys.version_info < (3, 10):
def _find(
self, tests, obj, name, module, source_lines, globs, seen
) -> None:
"""Override _find to work around issue in stdlib.
https://github.com/pytest-dev/pytest/issues/3456
https://github.com/python/cpython/issues/69718
"""
if _is_mocked(obj):
return # pragma: no cover
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
)
if sys.version_info < (3, 13):
def _from_module(self, module, object):
@ -552,14 +561,11 @@ class DoctestModule(Module):
# Type ignored because this is a private function.
return super()._from_module(module, object) # type: ignore[misc]
else: # pragma: no cover
pass
try:
module = self.obj
except Collector.CollectError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
skip(f"unable to import module {self.path!r}")
else:
raise

View File

@ -10,16 +10,19 @@ import inspect
import os
from pathlib import Path
import sys
import types
from typing import AbstractSet
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import Final
from typing import final
from typing import Generator
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import Mapping
from typing import MutableMapping
from typing import NoReturn
from typing import Optional
@ -57,6 +60,7 @@ from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
from _pytest.deprecated import YIELD_FIXTURE
from _pytest.main import Session
from _pytest.mark import Mark
from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator
@ -75,7 +79,6 @@ if sys.version_info < (3, 11):
if TYPE_CHECKING:
from _pytest.main import Session
from _pytest.python import CallSpec2
from _pytest.python import Function
from _pytest.python import Metafunc
@ -102,8 +105,8 @@ _FixtureCachedResult = Union[
None,
# Cache key.
object,
# Exception if raised.
BaseException,
# The exception and the original traceback.
Tuple[BaseException, Optional[types.TracebackType]],
],
]
@ -158,6 +161,12 @@ def getfixturemarker(obj: object) -> FixtureFunctionMarker | None:
)
# Algorithm for sorting on a per-parametrized resource setup basis.
# It is called for Session scope first and performs sorting
# down to the lower scopes such as to minimize number of "high scope"
# setups and teardowns.
@dataclasses.dataclass(frozen=True)
class FixtureArgKey:
argname: str
@ -166,100 +175,95 @@ class FixtureArgKey:
item_cls: type | None
def get_parametrized_fixture_keys(
_V = TypeVar("_V")
OrderedSet = Dict[_V, None]
def get_parametrized_fixture_argkeys(
item: nodes.Item, scope: Scope
) -> Iterator[FixtureArgKey]:
"""Return list of keys for all parametrized arguments which match
the specified scope."""
assert scope is not Scope.Function
try:
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
except AttributeError:
return
item_cls = None
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
# Package key = module's directory.
scoped_item_path = item.path.parent
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
else:
assert_never(scope)
for argname in callspec.indices:
if callspec._arg2scope[argname] != scope:
continue
item_cls = None
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
scoped_item_path = item.path
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
else:
assert_never(scope)
param_index = callspec.indices[argname]
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
# Algorithm for sorting on a per-parametrized resource setup basis.
# It is called for Session scope first and performs sorting
# down to the lower scopes such as to minimize number of "high scope"
# setups and teardowns.
def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]:
argkeys_cache: dict[Scope, dict[nodes.Item, dict[FixtureArgKey, None]]] = {}
argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
items_by_argkey: dict[
Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
] = {}
for scope in HIGH_SCOPES:
scoped_argkeys_cache = argkeys_cache[scope] = {}
scoped_argkeys_by_item = argkeys_by_item[scope] = {}
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
for item in items:
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
if keys:
scoped_argkeys_cache[item] = keys
for key in keys:
scoped_items_by_argkey[key][item] = None
items_dict = dict.fromkeys(items, None)
argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
if argkeys:
scoped_argkeys_by_item[item] = argkeys
for argkey in argkeys:
scoped_items_by_argkey[argkey][item] = None
items_set = dict.fromkeys(items)
return list(
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
reorder_items_atscope(
items_set, argkeys_by_item, items_by_argkey, Scope.Session
)
)
def fix_cache_order(
item: nodes.Item,
argkeys_cache: dict[Scope, dict[nodes.Item, dict[FixtureArgKey, None]]],
items_by_argkey: dict[Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
) -> None:
for scope in HIGH_SCOPES:
scoped_items_by_argkey = items_by_argkey[scope]
for key in argkeys_cache[scope].get(item, []):
scoped_items_by_argkey[key][item] = None
scoped_items_by_argkey[key].move_to_end(item, last=False)
def reorder_items_atscope(
items: dict[nodes.Item, None],
argkeys_cache: dict[Scope, dict[nodes.Item, dict[FixtureArgKey, None]]],
items_by_argkey: dict[Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
items: OrderedSet[nodes.Item],
argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
items_by_argkey: Mapping[
Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
],
scope: Scope,
) -> dict[nodes.Item, None]:
) -> OrderedSet[nodes.Item]:
if scope is Scope.Function or len(items) < 3:
return items
ignore: set[FixtureArgKey | None] = set()
items_deque = deque(items)
items_done: dict[nodes.Item, None] = {}
scoped_items_by_argkey = items_by_argkey[scope]
scoped_argkeys_cache = argkeys_cache[scope]
scoped_argkeys_by_item = argkeys_by_item[scope]
ignore: set[FixtureArgKey] = set()
items_deque = deque(items)
items_done: OrderedSet[nodes.Item] = {}
while items_deque:
no_argkey_group: dict[nodes.Item, None] = {}
no_argkey_items: OrderedSet[nodes.Item] = {}
slicing_argkey = None
while items_deque:
item = items_deque.popleft()
if item in items_done or item in no_argkey_group:
if item in items_done or item in no_argkey_items:
continue
argkeys = dict.fromkeys(
(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore
)
if not argkeys:
no_argkey_group[item] = None
no_argkey_items[item] = None
else:
slicing_argkey, _ = argkeys.popitem()
# We don't have to remove relevant items from later in the
@ -268,16 +272,23 @@ def reorder_items_atscope(
i for i in scoped_items_by_argkey[slicing_argkey] if i in items
]
for i in reversed(matching_items):
fix_cache_order(i, argkeys_cache, items_by_argkey)
items_deque.appendleft(i)
# Fix items_by_argkey order.
for other_scope in HIGH_SCOPES:
other_scoped_items_by_argkey = items_by_argkey[other_scope]
for argkey in argkeys_by_item[other_scope].get(i, ()):
other_scoped_items_by_argkey[argkey][i] = None
other_scoped_items_by_argkey[argkey].move_to_end(
i, last=False
)
break
if no_argkey_group:
no_argkey_group = reorder_items_atscope(
no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower()
if no_argkey_items:
reordered_no_argkey_items = reorder_items_atscope(
no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower()
)
for item in no_argkey_group:
items_done[item] = None
ignore.add(slicing_argkey)
items_done.update(reordered_no_argkey_items)
if slicing_argkey is not None:
ignore.add(slicing_argkey)
return items_done
@ -676,7 +687,7 @@ class TopRequest(FixtureRequest):
return self._pyfuncitem
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
return f"<FixtureRequest for {self.node!r}>"
def _fillfixtures(self) -> None:
item = self._pyfuncitem
@ -1049,8 +1060,8 @@ class FixtureDef(Generic[FixtureValue]):
# numpy arrays (#6497).
if my_cache_key is cache_key:
if self.cached_result[2] is not None:
exc = self.cached_result[2]
raise exc
exc, exc_tb = self.cached_result[2]
raise exc.with_traceback(exc_tb)
else:
result = self.cached_result[0]
return result
@ -1126,7 +1137,7 @@ def pytest_fixture_setup(
# Don't show the fixture as the skip location, as then the user
# wouldn't know which test skipped.
e._use_item_location = True
fixturedef.cached_result = (None, my_cache_key, e)
fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__))
raise
fixturedef.cached_result = (result, my_cache_key, None)
return result
@ -1614,10 +1625,7 @@ class FixtureManager:
name: str,
func: _FixtureFunc[object],
nodeid: str | None,
scope: Scope
| _ScopeName
| Callable[[str, Config], _ScopeName]
| None = "function",
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function",
params: Sequence[object] | None = None,
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
autouse: bool = False,
@ -1810,7 +1818,10 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
fixture_doc = inspect.getdoc(fixture_def.func)
if fixture_doc:
write_docstring(
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
tw,
fixture_doc.split("\n\n", maxsplit=1)[0]
if verbose <= 0
else fixture_doc,
)
else:
tw.line(" no docstring available", red=True)
@ -1887,12 +1898,14 @@ def _showfixtures_main(config: Config, session: Session) -> None:
continue
tw.write(f"{argname}", green=True)
if fixturedef.scope != "function":
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
tw.write(f" [{fixturedef.scope} scope]", cyan=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
doc = inspect.getdoc(fixturedef.func)
if doc:
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
write_docstring(
tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc
)
else:
tw.line(" no docstring available", red=True)
tw.line()

View File

@ -120,11 +120,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
)
config.trace.root.setwriter(debugfile.write)
undo_tracing = config.pluginmanager.enable_tracing()
sys.stderr.write("writing pytest debug information to %s\n" % path)
sys.stderr.write(f"writing pytest debug information to {path}\n")
def unset_tracing() -> None:
debugfile.close()
sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name)
sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n")
config.trace.root.setwriter(None)
undo_tracing()
@ -184,7 +184,7 @@ def showhelp(config: Config) -> None:
if help is None:
raise TypeError(f"help argument cannot be None for {name}")
spec = f"{name} ({type}):"
tw.write(" %s" % spec)
tw.write(f" {spec}")
spec_len = len(spec)
if spec_len > (indent_len - 3):
# Display help starting at a new line.
@ -242,7 +242,7 @@ def getpluginversioninfo(config: Config) -> list[str]:
lines = []
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
lines.append("setuptools registered plugins:")
lines.append("registered third-party plugins:")
for plugin, dist in plugininfo:
loc = getattr(plugin, "__file__", repr(plugin))
content = f"{dist.project_name}-{dist.version} at {loc}"

View File

@ -1,4 +1,5 @@
# mypy: allow-untyped-defs
# ruff: noqa: T100
"""Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins."""

View File

@ -11,6 +11,7 @@ https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jen
from __future__ import annotations
from datetime import datetime
from datetime import timezone
import functools
import os
import platform
@ -50,9 +51,9 @@ def bin_xml_escape(arg: object) -> str:
def repl(matchobj: Match[str]) -> str:
i = ord(matchobj.group())
if i <= 0xFF:
return "#x%02X" % i
return f"#x{i:02X}"
else:
return "#x%04X" % i
return f"#x{i:04X}"
# The spec range of valid chars is:
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
@ -146,7 +147,7 @@ class _NodeReporter:
self.attrs = temp_attrs
def to_xml(self) -> ET.Element:
testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration)
testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}")
properties = self.make_properties_node()
if properties is not None:
testcase.append(properties)
@ -663,8 +664,10 @@ class LogXML:
failures=str(self.stats["failure"]),
skipped=str(self.stats["skipped"]),
tests=str(numtests),
time="%.3f" % suite_time_delta,
timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
time=f"{suite_time_delta:.3f}",
timestamp=datetime.fromtimestamp(self.suite_start_time, timezone.utc)
.astimezone()
.isoformat(),
hostname=platform.node(),
)
global_properties = self._get_global_properties_node()

View File

@ -399,7 +399,7 @@ class LogCaptureHandler(logging_StreamHandler):
# The default behavior of logging is to print "Logging error"
# to stderr with the call stack and some extra details.
# pytest wants to make such mistakes visible during testing.
raise
raise # pylint: disable=misplaced-bare-raise
@final

View File

@ -35,7 +35,6 @@ from _pytest.config import PytestPluginManager
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.config.compat import PathAwareHookProxy
from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
@ -52,6 +51,8 @@ from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
from typing import Self
from _pytest.fixtures import FixtureManager
def pytest_addoption(parser: Parser) -> None:
parser.addini(
@ -368,22 +369,12 @@ def pytest_runtestloop(session: Session) -> bool:
def _in_venv(path: Path) -> bool:
"""Attempt to detect if ``path`` is the root of a Virtual Environment by
checking for the existence of the appropriate activate script."""
bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
checking for the existence of the pyvenv.cfg file.
[https://peps.python.org/pep-0405/]"""
try:
if not bindir.is_dir():
return False
return path.joinpath("pyvenv.cfg").is_file()
except OSError:
return False
activates = (
"activate",
"activate.csh",
"activate.fish",
"Activate",
"Activate.bat",
"Activate.ps1",
)
return any(fname.name in activates for fname in bindir.iterdir())
def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:

View File

@ -2,9 +2,11 @@
from __future__ import annotations
import collections
import dataclasses
from typing import AbstractSet
from typing import Collection
from typing import Iterable
from typing import Optional
from typing import TYPE_CHECKING
@ -21,6 +23,7 @@ from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config import UsageError
from _pytest.config.argparsing import NOT_SET
from _pytest.config.argparsing import Parser
from _pytest.stash import StashKey
@ -122,7 +125,7 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
parts = line.split(":", 1)
name = parts[0]
rest = parts[1] if len(parts) == 2 else ""
tw.write("@pytest.mark.%s:" % name, bold=True)
tw.write(f"@pytest.mark.{name}:", bold=True)
tw.line(rest)
tw.line()
config._ensure_unconfigure()
@ -181,7 +184,9 @@ class KeywordMatcher:
return cls(mapped_names)
def __call__(self, subname: str) -> bool:
def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool:
if kwargs:
raise UsageError("Keyword expressions do not support call parameters.")
subname = subname.lower()
names = (name.lower() for name in self._names)
@ -218,17 +223,26 @@ class MarkMatcher:
Tries to match on any marker names, attached to the given colitem.
"""
__slots__ = ("own_mark_names",)
__slots__ = ("own_mark_name_mapping",)
own_mark_names: AbstractSet[str]
own_mark_name_mapping: dict[str, list[Mark]]
@classmethod
def from_item(cls, item: Item) -> MarkMatcher:
mark_names = {mark.name for mark in item.iter_markers()}
return cls(mark_names)
def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher:
mark_name_mapping = collections.defaultdict(list)
for mark in markers:
mark_name_mapping[mark.name].append(mark)
return cls(mark_name_mapping)
def __call__(self, name: str) -> bool:
return name in self.own_mark_names
def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool:
if not (matches := self.own_mark_name_mapping.get(name, [])):
return False
for mark in matches:
if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()):
return True
return False
def deselect_by_mark(items: list[Item], config: Config) -> None:
@ -240,7 +254,7 @@ def deselect_by_mark(items: list[Item], config: Config) -> None:
remaining: list[Item] = []
deselected: list[Item] = []
for item in items:
if expr.evaluate(MarkMatcher.from_item(item)):
if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())):
remaining.append(item)
else:
deselected.append(item)

View File

@ -5,14 +5,19 @@ The grammar is:
expression: expr? EOF
expr: and_expr ('or' and_expr)*
and_expr: not_expr ('and' not_expr)*
not_expr: 'not' not_expr | '(' expr ')' | ident
not_expr: 'not' not_expr | '(' expr ')' | ident kwargs?
ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
kwargs: ('(' name '=' value ( ', ' name '=' value )* ')')
name: a valid ident, but not a reserved keyword
value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None'
The semantics are:
- Empty expression evaluates to False.
- ident evaluates to True of False according to a provided matcher function.
- ident evaluates to True or False according to a provided matcher function.
- or/and/not evaluate according to the usual boolean semantics.
- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
"""
from __future__ import annotations
@ -20,12 +25,15 @@ from __future__ import annotations
import ast
import dataclasses
import enum
import keyword
import re
import types
from typing import Callable
from typing import Iterator
from typing import Literal
from typing import Mapping
from typing import NoReturn
from typing import overload
from typing import Protocol
from typing import Sequence
@ -43,6 +51,9 @@ class TokenType(enum.Enum):
NOT = "not"
IDENT = "identifier"
EOF = "end of input"
EQUAL = "="
STRING = "string literal"
COMMA = ","
@dataclasses.dataclass(frozen=True)
@ -86,6 +97,27 @@ class Scanner:
elif input[pos] == ")":
yield Token(TokenType.RPAREN, ")", pos)
pos += 1
elif input[pos] == "=":
yield Token(TokenType.EQUAL, "=", pos)
pos += 1
elif input[pos] == ",":
yield Token(TokenType.COMMA, ",", pos)
pos += 1
elif (quote_char := input[pos]) in ("'", '"'):
end_quote_pos = input.find(quote_char, pos + 1)
if end_quote_pos == -1:
raise ParseError(
pos + 1,
f'closing quote "{quote_char}" is missing',
)
value = input[pos : end_quote_pos + 1]
if (backslash_pos := input.find("\\")) != -1:
raise ParseError(
backslash_pos + 1,
r'escaping with "\" not supported in marker expression',
)
yield Token(TokenType.STRING, value, pos)
pos += len(value)
else:
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
if match:
@ -106,6 +138,14 @@ class Scanner:
)
yield Token(TokenType.EOF, "", pos)
@overload
def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...
@overload
def accept(
self, type: TokenType, *, reject: Literal[False] = False
) -> Token | None: ...
def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
if self.current.type is type:
token = self.current
@ -166,18 +206,87 @@ def not_expr(s: Scanner) -> ast.expr:
return ret
ident = s.accept(TokenType.IDENT)
if ident:
return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
name = ast.Name(IDENT_PREFIX + ident.value, ast.Load())
if s.accept(TokenType.LPAREN):
ret = ast.Call(func=name, args=[], keywords=all_kwargs(s))
s.accept(TokenType.RPAREN, reject=True)
else:
ret = name
return ret
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
class MatcherAdapter(Mapping[str, bool]):
BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}
def single_kwarg(s: Scanner) -> ast.keyword:
keyword_name = s.accept(TokenType.IDENT, reject=True)
if not keyword_name.value.isidentifier():
raise ParseError(
keyword_name.pos + 1,
f"not a valid python identifier {keyword_name.value}",
)
if keyword.iskeyword(keyword_name.value):
raise ParseError(
keyword_name.pos + 1,
f"unexpected reserved python keyword `{keyword_name.value}`",
)
s.accept(TokenType.EQUAL, reject=True)
if value_token := s.accept(TokenType.STRING):
value: str | int | bool | None = value_token.value[1:-1] # strip quotes
else:
value_token = s.accept(TokenType.IDENT, reject=True)
if (
(number := value_token.value).isdigit()
or number.startswith("-")
and number[1:].isdigit()
):
value = int(number)
elif value_token.value in BUILTIN_MATCHERS:
value = BUILTIN_MATCHERS[value_token.value]
else:
raise ParseError(
value_token.pos + 1,
f'unexpected character/s "{value_token.value}"',
)
ret = ast.keyword(keyword_name.value, ast.Constant(value))
return ret
def all_kwargs(s: Scanner) -> list[ast.keyword]:
ret = [single_kwarg(s)]
while s.accept(TokenType.COMMA):
ret.append(single_kwarg(s))
return ret
class MatcherCall(Protocol):
def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ...
@dataclasses.dataclass
class MatcherNameAdapter:
matcher: MatcherCall
name: str
def __bool__(self) -> bool:
return self.matcher(self.name)
def __call__(self, **kwargs: str | int | bool | None) -> bool:
return self.matcher(self.name, **kwargs)
class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
"""Adapts a matcher function to a locals mapping as required by eval()."""
def __init__(self, matcher: Callable[[str], bool]) -> None:
def __init__(self, matcher: MatcherCall) -> None:
self.matcher = matcher
def __getitem__(self, key: str) -> bool:
return self.matcher(key[len(IDENT_PREFIX) :])
def __getitem__(self, key: str) -> MatcherNameAdapter:
return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :])
def __iter__(self) -> Iterator[str]:
raise NotImplementedError()
@ -211,7 +320,7 @@ class Expression:
)
return Expression(code)
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
def evaluate(self, matcher: MatcherCall) -> bool:
"""Evaluate the match expression.
:param matcher:
@ -220,5 +329,5 @@ class Expression:
:returns: Whether the expression matches or not.
"""
ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
ret: bool = bool(eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)))
return ret

View File

@ -28,6 +28,7 @@ from _pytest.config import Config
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
from _pytest.outcomes import fail
from _pytest.scope import _ScopeName
from _pytest.warning_types import PytestUnknownMarkWarning
@ -427,7 +428,6 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
# Typing for builtin pytest marks. This is cheating; it gives builtin marks
# special privilege, and breaks modularity. But practicality beats purity...
if TYPE_CHECKING:
from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,no-overload-impl]
@ -544,9 +544,9 @@ class MarkGenerator:
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
warnings.warn(
"Unknown pytest.mark.%s - is this a typo? You can register "
f"Unknown pytest.mark.{name} - is this a typo? You can register "
"custom marks to avoid this warning - for details, see "
"https://docs.pytest.org/en/stable/how-to/mark.html" % name,
"https://docs.pytest.org/en/stable/how-to/mark.html",
PytestUnknownMarkWarning,
2,
)

View File

@ -140,6 +140,7 @@ class MonkeyPatch:
which undoes any patching done inside the ``with`` block upon exit.
Example:
.. code-block:: python
import functools

View File

@ -26,6 +26,7 @@ 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._code.code import TracebackStyle
from _pytest.compat import LEGACY_PATH
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
@ -45,7 +46,6 @@ if TYPE_CHECKING:
from typing import Self
# Imported here due to circular import.
from _pytest._code.code import _TracebackStyle
from _pytest.main import Session
@ -408,7 +408,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
def _repr_failure_py(
self,
excinfo: ExceptionInfo[BaseException],
style: _TracebackStyle | None = None,
style: TracebackStyle | None = None,
) -> TerminalRepr:
from _pytest.fixtures import FixtureLookupError
@ -440,6 +440,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
else:
truncate_locals = True
truncate_args = False if self.config.getoption("verbose", 0) > 2 else True
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
# It is possible for a fixture/test to change the CWD while this code runs, which
# would then result in the user seeing confusing paths in the failure message.
@ -458,12 +460,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
style=style,
tbfilter=tbfilter,
truncate_locals=truncate_locals,
truncate_args=truncate_args,
)
def repr_failure(
self,
excinfo: ExceptionInfo[BaseException],
style: _TracebackStyle | None = None,
style: TracebackStyle | None = None,
) -> str | TerminalRepr:
"""Return a representation of a collection or test failure.

View File

@ -66,7 +66,7 @@ def pytest_unconfigure(config: Config) -> None:
# Write summary.
tr.write_sep("=", "Sending information to Paste Service")
pastebinurl = create_new_paste(sessionlog)
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
tr.write_line(f"pastebin session-log: {pastebinurl}\n")
def create_new_paste(contents: str | bytes) -> str:
@ -86,7 +86,7 @@ def create_new_paste(contents: str | bytes) -> str:
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
)
except OSError as exc_info: # urllib errors
return "bad response: %s" % exc_info
return f"bad response: {exc_info}"
m = re.search(r'href="/raw/(\w+)"', response)
if m:
return f"{url}/show/{m.group(1)}"

View File

@ -178,13 +178,13 @@ class LsofFdLeakChecker:
leaked_files = [t for t in lines2 if t[0] in new_fds]
if leaked_files:
error = [
"***** %s FD leakage detected" % len(leaked_files),
f"***** {len(leaked_files)} FD leakage detected",
*(str(f) for f in leaked_files),
"*** Before:",
*(str(f) for f in lines1),
"*** After:",
*(str(f) for f in lines2),
"***** %s FD leakage detected" % len(leaked_files),
f"***** {len(leaked_files)} FD leakage detected",
"*** function {}:{}: {} ".format(*item.location),
"See issue #2366",
]
@ -310,7 +310,7 @@ class HookRecorder:
del self.calls[i]
return call
lines = [f"could not find call {name!r}, in:"]
lines.extend([" %s" % x for x in self.calls])
lines.extend([f" {x}" for x in self.calls])
fail("\n".join(lines))
def getcall(self, name: str) -> RecordedHookCall:
@ -801,6 +801,7 @@ class Pytester:
The first created file.
Examples:
.. code-block:: python
pytester.makefile(".txt", "line1", "line2")
@ -854,6 +855,7 @@ class Pytester:
existing files.
Examples:
.. code-block:: python
def test_something(pytester):
@ -873,6 +875,7 @@ class Pytester:
existing files.
Examples:
.. code-block:: python
def test_something(pytester):
@ -1198,7 +1201,9 @@ class Pytester:
if str(x).startswith("--basetemp"):
break
else:
new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp"))
new_args.append(
"--basetemp={}".format(self.path.parent.joinpath("basetemp"))
)
return new_args
def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
@ -1479,7 +1484,7 @@ class Pytester:
"""
__tracebackhide__ = True
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
args = ("--basetemp=%s" % p, *args)
args = (f"--basetemp={p}", *args)
plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins:
args = ("-p", plugins[0], *args)
@ -1585,7 +1590,7 @@ class LineMatcher:
self._log("matched: ", repr(line))
break
else:
msg = "line %r not found in output" % line
msg = f"line {line!r} not found in output"
self._log(msg)
self._fail(msg)
@ -1597,7 +1602,7 @@ class LineMatcher:
for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline):
return self.lines[i + 1 :]
raise ValueError("line %r not found in output" % fnline)
raise ValueError(f"line {fnline!r} not found in output")
def _log(self, *args) -> None:
self._log_output.append(" ".join(str(x) for x in args))
@ -1682,7 +1687,7 @@ class LineMatcher:
started = True
break
elif match_func(nextline, line):
self._log("%s:" % match_nickname, repr(line))
self._log(f"{match_nickname}:", repr(line))
self._log(
"{:>{width}}".format("with:", width=wnick), repr(nextline)
)

View File

@ -226,7 +226,7 @@ def pytest_pycollect_makeitem(
filename, lineno = getfslineno(obj)
warnings.warn_explicit(
message=PytestCollectionWarning(
"cannot collect %r because it is not a function." % name
f"cannot collect {name!r} because it is not a function."
),
category=None,
filename=str(filename),
@ -370,7 +370,11 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
return False
def istestclass(self, obj: object, name: str) -> bool:
return self.classnamefilter(name) or self.isnosetest(obj)
if not (self.classnamefilter(name) or self.isnosetest(obj)):
return False
if inspect.isabstract(obj):
return False
return True
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
"""Check if the given name matches the prefix or glob-pattern defined

View File

@ -7,6 +7,7 @@ from decimal import Decimal
import math
from numbers import Complex
import pprint
import re
from types import TracebackType
from typing import Any
from typing import Callable
@ -128,6 +129,8 @@ def _recursive_sequence_map(f, x):
if isinstance(x, (list, tuple)):
seq_type = type(x)
return seq_type(_recursive_sequence_map(f, xi) for xi in x)
elif _is_sequence_like(x):
return [_recursive_sequence_map(f, xi) for xi in x]
else:
return f(x)
@ -453,7 +456,7 @@ class ApproxScalar(ApproxBase):
return False
# Return true if the two numbers are within the tolerance.
result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type]
result: bool = abs(self.expected - actual) <= self.tolerance
return result
# Ignore type because of https://github.com/python/mypy/issues/4266.
@ -720,11 +723,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
elif _is_numpy_array(expected):
expected = _as_numpy_array(expected)
cls = ApproxNumpy
elif (
hasattr(expected, "__getitem__")
and isinstance(expected, Sized)
and not isinstance(expected, (str, bytes))
):
elif _is_sequence_like(expected):
cls = ApproxSequenceLike
elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
@ -735,6 +734,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
return cls(expected, rel, abs, nan_ok)
def _is_sequence_like(expected: object) -> bool:
return (
hasattr(expected, "__getitem__")
and isinstance(expected, Sized)
and not isinstance(expected, (str, bytes))
)
def _is_numpy_array(obj: object) -> bool:
"""
Return true if the given object is implicitly convertible to ndarray,
@ -980,6 +987,14 @@ class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
self.message = message
self.match_expr = match_expr
self.excinfo: _pytest._code.ExceptionInfo[E] | None = None
if self.match_expr is not None:
re_error = None
try:
re.compile(self.match_expr)
except re.error as e:
re_error = e
if re_error is not None:
fail(f"Invalid regex pattern provided to 'match': {re_error}")
def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
self.excinfo = _pytest._code.ExceptionInfo.for_later()

View File

@ -13,6 +13,7 @@ from typing import Iterator
from typing import Literal
from typing import Mapping
from typing import NoReturn
from typing import Sequence
from typing import TYPE_CHECKING
from _pytest._code.code import ExceptionChainRepr
@ -30,6 +31,7 @@ from _pytest._io import TerminalWriter
from _pytest.config import Config
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.outcomes import fail
from _pytest.outcomes import skip
@ -190,11 +192,26 @@ class BaseReport:
return domain
return None
def _get_verbose_word(self, config: Config):
def _get_verbose_word_with_markup(
self, config: Config, default_markup: Mapping[str, bool]
) -> tuple[str, Mapping[str, bool]]:
_category, _short, verbose = config.hook.pytest_report_teststatus(
report=self, config=config
)
return verbose
if isinstance(verbose, str):
return verbose, default_markup
if isinstance(verbose, Sequence) and len(verbose) == 2:
word, markup = verbose
if isinstance(word, str) and isinstance(markup, Mapping):
return word, markup
fail( # pragma: no cover
"pytest_report_teststatus() hook (from a plugin) returned "
f"an invalid verbose value: {verbose!r}.\nExpected either a string "
"or a tuple of (word, markup)."
)
def _to_json(self) -> dict[str, Any]:
"""Return the contents of this report as a dict of builtin entries,
@ -226,10 +243,10 @@ def _report_unserialization_failure(
url = "https://github.com/pytest-dev/pytest/issues"
stream = StringIO()
pprint("-" * 100, stream=stream)
pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
pprint("report_name: %s" % report_class, stream=stream)
pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream)
pprint(f"report_name: {report_class}", stream=stream)
pprint(reportdict, stream=stream)
pprint("Please report this bug at %s" % url, stream=stream)
pprint(f"Please report this bug at {url}", stream=stream)
pprint("-" * 100, stream=stream)
raise RuntimeError(stream.getvalue())

View File

@ -7,6 +7,7 @@ import bdb
import dataclasses
import os
import sys
import types
from typing import Callable
from typing import cast
from typing import final
@ -85,7 +86,7 @@ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
if not durations:
tr.write_sep("=", "slowest durations")
else:
tr.write_sep("=", "slowest %s durations" % durations)
tr.write_sep("=", f"slowest {durations} durations")
dlist = dlist[:durations]
for i, rep in enumerate(dlist):
@ -129,6 +130,10 @@ def runtestprotocol(
show_test_item(item)
if not item.config.getoption("setuponly", False):
reports.append(call_and_report(item, "call", log))
# If the session is about to fail or stop, teardown everything - this is
# necessary to correctly report fixture teardown errors (see #11706)
if item.session.shouldfail or item.session.shouldstop:
nextitem = None
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
# After all teardown hooks have been called
# want funcargs and request info to go away.
@ -162,7 +167,7 @@ def pytest_runtest_call(item: Item) -> None:
del sys.last_value
del sys.last_traceback
if sys.version_info >= (3, 12, 0):
del sys.last_exc # type: ignore[attr-defined]
del sys.last_exc
except AttributeError:
pass
try:
@ -172,11 +177,11 @@ def pytest_runtest_call(item: Item) -> None:
sys.last_type = type(e)
sys.last_value = e
if sys.version_info >= (3, 12, 0):
sys.last_exc = e # type: ignore[attr-defined]
sys.last_exc = e
assert e.__traceback__ is not None
# Skip *this* frame
sys.last_traceback = e.__traceback__.tb_next
raise e
raise
def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
@ -486,7 +491,7 @@ class SetupState:
# Node's finalizers.
list[Callable[[], object]],
# Node's exception and original traceback, if its setup raised.
OutcomeException | Exception | None,
tuple[OutcomeException | Exception, types.TracebackType | None] | None,
],
] = {}
@ -499,7 +504,7 @@ class SetupState:
for col, (finalizers, exc) in self.stack.items():
assert col in needed_collectors, "previous item was not torn down properly"
if exc:
raise exc
raise exc[0].with_traceback(exc[1])
for col in needed_collectors[len(self.stack) :]:
assert col not in self.stack
@ -508,8 +513,8 @@ class SetupState:
try:
col.setup()
except TEST_OUTCOME as exc:
self.stack[col] = (self.stack[col][0], exc)
raise exc
self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__))
raise
def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
"""Attach a finalizer to the given node.

View File

@ -117,7 +117,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
result = eval(condition_code, globals_)
except SyntaxError as exc:
msglines = [
"Error evaluating %r condition" % mark.name,
f"Error evaluating {mark.name!r} condition",
" " + condition,
" " + " " * (exc.offset or 0) + "^",
"SyntaxError: invalid syntax",
@ -125,7 +125,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
fail("\n".join(msglines), pytrace=False)
except Exception as exc:
msglines = [
"Error evaluating %r condition" % mark.name,
f"Error evaluating {mark.name!r} condition",
" " + condition,
*traceback.format_exception_only(type(exc), exc),
]
@ -137,7 +137,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
result = bool(condition)
except Exception as exc:
msglines = [
"Error evaluating %r condition as a boolean" % mark.name,
f"Error evaluating {mark.name!r} condition as a boolean",
*traceback.format_exception_only(type(exc), exc),
]
fail("\n".join(msglines), pytrace=False)
@ -149,7 +149,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
else:
# XXX better be checked at collection time
msg = (
"Error evaluating %r: " % mark.name
f"Error evaluating {mark.name!r}: "
+ "you need to specify reason=STRING when using booleans as conditions."
)
fail(msg, pytrace=False)

View File

@ -1,17 +1,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from _pytest import nodes
from _pytest.cacheprovider import Cache
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.main import Session
from _pytest.reports import TestReport
import pytest
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
STEPWISE_CACHE_DIR = "cache/stepwise"
@ -38,7 +32,6 @@ def pytest_addoption(parser: Parser) -> None:
)
@pytest.hookimpl
def pytest_configure(config: Config) -> None:
if config.option.stepwise_skip:
# allow --stepwise-skip to work on its own merits.

View File

@ -212,6 +212,13 @@ def pytest_addoption(parser: Parser) -> None:
choices=["auto", "long", "short", "no", "line", "native"],
help="Traceback print mode (auto/long/short/line/native/no)",
)
group._addoption(
"--xfail-tb",
action="store_true",
dest="xfail_tb",
default=False,
help="Show tracebacks for xfail (as long as --tb != no)",
)
group._addoption(
"--show-capture",
action="store",
@ -428,7 +435,7 @@ class TerminalReporter:
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
return char in self.reportchars
def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None:
fspath = self.config.rootpath / nodeid.split("::")[0]
if self.currentfspath is None or fspath != self.currentfspath:
if self.currentfspath is not None and self._show_progress_info:
@ -561,10 +568,11 @@ class TerminalReporter:
def pytest_runtest_logstart(
self, nodeid: str, location: tuple[str, int | None, str]
) -> None:
fspath, lineno, domain = location
# Ensure that the path is printed before the
# 1st test of a module starts running.
if self.showlongtestinfo:
line = self._locationline(nodeid, *location)
line = self._locationline(nodeid, fspath, lineno, domain)
self.write_ensure_prefix(line, "")
self.flush()
elif self.showfspath:
@ -587,7 +595,6 @@ class TerminalReporter:
if not letter and not word:
# Probably passed setup/teardown.
return
running_xdist = hasattr(rep, "node")
if markup is None:
was_xfail = hasattr(report, "wasxfail")
if rep.passed and not was_xfail:
@ -600,11 +607,20 @@ class TerminalReporter:
markup = {"yellow": True}
else:
markup = {}
self._progress_nodeids_reported.add(rep.nodeid)
if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0:
self._tw.write(letter, **markup)
# When running in xdist, the logreport and logfinish of multiple
# items are interspersed, e.g. `logreport`, `logreport`,
# `logfinish`, `logfinish`. To avoid the "past edge" calculation
# from getting confused and overflowing (#7166), do the past edge
# printing here and not in logfinish, except for the 100% which
# should only be printed after all teardowns are finished.
if self._show_progress_info and not self._is_last_item:
self._write_progress_information_if_past_edge()
else:
self._progress_nodeids_reported.add(rep.nodeid)
line = self._locationline(rep.nodeid, *rep.location)
running_xdist = hasattr(rep, "node")
if not running_xdist:
self.write_ensure_prefix(line, word, **markup)
if rep.skipped or hasattr(report, "wasxfail"):
@ -627,7 +643,7 @@ class TerminalReporter:
self._write_progress_information_filling_space()
else:
self.ensure_newline()
self._tw.write("[%s]" % rep.node.gateway.id)
self._tw.write(f"[{rep.node.gateway.id}]")
if self._show_progress_info:
self._tw.write(
self._get_progress_information_message() + " ", cyan=True
@ -644,39 +660,29 @@ class TerminalReporter:
assert self._session is not None
return len(self._progress_nodeids_reported) == self._session.testscollected
def pytest_runtest_logfinish(self, nodeid: str) -> None:
assert self._session
@hookimpl(wrapper=True)
def pytest_runtestloop(self) -> Generator[None, object, object]:
result = yield
# Write the final/100% progress -- deferred until the loop is complete.
if (
self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0
and self._show_progress_info
and self._progress_nodeids_reported
):
if self._show_progress_info == "count":
num_tests = self._session.testscollected
progress_length = len(f" [{num_tests}/{num_tests}]")
else:
progress_length = len(" [100%]")
self._write_progress_information_filling_space()
self._progress_nodeids_reported.add(nodeid)
if self._is_last_item:
self._write_progress_information_filling_space()
else:
main_color, _ = self._get_main_color()
w = self._width_of_current_line
past_edge = w + progress_length + 1 >= self._screen_width
if past_edge:
msg = self._get_progress_information_message()
self._tw.write(msg + "\n", **{main_color: True})
return result
def _get_progress_information_message(self) -> str:
assert self._session
collected = self._session.testscollected
if self._show_progress_info == "count":
if collected:
progress = self._progress_nodeids_reported
progress = len(self._progress_nodeids_reported)
counter_format = f"{{:{len(str(collected))}d}}"
format_string = f" [{counter_format}/{{}}]"
return format_string.format(len(progress), collected)
return format_string.format(progress, collected)
return f" [ {collected} / {collected} ]"
else:
if collected:
@ -685,6 +691,20 @@ class TerminalReporter:
)
return " [100%]"
def _write_progress_information_if_past_edge(self) -> None:
w = self._width_of_current_line
if self._show_progress_info == "count":
assert self._session
num_tests = self._session.testscollected
progress_length = len(f" [{num_tests}/{num_tests}]")
else:
progress_length = len(" [100%]")
past_edge = w + progress_length + 1 >= self._screen_width
if past_edge:
main_color, _ = self._get_main_color()
msg = self._get_progress_information_message()
self._tw.write(msg + "\n", **{main_color: True})
def _write_progress_information_filling_space(self) -> None:
color, _ = self._get_main_color()
msg = self._get_progress_information_message()
@ -801,7 +821,9 @@ class TerminalReporter:
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
result.append(
"plugins: {}".format(", ".join(_plugin_nameversions(plugininfo)))
)
return result
def pytest_collection_finish(self, session: Session) -> None:
@ -933,7 +955,7 @@ class TerminalReporter:
line += "[".join(values)
return line
# collect_fspath comes from testid which has a "/"-normalized path.
# fspath comes from testid which has a "/"-normalized path.
if fspath:
res = mkrel(nodeid)
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
@ -1067,24 +1089,29 @@ class TerminalReporter:
self._tw.line(content)
def summary_failures(self) -> None:
self.summary_failures_combined("failed", "FAILURES")
style = self.config.option.tbstyle
self.summary_failures_combined("failed", "FAILURES", style=style)
def summary_xfailures(self) -> None:
self.summary_failures_combined("xfailed", "XFAILURES", needed_opt="x")
show_tb = self.config.option.xfail_tb
style = self.config.option.tbstyle if show_tb else "no"
self.summary_failures_combined("xfailed", "XFAILURES", style=style)
def summary_failures_combined(
self,
which_reports: str,
sep_title: str,
*,
style: str,
needed_opt: str | None = None,
) -> None:
if self.config.option.tbstyle != "no":
if style != "no":
if not needed_opt or self.hasopt(needed_opt):
reports: list[BaseReport] = self.getreports(which_reports)
if not reports:
return
self.write_sep("=", sep_title)
if self.config.option.tbstyle == "line":
if style == "line":
for rep in reports:
line = self._getcrashline(rep)
self.write_line(line)
@ -1179,10 +1206,10 @@ class TerminalReporter:
def show_xfailed(lines: list[str]) -> None:
xfailed = self.stats.get("xfailed", [])
for rep in xfailed:
verbose_word = rep._get_verbose_word(self.config)
markup_word = self._tw.markup(
verbose_word, **{_color_for_type["warnings"]: True}
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
self.config, {_color_for_type["warnings"]: True}
)
markup_word = self._tw.markup(verbose_word, **verbose_markup)
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
line = f"{markup_word} {nodeid}"
reason = rep.wasxfail
@ -1194,10 +1221,10 @@ class TerminalReporter:
def show_xpassed(lines: list[str]) -> None:
xpassed = self.stats.get("xpassed", [])
for rep in xpassed:
verbose_word = rep._get_verbose_word(self.config)
markup_word = self._tw.markup(
verbose_word, **{_color_for_type["warnings"]: True}
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
self.config, {_color_for_type["warnings"]: True}
)
markup_word = self._tw.markup(verbose_word, **verbose_markup)
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
line = f"{markup_word} {nodeid}"
reason = rep.wasxfail
@ -1210,10 +1237,10 @@ class TerminalReporter:
fskips = _folded_skips(self.startpath, skipped) if skipped else []
if not fskips:
return
verbose_word = skipped[0]._get_verbose_word(self.config)
markup_word = self._tw.markup(
verbose_word, **{_color_for_type["warnings"]: True}
verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup(
self.config, {_color_for_type["warnings"]: True}
)
markup_word = self._tw.markup(verbose_word, **verbose_markup)
prefix = "Skipped: "
for num, fspath, lineno, reason in fskips:
if reason.startswith(prefix):
@ -1394,8 +1421,10 @@ def _get_line_with_reprcrash_message(
config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
) -> str:
"""Get summary line for a report, trying to add reprcrash message."""
verbose_word = rep._get_verbose_word(config)
word = tw.markup(verbose_word, **word_markup)
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
config, word_markup
)
word = tw.markup(verbose_word, **verbose_markup)
node = _get_node_id_with_markup(tw, config, rep)
line = f"{word} {node}"

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import inspect
import sys
import traceback
import types
@ -40,23 +41,29 @@ if TYPE_CHECKING:
import twisted.trial.unittest
_SysExcInfoType = Union[
Tuple[Type[BaseException], BaseException, types.TracebackType],
Tuple[None, None, None],
]
_SysExcInfoType = Union[
Tuple[Type[BaseException], BaseException, types.TracebackType],
Tuple[None, None, None],
]
def pytest_pycollect_makeitem(
collector: Module | Class, name: str, obj: object
) -> UnitTestCase | None:
# Has unittest been imported and is obj a subclass of its TestCase?
try:
# Has unittest been imported?
ut = sys.modules["unittest"]
# Is obj a subclass of unittest.TestCase?
# Type ignored because `ut` is an opaque module.
if not issubclass(obj, ut.TestCase): # type: ignore
return None
except Exception:
return None
# Is obj a concrete class?
# Abstract classes can't be instantiated so no point collecting them.
if inspect.isabstract(obj):
return None
# Yes, so let's collect it.
return UnitTestCase.from_parent(collector, name=name, obj=obj)

View File

@ -209,7 +209,7 @@ class CommonFSTests:
@pytest.mark.parametrize(
"fil",
["*dir", "*dir", pytest.mark.skip("sys.version_info <" " (3,6)")(b"*dir")],
["*dir", "*dir", pytest.mark.skip("sys.version_info < (3,6)")(b"*dir")],
)
def test_visit_filterfunc_is_string(self, path1, fil):
lst = []
@ -553,7 +553,7 @@ def batch_make_numbered_dirs(rootdir, repeats):
for i in range(repeats):
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
file_ = dir_.join("foo")
file_.write_text("%s" % i, encoding="utf-8")
file_.write_text(f"{i}", encoding="utf-8")
actual = int(file_.read_text(encoding="utf-8"))
assert (
actual == i
@ -565,9 +565,9 @@ def batch_make_numbered_dirs(rootdir, repeats):
class TestLocalPath(CommonFSTests):
def test_join_normpath(self, tmpdir):
assert tmpdir.join(".") == tmpdir
p = tmpdir.join("../%s" % tmpdir.basename)
p = tmpdir.join(f"../{tmpdir.basename}")
assert p == tmpdir
p = tmpdir.join("..//%s/" % tmpdir.basename)
p = tmpdir.join(f"..//{tmpdir.basename}/")
assert p == tmpdir
@skiponwin32
@ -724,7 +724,7 @@ class TestLocalPath(CommonFSTests):
@pytest.mark.parametrize("bin", (False, True))
def test_dump(self, tmpdir, bin):
path = tmpdir.join("dumpfile%s" % int(bin))
path = tmpdir.join(f"dumpfile{int(bin)}")
try:
d = {"answer": 42}
path.dump(d, bin=bin)

View File

@ -402,7 +402,7 @@ class TestGeneralUsage:
for name, value in vars(hookspec).items():
if name.startswith("pytest_"):
assert value.__doc__, "no docstring for %s" % name
assert value.__doc__, f"no docstring for {name}"
def test_initialization_error_issue49(self, pytester: Pytester) -> None:
pytester.makeconftest(
@ -975,7 +975,7 @@ class TestDurations:
for x in tested:
for y in ("call",): # 'setup', 'call', 'teardown':
for line in result.stdout.lines:
if ("test_%s" % x) in line and y in line:
if (f"test_{x}") in line and y in line:
break
else:
raise AssertionError(f"not found {x} {y}")
@ -988,7 +988,7 @@ class TestDurations:
for x in "123":
for y in ("call",): # 'setup', 'call', 'teardown':
for line in result.stdout.lines:
if ("test_%s" % x) in line and y in line:
if (f"test_{x}") in line and y in line:
break
else:
raise AssertionError(f"not found {x} {y}")
@ -1466,14 +1466,21 @@ def test_issue_9765(pytester: Pytester) -> None:
}
)
subprocess.run([sys.executable, "setup.py", "develop"], check=True)
subprocess.run(
[sys.executable, "-Im", "pip", "install", "-e", "."],
check=True,
)
try:
# We are using subprocess.run rather than pytester.run on purpose.
# pytester.run is adding the current directory to PYTHONPATH which avoids
# the bug. We also use pytest rather than python -m pytest for the same
# PYTHONPATH reason.
subprocess.run(
["pytest", "my_package"], capture_output=True, check=True, text=True
["pytest", "my_package"],
capture_output=True,
check=True,
encoding="utf-8",
text=True,
)
except subprocess.CalledProcessError as exc:
raise AssertionError(

View File

@ -11,6 +11,7 @@ import re
import sys
import textwrap
from typing import Any
from typing import cast
from typing import TYPE_CHECKING
import _pytest._code
@ -27,7 +28,7 @@ import pytest
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
from _pytest._code.code import TracebackStyle
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup
@ -712,6 +713,29 @@ raise ValueError()
assert full_reprlocals.lines
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
def test_repr_args_not_truncated(self, importasmod) -> None:
mod = importasmod(
"""
def func1(m):
raise ValueError("hello\\nworld")
"""
)
excinfo = pytest.raises(ValueError, mod.func1, "m" * 500)
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True, truncate_args=True)
reprfuncargs = p.repr_args(entry)
assert reprfuncargs is not None
arg1 = cast(str, reprfuncargs.args[0][1])
assert len(arg1) < 500
assert "..." in arg1
# again without truncate
p = FormattedExcinfo(funcargs=True, truncate_args=False)
reprfuncargs = p.repr_args(entry)
assert reprfuncargs is not None
assert reprfuncargs.args[0] == ("m", repr("m" * 500))
assert "..." not in cast(str, reprfuncargs.args[0][1])
def test_repr_tracebackentry_lines(self, importasmod) -> None:
mod = importasmod(
"""
@ -901,7 +925,7 @@ raise ValueError()
)
excinfo = pytest.raises(ValueError, mod.entry)
styles: tuple[_TracebackStyle, ...] = ("long", "short")
styles: tuple[TracebackStyle, ...] = ("long", "short")
for style in styles:
p = FormattedExcinfo(style=style)
reprtb = p.repr_traceback(excinfo)
@ -1028,7 +1052,7 @@ raise ValueError()
)
excinfo = pytest.raises(ValueError, mod.entry)
styles: tuple[_TracebackStyle, ...] = ("short", "long", "no")
styles: tuple[TracebackStyle, ...] = ("short", "long", "no")
for style in styles:
for showlocals in (True, False):
repr = excinfo.getrepr(style=style, showlocals=showlocals)
@ -1410,7 +1434,7 @@ raise ValueError()
mod.f()
# emulate the issue described in #1984
attr = "__%s__" % reason
attr = f"__{reason}__"
getattr(excinfo.value, attr).__traceback__ = None
r = excinfo.getrepr()

View File

@ -13,4 +13,4 @@ if __name__ == "__main__":
executable = os.path.join(os.getcwd(), "dist", "runtests_script", "runtests_script")
if sys.platform.startswith("win"):
executable += ".exe"
sys.exit(os.system("%s tests" % executable))
sys.exit(os.system(f"{executable} tests"))

View File

@ -146,7 +146,7 @@ def test_big_repr():
def test_repr_on_newstyle() -> None:
class Function:
def __repr__(self):
return "<%s>" % (self.name) # type: ignore[attr-defined]
return f"<{self.name}>" # type: ignore[attr-defined]
assert saferepr(Function())

View File

@ -1,7 +1,7 @@
anyio[curio,trio]==4.4.0
django==5.0.6
pytest-asyncio==0.23.7
pytest-bdd==7.1.2
pytest-bdd==7.2.0
pytest-cov==5.0.0
pytest-django==4.8.0
pytest-flakes==4.0.5

View File

@ -953,6 +953,43 @@ class TestApprox:
with pytest.raises(TypeError, match="only supports ordered sequences"):
assert {1, 2, 3} == approx({1, 2, 3})
def test_strange_sequence(self):
"""https://github.com/pytest-dev/pytest/issues/11797"""
a = MyVec3(1, 2, 3)
b = MyVec3(0, 1, 2)
# this would trigger the error inside the test
pytest.approx(a, abs=0.5)._repr_compare(b)
assert b == pytest.approx(a, abs=2)
assert b != pytest.approx(a, abs=0.5)
class MyVec3: # incomplete
"""sequence like"""
_x: int
_y: int
_z: int
def __init__(self, x: int, y: int, z: int):
self._x, self._y, self._z = x, y, z
def __repr__(self) -> str:
return f"<MyVec3 {self._x} {self._y} {self._z}>"
def __len__(self) -> int:
return 3
def __getitem__(self, key: int) -> int:
if key == 0:
return self._x
if key == 1:
return self._y
if key == 2:
return self._z
raise IndexError(key)
class TestRecursiveSequenceMap:
def test_map_over_scalar(self):
@ -980,3 +1017,6 @@ class TestRecursiveSequenceMap:
(5, 8),
[(7)],
]
def test_map_over_sequence_like(self):
assert _recursive_sequence_map(int, MyVec3(1, 2, 3)) == [1, 2, 3]

View File

@ -37,9 +37,9 @@ class TestModule:
[
"*import*mismatch*",
"*imported*test_whatever*",
"*%s*" % p1,
f"*{p1}*",
"*not the same*",
"*%s*" % p2,
f"*{p2}*",
"*HINT*",
]
)
@ -263,6 +263,32 @@ class TestClass:
result = pytester.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED
def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None:
"""Regression test for #12275 (non-unittest version)."""
pytester.makepyfile(
"""
import abc
class TestBase(abc.ABC):
@abc.abstractmethod
def abstract1(self): pass
@abc.abstractmethod
def abstract2(self): pass
def test_it(self): pass
class TestPartial(TestBase):
def abstract1(self): pass
class TestConcrete(TestPartial):
def abstract2(self): pass
"""
)
result = pytester.runpytest()
assert result.ret == ExitCode.OK
result.assert_outcomes(passed=1)
class TestFunction:
def test_getmodulecollector(self, pytester: Pytester) -> None:

View File

@ -2289,18 +2289,17 @@ class TestFixtureMarker:
This was a regression introduced in the fix for #736.
"""
pytester.makepyfile(
"""
f"""
import pytest
@pytest.fixture(params=[1, 2])
def fixt(request):
return request.param
@pytest.mark.parametrize(%s, [(3, 'x'), (4, 'x')])
@pytest.mark.parametrize({param_args}, [(3, 'x'), (4, 'x')])
def test_foo(fixt, val):
pass
"""
% param_args
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=2)
@ -3418,6 +3417,28 @@ class TestErrors:
["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"]
)
def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None:
"""Regression test for #12204."""
pytester.makepyfile(
"""
import pytest
@pytest.fixture(scope="session")
def bad(): 1 / 0
def test_1(bad): pass
def test_2(bad): pass
def test_3(bad): pass
"""
)
result = pytester.runpytest_inprocess("--tb=native")
assert result.ret == ExitCode.TESTS_FAILED
failures = result.reprec.getfailures() # type: ignore[attr-defined]
assert len(failures) == 3
lines1 = failures[1].longrepr.reprtraceback.reprentries[0].lines
lines2 = failures[2].longrepr.reprtraceback.reprentries[0].lines
assert len(lines1) == len(lines2)
class TestShowFixtures:
def test_funcarg_compat(self, pytester: Pytester) -> None:
@ -4274,6 +4295,39 @@ class TestScopeOrdering:
request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
def test_parametrized_package_scope_reordering(self, pytester: Pytester) -> None:
"""A paramaterized package-scoped fixture correctly reorders items to
minimize setups & teardowns.
Regression test for #12328.
"""
pytester.makepyfile(
__init__="",
conftest="""
import pytest
@pytest.fixture(scope="package", params=["a", "b"])
def fix(request):
return request.param
""",
test_1="def test1(fix): pass",
test_2="def test2(fix): pass",
)
result = pytester.runpytest("--setup-plan")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(
[
" SETUP P fix['a']",
" test_1.py::test1[a] (fixtures used: fix, request)",
" test_2.py::test2[a] (fixtures used: fix, request)",
" TEARDOWN P fix['a']",
" SETUP P fix['b']",
" test_1.py::test1[b] (fixtures used: fix, request)",
" test_2.py::test2[b] (fixtures used: fix, request)",
" TEARDOWN P fix['b']",
],
)
def test_multiple_packages(self, pytester: Pytester) -> None:
"""Complex test involving multiple package fixtures. Make sure teardowns
are executed in order.

View File

@ -132,6 +132,26 @@ class TestRaises:
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*2 failed*"])
def test_raises_with_invalid_regex(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
def test_invalid_regex():
with pytest.raises(ValueError, match="invalid regex character ["):
raise ValueError()
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"*Invalid regex pattern provided to 'match': unterminated character set at position 24*",
]
)
result.stdout.no_fnmatch_line("*Traceback*")
result.stdout.no_fnmatch_line("*File*")
result.stdout.no_fnmatch_line("*line*")
def test_noclass(self) -> None:
with pytest.raises(TypeError):
pytest.raises("wrong", lambda: None) # type: ignore[call-overload]

View File

@ -101,7 +101,7 @@ class TestImportHookInstallation:
""",
}
pytester.makepyfile(**contents)
result = pytester.runpytest_subprocess("--assert=%s" % mode)
result = pytester.runpytest_subprocess(f"--assert={mode}")
if mode == "plain":
expected = "E AssertionError"
elif mode == "rewrite":
@ -163,7 +163,7 @@ class TestImportHookInstallation:
""",
}
pytester.makepyfile(**contents)
result = pytester.runpytest_subprocess("--assert=%s" % mode)
result = pytester.runpytest_subprocess(f"--assert={mode}")
if mode == "plain":
expected = "E AssertionError"
elif mode == "rewrite":
@ -223,7 +223,7 @@ class TestImportHookInstallation:
) -> None:
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
# Make sure the hook is installed early enough so that plugins
# installed via setuptools are rewritten.
# installed via distribution package are rewritten.
pytester.mkdir("hampkg")
contents = {
"hampkg/__init__.py": """\
@ -280,7 +280,7 @@ class TestImportHookInstallation:
}
pytester.makepyfile(**contents)
result = pytester.run(
sys.executable, "mainwrapper.py", "-s", "--assert=%s" % mode
sys.executable, "mainwrapper.py", "-s", f"--assert={mode}"
)
if mode == "plain":
expected = "E AssertionError"
@ -2045,3 +2045,36 @@ def test_fine_grained_assertion_verbosity(pytester: Pytester):
f"E AssertionError: assert 'hello world' in '{long_text}'",
]
)
def test_full_output_vvv(pytester: Pytester) -> None:
pytester.makepyfile(
r"""
def crash_helper(m):
assert 1 == 2
def test_vvv():
crash_helper(500 * "a")
"""
)
result = pytester.runpytest("")
# without -vvv, the passed args are truncated
expected_non_vvv_arg_line = "m = 'aaaaaaaaaaaaaaa*..aaaaaaaaaaaa*"
result.stdout.fnmatch_lines(
[
expected_non_vvv_arg_line,
"test_full_output_vvv.py:2: AssertionError",
],
)
# double check that the untruncated part is not in the output
expected_vvv_arg_line = "m = '{}'".format(500 * "a")
result.stdout.no_fnmatch_line(expected_vvv_arg_line)
# but with "-vvv" the args are not truncated
result = pytester.runpytest("-vvv")
result.stdout.fnmatch_lines(
[
expected_vvv_arg_line,
"test_full_output_vvv.py:2: AssertionError",
]
)
result.stdout.no_fnmatch_line(expected_non_vvv_arg_line)

View File

@ -10,6 +10,7 @@ import marshal
import os
from pathlib import Path
import py_compile
import re
import stat
import sys
import textwrap
@ -24,6 +25,7 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest.assertion import util
from _pytest.assertion.rewrite import _get_assertion_exprs
from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
from _pytest.assertion.rewrite import _saferepr
from _pytest.assertion.rewrite import AssertionRewritingHook
from _pytest.assertion.rewrite import get_cache_dir
from _pytest.assertion.rewrite import PYC_TAIL
@ -128,6 +130,7 @@ class TestAssertionRewrite:
if isinstance(node, ast.Import):
continue
for n in [node, *ast.iter_child_nodes(node)]:
assert isinstance(n, (ast.stmt, ast.expr))
assert n.lineno == 3
assert n.col_offset == 0
assert n.end_lineno == 6
@ -306,9 +309,7 @@ class TestAssertionRewrite:
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(
["*AssertionError*%s*" % repr((1, 2)), "*assert 1 == 2*"]
)
result.stdout.fnmatch_lines([f"*AssertionError*{(1, 2)!r}*", "*assert 1 == 2*"])
def test_assertion_message_expr(self, pytester: Pytester) -> None:
pytester.makepyfile(
@ -906,7 +907,7 @@ def test_rewritten():
assert test_optimized.__doc__ is None"""
)
p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-")
tmp = "--basetemp=%s" % p
tmp = f"--basetemp={p}"
with monkeypatch.context() as mp:
mp.setenv("PYTHONOPTIMIZE", "2")
mp.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
@ -2037,7 +2038,9 @@ class TestPyCacheDir:
assert test_foo_pyc.is_file()
# normal file: not touched by pytest, normal cache tag
bar_init_pyc = get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc"
bar_init_pyc = (
get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc"
)
assert bar_init_pyc.is_file()
@ -2104,3 +2107,26 @@ class TestIssue11140:
)
result = pytester.runpytest()
assert result.ret == 0
class TestSafereprUnbounded:
class Help:
def bound_method(self): # pragma: no cover
pass
def test_saferepr_bound_method(self):
"""saferepr() of a bound method should show only the method name"""
assert _saferepr(self.Help().bound_method) == "bound_method"
def test_saferepr_unbounded(self):
"""saferepr() of an unbound method should still show the full information"""
obj = self.Help()
# using id() to fetch memory address fails on different platforms
pattern = re.compile(
rf"<{Path(__file__).stem}.{self.__class__.__name__}.Help object at 0x[0-9a-fA-F]*>",
)
assert pattern.match(_saferepr(obj))
assert (
_saferepr(self.Help)
== f"<class '{Path(__file__).stem}.{self.__class__.__name__}.Help'>"
)

View File

@ -206,7 +206,7 @@ def test_cache_reportheader(
monkeypatch.delenv("TOX_ENV_DIR", raising=False)
expected = ".pytest_cache"
result = pytester.runpytest("-v")
result.stdout.fnmatch_lines(["cachedir: %s" % expected])
result.stdout.fnmatch_lines([f"cachedir: {expected}"])
def test_cache_reportheader_external_abspath(
@ -1163,7 +1163,7 @@ class TestNewFirst:
)
p1.write_text(
"def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8"
"def test_1(): assert 1\ndef test_2(): assert 1\n", encoding="utf-8"
)
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))

View File

@ -105,16 +105,15 @@ class TestCaptureManager:
def test_capturing_unicode(pytester: Pytester, method: str) -> None:
obj = "'b\u00f6y'"
pytester.makepyfile(
"""\
f"""\
# taken from issue 227 from nosetests
def test_unicode():
import sys
print(sys.stdout)
print(%s)
print({obj})
"""
% obj
)
result = pytester.runpytest("--capture=%s" % method)
result = pytester.runpytest(f"--capture={method}")
result.stdout.fnmatch_lines(["*1 passed*"])
@ -126,7 +125,7 @@ def test_capturing_bytes_in_utf8_encoding(pytester: Pytester, method: str) -> No
print('b\\u00f6y')
"""
)
result = pytester.runpytest("--capture=%s" % method)
result = pytester.runpytest(f"--capture={method}")
result.stdout.fnmatch_lines(["*1 passed*"])

View File

@ -152,20 +152,8 @@ class TestCollectFS:
assert "test_notfound" not in s
assert "test_found" in s
@pytest.mark.parametrize(
"fname",
(
"activate",
"activate.csh",
"activate.fish",
"Activate",
"Activate.bat",
"Activate.ps1",
),
)
def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None:
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
ensure_file(pytester.path / "virtual" / bindir / fname)
def test_ignored_virtualenvs(self, pytester: Pytester) -> None:
ensure_file(pytester.path / "virtual" / "pyvenv.cfg")
testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py")
testfile.write_text("def test_hello(): pass", encoding="utf-8")
@ -179,23 +167,11 @@ class TestCollectFS:
result = pytester.runpytest("virtual")
assert "test_invenv" in result.stdout.str()
@pytest.mark.parametrize(
"fname",
(
"activate",
"activate.csh",
"activate.fish",
"Activate",
"Activate.bat",
"Activate.ps1",
),
)
def test_ignored_virtualenvs_norecursedirs_precedence(
self, pytester: Pytester, fname: str
self, pytester: Pytester
) -> None:
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
# norecursedirs takes priority
ensure_file(pytester.path / ".virtual" / bindir / fname)
ensure_file(pytester.path / ".virtual" / "pyvenv.cfg")
testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py")
testfile.write_text("def test_hello(): pass", encoding="utf-8")
result = pytester.runpytest("--collect-in-virtualenv")
@ -204,27 +180,13 @@ class TestCollectFS:
result = pytester.runpytest("--collect-in-virtualenv", ".virtual")
assert "test_invenv" in result.stdout.str()
@pytest.mark.parametrize(
"fname",
(
"activate",
"activate.csh",
"activate.fish",
"Activate",
"Activate.bat",
"Activate.ps1",
),
)
def test__in_venv(self, pytester: Pytester, fname: str) -> None:
def test__in_venv(self, pytester: Pytester) -> None:
"""Directly test the virtual env detection function"""
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
# no bin/activate, not a virtualenv
# no pyvenv.cfg, not a virtualenv
base_path = pytester.mkdir("venv")
assert _in_venv(base_path) is False
# with bin/activate, totally a virtualenv
bin_path = base_path.joinpath(bindir)
bin_path.mkdir()
bin_path.joinpath(fname).touch()
# with pyvenv.cfg, totally a virtualenv
base_path.joinpath("pyvenv.cfg").touch()
assert _in_venv(base_path) is True
def test_custom_norecursedirs(self, pytester: Pytester) -> None:
@ -276,14 +238,14 @@ class TestCollectFS:
# collects the tests
for dirname in ("a", "b", "c"):
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
assert [x.name for x in items] == ["test_%s" % dirname]
assert [x.name for x in items] == [f"test_{dirname}"]
# changing cwd to each subdirectory and running pytest without
# arguments collects the tests in that directory normally
for dirname in ("a", "b", "c"):
monkeypatch.chdir(pytester.path.joinpath(dirname))
items, reprec = pytester.inline_genitems()
assert [x.name for x in items] == ["test_%s" % dirname]
assert [x.name for x in items] == [f"test_{dirname}"]
def test_missing_permissions_on_unselected_directory_doesnt_crash(
self, pytester: Pytester
@ -590,7 +552,7 @@ class TestSession:
def test_collect_custom_nodes_multi_id(self, pytester: Pytester) -> None:
p = pytester.makepyfile("def test_func(): pass")
pytester.makeconftest(
"""
f"""
import pytest
class SpecialItem(pytest.Item):
def runtest(self):
@ -599,10 +561,9 @@ class TestSession:
def collect(self):
return [SpecialItem.from_parent(name="check", parent=self)]
def pytest_collect_file(file_path, parent):
if file_path.name == %r:
if file_path.name == {p.name!r}:
return SpecialFile.from_parent(path=file_path, parent=parent)
"""
% p.name
)
id = p.name
@ -880,7 +841,7 @@ def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None:
result = pytester.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines(["*2 passed*"])
res = pytester.runpytest("%s::item2" % p.name)
res = pytester.runpytest(f"{p.name}::item2")
res.stdout.fnmatch_lines(["*1 passed*"])
@ -1462,7 +1423,7 @@ def test_collect_symlink_out_of_tree(pytester: Pytester) -> None:
symlink_to_sub = out_of_tree.joinpath("symlink_to_sub")
symlink_or_skip(sub, symlink_to_sub)
os.chdir(sub)
result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub)
result = pytester.runpytest("-vs", f"--rootdir={sub}", symlink_to_sub)
result.stdout.fnmatch_lines(
[
# Should not contain "sub/"!

View File

@ -64,13 +64,12 @@ class TestParseIni:
p1 = pytester.makepyfile("def test(): pass")
pytester.makefile(
".cfg",
setup="""
setup=f"""
[tool:pytest]
testpaths=%s
testpaths={p1.name}
[pytest]
testpaths=ignored
"""
% p1.name,
""",
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"])
@ -835,11 +834,10 @@ class TestConfigAPI:
)
if str_val != "no-ini":
pytester.makeini(
"""
f"""
[pytest]
strip=%s
strip={str_val}
"""
% str_val
)
config = pytester.parseconfig()
assert config.getini("strip") is bool_val
@ -1287,8 +1285,8 @@ def test_invalid_options_show_extra_information(pytester: Pytester) -> None:
result.stderr.fnmatch_lines(
[
"*error: unrecognized arguments: --invalid-option*",
"* inifile: %s*" % pytester.path.joinpath("tox.ini"),
"* rootdir: %s*" % pytester.path,
"* inifile: {}*".format(pytester.path.joinpath("tox.ini")),
f"* rootdir: {pytester.path}*",
]
)
@ -1420,8 +1418,8 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
def test_get_plugin_specs_as_list() -> None:
def exp_match(val: object) -> str:
return (
"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %s"
% re.escape(repr(val))
f"Plugins may be specified as a sequence or a ','-separated string "
f"of plugin names. Got: {re.escape(repr(val))}"
)
with pytest.raises(pytest.UsageError, match=exp_match({"foo"})):
@ -1834,10 +1832,10 @@ class TestOverrideIniArgs:
self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot
) -> None:
cache_dir = ".custom_cache"
monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir)
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
config = _config_for_test
config._preparse([], addopts=True)
assert config._override_ini == ["cache_dir=%s" % cache_dir]
assert config._override_ini == [f"cache_dir={cache_dir}"]
def test_addopts_from_env_not_concatenated(
self, monkeypatch: MonkeyPatch, _config_for_test
@ -2045,7 +2043,7 @@ def test_invocation_args(pytester: Pytester) -> None:
)
def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None:
p = pytester.makepyfile("def test(): pass")
result = pytester.runpytest(str(p), "-pno:%s" % plugin)
result = pytester.runpytest(str(p), f"-pno:{plugin}")
if plugin == "python":
assert result.ret == ExitCode.USAGE_ERROR
@ -2062,7 +2060,7 @@ def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None
result.stdout.fnmatch_lines(["* 1 passed in *"])
p = pytester.makepyfile("def test(): assert 0")
result = pytester.runpytest(str(p), "-pno:%s" % plugin)
result = pytester.runpytest(str(p), f"-pno:{plugin}")
assert result.ret == ExitCode.TESTS_FAILED
if plugin != "terminal":
result.stdout.fnmatch_lines(["* 1 failed in *"])

View File

@ -279,7 +279,7 @@ def test_conftest_confcutdir(pytester: Pytester) -> None:
),
encoding="utf-8",
)
result = pytester.runpytest("-h", "--confcutdir=%s" % x, x)
result = pytester.runpytest("-h", f"--confcutdir={x}", x)
result.stdout.fnmatch_lines(["*--xyz*"])
result.stdout.no_fnmatch_line("*warning: could not load initial*")
@ -379,7 +379,7 @@ def test_conftest_symlink_files(pytester: Pytester) -> None:
"""
),
}
pytester.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
pytester.makepyfile(**{f"real/{k}": v for k, v in source.items()})
# Create a build directory that contains symlinks to actual files
# but doesn't symlink actual directories.
@ -401,7 +401,7 @@ def test_conftest_badcase(pytester: Pytester) -> None:
"""Check conftest.py loading when directory casing is wrong (#5792)."""
pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True)
source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""}
pytester.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()})
pytester.makepyfile(**{f"JenkinsRoot/{k}": v for k, v in source.items()})
os.chdir(pytester.path.joinpath("jenkinsroot/test"))
result = pytester.runpytest()
@ -637,9 +637,9 @@ class TestConftestVisibility:
) -> None:
"""#616"""
dirs = self._setup_tree(pytester)
print("pytest run in cwd: %s" % (dirs[chdir].relative_to(pytester.path)))
print("pytestarg : %s" % testarg)
print("expected pass : %s" % expect_ntests_passed)
print(f"pytest run in cwd: {dirs[chdir].relative_to(pytester.path)}")
print(f"pytestarg : {testarg}")
print(f"expected pass : {expect_ntests_passed}")
os.chdir(dirs[chdir])
reprec = pytester.inline_run(
testarg,
@ -698,7 +698,7 @@ def test_search_conftest_up_to_inifile(
args = [str(src)]
if confcutdir:
args = ["--confcutdir=%s" % root.joinpath(confcutdir)]
args = [f"--confcutdir={root.joinpath(confcutdir)}"]
result = pytester.runpytest(*args)
match = ""
if passed:

View File

@ -222,7 +222,7 @@ class TestPDB:
pass
"""
)
child = pytester.spawn_pytest("--pdb %s" % p1)
child = pytester.spawn_pytest(f"--pdb {p1}")
child.expect("captured stdout")
child.expect("get rekt")
child.expect("captured stderr")
@ -247,7 +247,7 @@ class TestPDB:
assert False
"""
)
child = pytester.spawn_pytest("--pdb %s" % p1)
child = pytester.spawn_pytest(f"--pdb {p1}")
child.expect("Pdb")
output = child.before.decode("utf8")
child.sendeof()
@ -284,7 +284,7 @@ class TestPDB:
assert False
"""
)
child = pytester.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1)
child = pytester.spawn_pytest(f"--show-capture=all --pdb -p no:logging {p1}")
child.expect("get rekt")
output = child.before.decode("utf8")
assert "captured log" not in output
@ -304,7 +304,7 @@ class TestPDB:
pytest.raises(ValueError, globalfunc)
"""
)
child = pytester.spawn_pytest("--pdb %s" % p1)
child = pytester.spawn_pytest(f"--pdb {p1}")
child.expect(".*def test_1")
child.expect(".*pytest.raises.*globalfunc")
child.expect("Pdb")
@ -321,7 +321,7 @@ class TestPDB:
xxx
"""
)
child = pytester.spawn_pytest("--pdb %s" % p1)
child = pytester.spawn_pytest(f"--pdb {p1}")
# child.expect(".*import pytest.*")
child.expect("Pdb")
child.sendline("c")
@ -336,7 +336,7 @@ class TestPDB:
"""
)
p1 = pytester.makepyfile("def test_func(): pass")
child = pytester.spawn_pytest("--pdb %s" % p1)
child = pytester.spawn_pytest(f"--pdb {p1}")
child.expect("Pdb")
# INTERNALERROR is only displayed once via terminal reporter.
@ -462,7 +462,7 @@ class TestPDB:
assert 0
"""
)
child = pytester.spawn_pytest("--pdb %s" % str(p1))
child = pytester.spawn_pytest(f"--pdb {p1!s}")
child.send("caplog.record_tuples\n")
child.expect_exact(
"[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]"
@ -502,7 +502,7 @@ class TestPDB:
'''
"""
)
child = pytester.spawn_pytest("--doctest-modules --pdb %s" % p1)
child = pytester.spawn_pytest(f"--doctest-modules --pdb {p1}")
child.expect("Pdb")
assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8")
@ -529,7 +529,7 @@ class TestPDB:
)
# NOTE: does not use pytest.set_trace, but Python's patched pdb,
# therefore "-s" is required.
child = pytester.spawn_pytest("--doctest-modules --pdb -s %s" % p1)
child = pytester.spawn_pytest(f"--doctest-modules --pdb -s {p1}")
child.expect("Pdb")
child.sendline("q")
rest = child.read().decode("utf8")
@ -622,7 +622,7 @@ class TestPDB:
pytest.fail("expected_failure")
"""
)
child = pytester.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
child = pytester.spawn_pytest(f"--pdbcls=mytest:CustomPdb {p1!s}")
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect(r"\n\(Pdb")
child.sendline("debug foo()")
@ -659,7 +659,7 @@ class TestPDB:
pytest.set_trace()
"""
)
child = pytester.spawn_pytest("-s %s" % p1)
child = pytester.spawn_pytest(f"-s {p1}")
child.expect(r">>> PDB set_trace >>>")
child.expect("Pdb")
child.sendline("c")
@ -915,7 +915,7 @@ class TestPDB:
"""
)
monkeypatch.setenv("PYTHONPATH", str(pytester.path))
child = pytester.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1))
child = pytester.spawn_pytest(f"--pdbcls=custom_pdb:CustomPdb {p1!s}")
child.expect("__init__")
child.expect("custom set_trace>")
@ -1209,8 +1209,7 @@ def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> Non
child.expect("Pdb")
before = child.before.decode("utf8")
assert (
"> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture)
in before
f"> PDB set_trace (IO-capturing turned off for fixture {fixture}) >" in before
)
# Test that capturing is really suspended.
@ -1226,7 +1225,7 @@ def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> Non
TestPDB.flush(child)
assert child.exitstatus == 0
assert "= 1 passed in" in rest
assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest
assert f"> PDB continue (IO-capturing resumed for fixture {fixture}) >" in rest
def test_pdbcls_via_local_module(pytester: Pytester) -> None:

View File

@ -1157,7 +1157,7 @@ class TestDoctestSkips:
pytester.maketxtfile(doctest)
else:
assert mode == "module"
pytester.makepyfile('"""\n%s"""' % doctest)
pytester.makepyfile(f'"""\n{doctest}"""')
return makeit

View File

@ -103,7 +103,7 @@ def test_timeout(pytester: Pytester, enabled: bool) -> None:
result = pytester.runpytest_subprocess(*args)
tb_output = "most recent call first"
if enabled:
result.stderr.fnmatch_lines(["*%s*" % tb_output])
result.stderr.fnmatch_lines([f"*{tb_output}*"])
else:
assert tb_output not in result.stderr.str()
result.stdout.fnmatch_lines(["*1 passed*"])

View File

@ -12,7 +12,7 @@ def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
assert result.ret == 0
result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"])
if pytestconfig.pluginmanager.list_plugin_distinfo():
result.stdout.fnmatch_lines(["*setuptools registered plugins:", "*at*"])
result.stdout.fnmatch_lines(["*registered third-party plugins:", "*at*"])
def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import datetime
from datetime import timezone
import os
from pathlib import Path
import platform
@ -42,7 +43,7 @@ class RunAndParse:
if family:
args = ("-o", "junit_family=" + family, *args)
xml_path = self.pytester.path.joinpath("junit.xml")
result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args)
result = self.pytester.runpytest(f"--junitxml={xml_path}", *args)
if family == "xunit2":
with xml_path.open(encoding="utf-8") as f:
self.schema.validate(f)
@ -218,11 +219,11 @@ class TestPython:
pass
"""
)
start_time = datetime.now()
start_time = datetime.now(timezone.utc)
result, dom = run_and_parse(family=xunit_family)
node = dom.find_first_by_tag("testsuite")
timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
assert start_time <= timestamp < datetime.now()
timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z")
assert start_time <= timestamp < datetime.now(timezone.utc)
def test_timing_function(
self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing
@ -518,7 +519,7 @@ class TestPython:
)
result, dom = run_and_parse(
"-o", "junit_logging=%s" % junit_logging, family=xunit_family
"-o", f"junit_logging={junit_logging}", family=xunit_family
)
assert result.ret, "Expected ret > 0"
node = dom.find_first_by_tag("testsuite")
@ -603,11 +604,11 @@ class TestPython:
for index, char in enumerate("<&'"):
tnode = node.find_nth_by_tag("testcase", index)
tnode.assert_attr(
classname="test_failure_escape", name="test_func[%s]" % char
classname="test_failure_escape", name=f"test_func[{char}]"
)
sysout = tnode.find_first_by_tag("system-out")
text = sysout.text
assert "%s\n" % char in text
assert f"{char}\n" in text
@parametrize_families
def test_junit_prefixing(
@ -692,7 +693,7 @@ class TestPython:
assert 0
"""
)
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
node = dom.find_first_by_tag("testsuite")
tnode = node.find_first_by_tag("testcase")
if junit_logging in ["system-err", "out-err", "all"]:
@ -762,13 +763,12 @@ class TestPython:
def test_unicode(self, pytester: Pytester, run_and_parse: RunAndParse) -> None:
value = "hx\xc4\x85\xc4\x87\n"
pytester.makepyfile(
"""\
f"""\
# coding: latin1
def test_hello():
print(%r)
print({value!r})
assert 0
"""
% value
)
result, dom = run_and_parse()
assert result.ret == 1
@ -803,7 +803,7 @@ class TestPython:
print('hello-stdout')
"""
)
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
node = dom.find_first_by_tag("testsuite")
pnode = node.find_first_by_tag("testcase")
if junit_logging == "no":
@ -827,7 +827,7 @@ class TestPython:
sys.stderr.write('hello-stderr')
"""
)
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
node = dom.find_first_by_tag("testsuite")
pnode = node.find_first_by_tag("testcase")
if junit_logging == "no":
@ -856,7 +856,7 @@ class TestPython:
pass
"""
)
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
node = dom.find_first_by_tag("testsuite")
pnode = node.find_first_by_tag("testcase")
if junit_logging == "no":
@ -886,7 +886,7 @@ class TestPython:
pass
"""
)
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
node = dom.find_first_by_tag("testsuite")
pnode = node.find_first_by_tag("testcase")
if junit_logging == "no":
@ -917,7 +917,7 @@ class TestPython:
sys.stdout.write('hello-stdout call')
"""
)
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
node = dom.find_first_by_tag("testsuite")
pnode = node.find_first_by_tag("testcase")
if junit_logging == "no":
@ -1011,7 +1011,7 @@ def test_nullbyte(pytester: Pytester, junit_logging: str) -> None:
"""
)
xmlf = pytester.path.joinpath("junit.xml")
pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
pytester.runpytest(f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}")
text = xmlf.read_text(encoding="utf-8")
assert "\x00" not in text
if junit_logging == "system-out":
@ -1033,7 +1033,7 @@ def test_nullbyte_replace(pytester: Pytester, junit_logging: str) -> None:
"""
)
xmlf = pytester.path.joinpath("junit.xml")
pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
pytester.runpytest(f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}")
text = xmlf.read_text(encoding="utf-8")
if junit_logging == "system-out":
assert "#x0" in text
@ -1069,9 +1069,9 @@ def test_invalid_xml_escape() -> None:
for i in invalid:
got = bin_xml_escape(chr(i))
if i <= 0xFF:
expected = "#x%02X" % i
expected = f"#x{i:02X}"
else:
expected = "#x%04X" % i
expected = f"#x{i:04X}"
assert got == expected
for i in valid:
assert chr(i) == bin_xml_escape(chr(i))
@ -1746,7 +1746,7 @@ def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430(
"""
)
result, dom = run_and_parse(
"-o", "junit_logging=%s" % junit_logging, family=xunit_family
"-o", f"junit_logging={junit_logging}", family=xunit_family
)
assert result.ret == 1
node = dom.find_first_by_tag("testcase")

View File

@ -81,7 +81,7 @@ def test_tmpdir_always_is_realpath(pytester: pytest.Pytester) -> None:
assert os.path.realpath(str(tmpdir)) == str(tmpdir)
"""
)
result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp)
result = pytester.runpytest("-s", p, f"--basetemp={linktemp}/bt")
assert not result.ret

View File

@ -233,6 +233,54 @@ def test_mark_option(
assert passed_str == expected_passed
@pytest.mark.parametrize(
("expr", "expected_passed"),
[
("car(color='red')", ["test_one"]),
("car(color='red') or car(color='blue')", ["test_one", "test_two"]),
("car and not car(temp=5)", ["test_one", "test_three"]),
("car(temp=4)", ["test_one"]),
("car(temp=4) or car(temp=5)", ["test_one", "test_two"]),
("car(temp=4) and car(temp=5)", []),
("car(temp=-5)", ["test_three"]),
("car(ac=True)", ["test_one"]),
("car(ac=False)", ["test_two"]),
("car(ac=None)", ["test_three"]), # test NOT_NONE_SENTINEL
],
ids=str,
)
def test_mark_option_with_kwargs(
expr: str, expected_passed: list[str | None], pytester: Pytester
) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.mark.car
@pytest.mark.car(ac=True)
@pytest.mark.car(temp=4)
@pytest.mark.car(color="red")
def test_one():
pass
@pytest.mark.car
@pytest.mark.car(ac=False)
@pytest.mark.car(temp=5)
@pytest.mark.car(color="blue")
def test_two():
pass
@pytest.mark.car
@pytest.mark.car(ac=None)
@pytest.mark.car(temp=-5)
def test_three():
pass
"""
)
rec = pytester.inline_run("-m", expr)
passed, skipped, fail = rec.listoutcomes()
passed_str = [x.nodeid.split("::")[-1] for x in passed]
assert passed_str == expected_passed
@pytest.mark.parametrize(
("expr", "expected_passed"),
[("interface", ["test_interface"]), ("not interface", ["test_nointer"])],
@ -372,6 +420,10 @@ def test_parametrize_with_module(pytester: Pytester) -> None:
"not or",
"at column 5: expected not OR left parenthesis OR identifier; got or",
),
(
"nonexistent_mark(non_supported='kwarg')",
"Keyword expressions do not support call parameters",
),
],
)
def test_keyword_option_wrong_arguments(

View File

@ -1,14 +1,17 @@
from __future__ import annotations
from typing import Callable
from typing import cast
from _pytest.mark import MarkMatcher
from _pytest.mark.expression import Expression
from _pytest.mark.expression import MatcherCall
from _pytest.mark.expression import ParseError
import pytest
def evaluate(input: str, matcher: Callable[[str], bool]) -> bool:
return Expression.compile(input).evaluate(matcher)
return Expression.compile(input).evaluate(cast(MatcherCall, matcher))
def test_empty_is_false() -> None:
@ -153,6 +156,8 @@ def test_syntax_errors(expr: str, column: int, message: str) -> None:
"1234",
"1234abcd",
"1234and",
"1234or",
"1234not",
"notandor",
"not_and_or",
"not[and]or",
@ -195,3 +200,120 @@ def test_valid_idents(ident: str) -> None:
def test_invalid_idents(ident: str) -> None:
with pytest.raises(ParseError):
evaluate(ident, lambda ident: True)
@pytest.mark.parametrize(
"expr, expected_error_msg",
(
("mark(True=False)", "unexpected reserved python keyword `True`"),
("mark(def=False)", "unexpected reserved python keyword `def`"),
("mark(class=False)", "unexpected reserved python keyword `class`"),
("mark(if=False)", "unexpected reserved python keyword `if`"),
("mark(else=False)", "unexpected reserved python keyword `else`"),
("mark(valid=False, def=1)", "unexpected reserved python keyword `def`"),
("mark(1)", "not a valid python identifier 1"),
("mark(var:=False", "not a valid python identifier var:"),
("mark(1=2)", "not a valid python identifier 1"),
("mark(/=2)", "not a valid python identifier /"),
("mark(var==", "expected identifier; got ="),
("mark(var)", "expected =; got right parenthesis"),
("mark(var=none)", 'unexpected character/s "none"'),
("mark(var=1.1)", 'unexpected character/s "1.1"'),
("mark(var=')", """closing quote "'" is missing"""),
('mark(var=")', 'closing quote """ is missing'),
("""mark(var="')""", 'closing quote """ is missing'),
("""mark(var='")""", """closing quote "'" is missing"""),
(
r"mark(var='\hugo')",
r'escaping with "\\" not supported in marker expression',
),
("mark(empty_list=[])", r'unexpected character/s "\[\]"'),
("'str'", "expected not OR left parenthesis OR identifier; got string literal"),
),
)
def test_invalid_kwarg_name_or_value(
expr: str, expected_error_msg: str, mark_matcher: MarkMatcher
) -> None:
with pytest.raises(ParseError, match=expected_error_msg):
assert evaluate(expr, mark_matcher)
@pytest.fixture(scope="session")
def mark_matcher() -> MarkMatcher:
markers = [
pytest.mark.number_mark(a=1, b=2, c=3, d=999_999).mark,
pytest.mark.builtin_matchers_mark(x=True, y=False, z=None).mark,
pytest.mark.str_mark(
m="M", space="with space", empty="", aaאבגדcc="aaאבגדcc", אבגד="אבגד"
).mark,
]
return MarkMatcher.from_markers(markers)
@pytest.mark.parametrize(
"expr, expected",
(
# happy cases
("number_mark(a=1)", True),
("number_mark(b=2)", True),
("number_mark(a=1,b=2)", True),
("number_mark(a=1, b=2)", True),
("number_mark(d=999999)", True),
("number_mark(a = 1,b= 2, c = 3)", True),
# sad cases
("number_mark(a=6)", False),
("number_mark(b=6)", False),
("number_mark(a=1,b=6)", False),
("number_mark(a=6,b=2)", False),
("number_mark(a = 1,b= 2, c = 6)", False),
("number_mark(a='1')", False),
),
)
def test_keyword_expressions_with_numbers(
expr: str, expected: bool, mark_matcher: MarkMatcher
) -> None:
assert evaluate(expr, mark_matcher) is expected
@pytest.mark.parametrize(
"expr, expected",
(
("builtin_matchers_mark(x=True)", True),
("builtin_matchers_mark(x=False)", False),
("builtin_matchers_mark(y=True)", False),
("builtin_matchers_mark(y=False)", True),
("builtin_matchers_mark(z=None)", True),
("builtin_matchers_mark(z=False)", False),
("builtin_matchers_mark(z=True)", False),
("builtin_matchers_mark(z=0)", False),
("builtin_matchers_mark(z=1)", False),
),
)
def test_builtin_matchers_keyword_expressions(
expr: str, expected: bool, mark_matcher: MarkMatcher
) -> None:
assert evaluate(expr, mark_matcher) is expected
@pytest.mark.parametrize(
"expr, expected",
(
("str_mark(m='M')", True),
('str_mark(m="M")', True),
("str_mark(aaאבגדcc='aaאבגדcc')", True),
("str_mark(אבגד='אבגד')", True),
("str_mark(space='with space')", True),
("str_mark(empty='')", True),
('str_mark(empty="")', True),
("str_mark(m='wrong')", False),
("str_mark(aaאבגדcc='wrong')", False),
("str_mark(אבגד='wrong')", False),
("str_mark(m='')", False),
('str_mark(m="")', False),
),
)
def test_str_keyword_expressions(
expr: str, expected: bool, mark_matcher: MarkMatcher
) -> None:
assert evaluate(expr, mark_matcher) is expected

View File

@ -442,7 +442,7 @@ def test_syspath_prepend_with_namespace_packages(
lib = ns.joinpath(dirname)
lib.mkdir()
lib.joinpath("__init__.py").write_text(
"def check(): return %r" % dirname, encoding="utf-8"
f"def check(): return {dirname!r}", encoding="utf-8"
)
monkeypatch.syspath_prepend("hello")

View File

@ -171,7 +171,7 @@ class TestPaste:
assert type(data) is bytes
lexer = "text"
assert url == "https://bpa.st"
assert "lexer=%s" % lexer in data.decode()
assert f"lexer={lexer}" in data.decode()
assert "code=full-paste-contents" in data.decode()
assert "expiry=1week" in data.decode()

Some files were not shown because too many files have changed in this diff Show More