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)
|