From bd194a726a35f33102c43415e3da6d4a0c14a839 Mon Sep 17 00:00:00 2001 From: "Massa, Francesca" Date: Mon, 10 Feb 2025 20:18:05 +0100 Subject: [PATCH] implement tag validation functionality and tests --- docs/configuration.rst | 9 ++++ src/sphinx_tags/__init__.py | 38 +++++++++++++++ test/sources/test-validations/conf.py | 6 +++ test/sources/test-validations/index.rst | 5 ++ test/test_badges.py | 4 +- test/test_validation_tags.py | 65 +++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 test/sources/test-validations/conf.py create mode 100644 test/sources/test-validations/index.rst create mode 100644 test/test_validation_tags.py diff --git a/docs/configuration.rst b/docs/configuration.rst index fcc00de..94952a0 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -29,6 +29,15 @@ A few custom configuration keys can be used in your ``conf.py`` file. - Whether to display tags using sphinx-design badges. **Default:** ``False`` - ``tags_badge_colors`` - Colors to use for badges based on tag name. **Default:** ``{}`` +- ``tags_allowed_tag_names_regex`` + - Define one or multiple regular expressions that each tag name must pass. + All names are allowed by default. **Default:** ``[]`` +- ``tags_minimum_tag_count`` + - Define a required minimum amount of tags per directive. + No minimum is set by default. **Default:** ``-1`` +- ``tags_maximum_tag_count`` + - Define a required maximum amount of tags per directive. + No minimum is set by default. **Default:** ``-1`` Tags overview page diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index 7deeef6..60a2971 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -75,6 +75,9 @@ def run(self): # (can happen after _normalize_tag()) page_tags = list(filter(None, page_tags)) + # validate if tags meet the requirements set by config + self.validate(page_tags) + tag_dir = Path(self.env.app.srcdir) / self.env.app.config.tags_output_dir result = nodes.paragraph() result["classes"] = ["tags"] @@ -111,6 +114,38 @@ def run(self): return [result] + def validate(self, page_tags): + """Validate each tag against the allowed tag names and tag count constraints.""" + + tags_allowed_tag_names_regex = self.env.app.config.tags_allowed_tag_names_regex + minimum_tag_count = self.env.app.config.tags_minimum_tag_count + maximum_tag_count = self.env.app.config.tags_maximum_tag_count + + logger.verbose( + f"Validating tags {page_tags} with constraints: regex {tags_allowed_tag_names_regex}, min {minimum_tag_count}, max {maximum_tag_count}" + ) + count = len(page_tags) + + if minimum_tag_count >= 0 and count < minimum_tag_count: + raise ExtensionError( + f"Minimum tag count of {minimum_tag_count} not met for tags {page_tags} (count: {count})." + ) + + if 0 <= maximum_tag_count < count: + raise ExtensionError( + f"Maximum tag count of {maximum_tag_count} exceeded for tags {page_tags} (count: {count})." + ) + + if tags_allowed_tag_names_regex: + for tag in page_tags: + if not any( + re.fullmatch(pattern, tag) + for pattern in tags_allowed_tag_names_regex + ): + raise ExtensionError( + f"Tag '{tag}' is not in the list of allowed tag names." + ) + def _get_plaintext_node( self, tag: str, file_basename: str, relative_tag_dir: Path ) -> List[nodes.Node]: @@ -444,6 +479,9 @@ def setup(app): app.add_config_value("tags_index_head", "Tags", "html") app.add_config_value("tags_create_badges", False, "html") app.add_config_value("tags_badge_colors", {}, "html") + app.add_config_value("tags_allowed_tag_names_regex", [], "html") + app.add_config_value("tags_minimum_tag_count", -1, "html") + app.add_config_value("tags_maximum_tag_count", -1, "html") # internal config values app.add_config_value( diff --git a/test/sources/test-validations/conf.py b/test/sources/test-validations/conf.py new file mode 100644 index 0000000..a545f21 --- /dev/null +++ b/test/sources/test-validations/conf.py @@ -0,0 +1,6 @@ +extensions = ["sphinx_tags"] +tags_create_tags = True +tags_extension = ["rst"] +tags_allowed_tag_names_regex = ["tag.*"] +tags_minimum_tag_count = -1 +tags_maximum_tag_count = -1 diff --git a/test/sources/test-validations/index.rst b/test/sources/test-validations/index.rst new file mode 100644 index 0000000..0c3c5a1 --- /dev/null +++ b/test/sources/test-validations/index.rst @@ -0,0 +1,5 @@ +Test Doc +======== +Test document + +.. tags:: tag 1, tag 2, tag 3 diff --git a/test/test_badges.py b/test/test_badges.py index 31906f9..ce511e5 100644 --- a/test/test_badges.py +++ b/test/test_badges.py @@ -18,13 +18,13 @@ @pytest.mark.sphinx("html", testroot="badges") -def test_build(app: SphinxTestApp, status: StringIO, warning: StringIO): +def test_build(app: SphinxTestApp, status: StringIO): app.build() assert "build succeeded" in status.getvalue() @pytest.mark.sphinx("html", testroot="badges") -def test_badges(app: SphinxTestApp, status: StringIO, warning: StringIO): +def test_badges(app: SphinxTestApp, status: StringIO): """Parse output HTML for a page with badges, find badge links, and check for CSS classes for expected badge colors """ diff --git a/test/test_validation_tags.py b/test/test_validation_tags.py new file mode 100644 index 0000000..b748ec5 --- /dev/null +++ b/test/test_validation_tags.py @@ -0,0 +1,65 @@ +"""Tests for tag validation logic""" + +from io import StringIO +import pytest +from sphinx.errors import ExtensionError +from sphinx.testing.util import SphinxTestApp +from test.conftest import OUTPUT_ROOT_DIR +import logging +from sphinx_tags import TagLinks +from unittest.mock import MagicMock + +OUTPUT_DIR = OUTPUT_ROOT_DIR / "general" + +"""Positive cases""" + + +@pytest.mark.sphinx("html", testroot="validations") +def test_default(app: SphinxTestApp, status: StringIO): + app.build(force_all=True) + assert "build succeeded" in status.getvalue() + + +@pytest.mark.sphinx( + "html", testroot="validations", confoverrides={"tags_maximum_tag_count": 3} +) +def test_maximum_pass(app: SphinxTestApp, status: StringIO): + app.build(force_all=True) + assert "build succeeded" in status.getvalue() + + +@pytest.mark.sphinx( + "html", testroot="validations", confoverrides={"tags_minimum_tag_count": 2} +) +def test_minimum_pass(app: SphinxTestApp, status: StringIO): + app.build(force_all=True) + assert "build succeeded" in status.getvalue() + + +"""Negative cases""" + + +@pytest.mark.sphinx( + "html", + testroot="validations", + confoverrides={"tags_allowed_tag_names_regex": ["nottag.*"]}, +) +def test_allowed_tag_names_regex_error(app: SphinxTestApp): + with pytest.raises(ExtensionError): + app.build(force_all=True) + + +@pytest.mark.sphinx( + "html", testroot="validations", confoverrides={"tags_minimum_tag_count": 4} +) +def test_minimum_error(app: SphinxTestApp): + with pytest.raises(ExtensionError): + app.build(force_all=True) + + +@pytest.mark.sphinx( + "html", testroot="validations", confoverrides={"tags_maximum_tag_count": 1} +) +def test_maximum_error(app: SphinxTestApp): + with pytest.raises(ExtensionError): + app.build(force_all=True)