From 6f7b0d53f4b863afb8202e64df8536255da3bf52 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 20 Apr 2024 16:19:16 +0100 Subject: [PATCH 1/6] Improve docstrings --- src/ini2toml/api.py | 4 ---- src/ini2toml/base_translator.py | 3 ++- src/ini2toml/translator.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ini2toml/api.py b/src/ini2toml/api.py index d917d10..27a419f 100644 --- a/src/ini2toml/api.py +++ b/src/ini2toml/api.py @@ -19,7 +19,6 @@ .. _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 @@ -28,7 +27,4 @@ "FullTranslator", "LiteTranslator", "Translator", - "errors", - "types", - "transformations", ] diff --git a/src/ini2toml/base_translator.py b/src/ini2toml/base_translator.py index f045e9e..427f8d0 100644 --- a/src/ini2toml/base_translator.py +++ b/src/ini2toml/base_translator.py @@ -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). """ diff --git a/src/ini2toml/translator.py b/src/ini2toml/translator.py index 31300e6..646cfb4 100644 --- a/src/ini2toml/translator.py +++ b/src/ini2toml/translator.py @@ -75,7 +75,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). """ From 8af6e0e477844489efbc0dfb70b0a6685d87bf47 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 20 Apr 2024 18:21:41 +0100 Subject: [PATCH 2/6] Docstring and private visibility improvements --- src/ini2toml/api.py | 3 +-- src/ini2toml/plugins/best_effort.py | 10 +++++----- src/ini2toml/plugins/pytest.py | 8 ++++---- src/ini2toml/transformations.py | 11 +++++++++++ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/ini2toml/api.py b/src/ini2toml/api.py index 27a419f..245bcef 100644 --- a/src/ini2toml/api.py +++ b/src/ini2toml/api.py @@ -13,8 +13,7 @@ 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/ """ diff --git a/src/ini2toml/plugins/best_effort.py b/src/ini2toml/plugins/best_effort.py index c620e93..78bf3c7 100644 --- a/src/ini2toml/plugins/best_effort.py +++ b/src/ini2toml/plugins/best_effort.py @@ -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): @@ -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()) diff --git a/src/ini2toml/plugins/pytest.py b/src/ini2toml/plugins/pytest.py index b9e42d6..4029680 100644 --- a/src/ini2toml/plugins/pytest.py +++ b/src/ini2toml/plugins/pytest.py @@ -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. @@ -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: diff --git a/src/ini2toml/transformations.py b/src/ini2toml/transformations.py index 8ceb6e4..5c58f88 100644 --- a/src/ini2toml/transformations.py +++ b/src/ini2toml/transformations.py @@ -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 @@ -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") @@ -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): @@ -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() @@ -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) From 4a6a73c89a9db3ee0d1ac19af6fa1f88fb4f96c5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 20 Apr 2024 18:24:27 +0100 Subject: [PATCH 3/6] Small modernisation of docs/conf.py --- docs/conf.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3ef9a39..594d535 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,10 +28,7 @@ # setup.py install" in the RTD Advanced Settings. # Additionally it helps us to avoid running apidoc manually -try: # for Sphinx >= 1.7 - from sphinx.ext import apidoc -except ImportError: - from sphinx import apidoc +from sphinx.ext import apidoc output_dir = os.path.join(__location__, "api") module_dir = os.path.join(__location__, "../src/ini2toml") @@ -43,16 +40,10 @@ 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) + args = f"--implicit-namespaces -e -f -o {output_dir} {module_dir}" + apidoc.main(args.split()) except Exception as e: - print("Running `sphinx-apidoc` failed!\n{}".format(e)) + print(f"Running `sphinx-apidoc` failed!\n{e}") # -- General configuration --------------------------------------------------- From 935ff410170e2e33664a5d9d2cfc36b8f7902e6b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 20 Apr 2024 19:53:39 +0100 Subject: [PATCH 4/6] Generate API docs selectively --- docs/conf.py | 24 +++---------- docs/public_api_docs.py | 77 +++++++++++++++++++++++++++++++++++++++++ src/ini2toml/errors.py | 4 ++- src/ini2toml/types.py | 5 +++ 4 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 docs/public_api_docs.py diff --git a/docs/conf.py b/docs/conf.py index 594d535..55c9e3d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,32 +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 ------------------------------------------ -from sphinx.ext 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 - - args = f"--implicit-namespaces -e -f -o {output_dir} {module_dir}" - apidoc.main(args.split()) -except Exception as e: - print(f"Running `sphinx-apidoc` failed!\n{e}") +public_api_docs.gen_stubs(module_dir, output_dir) # -- General configuration --------------------------------------------------- diff --git a/docs/public_api_docs.py b/docs/public_api_docs.py new file mode 100644 index 0000000..8f4b219 --- /dev/null +++ b/docs/public_api_docs.py @@ -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) diff --git a/src/ini2toml/errors.py b/src/ini2toml/errors.py index 775eda7..464247a 100644 --- a/src/ini2toml/errors.py +++ b/src/ini2toml/errors.py @@ -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) @@ -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) @@ -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}) diff --git a/src/ini2toml/types.py b/src/ini2toml/types.py index 0eeb194..ff11f2a 100644 --- a/src/ini2toml/types.py +++ b/src/ini2toml/types.py @@ -43,6 +43,8 @@ class CLIChoice(Protocol): + """:meta private:""" + name: str help_text: str @@ -67,6 +69,7 @@ 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): @@ -74,6 +77,7 @@ 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, @@ -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] From eb2275f5f2c390795c40f87f192039d23f5bb1fa Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 20 Apr 2024 20:00:23 +0100 Subject: [PATCH 5/6] Improve docs for translators --- src/ini2toml/translator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ini2toml/translator.py b/src/ini2toml/translator.py index 646cfb4..e809123 100644 --- a/src/ini2toml/translator.py +++ b/src/ini2toml/translator.py @@ -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. @@ -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). """ From 00182a3564e74abcddee867270da200a64c0c109 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 20 Apr 2024 20:12:37 +0100 Subject: [PATCH 6/6] Fix improper rst --- src/ini2toml/transformations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ini2toml/transformations.py b/src/ini2toml/transformations.py index 5c58f88..de0e4c4 100644 --- a/src/ini2toml/transformations.py +++ b/src/ini2toml/transformations.py @@ -58,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