From 0b0dd6ddd5db76cf7385f393520241f03a958aed Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Apr 2022 22:07:20 +0300 Subject: [PATCH] Add tests for `pep_sphinx_extensions` (#2545) Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Co-authored-by: CAM Gerlach --- .github/workflows/render.yml | 1 - .github/workflows/test.yml | 51 ++++++++ .gitignore | 2 + .pre-commit-config.yaml | 24 ++++ Makefile | 3 + .../pep_processor/transforms/pep_headers.py | 4 +- .../pep_zero_generator/author.py | 6 +- pep_sphinx_extensions/tests/__init__.py | 0 .../transform/test_pep_footer.py | 34 ++++++ .../transform/test_pep_headers.py | 114 ++++++++++++++++++ .../pep_processor/transform/test_pep_zero.py | 25 ++++ .../tests/pep_zero_generator/test_author.py | 69 +++++++++++ .../tests/pep_zero_generator/test_parser.py | 88 ++++++++++++++ .../test_pep_index_generator.py | 12 ++ .../tests/pep_zero_generator/test_writer.py | 76 ++++++++++++ pep_sphinx_extensions/tests/peps/pep-9000.rst | 7 ++ pep_sphinx_extensions/tests/peps/pep-9001.rst | 7 ++ pep_sphinx_extensions/tests/utils.py | 6 + pytest.ini | 8 ++ requirements.txt | 4 + tox.ini | 12 ++ 21 files changed, 545 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 pep_sphinx_extensions/tests/__init__.py create mode 100644 pep_sphinx_extensions/tests/pep_processor/transform/test_pep_footer.py create mode 100644 pep_sphinx_extensions/tests/pep_processor/transform/test_pep_headers.py create mode 100644 pep_sphinx_extensions/tests/pep_processor/transform/test_pep_zero.py create mode 100644 pep_sphinx_extensions/tests/pep_zero_generator/test_author.py create mode 100644 pep_sphinx_extensions/tests/pep_zero_generator/test_parser.py create mode 100644 pep_sphinx_extensions/tests/pep_zero_generator/test_pep_index_generator.py create mode 100644 pep_sphinx_extensions/tests/pep_zero_generator/test_writer.py create mode 100644 pep_sphinx_extensions/tests/peps/pep-9000.rst create mode 100644 pep_sphinx_extensions/tests/peps/pep-9001.rst create mode 100644 pep_sphinx_extensions/tests/utils.py create mode 100644 pytest.ini create mode 100644 tox.ini diff --git a/.github/workflows/render.yml b/.github/workflows/render.yml index bafa531da..264716958 100644 --- a/.github/workflows/render.yml +++ b/.github/workflows/render.yml @@ -22,7 +22,6 @@ jobs: - name: 👷‍ Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade -r requirements.txt - name: 🔧 Render PEPs run: make pages -j$(nproc) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..78833bb56 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test Sphinx Extensions + +on: + push: + paths: + - ".github/workflows/test.yml" + - "pep_sphinx_extensions/**" + - "tox.ini" + pull_request: + paths: + - ".github/workflows/test.yml" + - "pep_sphinx_extensions/**" + - "tox.ini" + workflow_dispatch: + +env: + FORCE_COLOR: 1 + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10"] + os: [windows-latest, macos-latest, ubuntu-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U wheel + python -m pip install -U tox + + - name: Run tests with tox + run: | + tox -e py -- -v --cov-report term + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + flags: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index b9c892157..d63361827 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ __pycache__ *.pyo *~ *env +.coverage +.tox .vscode *.swp /build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a07ab6cd8..bb2ed78a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,6 +42,30 @@ repos: - id: check-yaml name: "Check YAML" + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + name: "Format with Black" + args: + - '--target-version=py39' + - '--target-version=py310' + files: 'pep_sphinx_extensions/tests/.*' + + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + name: "Sort imports with isort" + args: ['--profile=black', '--atomic'] + files: 'pep_sphinx_extensions/tests/.*' + + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 0.5.2 + hooks: + - id: tox-ini-fmt + name: "Format tox.ini" + # RST checks - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 diff --git a/Makefile b/Makefile index a6cc86c2f..616f725cc 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,9 @@ lint: venv $(VENVDIR)/bin/python3 -m pre_commit --version > /dev/null || $(VENVDIR)/bin/python3 -m pip install pre-commit $(VENVDIR)/bin/python3 -m pre_commit run --all-files +test: venv + $(VENVDIR)/bin/python3 -bb -X dev -W error -m pytest + spellcheck: venv $(VENVDIR)/bin/python3 -m pre_commit --version > /dev/null || $(VENVDIR)/bin/python3 -m pip install pre-commit $(VENVDIR)/bin/python3 -m pre_commit run --all-files --hook-stage manual codespell diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py index 3e237c27e..90872de0a 100644 --- a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py +++ b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py @@ -204,8 +204,8 @@ def _process_pretty_url(url: str) -> tuple[str, str]: item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts) except KeyError as error: raise ValueError( - "{url} not a link to a recognized domain to prettify") from error - item_name = item_name.title().replace("Sig", "SIG") + f"{url} not a link to a recognized domain to prettify") from error + item_name = item_name.title().replace("Sig", "SIG").replace("Pep", "PEP") return item_name, item_type diff --git a/pep_sphinx_extensions/pep_zero_generator/author.py b/pep_sphinx_extensions/pep_zero_generator/author.py index 22299b056..4425c6b3c 100644 --- a/pep_sphinx_extensions/pep_zero_generator/author.py +++ b/pep_sphinx_extensions/pep_zero_generator/author.py @@ -33,10 +33,6 @@ def parse_author_email(author_email_tuple: tuple[str, str], authors_overrides: d if name_parts.mononym is not None: return Author(name_parts.mononym, name_parts.mononym, email) - if name_parts.surname[1] == ".": - # Add an escape to avoid docutils turning `v.` into `22.`. - name_parts.surname = f"\\{name_parts.surname}" - if name_parts.suffix: last_first = f"{name_parts.surname}, {name_parts.forename}, {name_parts.suffix}" return Author(last_first, name_parts.surname, email) @@ -63,7 +59,7 @@ def _parse_name(full_name: str) -> _Name: num_parts = len(name_parts) suffix = raw_suffix.strip() - if num_parts == 0: + if name_parts == [""]: raise ValueError("Name is empty!") elif num_parts == 1: return _Name(mononym=name_parts[0], suffix=suffix) diff --git a/pep_sphinx_extensions/tests/__init__.py b/pep_sphinx_extensions/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_footer.py b/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_footer.py new file mode 100644 index 000000000..ad8cf2782 --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_footer.py @@ -0,0 +1,34 @@ +from pathlib import Path + +from pep_sphinx_extensions.pep_processor.transforms import pep_footer + + +def test_add_source_link(): + out = pep_footer._add_source_link(Path("pep-0008.txt")) + + assert "https://github.com/python/peps/blob/main/pep-0008.txt" in str(out) + + +def test_add_commit_history_info(): + out = pep_footer._add_commit_history_info(Path("pep-0008.txt")) + + assert str(out).startswith( + "Last modified: " + '' + ) + # A variable timestamp comes next, don't test that + assert str(out).endswith("") + + +def test_add_commit_history_info_invalid(): + out = pep_footer._add_commit_history_info(Path("pep-not-found.txt")) + + assert str(out) == "" + + +def test_get_last_modified_timestamps(): + out = pep_footer._get_last_modified_timestamps() + + assert len(out) >= 585 + # Should be a Unix timestamp and at least this + assert out["pep-0008.txt"] >= 1643124055 diff --git a/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_headers.py b/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_headers.py new file mode 100644 index 000000000..21e398082 --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_headers.py @@ -0,0 +1,114 @@ +import pytest + +from pep_sphinx_extensions.pep_processor.transforms import pep_headers + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ("my-mailing-list@example.com", "my-mailing-list@example.com"), + ("python-tulip@googlegroups.com", "https://groups.google.com/g/python-tulip"), + ("db-sig@python.org", "https://mail.python.org/mailman/listinfo/db-sig"), + ("import-sig@python.org", "https://mail.python.org/pipermail/import-sig/"), + ( + "python-announce@python.org", + "https://mail.python.org/archives/list/python-announce@python.org/", + ), + ], +) +def test_generate_list_url(test_input, expected): + out = pep_headers._generate_list_url(test_input) + + assert out == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + "https://mail.python.org/pipermail/python-3000/2006-November/004190.html", + ("Python-3000", "message"), + ), + ( + "https://mail.python.org/archives/list/python-dev@python.org/thread/HW2NFOEMCVCTAFLBLC3V7MLM6ZNMKP42/", + ("Python-Dev", "thread"), + ), + ( + "https://mail.python.org/mailman3/lists/capi-sig.python.org/", + ("Capi-SIG", "list"), + ), + ( + "https://mail.python.org/mailman/listinfo/web-sig", + ("Web-SIG", "list"), + ), + ( + "https://discuss.python.org/t/pep-643-metadata-for-package-source-distributions/5577", + ("Discourse", "thread"), + ), + ( + "https://discuss.python.org/c/peps/", + ("PEPs Discourse", "category"), + ), + ], +) +def test_process_pretty_url(test_input, expected): + out = pep_headers._process_pretty_url(test_input) + + assert out == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + "https://example.com/", + "https://example.com/ not a link to a recognized domain to prettify", + ), + ( + "https://mail.python.org", + "https://mail.python.org not a link to a list, message or thread", + ), + ( + "https://discuss.python.org/", + "https://discuss.python.org not a link to a Discourse thread or category", + ), + ], +) +def test_process_pretty_url_invalid(test_input, expected): + with pytest.raises(ValueError, match=expected): + pep_headers._process_pretty_url(test_input) + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + "https://mail.python.org/pipermail/python-3000/2006-November/004190.html", + "Python-3000 message", + ), + ( + "https://mail.python.org/archives/list/python-dev@python.org/thread/HW2NFOEMCVCTAFLBLC3V7MLM6ZNMKP42/", + "Python-Dev thread", + ), + ( + "https://mail.python.org/mailman3/lists/capi-sig.python.org/", + "Capi-SIG list", + ), + ( + "https://mail.python.org/mailman/listinfo/web-sig", + "Web-SIG list", + ), + ( + "https://discuss.python.org/t/pep-643-metadata-for-package-source-distributions/5577", + "Discourse thread", + ), + ( + "https://discuss.python.org/c/peps/", + "PEPs Discourse category", + ), + ], +) +def test_make_link_pretty(test_input, expected): + out = pep_headers._make_link_pretty(test_input) + + assert out == expected diff --git a/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_zero.py b/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_zero.py new file mode 100644 index 000000000..09b2effea --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_processor/transform/test_pep_zero.py @@ -0,0 +1,25 @@ +import pytest +from docutils import nodes + +from pep_sphinx_extensions.pep_processor.transforms import pep_zero + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + nodes.reference( + "", text="user@example.com", refuri="mailto:user@example.com" + ), + 'user at example.com', + ), + ( + nodes.reference("", text="Introduction", refid="introduction"), + 'Introduction', + ), + ], +) +def test_generate_list_url(test_input, expected): + out = pep_zero._mask_email(test_input) + + assert str(out) == expected diff --git a/pep_sphinx_extensions/tests/pep_zero_generator/test_author.py b/pep_sphinx_extensions/tests/pep_zero_generator/test_author.py new file mode 100644 index 000000000..8334b1c5f --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_zero_generator/test_author.py @@ -0,0 +1,69 @@ +import pytest + +from pep_sphinx_extensions.pep_zero_generator import author +from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + ("First Last", "first@example.com"), + author.Author( + last_first="Last, First", nick="Last", email="first@example.com" + ), + ), + ( + ("Guido van Rossum", "guido@example.com"), + author.Author( + last_first="van Rossum, Guido (GvR)", + nick="GvR", + email="guido@example.com", + ), + ), + ( + ("Hugo van Kemenade", "hugo@example.com"), + author.Author( + last_first="van Kemenade, Hugo", + nick="van Kemenade", + email="hugo@example.com", + ), + ), + ( + ("Eric N. Vander Weele", "eric@example.com"), + author.Author( + last_first="Vander Weele, Eric N.", + nick="Vander Weele", + email="eric@example.com", + ), + ), + ( + ("Mariatta", "mariatta@example.com"), + author.Author( + last_first="Mariatta", nick="Mariatta", email="mariatta@example.com" + ), + ), + ( + ("First Last Jr.", "first@example.com"), + author.Author( + last_first="Last, First, Jr.", nick="Last", email="first@example.com" + ), + ), + pytest.param( + ("First Last", "first at example.com"), + author.Author( + last_first="Last, First", nick="Last", email="first@example.com" + ), + marks=pytest.mark.xfail, + ), + ], +) +def test_parse_author_email(test_input, expected): + out = author.parse_author_email(test_input, AUTHORS_OVERRIDES) + + assert out == expected + + +def test_parse_author_email_empty_name(): + with pytest.raises(ValueError, match="Name is empty!"): + author.parse_author_email(("", "user@example.com"), AUTHORS_OVERRIDES) diff --git a/pep_sphinx_extensions/tests/pep_zero_generator/test_parser.py b/pep_sphinx_extensions/tests/pep_zero_generator/test_parser.py new file mode 100644 index 000000000..c339434e0 --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_zero_generator/test_parser.py @@ -0,0 +1,88 @@ +from pathlib import Path + +import pytest + +from pep_sphinx_extensions.pep_zero_generator import parser +from pep_sphinx_extensions.pep_zero_generator.author import Author +from pep_sphinx_extensions.pep_zero_generator.errors import PEPError +from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES + + +def test_pep_repr(): + pep8 = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES) + + assert repr(pep8) == "" + + +def test_pep_less_than(): + pep8 = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES) + pep3333 = parser.PEP(Path("pep-3333.txt"), AUTHORS_OVERRIDES) + + assert pep8 < pep3333 + + +def test_pep_equal(): + pep_a = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES) + pep_b = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES) + + assert pep_a == pep_b + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (80, "Style Guide for Python Code"), + (10, "Style ..."), + ], +) +def test_pep_details(test_input, expected): + pep8 = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES) + + assert pep8.details(title_length=test_input) == { + "authors": "GvR, Warsaw, Coghlan", + "number": 8, + "status": " ", + "title": expected, + "type": "P", + } + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + "First Last ", + [Author(last_first="Last, First", nick="Last", email="user@example.com")], + ), + ( + "First Last", + [Author(last_first="Last, First", nick="Last", email="")], + ), + ( + "user@example.com (First Last)", + [Author(last_first="Last, First", nick="Last", email="user@example.com")], + ), + pytest.param( + "First Last ", + [Author(last_first="Last, First", nick="Last", email="user@example.com")], + marks=pytest.mark.xfail, + ), + ], +) +def test_parse_authors(test_input, expected): + # Arrange + pep = parser.PEP(Path("pep-0160.txt"), AUTHORS_OVERRIDES) + + # Act + out = parser._parse_authors(pep, test_input, AUTHORS_OVERRIDES) + + # Assert + assert out == expected + + +def test_parse_authors_invalid(): + + pep = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES) + + with pytest.raises(PEPError, match="no authors found"): + parser._parse_authors(pep, "", AUTHORS_OVERRIDES) diff --git a/pep_sphinx_extensions/tests/pep_zero_generator/test_pep_index_generator.py b/pep_sphinx_extensions/tests/pep_zero_generator/test_pep_index_generator.py new file mode 100644 index 000000000..35a6a937c --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_zero_generator/test_pep_index_generator.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from pep_sphinx_extensions.pep_zero_generator import parser, pep_index_generator +from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES + + +def test_create_pep_json(): + peps = [parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)] + + out = pep_index_generator.create_pep_json(peps) + + assert '"url": "https://peps.python.org/pep-0008/"' in out diff --git a/pep_sphinx_extensions/tests/pep_zero_generator/test_writer.py b/pep_sphinx_extensions/tests/pep_zero_generator/test_writer.py new file mode 100644 index 000000000..9cae97a0a --- /dev/null +++ b/pep_sphinx_extensions/tests/pep_zero_generator/test_writer.py @@ -0,0 +1,76 @@ +from pathlib import Path + +import pytest + +from pep_sphinx_extensions.pep_zero_generator import parser, writer +from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES + + +def test_pep_zero_writer_emit_text_newline(): + pep0_writer = writer.PEPZeroWriter() + pep0_writer.emit_text("my text 1") + pep0_writer.emit_newline() + pep0_writer.emit_text("my text 2") + + assert pep0_writer.output == ["my text 1", "", "my text 2"] + + +def test_pep_zero_writer_emit_title(): + pep0_writer = writer.PEPZeroWriter() + pep0_writer.emit_title("My Title") + pep0_writer.emit_subtitle("My Subtitle") + + assert pep0_writer.output == [ + "My Title", + "========", + "", + "My Subtitle", + "-----------", + "", + ] + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + "pep-9000.rst", + { + "Fussyreverend, Francis": "one@example.com", + "Soulfulcommodore, Javier": "two@example.com", + }, + ), + ( + "pep-9001.rst", + {"Fussyreverend, Francis": "", "Soulfulcommodore, Javier": ""}, + ), + ], +) +def test_verify_email_addresses(test_input, expected): + # Arrange + peps = [ + parser.PEP( + Path(f"pep_sphinx_extensions/tests/peps/{test_input}"), AUTHORS_OVERRIDES + ) + ] + + # Act + out = writer._verify_email_addresses(peps) + + # Assert + assert out == expected + + +def test_sort_authors(): + # Arrange + authors_dict = { + "Zebra, Zoë": "zoe@example.com", + "lowercase, laurence": "laurence@example.com", + "Aardvark, Alfred": "alfred@example.com", + } + + # Act + out = writer._sort_authors(authors_dict) + + # Assert + assert out == ["Aardvark, Alfred", "lowercase, laurence", "Zebra, Zoë"] diff --git a/pep_sphinx_extensions/tests/peps/pep-9000.rst b/pep_sphinx_extensions/tests/peps/pep-9000.rst new file mode 100644 index 000000000..84a117c17 --- /dev/null +++ b/pep_sphinx_extensions/tests/peps/pep-9000.rst @@ -0,0 +1,7 @@ +PEP: 9000 +Title: Test with authors with email addresses +Author: Francis Fussyreverend , + Javier Soulfulcommodore +Created: 20-Apr-2022 +Status: Draft +Type: Process diff --git a/pep_sphinx_extensions/tests/peps/pep-9001.rst b/pep_sphinx_extensions/tests/peps/pep-9001.rst new file mode 100644 index 000000000..4a1a9e115 --- /dev/null +++ b/pep_sphinx_extensions/tests/peps/pep-9001.rst @@ -0,0 +1,7 @@ +PEP: 9001 +Title: Test with authors with no email addresses +Author: Francis Fussyreverend, + Javier Soulfulcommodore +Created: 20-Apr-2022 +Status: Draft +Type: Process diff --git a/pep_sphinx_extensions/tests/utils.py b/pep_sphinx_extensions/tests/utils.py new file mode 100644 index 000000000..19167d552 --- /dev/null +++ b/pep_sphinx_extensions/tests/utils.py @@ -0,0 +1,6 @@ +AUTHORS_OVERRIDES = { + "Guido van Rossum": { + "Surname First": "van Rossum, Guido (GvR)", + "Name Reference": "GvR", + }, +} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..b872465bf --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +addopts = -r a --strict-config --strict-markers --import-mode=importlib --cov pep_sphinx_extensions --cov-report html --cov-report xml +empty_parameter_set_mark = fail_at_collect +filterwarnings = + error +minversion = 6.0 +testpaths = pep_sphinx_extensions +xfail_strict = True diff --git a/requirements.txt b/requirements.txt index 6f79491b0..0f4493675 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,7 @@ docutils >= 0.17.1 # For RSS feedgen >= 0.9.0 # For RSS feed + +# For tests +pytest +pytest-cov diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..396ef49d6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[tox] +envlist = + py{311, 310, 39} +skipsdist = true + +[testenv] +passenv = + FORCE_COLOR +deps = + -rrequirements.txt +commands = + python -bb -X dev -W error -m pytest {posargs}