texpkg 0.2.0__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.0 → texpkg-0.2.2}/PKG-INFO +1 -1
- {texpkg-0.2.0 → texpkg-0.2.2}/pyproject.toml +1 -1
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/adopt.py +39 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/handedits.py +4 -3
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/release.py +31 -5
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/state.py +4 -1
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/declaration.py +75 -4
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/document.py +1 -1
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/generate.py +6 -4
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/generated.py +7 -3
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/hook.py +5 -10
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/testing.py +16 -8
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_deck/__init__.py +14 -3
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_build.py +7 -4
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_declaration.py +21 -1
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_embed.py +2 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_end_to_end.py +2 -2
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_generated.py +25 -0
- texpkg-0.2.2/tests/test_profile.py +389 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_typing.py +1 -1
- texpkg-0.2.0/tests/test_profile.py +0 -207
- {texpkg-0.2.0 → texpkg-0.2.2}/LICENSE +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/README.md +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/classify.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/directory.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/envelope.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/export.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/history.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/payload.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/provenance_tex.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/snapshots.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/_internal/versions.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/assets.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/contracts.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/errors.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/layout.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/migration.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/protocol.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/provenance.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg/py.typed +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/checks.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/embed.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/isolate.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/_internal/snapshot.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/cli.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/src/texpkg_build/py.typed +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/conftest.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/consumers/acme-brand-tex/pdm_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/consumers/acme-brand-tex/pyproject.toml +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/consumers/acme-deck-tex/pdm_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/consumers/acme-deck-tex/pyproject.toml +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/consumers/acme-series-tex/pdm_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/consumers/acme-series-tex/pyproject.toml +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/acme.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/assets/NOTICE.txt +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/assets/lmroman10-regular.otf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/assets/lockup.pdf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/README-expert.md +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/acme-brand-provenance.sty +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/acme-brand.sty +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/deck-compact.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/deck-wide.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/deck.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/expert-template.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/glossary.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/plan.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/generated/state.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/other/lockup.pdf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_brand/tokens.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_deck/assets/DECK.txt +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_deck/generated/brief.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_deck/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/_kinds/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/_kinds/long.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/_kinds/short.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/assets/lmroman10-regular.otf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/series.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series/series.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_series_gen/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_sheet/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/fixtures/acme_sheet/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/helpers.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/projects.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/spawn.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_compile.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_contracts.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_document.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_export.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_extras.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_isolated.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_migration.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_module.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_payload.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_provenance.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.2}/tests/test_public_api.py +0 -0
- {texpkg-0.2.0 → 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:
|
|
@@ -19,13 +19,14 @@ _SETTING = re.compile(r"settings\[(\d+)\]")
|
|
|
19
19
|
_ENTRY = re.compile(r"preamble\.(before_class|before_package|after_package)\[(\d+)\]")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def extra_path(plan_path: str, *, locked: int,
|
|
22
|
+
def extra_path(plan_path: str, *, locked: int, leading: PreambleSlots) -> str:
|
|
23
23
|
"""The data path of a line that a setting or a preamble entry of the document wrote (D10), or `""`: the kind's own
|
|
24
|
-
`locked` settings come first in the plan, then the document's, and the layout's
|
|
24
|
+
`locked` settings come first in the plan, then the document's, and the `leading` entries (the layout's and the
|
|
25
|
+
kind's locked ones) lead each preamble slot."""
|
|
25
26
|
if found := _SETTING.fullmatch(plan_path):
|
|
26
27
|
return f"/settings/{int(found[1]) - locked}" if int(found[1]) >= locked else ""
|
|
27
28
|
if found := _ENTRY.fullmatch(plan_path):
|
|
28
|
-
own = int(found[2]) - len(getattr(
|
|
29
|
+
own = int(found[2]) - len(getattr(leading, found[1]))
|
|
29
30
|
return f"/preamble/{found[1]}/{own}" if own >= 0 else ""
|
|
30
31
|
return ""
|
|
31
32
|
|
|
@@ -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 []))
|
|
@@ -79,21 +91,32 @@ class Release:
|
|
|
79
91
|
"""The lines that find the payload, which come before a document's own entries (A5)."""
|
|
80
92
|
return self.dist.layout.preamble(graphics=self._has_graphics(kind))
|
|
81
93
|
|
|
94
|
+
def fixed_preamble(self, kind: Kind) -> PreambleSlots:
|
|
95
|
+
"""What leads each preamble slot of every document of `kind`: the layout's lines, then the entries the base kind
|
|
96
|
+
locks (a profile's), then the kind's own (K020)."""
|
|
97
|
+
base = self.dist.base_kind(kind)
|
|
98
|
+
slots = [self.layout_preamble(kind), *([base.locked_preamble] if base else []), kind.locked_preamble]
|
|
99
|
+
return PreambleSlots(
|
|
100
|
+
before_class=tuple(e for s in slots for e in s.before_class),
|
|
101
|
+
before_package=tuple(e for s in slots for e in s.before_package),
|
|
102
|
+
after_package=tuple(e for s in slots for e in s.after_package),
|
|
103
|
+
)
|
|
104
|
+
|
|
82
105
|
def plan(self, kind: Kind, tree: Node, variant: str | None, extras: Extras = Extras()) -> Plan: # noqa: B008
|
|
83
106
|
"""The `.tex` of `tree` as lines with provenance: the kind's presets, then the variant's settings (L8), then the
|
|
84
|
-
document's own settings (D10).
|
|
107
|
+
document's own settings (D10); in each preamble slot the fixed entries, then the document's own.
|
|
85
108
|
|
|
86
109
|
An exposed base kind is written by the base's package; the directory's layout is this distribution's."""
|
|
87
110
|
body = tree if kind.expand is None else kind.expand(tree)
|
|
88
111
|
package = self.dist.owner(kind.name).package
|
|
89
|
-
found, own = self.
|
|
112
|
+
found, own = self.fixed_preamble(kind), extras.preamble
|
|
90
113
|
return package.plan(
|
|
91
114
|
TexDocument(
|
|
92
115
|
body,
|
|
93
|
-
options=extras.options,
|
|
116
|
+
options=kind.class_options(extras.options, variant),
|
|
94
117
|
settings=(*self.settings(kind, variant), *extras.settings),
|
|
95
118
|
preamble=PreambleSlots(
|
|
96
|
-
before_class=own.before_class,
|
|
119
|
+
before_class=(*found.before_class, *own.before_class),
|
|
97
120
|
before_package=(*found.before_package, *own.before_package),
|
|
98
121
|
after_package=(*found.after_package, *own.after_package),
|
|
99
122
|
),
|
|
@@ -152,6 +175,7 @@ class Release:
|
|
|
152
175
|
for variant, name in kind.documents()
|
|
153
176
|
},
|
|
154
177
|
"defaults": state.defaults_table(kind.model),
|
|
178
|
+
"kind_defaults": self.kind_defaults_state(kind),
|
|
155
179
|
"sty": state.sty_hashes(self.sty),
|
|
156
180
|
"stack": self.stack(),
|
|
157
181
|
}
|
|
@@ -289,6 +313,8 @@ class KindApi[T: Command | Environment]:
|
|
|
289
313
|
consumer=migrations,
|
|
290
314
|
policy=default_changes,
|
|
291
315
|
saved_defaults=template.get("defaults"),
|
|
316
|
+
saved_kind_defaults=template.get("kind_defaults"),
|
|
317
|
+
kind_defaults=self.release.kind_defaults_state(self.kind),
|
|
292
318
|
base_data=dict(self.release.contracts.base_data) if self.kind.name in self.release.dist.expose else None,
|
|
293
319
|
)
|
|
294
320
|
return Document(cast("T", tree), path, self.release, self.kind, extras)
|
|
@@ -84,6 +84,7 @@ def not_allowed(kind: Kind, extras: Extras) -> list[dict[str, Any]]:
|
|
|
84
84
|
data = extras_state(extras)
|
|
85
85
|
settable = {c.__name__ for c in kind.settable}
|
|
86
86
|
entries = {c.__name__ for c in kind.preamble}
|
|
87
|
+
locked = set(kind.locked_classes())
|
|
87
88
|
found = [
|
|
88
89
|
(f"/settings/{i}", f"{type(v).__name__} is not settable in kind {kind.name}", data["settings"][i])
|
|
89
90
|
for i, v in enumerate(extras.settings)
|
|
@@ -97,7 +98,9 @@ def not_allowed(kind: Kind, extras: Extras) -> list[dict[str, Any]]:
|
|
|
97
98
|
found += [
|
|
98
99
|
(
|
|
99
100
|
f"/preamble/{slot}/{i}",
|
|
100
|
-
f"{type(item).__name__} is
|
|
101
|
+
f"{type(item).__name__} is locked by kind {kind.name}"
|
|
102
|
+
if type(item).__name__ in locked
|
|
103
|
+
else f"{type(item).__name__} is not an allowed preamble entry in kind {kind.name}",
|
|
101
104
|
data["preamble"][slot][i],
|
|
102
105
|
)
|
|
103
106
|
for slot, i, item in extras.preamble.items()
|
|
@@ -19,7 +19,7 @@ from texmodel.components import AnyValue, Command, Environment, Node, Passthroug
|
|
|
19
19
|
from texmodel.document import OPTION
|
|
20
20
|
from texmodel.interface import Body, BodyKind
|
|
21
21
|
from texmodel.package import Package
|
|
22
|
-
from texmodel.preamble import Entry
|
|
22
|
+
from texmodel.preamble import Entry, PreambleSlots
|
|
23
23
|
|
|
24
24
|
from .assets import Asset, Resolved
|
|
25
25
|
from .errors import DeclarationError
|
|
@@ -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,10 @@ 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
|
|
126
|
+
the layout's lines and before the document's own: a document may not carry an entry class that is locked."""
|
|
109
127
|
|
|
110
128
|
name: str
|
|
111
129
|
model: Model
|
|
@@ -120,9 +138,11 @@ class Kind:
|
|
|
120
138
|
settable: tuple[type[Value[Any]], ...] = ()
|
|
121
139
|
options: tuple[str, ...] = ()
|
|
122
140
|
preamble: tuple[type[Entry], ...] = ()
|
|
141
|
+
locked_preamble: PreambleSlots = dataclasses.field(default_factory=PreambleSlots)
|
|
142
|
+
defaults: tuple[AnyValue, ...] = ()
|
|
123
143
|
|
|
124
144
|
def __post_init__(self) -> None:
|
|
125
|
-
for field in ("presets", "migrations", "allow_raw", "variants", "settable", "options", "preamble"):
|
|
145
|
+
for field in ("presets", "migrations", "allow_raw", "variants", "settable", "options", "preamble", "defaults"):
|
|
126
146
|
object.__setattr__(self, field, tuple(getattr(self, field)))
|
|
127
147
|
if not _KIND.fullmatch(self.name) or keyword.iskeyword(self.name):
|
|
128
148
|
raise DeclarationError(
|
|
@@ -173,6 +193,7 @@ class Kind:
|
|
|
173
193
|
"""What a document may carry (D10): no name twice, real class options, nothing both written and settable."""
|
|
174
194
|
self._unique("settable value", [c.__name__ for c in self.settable])
|
|
175
195
|
self._unique("preamble entry", [c.__name__ for c in self.preamble])
|
|
196
|
+
self._check_locked()
|
|
176
197
|
self._unique("option", list(self.options))
|
|
177
198
|
if bad := next((o for o in self.options if not OPTION.fullmatch(o)), None):
|
|
178
199
|
raise DeclarationError(
|
|
@@ -189,6 +210,14 @@ class Kind:
|
|
|
189
210
|
path=self.name,
|
|
190
211
|
hint="a value is written for every document or set by a document, not both",
|
|
191
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
|
+
)
|
|
192
221
|
object.__setattr__(self, "stem", self.stem or self.model.__name__.lower())
|
|
193
222
|
if not _STEM.fullmatch(self.stem):
|
|
194
223
|
raise DeclarationError(
|
|
@@ -198,6 +227,34 @@ class Kind:
|
|
|
198
227
|
hint="letters, digits, '_' and '-', e.g. stem='brand-deck' for brand-deck.tex",
|
|
199
228
|
)
|
|
200
229
|
|
|
230
|
+
def locked_classes(self) -> tuple[str, ...]:
|
|
231
|
+
"""The names of the entry classes the kind writes into every document."""
|
|
232
|
+
return tuple(type(item).__name__ for _, _, item in self.locked_preamble.items())
|
|
233
|
+
|
|
234
|
+
def _check_locked(self) -> None:
|
|
235
|
+
"""Locked entries are typed, in a slot they fit, each class once, and not also allowed to a document (K020)."""
|
|
236
|
+
for slot, _, item in self.locked_preamble.items():
|
|
237
|
+
slots = getattr(item, "slots", ())
|
|
238
|
+
if slot not in slots:
|
|
239
|
+
raise self.locked_error(
|
|
240
|
+
f"{type(item).__name__} cannot sit in {slot}" if slots else "is not a typed entry",
|
|
241
|
+
f"a typed entry (RequireMetadata, PdfInfo, ...) in a slot it fits: {list(slots) or 'none'}",
|
|
242
|
+
item,
|
|
243
|
+
)
|
|
244
|
+
names = self.locked_classes()
|
|
245
|
+
if twice := next((n for n in names if names.count(n) > 1), None):
|
|
246
|
+
raise self.locked_error("is locked twice", "lock each entry class once", twice)
|
|
247
|
+
if both := next((c for c in self.preamble if c.__name__ in names), None):
|
|
248
|
+
raise self.locked_error(
|
|
249
|
+
"is locked and also allowed to a document", "a class is locked or allowed, not both", both.__name__
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def locked_error(self, what: str, hint: str, entry: object) -> DeclarationError:
|
|
253
|
+
name = entry if isinstance(entry, str) else type(entry).__name__
|
|
254
|
+
return DeclarationError(
|
|
255
|
+
f"kind {self.name}: locked preamble entry {name} {what}", code="TP-K020", path=self.name, hint=hint
|
|
256
|
+
)
|
|
257
|
+
|
|
201
258
|
def _unique(self, what: str, names: list[str]) -> None:
|
|
202
259
|
if twice := next((n for n in names if names.count(n) > 1), None):
|
|
203
260
|
raise DeclarationError(
|
|
@@ -208,6 +265,11 @@ class Kind:
|
|
|
208
265
|
"""The `.tex` a save writes, as `(variant, file name)`: the kind's own, then one per variant (K7)."""
|
|
209
266
|
return ((None, f"{self.stem}.tex"), *((v.name, f"{self.stem}-{v.name}.tex") for v in self.variants))
|
|
210
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
|
+
|
|
211
273
|
def settings(self, variant: str | None = None) -> tuple[AnyValue, ...]:
|
|
212
274
|
"""What a document of the kind is written with (L8): the presets, then the settings of `variant`, if named."""
|
|
213
275
|
if variant is None:
|
|
@@ -347,6 +409,15 @@ class Distribution:
|
|
|
347
409
|
)
|
|
348
410
|
if problem := self.expansion_problem(kind, kind.example()):
|
|
349
411
|
raise self._bad_expansion(kind, problem)
|
|
412
|
+
base = offered[kind.over]
|
|
413
|
+
if clash := next((n for n in kind.locked_classes() if n in base.locked_classes()), None):
|
|
414
|
+
raise kind.locked_error(
|
|
415
|
+
"is locked by the base kind too", "the base kind's locked entries are written first", clash
|
|
416
|
+
)
|
|
417
|
+
if clash := next((c for c in kind.preamble if c.__name__ in base.locked_classes()), None):
|
|
418
|
+
raise kind.locked_error(
|
|
419
|
+
"is locked by the base kind", "a profile cannot allow what its base locks", clash.__name__
|
|
420
|
+
)
|
|
350
421
|
|
|
351
422
|
def expansion_problem(self, kind: Kind, tree: Node) -> str | None:
|
|
352
423
|
"""Why `tree` of a profile kind does not expand to a valid tree of the base kind (L3); `None` if it does."""
|
|
@@ -169,7 +169,7 @@ class Document[T: Command | Environment]:
|
|
|
169
169
|
own = handedits.extra_path(
|
|
170
170
|
plan_path,
|
|
171
171
|
locked=len(self._release.settings(self._kind, variant)),
|
|
172
|
-
|
|
172
|
+
leading=self._release.fixed_preamble(self._kind),
|
|
173
173
|
)
|
|
174
174
|
if own:
|
|
175
175
|
return own
|
|
@@ -155,9 +155,11 @@ def _tuple(items: list[str]) -> str:
|
|
|
155
155
|
return "(" + "".join(f"{item!r}, " for item in items).strip() + ")"
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def sources(dist: Distribution,
|
|
159
|
-
"""The generated files by path inside the module, for `dist` released at `version`. `
|
|
160
|
-
`[tool.texpkg] distribution
|
|
158
|
+
def sources(dist: Distribution, pointer: str, version: str, contracts: Contracts) -> dict[str, str]:
|
|
159
|
+
"""The generated files by path inside the module, for `dist` released at `version`. `pointer` is the
|
|
160
|
+
`module:attribute` of `[tool.texpkg] distribution`: where the declaring package holds the declaration."""
|
|
161
|
+
module, _, pointer_attr = pointer.partition(":")
|
|
162
|
+
where = module.removeprefix(dist.declared_in) # "" when the declaration is the package's `__init__`
|
|
161
163
|
offered = dist.kinds_offered()
|
|
162
164
|
names = [k.name for k in offered]
|
|
163
165
|
sty = render_sty(dist, version)
|
|
@@ -233,7 +235,7 @@ def sources(dist: Distribution, pointer_attr: str, version: str, contracts: Cont
|
|
|
233
235
|
|
|
234
236
|
from texpkg._internal.release import Release
|
|
235
237
|
|
|
236
|
-
from ._declaration import {pointer_attr}
|
|
238
|
+
from ._declaration{where} import {pointer_attr}
|
|
237
239
|
from ._version import CONTRACTS, VERSION
|
|
238
240
|
{base_import}
|
|
239
241
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
`[tool.texpkg] generate = "module:function"` names a pure `function(root) -> {path: source}`, where `root` is the
|
|
4
4
|
declaring package's directory and each path is relative to it. `texpkg-build generate` writes the files; the build runs
|
|
5
5
|
the function again and refuses a file that is missing, differs (stale or hand-edited), or is left over from an earlier
|
|
6
|
-
run. The function must not import
|
|
6
|
+
run. The function must not import a module that imports the files it produces, as the declaration does: keep the
|
|
7
|
+
declaration in a module of its own (`pkg.declaration:dist`) and `pkg/__init__.py` free of imports, and the function can
|
|
8
|
+
share the package's pure modules.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
import importlib
|
|
@@ -20,8 +22,10 @@ HEADER = "# Generated by texpkg-build generate. Do not edit.\n# fmt: off\n"
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def package_directory(source: pathlib.Path, module: str) -> pathlib.Path:
|
|
23
|
-
"""The directory of the declaring package
|
|
24
|
-
|
|
25
|
+
"""The directory of the declaring package, found without importing it: `module` itself when it is a package
|
|
26
|
+
(`pkg:dist`), else the package that holds it (`pkg.declaration:dist`), as `Distribution(declared_in)` is."""
|
|
27
|
+
path = source.joinpath(*module.split("."))
|
|
28
|
+
return path if path.is_dir() else path.parent
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def _function(pointer: str, isolation: Isolation) -> Callable[[pathlib.Path], Mapping[str, str]]:
|
|
@@ -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:
|
|
@@ -73,7 +68,7 @@ def pdm_build_update_files(context: Context, files: dict[str, pathlib.Path]) ->
|
|
|
73
68
|
loaded = load_declaration(f"{module}:{attr}", version=version)
|
|
74
69
|
checks.module_satisfies_the_protocols(loaded)
|
|
75
70
|
contracts, held = checks.contract_gate(context.root, dist, loaded._bound.RELEASE, version) # pyright: ignore[reportAttributeAccessIssue]
|
|
76
|
-
module_files = generate.sources(dist, attr, version, contracts)
|
|
71
|
+
module_files = generate.sources(dist, f"{module}:{attr}", version, contracts)
|
|
77
72
|
files.clear() # a wheel holds the generated module, not the author's sources
|
|
78
73
|
embed.embed(
|
|
79
74
|
context.build_dir / dist.module, dist, declaration, module_files, held
|
|
@@ -54,7 +54,7 @@ def load_declaration(pointer: str, *, version: str = "0.0.0", contracts: Contrac
|
|
|
54
54
|
root = pathlib.Path(scratch.name) / dist.module
|
|
55
55
|
for loaded in [n for n in sys.modules if _is_under(n, dist.module)]:
|
|
56
56
|
del sys.modules[loaded]
|
|
57
|
-
_write(dist,
|
|
57
|
+
_write(dist, pointer, version, contracts or Contracts(), root)
|
|
58
58
|
spec = importlib.util.spec_from_file_location(
|
|
59
59
|
dist.module, root / "__init__.py", submodule_search_locations=[str(root)]
|
|
60
60
|
)
|
|
@@ -102,16 +102,24 @@ class _Flatten(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
|
|
102
102
|
"""The module is the nested one, already running."""
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def
|
|
106
|
-
"""The
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
def _pointer_of(dist: Distribution) -> str:
|
|
106
|
+
"""The `module:attribute` a base's declaration is imported by: the module of its package that holds it."""
|
|
107
|
+
for name, module in sorted(sys.modules.items()):
|
|
108
|
+
if _is_under(name, dist.declared_in):
|
|
109
|
+
if attr := next((n for n, value in vars(module).items() if value is dist), None):
|
|
110
|
+
return f"{name}:{attr}"
|
|
111
|
+
raise DeclarationError(
|
|
112
|
+
f"{dist.name} is not held by a module of {dist.declared_in}",
|
|
113
|
+
code="TP-B003",
|
|
114
|
+
path=dist.name,
|
|
115
|
+
hint="declare the distribution at the top level of a module of its package",
|
|
116
|
+
)
|
|
109
117
|
|
|
110
118
|
|
|
111
|
-
def _write(dist: Distribution,
|
|
119
|
+
def _write(dist: Distribution, pointer: str, version: str, contracts: Contracts, root: pathlib.Path) -> None:
|
|
112
120
|
"""The generated module of `dist` in `root`, and in `root/_embedded/<base>` that of each base: embedding without
|
|
113
121
|
copying, since the declarations are the modules already imported."""
|
|
114
|
-
for path, text in generate.sources(dist,
|
|
122
|
+
for path, text in generate.sources(dist, pointer, version, contracts).items():
|
|
115
123
|
(root / path).parent.mkdir(parents=True, exist_ok=True)
|
|
116
124
|
(root / path).write_text(text, encoding="utf-8")
|
|
117
125
|
if dist.base:
|
|
@@ -119,7 +127,7 @@ def _write(dist: Distribution, attr: str, version: str, contracts: Contracts, ro
|
|
|
119
127
|
(root / "_embedded" / "__init__.py").write_text("", encoding="utf-8")
|
|
120
128
|
_write(
|
|
121
129
|
dist.base,
|
|
122
|
-
|
|
130
|
+
_pointer_of(dist.base),
|
|
123
131
|
dist.base.package.version,
|
|
124
132
|
contracts,
|
|
125
133
|
root / "_embedded" / dist.base.module,
|
|
@@ -6,9 +6,10 @@ import pathlib
|
|
|
6
6
|
# exported by its kinds).
|
|
7
7
|
import acme_brand_tex # pyright: ignore[reportMissingImports]
|
|
8
8
|
from acme_brand_tex._declaration.model import FactGap, Ink # pyright: ignore[reportMissingImports]
|
|
9
|
-
from texmodel.
|
|
9
|
+
from texmodel.preamble import PassOption, PreambleSlots, RequireMetadata
|
|
10
|
+
from texmodel.types import CMYK, Length
|
|
10
11
|
|
|
11
|
-
from texpkg import Asset, Distribution, Kind, Role
|
|
12
|
+
from texpkg import Asset, Distribution, Kind, Role, Variant
|
|
12
13
|
|
|
13
14
|
from .model import Brief, Heading, Headline, Point, Slide, Tint, expand, pkg
|
|
14
15
|
|
|
@@ -27,7 +28,17 @@ dist = Distribution(
|
|
|
27
28
|
package=pkg,
|
|
28
29
|
assets=(Asset("assets/DECK.txt", Role.NOTE, "what the deck package is"),),
|
|
29
30
|
kinds=(
|
|
30
|
-
Kind(
|
|
31
|
+
Kind(
|
|
32
|
+
"slide",
|
|
33
|
+
Slide,
|
|
34
|
+
presets=(),
|
|
35
|
+
example=slide_example,
|
|
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",)),),
|
|
40
|
+
locked_preamble=PreambleSlots(before_class=(RequireMetadata(pdfversion="2.0"),)), # every slide has it
|
|
41
|
+
),
|
|
31
42
|
Kind(
|
|
32
43
|
"brief",
|
|
33
44
|
Brief,
|
|
@@ -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):
|
|
@@ -10,7 +10,7 @@ import pytest
|
|
|
10
10
|
from acme_brand import Card, Compact, Deck, Fact, FactGap, Glossary, Ink, Mark, Term, Title, dist, expert_example
|
|
11
11
|
from texmodel.components import Command, Environment, Passthrough, Value
|
|
12
12
|
from texmodel.package import Package
|
|
13
|
-
from texmodel.preamble import PdfInfo
|
|
13
|
+
from texmodel.preamble import PassOption, PdfInfo, PreambleSlots, RequireMetadata
|
|
14
14
|
from texmodel.types import Length, Raw, Text
|
|
15
15
|
|
|
16
16
|
from texpkg import Asset, DefaultChange, Distribution, Kind, Migrated, Role, Variant, migration
|
|
@@ -262,12 +262,32 @@ def test_a_distribution_declared_in_a_zipped_package_checks_its_root_through_the
|
|
|
262
262
|
({"options": ("!",)}, "TP-K016"),
|
|
263
263
|
({"settable": (FactGap,)}, "TP-K017"), # a preset of the kind
|
|
264
264
|
({"settable": (Compact,)}, "TP-K017"), # a variant's setting
|
|
265
|
+
({"locked_preamble": PreambleSlots(before_class=(Raw(r"\\x"),))}, "TP-K020"), # not a typed entry
|
|
266
|
+
({"locked_preamble": PreambleSlots(before_package=(RequireMetadata(lang="en"),))}, "TP-K020"), # wrong slot
|
|
267
|
+
({"locked_preamble": PreambleSlots(before_class=(PassOption("a", "b"), PassOption("c", "d")))}, "TP-K020"),
|
|
268
|
+
({"locked_preamble": PreambleSlots(before_class=(RequireMetadata(lang="en"),))}, "TP-K020"), # also allowed
|
|
265
269
|
],
|
|
266
270
|
)
|
|
267
271
|
def test_what_a_kind_lets_a_document_carry_is_checked(changes, code):
|
|
268
272
|
assert code_of(dataclasses.replace, dist.kind("expert"), **changes) == code
|
|
269
273
|
|
|
270
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
|
+
|
|
284
|
+
def test_a_kind_locks_entries_of_classes_it_does_not_also_allow():
|
|
285
|
+
locked = PreambleSlots(before_class=(PassOption("xcolor", "table"),), before_package=(PassOption("a", "b"),))
|
|
286
|
+
assert code_of(dataclasses.replace, dist.kind("expert"), locked_preamble=locked) == "TP-K020" # twice, in two slots
|
|
287
|
+
kind = dataclasses.replace(dist.kind("expert"), locked_preamble=PreambleSlots(before_class=locked.before_class))
|
|
288
|
+
assert kind.locked_classes() == ("PassOption",) and dist.kind("expert").locked_classes() == ()
|
|
289
|
+
|
|
290
|
+
|
|
271
291
|
def test_a_settable_value_is_registered_by_the_package_that_reads_it_back():
|
|
272
292
|
other = Package("other", prefix="ot", version="1.0.0", date="2026/10/22", description="Another")
|
|
273
293
|
|
|
@@ -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
|