diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b23915d..0000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -ignore = E203, E231, E501, E722, W503, B950 -select = C,E,F,W,B,B9,I diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d7fc2a1..d194fab 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,30 +1,41 @@ name: deploy on: + workflow_dispatch: + pull_request: push: + branches: + - main tags: - v* jobs: - deploy: + dist: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: hynek/build-and-inspect-python-package@v2 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' + deploy: + needs: [dist] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + id-token: write + attestations: write - - name: Install dependencies for build - run: pip install --upgrade setuptools build + steps: + - uses: actions/download-artifact@v4 + with: + name: Packages + path: dist - - name: Build - run: python -m build + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@v2 + with: + subject-path: "dist/*" - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 3cdd8c0..3886257 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -11,8 +11,8 @@ jobs: name: Pre-commit checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ba83a7..b0fbd82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,74 +7,70 @@ on: - main pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: "1" + jobs: test: strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] python-version: - - '3.7' - '3.8' - '3.9' - '3.10' - include: - - os: ubuntu-22.04 - python-version: '3.7' - - os: windows-latest - python-version: '3.7' - + - '3.11' + - '3.12' + - '3.13' runs-on: ${{ matrix.os }} name: ${{ matrix.os }}, Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install test dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions + - uses: astral-sh/setup-uv@v5 - - name: Test packaging - run: tox -e pkg + - name: Install tox + run: uv tool install --with tox-gh-actions --with tox-uv tox - - name: Run tests with PyTest 4 + - name: Run tests with PyTest 8 run: tox - if: ${{ matrix.python-version != '3.10' }} env: - PLATFORM: ${{ matrix.os }} - PYTEST_MAJOR_VERSION: 4 + PYTEST_MAJOR_VERSION: 8 PYTEST_PLUGINS: pytest_github_actions_annotate_failures - - name: Run tests with PyTest 5 + - name: Run tests with PyTest 7 run: tox - if: ${{ matrix.python-version != '3.10' }} + if: runner.os != 'Windows' env: - PLATFORM: ${{ matrix.os }} - PYTEST_MAJOR_VERSION: 5 + PYTEST_MAJOR_VERSION: 7 PYTEST_PLUGINS: pytest_github_actions_annotate_failures - name: Run tests with PyTest 6 run: tox + if: runner.os != 'Windows' env: - PLATFORM: ${{ matrix.os }} PYTEST_MAJOR_VERSION: 6 PYTEST_PLUGINS: pytest_github_actions_annotate_failures - - name: Run tests with PyTest 7 - run: tox - env: - PLATFORM: ${{ matrix.os }} - PYTEST_MAJOR_VERSION: 7 - PYTEST_PLUGINS: pytest_github_actions_annotate_failures - post-test: name: All tests passed + if: always() + needs: [test] runs-on: ubuntu-latest - needs: test + timeout-minutes: 2 steps: - - run: echo ok + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df86f85..eb8015c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.1.0 + rev: "v5.0.0" hooks: - id: check-added-large-files - id: check-case-conflict @@ -12,17 +12,10 @@ repos: - id: mixed-line-ending - id: requirements-txt-fixer - id: trailing-whitespace - - id: fix-encoding-pragma -- repo: https://github.com/mgedmin/check-manifest - rev: "0.42" +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.9.2" hooks: - - id: check-manifest - -- repo: https://github.com/PyCQA/flake8 - rev: 3.8.3 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear - - flake8-import-order + - id: ruff + args: ["--fix", "--show-fixes"] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac4394..187567a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## Unreleased + +Nothing yet. + +## 0.3.0 (2025-01-17) + +- Test on Python 3.13 #89 +- Support pytest 7.4+ #97 (thanks to @edgarrmondragon) +- Require Python 3.8+ #87 (thanks to @edgarrmondragon) +- Require pytest 6+ #86 (thanks to @edgarrmondragon) +- Speed up CI and testing #93 +- Use Ruff formatter #96 +- Use dependency-groups for tests #99 +- Add GitHub Attestations #100 + ## 0.2.0 (2023-05-04) ### Incompatible changes diff --git a/MANIFEST.in b/MANIFEST.in index d07d8e6..ee937c8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include *.py include *.txt include *.yaml -include .flake8 include .pre-commit-config.yaml include CHANGELOG.md include LICENSE diff --git a/README.md b/README.md index ae74bd8..e60d585 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 - name: Install dependencies run: | @@ -36,5 +36,9 @@ If your test is running in a Docker container, you have to install this plugin a If your tests are run from a subdirectory of the git repository, you have to set the `PYTEST_RUN_PATH` environment variable to the path of that directory relative to the repository root in order for GitHub to identify the files with errors correctly. +### Warning annotations + +This plugin also supports warning annotations when used with Pytest 6.0+. To disable warning annotations, pass `--exclude-warning-annotations` to pytest. + ## Screenshot [![Image from Gyazo](https://i.gyazo.com/b578304465dd1b755ceb0e04692a57d9.png)](https://gyazo.com/b578304465dd1b755ceb0e04692a57d9) diff --git a/plugin_test.py b/plugin_test.py index 7e489ff..1b32b6c 100644 --- a/plugin_test.py +++ b/plugin_test.py @@ -1,24 +1,21 @@ -# -*- coding: utf-8 -*- -import os +from __future__ import annotations -from packaging import version +import os import pytest +from packaging import version - +PYTEST_VERSION = version.parse(pytest.__version__) pytest_plugins = "pytester" -# result.stderr.no_fnmatch_line() is added to testdir on pytest 5.3.0 +# result.stderr.no_fnmatch_line() was added to testdir on pytest 5.3.0 # https://docs.pytest.org/en/stable/changelog.html#pytest-5-3-0-2019-11-19 -def no_fnmatch_line(result, pattern): - if version.parse(pytest.__version__) >= version.parse("5.3.0"): - result.stderr.no_fnmatch_line(pattern + "*",) - else: - assert pattern not in result.stderr.str() +def no_fnmatch_line(result: pytest.RunResult, pattern: str): + result.stderr.no_fnmatch_line(pattern + "*") -def test_annotation_succeed_no_output(testdir): +def test_annotation_succeed_no_output(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -34,7 +31,7 @@ def test_success(): no_fnmatch_line(result, "::error file=test_annotation_succeed_no_output.py") -def test_annotation_pytest_error(testdir): +def test_annotation_pytest_error(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -52,11 +49,13 @@ def test_error(): result = testdir.runpytest_subprocess() result.stderr.re_match_lines( - [r"::error file=test_annotation_pytest_error\.py,line=8::test_error.*",] + [ + r"::error file=test_annotation_pytest_error\.py,line=8::test_error.*", + ] ) -def test_annotation_fail(testdir): +def test_annotation_fail(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -69,11 +68,13 @@ def test_fail(): testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") result = testdir.runpytest_subprocess() result.stderr.fnmatch_lines( - ["::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",] + [ + "::error file=test_annotation_fail.py,line=5::test_fail*assert 0*", + ] ) -def test_annotation_exception(testdir): +def test_annotation_exception(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -87,11 +88,51 @@ def test_fail(): testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") result = testdir.runpytest_subprocess() result.stderr.fnmatch_lines( - ["::error file=test_annotation_exception.py,line=5::test_fail*oops*",] + [ + "::error file=test_annotation_exception.py,line=5::test_fail*oops*", + ] + ) + + +def test_annotation_warning(testdir: pytest.Testdir): + testdir.makepyfile( + """ + import warnings + import pytest + pytest_plugins = 'pytest_github_actions_annotate_failures' + + def test_warning(): + warnings.warn('beware', Warning) + assert 1 + """ + ) + testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = testdir.runpytest_subprocess() + result.stderr.fnmatch_lines( + [ + "::warning file=test_annotation_warning.py,line=6::beware", + ] + ) + + +def test_annotation_exclude_warnings(testdir: pytest.Testdir): + testdir.makepyfile( + """ + import warnings + import pytest + pytest_plugins = 'pytest_github_actions_annotate_failures' + + def test_warning(): + warnings.warn('beware', Warning) + assert 1 + """ ) + testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = testdir.runpytest_subprocess("--exclude-warning-annotations") + assert not result.stderr.lines -def test_annotation_third_party_exception(testdir): +def test_annotation_third_party_exception(testdir: pytest.Testdir): testdir.makepyfile( my_module=""" def fn(): @@ -112,11 +153,43 @@ def test_fail(): testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") result = testdir.runpytest_subprocess() result.stderr.fnmatch_lines( - ["::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",] + [ + "::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*", + ] + ) + + +def test_annotation_third_party_warning(testdir: pytest.Testdir): + testdir.makepyfile( + my_module=""" + import warnings + + def fn(): + warnings.warn('beware', Warning) + """ + ) + + testdir.makepyfile( + """ + import pytest + from my_module import fn + pytest_plugins = 'pytest_github_actions_annotate_failures' + + def test_warning(): + fn() + """ + ) + testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = testdir.runpytest_subprocess() + result.stderr.fnmatch_lines( + # ["::warning file=test_annotation_third_party_warning.py,line=6::beware",] + [ + "::warning file=my_module.py,line=4::beware", + ] ) -def test_annotation_fail_disabled_outside_workflow(testdir): +def test_annotation_fail_disabled_outside_workflow(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -133,7 +206,7 @@ def test_fail(): ) -def test_annotation_fail_cwd(testdir): +def test_annotation_fail_cwd(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -149,11 +222,13 @@ def test_fail(): testdir.makefile(".ini", pytest="[pytest]\ntestpaths=..") result = testdir.runpytest_subprocess("--rootdir=foo") result.stderr.fnmatch_lines( - ["::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",] + [ + "::error file=test_annotation_fail_cwd0/test_annotation_fail_cwd.py,line=5::test_fail*assert 0*", + ] ) -def test_annotation_fail_runpath(testdir): +def test_annotation_fail_runpath(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -167,11 +242,13 @@ def test_fail(): testdir.monkeypatch.setenv("PYTEST_RUN_PATH", "some_path") result = testdir.runpytest_subprocess() result.stderr.fnmatch_lines( - ["::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",] + [ + "::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*", + ] ) -def test_annotation_long(testdir): +def test_annotation_long(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -203,7 +280,7 @@ def test_fail(): no_fnmatch_line(result, "::*assert x += 1*") -def test_class_method(testdir): +def test_class_method(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest @@ -225,7 +302,7 @@ def test_method(self): no_fnmatch_line(result, "::*x = 1*") -def test_annotation_param(testdir): +def test_annotation_param(testdir: pytest.Testdir): testdir.makepyfile( """ import pytest diff --git a/pyproject.toml b/pyproject.toml index 69c327f..291c086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,8 @@ build-backend = "setuptools.build_meta" [project] # https://peps.python.org/pep-0621/#readme -requires-python = ">=3.7" -version = "0.2.0" +requires-python = ">=3.8" +version = "0.3.0" name = "pytest-github-actions-annotate-failures" description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions" readme = "README.md" @@ -25,11 +25,12 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Framework :: Pytest", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "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 :: 3 :: Only", "Programming Language :: Python", "Topic :: System :: Systems Administration", @@ -39,7 +40,7 @@ classifiers = [ ] keywords = ["ansible", "testing", "molecule", "plugin"] dependencies = [ - "pytest>=4.0.0" + "pytest>=6.0.0" ] [project.urls] @@ -49,3 +50,37 @@ changelog = "https://github.com/pytest-dev/pytest-github-actions-annotate-failur [project.entry-points.pytest11] pytest_github_actions_annotate_failures = "pytest_github_actions_annotate_failures.plugin" + +[dependency-groups] +dev = [{ include-group = "test"}] +test = ["packaging"] + + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "I", # isort + "ARG", # flake8-unused-arguments + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "UP", # pyupgrade + "YTT", # flake8-2020 + "EXE", # flake8-executable +] +ignore = [ + "PLR", # Design related pylint codes +] +isort.required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["T20"] diff --git a/pytest_github_actions_annotate_failures/plugin.py b/pytest_github_actions_annotate_failures/plugin.py index de61ee9..3f58b95 100644 --- a/pytest_github_actions_annotate_failures/plugin.py +++ b/pytest_github_actions_annotate_failures/plugin.py @@ -1,15 +1,13 @@ -# -*- coding: utf-8 -*- - from __future__ import annotations +import contextlib import os import sys -from collections import OrderedDict from typing import TYPE_CHECKING -from _pytest._code.code import ExceptionRepr - import pytest +from _pytest._code.code import ExceptionRepr, ReprEntry +from packaging import version if TYPE_CHECKING: from _pytest.nodes import Item @@ -25,8 +23,11 @@ # https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py +PYTEST_VERSION = version.parse(pytest.__version__) + + @pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item: Item, call): +def pytest_runtest_makereport(item: Item, call): # noqa: ARG001 # execute all other hooks to obtain the report object outcome = yield report: CollectReport = outcome.get_result() @@ -37,32 +38,12 @@ def pytest_runtest_makereport(item: Item, call): return if report.when == "call" and report.failed: - # collect information to be annotated filesystempath, lineno, _ = report.location - runpath = os.environ.get("PYTEST_RUN_PATH") - if runpath: - filesystempath = os.path.join(runpath, filesystempath) - - # try to convert to absolute path in GitHub Actions - workspace = os.environ.get("GITHUB_WORKSPACE") - if workspace: - full_path = os.path.abspath(filesystempath) - try: - rel_path = os.path.relpath(full_path, workspace) - except ValueError: - # os.path.relpath() will raise ValueError on Windows - # when full_path and workspace have different mount points. - # https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20 - rel_path = filesystempath - if not rel_path.startswith(".."): - filesystempath = rel_path - if lineno is not None: # 0-index to 1-index lineno += 1 - # get the name of the current failed test, with parametrize info longrepr = report.head_line or item.name # get the error message and line number from the actual error @@ -70,37 +51,128 @@ def pytest_runtest_makereport(item: Item, call): if report.longrepr.reprcrash is not None: longrepr += "\n\n" + report.longrepr.reprcrash.message tb_entries = report.longrepr.reprtraceback.reprentries - if len(tb_entries) > 1 and tb_entries[0].reprfileloc is not None: + if tb_entries: + entry = tb_entries[0] # Handle third-party exceptions - lineno = tb_entries[0].reprfileloc.lineno + if isinstance(entry, ReprEntry) and entry.reprfileloc is not None: + lineno = entry.reprfileloc.lineno + filesystempath = entry.reprfileloc.path + elif report.longrepr.reprcrash is not None: lineno = report.longrepr.reprcrash.lineno elif isinstance(report.longrepr, tuple): - _, lineno, message = report.longrepr + filesystempath, lineno, message = report.longrepr longrepr += "\n\n" + message elif isinstance(report.longrepr, str): longrepr += "\n\n" + report.longrepr - print( - _error_workflow_command(filesystempath, lineno, longrepr), file=sys.stderr + workflow_command = _build_workflow_command( + "error", + compute_path(filesystempath), + lineno, + message=longrepr, + ) + print(workflow_command, file=sys.stderr) + + +def compute_path(filesystempath: str) -> str: + """Extract and process location information from the report.""" + runpath = os.environ.get("PYTEST_RUN_PATH") + if runpath: + filesystempath = os.path.join(runpath, filesystempath) + + # try to convert to absolute path in GitHub Actions + workspace = os.environ.get("GITHUB_WORKSPACE") + if workspace: + full_path = os.path.abspath(filesystempath) + try: + rel_path = os.path.relpath(full_path, workspace) + except ValueError: + # os.path.relpath() will raise ValueError on Windows + # when full_path and workspace have different mount points. + rel_path = filesystempath + if not rel_path.startswith(".."): + filesystempath = rel_path + + return filesystempath + + +class _AnnotateWarnings: + def pytest_warning_recorded(self, warning_message, when, nodeid, location): # noqa: ARG002 + # enable only in a workflow of GitHub Actions + # ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables + if os.environ.get("GITHUB_ACTIONS") != "true": + return + + filesystempath = warning_message.filename + workspace = os.environ.get("GITHUB_WORKFLOW") + + if workspace: + try: + rel_path = os.path.relpath(filesystempath, workspace) + except ValueError: + # os.path.relpath() will raise ValueError on Windows + # when full_path and workspace have different mount points. + rel_path = filesystempath + if not rel_path.startswith(".."): + filesystempath = rel_path + else: + with contextlib.suppress(ValueError): + filesystempath = os.path.relpath(filesystempath) + + workflow_command = _build_workflow_command( + "warning", + filesystempath, + warning_message.lineno, + message=warning_message.message.args[0], ) + print(workflow_command, file=sys.stderr) + + +def pytest_addoption(parser): + group = parser.getgroup("pytest_github_actions_annotate_failures") + group.addoption( + "--exclude-warning-annotations", + action="store_true", + default=False, + help="Annotate failures in GitHub Actions.", + ) + + +def pytest_configure(config): + if not config.option.exclude_warning_annotations: + config.pluginmanager.register(_AnnotateWarnings(), "annotate_warnings") + + +def _build_workflow_command( + command_name: str, + file: str, + line: int, + end_line: int | None = None, + column: int | None = None, + end_column: int | None = None, + title: str | None = None, + message: str | None = None, +): + """Build a command to annotate a workflow.""" + result = f"::{command_name} " + entries = [ + ("file", file), + ("line", line), + ("endLine", end_line), + ("col", column), + ("endColumn", end_column), + ("title", title), + ] -def _error_workflow_command(filesystempath, lineno, longrepr): - # Build collection of arguments. Ordering is strict for easy testing - details_dict = OrderedDict() - details_dict["file"] = filesystempath - if lineno is not None: - details_dict["line"] = lineno + result = result + ",".join(f"{k}={v}" for k, v in entries if v is not None) - details = ",".join("{}={}".format(k, v) for k, v in details_dict.items()) + if message is not None: + result = result + "::" + _escape(message) - if longrepr is None: - return "\n::error {}".format(details) - else: - longrepr = _escape(longrepr) - return "\n::error {}::{}".format(details, longrepr) + return result -def _escape(s): +def _escape(s: str) -> str: return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 84fa678..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -packaging==20.4 diff --git a/tox.ini b/tox.ini index a22edf4..f743182 100644 --- a/tox.ini +++ b/tox.ini @@ -1,48 +1,28 @@ [tox] envlist = - py27-pytest4-{linux,windows} - py{36,37,38,39}-pytest{4,5,6,7}-{linux,windows} - py{310}-pytest{6,7}-{linux,windows} - pkg + py{38,39,310,311,312,313}-pytest{6,7,8} [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 [gh-actions:env] -PLATFORM = - ubuntu-latest: linux - windows-latest: windows - PYTEST_MAJOR_VERSION = - 4: pytest4 - 5: pytest5 6: pytest6 7: pytest7 + 8: pytest8 [testenv] +min_version = 4.22.0 +groups = test deps = - -rrequirements.txt - pytest4: pytest>=4.0.0,<5.0.0 - pytest5: pytest>=5.0.0,<6.0.0 pytest6: pytest>=6.0.0,<7.0.0 pytest7: pytest>=7.0.0,<8.0.0 + pytest8: pytest>=8.0.0,<9.0.0 -commands = {envpython} -m pytest - -[testenv:pkg] -skip_install = true -deps = - twine - build -commands = - {envpython} -c 'import os.path, shutil, sys; \ - dist_dir = os.path.join("{toxinidir}", "dist"); \ - os.path.isdir(dist_dir) or sys.exit(0); \ - print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \ - shutil.rmtree(dist_dir)' - {envpython} -m build --outdir {toxinidir}/dist/ {toxinidir} - {envpython} -m twine check --strict dist/* +commands = {envpython} -m pytest {posargs}