Skip to content
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

ENH: tags stashed on env #89

Merged
merged 1 commit into from
Jan 2, 2024
Merged
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
61 changes: 22 additions & 39 deletions src/sphinx_tags/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import os
import re
from collections import defaultdict
JWCook marked this conversation as resolved.
Show resolved Hide resolved
from fnmatch import fnmatch
from pathlib import Path
from typing import List
Expand Down Expand Up @@ -36,16 +37,18 @@ class TagLinks(SphinxDirective):
separator = ","

def run(self):
# Undo splitting args by whitespace, and use our own separator (to support tags with spaces)
tags = " ".join(self.arguments).split(self.separator)
tags = [t.strip() for t in tags]
tagline = " ".join(self.arguments).split(self.separator)
tags = [tag.strip() for tag in tagline]

tag_dir = Path(self.env.app.srcdir) / self.env.app.config.tags_output_dir
result = nodes.paragraph()
result["classes"] = ["tags"]
result += nodes.inline(text=f"{self.env.app.config.tags_intro_text} ")
count = 0

current_doc_dir = Path(self.env.doc2path(self.env.docname)).parent
relative_tag_dir = Path(os.path.relpath(tag_dir, current_doc_dir))

for tag in tags:
count += 1
# We want the link to be the path to the _tags folder, relative to
Expand All @@ -56,10 +59,8 @@ def run(self):
# - subfolder
# |
# - current_doc_path
current_doc_dir = Path(self.env.doc2path(self.env.docname)).parent
relative_tag_dir = Path(os.path.relpath(tag_dir, current_doc_dir))
file_basename = _normalize_tag(tag)

file_basename = _normalize_tag(tag)
if self.env.app.config.tags_create_badges:
result += self._get_badge_node(tag, file_basename, relative_tag_dir)
tag_separator = " "
Expand All @@ -68,6 +69,10 @@ def run(self):
tag_separator = f"{self.separator} "
if not count == len(tags):
result += nodes.inline(text=tag_separator)

# register tags to global metadata for document
self.env.metadata[self.env.docname]["tags"] = tags

return [result]

def _get_plaintext_node(
Expand Down Expand Up @@ -193,30 +198,11 @@ def create_file(


class Entry:
"""Extracted info from source file (*.rst/*.md/*.ipynb)"""
"""Tags to pages map"""

def __init__(self, entrypath: Path):
def __init__(self, entrypath: Path, tags: list):
self.filepath = entrypath
self.lines = self.filepath.read_text(encoding="utf8").split("\n")
if self.filepath.suffix == ".rst":
tagstart = ".. tags::"
tagend = ""
elif self.filepath.suffix == ".md":
tagstart = "```{tags}"
tagend = "```"
elif self.filepath.suffix == ".ipynb":
tagstart = '".. tags::'
tagend = '"'
else:
raise ValueError(
"Unknown file extension. Currently, only .rst, .md .ipynb are supported."
)
tagline = [line for line in self.lines if tagstart in line]
self.tags = []
if tagline:
tagline = tagline[0].replace(tagstart, "").rstrip(tagend)
self.tags = tagline.split(",")
self.tags = [tag.strip() for tag in self.tags]
self.tags = tags

def assign_to_tags(self, tag_dict):
"""Append ourself to tags"""
Expand Down Expand Up @@ -245,6 +231,7 @@ def tagpage(tags, outdir, title, extension, tags_index_head):
This page contains a list of all available tags.

"""

tags = list(tags.values())

if "md" in extension:
Expand Down Expand Up @@ -294,24 +281,18 @@ def assign_entries(app):
pages = []
tags = {}

# Get document paths in the project that match specified file extensions
doc_paths = get_matching_files(
app.srcdir,
include_patterns=[f"**.{extension}" for extension in app.config.tags_extension],
exclude_patterns=app.config.exclude_patterns,
)

for path in doc_paths:
entry = Entry(Path(app.srcdir) / path)
for docname in app.env.found_docs:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had no idea this was a thing! Good find. I was looking for something like that for #58 but somehow missed it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! found it when trying to figure out the environment variable https://www.sphinx-doc.org/en/master/extdev/envapi.html

doctags = app.env.metadata[docname].get("tags", None)
if doctags is None:
continue # skip if no tags
entry = Entry(app.env.doc2path(docname), doctags)
entry.assign_to_tags(tags)
pages.append(entry)

return tags, pages


def update_tags(app):
"""Update tags according to pages found"""

if app.config.tags_create_tags:
tags_output_dir = Path(app.config.tags_output_dir)

Expand All @@ -324,6 +305,7 @@ def update_tags(app):

# Create pages for each tag
tags, pages = assign_entries(app)

for tag in tags.values():
tag.create_file(
[item for item in pages if tag.name in item.tags],
Expand Down Expand Up @@ -386,4 +368,5 @@ def setup(app):
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
"env_version": 1,
}
13 changes: 8 additions & 5 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import pytest
import sphinx.testing
import sphinx.testing.path

collect_ignore = ["sources", "outputs"]
pytest_plugins = "sphinx.testing.fixtures"
Expand All @@ -31,11 +30,15 @@ def rootdir():
resolved and copied as files.
"""

def copytree(src, dest):
shutil.copytree(src, dest, symlinks=True)
# patch copytree into Path objects to have sphinx 5 compatibility and
class PatchedPath(type(Path())):
def __new__(cls, *pathsegments):
return super().__new__(cls, *pathsegments)

with patch.object(sphinx.testing.path.path, "copytree", copytree):
yield sphinx.testing.path.path(SOURCE_ROOT_DIR)
def copytree(src, dest):
shutil.copytree(src, dest, symlinks=True)

yield PatchedPath(SOURCE_ROOT_DIR)


@pytest.fixture(scope="session", autouse=True)
Expand Down
4 changes: 2 additions & 2 deletions test/outputs/general/_tags/tag-4.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
My tags: [{(tag 4)}]
**********************
My tags: [{(tag 4)}]
********************


With this tag
Expand Down
2 changes: 1 addition & 1 deletion test/outputs/general/_tags/tagsindex.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tags overview
Tags
^^^^

* [{(tag 4)}] (1)
* [{(tag 4)}] (1)

* tag 3 (2)

Expand Down
2 changes: 1 addition & 1 deletion test/outputs/general/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Test document

* Tags overview

* [{(tag 4)}] (1)
* [{(tag 4)}] (1)

* Page 1

Expand Down
2 changes: 1 addition & 1 deletion test/sources/test-badges/conf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extensions = ["sphinx_design", "sphinx_tags", "myst_parser"]
extensions = ["sphinx_tags", "myst_parser", "sphinx_design"]
tags_create_tags = True
tags_extension = ["md"]
tags_create_badges = True
Expand Down
23 changes: 16 additions & 7 deletions test/test_badges.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@
EXPECTED_CLASSES = {
"tag-1": "sd-bg-primary",
"tag-2": "sd-bg-secondary",
"prefix-tag-3": "sd-bg-info",
"tag-4": "sd-bg-dark",
"prefix:tag-3": "sd-bg-info",
"tag 4": "sd-bg-dark",
}


@pytest.mark.sphinx("html", testroot="badges")
def test_build(app: SphinxTestApp, status: StringIO, warning: StringIO):
app.build()
assert "build succeeded" in status.getvalue()


@pytest.mark.sphinx("html", testroot="badges")
def test_badges(app: SphinxTestApp, status: StringIO, warning: StringIO):
"""Parse output HTML for a page with badges, find badge links, and check for CSS classes for
Expand All @@ -27,11 +33,14 @@ def test_badges(app: SphinxTestApp, status: StringIO, warning: StringIO):

build_dir = Path(app.srcdir) / "_build" / "html"
page_1 = (build_dir / "page_1.html").read_text()
assert page_1 is not None
soup = BeautifulSoup(page_1, "html.parser")
# print(soup.prettify())
assert soup is not None
print(soup)
badge_links = soup.find_all("span", class_="sd-badge")
print("badge: ", badge_links)

badge_links = soup.find_all("a", class_="sd-badge")
classes_by_tag = {Path(a.get("href")).stem: a.get("class") for a in badge_links}

for tag, cls in EXPECTED_CLASSES.items():
assert cls in classes_by_tag[tag]
for (tag, class_), span in zip(EXPECTED_CLASSES.items(), badge_links):
assert tag in span.text # hard to test tag 4 b/c of the icon
assert class_ in span["class"]
3 changes: 2 additions & 1 deletion test/test_general_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def run_all_formats():
pytest.param(marks=pytest.mark.sphinx("text", testroot="symlink")),
pytest.param(marks=pytest.mark.sphinx("text", testroot="ipynb")),
],
ids=["myst", "rst", "symlink", "ipynb"],
)


Expand All @@ -29,7 +30,7 @@ def test_build(app: SphinxTestApp, status: StringIO, warning: StringIO):

# Build with notebooks and text output results in spurious errors "File Not Found: _tags/*.html"
# For all other formats, ensure no warnings are raised
if not app.srcdir.endswith("ipynb"):
if not str(app.srcdir).endswith("ipynb"):
assert not warning.getvalue().strip()


Expand Down