Skip to content

Exporters

Exports go through the get_exporter factory, which returns a BaseExporter subclass for the requested format. Call its export method with an output path. The output of each format is described in Export Formats.

Registry

recursivist.exporters

Exporter registry.

Maps each supported export format to its exporter class and exposes get_exporter, the factory used to construct the right exporter for a requested format.

supported_formats

supported_formats() -> list[str]

Return the export format identifiers accepted by get_exporter.

This is the single source of truth for which formats are valid; callers (e.g. the CLI) should derive their validation from it rather than hard-coding a list.

Returns:

Type Description
list[str]

The supported format identifiers, sorted for stable presentation. Alias keys are

list[str]

included, so both "md" and "markdown" appear.

Source code in recursivist/exporters/__init__.py
def supported_formats() -> list[str]:
    """Return the export format identifiers accepted by
    [`get_exporter`][recursivist.exporters.get_exporter].

    This is the single source of truth for which formats are valid; callers (e.g. the
    CLI) should derive their validation from it rather than hard-coding a list.

    Returns:
        The supported format identifiers, sorted for stable presentation. Alias keys are
        included, so both ``"md"`` and ``"markdown"`` appear.
    """
    return sorted(_EXPORTERS)

canonical_extension

canonical_extension(format_type: str) -> str

Return the canonical output file extension for a format identifier.

The extension is read from the exporter class, so aliases that share an exporter collapse to a single extension.

Parameters:

Name Type Description Default
format_type str

Export format identifier (e.g. "json" or "markdown"). Matched case-insensitively.

required

Returns:

Type Description
str

The exporter's canonical file extension, without a leading dot (e.g. "md"

str

for both "md" and "markdown"). Falls back to the lowercased

str

format_type for unknown formats, mirroring

str

get_exporter's lookup.

Source code in recursivist/exporters/__init__.py
def canonical_extension(format_type: str) -> str:
    """Return the canonical output file extension for a format identifier.

    The extension is read from the exporter class, so aliases that share an exporter
    collapse to a single extension.

    Args:
        format_type: Export format identifier (e.g. ``"json"`` or ``"markdown"``).
            Matched case-insensitively.

    Returns:
        The exporter's canonical file extension, without a leading dot (e.g. ``"md"``
        for both ``"md"`` and ``"markdown"``). Falls back to the lowercased
        *format_type* for unknown formats, mirroring
        [`get_exporter`][recursivist.exporters.get_exporter]'s lookup.
    """
    exporter_class = _EXPORTERS.get(format_type.lower())
    if exporter_class is not None and exporter_class.extension:
        return exporter_class.extension
    return format_type.lower()

get_exporter

get_exporter(format_type: str, **kwargs: Any) -> BaseExporter

Construct the exporter for a given format.

Parameters:

Name Type Description Default
format_type str

Export format identifier (e.g. "json" or "txt"). Matched case-insensitively; "md" and "markdown" are equivalent.

required
**kwargs Any

Keyword arguments forwarded to the exporter's constructor (see BaseExporter).

{}

Returns:

Type Description
BaseExporter

A ready-to-use exporter instance; call its export method to write the output

BaseExporter

file.

Raises:

Type Description
ValueError

If format_type is not a supported format.

Source code in recursivist/exporters/__init__.py
def get_exporter(format_type: str, **kwargs: Any) -> BaseExporter:
    """Construct the exporter for a given format.

    Args:
        format_type: Export format identifier (e.g. ``"json"`` or ``"txt"``). Matched
            case-insensitively; ``"md"`` and ``"markdown"`` are equivalent.
        **kwargs: Keyword arguments forwarded to the exporter's constructor (see
            [`BaseExporter`][recursivist.exporters.base.BaseExporter]).

    Returns:
        A ready-to-use exporter instance; call its ``export`` method to write the output
        file.

    Raises:
        ValueError: If *format_type* is not a supported format.
    """
    exporter_class = _EXPORTERS.get(format_type.lower())
    if not exporter_class:
        raise ValueError(
            f"Unsupported export format: {format_type}. "
            f"Supported formats: {', '.join(_EXPORTERS.keys())}"
        )

    return exporter_class(**kwargs)

Base Exporter

recursivist.exporters.base

Shared base class for directory-structure exporters.

Defines BaseExporter, which stores the scanned structure and the resolved display options common to every output format. Concrete exporters subclass it and implement BaseExporter.export.

write_text is the single place export files are written: it makes undecodable file names encodable and writes atomically, so every format behaves the same way.

BaseExporter

Common base for the per-format exporters.

Holds the scanned structure and the resolved DisplayOptions; the actual output is produced by each subclass's export. For convenience, the individual pieces of the spec are also exposed as plain attributes (metrics, sort_key, show_loc/show_size/show_mtime/show_git_status) so exporters can read them directly.

Attributes:

Name Type Description
extension str

Canonical file extension for this format, without a leading dot (e.g. "md"). Set by each concrete subclass and used as the single source of truth for output filenames, so format aliases that share an exporter (such as "md" and "markdown") resolve to the same extension.

Source code in recursivist/exporters/base.py
class BaseExporter:
    """Common base for the per-format exporters.

    Holds the scanned structure and the resolved
    [`DisplayOptions`][recursivist.flags.DisplayOptions]; the actual output is produced
    by each subclass's [`export`][recursivist.exporters.base.BaseExporter.export]. For
    convenience, the individual pieces of the spec are also exposed as plain attributes
    (``metrics``, ``sort_key``,
    ``show_loc``/``show_size``/``show_mtime``/``show_git_status``) so exporters can read
    them directly.

    Attributes:
        extension: Canonical file extension for this format, without a leading dot (e.g.
            ``"md"``). Set by each concrete subclass and used as the single source of
            truth for output filenames, so format aliases that share an exporter (such
            as ``"md"`` and ``"markdown"``) resolve to the same extension.
    """

    extension: str = ""

    def __init__(
        self,
        structure: Directory,
        root_name: str,
        show_full_path: bool = False,
        spec: DisplayOptions | None = None,
        icon_style: str = "emoji",
    ) -> None:
        """Store the structure and display options for an export.

        Args:
            structure: Scanned directory structure to export.
            root_name: Display name of the root directory.
            show_full_path: Whether *structure* holds absolute paths (or GitHub blob
                URLs) to display instead of bare filenames.
            spec: Resolved sorting and annotation directives. Defaults to a plain
                [`DisplayOptions`][recursivist.flags.DisplayOptions] (no sorting, no
                annotations).
            icon_style: Icon style to use, either ``"emoji"`` or ``"nerd"``.
        """
        self.structure = structure
        self.root_name = root_name
        self.show_full_path = show_full_path
        self.spec = spec if spec is not None else DisplayOptions()
        self.metrics = self.spec.metrics
        self.sort_key = self.spec.sort_key
        self.show_loc = self.spec.show_loc
        self.show_size = self.spec.show_size
        self.show_mtime = self.spec.show_mtime
        self.show_git_status = self.spec.show_git_status
        self.icon_style = icon_style

    def export(self, output_path: str) -> None:
        """Write the export to *output_path*.

        Subclasses must override this method; the base implementation always raises
        `NotImplementedError`.
        """
        raise NotImplementedError("Subclasses must implement the export method.")

__init__

__init__(structure: Directory, root_name: str, show_full_path: bool = False, spec: DisplayOptions | None = None, icon_style: str = 'emoji') -> None

Store the structure and display options for an export.

Parameters:

Name Type Description Default
structure Directory

Scanned directory structure to export.

required
root_name str

Display name of the root directory.

required
show_full_path bool

Whether structure holds absolute paths (or GitHub blob URLs) to display instead of bare filenames.

False
spec DisplayOptions | None

Resolved sorting and annotation directives. Defaults to a plain DisplayOptions (no sorting, no annotations).

None
icon_style str

Icon style to use, either "emoji" or "nerd".

'emoji'
Source code in recursivist/exporters/base.py
def __init__(
    self,
    structure: Directory,
    root_name: str,
    show_full_path: bool = False,
    spec: DisplayOptions | None = None,
    icon_style: str = "emoji",
) -> None:
    """Store the structure and display options for an export.

    Args:
        structure: Scanned directory structure to export.
        root_name: Display name of the root directory.
        show_full_path: Whether *structure* holds absolute paths (or GitHub blob
            URLs) to display instead of bare filenames.
        spec: Resolved sorting and annotation directives. Defaults to a plain
            [`DisplayOptions`][recursivist.flags.DisplayOptions] (no sorting, no
            annotations).
        icon_style: Icon style to use, either ``"emoji"`` or ``"nerd"``.
    """
    self.structure = structure
    self.root_name = root_name
    self.show_full_path = show_full_path
    self.spec = spec if spec is not None else DisplayOptions()
    self.metrics = self.spec.metrics
    self.sort_key = self.spec.sort_key
    self.show_loc = self.spec.show_loc
    self.show_size = self.spec.show_size
    self.show_mtime = self.spec.show_mtime
    self.show_git_status = self.spec.show_git_status
    self.icon_style = icon_style

export

export(output_path: str) -> None

Write the export to output_path.

Subclasses must override this method; the base implementation always raises NotImplementedError.

Source code in recursivist/exporters/base.py
def export(self, output_path: str) -> None:
    """Write the export to *output_path*.

    Subclasses must override this method; the base implementation always raises
    `NotImplementedError`.
    """
    raise NotImplementedError("Subclasses must implement the export method.")

write_text

write_text(output_path: str, text: str) -> None

Atomically write text to output_path as UTF-8.

Lone surrogates, which stand in for the undecodable bytes of a non-UTF-8 file name, are replaced with U+FFFD so the text always encodes.

The text is written to a temporary file in the destination directory, which is then renamed over the destination. A failure at any point leaves an existing file untouched and creates no partial one. Symbolic links are followed and the permissions of a file being overwritten are kept, as with a plain open. A destination that exists but is not a regular file (such as /dev/stdout) cannot be replaced, so it is written to directly.

Parameters:

Name Type Description Default
output_path str

Path the file is written to.

required
text str

Content to write.

required

Raises:

Type Description
OSError

If the file cannot be written.

Source code in recursivist/exporters/base.py
def write_text(output_path: str, text: str) -> None:
    """Atomically write *text* to *output_path* as UTF-8.

    Lone surrogates, which stand in for the undecodable bytes of a non-UTF-8 file name,
    are replaced with U+FFFD so the text always encodes.

    The text is written to a temporary file in the destination directory, which is then
    renamed over the destination. A failure at any point leaves an existing file
    untouched and creates no partial one. Symbolic links are followed and the
    permissions of a file being overwritten are kept, as with a plain ``open``. A
    destination that exists but is not a regular file (such as ``/dev/stdout``) cannot
    be replaced, so it is written to directly.

    Args:
        output_path: Path the file is written to.
        text: Content to write.

    Raises:
        OSError: If the file cannot be written.
    """
    text = _SURROGATES.sub("\ufffd", text)
    target = os.path.realpath(output_path)
    if os.path.exists(target):
        if not os.path.isfile(target):
            with open(target, "w", encoding="utf-8") as f:
                f.write(text)
            return
        if not os.access(target, os.W_OK):
            raise PermissionError(errno.EACCES, os.strerror(errno.EACCES), output_path)

    temp_path = os.path.join(
        os.path.dirname(target), f".recursivist-{secrets.token_hex(8)}.tmp"
    )
    try:
        with open(temp_path, "x", encoding="utf-8") as f:
            f.write(text)
        with contextlib.suppress(OSError):
            shutil.copymode(target, temp_path)
        os.replace(temp_path, target)
    except BaseException:
        with contextlib.suppress(OSError):
            os.unlink(temp_path)
        raise