comicbox.config

[docs] package comicbox.config

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""Confuse config for comicbox."""

from argparse import Namespace
from collections.abc import Mapping
from pathlib import Path
from typing import Any

from confuse import Configuration
from confuse.templates import (
    Choice,
    Integer,
    MappingTemplate,
    OneOf,
    Optional,
    Sequence,
    String,
)

from comicbox._pdf import PAGE_FORMAT_VALUES
from comicbox.config.computed import compute_config
from comicbox.config.paths import (
    expand_glob_paths,
    post_process_set_for_path,
)
from comicbox.config.read import read_config_sources
from comicbox.config.settings import ComicboxSettings
from comicbox.version import PACKAGE_NAME

# Any non-Mapping container type — set/frozenset/tuple/list all pass.
# `_build_settings` normalizes the validated value into the right immutable
# type for the dataclass (frozenset for set-like fields, tuple for sequences),
# so the template intentionally doesn't pin element types here.
_NON_MAPPING_TYPES = (set, frozenset, tuple, list)
_NON_MAPPING_CONTAINER = OneOf(_NON_MAPPING_TYPES)

_TEMPLATE = MappingTemplate(
    {
        PACKAGE_NAME: MappingTemplate(
            {
                # Options
                "compute_pages": bool,
                "compute_page_count": bool,
                "config": Optional(OneOf((str, Path))),
                "delete_all_tags": bool,
                "delete_keys": Optional(_NON_MAPPING_CONTAINER),
                "delete_orig": bool,
                "dest_path": OneOf((str, Path)),
                "dry_run": bool,
                "loglevel": OneOf((String(), Integer())),
                "metadata": Optional(dict),
                "metadata_format": Optional(str),
                "metadata_cli": Optional(Sequence(str)),
                "pdf_page_format": Choice(("", *PAGE_FORMAT_VALUES)),
                "read": Optional(_NON_MAPPING_CONTAINER),
                "read_ignore": Optional(_NON_MAPPING_CONTAINER),
                "recurse": bool,
                "replace_metadata": bool,
                "stamp": bool,
                "stamp_notes": bool,
                "tagger": Optional(str),
                "theme": Optional(str),
                # Actions
                "cbz": Optional(bool),
                "covers": Optional(bool),
                "export": Optional(_NON_MAPPING_CONTAINER),
                "import_paths": Optional(Sequence(OneOf((str, Path)))),
                "index_from": Optional(int),
                "index_to": Optional(int),
                "print": Optional(OneOf((*_NON_MAPPING_TYPES, str))),
                "rename": Optional(bool),
                "validate": Optional(bool),
                "write": Optional(_NON_MAPPING_CONTAINER),
                # Targets
                "paths": Optional(OneOf((Sequence(OneOf((str, Path))), None))),
                # Computed
                "computed": Optional(
                    MappingTemplate(
                        {
                            "all_write_formats": frozenset,
                            "read_filename_formats": frozenset,
                            "read_file_formats": frozenset,
                            "read_metadata_lower_filenames": frozenset,
                            "is_read_comments": bool,
                            "is_skip_computed_from_tags": bool,
                        }
                    )
                ),
            }
        )
    }
)


def _build_settings(ad: Any) -> ComicboxSettings:
    """Convert a validated, computed confuse AttrDict into a ComicboxSettings dataclass."""
    inner: Any = ad.comicbox
    metadata_cli = inner.metadata_cli
    computed: Any = inner.computed
    return ComicboxSettings(
        # Options
        compute_pages=bool(inner.compute_pages),
        compute_page_count=bool(inner.compute_page_count),
        config=inner.config,
        delete_all_tags=bool(inner.delete_all_tags),
        delete_keys=frozenset(inner.delete_keys or ()),
        delete_orig=bool(inner.delete_orig),
        dest_path=inner.dest_path,
        dry_run=bool(inner.dry_run),
        loglevel=inner.loglevel,
        metadata=inner.metadata,
        metadata_format=inner.metadata_format,
        metadata_cli=tuple(metadata_cli) if metadata_cli else None,
        pdf_page_format=inner.pdf_page_format,
        read=frozenset(inner.read or ()),
        read_ignore=frozenset(inner.read_ignore) if inner.read_ignore else None,
        recurse=bool(inner.recurse),
        replace_metadata=bool(inner.replace_metadata),
        stamp=bool(inner.stamp),
        stamp_notes=bool(inner.stamp_notes),
        tagger=inner.tagger,
        theme=inner.theme,
        # Actions
        cbz=inner.cbz,
        covers=inner.covers,
        export=frozenset(inner.export or ()),
        import_paths=expand_glob_paths(inner.import_paths),
        index_from=inner.index_from,
        index_to=inner.index_to,
        print=frozenset(inner.print or ()),
        rename=inner.rename,
        validate=inner.validate,
        write=frozenset(inner.write or ()),
        # Targets
        paths=tuple(inner.paths or ()),
        # Computed
        all_write_formats=frozenset(computed.all_write_formats),
        read_filename_formats=frozenset(computed.read_filename_formats),
        read_file_formats=frozenset(computed.read_file_formats),
        read_metadata_lower_filenames=frozenset(computed.read_metadata_lower_filenames),
        is_read_comments=bool(computed.is_read_comments),
        is_skip_computed_from_tags=bool(computed.is_skip_computed_from_tags),
    )


def get_config(
    args: Namespace | Mapping | ComicboxSettings | None = None,
    *,
    modname: str = PACKAGE_NAME,
    path: str | Path | None = None,
    box: bool = False,
) -> ComicboxSettings:
    """
    Get the config dict, layering env and args over defaults.

    Setting the box arg to True reconfigures attributes based on path or no path.
    """
    if isinstance(args, ComicboxSettings):
        # Already a config
        return post_process_set_for_path(args, path, box=box)
    if isinstance(args, Mapping):
        args = dict(args)

    # Read Sources
    config = Configuration(PACKAGE_NAME, modname=modname, read=False)
    read_config_sources(config, args)

    # Compute
    config_program = config[PACKAGE_NAME]
    compute_config(config_program)

    ad = config.get(_TEMPLATE)
    settings = _build_settings(ad)
    return post_process_set_for_path(settings, path, box=box)