Skip to content

Commit

Permalink
MAINT: extract Style class to style module (#4)
Browse files Browse the repository at this point in the history
* DX: activate VSCode multi-file diff editor
  https://code.visualstudio.com/updates/v1_85\#_multifile-diff-editor
* DX: remove `__init__.py` from search excludes
  This is legacy from when this plugin was a namespace package.
* MAINT: autoupdate pre-commit hooks
  • Loading branch information
redeboer authored Dec 9, 2023
1 parent 7a06a85 commit 0f11adb
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 134 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand All @@ -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]
Expand Down
11 changes: 3 additions & 8 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.rulers": [88]
Expand All @@ -45,18 +45,13 @@
"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,
"python.analysis.typeCheckingMode": "strict",
"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
}
126 changes: 5 additions & 121 deletions src/sphinx_pybtex_etal_style/__init__.py
Original file line number Diff line number Diff line change
@@ -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]:
Expand All @@ -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)
128 changes: 128 additions & 0 deletions src/sphinx_pybtex_etal_style/style.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 0f11adb

Please sign in to comment.