Skip to content

Commit d254f55

Browse files
committed
Improve API documentation (#101)
2 parents 2801d66 + 00182a3 commit d254f55

File tree

10 files changed

+117
-49
lines changed

10 files changed

+117
-49
lines changed

docs/conf.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,16 @@
1818
# If extensions (or modules to document with autodoc) are in another directory,
1919
# add these directories to sys.path here. If the directory is relative to the
2020
# documentation root, use os.path.abspath to make it absolute, like shown here.
21+
sys.path.insert(0, os.path.join(__location__))
2122
sys.path.insert(0, os.path.join(__location__, "../src"))
2223

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

31-
try: # for Sphinx >= 1.7
32-
from sphinx.ext import apidoc
33-
except ImportError:
34-
from sphinx import apidoc
26+
import public_api_docs
3527

3628
output_dir = os.path.join(__location__, "api")
3729
module_dir = os.path.join(__location__, "../src/ini2toml")
38-
try:
39-
shutil.rmtree(output_dir)
40-
except FileNotFoundError:
41-
pass
42-
43-
try:
44-
import sphinx
45-
46-
cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"
47-
48-
args = cmd_line.split(" ")
49-
if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
50-
# This is a rudimentary parse_version to avoid external dependencies
51-
args = args[1:]
52-
53-
apidoc.main(args)
54-
except Exception as e:
55-
print("Running `sphinx-apidoc` failed!\n{}".format(e))
30+
public_api_docs.gen_stubs(module_dir, output_dir)
5631

5732
# -- General configuration ---------------------------------------------------
5833

docs/public_api_docs.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import shutil
2+
from pathlib import Path
3+
4+
TOC_TEMPLATE = """
5+
Module Reference
6+
================
7+
8+
.. toctree::
9+
:glob:
10+
:maxdepth: 2
11+
12+
ini2toml.api
13+
ini2toml.errors
14+
ini2toml.types
15+
16+
.. toctree::
17+
:maxdepth: 1
18+
19+
ini2toml.transformations
20+
21+
.. toctree::
22+
:caption: Plugins
23+
:glob:
24+
:maxdepth: 1
25+
26+
plugins/*
27+
"""
28+
29+
MODULE_TEMPLATE = """
30+
``{name}``
31+
~~{underline}~~
32+
33+
.. automodule:: {name}
34+
:members:{_members}
35+
:undoc-members:
36+
:show-inheritance:
37+
"""
38+
39+
40+
def gen_stubs(module_dir: str, output_dir: str):
41+
try_rmtree(output_dir) # Always start fresh
42+
Path(output_dir, "plugins").mkdir(parents=True, exist_ok=True)
43+
for module in iter_public():
44+
text = module_template(module)
45+
Path(output_dir, f"{module}.rst").write_text(text, encoding="utf-8")
46+
for module in iter_plugins(module_dir):
47+
text = module_template(module, "activate")
48+
Path(output_dir, f"plugins/{module}.rst").write_text(text, encoding="utf-8")
49+
Path(output_dir, "modules.rst").write_text(TOC_TEMPLATE, encoding="utf-8")
50+
51+
52+
def iter_public():
53+
lines = (x.strip() for x in TOC_TEMPLATE.splitlines())
54+
return (x for x in lines if x.startswith("ini2toml."))
55+
56+
57+
def iter_plugins(module_dir: str):
58+
return (
59+
f'ini2toml.plugins.{path.with_suffix("").name}'
60+
for path in Path(module_dir, "plugins").iterdir()
61+
if path.is_file()
62+
and path.name not in {".", "..", "__init__.py"}
63+
and not path.name.startswith("_")
64+
)
65+
66+
67+
def try_rmtree(target_dir: str):
68+
try:
69+
shutil.rmtree(target_dir)
70+
except FileNotFoundError:
71+
pass
72+
73+
74+
def module_template(name: str, *members: str) -> str:
75+
underline = "~" * len(name)
76+
_members = (" " + ", ".join(members)) if members else ""
77+
return MODULE_TEMPLATE.format(name=name, underline=underline, _members=_members)

src/ini2toml/api.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@
1313
for checking `structural polymorphism`_ during static analysis).
1414
These should be preferred when writing type hints and signatures.
1515
16-
Plugin authors can also rely on the functions exported by
17-
:mod:`~ini2toml.transformations`.
16+
Plugin authors can also use functions exported by :mod:`~ini2toml.transformations`.
1817
1918
.. _structural polymorphism: https://www.python.org/dev/peps/pep-0544/
2019
"""
2120

22-
from . import errors, transformations, types
2321
from .base_translator import BaseTranslator
2422
from .translator import FullTranslator, LiteTranslator, Translator
2523

@@ -28,7 +26,4 @@
2826
"FullTranslator",
2927
"LiteTranslator",
3028
"Translator",
31-
"errors",
32-
"types",
33-
"transformations",
3429
]

src/ini2toml/base_translator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ class BaseTranslator(Generic[T]):
6868
Tip
6969
---
7070
71-
Most of the times the usage of :class:`~ini2toml.translator.Translator` is preferred
71+
Most of the times the usage of :class:`~ini2toml.translator.Translator`
72+
(or its deterministic variants ``LiteTranslator``, ``FullTranslator``) is preferred
7273
over :class:`~ini2toml.base_translator.BaseTranslator` (unless you are vendoring
7374
``ini2toml`` and wants to reduce the number of files included in your project).
7475
"""

src/ini2toml/errors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def __init__(self, name: str, available: Sequence[str]):
1515

1616
@classmethod
1717
def check(cls, name: str, available: List[str]):
18+
""":meta private:"""
1819
if name not in available:
1920
raise cls(name, available)
2021

@@ -37,6 +38,7 @@ def __init__(self, name: str, new: Callable, existing: Callable):
3738
def check(
3839
cls, name: str, fn: Callable, registry: Mapping[str, types.ProfileAugmentation]
3940
):
41+
""":meta private:"""
4042
if name in registry:
4143
raise cls(name, fn, registry[name].fn)
4244

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

6466

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

6870
def __init__(self, block):
6971
super().__init__(f"{block.__class__}: {block}", {"block_object": block})

src/ini2toml/plugins/best_effort.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

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

10-
SECTION_SPLITTER = re.compile(r"\.|:|\\")
11-
KEY_SEP = "="
10+
_SECTION_SPLITTER = re.compile(r"\.|:|\\")
11+
_KEY_SEP = "="
1212

1313

1414
def activate(translator: Translator):
@@ -23,12 +23,12 @@ class BestEffort:
2323

2424
def __init__(
2525
self,
26-
key_sep=KEY_SEP,
27-
section_splitter=SECTION_SPLITTER,
26+
key_sep=_KEY_SEP,
27+
section_splitter=_SECTION_SPLITTER,
2828
):
2929
self.key_sep = key_sep
3030
self.section_splitter = section_splitter
31-
self.split_dict = partial(split_kv_pairs, key_sep=KEY_SEP)
31+
self.split_dict = partial(split_kv_pairs, key_sep=key_sep)
3232

3333
def process_values(self, doc: M) -> M:
3434
doc_items = list(doc.items())

src/ini2toml/plugins/pytest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
_logger = logging.getLogger(__name__)
1414

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

@@ -63,9 +63,9 @@ def process_section(self, section: MutableMapping):
6363
if field in self.DONT_TOUCH:
6464
continue
6565
if field in self.LINE_SEPARATED_LIST_VALUES:
66-
section[field] = split_lines(section[field])
66+
section[field] = _split_lines(section[field])
6767
elif field in self.SPACE_SEPARATED_LIST_VALUES:
68-
section[field] = split_spaces(section[field])
68+
section[field] = _split_spaces(section[field])
6969
elif hasattr(self, f"_process_{field}"):
7070
section[field] = getattr(self, f"_process_{field}")(section[field])
7171
else:

src/ini2toml/transformations.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
"""
22
Reusable value and type casting transformations
33
4+
This module is stable on a "best effort"-basis, and small backwards incompatibilities
5+
can be introduced in "minor"/"patch" version bumps (it will not be considered a
6+
regression automatically).
7+
While it can be used by plugin writers, it is not intended for general public use.
8+
49
.. testsetup:: *
510
611
# workaround for missing import in sphinx-doctest
@@ -53,7 +58,7 @@
5358
For example: transforming ``"2"`` (string) into ``2`` (integer).
5459
- The second one tries to preserve metadata (such as comments) from the original CFG/INI
5560
file. This kind of transformation processes a string value into an intermediary
56-
representation (e.g. :obj:`Commented`, :obj:`CommentedList`, obj:`CommentedKV`)
61+
representation (e.g. :obj:`Commented`, :obj:`CommentedList`, :obj:`CommentedKV`)
5762
that needs to be properly handled before adding to the TOML document.
5863
5964
In a higher level we can also consider an ensemble of transformations that transform an
@@ -68,15 +73,18 @@
6873

6974

7075
def noop(x: T) -> T:
76+
"""Return the value unchanged"""
7177
return x
7278

7379

7480
def is_true(value: str) -> bool:
81+
"""``value in ("true", "1", "yes", "on")``"""
7582
value = value.lower()
7683
return value in ("true", "1", "yes", "on")
7784

7885

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

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

8896

8997
def coerce_bool(value: str) -> bool:
98+
"""Convert the value based on :func:`~.is_true` and :func:`~.is_false`."""
9099
if is_true(value):
91100
return True
92101
if is_false(value):
@@ -158,6 +167,7 @@ def split_comment(
158167

159168

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

177187

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

181192

src/ini2toml/translator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Translator(BaseTranslator[str]):
2424
while ``BaseTranslator`` requires the user to explicitly set these parameters.
2525
2626
For most of the users ``Translator`` is recommended over ``BaseTranslator``.
27+
Most of the times ``Translator`` (or its deterministic variants ``LiteTranslator``,
28+
``FullTranslator``) is recommended over ``BaseTranslator``.
2729
2830
See :class:`~ini2toml.base_translator.BaseTranslator` for a description of the
2931
instantiation parameters.
@@ -75,7 +77,7 @@ def _discover_toml_dumps_fn() -> types.TomlDumpsFn:
7577

7678
class LiteTranslator(Translator):
7779
"""Similar to ``Translator``, but instead of trying to figure out ``ini_loads_fn``
78-
and ``toml_dumps_fn`` is will always try to the ``lite`` flavour
80+
and ``toml_dumps_fn`` is will always try to use the ``lite`` flavour
7981
(ignoring comments).
8082
"""
8183

@@ -110,7 +112,7 @@ def __init__(
110112

111113
class FullTranslator(Translator):
112114
"""Similar to ``Translator``, but instead of trying to figure out ``ini_loads_fn``
113-
and ``toml_dumps_fn`` is will always try to use the ``full`` version
115+
and ``toml_dumps_fn`` is will always try to use the ``full`` flavour
114116
(best effort to maintain comments).
115117
"""
116118

src/ini2toml/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343

4444

4545
class CLIChoice(Protocol):
46+
""":meta private:"""
47+
4648
name: str
4749
help_text: str
4850

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

7174

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

7882
def augment_profiles(
7983
self,
@@ -88,6 +92,7 @@ def augment_profiles(
8892
strings), ``name`` is taken from ``fn.__name__`` and ``help_text`` is taken from
8993
``fn.__doc__`` (docstring).
9094
"""
95+
...
9196

9297

9398
Plugin = Callable[[Translator], None]

0 commit comments

Comments
 (0)