Skip to content

omnipy.util.docstr_macros

Docstring macro expansion utilities.

This module provides functionality to expand macros in Python docstrings while preserving the original docstring (with unexpanded macros) in comment blocks above the docstring.

Authors
  • Sveinung Gundersen (concept, requirements, review and refactoring)
  • GitHub Copilot w/ Claude Sonnet 4.5 (impl. assistance, January 2026)

Created: January 2026

FUNCTION DESCRIPTION
expand_macros

Expand all macros in text.

find_macros_in_docstring

Find all macro references in a docstring.

get_macros_from_env

Load macro definitions from environment variables.

process_content

Process Python source code, expanding macros in docstrings.

ATTRIBUTE DESCRIPTION
ENV_MACRO_PREFIX

ORIGINAL_DOCSTRING_PREFIX

ENV_MACRO_PREFIX module-attribute

ENV_MACRO_PREFIX = 'OMNIPY_MACRO_'

ORIGINAL_DOCSTRING_PREFIX module-attribute

ORIGINAL_DOCSTRING_PREFIX = '%% Original docstring (managed by expand_docstr_macros.py) %%'

expand_macros

expand_macros(text: str, macros: dict[str, str]) -> str

Expand all macros in text.

Source code in src/omnipy/util/docstr_macros.py
def expand_macros(text: str, macros: dict[str, str]) -> str:
    """Expand all macros in text."""
    result = text

    for macro_name, macro_value in macros.items():
        macro_index = 0
        while macro_index != -1:
            macro_index = result.find(f'{{{{{macro_name}}}}}', macro_index)
            if macro_index == -1:
                break
            indent = ''
            if macro_index > 0:
                reversed_result_in_front_of_hit = result[macro_index - 1::-1]
                reverse_indent_match = re.match(r'^([ \t]*)', reversed_result_in_front_of_hit)
                indent = reverse_indent_match.group(1)[::-1] if reverse_indent_match else ''

            replace_value = ''
            for i, line in enumerate(macro_value.splitlines(keepends=True)):
                if i == 0:
                    replace_value += line
                else:
                    if macro_index == 0:
                        raise ValueError(
                            'Cannot indent macro expansion when macro is '
                            'placed at the start of the docstring. Please '
                            'break up multi-line macros placed at the '
                            'start of the docstring into smaller parts.',)
                    replace_value += indent + line

            result = (
                result[:macro_index] + replace_value
                + result[macro_index + len(f'{{{{{macro_name}}}}}'):])

    return result

find_macros_in_docstring

find_macros_in_docstring(docstring: str, macros: dict[str, str]) -> set[str]

Find all macro references in a docstring.

Source code in src/omnipy/util/docstr_macros.py
def find_macros_in_docstring(docstring: str, macros: dict[str, str]) -> set[str]:
    """Find all macro references in a docstring."""
    macros_found = set()
    for macro_name in macros.keys():
        if macro_name in docstring:
            macros_found.add(macro_name)
    return macros_found

get_macros_from_env

get_macros_from_env() -> dict[str, str]

Load macro definitions from environment variables.

Environment variables starting with 'OMNIPY_MACRO_' are treated as macro definitions. For example:

bash OMNIPY_MACRO_COMMON_PARAM='Parameters: param1: desc'

defines the macro COMMON_PARAM.

Returns: Dict mapping macro names (with {{...}} delimiters) to their expansion values

Source code in src/omnipy/util/docstr_macros.py
def get_macros_from_env() -> dict[str, str]:
    """
    Load macro definitions from environment variables.

    Environment variables starting with 'OMNIPY_MACRO_' are treated as macro definitions.
    For example:

    ```bash
    OMNIPY_MACRO_COMMON_PARAM='Parameters:\n  param1: desc'
    ```

    defines the macro COMMON_PARAM.

    Returns:
        Dict mapping macro names (with {{...}} delimiters) to their expansion values
    """
    import os
    macros = {}

    for key, value in os.environ.items():
        if key.startswith(ENV_MACRO_PREFIX):
            # Extract macro name from env var name
            # OMNIPY_MACRO_COMMON_PARAM -> COMMON_PARAM
            macro_name = key[len(ENV_MACRO_PREFIX):]
            macros[macro_name] = value

    return macros

process_content

process_content(content: str, macros: dict[str, str], verbose: bool = False) -> tuple[str, bool]

Process Python source code, expanding macros in docstrings.

Preserves complete original docstrings (with unexpanded macros) in comment blocks immediately before the expanded docstring.

PARAMETER DESCRIPTION
content

Python source code as a string

TYPE: str

macros

Dict mapping macro names to their expansion values

TYPE: dict[str, str]

verbose

Whether to print verbose output

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
tuple[str, bool]

Tuple of (processed_content, was_modified)

Source code in src/omnipy/util/docstr_macros.py
def process_content(  # noqa: C901
        content: str, macros: dict[str, str], verbose: bool = False) -> tuple[str, bool]:
    """
    Process Python source code, expanding macros in docstrings.

    Preserves complete original docstrings (with unexpanded macros) in
    comment blocks immediately before the expanded docstring.

    Args:
        content: Python source code as a string
        macros: Dict mapping macro names to their expansion values
        verbose: Whether to print verbose output

    Returns:
        Tuple of (processed_content, was_modified)
    """
    if verbose:
        print(f'Processing content with {len(macros)} macros available')

    # Pattern to match docstrings with optional preceding ORIGINAL_DOCSTRING comment block
    # Build the pattern with the marker prefix escaped properly
    escaped_prefix = re.escape(ORIGINAL_DOCSTRING_PREFIX)
    pattern = rf'''
        ^([\ \t]*)                             # Capture indentation at start of line
        (
            \#\ {escaped_prefix}\n             # Marker line
            ^(?:\1\#[^\n]*\n)*                 # Multiple comment lines with original docstring
            \1                                 # Same indentation before actual docstring
        )?
        ("""|\'\'\')                           # Opening quotes
        (.*?)                                  # Docstring content (non-greedy)
        \3                                     # Closing quotes (same as opening quotes)
    '''

    modified = False

    def _replace_docblock(match: re.Match[str]) -> str:
        """
        Replace a full docblock (comment + docstring), expanding any macros
        found.
        """
        nonlocal modified

        matched_docblock = match.group(0)  # Entire match (may include comment block)
        indent = match.group(1)  # Indentation (assuming same indentation for entire block)
        comment_block: str | None = match.group(2)  # Comment block if present, else None
        quote = match.group(3)  # Quote style (triple single or double)
        docstring_content = match.group(4)  # Just the docstring content

        if comment_block is not None:
            # Extract the original (unexpanded) docstring from comments
            original_docstring = _extract_original_docstring_from_comment_block(
                comment_block, verbose)
        else:
            # First time - the docstring text is the original
            original_docstring = docstring_content

        new_docblock, was_modified = _create_docblock_with_expansion(
            matched_docblock,
            original_docstring,
            indent,
            quote,
            macros,
            verbose,
        )

        if was_modified:
            modified = True

        return new_docblock

    # Apply the replacement
    new_content = re.sub(
        pattern, _replace_docblock, content, flags=re.VERBOSE | re.DOTALL | re.MULTILINE)

    return new_content, modified