diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9369ef..e7d8ebe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: black - repo: https://github.com/ComPWA/repo-maintenance - rev: 0.1.7 + rev: 0.1.9 hooks: - id: check-dev-files args: @@ -51,7 +51,7 @@ repos: - --repo-title=sphinx-pybtex-etal-style - repo: https://github.com/streetsidesoftware/cspell-cli - rev: v8.0.0 + rev: v8.1.1 hooks: - id: cspell @@ -76,17 +76,17 @@ repos: - python - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.3 + rev: v4.0.0-alpha.3-1 hooks: - id: prettier - repo: https://github.com/ComPWA/mirrors-pyright - rev: v1.1.338 + rev: v1.1.339 hooks: - id: pyright - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.1.7 hooks: - id: ruff args: [--fix] diff --git a/.vscode/settings.json b/.vscode/settings.json index fce99d7..4c877ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,7 +20,7 @@ }, "[python]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "ms-python.black-formatter", "editor.rulers": [88] @@ -45,6 +45,7 @@ "git.rebaseWhenSync": true, "github-actions.workflows.pinned.refresh.enabled": true, "github-actions.workflows.pinned.workflows": [".github/workflows/ci.yml"], + "multiDiffEditor.experimental.enabled": true, "mypy-type-checker.args": ["--config-file=${workspaceFolder}/pyproject.toml"], "mypy-type-checker.importStrategy": "fromEnvironment", "python.analysis.autoImportCompletions": false, @@ -52,11 +53,5 @@ "python.testing.unittestEnabled": false, "rewrap.wrappingColumn": 88, "ruff.enable": true, - "ruff.organizeImports": true, - "search.exclude": { - "**/tests/**/__init__.py": true, - "*/.pydocstyle": true, - "src/*/*/__init__.py": true, - "src/*/__init__.py": true - } + "ruff.organizeImports": true } diff --git a/src/sphinx_pybtex_etal_style/__init__.py b/src/sphinx_pybtex_etal_style/__init__.py index 0c17ac0..883b344 100644 --- a/src/sphinx_pybtex_etal_style/__init__.py +++ b/src/sphinx_pybtex_etal_style/__init__.py @@ -1,34 +1,15 @@ # pyright: reportMissingTypeStubs=false from __future__ import annotations -import sys -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any from pybtex.plugin import register_plugin -from pybtex.richtext import Tag, Text -from pybtex.style.formatting.unsrt import Style as UnsrtStyle -from pybtex.style.template import ( - FieldIsMissing, - Node, - _format_list, # pyright: ignore[reportPrivateUsage] - field, - href, - join, - node, - sentence, - words, -) + +from sphinx_pybtex_etal_style.style import UnsrtEtAl if TYPE_CHECKING: - from pybtex.database import Entry from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal - -ISBNResolvers = Literal["bookfinder", "isbnsearch"] def setup(app: Sphinx) -> dict[str, Any]: @@ -43,102 +24,5 @@ def setup(app: Sphinx) -> dict[str, Any]: def register_style(app: Sphinx, __: BuildEnvironment) -> None: - MyStyle.isbn_resolver = app.config.unsrt_etal_isbn_resolver - register_plugin("pybtex.style.formatting", "unsrt_et_al", MyStyle) - - -# Specify bibliography style -@node -def et_al(children, data, sep="", sep2=None, last_sep=None): # type: ignore[no-untyped-def] - if sep2 is None: - sep2 = sep - if last_sep is None: - last_sep = sep - parts = [part for part in _format_list(children, data) if part] - if len(parts) <= 1: - return Text(*parts) - if len(parts) == 2: # noqa: PLR2004 - return Text(sep2).join(parts) - if len(parts) == 3: # noqa: PLR2004 - return Text(last_sep).join([Text(sep).join(parts[:-1]), parts[-1]]) - return Text(parts[0], Tag("em", " et al")) - - -@node -def names(children, context, role, **kwargs): # type: ignore[no-untyped-def] - """Return formatted names.""" - if children: - msg = "The names field should not contain any children" - raise ValueError(msg) - try: - persons = context["entry"].persons[role] - except KeyError as exc: - raise FieldIsMissing(role, context["entry"]) from exc - - style = context["style"] - formatted_names = [ - style.format_name(person, style.abbreviate_names) for person in persons - ] - return et_al(**kwargs)[ # pyright: ignore[reportUntypedBaseClass] - formatted_names - ].format_data(context) - - -class MyStyle(UnsrtStyle): - isbn_resolver: ClassVar[ISBNResolvers] = "bookfinder" - - def __init__(self) -> None: - super().__init__(abbreviate_names=True) - - def format_names(self, role: Entry, as_sentence: bool = True) -> Node: - formatted_names = names(role, sep=", ", sep2=" and ", last_sep=", and ") - if as_sentence: - return sentence[formatted_names] - return formatted_names - - def format_eprint(self, e: Entry) -> Node: - if "doi" in e.fields: - return "" - return super().format_eprint(e) - - def format_url(self, e: Entry) -> Node: - if "doi" in e.fields or "eprint" in e.fields: - return "" - return words[ - href[ - field("url", raw=True), - field("url", raw=True, apply_func=remove_http), - ] - ] - - def format_isbn(self, e: Entry) -> Node: - raw_isbn = field("isbn", raw=True, apply_func=remove_dashes_and_spaces) - if self.isbn_resolver == "bookfinder": - url = join[ - "https://www.bookfinder.com/search/?isbn=", - raw_isbn, - "&mode=isbn&st=sr&ac=qr", - ] - elif self.isbn_resolver == "isbnsearch": - url = join["https://isbnsearch.org/isbn/", raw_isbn] - else: - msg = ( - f"Unknown unsrt_etal_isbn_resolver: {self.isbn_resolver}. Valid options" - f" are {', '.join(ISBNResolvers.__args__)}." - ) - raise NotImplementedError(msg) - return href[url, join["ISBN:", field("isbn", raw=True)]] - - -def remove_dashes_and_spaces(isbn: str) -> str: - to_remove = ["-", " "] - for remove in to_remove: - isbn = isbn.replace(remove, "") - return isbn - - -def remove_http(url: str) -> str: - to_remove = ["https://", "http://"] - for remove in to_remove: - url = url.replace(remove, "") - return url + UnsrtEtAl.isbn_resolver = app.config.unsrt_etal_isbn_resolver + register_plugin("pybtex.style.formatting", "unsrt_et_al", UnsrtEtAl) diff --git a/src/sphinx_pybtex_etal_style/style.py b/src/sphinx_pybtex_etal_style/style.py new file mode 100644 index 0000000..c93b484 --- /dev/null +++ b/src/sphinx_pybtex_etal_style/style.py @@ -0,0 +1,128 @@ +"""Style definition for :code:`unsrt_etal`.""" + +# pyright: reportMissingTypeStubs=false +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, ClassVar + +from pybtex.richtext import Tag, Text +from pybtex.style.formatting.unsrt import Style as UnsrtStyle +from pybtex.style.template import ( + FieldIsMissing, + Node, + _format_list, # pyright: ignore[reportPrivateUsage] + field, + href, + join, + node, + sentence, + words, +) + +if TYPE_CHECKING: + from pybtex.database import Entry +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + + +ISBNResolvers = Literal["bookfinder", "isbnsearch"] + + +# Specify bibliography style +@node +def et_al(children, data, sep="", sep2=None, last_sep=None): # type: ignore[no-untyped-def] + if sep2 is None: + sep2 = sep + if last_sep is None: + last_sep = sep + parts = [part for part in _format_list(children, data) if part] + if len(parts) <= 1: + return Text(*parts) + if len(parts) == 2: # noqa: PLR2004 + return Text(sep2).join(parts) + if len(parts) == 3: # noqa: PLR2004 + return Text(last_sep).join([Text(sep).join(parts[:-1]), parts[-1]]) + return Text(parts[0], Tag("em", " et al")) + + +@node +def names(children, context, role, **kwargs): # type: ignore[no-untyped-def] + """Return formatted names.""" + if children: + msg = "The names field should not contain any children" + raise ValueError(msg) + try: + persons = context["entry"].persons[role] + except KeyError as exc: + raise FieldIsMissing(role, context["entry"]) from exc + + style = context["style"] + formatted_names = [ + style.format_name(person, style.abbreviate_names) for person in persons + ] + return et_al(**kwargs)[ # pyright: ignore[reportUntypedBaseClass] + formatted_names + ].format_data(context) + + +class UnsrtEtAl(UnsrtStyle): + isbn_resolver: ClassVar[ISBNResolvers] = "bookfinder" + + def __init__(self) -> None: + super().__init__(abbreviate_names=True) + + def format_names(self, role: Entry, as_sentence: bool = True) -> Node: + formatted_names = names(role, sep=", ", sep2=" and ", last_sep=", and ") + if as_sentence: + return sentence[formatted_names] + return formatted_names + + def format_eprint(self, e: Entry) -> Node: + if "doi" in e.fields: + return "" + return super().format_eprint(e) + + def format_url(self, e: Entry) -> Node: + if "doi" in e.fields or "eprint" in e.fields: + return "" + return words[ + href[ + field("url", raw=True), + field("url", raw=True, apply_func=remove_http), + ] + ] + + def format_isbn(self, e: Entry) -> Node: + raw_isbn = field("isbn", raw=True, apply_func=remove_dashes_and_spaces) + if self.isbn_resolver == "bookfinder": + url = join[ + "https://www.bookfinder.com/search/?isbn=", + raw_isbn, + "&mode=isbn&st=sr&ac=qr", + ] + elif self.isbn_resolver == "isbnsearch": + url = join["https://isbnsearch.org/isbn/", raw_isbn] + else: + msg = ( + f"Unknown unsrt_etal_isbn_resolver: {self.isbn_resolver}. Valid options" + f" are {', '.join(ISBNResolvers.__args__)}." + ) + raise NotImplementedError(msg) + return href[url, join["ISBN:", field("isbn", raw=True)]] + + +def remove_dashes_and_spaces(isbn: str) -> str: + to_remove = ["-", " "] + for remove in to_remove: + isbn = isbn.replace(remove, "") + return isbn + + +def remove_http(url: str) -> str: + to_remove = ["https://", "http://"] + for remove in to_remove: + url = url.replace(remove, "") + return url