Skip to content

implement tag validation functionality and tests #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions src/sphinx_tags/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions test/sources/test-validations/conf.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions test/sources/test-validations/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Test Doc
========
Test document

.. tags:: tag 1, tag 2, tag 3
4 changes: 2 additions & 2 deletions test/test_badges.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
65 changes: 65 additions & 0 deletions test/test_validation_tags.py
Original file line number Diff line number Diff line change
@@ -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)