Skip to content

Commit

Permalink
Improve API documentation (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed Apr 20, 2024
2 parents 2801d66 + 00182a3 commit d254f55
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 49 deletions.
33 changes: 4 additions & 29 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,16 @@
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.join(__location__))
sys.path.insert(0, os.path.join(__location__, "../src"))

# -- Run sphinx-apidoc -------------------------------------------------------
# This hack is necessary since RTD does not issue `sphinx-apidoc` before running
# `sphinx-build -b html . _build/html`. See Issue:
# https://github.com/readthedocs/readthedocs.org/issues/1139
# DON'T FORGET: Check the box "Install your project inside a virtualenv using
# setup.py install" in the RTD Advanced Settings.
# Additionally it helps us to avoid running apidoc manually
# -- Automatically generated content ------------------------------------------

try: # for Sphinx >= 1.7
from sphinx.ext import apidoc
except ImportError:
from sphinx import apidoc
import public_api_docs

output_dir = os.path.join(__location__, "api")
module_dir = os.path.join(__location__, "../src/ini2toml")
try:
shutil.rmtree(output_dir)
except FileNotFoundError:
pass

try:
import sphinx

cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"

args = cmd_line.split(" ")
if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
# This is a rudimentary parse_version to avoid external dependencies
args = args[1:]

apidoc.main(args)
except Exception as e:
print("Running `sphinx-apidoc` failed!\n{}".format(e))
public_api_docs.gen_stubs(module_dir, output_dir)

# -- General configuration ---------------------------------------------------

Expand Down
77 changes: 77 additions & 0 deletions docs/public_api_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import shutil
from pathlib import Path

TOC_TEMPLATE = """
Module Reference
================
.. toctree::
:glob:
:maxdepth: 2
ini2toml.api
ini2toml.errors
ini2toml.types
.. toctree::
:maxdepth: 1
ini2toml.transformations
.. toctree::
:caption: Plugins
:glob:
:maxdepth: 1
plugins/*
"""

MODULE_TEMPLATE = """
``{name}``
~~{underline}~~
.. automodule:: {name}
:members:{_members}
:undoc-members:
:show-inheritance:
"""


def gen_stubs(module_dir: str, output_dir: str):
try_rmtree(output_dir) # Always start fresh
Path(output_dir, "plugins").mkdir(parents=True, exist_ok=True)
for module in iter_public():
text = module_template(module)
Path(output_dir, f"{module}.rst").write_text(text, encoding="utf-8")
for module in iter_plugins(module_dir):
text = module_template(module, "activate")
Path(output_dir, f"plugins/{module}.rst").write_text(text, encoding="utf-8")
Path(output_dir, "modules.rst").write_text(TOC_TEMPLATE, encoding="utf-8")


def iter_public():
lines = (x.strip() for x in TOC_TEMPLATE.splitlines())
return (x for x in lines if x.startswith("ini2toml."))


def iter_plugins(module_dir: str):
return (
f'ini2toml.plugins.{path.with_suffix("").name}'
for path in Path(module_dir, "plugins").iterdir()
if path.is_file()
and path.name not in {".", "..", "__init__.py"}
and not path.name.startswith("_")
)


def try_rmtree(target_dir: str):
try:
shutil.rmtree(target_dir)
except FileNotFoundError:
pass


def module_template(name: str, *members: str) -> str:
underline = "~" * len(name)
_members = (" " + ", ".join(members)) if members else ""
return MODULE_TEMPLATE.format(name=name, underline=underline, _members=_members)
7 changes: 1 addition & 6 deletions src/ini2toml/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
for checking `structural polymorphism`_ during static analysis).
These should be preferred when writing type hints and signatures.
Plugin authors can also rely on the functions exported by
:mod:`~ini2toml.transformations`.
Plugin authors can also use functions exported by :mod:`~ini2toml.transformations`.
.. _structural polymorphism: https://www.python.org/dev/peps/pep-0544/
"""

from . import errors, transformations, types
from .base_translator import BaseTranslator
from .translator import FullTranslator, LiteTranslator, Translator

Expand All @@ -28,7 +26,4 @@
"FullTranslator",
"LiteTranslator",
"Translator",
"errors",
"types",
"transformations",
]
3 changes: 2 additions & 1 deletion src/ini2toml/base_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ class BaseTranslator(Generic[T]):
Tip
---
Most of the times the usage of :class:`~ini2toml.translator.Translator` is preferred
Most of the times the usage of :class:`~ini2toml.translator.Translator`
(or its deterministic variants ``LiteTranslator``, ``FullTranslator``) is preferred
over :class:`~ini2toml.base_translator.BaseTranslator` (unless you are vendoring
``ini2toml`` and wants to reduce the number of files included in your project).
"""
Expand Down
4 changes: 3 additions & 1 deletion src/ini2toml/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(self, name: str, available: Sequence[str]):

@classmethod
def check(cls, name: str, available: List[str]):
""":meta private:"""
if name not in available:
raise cls(name, available)

Expand All @@ -37,6 +38,7 @@ def __init__(self, name: str, new: Callable, existing: Callable):
def check(
cls, name: str, fn: Callable, registry: Mapping[str, types.ProfileAugmentation]
):
""":meta private:"""
if name in registry:
raise cls(name, fn, registry[name].fn)

Expand All @@ -63,7 +65,7 @@ def __init__(self, key):


class InvalidCfgBlock(ValueError): # pragma: no cover -- not supposed to happen
"""Something is wrong with the provided CFG AST, the given block is not valid."""
"""Something is wrong with the provided ``.ini/.cfg`` AST"""

def __init__(self, block):
super().__init__(f"{block.__class__}: {block}", {"block_object": block})
10 changes: 5 additions & 5 deletions src/ini2toml/plugins/best_effort.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

M = TypeVar("M", bound=IntermediateRepr)

SECTION_SPLITTER = re.compile(r"\.|:|\\")
KEY_SEP = "="
_SECTION_SPLITTER = re.compile(r"\.|:|\\")
_KEY_SEP = "="


def activate(translator: Translator):
Expand All @@ -23,12 +23,12 @@ class BestEffort:

def __init__(
self,
key_sep=KEY_SEP,
section_splitter=SECTION_SPLITTER,
key_sep=_KEY_SEP,
section_splitter=_SECTION_SPLITTER,
):
self.key_sep = key_sep
self.section_splitter = section_splitter
self.split_dict = partial(split_kv_pairs, key_sep=KEY_SEP)
self.split_dict = partial(split_kv_pairs, key_sep=key_sep)

def process_values(self, doc: M) -> M:
doc_items = list(doc.items())
Expand Down
8 changes: 4 additions & 4 deletions src/ini2toml/plugins/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

_logger = logging.getLogger(__name__)

split_spaces = partial(split_list, sep=" ")
split_lines = partial(split_list, sep="\n")
_split_spaces = partial(split_list, sep=" ")
_split_lines = partial(split_list, sep="\n")
# ^ most of the list values in pytest use whitespace separators,
# but markers/filterwarnings are a special case.

Expand Down Expand Up @@ -63,9 +63,9 @@ def process_section(self, section: MutableMapping):
if field in self.DONT_TOUCH:
continue
if field in self.LINE_SEPARATED_LIST_VALUES:
section[field] = split_lines(section[field])
section[field] = _split_lines(section[field])
elif field in self.SPACE_SEPARATED_LIST_VALUES:
section[field] = split_spaces(section[field])
section[field] = _split_spaces(section[field])
elif hasattr(self, f"_process_{field}"):
section[field] = getattr(self, f"_process_{field}")(section[field])
else:
Expand Down
13 changes: 12 additions & 1 deletion src/ini2toml/transformations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"""
Reusable value and type casting transformations
This module is stable on a "best effort"-basis, and small backwards incompatibilities
can be introduced in "minor"/"patch" version bumps (it will not be considered a
regression automatically).
While it can be used by plugin writers, it is not intended for general public use.
.. testsetup:: *
# workaround for missing import in sphinx-doctest
Expand Down Expand Up @@ -53,7 +58,7 @@
For example: transforming ``"2"`` (string) into ``2`` (integer).
- The second one tries to preserve metadata (such as comments) from the original CFG/INI
file. This kind of transformation processes a string value into an intermediary
representation (e.g. :obj:`Commented`, :obj:`CommentedList`, obj:`CommentedKV`)
representation (e.g. :obj:`Commented`, :obj:`CommentedList`, :obj:`CommentedKV`)
that needs to be properly handled before adding to the TOML document.
In a higher level we can also consider an ensemble of transformations that transform an
Expand All @@ -68,15 +73,18 @@


def noop(x: T) -> T:
"""Return the value unchanged"""
return x


def is_true(value: str) -> bool:
"""``value in ("true", "1", "yes", "on")``"""
value = value.lower()
return value in ("true", "1", "yes", "on")


def is_false(value: str) -> bool:
"""``value in ("false", "0", "no", "off", "none", "null", "nil")``"""
value = value.lower()
return value in ("false", "0", "no", "off", "none", "null", "nil")

Expand All @@ -87,6 +95,7 @@ def is_float(value: str) -> bool:


def coerce_bool(value: str) -> bool:
"""Convert the value based on :func:`~.is_true` and :func:`~.is_false`."""
if is_true(value):
return True
if is_false(value):
Expand Down Expand Up @@ -158,6 +167,7 @@ def split_comment(


def split_comment(value, coerce_fn=noop, comment_prefixes=CP):
"""Split a "comment suffix" from the value."""
if not isinstance(value, str):
return value
value = value.strip()
Expand All @@ -176,6 +186,7 @@ def split_comment(value, coerce_fn=noop, comment_prefixes=CP):


def split_scalar(value: str, *, comment_prefixes=CP) -> Commented[Scalar]:
"""Combination of :func:`~.split_comment` and :func:`~.coerce_scalar`."""
return split_comment(value, coerce_scalar, comment_prefixes)


Expand Down
6 changes: 4 additions & 2 deletions src/ini2toml/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Translator(BaseTranslator[str]):
while ``BaseTranslator`` requires the user to explicitly set these parameters.
For most of the users ``Translator`` is recommended over ``BaseTranslator``.
Most of the times ``Translator`` (or its deterministic variants ``LiteTranslator``,
``FullTranslator``) is recommended over ``BaseTranslator``.
See :class:`~ini2toml.base_translator.BaseTranslator` for a description of the
instantiation parameters.
Expand Down Expand Up @@ -75,7 +77,7 @@ def _discover_toml_dumps_fn() -> types.TomlDumpsFn:

class LiteTranslator(Translator):
"""Similar to ``Translator``, but instead of trying to figure out ``ini_loads_fn``
and ``toml_dumps_fn`` is will always try to the ``lite`` flavour
and ``toml_dumps_fn`` is will always try to use the ``lite`` flavour
(ignoring comments).
"""

Expand Down Expand Up @@ -110,7 +112,7 @@ def __init__(

class FullTranslator(Translator):
"""Similar to ``Translator``, but instead of trying to figure out ``ini_loads_fn``
and ``toml_dumps_fn`` is will always try to use the ``full`` version
and ``toml_dumps_fn`` is will always try to use the ``full`` flavour
(best effort to maintain comments).
"""

Expand Down
5 changes: 5 additions & 0 deletions src/ini2toml/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@


class CLIChoice(Protocol):
""":meta private:"""

name: str
help_text: str

Expand All @@ -67,13 +69,15 @@ def is_active(self, explicitly_active: Optional[bool] = None) -> bool:
explicitly asked for the augmentation, ``False`` if the user explicitly denied
the augmentation, or ``None`` otherwise.
"""
...


class Translator(Protocol):
def __getitem__(self, profile_name: str) -> Profile:
"""Create and register (and return) a translation :class:`Profile`
(or return a previously registered one) (see :ref:`core-concepts`).
"""
...

def augment_profiles(
self,
Expand All @@ -88,6 +92,7 @@ def augment_profiles(
strings), ``name`` is taken from ``fn.__name__`` and ``help_text`` is taken from
``fn.__doc__`` (docstring).
"""
...


Plugin = Callable[[Translator], None]
Expand Down

0 comments on commit d254f55

Please sign in to comment.