Skip to content

Commit

Permalink
Configure Sphinx via technote.toml
Browse files Browse the repository at this point in the history
This establishes the basic system for configuring the sphinx build using
technote.toml. The technote.sphinxconf module provides the base Sphinx
configuration for conf.py. The TechnoteSphinxConfig class contains an
instance of the TechnoteTable model representing the technote.toml file,
and also provides helper methods for setting variables in the
conf.py/technote.sphinxconf context.
  • Loading branch information
jonathansick committed Nov 8, 2022
1 parent 300266a commit 52351a6
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 15 deletions.
5 changes: 1 addition & 4 deletions demo/conf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
project = "Demo technote"
author = "Rubin Observatory"
exclude_patterns = ["_build", "README.rst", "README.md", "Makefile"]
html_theme = "technote"
from technote.sphinxconf import * # noqa: F401 F403
16 changes: 16 additions & 0 deletions demo/technote.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[technote]
id = "SQR-000"
series_id = "SQR"
title = "The technical note system"
canonical_url = "https://sqr-000.lsst.io"
github_url = "https://github.com/lsst-sqre/sqr-000"
github_default_branch = "master"
date_created = "2015-11-18"
date_updated = "2015-11-23"

[[technote.authors]]
name = { given_names = "Jonathan", family_names = "Sick" }
orcid = "https://orcid.org/0000-0003-3001-676X"
affiliations = [
{ name = "Rubin Observatory", ror = "https://ror.org/048g3cy84" }
]
154 changes: 151 additions & 3 deletions src/technote/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,29 @@
from __future__ import annotations

import re
import sys
from dataclasses import dataclass
from datetime import date
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
from pathlib import Path
from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union

if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib

from pydantic import (
BaseModel,
EmailStr,
Extra,
Field,
HttpUrl,
ValidationError,
root_validator,
validator,
)
from sphinx.errors import ConfigError

from .metadata.orcid import Orcid
from .metadata.ror import Ror
Expand Down Expand Up @@ -105,7 +115,15 @@ class SphinxTable(BaseModel):
description="Additional Sphinx extensions to use in the build.",
)

intersphinx: IntersphinxTable
intersphinx: IntersphinxTable = Field(
default_factory=IntersphinxTable,
description="Intersphinx configurations.",
)

linkcheck: LinkcheckTable = Field(
default_factory=LinkcheckTable,
description="Link check builder settings.",
)


class Organization(BaseModel):
Expand Down Expand Up @@ -174,6 +192,14 @@ class PersonName(BaseModel):
),
)

@property
def plain_text_name(self) -> str:
"""The name in plain text."""
if self.name is not None:
return self.name
else:
return f"{self.given_names} {self.family_names}"

@validator("family_names", "given_names", "name")
def clean_whitespace(cls, v: Optional[str]) -> Optional[str]:
if v:
Expand Down Expand Up @@ -416,10 +442,132 @@ class TechnoteTable(BaseModel):
default_factory=list,
)

sphinx: SphinxTable = Field(default_factory=lambda: SphinxTable)
sphinx: SphinxTable = Field(default_factory=SphinxTable)


class TechnoteToml(BaseModel, extra=Extra.ignore):
"""A model of a ``technote.toml`` configuration file."""

technote: TechnoteTable

@classmethod
def parse_toml(cls, content: str) -> TechnoteToml:
"""Load a ``technote.toml`` file from the project directory.
Parameters
----------
content
The string content of a ``technote.toml`` file.
Returns
-------
TechnoteToml
The parsed `TechnoteToml`.
"""
return cls.parse_obj(tomllib.loads(content))


@dataclass
class TechnoteSphinxConfig:
"""A wrapper around `TechnoteToml` that assists in setting Sphinx
configurations in a conf.py file (via `technote.sphinxconf`).
"""

toml: TechnoteToml
"""The parse ``technote.toml`` file."""

@classmethod
def find_and_load(cls) -> TechnoteSphinxConfig:
"""Find the ``technote.toml`` file in the current Sphinx build and
load it.
Returns
-------
TechnoteSphinxConfig
The technote configuration, useful for configuring the Sphinx
project.
"""
path = Path("technote.toml")
if not path.is_file:
raise ConfigError("Cannot find the technote.toml file.")
return cls.load(path.read_text())

@classmethod
def load(cls, toml_content: str) -> TechnoteSphinxConfig:
"""Load the content of a ``technote.toml`` file.
Parameters
----------
content
The string content of a ``technote.toml`` file.
Returns
-------
TechnoteSphinxConfig
The sphinx configuration wrapper class around `TechnoteToml`.
"""
try:
parsed_toml = TechnoteToml.parse_toml(toml_content)
except ValidationError as e:
message = (
f"Syntax or validation issue in technote.toml:\n\n" f"{str(e)}"
)
raise ConfigError(message)

return cls(toml=parsed_toml)

@property
def title(self) -> str:
if self.toml.technote.title:
return self.toml.technote.title
else:
return "FIXME introspect title"

@property
def author(self) -> str:
if self.toml.technote.authors:
return ", ".join(
a.name.plain_text_name for a in self.toml.technote.authors
)
else:
return ""

def append_extensions(self, extensions: List[str]) -> None:
"""Append user-configured extensions to an existing list."""
for new_ext in self.toml.technote.sphinx.extensions:
if new_ext not in extensions:
extensions.append(new_ext)

def extend_intersphinx_mapping(
self, mapping: MutableMapping[str, Tuple[str, Union[str, None]]]
) -> None:
"""Extend the ``intersphinx_mapping`` dictionary with configured
projects.
"""
for (
project,
url,
) in self.toml.technote.sphinx.intersphinx.projects.items():
mapping[project] = (str(url), None)

def append_linkcheck_ignore(self, link_patterns: List[str]) -> None:
"""Append URL patterns for sphinx.linkcheck.ignore to existing
patterns.
"""
link_patterns.extend(self.toml.technote.sphinx.linkcheck.ignore)

def append_nitpick_ignore(
self, nitpick_ignore: List[Tuple[str, str]]
) -> None:
nitpick_ignore.extend(self.toml.technote.sphinx.nitpick_ignore)

def append_nitpick_ignore_regex(
self, nitpick_ignore_regex: List[Tuple[str, str]]
) -> None:
nitpick_ignore_regex.extend(
self.toml.technote.sphinx.nitpick_ignore_regex
)

@property
def nitpicky(self) -> bool:
return self.toml.technote.sphinx.nitpicky
77 changes: 77 additions & 0 deletions src/technote/sphinxconf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Sphinx configuration for technotes.
To use this configuration in a Technote project, write a conf.py containing::
from technote.sphinxconf import *
"""

from __future__ import annotations

from typing import Dict, List, Tuple, Union

from .config import TechnoteSphinxConfig

# Restrict to only Sphinx configurations
__all__ = [
# SPHINX
"project",
"author",
"exclude_patterns",
"html_theme",
"extensions",
"nitpicky",
"nitpick_ignore",
"nitpick_ignore_regex",
# INTERSPHINX
"intersphinx_mapping",
"intersphinx_timeout",
"intersphinx_cache_limit",
# LINKCHECK
"linkcheck_retries",
"linkcheck_timeout",
"linkcheck_ignore",
]

_t = TechnoteSphinxConfig.find_and_load()

# ============================================================================
# SPHINX General sphinx settings
# ============================================================================

project = _t.title
author = _t.author
exclude_patterns = ["_build", "README.rst", "README.md", "Makefile"]
html_theme = "technote"

extensions: List[str] = []
_t.append_extensions(extensions)

# Nitpicky settings and ignored errors
nitpicky = _t.nitpicky

nitpick_ignore: List[Tuple[str, str]] = []
_t.append_nitpick_ignore(nitpick_ignore)

nitpick_ignore_regex: List[Tuple[str, str]] = []
_t.append_nitpick_ignore_regex(nitpick_ignore_regex)

# ============================================================================
# INTERSPHINX Intersphinx settings
# ============================================================================

intersphinx_mapping: Dict[str, Tuple[str, Union[str, None]]] = {}
_t.extend_intersphinx_mapping(intersphinx_mapping)

intersphinx_timeout = 10.0 # seconds

intersphinx_cache_limit = 5 # days


# ============================================================================
# LINKCHECK Link check builder settings
# ============================================================================

linkcheck_retries = 2
linkcheck_timeout = 15
linkcheck_ignore: List[str] = []
_t.append_linkcheck_ignore(linkcheck_ignore)
9 changes: 1 addition & 8 deletions tests/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@

from __future__ import annotations

import sys

if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib

from technote.config import TechnoteToml

sample_toml = """
Expand All @@ -34,5 +27,5 @@ def test_toml_parsing() -> None:
"""Test TechnoteToml by parsing a sample document that should be
well-formatted.
"""
technote_toml = TechnoteToml.parse_obj(tomllib.loads(sample_toml))
technote_toml = TechnoteToml.parse_toml(sample_toml)
assert technote_toml.technote.id == "SQR-000"

0 comments on commit 52351a6

Please sign in to comment.