diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 83e32e00..fc1f52cb 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,11 +1,11 @@ [bumpversion] -current_version = 6.3.0 +current_version = 7.0.0 commit = True tag = True -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" [bumpversion:file (badge):README.rst] search = /v{current_version}.svg diff --git a/.cookiecutterrc b/.cookiecutterrc index b28f38bb..8441f282 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' - version: 6.3.0 + version: 7.0.0 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 631c2b31..0b0ba059 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -60,515 +60,155 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage78 (ubuntu)' + - name: 'py39-pytest84-xdist38-coverage710 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage78' + tox_env: 'py39-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage78 (windows)' + - name: 'py39-pytest84-xdist38-coverage710 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage78' + tox_env: 'py39-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py39-pytest83-xdist36-coverage78 (macos)' + - name: 'py39-pytest84-xdist38-coverage710 (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' - tox_env: 'py39-pytest83-xdist36-coverage78' + tox_env: 'py39-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py39-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist37-coverage78 (windows)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py39-pytest83-xdist37-coverage78 (macos)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py39-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py39-pytest84-xdist36-coverage78 (windows)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py39-pytest84-xdist36-coverage78 (macos)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py39-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py39-pytest84-xdist37-coverage78 (windows)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py39-pytest84-xdist37-coverage78 (macos)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py310-pytest83-xdist36-coverage78 (ubuntu)' + - name: 'py310-pytest84-xdist38-coverage710 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage78' + tox_env: 'py310-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py310-pytest83-xdist36-coverage78 (windows)' + - name: 'py310-pytest84-xdist38-coverage710 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage78' + tox_env: 'py310-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py310-pytest83-xdist36-coverage78 (macos)' + - name: 'py310-pytest84-xdist38-coverage710 (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' - tox_env: 'py310-pytest83-xdist36-coverage78' + tox_env: 'py310-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py310-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py310-pytest83-xdist37-coverage78 (windows)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py310-pytest83-xdist37-coverage78 (macos)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py310-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py310-pytest84-xdist36-coverage78 (windows)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py310-pytest84-xdist36-coverage78 (macos)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py310-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py310-pytest84-xdist37-coverage78 (windows)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py310-pytest84-xdist37-coverage78 (macos)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py311-pytest83-xdist36-coverage78 (ubuntu)' + - name: 'py311-pytest84-xdist38-coverage710 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage78' + tox_env: 'py311-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py311-pytest83-xdist36-coverage78 (windows)' + - name: 'py311-pytest84-xdist38-coverage710 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage78' + tox_env: 'py311-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py311-pytest83-xdist36-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'py311-pytest83-xdist37-coverage78 (ubuntu)' + - name: 'py311-pytest84-xdist38-coverage710 (macos)' python: '3.11' toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py311-pytest83-xdist37-coverage78 (windows)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py311-pytest83-xdist37-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py311-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py311-pytest84-xdist36-coverage78 (windows)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py311-pytest84-xdist36-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py311-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py311-pytest84-xdist37-coverage78 (windows)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py311-pytest84-xdist37-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py312-pytest83-xdist36-coverage78 (ubuntu)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py312-pytest83-xdist36-coverage78 (windows)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'py312-pytest83-xdist36-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'arm64' - tox_env: 'py312-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'py312-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py312-pytest83-xdist37-coverage78 (windows)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py312-pytest83-xdist37-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' python_arch: 'arm64' - tox_env: 'py312-pytest83-xdist37-coverage78' + tox_env: 'py311-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py312-pytest84-xdist36-coverage78 (ubuntu)' + - name: 'py312-pytest84-xdist38-coverage710 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest84-xdist36-coverage78' + tox_env: 'py312-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py312-pytest84-xdist36-coverage78 (windows)' + - name: 'py312-pytest84-xdist38-coverage710 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest84-xdist36-coverage78' + tox_env: 'py312-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py312-pytest84-xdist36-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'arm64' - tox_env: 'py312-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py312-pytest84-xdist37-coverage78 (ubuntu)' + - name: 'py312-pytest84-xdist38-coverage710 (macos)' python: '3.12' toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py312-pytest84-xdist37-coverage78 (windows)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py312-pytest84-xdist37-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'arm64' - tox_env: 'py312-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py313-pytest83-xdist36-coverage78 (ubuntu)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py313-pytest83-xdist36-coverage78 (windows)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'py313-pytest83-xdist36-coverage78 (macos)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'arm64' - tox_env: 'py313-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'py313-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py313-pytest83-xdist37-coverage78 (windows)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py313-pytest83-xdist37-coverage78 (macos)' - python: '3.13' - toxpython: 'python3.13' python_arch: 'arm64' - tox_env: 'py313-pytest83-xdist37-coverage78' + tox_env: 'py312-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py313-pytest84-xdist36-coverage78 (ubuntu)' + - name: 'py313-pytest84-xdist38-coverage710 (ubuntu)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'py313-pytest84-xdist36-coverage78' + tox_env: 'py313-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py313-pytest84-xdist36-coverage78 (windows)' + - name: 'py313-pytest84-xdist38-coverage710 (windows)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'py313-pytest84-xdist36-coverage78' + tox_env: 'py313-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py313-pytest84-xdist36-coverage78 (macos)' + - name: 'py313-pytest84-xdist38-coverage710 (macos)' python: '3.13' toxpython: 'python3.13' python_arch: 'arm64' - tox_env: 'py313-pytest84-xdist36-coverage78' + tox_env: 'py313-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py313-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py313-pytest84-xdist37-coverage78 (windows)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py313-pytest84-xdist37-coverage78 (macos)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'arm64' - tox_env: 'py313-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy39-pytest83-xdist36-coverage78 (ubuntu)' + - name: 'pypy39-pytest84-xdist38-coverage710 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage78' + tox_env: 'pypy39-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'pypy39-pytest83-xdist36-coverage78 (windows)' + - name: 'pypy39-pytest84-xdist38-coverage710 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage78' + tox_env: 'pypy39-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'pypy39-pytest83-xdist36-coverage78 (macos)' + - name: 'pypy39-pytest84-xdist38-coverage710 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' - tox_env: 'pypy39-pytest83-xdist36-coverage78' + tox_env: 'pypy39-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'pypy39-pytest83-xdist37-coverage78 (ubuntu)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'pypy39-pytest83-xdist37-coverage78 (windows)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'pypy39-pytest83-xdist37-coverage78 (macos)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'arm64' - tox_env: 'pypy39-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy39-pytest84-xdist36-coverage78 (ubuntu)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'pypy39-pytest84-xdist36-coverage78 (windows)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'pypy39-pytest84-xdist36-coverage78 (macos)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'arm64' - tox_env: 'pypy39-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'pypy39-pytest84-xdist37-coverage78 (ubuntu)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'pypy39-pytest84-xdist37-coverage78 (windows)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'pypy39-pytest84-xdist37-coverage78 (macos)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'arm64' - tox_env: 'pypy39-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy310-pytest83-xdist36-coverage78 (ubuntu)' + - name: 'pypy310-pytest84-xdist38-coverage710 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage78' + tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'pypy310-pytest83-xdist36-coverage78 (windows)' + - name: 'pypy310-pytest84-xdist38-coverage710 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage78' + tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'pypy310-pytest83-xdist36-coverage78 (macos)' + - name: 'pypy310-pytest84-xdist38-coverage710 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' - tox_env: 'pypy310-pytest83-xdist36-coverage78' + tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'pypy310-pytest83-xdist37-coverage78 (ubuntu)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'pypy310-pytest83-xdist37-coverage78 (windows)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'pypy310-pytest83-xdist37-coverage78 (macos)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'arm64' - tox_env: 'pypy310-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy310-pytest84-xdist36-coverage78 (ubuntu)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'pypy310-pytest84-xdist36-coverage78 (windows)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' + - name: 'pypy311-pytest84-xdist38-coverage710 (ubuntu)' + python: 'pypy-3.11' + toxpython: 'pypy3.11' python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'pypy310-pytest84-xdist36-coverage78 (macos)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'arm64' - tox_env: 'pypy310-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'pypy310-pytest84-xdist37-coverage78 (ubuntu)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist37-coverage78' + tox_env: 'pypy311-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'pypy310-pytest84-xdist37-coverage78 (windows)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' + - name: 'pypy311-pytest84-xdist38-coverage710 (windows)' + python: 'pypy-3.11' + toxpython: 'pypy3.11' python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist37-coverage78' + tox_env: 'pypy311-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'pypy310-pytest84-xdist37-coverage78 (macos)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' + - name: 'pypy311-pytest84-xdist38-coverage710 (macos)' + python: 'pypy-3.11' + toxpython: 'pypy3.11' python_arch: 'arm64' - tox_env: 'pypy310-pytest84-xdist37-coverage78' + tox_env: 'pypy311-pytest84-xdist38-coverage710' os: 'macos-latest' steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d2f108a..e8687d3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,15 +6,21 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.12 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format + - id: taplo-lint - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - exclude: '.*\.pth$' + - id: mixed-line-ending + args: [--fix=lf] - id: debug-statements diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 00000000..8f8054d3 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,3 @@ +[formatting] +array_auto_collapse = false +indent_string = " " diff --git a/AUTHORS.rst b/AUTHORS.rst index bcb66ddc..42933ffa 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -64,3 +64,4 @@ Authors * Dawn James - https://github.com/dawngerpony * Tsvika Shapira - https://github.com/tsvikas * Marcos Boger - https://github.com/marcosboger +* Ofek Lev - https://github.com/ofek diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9caaa8f5..4524aad3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,30 @@ Changelog ========= +7.0.0 (2025-09-09) +------------------ + +* Dropped support for subprocesses measurement. + + It was a feature added long time ago when coverage lacked a nice way to measure subprocesses created in tests. + It relied on a ``.pth`` file, there was no way to opt-out and it created bad interations + with `coverage's new patch system `_ added + in `7.10 `_. + + To migrate to this release you might need to enable the suprocess patch, example for ``.coveragerc``: + + .. code-block:: ini + + [run] + patch = subprocess + + This release also requires at least coverage 7.10.6. +* Switched packaging to have metadata completely in ``pyproject.toml`` and use `hatchling `_ for + building. + Contributed by Ofek Lev in `#551 `_ + with some extras in `#716 `_. +* Removed some not really necessary testing deps like ``six``. + 6.3.0 (2025-09-06) ------------------ @@ -258,8 +282,8 @@ Changelog ------------------ * Fixed ``RecursionError`` that can occur when using - `cleanup_on_signal `__ or - `cleanup_on_sigterm `__. + `cleanup_on_signal `__ or + `cleanup_on_sigterm `__. See: `#294 `_. The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API. * Added compatibility with future xdist release that deprecates some internals diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 529ba8f4..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,30 +0,0 @@ -graft docs -graft examples -prune examples/*/.tox -prune examples/*/htmlcov -prune examples/*/*/htmlcov -prune examples/adhoc-layout/*.egg-info -prune examples/src-layout/src/*.egg-info - -graft .github/workflows -graft src -graft ci -graft tests - -include .bumpversion.cfg -include .cookiecutterrc -include .coveragerc -include .editorconfig -include .pre-commit-config.yaml -include .readthedocs.yml -include pytest.ini -include tox.ini - -include AUTHORS.rst -include CHANGELOG.rst -include CONTRIBUTING.rst -include LICENSE -include README.rst -include SECURITY.md - -global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index 750f8d30..143d9422 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,7 @@ Overview - |github-actions| * - package - |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| |commits-since| + .. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat :target: https://readthedocs.org/projects/pytest-cov/ :alt: Documentation Status @@ -39,16 +40,17 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/pytest-cov -.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v6.3.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v7.0.0.svg :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v6.3.0...master + :target: https://github.com/pytest-dev/pytest-cov/compare/v7.0.0...master .. end-badges -This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: +This plugin provides coverage functionality as a pytest plugin. Compared to just using ``coverage run`` this plugin does some extras: -* Subprocess support: you can fork or run stuff in a subprocess and will get covered without any fuss. -* Xdist support: you can use all of pytest-xdist's features and still get coverage. +* Automatic erasing and combination of .coverage files and default reporting. +* Support for detailed coverage contexts (add ``--cov-context=test`` to have the full test name including parametrization as the context). +* Xdist support: you can use all of pytest-xdist's features including remote interpreters and still get coverage. * Consistent pytest behavior. If you run ``coverage run -m pytest`` you will have slightly different ``sys.path`` (CWD will be in it, unlike when running ``pytest``). @@ -68,11 +70,10 @@ For distributed testing support install pytest-xdist:: pip install pytest-xdist -Upgrading from ancient pytest-cov ---------------------------------- +Upgrading from pytest-cov 6.3 +----------------------------- -`pytest-cov 2.0` is using a new ``.pth`` file (``pytest-cov.pth``). You may want to manually remove the older -``init_cov_core.pth`` from site-packages as it's not automatically removed. +`pytest-cov 6.3` and older were using a ``.pth`` file to enable coverage measurements in subprocesses. This was removed in `pytest-cov 7` - use `coverage's patch options `_ to enable subprocess measurements. Uninstalling ------------ @@ -111,10 +112,6 @@ Documentation https://pytest-cov.readthedocs.io/en/latest/ - - - - Coverage Data File ================== @@ -132,12 +129,6 @@ For distributed testing the workers must have the pytest-cov package installed. the plugin must be registered through setuptools for pytest to start the plugin on the worker. -For subprocess measurement environment variables must make it from the main process to the -subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must -do normal site initialisation so that the environment variables can be detected and coverage -started. See the `subprocess support docs `_ -for more details of how this works. - Security ======== diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 08d6c90b..ee69e2c5 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -20,8 +20,6 @@ def exec_in_env(): else: bin_path = env_path / 'bin' if not env_path.exists(): - import subprocess - print(f'Making bootstrap env in: {env_path} ...') try: check_call([sys.executable, '-m', 'venv', env_path]) @@ -59,7 +57,7 @@ def main(): # This uses sys.executable the same way that the call in # cookiecutter-pylibrary/hooks/post_gen_project.py # invokes this bootstrap.py itself. - for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], text=True).splitlines() + for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() ] tox_environments = [line for line in tox_environments if line.startswith('py')] for template in templates_path.rglob('*'): diff --git a/ci/requirements.txt b/ci/requirements.txt index b4f18520..fdb6b93a 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,5 +1,4 @@ -virtualenv>=16.6.0 -pip>=19.1.1 -setuptools>=18.0.1 -tox -twine +pip>=25 +setuptools>=80 +tox>=4 +virtualenv>=20.34 diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml index 22fec036..df6150ae 100644 --- a/ci/templates/.github/workflows/test.yml +++ b/ci/templates/.github/workflows/test.yml @@ -17,10 +17,10 @@ jobs: - {python-version: "pypy-3.9", tox-python-version: "pypy3"} - {python-version: "3.11", tox-python-version: "py311"} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -88,10 +88,10 @@ jobs: {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} diff --git a/docs/conf.py b/docs/conf.py index 97a92563..f4250bc7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' -version = release = '6.3.0' +version = release = '7.0.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/docs/config.rst b/docs/config.rst index ff6689d8..4369d1e7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -48,14 +48,14 @@ Or for ``pyproject.toml``: :: Caveats ======= -A unfortunate consequence of coverage.py's history is that ``.coveragerc`` is a magic name: it's the default file but it also +An unfortunate consequence of coverage.py's history is that ``.coveragerc`` is a magic name: it's the default file but it also means "try to also lookup coverage configuration in ``tox.ini`` or ``setup.cfg``". In practical terms this means that if you have multiple configuration files around (``tox.ini``, ``pyproject.toml`` or ``setup.cfg``) you might need to use ``--cov-config`` to make coverage use the correct configuration file. Also, if you change the working directory and also use subprocesses in a test you might also need to use ``--cov-config`` to make pytest-cov -will use the expected configuration file in the subprocess. +use the expected configuration file in the subprocess. Reference ========= diff --git a/docs/plugins.rst b/docs/plugins.rst index 577870de..6c6b1c13 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -6,19 +6,5 @@ Getting coverage on pytest plugins is a very particular situation. Because of ho entrypoints) it doesn't allow controlling the order in which the plugins load. See `pytest/issues/935 `_ for technical details. -The current way of dealing with this problem is using the append feature and manually starting ``pytest-cov``'s engine, eg:: - - COV_CORE_SOURCE=src COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage.eager pytest --cov=src --cov-append - -Alternatively you can have this in ``tox.ini`` (if you're using `Tox `_ of course):: - - [testenv] - setenv = - COV_CORE_SOURCE= - COV_CORE_CONFIG={toxinidir}/.coveragerc - COV_CORE_DATAFILE={toxinidir}/.coverage - -And in ``pytest.ini`` / ``tox.ini`` / ``setup.cfg``:: - - [tool:pytest] - addopts = --cov --cov-append +**Currently there is no way to measure your pytest plugin if you use pytest-cov**. +You should change your test invocations to use ``coverage run -m pytest ...`` instead. diff --git a/docs/releasing.rst b/docs/releasing.rst index 9afe600d..3032344d 100644 --- a/docs/releasing.rst +++ b/docs/releasing.rst @@ -23,7 +23,7 @@ The process for releasing should follow these steps: These files need to be removed to force distutils/setuptools to rebuild everything and recreate the egg-info metadata. #. Build the dists:: - python3 setup.py clean --all sdist bdist_wheel + python -m build #. Verify that the resulting archives (found in ``dist/``) are good. #. Upload the sdist and wheel with twine:: diff --git a/docs/reporting.rst b/docs/reporting.rst index a8da25d4..06bad7cc 100644 --- a/docs/reporting.rst +++ b/docs/reporting.rst @@ -3,12 +3,12 @@ Reporting It is possible to generate any combination of the reports for a single test run. -The available reports are terminal (with or without missing line numbers shown), HTML, XML, JSON, Markdown (either in 'write' or 'append' mode to file), LCOV and -annotated source code. +The available reports are terminal (with or without missing line numbers shown), HTML, XML, JSON, Markdown (either in 'write' or 'append' +mode to file), LCOV and annotated source code. -The terminal report without line numbers (default):: +The default is terminal report without line numbers:: - pytest --cov-report term --cov=myproj tests/ + pytest --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover @@ -22,7 +22,7 @@ The terminal report without line numbers (default):: The terminal report with line numbers:: - pytest --cov-report term-missing --cov=myproj tests/ + pytest --cov-report=term-missing --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover Missing @@ -49,32 +49,45 @@ The terminal report with skip covered:: You can use ``skip-covered`` with ``term-missing`` as well. e.g. ``--cov-report term-missing:skip-covered`` -The report options below output to files without showing anything on the terminal:: +If any reporting options are used then the default (``--cov-report=term`` is not added automatically). For example this would not show any +terminal output: + +.. code-block:: bash pytest --cov-report html - --cov-report xml - --cov-report json - --cov-report markdown - --cov-report markdown-append:cov-append.md - --cov-report lcov - --cov-report annotate - --cov=myproj tests/ - -The output location for each of these reports can be specified. The output location for the XML, JSON, Markdown and LCOV -report is a file. markdown-append option is specially useful for appending the report to an existing file. Example for GitHub Actions: ---cov-report=markdown-append:${GITHUB_STEP_SUMMARY}. Where as the output location for the HTML and annotated source code reports are -directories:: + --cov-report xml + --cov-report json + --cov-report markdown + --cov-report markdown-append:cov-append.md + --cov-report lcov + --cov-report annotate + --cov=myproj tests/ + +You can specify output paths for reports. The output location for the XML, JSON, Markdown and LCOV +report is a file. Where as the output location for the HTML and annotated source code reports are +directories: + +.. code-block:: bash pytest --cov-report html:cov_html - --cov-report xml:cov.xml - --cov-report json:cov.json - --cov-report markdown:cov.md - --cov-report markdown-append:cov-append.md - --cov-report lcov:cov.info - --cov-report annotate:cov_annotate - --cov=myproj tests/ - -The final report option can also suppress printing to the terminal:: + --cov-report xml:cov.xml + --cov-report json:cov.json + --cov-report markdown:cov.md + --cov-report markdown-append:cov-append.md + --cov-report lcov:cov.info + --cov-report annotate:cov_annotate + --cov=myproj tests/ + +Example for GitHub Actions with ``markdown-append``: + +.. code-block:: bash + + pytest --cov-report=markdown-append:${GITHUB_STEP_SUMMARY}. + --cov=myproj tests/ + +To disable the default ``term`` report provide an empty report: + +.. code-block:: bash pytest --cov-report= --cov=myproj tests/ diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 56044392..7e552c45 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -2,189 +2,21 @@ Subprocess support ================== -Normally coverage writes the data via a pretty standard atexit handler. However, if the subprocess doesn't exit on its -own then the atexit handler might not run. Why that happens is best left to the adventurous to discover by waddling -through the Python bug tracker. - -pytest-cov supports subprocesses, and works around these atexit limitations. However, there are a few pitfalls that need to be explained. - -But first, how does pytest-cov's subprocess support works? - -pytest-cov packaging injects a pytest-cov.pth into the installation. This file effectively runs this at *every* python startup: - -.. code-block:: python - - if 'COV_CORE_SOURCE' in os.environ: - try: - from pytest_cov.embed import init - init() - except Exception as exc: - sys.stderr.write( - "pytest-cov: Failed to setup subprocess coverage. " - "Environ: {0!r} " - "Exception: {1!r}\n".format( - dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')), - exc - ) - ) - -The pytest plugin will set this ``COV_CORE_SOURCE`` environment variable thus any subprocess that inherits the environment variables -(the default behavior) will run ``pytest_cov.embed.init`` which in turn sets up coverage according to these variables: - -* ``COV_CORE_SOURCE`` -* ``COV_CORE_CONFIG`` -* ``COV_CORE_DATAFILE`` -* ``COV_CORE_BRANCH`` -* ``COV_CORE_CONTEXT`` - -Why does it have the ``COV_CORE`` you wonder? Well, it's mostly historical reasons: long time ago pytest-cov depended on a cov-core package -that implemented common functionality for pytest-cov, nose-cov and nose2-cov. The dependency is gone but the convention is kept. It could -be changed but it would break all projects that manually set these intended-to-be-internal-but-sadly-not-in-reality environment variables. - -Coverage's subprocess support -============================= - -Now that you understand how pytest-cov works you can easily figure out that using -`coverage's recommended `_ way of dealing with subprocesses, -by either having this in a ``.pth`` file or ``sitecustomize.py`` will break everything: - -.. code-block:: - - import coverage; coverage.process_startup() # this will break pytest-cov - -Do not do that as that will restart coverage with the wrong options. - -If you use ``multiprocessing`` -============================== - -Builtin support for multiprocessing was dropped in pytest-cov 4.0. -This support was mostly working but very broken in certain scenarios (see `issue 82408 `_) -and made the test suite very flaky and slow. - -However, there is `builtin multiprocessing support in coverage `_ -and you can migrate to that. All you need is this in your preferred configuration file (example: ``.coveragerc``): +Subprocess support was removed in pytest-cov 7.0 due to various complexities resulting from coverage's own subprocess support. +To migrate you should change your coverage config to have at least this: .. code-block:: ini [run] - concurrency = multiprocessing - parallel = true - sigterm = true - -Now as a side-note, it's a good idea in general to properly close your Pool by using ``Pool.join()``: - -.. code-block:: python - - from multiprocessing import Pool - - def f(x): - return x*x - - if __name__ == '__main__': - p = Pool(5) - try: - print(p.map(f, [1, 2, 3])) - finally: - p.close() # Marks the pool as closed. - p.join() # Waits for workers to exit. - - -.. _cleanup_on_sigterm: - -Signal handlers -=============== - -pytest-cov provides a signal handling routines, mostly for special situations where you'd have custom signal handling that doesn't -allow atexit to properly run and the now-gone multiprocessing support: - -* ``pytest_cov.embed.cleanup_on_sigterm()`` -* ``pytest_cov.embed.cleanup_on_signal(signum)`` (e.g.: ``cleanup_on_signal(signal.SIGHUP)``) - -If you use multiprocessing --------------------------- - -It is not recommanded to use these signal handlers with multiprocessing as registering signal handlers will cause deadlocks in the pool, -see: https://bugs.python.org/issue38227). - -If you got custom signal handling ---------------------------------- - -**pytest-cov 2.6** has a rudimentary ``pytest_cov.embed.cleanup_on_sigterm`` you can use to register a SIGTERM handler -that flushes the coverage data. - -**pytest-cov 2.7** adds a ``pytest_cov.embed.cleanup_on_signal`` function and changes the implementation to be more -robust: the handler will call the previous handler (if you had previously registered any), and is re-entrant (will -defer extra signals if delivered while the handler runs). - -For example, if you reload on SIGHUP you should have something like this: - -.. code-block:: python - - import os - import signal - - def restart_service(frame, signum): - os.exec( ... ) # or whatever your custom signal would do - signal.signal(signal.SIGHUP, restart_service) - - try: - from pytest_cov.embed import cleanup_on_signal - except ImportError: - pass - else: - cleanup_on_signal(signal.SIGHUP) - -Note that both ``cleanup_on_signal`` and ``cleanup_on_sigterm`` will run the previous signal handler. - -Alternatively you can do this: - -.. code-block:: python - - import os - import signal - - try: - from pytest_cov.embed import cleanup - except ImportError: - cleanup = None - - def restart_service(frame, signum): - if cleanup is not None: - cleanup() - - os.exec( ... ) # or whatever your custom signal would do - signal.signal(signal.SIGHUP, restart_service) - -If you use Windows ------------------- - -On Windows you can register a handler for SIGTERM but it doesn't actually work. It will work if you -`os.kill(os.getpid(), signal.SIGTERM)` (send SIGTERM to the current process) but for most intents and purposes that's -completely useless. - -Consequently this means that if you use multiprocessing you got no choice but to use the close/join pattern as described -above. Using the context manager API or `terminate` won't work as it relies on SIGTERM. - -However you can have a working handler for SIGBREAK (with some caveats): - -.. code-block:: python + patch = subprocess - import os - import signal +Or if you use pyproject.toml: - def shutdown(frame, signum): - # your app's shutdown or whatever - signal.signal(signal.SIGBREAK, shutdown) +.. code-block:: toml - try: - from pytest_cov.embed import cleanup_on_signal - except ImportError: - pass - else: - cleanup_on_signal(signal.SIGBREAK) + [tool.coverage.run] + patch = ["subprocess"] -The `caveats `_ being -roughly: +Note that if you enable the subprocess patch then ``parallel = true`` is automatically set. -* you need to deliver ``signal.CTRL_BREAK_EVENT`` -* it gets delivered to the whole process group, and that can have unforeseen consequences +If it still doesn't produce the same coverage as before you may need to enable more patches, see the `coverage config `_ and `subprocess `_ documentation. diff --git a/pyproject.toml b/pyproject.toml index e795c6de..1125a4c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,76 @@ [build-system] -requires = [ - "setuptools>=30.3.0", +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "pytest-cov" +dynamic = ["readme"] +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +license = "MIT" +requires-python = ">=3.9" +authors = [ + { name = "Marc Schlaich", email = "marc.schlaich@gmail.com" }, +] +maintainers = [ + { name = "Ionel Cristian Mărieș", email = "contact@ionelmc.ro" }, +] +keywords = [ + "cover", + "coverage", + "distributed", + "parallel", + "py.test", + "pytest", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Testing", + "Topic :: Utilities", +] +dependencies = [ + "coverage[toml]>=7.10.6", + "pytest>=7", + "pluggy>=1.2", +] + +[project.optional-dependencies] +testing = [ + "process-tests", + "pytest-xdist", + "virtualenv", +] + +[project.entry-points.pytest11] +pytest_cov = "pytest_cov.plugin" + +[project.urls] +"Sources" = "https://github.com/pytest-dev/pytest-cov" +"Documentation" = "https://pytest-cov.readthedocs.io/" +"Changelog" = "https://pytest-cov.readthedocs.io/en/latest/changelog.html" +"Issue Tracker" = "https://github.com/pytest-dev/pytest-cov/issues" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" +fragments = [ + { path = "README.rst" }, + { path = "CHANGELOG.rst" }, ] [tool.ruff] @@ -14,30 +84,31 @@ target-version = "py39" [tool.ruff.lint] ignore = [ - "RUF001", # ruff-specific rules ambiguous-unicode-character-string - "S101", # flake8-bandit assert - "S308", # flake8-bandit suspicious-mark-safe-usage - "E501", # pycodestyle line-too-long + "PLC0415", # `import` should be at the top-level of a file + "RUF001", # ruff-specific rules ambiguous-unicode-character-string + "S101", # flake8-bandit assert + "S308", # flake8-bandit suspicious-mark-safe-usage + "E501", # pycodestyle line-too-long ] select = [ - "B", # flake8-bugbear - "C4", # flake8-comprehensions + "B", # flake8-bugbear + "C4", # flake8-comprehensions "DTZ", # flake8-datetimez - "E", # pycodestyle errors + "E", # pycodestyle errors "EXE", # flake8-executable - "F", # pyflakes - "I", # isort + "F", # pyflakes + "I", # isort "INT", # flake8-gettext "PIE", # flake8-pie "PLC", # pylint convention "PLE", # pylint errors - "PT", # flake8-pytest-style + "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "RSE", # flake8-raise "RUF", # ruff-specific rules - "S", # flake8-bandit - "UP", # pyupgrade - "W", # pycodestyle warnings + "S", # flake8-bandit + "UP", # pyupgrade + "W", # pycodestyle warnings ] [tool.ruff.lint.flake8-pytest-style] diff --git a/setup.py b/setup.py deleted file mode 100755 index 3532adac..00000000 --- a/setup.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python - -import re -from itertools import chain -from pathlib import Path - -from setuptools import Command -from setuptools import find_packages -from setuptools import setup - -try: - # https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html - from setuptools.command.build import build -except ImportError: - from distutils.command.build import build - -from setuptools.command.develop import develop -from setuptools.command.easy_install import easy_install -from setuptools.command.install_lib import install_lib - - -def read(*names, **kwargs): - with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: - return fh.read() - - -class BuildWithPTH(build): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.build_lib) / Path(path).name) - self.copy_file(path, dest) - - -class EasyInstallWithPTH(easy_install): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.install_dir) / Path(path).name) - self.copy_file(path, dest) - - -class InstallLibWithPTH(install_lib): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.install_dir) / Path(path).name) - self.copy_file(path, dest) - self.outputs = [dest] - - def get_outputs(self): - return chain(super().get_outputs(), self.outputs) - - -class DevelopWithPTH(develop): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.install_dir) / Path(path).name) - self.copy_file(path, dest) - - -class GeneratePTH(Command): - user_options = () - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - with Path(__file__).parent.joinpath('src', 'pytest-cov.pth').open('w') as fh: - with Path(__file__).parent.joinpath('src', 'pytest-cov.embed').open() as sh: - fh.write(f'import os, sys;exec({sh.read().replace(" ", " ")!r})') - - -setup( - name='pytest-cov', - version='6.3.0', - license='MIT', - description='Pytest plugin for measuring coverage.', - long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), - author='Marc Schlaich', - author_email='marc.schlaich@gmail.com', - url='https://github.com/pytest-dev/pytest-cov', - packages=find_packages('src'), - package_dir={'': 'src'}, - py_modules=[path.stem for path in Path('src').glob('*.py')], - include_package_data=True, - zip_safe=False, - classifiers=[ - # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers - 'Development Status :: 5 - Production/Stable', - 'Framework :: Pytest', - 'Intended Audience :: Developers', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Testing', - 'Topic :: Utilities', - ], - project_urls={ - 'Documentation': 'https://pytest-cov.readthedocs.io/', - 'Changelog': 'https://pytest-cov.readthedocs.io/en/latest/changelog.html', - 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues', - }, - keywords=[ - 'cover', - 'coverage', - 'pytest', - 'py.test', - 'distributed', - 'parallel', - ], - python_requires='>=3.9', - install_requires=[ - 'pytest>=6.2.5', - 'coverage[toml]>=7.5', - 'pluggy>=1.2', - ], - extras_require={ - 'testing': [ - 'fields', - 'hunter', - 'process-tests', - 'pytest-xdist', - 'virtualenv', - ] - }, - entry_points={ - 'pytest11': [ - 'pytest_cov = pytest_cov.plugin', - ], - }, - cmdclass={ - 'build': BuildWithPTH, - 'easy_install': EasyInstallWithPTH, - 'install_lib': InstallLibWithPTH, - 'develop': DevelopWithPTH, - 'genpth': GeneratePTH, - }, -) diff --git a/src/pytest-cov.embed b/src/pytest-cov.embed deleted file mode 100644 index 630a2a72..00000000 --- a/src/pytest-cov.embed +++ /dev/null @@ -1,13 +0,0 @@ -if 'COV_CORE_SOURCE' in os.environ: - try: - from pytest_cov.embed import init - init() - except Exception as exc: - sys.stderr.write( - "pytest-cov: Failed to setup subprocess coverage. " - "Environ: {0!r} " - "Exception: {1!r}\n".format( - dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')), - exc - ) - ) diff --git a/src/pytest-cov.pth b/src/pytest-cov.pth deleted file mode 100644 index 8ed1a516..00000000 --- a/src/pytest-cov.pth +++ /dev/null @@ -1 +0,0 @@ -import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 1c10f58d..62d553a4 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,6 +1,6 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '6.3.0' +__version__ = '7.0.0' import pytest diff --git a/src/pytest_cov/compat.py b/src/pytest_cov/compat.py deleted file mode 100644 index 453709d7..00000000 --- a/src/pytest_cov/compat.py +++ /dev/null @@ -1,15 +0,0 @@ -class SessionWrapper: - def __init__(self, session): - self._session = session - if hasattr(session, 'testsfailed'): - self._attr = 'testsfailed' - else: - self._attr = '_testsfailed' - - @property - def testsfailed(self): - return getattr(self._session, self._attr) - - @testsfailed.setter - def testsfailed(self, value): - setattr(self._session, self._attr, value) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py deleted file mode 100644 index 153cb83d..00000000 --- a/src/pytest_cov/embed.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Activate coverage at python startup if appropriate. - -The python site initialisation will ensure that anything we import -will be removed and not visible at the end of python startup. However -we minimise all work by putting these init actions in this separate -module and only importing what is needed when needed. - -For normal python startup when coverage should not be activated the pth -file checks a single env var and does not import or call the init fn -here. - -For python startup when an ancestor process has set the env indicating -that code coverage is being collected we activate coverage based on -info passed via env vars. -""" - -import atexit -import os -import signal - -_active_cov = None - - -def init(): - # Only continue if ancestor process has set everything needed in - # the env. - global _active_cov - - cov_source = os.environ.get('COV_CORE_SOURCE') - cov_config = os.environ.get('COV_CORE_CONFIG') - cov_datafile = os.environ.get('COV_CORE_DATAFILE') - cov_branch = True if os.environ.get('COV_CORE_BRANCH') == 'enabled' else None - cov_context = os.environ.get('COV_CORE_CONTEXT') - - if cov_datafile: - if _active_cov: - cleanup() - # Import what we need to activate coverage. - import coverage - - # Determine all source roots. - if cov_source in os.pathsep: - cov_source = None - else: - cov_source = cov_source.split(os.pathsep) - if cov_config == os.pathsep: - cov_config = True - - # Activate coverage for this process. - cov = _active_cov = coverage.Coverage( - source=cov_source, - branch=cov_branch, - data_suffix=True, - config_file=cov_config, - auto_data=True, - data_file=cov_datafile, - ) - cov.load() - cov.start() - if cov_context: - cov.switch_context(cov_context) - cov._warn_no_data = False - cov._warn_unimported_source = False - cov._warn_preimported_source = False - return cov - - -def _cleanup(cov): - if cov is not None: - cov.stop() - cov.save() - cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister - try: - atexit.unregister(cov._atexit) - except Exception: # noqa: S110 - pass - - -def cleanup(): - global _active_cov - global _cleanup_in_progress - global _pending_signal - - _cleanup_in_progress = True - _cleanup(_active_cov) - _active_cov = None - _cleanup_in_progress = False - if _pending_signal: - pending_signal = _pending_signal - _pending_signal = None - _signal_cleanup_handler(*pending_signal) - - -_previous_handlers = {} -_pending_signal = None -_cleanup_in_progress = False - - -def _signal_cleanup_handler(signum, frame): - global _pending_signal - if _cleanup_in_progress: - _pending_signal = signum, frame - return - cleanup() - _previous_handler = _previous_handlers.get(signum) - if _previous_handler == signal.SIG_IGN: - return - elif _previous_handler and _previous_handler is not _signal_cleanup_handler: - _previous_handler(signum, frame) - elif signum == signal.SIGTERM: - os._exit(128 + signum) - elif signum == signal.SIGINT: - raise KeyboardInterrupt - - -def cleanup_on_signal(signum): - previous = signal.getsignal(signum) - if previous is not _signal_cleanup_handler: - _previous_handlers[signum] = previous - signal.signal(signum, _signal_cleanup_handler) - - -def cleanup_on_sigterm(): - cleanup_on_signal(signal.SIGTERM) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 99ea6ddd..ca631272 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -10,7 +10,6 @@ import socket import sys import warnings -from io import StringIO from pathlib import Path from typing import Union @@ -20,7 +19,6 @@ from . import CentralCovContextWarning from . import DistCovError -from .embed import cleanup class BrokenCovConfigError(Exception): @@ -62,10 +60,6 @@ def ensure_topdir_wrapper(self, *args, **kwargs): return ensure_topdir_wrapper -def _data_suffix(name): - return f'{filename_suffix(True)}.{name}' - - class CovController: """Base class for different plugin implementations.""" @@ -100,12 +94,10 @@ def ensure_topdir(self): def pause(self): self.started = False self.cov.stop() - self.unset_env() @_ensure_topdir def resume(self): self.cov.start() - self.set_env() self.started = True def start(self): @@ -114,32 +106,6 @@ def start(self): def finish(self): self.started = False - @_ensure_topdir - def set_env(self): - """Put info about coverage into the env so that subprocesses can activate coverage.""" - if self.cov_source is None: - os.environ['COV_CORE_SOURCE'] = os.pathsep - else: - os.environ['COV_CORE_SOURCE'] = os.pathsep.join(self.cov_source) - config_file = Path(self.cov_config) - if config_file.exists(): - os.environ['COV_CORE_CONFIG'] = os.fspath(config_file.resolve()) - else: - os.environ['COV_CORE_CONFIG'] = os.pathsep - # this still uses the old abspath cause apparently Python 3.9 on Windows has a buggy Path.resolve() - os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) # noqa: PTH100 - if self.cov_branch: - os.environ['COV_CORE_BRANCH'] = 'enabled' - - @staticmethod - def unset_env(): - """Remove coverage info from env.""" - os.environ.pop('COV_CORE_SOURCE', None) - os.environ.pop('COV_CORE_CONFIG', None) - os.environ.pop('COV_CORE_DATAFILE', None) - os.environ.pop('COV_CORE_BRANCH', None) - os.environ.pop('COV_CORE_CONTEXT', None) - @staticmethod def get_node_desc(platform, version_info): """Return a description of this node.""" @@ -291,12 +257,10 @@ class Central(CovController): @_ensure_topdir def start(self): - cleanup() - self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('c'), + data_suffix=True, config_file=self.cov_config, ) if self.cov.config.dynamic_context == 'test_function': @@ -309,7 +273,7 @@ def start(self): self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('cc'), + data_suffix=f'{filename_suffix(True)}.combine', data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 config_file=self.cov_config, ) @@ -318,7 +282,6 @@ def start(self): if not self.cov_append: self.cov.erase() self.cov.start() - self.set_env() super().start() @@ -327,7 +290,6 @@ def finish(self): """Stop coverage, save data to file and set the list of coverage objects to report on.""" super().finish() - self.unset_env() self.cov.stop() self.cov.save() @@ -345,12 +307,10 @@ class DistMaster(CovController): @_ensure_topdir def start(self): - cleanup() - self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('m'), + data_suffix=True, config_file=self.cov_config, ) if self.cov.config.dynamic_context == 'test_function': @@ -365,7 +325,7 @@ def start(self): self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('mc'), + data_suffix=f'{filename_suffix(True)}.combine', data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 config_file=self.cov_config, ) @@ -405,18 +365,10 @@ def testnodedown(self, node, error): output['cov_worker_node_id'], ) - cov = coverage.Coverage(source=self.cov_source, branch=self.cov_branch, data_suffix=data_suffix, config_file=self.cov_config) - cov.start() - if coverage.version_info < (5, 0): - data = CoverageData() - data.read_fileobj(StringIO(output['cov_worker_data'])) - cov.data.update(data) - else: - data = CoverageData(no_disk=True, suffix='should-not-exist') - data.loads(output['cov_worker_data']) - cov.get_data().update(data) - cov.stop() - cov.save() + cov_data = CoverageData( + suffix=data_suffix, + ) + cov_data.loads(output['cov_worker_data']) path = output['cov_worker_path'] self.cov.config.paths['source'].append(path) @@ -443,15 +395,13 @@ class DistWorker(CovController): @_ensure_topdir def start(self): - cleanup() - # Determine whether we are collocated with master. self.is_collocated = ( socket.gethostname() == self.config.workerinput['cov_master_host'] and self.topdir == self.config.workerinput['cov_master_topdir'] ) - # If we are not collocated then rewrite master paths to worker paths. + # If we are not collocated, then rewrite master paths to worker paths. if not self.is_collocated: master_topdir = self.config.workerinput['cov_master_topdir'] worker_topdir = self.topdir @@ -463,13 +413,13 @@ def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix(f'w{self.nodeid}'), + data_suffix=True, config_file=self.cov_config, ) # Prevent workers from issuing module-not-measured type of warnings (expected for a workers to not have coverage in all the files). self.cov._warn_unimported_source = False self.cov.start() - self.set_env() + super().start() @_ensure_topdir @@ -477,7 +427,6 @@ def finish(self): """Stop coverage and send relevant info back to the master.""" super().finish() - self.unset_env() self.cov.stop() if self.is_collocated: @@ -497,12 +446,7 @@ def finish(self): # it on the master node. # Send all the data to the master over the channel. - if coverage.version_info < (5, 0): - buff = StringIO() - self.cov.data.write_fileobj(buff) - data = buff.getvalue() - else: - data = self.cov.get_data().dumps() + data = self.cov.get_data().dumps() self.config.workeroutput.update( { diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index c49a655d..553a9203 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -8,17 +8,11 @@ from pathlib import Path from typing import TYPE_CHECKING -import coverage import pytest -from coverage.exceptions import CoverageWarning -from coverage.results import display_covered -from coverage.results import should_fail_under from . import CovDisabledWarning from . import CovReportWarning from . import PytestCovWarning -from . import compat -from . import embed if TYPE_CHECKING: from .engine import CovController @@ -37,9 +31,6 @@ def validate_report(arg): msg = f'invalid choice: "{arg}" (choose from "{all_choices}")' raise argparse.ArgumentTypeError(msg) - if report_type == 'lcov' and coverage.version_info <= (6, 3): - raise argparse.ArgumentTypeError('LCOV output is only supported with coverage.py >= 6.3') - if len(values) == 1: return report_type, None @@ -70,8 +61,6 @@ def validate_fail_under(num_str): def validate_context(arg): - if coverage.version_info <= (5, 0): - raise argparse.ArgumentTypeError('Contexts are only supported with coverage.py >= 5.x') if arg != 'test': raise argparse.ArgumentTypeError('The only supported value is "test".') return arg @@ -345,6 +334,8 @@ def pytest_runtestloop(self, session): break else: warnings.simplefilter('once', PytestCovWarning) + from coverage.exceptions import CoverageWarning + for _, _, category, _, _ in warnings.filters: if category is CoverageWarning: break @@ -353,9 +344,7 @@ def pytest_runtestloop(self, session): result = yield - compat_session = compat.SessionWrapper(session) - - self.failed = bool(compat_session.testsfailed) + self.failed = bool(session.testsfailed) if self.cov_controller is not None: self.cov_controller.finish() @@ -363,6 +352,8 @@ def pytest_runtestloop(self, session): # import coverage lazily here to avoid importing # it for unit tests that don't need it from coverage.misc import CoverageException + from coverage.results import display_covered + from coverage.results import should_fail_under try: self.cov_total = self.cov_controller.summary(self.cov_report) @@ -384,7 +375,7 @@ def pytest_runtestloop(self, session): ) session.config.pluginmanager.getplugin('terminalreporter').write(f'\nERROR: {message}\n', red=True, bold=True) # make sure we get the EXIT_TESTSFAILED exit code - compat_session.testsfailed += 1 + session.testsfailed += 1 return result @@ -426,15 +417,6 @@ def pytest_terminal_summary(self, terminalreporter): ) terminalreporter.write(message, **markup) - def pytest_runtest_setup(self, item): - if os.getpid() != self.pid: - # test is run in another process than session, run - # coverage manually - embed.init() - - def pytest_runtest_teardown(self, item): - embed.cleanup() - @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): if item.get_closest_marker('no_cover') or 'no_cover' in getattr(item, 'fixturenames', ()): @@ -462,9 +444,7 @@ def pytest_runtest_call(self, item): def switch_context(self, item, when): if self.cov_controller.started: - context = f'{item.nodeid}|{when}' - self.cov_controller.cov.switch_context(context) - os.environ['COV_CORE_CONTEXT'] = context + self.cov_controller.cov.switch_context(f'{item.nodeid}|{when}') @pytest.fixture diff --git a/tests/contextful.py b/tests/contextful.py index b1d0804b..6e57a601 100644 --- a/tests/contextful.py +++ b/tests/contextful.py @@ -58,7 +58,7 @@ def test_06(some_data, more_data): assert len(some_data) == len(more_data) # r6 -@pytest.fixture(scope='session') +@pytest.fixture def expensive_data(): return list(range(10)) # s7 diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index c89dbac6..6ca09fe5 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1,28 +1,21 @@ -# ruff: noqa import collections import glob import os import platform import re -import subprocess import sys -from io import StringIO from itertools import chain +from pathlib import Path +from types import SimpleNamespace import coverage -import py import pytest -import virtualenv -import xdist -from fields import Namespace from process_tests import TestProcess as _TestProcess from process_tests import dump_on_error from process_tests import wait_for_strings import pytest_cov.plugin -coverage, platform # required for skipif mark on test_cov_min_from_coveragerc - max_worker_restart_0 = '--max-worker-restart=0' SCRIPT = """ @@ -168,7 +161,7 @@ def test_foo(cov): def adjust_sys_path(): """Adjust PYTHONPATH during tests to make "helper" importable in SCRIPT.""" orig_path = os.environ.get('PYTHONPATH', None) - new_path = os.path.dirname(__file__) + new_path = str(Path(__file__).parent) if orig_path is not None: new_path = os.pathsep.join([new_path, orig_path]) os.environ['PYTHONPATH'] = new_path @@ -191,7 +184,7 @@ def adjust_sys_path(): ids=['branch2x', 'branch1c', 'branch1a', 'nobranch'], ) def prop(request): - return Namespace( + return SimpleNamespace( code=SCRIPT, code2=SCRIPT2, conf=request.param[0], @@ -299,9 +292,8 @@ def test_term_report_does_not_interact_with_html_output(testdir): dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) expected = [dest_dir.join('index.html'), dest_dir.join('test_funcarg_py.html')] - if coverage.version_info >= (7, 5): - expected.insert(0, dest_dir.join('function_index.html')) - expected.insert(0, dest_dir.join('class_index.html')) + expected.insert(0, dest_dir.join('function_index.html')) + expected.insert(0, dest_dir.join('class_index.html')) assert sorted(dest_dir.visit('**/*.html')) == expected assert dest_dir.join('index.html').check() assert result.ret == 0 @@ -349,7 +341,7 @@ def test_xml_output_dir(testdir): def test_json_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=json:' + JSON_REPORT_NAME, script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=json:' + JSON_REPORT_NAME, script) result.stdout.fnmatch_lines( [ @@ -365,7 +357,7 @@ def test_json_output_dir(testdir): def test_markdown_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script) result.stdout.fnmatch_lines( [ @@ -381,7 +373,7 @@ def test_markdown_output_dir(testdir): def test_markdown_append_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script) result.stdout.fnmatch_lines( [ @@ -399,7 +391,7 @@ def test_markdown_and_markdown_append_work_together(testdir): result = testdir.runpytest( '-v', - '--cov=%s' % script.dirpath(), + f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script, @@ -422,7 +414,7 @@ def test_markdown_and_markdown_append_pointing_to_same_file_throws_error(testdir result = testdir.runpytest( '-v', - '--cov=%s' % script.dirpath(), + f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, '--cov-report=markdown-append:' + MARKDOWN_REPORT_NAME, script, @@ -432,7 +424,6 @@ def test_markdown_and_markdown_append_pointing_to_same_file_throws_error(testdir assert result.ret == 4 -@pytest.mark.skipif('coverage.version_info < (6, 3)') def test_lcov_output_dir(testdir): script = testdir.makepyfile(SCRIPT) @@ -449,23 +440,6 @@ def test_lcov_output_dir(testdir): assert result.ret == 0 -@pytest.mark.skipif('coverage.version_info >= (6, 3)') -def test_lcov_not_supported(testdir): - script = testdir.makepyfile('a = 1') - result = testdir.runpytest( - '-v', - f'--cov={script.dirpath()}', - '--cov-report=lcov', - script, - ) - result.stderr.fnmatch_lines( - [ - '*argument --cov-report: LCOV output is only supported with coverage.py >= 6.3', - ] - ) - assert result.ret != 0 - - def test_term_output_dir(testdir): script = testdir.makepyfile(SCRIPT) @@ -486,7 +460,7 @@ def test_term_missing_output_dir(testdir): result.stderr.fnmatch_lines( [ - '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR, + f'*argument --cov-report: output specifier not supported for: "term-missing:{DEST_DIR}"*', ] ) assert result.ret != 0 @@ -652,7 +626,6 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): aliased [coverage:run] source = mod -parallel = true {prop.conf} """ ) @@ -713,6 +686,75 @@ def test_foobar(bad): assert result.ret == 0 +@pytest.mark.skipif(sys.platform == 'win32', reason='No redis server on Windows') +@pytest.mark.skipif(sys.platform == 'darwin', reason='No redis server on OSX') +def test_celery(pytester): + pytester.makepyfile( + small_celery=""" +import os + +from celery import Celery +from celery.contrib.testing import worker +from testcontainers.redis import RedisContainer +import pytest + +app = Celery("tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0") + +@app.task +def add(x, y): + return x + y + +@pytest.fixture(scope="session") +def redis_container(): + with RedisContainer() as container: + yield container + + +@pytest.fixture +def celery_app(redis_container): + host = redis_container.get_container_host_ip() + port = redis_container.get_exposed_port(6379) + redis_url = f"redis://{host}:{port}/0" + + app.conf.update(broker_url=redis_url, result_backend=redis_url) + return app + +@pytest.fixture +def celery_worker(celery_app): + with worker.start_worker( + celery_app, + pool="prefork", + perform_ping_check=False, + ): + yield + print('CELERY SHUTDOWN') + print('CELERY SHUTDOWN DONE') + print(os.listdir()) + + +def test_add_task(celery_worker): + result = add.delay(4, 4) + assert result.get() == 8 +""" + ) + + pytester.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess", "_exit"] +""" + ) + result = pytester.runpytest('-vv', '-s', '--cov', '--cov-report=term-missing', 'small_celery.py') + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'small_celery* 100%*', + ] + ) + assert result.ret == 0 + + def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): src = testdir.mkdir('src') src.join('parent_script.py').write(SCRIPT_PARENT) @@ -732,7 +774,7 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): source = parent_script child_script -parallel = true +patch = subprocess """ ) @@ -948,6 +990,12 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): def test_central_subprocess(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +""" + ) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -971,7 +1019,7 @@ def test_central_subprocess_change_cwd(testdir): coveragerc=""" [run] branch = true -parallel = true +patch = subprocess """, ) @@ -998,7 +1046,7 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey '', coveragerc=""" [run] -parallel = true +patch = subprocess """, ) @@ -1029,7 +1077,7 @@ def test_foo(): '', coveragerc=""" [run] -parallel = true +patch = subprocess """, ) result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) @@ -1044,6 +1092,12 @@ def test_foo(): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_collocated(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +""" + ) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -1071,6 +1125,9 @@ def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): dir2 = tmpdir.mkdir('dir2') testdir.tmpdir.join('.coveragerc').write( f""" +[run] +patch = subprocess + [paths] source = {scripts.dirpath()} @@ -1124,43 +1181,6 @@ def test_invalid_coverage_source(testdir): assert not matching_lines -@pytest.mark.skipif("'dev' in pytest.__version__") -@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -@pytest.mark.skipif( - 'tuple(map(int, xdist.__version__.split("."))) >= (2, 3, 0)', - reason='Since pytest-xdist 2.3.0 the parent sys.path is copied in the child process', -) -def test_dist_missing_data(testdir): - """Test failure when using a worker without pytest-cov installed.""" - venv_path = os.path.join(str(testdir.tmpdir), 'venv') - virtualenv.cli_run([venv_path]) - if sys.platform == 'win32': - if platform.python_implementation() == 'PyPy': - exe = os.path.join(venv_path, 'bin', 'python.exe') - else: - exe = os.path.join(venv_path, 'Scripts', 'python.exe') - else: - exe = os.path.join(venv_path, 'bin', 'python') - subprocess.check_call( - [exe, '-mpip', 'install', f'py=={py.__version__}', f'pytest=={pytest.__version__}', f'pytest_xdist=={xdist.__version__}'] - ) - script = testdir.makepyfile(SCRIPT) - - result = testdir.runpytest( - '-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//python={exe}', - max_worker_restart_0, - str(script), - ) - result.stdout.fnmatch_lines( - ['The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.'] - ) - - def test_funcarg(testdir): script = testdir.makepyfile(SCRIPT_FUNCARG) @@ -1179,9 +1199,15 @@ def test_funcarg_not_active(testdir): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') +@pytest.mark.skipif(sys.platform == 'win32', reason="SIGTERM isn't really supported on Windows") def test_cleanup_on_sigterm(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess", "_exit"] +""" + ) + script = testdir.makepyfile( ''' import os, signal, subprocess, sys, time @@ -1204,9 +1230,6 @@ def test_run(): if __name__ == "__main__": signal.signal(signal.SIGTERM, cleanup) - from pytest_cov.embed import cleanup_on_sigterm - cleanup_on_sigterm() - try: time.sleep(10) except BaseException as exc: @@ -1216,25 +1239,43 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'test_cleanup_on_sigterm* 100%', + '*1 passed*', + ] + ) assert result.ret == 0 -@pytest.mark.skipif('sys.platform != "win32"') +@pytest.mark.skipif(sys.platform != 'win32', reason='SIGBREAK is Windows only') @pytest.mark.parametrize( 'setup', [ - ('signal.signal(signal.SIGBREAK, signal.SIG_DFL); cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), - ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), - ('cleanup()', '73% 19-22'), + ( + 'signal.signal(signal.SIGBREAK, signal.SIG_DFL)', + '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '62% 4, 23-28', + ), + ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), + ('', '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '67% 4, 25-28'), ], ) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +""" + ) script = testdir.makepyfile( """ import os, signal, subprocess, sys, time +def cleanup(num, frame): + raise Exception() + def test_run(): proc = subprocess.Popen( [sys.executable, __file__], @@ -1245,10 +1286,13 @@ def test_run(): proc.send_signal(signal.CTRL_BREAK_EVENT) stdout, stderr = proc.communicate() assert not stderr - assert stdout in [b"^C", b"", b"captured IOError(4, 'Interrupted function call')\\n"] + assert stdout in [ + b"^C", + b"", + b"captured Exception()\\r\\n", + b"captured IOError(4, 'Interrupted function call')\\n"] if __name__ == "__main__": - from pytest_cov.embed import cleanup_on_signal, cleanup """ + setup[0] + """ @@ -1267,17 +1311,14 @@ def test_run(): @pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") -@pytest.mark.xfail('sys.platform == "darwin"', reason='Something weird going on Macs...') -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') -@pytest.mark.parametrize( - 'setup', - [ - ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), - ('cleanup_on_sigterm()', '88% 18-19'), - ('cleanup()', '75% 16-19'), - ], -) -def test_cleanup_on_sigterm_sig_dfl(pytester, testdir, setup): +def test_cleanup_on_sigterm_sig_dfl(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +sigterm = true +""" + ) script = testdir.makepyfile( """ import os, signal, subprocess, sys, time @@ -1288,15 +1329,13 @@ def test_run(): proc.terminate() stdout, stderr = proc.communicate() assert not stderr + print([stdout, stderr]) assert stdout == b"" + assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] if __name__ == "__main__": - from pytest_cov.embed import cleanup_on_sigterm, cleanup - """ - + setup[0] - + """ - + foobar = 123 try: time.sleep(10) except BaseException as exc: @@ -1304,16 +1343,23 @@ def test_run(): """ ) - result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + result = testdir.runpytest( + '-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-report=html', script + ) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32"', reason='SIGINT is subtly broken on Windows') -@pytest.mark.xfail('sys.platform == "darwin"', reason='Something weird going on Macs...') -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +sigterm = true +""" + ) script = testdir.makepyfile( ''' import os, signal, subprocess, sys, time @@ -1329,9 +1375,6 @@ def test_run(): assert proc.returncode == 0 if __name__ == "__main__": - from pytest_cov.embed import cleanup_on_signal - cleanup_on_signal(signal.SIGINT) - try: time.sleep(10) except BaseException as exc: @@ -1341,44 +1384,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason='fork not available on Windows') -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') -def test_cleanup_on_sigterm_sig_ign(testdir): - script = testdir.makepyfile( - """ -import os, signal, subprocess, sys, time - -def test_run(): - proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - time.sleep(1) - proc.send_signal(signal.SIGINT) - time.sleep(1) - proc.terminate() - stdout, stderr = proc.communicate() - assert not stderr - assert stdout == b"" - assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] - -if __name__ == "__main__": - signal.signal(signal.SIGINT, signal.SIG_IGN) - - from pytest_cov.embed import cleanup_on_signal - cleanup_on_signal(signal.SIGINT) - - try: - time.sleep(10) - except BaseException as exc: - print("captured %r" % exc) - """ - ) - - result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 100%', '*1 passed*']) assert result.ret == 0 @@ -1624,17 +1630,6 @@ def test_foo(): SCRIPT_SIMPLE_RESULT = '4 * 100%' -@pytest.mark.skipif('tuple(map(int, xdist.__version__.split("."))) >= (3, 0, 2)', reason='--boxed option was removed in version 3.0.2') -@pytest.mark.skipif('sys.platform == "win32"') -def test_dist_boxed(testdir): - script = testdir.makepyfile(SCRIPT_SIMPLE) - - result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) - assert result.ret == 0 - - @pytest.mark.skipif('sys.platform == "win32"') @pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason='strange optimization on PyPy3') def test_dist_bare_cov(testdir): @@ -1648,7 +1643,7 @@ def test_dist_bare_cov(testdir): def test_not_started_plugin_does_not_fail(testdir): class ns: - cov_source = [True] + cov_source = (True,) cov_report = '' plugin = pytest_cov.plugin.CovPlugin(ns, None, start=False) @@ -1696,14 +1691,13 @@ def test_external_data_file(testdir): testdir.tmpdir.join('.coveragerc').write( """ [run] -data_file = %s -""" - % testdir.tmpdir.join('some/special/place/coverage-data').ensure() +data_file = {} +""".format(testdir.tmpdir.join('some/special/place/coverage-data').ensure()) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 - assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) + assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1713,14 +1707,13 @@ def test_external_data_file_xdist(testdir): """ [run] parallel = true -data_file = %s -""" - % testdir.tmpdir.join('some/special/place/coverage-data').ensure() +data_file = {} +""".format(testdir.tmpdir.join('some/special/place/coverage-data').ensure()) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '-n', '1', max_worker_restart_0, script) assert result.ret == 0 - assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) + assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1747,7 +1740,7 @@ def test_external_data_file_negative(testdir): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 - assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) + assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) # noqa: PTH207 @xdist_params @@ -1804,7 +1797,6 @@ def test_dynamic_context(pytester, testdir, opts, prop): testdir.makepyprojecttoml(f""" [tool.coverage.run] dynamic_context = "test_function" -parallel = true {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) @@ -1824,7 +1816,6 @@ def test_simple(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.makepyprojecttoml(f""" [tool.coverage.run] -parallel = true {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) @@ -1857,6 +1848,10 @@ def test_do_not_append_coverage(pytester, testdir, opts, prop): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_append_coverage_subprocess(testdir): + testdir.makepyprojecttoml(""" +[tool.coverage.run] +patch = ["subprocess"] +""") scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -1881,28 +1876,6 @@ def test_append_coverage_subprocess(testdir): assert result.ret == 0 -def test_pth_failure(monkeypatch): - with open('src/pytest-cov.pth') as fh: - payload = fh.read() - - class SpecificError(Exception): - pass - - def bad_init(): - raise SpecificError - - buff = StringIO() - - from pytest_cov import embed - - monkeypatch.setattr(embed, 'init', bad_init) - monkeypatch.setattr(sys, 'stderr', buff) - monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') - exec(payload) - expected = "pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" - assert buff.getvalue() == expected - - def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) @@ -1968,6 +1941,7 @@ def find_labels(text, pattern): 'test_contexts.py::test_07|setup': 's7', 'test_contexts.py::test_07|run': 'r7', 'test_contexts.py::test_08|run': 'r8', + 'test_contexts.py::test_08|setup': 's7', 'test_contexts.py::test_09[1]|setup': 's9-1', 'test_contexts.py::test_09[1]|run': 'r9-1', 'test_contexts.py::test_09[2]|setup': 's9-2', @@ -1986,11 +1960,9 @@ def find_labels(text, pattern): } -@pytest.mark.skipif('coverage.version_info < (5, 0)') -@pytest.mark.skipif('coverage.version_info > (6, 4)') @xdist_params def test_contexts(pytester, testdir, opts): - with open(os.path.join(os.path.dirname(__file__), 'contextful.py')) as f: + with Path(__file__).parent.joinpath('contextful.py').open() as f: contextful_tests = f.read() script = testdir.makepyfile(contextful_tests) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-context=test', script, *opts.split()) @@ -2007,7 +1979,7 @@ def test_contexts(pytester, testdir, opts): measured = data.measured_files() assert len(measured) == 1 test_context_path = next(iter(measured)) - assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() + assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() # noqa: PTH100 line_data = find_labels(contextful_tests, r'[crst]\d+(?:-\d+)?') for context, label in EXPECTED_CONTEXTS.items(): @@ -2018,23 +1990,6 @@ def test_contexts(pytester, testdir, opts): assert line_data[label] == actual, f'Wrong lines for context {context!r}' -@pytest.mark.skipif('coverage.version_info >= (5, 0)') -def test_contexts_not_supported(testdir): - script = testdir.makepyfile('a = 1') - result = testdir.runpytest( - '-v', - f'--cov={script.dirpath()}', - '--cov-context=test', - script, - ) - result.stderr.fnmatch_lines( - [ - '*argument --cov-context: Contexts are only supported with coverage.py >= 5.x', - ] - ) - assert result.ret != 0 - - def test_contexts_no_cover(testdir): script = testdir.makepyfile(""" import pytest diff --git a/tox.ini b/tox.ini index 171e7b66..be756012 100644 --- a/tox.ini +++ b/tox.ini @@ -10,11 +10,12 @@ passenv = ; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] +isolated_build = true envlist = clean, check, docs, - {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest83,pytest84}-{xdist36,xdist37}-{coverage78}, + {py39,py310,py311,py312,py313,pypy39,pypy310,pypy311}-{pytest84}-{xdist38}-{coverage710}, report ignore_basepython_conflict = true @@ -41,7 +42,7 @@ setenv = pytest81: _DEP_PYTEST=pytest==8.1.1 pytest82: _DEP_PYTEST=pytest==8.2.2 pytest83: _DEP_PYTEST=pytest==8.3.5 - pytest84: _DEP_PYTEST=pytest==8.4.0 + pytest84: _DEP_PYTEST=pytest==8.4.2 xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 @@ -49,6 +50,7 @@ setenv = xdist35: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 xdist36: _DEP_PYTESTXDIST=pytest-xdist==3.6.1 xdist37: _DEP_PYTESTXDIST=pytest-xdist==3.7.0 + xdist38: _DEP_PYTESTXDIST=pytest-xdist==3.8.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist coverage72: _DEP_COVERAGE=coverage==7.2.7 @@ -58,6 +60,8 @@ setenv = coverage76: _DEP_COVERAGE=coverage==7.6.12 coverage77: _DEP_COVERAGE=coverage==7.7.1 coverage78: _DEP_COVERAGE=coverage==7.8.2 + coverage79: _DEP_COVERAGE=coverage==7.9.2 + coverage710: _DEP_COVERAGE=coverage==7.10.6 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = @@ -66,6 +70,8 @@ deps = {env:_DEP_PYTEST:pytest} {env:_DEP_PYTESTXDIST:pytest-xdist} {env:_DEP_COVERAGE:coverage} + celery[redis] + testcontainers[redis] pip_pre = true commands = {posargs:pytest -vv} @@ -73,16 +79,14 @@ commands = [testenv:check] deps = docutils - check-manifest - pre-commit - readme-renderer - pygments - isort + twine + uv + prek skip_install = true commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest . - pre-commit run --all-files --show-diff-on-failure + uv build --sdist --wheel + twine check --strict dist/* + prek run --all-files --show-diff-on-failure [testenv:docs] usedevelop = true