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}