texpkg 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {texpkg-0.2.1 → texpkg-0.2.2}/PKG-INFO +1 -1
- {texpkg-0.2.1 → texpkg-0.2.2}/pyproject.toml +1 -1
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/adopt.py +39 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/release.py +17 -2
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/declaration.py +34 -3
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/hook.py +4 -9
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_deck/__init__.py +5 -2
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_build.py +7 -4
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_declaration.py +9 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_embed.py +2 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_end_to_end.py +2 -2
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_profile.py +100 -2
- {texpkg-0.2.1 → texpkg-0.2.2}/LICENSE +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/README.md +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/classify.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/directory.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/envelope.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/export.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/handedits.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/history.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/payload.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/provenance_tex.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/snapshots.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/state.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/_internal/versions.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/assets.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/contracts.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/document.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/errors.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/layout.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/migration.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/protocol.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/provenance.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg/py.typed +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/checks.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/embed.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/generate.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/generated.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/isolate.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/_internal/snapshot.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/cli.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/py.typed +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/src/texpkg_build/testing.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/conftest.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/consumers/acme-brand-tex/pdm_build.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/consumers/acme-brand-tex/pyproject.toml +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/consumers/acme-deck-tex/pdm_build.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/consumers/acme-deck-tex/pyproject.toml +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/consumers/acme-series-tex/pdm_build.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/consumers/acme-series-tex/pyproject.toml +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/acme.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/assets/NOTICE.txt +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/assets/lmroman10-regular.otf +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/assets/lockup.pdf +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/README-expert.md +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/acme-brand-provenance.sty +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/acme-brand.sty +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/deck-compact.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/deck-wide.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/deck.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/expert-template.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/glossary.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/plan.json +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/state.json +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/model.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/other/lockup.pdf +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_brand/tokens.json +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_deck/assets/DECK.txt +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_deck/generated/brief.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_deck/model.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/_kinds/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/_kinds/long.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/_kinds/short.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/assets/lmroman10-regular.otf +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/model.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/series.json +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series/series.tex +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_series_gen/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_sheet/__init__.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/fixtures/acme_sheet/model.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/helpers.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/projects.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/spawn.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_compile.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_contracts.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_document.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_export.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_extras.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_generated.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_isolated.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_migration.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_module.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_payload.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_provenance.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_public_api.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/test_typing.py +0 -0
- {texpkg-0.2.1 → texpkg-0.2.2}/tests/venvs.py +0 -0
|
@@ -7,6 +7,7 @@ left aborts with the report.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import copy
|
|
10
|
+
import dataclasses
|
|
10
11
|
import re
|
|
11
12
|
from collections.abc import Iterator, Mapping, Sequence
|
|
12
13
|
from typing import Any, cast
|
|
@@ -110,6 +111,39 @@ def _extras_problems(kind: Kind, package: Package, data: dict[str, Any]) -> tupl
|
|
|
110
111
|
return extras, state.not_allowed(kind, extras)
|
|
111
112
|
|
|
112
113
|
|
|
114
|
+
def kind_default_changes(
|
|
115
|
+
package: Package,
|
|
116
|
+
extras: Extras,
|
|
117
|
+
saved: Mapping[str, Any],
|
|
118
|
+
current: Mapping[str, Any],
|
|
119
|
+
policy: DefaultChange | Mapping[str, DefaultChange],
|
|
120
|
+
) -> tuple[Extras, list[dict[str, Any]]]:
|
|
121
|
+
"""Apply the consumer's policy to every kind default this release changed (V22), for a value the document does
|
|
122
|
+
not set itself: following needs nothing (the kind writes the new default); a frozen one becomes a setting of the
|
|
123
|
+
document with the old value; one that asks is returned as unresolved. The key is the value's class name, the path
|
|
124
|
+
`/defaults/<class>`."""
|
|
125
|
+
own = {type(v).__name__ for v in extras.settings}
|
|
126
|
+
frozen: list[Any] = []
|
|
127
|
+
asked: list[dict[str, Any]] = []
|
|
128
|
+
for name in sorted(saved.keys() & current.keys()):
|
|
129
|
+
if saved[name] == current[name] or name in own:
|
|
130
|
+
continue
|
|
131
|
+
action = _policy(policy, f"/defaults/{name}", name)
|
|
132
|
+
if action is DefaultChange.FREEZE:
|
|
133
|
+
frozen.append(saved[name])
|
|
134
|
+
elif action is DefaultChange.ASK:
|
|
135
|
+
reason = f"the default of {name} changed from {saved[name]!r} to {current[name]!r}"
|
|
136
|
+
asked.append({"path": f"/defaults/{name}", "reason": reason, "value": saved[name], "boundary": "defaults"})
|
|
137
|
+
if not frozen:
|
|
138
|
+
return extras, asked
|
|
139
|
+
try:
|
|
140
|
+
kept = state.extras_from(package, {"settings": frozen}).settings
|
|
141
|
+
except ExceptionGroup as group:
|
|
142
|
+
reason = "; ".join(str(e) for e in group.exceptions)
|
|
143
|
+
return extras, [*asked, {"path": "/defaults", "reason": reason, "value": frozen, "boundary": "defaults"}]
|
|
144
|
+
return dataclasses.replace(extras, settings=(*extras.settings, *kept)), asked
|
|
145
|
+
|
|
146
|
+
|
|
113
147
|
def adopt(
|
|
114
148
|
*,
|
|
115
149
|
kind: Kind,
|
|
@@ -120,6 +154,8 @@ def adopt(
|
|
|
120
154
|
consumer: Sequence[Migration],
|
|
121
155
|
policy: DefaultChange | Mapping[str, DefaultChange],
|
|
122
156
|
saved_defaults: Mapping[str, Any] | None,
|
|
157
|
+
saved_kind_defaults: Mapping[str, Any] | None = None,
|
|
158
|
+
kind_defaults: Mapping[str, Any] | None = None,
|
|
123
159
|
base_data: Mapping[int, int] | None = None,
|
|
124
160
|
) -> tuple[Node, Extras]:
|
|
125
161
|
"""The tree and extras of `kind` that `envelope` describes at `data` version `current`, or a `MigrationError` naming
|
|
@@ -141,6 +177,9 @@ def adopt(
|
|
|
141
177
|
schema = state.tree_schema(kind.model, "validation")
|
|
142
178
|
extras, found = _extras_problems(kind, package, extras_data)
|
|
143
179
|
left += [{**p, "boundary": "validation"} for p in found]
|
|
180
|
+
if saved_kind_defaults is not None and not found:
|
|
181
|
+
extras, asked = kind_default_changes(package, extras, saved_kind_defaults, kind_defaults or {}, policy)
|
|
182
|
+
left += asked
|
|
144
183
|
left += [{**p, "boundary": "validation"} for p in unplaced(tree, schema)]
|
|
145
184
|
left += [{**p, "boundary": "validation"} for p in problems(kind.model, state.given(tree), schema)]
|
|
146
185
|
if left:
|
|
@@ -69,7 +69,19 @@ class Release:
|
|
|
69
69
|
"""What a document of `kind` is written with (L8): for a profile kind, the base kind's presets first, then
|
|
70
70
|
its own locks, then the variant's."""
|
|
71
71
|
base = self.dist.base_kind(kind)
|
|
72
|
-
return (*(base.presets if base else ()), *kind.settings(variant))
|
|
72
|
+
return (*(base.presets if base else ()), *kind.settings(variant), *self.kind_defaults(kind))
|
|
73
|
+
|
|
74
|
+
def kind_defaults(self, kind: Kind) -> tuple[AnyValue, ...]:
|
|
75
|
+
"""Where a document of `kind` starts for the values it may set: a profile's base kind's, then its own (V21)."""
|
|
76
|
+
base = self.dist.base_kind(kind)
|
|
77
|
+
return (*(base.defaults if base else ()), *kind.defaults)
|
|
78
|
+
|
|
79
|
+
def kind_defaults_state(self, kind: Kind) -> dict[str, Any]:
|
|
80
|
+
"""The settable ones among them, by class name, in the data shape of a setting: what the template state keeps
|
|
81
|
+
to tell, at `load`, a default the kind has changed."""
|
|
82
|
+
found = tuple(v for v in self.kind_defaults(kind) if type(v) in kind.settable)
|
|
83
|
+
data = state.extras_state(Extras(settings=found)).get("settings", [])
|
|
84
|
+
return json.loads(dumps({type(v).__name__: d for v, d in zip(found, data, strict=True)}))
|
|
73
85
|
|
|
74
86
|
def _has_graphics(self, kind: Kind) -> bool:
|
|
75
87
|
kinds = (kind.name, *([kind.over] if kind.over else []))
|
|
@@ -101,7 +113,7 @@ class Release:
|
|
|
101
113
|
return package.plan(
|
|
102
114
|
TexDocument(
|
|
103
115
|
body,
|
|
104
|
-
options=extras.options,
|
|
116
|
+
options=kind.class_options(extras.options, variant),
|
|
105
117
|
settings=(*self.settings(kind, variant), *extras.settings),
|
|
106
118
|
preamble=PreambleSlots(
|
|
107
119
|
before_class=(*found.before_class, *own.before_class),
|
|
@@ -163,6 +175,7 @@ class Release:
|
|
|
163
175
|
for variant, name in kind.documents()
|
|
164
176
|
},
|
|
165
177
|
"defaults": state.defaults_table(kind.model),
|
|
178
|
+
"kind_defaults": self.kind_defaults_state(kind),
|
|
166
179
|
"sty": state.sty_hashes(self.sty),
|
|
167
180
|
"stack": self.stack(),
|
|
168
181
|
}
|
|
@@ -300,6 +313,8 @@ class KindApi[T: Command | Environment]:
|
|
|
300
313
|
consumer=migrations,
|
|
301
314
|
policy=default_changes,
|
|
302
315
|
saved_defaults=template.get("defaults"),
|
|
316
|
+
saved_kind_defaults=template.get("kind_defaults"),
|
|
317
|
+
kind_defaults=self.release.kind_defaults_state(self.kind),
|
|
303
318
|
base_data=dict(self.release.contracts.base_data) if self.kind.name in self.release.dist.expose else None,
|
|
304
319
|
)
|
|
305
320
|
return Document(cast("T", tree), path, self.release, self.kind, extras)
|
|
@@ -36,10 +36,13 @@ type Model = type[Command] | type[Environment]
|
|
|
36
36
|
|
|
37
37
|
@dataclasses.dataclass(frozen=True)
|
|
38
38
|
class Variant:
|
|
39
|
-
"""A named set of settings applied to the same document (K6): `doc.render(variant="compact")`.
|
|
39
|
+
"""A named set of settings and class options applied to the same document (K6): `doc.render(variant="compact")`.
|
|
40
|
+
The options are written after the document's own, and need not be among the kind's `options`: the variant is the
|
|
41
|
+
author's choice, not the document's."""
|
|
40
42
|
|
|
41
43
|
name: str
|
|
42
44
|
settings: tuple[AnyValue, ...] = ()
|
|
45
|
+
options: tuple[str, ...] = ()
|
|
43
46
|
|
|
44
47
|
def __post_init__(self) -> None:
|
|
45
48
|
if not _VARIANT.fullmatch(self.name):
|
|
@@ -50,6 +53,18 @@ class Variant:
|
|
|
50
53
|
hint="it is part of a file name: deck-<variant>.tex",
|
|
51
54
|
)
|
|
52
55
|
object.__setattr__(self, "settings", tuple(self.settings))
|
|
56
|
+
object.__setattr__(self, "options", tuple(self.options))
|
|
57
|
+
if bad := next((o for o in self.options if not OPTION.fullmatch(o)), None):
|
|
58
|
+
raise DeclarationError(
|
|
59
|
+
f"variant {self.name}: option {bad!r} is not a class option",
|
|
60
|
+
code="TP-K016",
|
|
61
|
+
path=self.name,
|
|
62
|
+
hint="letters, digits, '=', '.' and '-', starting with a letter or digit",
|
|
63
|
+
)
|
|
64
|
+
if twice := next((o for o in self.options if self.options.count(o) > 1), None):
|
|
65
|
+
raise DeclarationError(
|
|
66
|
+
f"variant {self.name}: option {twice!r} is declared twice", code="TP-K002", path=self.name
|
|
67
|
+
)
|
|
53
68
|
|
|
54
69
|
|
|
55
70
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -105,7 +120,9 @@ class Kind:
|
|
|
105
120
|
|
|
106
121
|
A document may carry what the kind allows and nothing else: `settable` (values a document sets, after `presets`,
|
|
107
122
|
which every document is written with), `options` (class options) and `preamble` (entry classes). A value is a preset
|
|
108
|
-
or settable, not both. `
|
|
123
|
+
or settable, not both. `defaults` are the kind's own start values for settable ones: written after the presets and
|
|
124
|
+
the variant's settings and before the document's own, so a document that sets the value overrides them (V21).
|
|
125
|
+
`locked_preamble` holds entries every document is written with, in the slots given, after
|
|
109
126
|
the layout's lines and before the document's own: a document may not carry an entry class that is locked."""
|
|
110
127
|
|
|
111
128
|
name: str
|
|
@@ -122,9 +139,10 @@ class Kind:
|
|
|
122
139
|
options: tuple[str, ...] = ()
|
|
123
140
|
preamble: tuple[type[Entry], ...] = ()
|
|
124
141
|
locked_preamble: PreambleSlots = dataclasses.field(default_factory=PreambleSlots)
|
|
142
|
+
defaults: tuple[AnyValue, ...] = ()
|
|
125
143
|
|
|
126
144
|
def __post_init__(self) -> None:
|
|
127
|
-
for field in ("presets", "migrations", "allow_raw", "variants", "settable", "options", "preamble"):
|
|
145
|
+
for field in ("presets", "migrations", "allow_raw", "variants", "settable", "options", "preamble", "defaults"):
|
|
128
146
|
object.__setattr__(self, field, tuple(getattr(self, field)))
|
|
129
147
|
if not _KIND.fullmatch(self.name) or keyword.iskeyword(self.name):
|
|
130
148
|
raise DeclarationError(
|
|
@@ -192,6 +210,14 @@ class Kind:
|
|
|
192
210
|
path=self.name,
|
|
193
211
|
hint="a value is written for every document or set by a document, not both",
|
|
194
212
|
)
|
|
213
|
+
self._unique("default", [type(v).__name__ for v in self.defaults])
|
|
214
|
+
if loose := next((v for v in self.defaults if type(v) not in self.settable), None):
|
|
215
|
+
raise DeclarationError(
|
|
216
|
+
f"kind {self.name}: a default for {type(loose).__name__}, which is not settable",
|
|
217
|
+
code="TP-K021",
|
|
218
|
+
path=self.name,
|
|
219
|
+
hint="a default is where a document starts for a value it may set: add the class to settable",
|
|
220
|
+
)
|
|
195
221
|
object.__setattr__(self, "stem", self.stem or self.model.__name__.lower())
|
|
196
222
|
if not _STEM.fullmatch(self.stem):
|
|
197
223
|
raise DeclarationError(
|
|
@@ -239,6 +265,11 @@ class Kind:
|
|
|
239
265
|
"""The `.tex` a save writes, as `(variant, file name)`: the kind's own, then one per variant (K7)."""
|
|
240
266
|
return ((None, f"{self.stem}.tex"), *((v.name, f"{self.stem}-{v.name}.tex") for v in self.variants))
|
|
241
267
|
|
|
268
|
+
def class_options(self, own: tuple[str, ...], variant: str | None = None) -> tuple[str, ...]:
|
|
269
|
+
"""The class options of a `.tex` (K6): the document's `own`, then the variant's that are not among them."""
|
|
270
|
+
found = next((v for v in self.variants if v.name == variant), None)
|
|
271
|
+
return (*own, *(o for o in (found.options if found else ()) if o not in own))
|
|
272
|
+
|
|
242
273
|
def settings(self, variant: str | None = None) -> tuple[AnyValue, ...]:
|
|
243
274
|
"""What a document of the kind is written with (L8): the presets, then the settings of `variant`, if named."""
|
|
244
275
|
if variant is None:
|
|
@@ -36,22 +36,17 @@ def _pointer(context: Context) -> tuple[str, str]:
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def pdm_build_initialize(context: Context) -> None:
|
|
39
|
-
"""A wheel needs pydantic and nothing else: texmodel, texpkg and the bases are embedded in it (B7).
|
|
39
|
+
"""A wheel needs pydantic and nothing else: texmodel, texpkg and the bases are embedded in it (B7). An editable
|
|
40
|
+
install embeds nothing, so it needs texpkg itself, which brings texmodel and pydantic."""
|
|
40
41
|
metadata = context.config.metadata
|
|
41
|
-
metadata["dependencies"] = [PYDANTIC]
|
|
42
|
+
metadata["dependencies"] = ["texpkg"] if context.target == "editable" else [PYDANTIC]
|
|
42
43
|
if "dynamic" in metadata and "dependencies" in metadata["dynamic"]:
|
|
43
44
|
metadata["dynamic"].remove("dependencies")
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def pdm_build_update_files(context: Context, files: dict[str, pathlib.Path]) -> None:
|
|
47
|
-
if context.target == "sdist":
|
|
48
|
-
return
|
|
49
48
|
if context.target != "wheel":
|
|
50
|
-
|
|
51
|
-
f"a {context.target} build of a texpkg package",
|
|
52
|
-
code="TP-B008",
|
|
53
|
-
hint="the module is generated from the declaration: test it with texpkg_build.testing.load_declaration",
|
|
54
|
-
)
|
|
49
|
+
return # the sdist holds the sources, an editable install links them: the module is made for a wheel (B6)
|
|
55
50
|
module, attr = _pointer(context)
|
|
56
51
|
source = context.root / (context.config.build_config.package_dir or ".")
|
|
57
52
|
with author_source(source) as isolation:
|
|
@@ -7,9 +7,9 @@ import pathlib
|
|
|
7
7
|
import acme_brand_tex # pyright: ignore[reportMissingImports]
|
|
8
8
|
from acme_brand_tex._declaration.model import FactGap, Ink # pyright: ignore[reportMissingImports]
|
|
9
9
|
from texmodel.preamble import PassOption, PreambleSlots, RequireMetadata
|
|
10
|
-
from texmodel.types import Length
|
|
10
|
+
from texmodel.types import CMYK, Length
|
|
11
11
|
|
|
12
|
-
from texpkg import Asset, Distribution, Kind, Role
|
|
12
|
+
from texpkg import Asset, Distribution, Kind, Role, Variant
|
|
13
13
|
|
|
14
14
|
from .model import Brief, Heading, Headline, Point, Slide, Tint, expand, pkg
|
|
15
15
|
|
|
@@ -34,6 +34,9 @@ dist = Distribution(
|
|
|
34
34
|
presets=(),
|
|
35
35
|
example=slide_example,
|
|
36
36
|
preamble=(PassOption,),
|
|
37
|
+
settable=(Ink,),
|
|
38
|
+
defaults=(Ink(CMYK(0, 0, 0, 0.3)),), # where a slide starts; a slide may set its own
|
|
39
|
+
variants=(Variant("review", options=("draft",)),),
|
|
37
40
|
locked_preamble=PreambleSlots(before_class=(RequireMetadata(pdfversion="2.0"),)), # every slide has it
|
|
38
41
|
),
|
|
39
42
|
Kind(
|
|
@@ -179,11 +179,14 @@ def test_the_declaration_must_be_a_package(tmp_path):
|
|
|
179
179
|
assert raised.value.code == "TP-B004"
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def
|
|
182
|
+
def test_an_editable_install_links_the_sources_and_needs_texpkg(project, tmp_path, monkeypatch):
|
|
183
183
|
monkeypatch.chdir(project)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
assert
|
|
184
|
+
built = tmp_path / "dist" / pdm.backend.build_editable(str(tmp_path / "dist"))
|
|
185
|
+
names = projects.names(built)
|
|
186
|
+
assert not any("acme_brand_tex" in n and n.endswith(".py") for n in names) # no generated module
|
|
187
|
+
with zipfile.ZipFile(built) as z:
|
|
188
|
+
metadata = z.read(next(n for n in names if n.endswith("METADATA"))).decode()
|
|
189
|
+
assert "Requires-Dist: texpkg" in metadata and "pydantic" not in metadata
|
|
187
190
|
|
|
188
191
|
|
|
189
192
|
def test_a_generated_module_that_breaks_the_protocol_is_refused(tmp_path, monkeypatch):
|
|
@@ -272,6 +272,15 @@ def test_what_a_kind_lets_a_document_carry_is_checked(changes, code):
|
|
|
272
272
|
assert code_of(dataclasses.replace, dist.kind("expert"), **changes) == code
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
def test_a_variants_options_follow_the_documents_without_repeating_them():
|
|
276
|
+
expert = dist.kind("expert")
|
|
277
|
+
review = dataclasses.replace(expert, variants=(Variant("review", options=("draft", "a4paper")),))
|
|
278
|
+
assert review.class_options(("a4paper",), "review") == ("a4paper", "draft")
|
|
279
|
+
assert review.class_options(("a4paper",)) == ("a4paper",) and review.class_options((), "missing") == ()
|
|
280
|
+
assert code_of(Variant, "x", options=("two words",)) == "TP-K016"
|
|
281
|
+
assert code_of(Variant, "x", options=("draft", "draft")) == "TP-K002"
|
|
282
|
+
|
|
283
|
+
|
|
275
284
|
def test_a_kind_locks_entries_of_classes_it_does_not_also_allow():
|
|
276
285
|
locked = PreambleSlots(before_class=(PassOption("xcolor", "table"),), before_package=(PassOption("a", "b"),))
|
|
277
286
|
assert code_of(dataclasses.replace, dist.kind("expert"), locked_preamble=locked) == "TP-K020" # twice, in two slots
|
|
@@ -200,6 +200,7 @@ def test_a_wheel_installs_into_a_fresh_environment_that_has_only_pydantic(brand_
|
|
|
200
200
|
report = json.loads(venvs.run(python, SCRIPT, tmp_path / "out", cwd=tmp_path))
|
|
201
201
|
assert report["names"] == ["slide", "brief", "vocab"] and report["version"] == "0.1.0"
|
|
202
202
|
assert report["slide"] == [
|
|
203
|
+
"_texpkg/plan-review.json",
|
|
203
204
|
"_texpkg/plan.json",
|
|
204
205
|
"_texpkg/state.json",
|
|
205
206
|
"_texpkg/template.json",
|
|
@@ -210,6 +211,7 @@ def test_a_wheel_installs_into_a_fresh_environment_that_has_only_pydantic(brand_
|
|
|
210
211
|
"res/texpkg/acme-brand.sty",
|
|
211
212
|
"res/texpkg/acme-deck-provenance.sty",
|
|
212
213
|
"res/texpkg/acme-deck.sty",
|
|
214
|
+
"slide-review.tex",
|
|
213
215
|
"slide.tex",
|
|
214
216
|
]
|
|
215
217
|
assert "res/texpkg/acme-brand.sty" in report["vocab"] and "glossary.tex" in report["vocab"]
|
|
@@ -122,8 +122,8 @@ def test_two_wheels_are_installed_used_saved_compiled_and_a_new_release_is_adopt
|
|
|
122
122
|
]
|
|
123
123
|
sources = sorted((tmp_path / "docs").rglob("*.tex"))
|
|
124
124
|
assert (
|
|
125
|
-
len(sources) == 3 + 1 + 1 +
|
|
126
|
-
) # expert and its two variants, vocab, then the deck's brief, slide and vocab
|
|
125
|
+
len(sources) == 3 + 1 + 1 + 2 + 1
|
|
126
|
+
) # expert and its two variants, vocab, then the deck's brief, slide and its variant, and vocab
|
|
127
127
|
for source in sources:
|
|
128
128
|
compiler.compile(
|
|
129
129
|
source, tmp_path / "out" / source.parent.parent.name / source.parent.name / source.stem
|
|
@@ -15,7 +15,7 @@ from hypothesis import (
|
|
|
15
15
|
from texmodel.preamble import PassOption, PreambleSlots, RequireMetadata
|
|
16
16
|
from texmodel.types import CMYK
|
|
17
17
|
|
|
18
|
-
from texpkg import Design, Kind
|
|
18
|
+
from texpkg import DefaultChange, Design, Kind
|
|
19
19
|
from texpkg.errors import DeclarationError, MigrationError, NotAllowedError
|
|
20
20
|
from texpkg_build._internal import checks
|
|
21
21
|
from texpkg_build.testing import check_expansion, load_declaration
|
|
@@ -223,6 +223,15 @@ def test_a_kind_writes_its_locked_entries_before_the_documents_own(deck, tmp_pat
|
|
|
223
223
|
assert at == sorted(at) and at[1] < next(i for i, line in enumerate(lines) if "\\documentclass" in line)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def test_a_variant_writes_its_class_options_into_its_own_tex(deck, tmp_path):
|
|
227
|
+
slide = deck.kinds.modules()["slide"]
|
|
228
|
+
doc = slide.create(tmp_path, slide.example())
|
|
229
|
+
assert "\\documentclass{" in doc.render() and "\\documentclass[draft]{" in doc.render("review")
|
|
230
|
+
doc.save()
|
|
231
|
+
assert "\\documentclass[draft]{" in (tmp_path / "slide-review.tex").read_text()
|
|
232
|
+
assert "\\documentclass{" in (tmp_path / "slide.tex").read_text()
|
|
233
|
+
|
|
234
|
+
|
|
226
235
|
def test_a_document_may_not_carry_an_entry_class_the_kind_locks(deck, tmp_path):
|
|
227
236
|
slide = deck.kinds.modules()["slide"]
|
|
228
237
|
with pytest.raises(NotAllowedError) as raised:
|
|
@@ -261,7 +270,8 @@ def test_a_hand_edit_of_the_documents_own_entry_is_found_at_its_data_path(deck,
|
|
|
261
270
|
def test_a_document_with_a_locked_entry_compiles(deck, compiler, tmp_path):
|
|
262
271
|
slide = deck.kinds.modules()["slide"]
|
|
263
272
|
slide.create(tmp_path / "doc", slide.example(), preamble=OWN).save()
|
|
264
|
-
|
|
273
|
+
for name in ("slide", "slide-review"): # the variant passes `draft` to the class
|
|
274
|
+
compiler.compile(tmp_path / f"doc/{name}.tex", tmp_path / "out" / name).raise_for_status()
|
|
265
275
|
|
|
266
276
|
|
|
267
277
|
def locked_deck(deck, **changes): # noqa: ANN001, ANN003, ANN201
|
|
@@ -289,3 +299,91 @@ def test_a_profile_may_not_lock_or_allow_what_its_base_kind_locks(deck):
|
|
|
289
299
|
with pytest.raises(DeclarationError) as raised:
|
|
290
300
|
locked_deck(deck, preamble=(PassOption,))
|
|
291
301
|
assert raised.value.code == "TP-K020" and "locked by the base kind" in str(raised.value)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# -- a kind's own defaults for what a document may set (V21) -------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
OLD_INK = {"type": "Ink", "value": {"c": 0.0, "m": 0.0, "y": 0.0, "k": 0.9}}
|
|
307
|
+
GREY = "\\definecolor{ab-ink}{cmyk}{0,0,0,0.3}"
|
|
308
|
+
BLACK = "\\definecolor{ab-ink}{cmyk}{0,0,0,0.9}"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def slide_saved_with(deck, tmp_path, *, settings=(), old=OLD_INK): # noqa: ANN001, ANN002, ANN201
|
|
312
|
+
"""A slide document whose saved template says the kind's default for `Ink` used to be `old`."""
|
|
313
|
+
slide = deck.kinds.modules()["slide"]
|
|
314
|
+
slide.create(tmp_path, slide.example(), settings=settings).save()
|
|
315
|
+
path = tmp_path / "_texpkg/template.json"
|
|
316
|
+
template = json.loads(path.read_text())
|
|
317
|
+
assert template["kind_defaults"] == {"Ink": {"type": "Ink", "value": {"c": 0.0, "m": 0.0, "y": 0.0, "k": 0.3}}}
|
|
318
|
+
template["kind_defaults"]["Ink"] = old
|
|
319
|
+
path.write_text(json.dumps(template))
|
|
320
|
+
return slide
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_a_kinds_default_is_written_before_what_the_document_sets(deck, tmp_path):
|
|
324
|
+
slide = deck.kinds.modules()["slide"]
|
|
325
|
+
lines = slide.create(tmp_path / "a", slide.example()).render().splitlines()
|
|
326
|
+
assert GREY in lines
|
|
327
|
+
own = slide.create(tmp_path / "b", slide.example(), settings=[Ink(CMYK(0, 0, 0, 0.6))])
|
|
328
|
+
lines = own.render().splitlines()
|
|
329
|
+
assert lines.index(GREY) < lines.index(
|
|
330
|
+
"\\definecolor{ab-ink}{cmyk}{0,0,0,0.6}"
|
|
331
|
+
) # the document's comes last: it wins
|
|
332
|
+
own.save()
|
|
333
|
+
(tmp_path / "b/slide.tex").write_text((tmp_path / "b/slide.tex").read_text().replace("{0,0,0,0.6}", "{0,0,0,0.7}"))
|
|
334
|
+
assert {e.path for e in own.hand_edits()} == {"/settings/0"} # the default is not mistaken for it
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_a_changed_default_is_followed_frozen_or_asked_for_when_the_document_sets_nothing(deck, tmp_path):
|
|
338
|
+
slide = slide_saved_with(deck, tmp_path)
|
|
339
|
+
followed = slide.load(tmp_path)
|
|
340
|
+
assert followed.settings == () and GREY in followed.render()
|
|
341
|
+
frozen = slide.load(tmp_path, default_changes=DefaultChange.FREEZE)
|
|
342
|
+
assert [type(v) for v in frozen.settings] == [Ink]
|
|
343
|
+
lines = frozen.render().splitlines()
|
|
344
|
+
assert lines.index(GREY) < lines.index(BLACK) # the old value is written after the new default, so it wins
|
|
345
|
+
by_class = slide.load(tmp_path, default_changes={"Ink": DefaultChange.FREEZE})
|
|
346
|
+
assert [v.value for v in by_class.settings] == [v.value for v in frozen.settings]
|
|
347
|
+
with pytest.raises(MigrationError) as raised:
|
|
348
|
+
slide.load(tmp_path, default_changes=DefaultChange.ASK)
|
|
349
|
+
(item,) = raised.value.unresolved
|
|
350
|
+
assert item.path == "/defaults/Ink" and "changed from" in item.reason and item.boundary == "defaults"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_a_value_the_document_sets_is_not_a_changed_default(deck, tmp_path):
|
|
354
|
+
slide = slide_saved_with(deck, tmp_path, settings=[Ink(CMYK(0, 0, 0, 0.6))])
|
|
355
|
+
kept = slide.load(tmp_path, default_changes=DefaultChange.ASK)
|
|
356
|
+
assert BLACK not in kept.render() and "{0,0,0,0.6}" in kept.render()
|
|
357
|
+
again = slide.load(tmp_path, default_changes=DefaultChange.FREEZE)
|
|
358
|
+
assert [v.value for v in again.settings] == [v.value for v in kept.settings]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_a_default_that_did_not_change_or_was_not_saved_asks_nothing(deck, tmp_path):
|
|
362
|
+
slide = slide_saved_with(deck, tmp_path, old={"type": "Ink", "value": {"c": 0.0, "m": 0.0, "y": 0.0, "k": 0.3}})
|
|
363
|
+
assert slide.load(tmp_path, default_changes=DefaultChange.ASK).settings == ()
|
|
364
|
+
path = tmp_path / "_texpkg/template.json"
|
|
365
|
+
template = json.loads(path.read_text())
|
|
366
|
+
del template["kind_defaults"] # saved before the kind had a default
|
|
367
|
+
path.write_text(json.dumps(template))
|
|
368
|
+
assert slide.load(tmp_path, default_changes=DefaultChange.ASK).settings == ()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_a_frozen_default_the_class_no_longer_reads_is_unresolved(deck, tmp_path):
|
|
372
|
+
slide = slide_saved_with(deck, tmp_path, old={"type": "Ink", "value": "not a colour"})
|
|
373
|
+
with pytest.raises(MigrationError) as raised:
|
|
374
|
+
slide.load(tmp_path, default_changes=DefaultChange.FREEZE)
|
|
375
|
+
(item,) = raised.value.unresolved
|
|
376
|
+
assert item.path == "/defaults" and item.boundary == "defaults"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_a_default_is_for_a_settable_value_once(acme_brand_tex):
|
|
380
|
+
from acme_brand import Compact, Ink # noqa: PLC0415
|
|
381
|
+
|
|
382
|
+
expert = acme_brand.dist.kind("expert")
|
|
383
|
+
twice = (Ink(CMYK(0, 0, 0, 0.1)), Ink(CMYK(0, 0, 0, 0.2)))
|
|
384
|
+
with pytest.raises(DeclarationError) as raised:
|
|
385
|
+
dataclasses.replace(expert, defaults=twice)
|
|
386
|
+
assert raised.value.code == "TP-K002"
|
|
387
|
+
with pytest.raises(DeclarationError) as raised:
|
|
388
|
+
dataclasses.replace(expert, defaults=(Compact(True),))
|
|
389
|
+
assert raised.value.code == "TP-K021" and "Compact" in str(raised.value)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|