texmodel 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- texmodel/__init__.py +6 -0
- texmodel/_internal/__init__.py +1 -0
- texmodel/_internal/render.py +323 -0
- texmodel/_internal/scan.py +23 -0
- texmodel/body.py +364 -0
- texmodel/bundle.py +69 -0
- texmodel/cli.py +211 -0
- texmodel/components.py +757 -0
- texmodel/config.py +32 -0
- texmodel/drift.py +55 -0
- texmodel/errors.py +38 -0
- texmodel/package.py +482 -0
- texmodel/plan.py +92 -0
- texmodel/pointer.py +70 -0
- texmodel/preamble.py +110 -0
- texmodel/py.typed +0 -0
- texmodel/spec.py +235 -0
- texmodel/std/__init__.py +37 -0
- texmodel/std/adjustbox.py +19 -0
- texmodel/std/anyfontsize.py +7 -0
- texmodel/std/calc.py +7 -0
- texmodel/std/enumitem.py +18 -0
- texmodel/std/eso_pic.py +49 -0
- texmodel/std/etoolbox.py +127 -0
- texmodel/std/fontspec.py +36 -0
- texmodel/std/geometry.py +17 -0
- texmodel/std/graphicx.py +26 -0
- texmodel/std/hyphenat.py +7 -0
- texmodel/std/latex.py +516 -0
- texmodel/std/microtype.py +27 -0
- texmodel/std/pdfrender.py +17 -0
- texmodel/std/tcolorbox.py +43 -0
- texmodel/std/tikz.py +36 -0
- texmodel/std/xcolor.py +47 -0
- texmodel/testing.py +297 -0
- texmodel/types.py +264 -0
- texmodel-0.2.0.dist-info/METADATA +525 -0
- texmodel-0.2.0.dist-info/RECORD +61 -0
- texmodel-0.2.0.dist-info/WHEEL +4 -0
- texmodel-0.2.0.dist-info/entry_points.txt +9 -0
- texmodel-0.2.0.dist-info/licenses/LICENSE +21 -0
- texmodel_build/__init__.py +10 -0
- texmodel_build/_internal/__init__.py +1 -0
- texmodel_build/_internal/converge.py +19 -0
- texmodel_build/_internal/env.py +42 -0
- texmodel_build/_internal/logparse.py +170 -0
- texmodel_build/_internal/proofpdf.py +52 -0
- texmodel_build/_internal/spawn.py +36 -0
- texmodel_build/_internal/styscan.py +37 -0
- texmodel_build/cli.py +349 -0
- texmodel_build/compiler.py +341 -0
- texmodel_build/config.py +58 -0
- texmodel_build/diagnostics.py +225 -0
- texmodel_build/engines.py +35 -0
- texmodel_build/errors.py +17 -0
- texmodel_build/py.typed +0 -0
- texmodel_build/result.py +65 -0
- texmodel_build/roots.py +41 -0
- texmodel_build/targets.py +43 -0
- texmodel_build/testkit.py +59 -0
- texmodel_build/verify.py +158 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The hermetic environment a compile runs in (C3): only what TeX needs, nothing inherited by accident."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_env(
|
|
8
|
+
*,
|
|
9
|
+
roots: Sequence[str],
|
|
10
|
+
fonts: Sequence[str] = (),
|
|
11
|
+
epoch: int | None = None,
|
|
12
|
+
cache: str | None = None,
|
|
13
|
+
inherit: Sequence[str] = (),
|
|
14
|
+
) -> dict[str, str]:
|
|
15
|
+
"""`roots` are paths as the compiler sees them (container paths in a container).
|
|
16
|
+
|
|
17
|
+
A trailing separator in the search variables keeps the default search path after the roots.
|
|
18
|
+
"""
|
|
19
|
+
search = os.pathsep.join([".", *roots]) + os.pathsep
|
|
20
|
+
rooted = os.pathsep.join(roots) + os.pathsep if roots else ""
|
|
21
|
+
env = {
|
|
22
|
+
"TEXINPUTS": search,
|
|
23
|
+
"OPENTYPEFONTS": rooted,
|
|
24
|
+
"TTFONTS": rooted,
|
|
25
|
+
# Logs are parsed, so never hard-wrapped.
|
|
26
|
+
"max_print_line": "10000",
|
|
27
|
+
"error_line": "254",
|
|
28
|
+
"half_error_line": "238",
|
|
29
|
+
}
|
|
30
|
+
if rooted == "":
|
|
31
|
+
del env["OPENTYPEFONTS"], env["TTFONTS"]
|
|
32
|
+
if fonts:
|
|
33
|
+
env["OSFONTDIR"] = os.pathsep.join(fonts) # families a document names but never paths
|
|
34
|
+
if cache:
|
|
35
|
+
env["TEXMFVAR"] = env["TEXMFCACHE"] = cache
|
|
36
|
+
if epoch is not None:
|
|
37
|
+
env["SOURCE_DATE_EPOCH"] = str(epoch)
|
|
38
|
+
env["FORCE_SOURCE_DATE"] = "1"
|
|
39
|
+
for name in inherit:
|
|
40
|
+
if name in os.environ:
|
|
41
|
+
env[name] = os.environ[name]
|
|
42
|
+
return env
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Parse a LuaTeX/LaTeX log into typed diagnostics (C2).
|
|
2
|
+
|
|
3
|
+
The compile driver runs with `max_print_line` raised, so log lines are never hard-wrapped, and with
|
|
4
|
+
`-file-line-error`, so most errors read `file:line: message`; a few (a missing file) still read `! message`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
|
|
11
|
+
from ..diagnostics import (
|
|
12
|
+
Diagnostic,
|
|
13
|
+
DocumentRefusal,
|
|
14
|
+
Measure,
|
|
15
|
+
Measurement,
|
|
16
|
+
MissingCharacter,
|
|
17
|
+
MissingFile,
|
|
18
|
+
OverfullBox,
|
|
19
|
+
PackageError,
|
|
20
|
+
PackageWarning,
|
|
21
|
+
RerunNeeded,
|
|
22
|
+
TexError,
|
|
23
|
+
UndefinedControlSequence,
|
|
24
|
+
UndefinedReference,
|
|
25
|
+
overset,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_LOC = re.compile(r"^(?P<file>[^\s:][^:]*):(?P<line>\d+): (?P<msg>.*)$")
|
|
29
|
+
_BANG = re.compile(r"^! (?P<msg>.*)$")
|
|
30
|
+
_CONTEXT = re.compile(r"^l\.(?P<line>\d+) (?P<text>.*)$")
|
|
31
|
+
_CS = re.compile(r"(\\[A-Za-z@]+|\\.)\s*$")
|
|
32
|
+
_MISSING_CHAR = re.compile(r"^Missing character: There is no .+ \(U\+(?P<cp>[0-9A-Fa-f]+)\) in font (?P<font>.+)!$")
|
|
33
|
+
_OVERFULL = re.compile(r"^Overfull \\(?P<dir>[hv])box \((?P<amount>[\d.]+)pt too (?:wide|high)\)(?P<rest>.*)$")
|
|
34
|
+
_FIRST_LINE = re.compile(r"lines? (\d+)")
|
|
35
|
+
_RERUN = re.compile(r"Rerun to get|Rerun LaTeX|Please \(re\)run|rerunfilecheck Warning: File .* has changed")
|
|
36
|
+
_UNDEFINED_REF = re.compile(
|
|
37
|
+
r"LaTeX Warning: (?P<kind>Reference|Citation) `(?P<name>.+?)' on page \d+ undefined on input line (?P<line>\d+)"
|
|
38
|
+
)
|
|
39
|
+
_MISSING_FILE = re.compile(r"File `(?P<name>.+?)' not found")
|
|
40
|
+
_PACKAGE_ERROR = re.compile(r"Package (?P<package>\S+) Error:\s*(?P<rest>.*)$")
|
|
41
|
+
_PACKAGE_WARNING = re.compile(r"^Package (?P<package>\S+) Warning:\s*(?P<rest>.*)$")
|
|
42
|
+
_ON_LINE = re.compile(r"\s*on input line (?P<line>\d+)\.?$")
|
|
43
|
+
_PROBE = re.compile(r"^texmodel:probe:(?P<name>[^=\s]+)=(?P<value>.*)$")
|
|
44
|
+
_FIT_PROBE = re.compile(r"^fit\.(?P<item>.+)$")
|
|
45
|
+
_NEEDS_HAS = re.compile(r"^(?P<needs>[^/]+)/(?P<has>[^/]+)$")
|
|
46
|
+
_REFUSE = re.compile(r"^texmodel:refuse:(?P<reason>.*)$")
|
|
47
|
+
_PAGES = re.compile(r"Output written on .* \((?P<n>\d+) pages?, \d+ bytes\)")
|
|
48
|
+
# Consequences of an error already reported, not errors of their own.
|
|
49
|
+
_CONSEQUENCE = ("Emergency stop.", "==> Fatal error occurred")
|
|
50
|
+
_LOOKAHEAD = 15
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_log(text: str, measures: Sequence[Measure] = ()) -> tuple[Diagnostic, ...]:
|
|
54
|
+
"""Typed diagnostics; `measures` read a package's warnings as `Overset` (B2) besides the warning itself."""
|
|
55
|
+
lines = text.splitlines()
|
|
56
|
+
found: dict[tuple[object, ...], Diagnostic] = {}
|
|
57
|
+
|
|
58
|
+
def add(diagnostic: Diagnostic) -> None:
|
|
59
|
+
# The same finding repeats per pass and per occurrence; keep one with a count.
|
|
60
|
+
key = tuple(v for k, v in sorted(dataclasses.asdict(diagnostic).items()) if k != "count") + (type(diagnostic),)
|
|
61
|
+
seen = found.get(key)
|
|
62
|
+
found[key] = dataclasses.replace(seen, count=seen.count + 1) if seen else diagnostic
|
|
63
|
+
|
|
64
|
+
for i, line in enumerate(lines):
|
|
65
|
+
if m := _LOC.match(line):
|
|
66
|
+
_error(lines, i, m["msg"], m["file"], int(m["line"]), add)
|
|
67
|
+
elif m := _BANG.match(line):
|
|
68
|
+
_error(lines, i, m["msg"], None, _context(lines, i)[0], add)
|
|
69
|
+
else:
|
|
70
|
+
_note(lines, i, add)
|
|
71
|
+
for found_one in list(found.values()):
|
|
72
|
+
if isinstance(found_one, PackageWarning):
|
|
73
|
+
for rule in measures:
|
|
74
|
+
if rule.package == found_one.package and (m := rule.pattern.search(found_one.message)):
|
|
75
|
+
if over := overset(m["item"], m["needs"], m["has"], line=found_one.line):
|
|
76
|
+
add(over)
|
|
77
|
+
return tuple(found.values())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _note(lines: list[str], i: int, add: Callable[[Diagnostic], None]) -> None:
|
|
81
|
+
"""Lines that carry a finding without being an error message: missing glyphs, boxes, references, probes."""
|
|
82
|
+
line = lines[i]
|
|
83
|
+
if m := _MISSING_CHAR.match(line):
|
|
84
|
+
font = m["font"].strip('"')
|
|
85
|
+
cp = m["cp"].upper()
|
|
86
|
+
add(MissingCharacter(message=f"no glyph for U+{cp} in {font}", font=font, codepoint=int(cp, 16)))
|
|
87
|
+
elif m := _OVERFULL.match(line):
|
|
88
|
+
first = _FIRST_LINE.search(m["rest"])
|
|
89
|
+
add(
|
|
90
|
+
OverfullBox(
|
|
91
|
+
message=line, amount_pt=float(m["amount"]), direction=m["dir"], line=int(first[1]) if first else None
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
elif m := _UNDEFINED_REF.search(line):
|
|
95
|
+
message = f"{m['kind']} `{m['name']}' undefined"
|
|
96
|
+
add(UndefinedReference(message=message, kind=m["kind"], name=m["name"], line=int(m["line"])))
|
|
97
|
+
elif _RERUN.search(line):
|
|
98
|
+
add(RerunNeeded(message="rerun needed"))
|
|
99
|
+
elif m := _PROBE.match(line):
|
|
100
|
+
_probe(m, add)
|
|
101
|
+
elif m := _REFUSE.match(line):
|
|
102
|
+
add(DocumentRefusal(message=m["reason"]))
|
|
103
|
+
elif m := _PACKAGE_WARNING.match(line):
|
|
104
|
+
package = m["package"]
|
|
105
|
+
detail = [m["rest"].strip()]
|
|
106
|
+
for following in lines[i + 1 : i + 1 + _LOOKAHEAD]:
|
|
107
|
+
if not following.startswith(f"({package})"):
|
|
108
|
+
break
|
|
109
|
+
detail.append(following[len(package) + 2 :].strip())
|
|
110
|
+
message = " ".join(d for d in detail if d)
|
|
111
|
+
where = _ON_LINE.search(message)
|
|
112
|
+
add(
|
|
113
|
+
PackageWarning(
|
|
114
|
+
message=_ON_LINE.sub("", message), package=package, line=int(where["line"]) if where else None
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _probe(m: re.Match[str], add: Callable[[Diagnostic], None]) -> None:
|
|
120
|
+
"""A probe is a `Measurement`; `fit.<item>=<needs>/<has>` that does not fit is also an `Overset` (B2)."""
|
|
121
|
+
add(Measurement(message=m[0], name=m["name"], value=m["value"]))
|
|
122
|
+
if (item := _FIT_PROBE.match(m["name"])) and (fit := _NEEDS_HAS.match(m["value"])):
|
|
123
|
+
if over := overset(item["item"], fit["needs"], fit["has"]):
|
|
124
|
+
add(over)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def page_count(text: str) -> int | None:
|
|
128
|
+
m = _PAGES.search(text)
|
|
129
|
+
return int(m["n"]) if m else None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _context(lines: list[str], i: int) -> tuple[int | None, str]:
|
|
133
|
+
"""The `l.N <text>` line TeX prints after an error: the line number and the text up to the break."""
|
|
134
|
+
for line in lines[i + 1 : i + 1 + _LOOKAHEAD]:
|
|
135
|
+
if m := _CONTEXT.match(line):
|
|
136
|
+
return int(m["line"]), m["text"]
|
|
137
|
+
return None, ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _error(
|
|
141
|
+
lines: list[str], i: int, msg: str, file: str | None, line: int | None, add: Callable[[Diagnostic], None]
|
|
142
|
+
) -> None:
|
|
143
|
+
msg = msg.strip()
|
|
144
|
+
if msg.startswith(_CONSEQUENCE):
|
|
145
|
+
return
|
|
146
|
+
if msg.startswith("Undefined control sequence"):
|
|
147
|
+
top = lines[i + 1] if i + 1 < len(lines) else ""
|
|
148
|
+
cs = _CS.search(
|
|
149
|
+
top if top and not _CONTEXT.match(top) else _context(lines, i)[1]
|
|
150
|
+
) # `<argument> \name`, else `l.N`
|
|
151
|
+
add(
|
|
152
|
+
UndefinedControlSequence(
|
|
153
|
+
message=f"undefined control sequence {cs[1] if cs else '?'}",
|
|
154
|
+
name=cs[1] if cs else "",
|
|
155
|
+
file=file,
|
|
156
|
+
line=line,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
elif m := _MISSING_FILE.search(msg):
|
|
160
|
+
add(MissingFile(message=f"file `{m['name']}' not found", name=m["name"], file=file, line=line))
|
|
161
|
+
elif m := _PACKAGE_ERROR.search(msg):
|
|
162
|
+
package = m["package"]
|
|
163
|
+
detail = [m["rest"].strip()]
|
|
164
|
+
for following in lines[i + 1 : i + 1 + _LOOKAHEAD]:
|
|
165
|
+
if not following.startswith(f"({package})"):
|
|
166
|
+
break
|
|
167
|
+
detail.append(following[len(package) + 2 :].strip())
|
|
168
|
+
add(PackageError(message=" ".join(d for d in detail if d), package=package, file=file, line=line))
|
|
169
|
+
else:
|
|
170
|
+
add(TexError(message=msg, file=file, line=line))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The built-in verifier over proof-pdf (the `proof-pdf` extra): PDF/A, PDF/X-4, geometry, colour, proofing."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import pathlib
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..diagnostics import Severity
|
|
9
|
+
from ..verify import Finding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _profile(options: Mapping[str, Any]) -> Any:
|
|
13
|
+
from proof_pdf.models import PageGeometry, Profile
|
|
14
|
+
|
|
15
|
+
opts = dict(options)
|
|
16
|
+
if "geometry" in opts:
|
|
17
|
+
opts["geometry"] = PageGeometry(**opts["geometry"])
|
|
18
|
+
if "proof_profile" in opts:
|
|
19
|
+
opts["proof_profile"] = pathlib.Path(opts["proof_profile"])
|
|
20
|
+
if "allow_spots" in opts:
|
|
21
|
+
opts["allow_spots"] = tuple(opts["allow_spots"])
|
|
22
|
+
return Profile(**opts)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProofPdf:
|
|
26
|
+
"""Options are proof_pdf.models.Profile's fields (`geometry` as a table); a file is held to exactly that profile."""
|
|
27
|
+
|
|
28
|
+
name = "proof-pdf"
|
|
29
|
+
|
|
30
|
+
def unavailable(self, options: Mapping[str, Any]) -> str | None:
|
|
31
|
+
try:
|
|
32
|
+
from proof_pdf import core
|
|
33
|
+
except ImportError:
|
|
34
|
+
return "proof-pdf is not installed (pip install 'texmodel[proof-pdf]')"
|
|
35
|
+
try:
|
|
36
|
+
profile = _profile(options)
|
|
37
|
+
except (TypeError, ValueError) as exc:
|
|
38
|
+
return f"bad proof-pdf options: {exc}"
|
|
39
|
+
missing = core.missing_tools(profile)
|
|
40
|
+
return f"missing tools: {', '.join(missing)}" if missing else None
|
|
41
|
+
|
|
42
|
+
def verify(self, path: pathlib.Path, options: Mapping[str, Any]) -> Sequence[Finding]:
|
|
43
|
+
from proof_pdf import core, models
|
|
44
|
+
|
|
45
|
+
# ponytail: asyncio.run per file, so not callable inside a running loop; fine for the CLI and threads.
|
|
46
|
+
report = asyncio.run(core.verify_file(path, _profile(options)))
|
|
47
|
+
return [
|
|
48
|
+
Finding(
|
|
49
|
+
f.check, Severity.ERROR if f.severity is models.Severity.ERROR else Severity.WARNING, f.message, f.page
|
|
50
|
+
)
|
|
51
|
+
for f in report.findings
|
|
52
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""The one place a process starts (C3): argv lists only, never a shell string.
|
|
2
|
+
|
|
3
|
+
import-linter forbids importing `subprocess` anywhere else.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import dataclasses
|
|
7
|
+
import pathlib
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from ..errors import ToolchainError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass(frozen=True)
|
|
15
|
+
class Proc:
|
|
16
|
+
returncode: int
|
|
17
|
+
stdout: str
|
|
18
|
+
stderr: str
|
|
19
|
+
timed_out: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def text(data: str | bytes | None) -> str:
|
|
23
|
+
return data.decode(errors="replace") if isinstance(data, bytes) else (data or "")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def spawn(argv: Sequence[str], *, cwd: pathlib.Path | None, env: dict[str, str], timeout: float | None) -> Proc:
|
|
27
|
+
"""The one place a process starts."""
|
|
28
|
+
try:
|
|
29
|
+
done = subprocess.run( # noqa: S603
|
|
30
|
+
list(argv), cwd=cwd, env=env, capture_output=True, text=True, errors="replace", timeout=timeout, check=False
|
|
31
|
+
)
|
|
32
|
+
except subprocess.TimeoutExpired as exc:
|
|
33
|
+
return Proc(-1, text(exc.stdout), text(exc.stderr), timed_out=True)
|
|
34
|
+
except FileNotFoundError as exc:
|
|
35
|
+
raise ToolchainError(f"{argv[0]}: not found", code="TM-T001", hint="run `texmodel-build doctor`") from exc
|
|
36
|
+
return Proc(done.returncode, done.stdout, done.stderr)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""What a `.sty` or `.cls` file asks TeX to load (D2): the files `\\RequirePackage` and its kin name.
|
|
2
|
+
|
|
3
|
+
A scan, not an expansion: a name built from a macro (`\\RequirePackage{\\pkg}`) cannot be known and is reported as
|
|
4
|
+
unchecked, never guessed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
|
|
10
|
+
_COMMENT = re.compile(r"(?<!\\)%.*")
|
|
11
|
+
_LOAD = re.compile(
|
|
12
|
+
r"\\(?P<cmd>RequirePackage(?:WithOptions)?|usepackage|LoadClass(?:WithOptions)?|tcbuselibrary)"
|
|
13
|
+
r"\s*(?:\[[^\]]*\])?\s*\{(?P<names>[^}]*)\}"
|
|
14
|
+
)
|
|
15
|
+
SAFE_NAME = re.compile(r"[A-Za-z0-9._-]+") # also the only names the toolchain probe will take on a shell line
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _file(cmd: str, name: str) -> str:
|
|
19
|
+
if cmd == "tcbuselibrary":
|
|
20
|
+
return f"tcb{name}.code.tex"
|
|
21
|
+
return f"{name}.cls" if cmd.startswith("LoadClass") else f"{name}.sty"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def needed(texts: Iterable[str]) -> tuple[list[str], list[str]]:
|
|
25
|
+
"""The files named in `texts`, in order and once, and the names that are not plain file names (unchecked)."""
|
|
26
|
+
files: dict[str, None] = {}
|
|
27
|
+
unchecked: dict[str, None] = {}
|
|
28
|
+
for text in texts:
|
|
29
|
+
for m in _LOAD.finditer(_COMMENT.sub("", text)):
|
|
30
|
+
for name in (n.strip() for n in m["names"].split(",")):
|
|
31
|
+
if not name:
|
|
32
|
+
continue
|
|
33
|
+
if SAFE_NAME.fullmatch(name):
|
|
34
|
+
files[_file(m["cmd"], name)] = None
|
|
35
|
+
else:
|
|
36
|
+
unchecked[name] = None
|
|
37
|
+
return list(files), list(unchecked)
|