texpkg 0.2.0__tar.gz → 0.2.1__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.1}/PKG-INFO +1 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/pyproject.toml +1 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/handedits.py +4 -3
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/release.py +14 -3
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/state.py +4 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/declaration.py +42 -2
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/document.py +1 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/generate.py +6 -4
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/generated.py +7 -3
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/hook.py +1 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/testing.py +16 -8
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_deck/__init__.py +9 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_declaration.py +12 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_generated.py +25 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_profile.py +85 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_typing.py +1 -1
- {texpkg-0.2.0 → texpkg-0.2.1}/LICENSE +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/README.md +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/adopt.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/classify.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/directory.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/envelope.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/export.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/history.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/payload.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/provenance_tex.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/snapshots.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/_internal/versions.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/assets.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/contracts.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/errors.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/layout.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/migration.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/protocol.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/provenance.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg/py.typed +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/checks.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/embed.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/isolate.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/_internal/snapshot.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/cli.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/src/texpkg_build/py.typed +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/conftest.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/consumers/acme-brand-tex/pdm_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/consumers/acme-brand-tex/pyproject.toml +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/consumers/acme-deck-tex/pdm_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/consumers/acme-deck-tex/pyproject.toml +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/consumers/acme-series-tex/pdm_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/consumers/acme-series-tex/pyproject.toml +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/acme.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/assets/NOTICE.txt +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/assets/lmroman10-regular.otf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/assets/lockup.pdf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/README-expert.md +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/acme-brand-provenance.sty +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/acme-brand.sty +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/deck-compact.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/deck-wide.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/deck.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/expert-template.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/glossary.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/plan.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/generated/state.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/other/lockup.pdf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_brand/tokens.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_deck/assets/DECK.txt +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_deck/generated/brief.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_deck/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/_kinds/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/_kinds/long.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/_kinds/short.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/assets/lmroman10-regular.otf +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/series.json +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series/series.tex +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_series_gen/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_sheet/__init__.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/fixtures/acme_sheet/model.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/helpers.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/projects.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/spawn.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_build.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_compile.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_contracts.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_document.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_embed.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_end_to_end.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_export.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_extras.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_isolated.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_migration.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_module.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_payload.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_provenance.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/test_public_api.py +0 -0
- {texpkg-0.2.0 → texpkg-0.2.1}/tests/venvs.py +0 -0
|
@@ -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
|
|
|
@@ -79,21 +79,32 @@ class Release:
|
|
|
79
79
|
"""The lines that find the payload, which come before a document's own entries (A5)."""
|
|
80
80
|
return self.dist.layout.preamble(graphics=self._has_graphics(kind))
|
|
81
81
|
|
|
82
|
+
def fixed_preamble(self, kind: Kind) -> PreambleSlots:
|
|
83
|
+
"""What leads each preamble slot of every document of `kind`: the layout's lines, then the entries the base kind
|
|
84
|
+
locks (a profile's), then the kind's own (K020)."""
|
|
85
|
+
base = self.dist.base_kind(kind)
|
|
86
|
+
slots = [self.layout_preamble(kind), *([base.locked_preamble] if base else []), kind.locked_preamble]
|
|
87
|
+
return PreambleSlots(
|
|
88
|
+
before_class=tuple(e for s in slots for e in s.before_class),
|
|
89
|
+
before_package=tuple(e for s in slots for e in s.before_package),
|
|
90
|
+
after_package=tuple(e for s in slots for e in s.after_package),
|
|
91
|
+
)
|
|
92
|
+
|
|
82
93
|
def plan(self, kind: Kind, tree: Node, variant: str | None, extras: Extras = Extras()) -> Plan: # noqa: B008
|
|
83
94
|
"""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).
|
|
95
|
+
document's own settings (D10); in each preamble slot the fixed entries, then the document's own.
|
|
85
96
|
|
|
86
97
|
An exposed base kind is written by the base's package; the directory's layout is this distribution's."""
|
|
87
98
|
body = tree if kind.expand is None else kind.expand(tree)
|
|
88
99
|
package = self.dist.owner(kind.name).package
|
|
89
|
-
found, own = self.
|
|
100
|
+
found, own = self.fixed_preamble(kind), extras.preamble
|
|
90
101
|
return package.plan(
|
|
91
102
|
TexDocument(
|
|
92
103
|
body,
|
|
93
104
|
options=extras.options,
|
|
94
105
|
settings=(*self.settings(kind, variant), *extras.settings),
|
|
95
106
|
preamble=PreambleSlots(
|
|
96
|
-
before_class=own.before_class,
|
|
107
|
+
before_class=(*found.before_class, *own.before_class),
|
|
97
108
|
before_package=(*found.before_package, *own.before_package),
|
|
98
109
|
after_package=(*found.after_package, *own.after_package),
|
|
99
110
|
),
|
|
@@ -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
|
|
@@ -105,7 +105,8 @@ class Kind:
|
|
|
105
105
|
|
|
106
106
|
A document may carry what the kind allows and nothing else: `settable` (values a document sets, after `presets`,
|
|
107
107
|
which every document is written with), `options` (class options) and `preamble` (entry classes). A value is a preset
|
|
108
|
-
or settable, not both.
|
|
108
|
+
or settable, not both. `locked_preamble` holds entries every document is written with, in the slots given, after
|
|
109
|
+
the layout's lines and before the document's own: a document may not carry an entry class that is locked."""
|
|
109
110
|
|
|
110
111
|
name: str
|
|
111
112
|
model: Model
|
|
@@ -120,6 +121,7 @@ class Kind:
|
|
|
120
121
|
settable: tuple[type[Value[Any]], ...] = ()
|
|
121
122
|
options: tuple[str, ...] = ()
|
|
122
123
|
preamble: tuple[type[Entry], ...] = ()
|
|
124
|
+
locked_preamble: PreambleSlots = dataclasses.field(default_factory=PreambleSlots)
|
|
123
125
|
|
|
124
126
|
def __post_init__(self) -> None:
|
|
125
127
|
for field in ("presets", "migrations", "allow_raw", "variants", "settable", "options", "preamble"):
|
|
@@ -173,6 +175,7 @@ class Kind:
|
|
|
173
175
|
"""What a document may carry (D10): no name twice, real class options, nothing both written and settable."""
|
|
174
176
|
self._unique("settable value", [c.__name__ for c in self.settable])
|
|
175
177
|
self._unique("preamble entry", [c.__name__ for c in self.preamble])
|
|
178
|
+
self._check_locked()
|
|
176
179
|
self._unique("option", list(self.options))
|
|
177
180
|
if bad := next((o for o in self.options if not OPTION.fullmatch(o)), None):
|
|
178
181
|
raise DeclarationError(
|
|
@@ -198,6 +201,34 @@ class Kind:
|
|
|
198
201
|
hint="letters, digits, '_' and '-', e.g. stem='brand-deck' for brand-deck.tex",
|
|
199
202
|
)
|
|
200
203
|
|
|
204
|
+
def locked_classes(self) -> tuple[str, ...]:
|
|
205
|
+
"""The names of the entry classes the kind writes into every document."""
|
|
206
|
+
return tuple(type(item).__name__ for _, _, item in self.locked_preamble.items())
|
|
207
|
+
|
|
208
|
+
def _check_locked(self) -> None:
|
|
209
|
+
"""Locked entries are typed, in a slot they fit, each class once, and not also allowed to a document (K020)."""
|
|
210
|
+
for slot, _, item in self.locked_preamble.items():
|
|
211
|
+
slots = getattr(item, "slots", ())
|
|
212
|
+
if slot not in slots:
|
|
213
|
+
raise self.locked_error(
|
|
214
|
+
f"{type(item).__name__} cannot sit in {slot}" if slots else "is not a typed entry",
|
|
215
|
+
f"a typed entry (RequireMetadata, PdfInfo, ...) in a slot it fits: {list(slots) or 'none'}",
|
|
216
|
+
item,
|
|
217
|
+
)
|
|
218
|
+
names = self.locked_classes()
|
|
219
|
+
if twice := next((n for n in names if names.count(n) > 1), None):
|
|
220
|
+
raise self.locked_error("is locked twice", "lock each entry class once", twice)
|
|
221
|
+
if both := next((c for c in self.preamble if c.__name__ in names), None):
|
|
222
|
+
raise self.locked_error(
|
|
223
|
+
"is locked and also allowed to a document", "a class is locked or allowed, not both", both.__name__
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def locked_error(self, what: str, hint: str, entry: object) -> DeclarationError:
|
|
227
|
+
name = entry if isinstance(entry, str) else type(entry).__name__
|
|
228
|
+
return DeclarationError(
|
|
229
|
+
f"kind {self.name}: locked preamble entry {name} {what}", code="TP-K020", path=self.name, hint=hint
|
|
230
|
+
)
|
|
231
|
+
|
|
201
232
|
def _unique(self, what: str, names: list[str]) -> None:
|
|
202
233
|
if twice := next((n for n in names if names.count(n) > 1), None):
|
|
203
234
|
raise DeclarationError(
|
|
@@ -347,6 +378,15 @@ class Distribution:
|
|
|
347
378
|
)
|
|
348
379
|
if problem := self.expansion_problem(kind, kind.example()):
|
|
349
380
|
raise self._bad_expansion(kind, problem)
|
|
381
|
+
base = offered[kind.over]
|
|
382
|
+
if clash := next((n for n in kind.locked_classes() if n in base.locked_classes()), None):
|
|
383
|
+
raise kind.locked_error(
|
|
384
|
+
"is locked by the base kind too", "the base kind's locked entries are written first", clash
|
|
385
|
+
)
|
|
386
|
+
if clash := next((c for c in kind.preamble if c.__name__ in base.locked_classes()), None):
|
|
387
|
+
raise kind.locked_error(
|
|
388
|
+
"is locked by the base kind", "a profile cannot allow what its base locks", clash.__name__
|
|
389
|
+
)
|
|
350
390
|
|
|
351
391
|
def expansion_problem(self, kind: Kind, tree: Node) -> str | None:
|
|
352
392
|
"""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]]:
|
|
@@ -73,7 +73,7 @@ def pdm_build_update_files(context: Context, files: dict[str, pathlib.Path]) ->
|
|
|
73
73
|
loaded = load_declaration(f"{module}:{attr}", version=version)
|
|
74
74
|
checks.module_satisfies_the_protocols(loaded)
|
|
75
75
|
contracts, held = checks.contract_gate(context.root, dist, loaded._bound.RELEASE, version) # pyright: ignore[reportAttributeAccessIssue]
|
|
76
|
-
module_files = generate.sources(dist, attr, version, contracts)
|
|
76
|
+
module_files = generate.sources(dist, f"{module}:{attr}", version, contracts)
|
|
77
77
|
files.clear() # a wheel holds the generated module, not the author's sources
|
|
78
78
|
embed.embed(
|
|
79
79
|
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,6 +6,7 @@ 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.preamble import PassOption, PreambleSlots, RequireMetadata
|
|
9
10
|
from texmodel.types import Length
|
|
10
11
|
|
|
11
12
|
from texpkg import Asset, Distribution, Kind, Role
|
|
@@ -27,7 +28,14 @@ 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
|
+
locked_preamble=PreambleSlots(before_class=(RequireMetadata(pdfversion="2.0"),)), # every slide has it
|
|
38
|
+
),
|
|
31
39
|
Kind(
|
|
32
40
|
"brief",
|
|
33
41
|
Brief,
|
|
@@ -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,23 @@ 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_kind_locks_entries_of_classes_it_does_not_also_allow():
|
|
276
|
+
locked = PreambleSlots(before_class=(PassOption("xcolor", "table"),), before_package=(PassOption("a", "b"),))
|
|
277
|
+
assert code_of(dataclasses.replace, dist.kind("expert"), locked_preamble=locked) == "TP-K020" # twice, in two slots
|
|
278
|
+
kind = dataclasses.replace(dist.kind("expert"), locked_preamble=PreambleSlots(before_class=locked.before_class))
|
|
279
|
+
assert kind.locked_classes() == ("PassOption",) and dist.kind("expert").locked_classes() == ()
|
|
280
|
+
|
|
281
|
+
|
|
271
282
|
def test_a_settable_value_is_registered_by_the_package_that_reads_it_back():
|
|
272
283
|
other = Package("other", prefix="ot", version="1.0.0", date="2026/10/22", description="Another")
|
|
273
284
|
|
|
@@ -165,6 +165,31 @@ def test_generate_check_reports_drift_and_generate_repairs_it(project):
|
|
|
165
165
|
assert ".times(1, 4)" in (project / "src/acme_series/_kinds/short.py").read_text()
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def test_the_declaration_may_be_a_module_of_its_package_and_the_package_import_free(project, tmp_path):
|
|
169
|
+
package = project / "src/acme_series"
|
|
170
|
+
(package / "declaration.py").write_text((package / "__init__.py").read_text())
|
|
171
|
+
(package / "__init__.py").write_text("")
|
|
172
|
+
pyproject = project / "pyproject.toml"
|
|
173
|
+
pyproject.write_text(pyproject.read_text().replace("acme_series:dist", "acme_series.declaration:dist"))
|
|
174
|
+
assert run_cli(project, "--check").exit_code == 0
|
|
175
|
+
change_data(project)
|
|
176
|
+
assert run_cli(project, "--check").exit_code == 1 and run_cli(project).exit_code == 0
|
|
177
|
+
assert "acme_series_tex/kinds/short.py" in projects.names(projects.wheel(project, tmp_path / "dist"))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_a_base_no_module_holds_is_refused_by_name(tmp_path):
|
|
181
|
+
import dataclasses # noqa: PLC0415
|
|
182
|
+
|
|
183
|
+
import acme_brand # noqa: PLC0415
|
|
184
|
+
|
|
185
|
+
from texpkg_build import testing # noqa: PLC0415
|
|
186
|
+
|
|
187
|
+
copy = dataclasses.replace(acme_brand.dist) # equal, but no module holds this one
|
|
188
|
+
with pytest.raises(DeclarationError) as raised:
|
|
189
|
+
testing._pointer_of(copy) # noqa: SLF001
|
|
190
|
+
assert raised.value.code == "TP-B003" and "acme-brand-tex" in str(raised.value)
|
|
191
|
+
|
|
192
|
+
|
|
168
193
|
def test_generate_reports_a_bad_pointer_and_a_failing_generator(project):
|
|
169
194
|
pyproject = project / "pyproject.toml"
|
|
170
195
|
pyproject.write_text(pyproject.read_text().replace('generate = "acme_series_gen:sources"', 'generate = "nothing"'))
|
|
@@ -12,10 +12,11 @@ from hypothesis import (
|
|
|
12
12
|
given,
|
|
13
13
|
strategies as st,
|
|
14
14
|
)
|
|
15
|
+
from texmodel.preamble import PassOption, PreambleSlots, RequireMetadata
|
|
15
16
|
from texmodel.types import CMYK
|
|
16
17
|
|
|
17
18
|
from texpkg import Design, Kind
|
|
18
|
-
from texpkg.errors import DeclarationError
|
|
19
|
+
from texpkg.errors import DeclarationError, MigrationError, NotAllowedError
|
|
19
20
|
from texpkg_build._internal import checks
|
|
20
21
|
from texpkg_build.testing import check_expansion, load_declaration
|
|
21
22
|
|
|
@@ -205,3 +206,86 @@ def test_a_profile_kind_is_declared_whole(deck):
|
|
|
205
206
|
with pytest.raises(DeclarationError) as raised:
|
|
206
207
|
dataclasses.replace(deck_dist, kinds=(dataclasses.replace(brief, **changes),))
|
|
207
208
|
assert raised.value.code == code
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# -- locked preamble entries (K020) ----------------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
LOCK = PreambleSlots(before_class=(RequireMetadata(lang="en"),))
|
|
214
|
+
BASE_LOCK = PreambleSlots(before_class=(PassOption("graphicx", "draft"),)) # what a base kind locks
|
|
215
|
+
OWN = PreambleSlots(before_class=(PassOption("xcolor", "table"),))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_a_kind_writes_its_locked_entries_before_the_documents_own(deck, tmp_path):
|
|
219
|
+
slide = deck.kinds.modules()["slide"]
|
|
220
|
+
lines = slide.create(tmp_path, slide.example(), preamble=OWN).render().splitlines()
|
|
221
|
+
wanted = ["\\DocumentMetadata{pdfversion=2.0}", "\\PassOptionsToPackage{table}{xcolor}"]
|
|
222
|
+
at = [lines.index(line) for line in wanted]
|
|
223
|
+
assert at == sorted(at) and at[1] < next(i for i, line in enumerate(lines) if "\\documentclass" in line)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_a_document_may_not_carry_an_entry_class_the_kind_locks(deck, tmp_path):
|
|
227
|
+
slide = deck.kinds.modules()["slide"]
|
|
228
|
+
with pytest.raises(NotAllowedError) as raised:
|
|
229
|
+
slide.create(tmp_path, slide.example(), preamble=LOCK)
|
|
230
|
+
(item,) = raised.value.items
|
|
231
|
+
assert item["path"] == "/preamble/before_class/0" and "locked by kind slide" in item["reason"]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_locked_entries_are_in_no_documents_data_and_a_stored_one_is_unresolved(deck, tmp_path):
|
|
235
|
+
slide = deck.kinds.modules()["slide"]
|
|
236
|
+
slide.create(tmp_path, slide.example(), preamble=OWN).save()
|
|
237
|
+
path = tmp_path / "_texpkg/state.json"
|
|
238
|
+
stored = json.loads(path.read_text())
|
|
239
|
+
assert [e["type"] for e in stored["preamble"]["before_class"]] == ["PassOption"] # not the lock
|
|
240
|
+
stored["preamble"]["before_class"].append({"type": "RequireMetadata", "entries": {"lang": "en"}})
|
|
241
|
+
path.write_text(json.dumps(stored))
|
|
242
|
+
with pytest.raises(MigrationError) as raised:
|
|
243
|
+
slide.load(tmp_path)
|
|
244
|
+
assert raised.value.code == "TP-M003" and "locked by kind slide" in str(raised.value)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_a_hand_edit_of_the_documents_own_entry_is_found_at_its_data_path(deck, tmp_path):
|
|
248
|
+
slide = deck.kinds.modules()["slide"]
|
|
249
|
+
doc = slide.create(tmp_path, slide.example(), preamble=OWN)
|
|
250
|
+
doc.save()
|
|
251
|
+
tex = tmp_path / "slide.tex"
|
|
252
|
+
tex.write_text(
|
|
253
|
+
tex.read_text().replace("{table}{xcolor}", "{x11names}{xcolor}").replace("pdfversion=2.0", "pdfversion=1.7")
|
|
254
|
+
)
|
|
255
|
+
paths = {e.path for e in doc.hand_edits()}
|
|
256
|
+
assert paths == {"/preamble/before_class/0", "preamble.before_class[0]"} # the own entry, and the locked line
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@pytest.mark.tex
|
|
260
|
+
@pytest.mark.timeout(300)
|
|
261
|
+
def test_a_document_with_a_locked_entry_compiles(deck, compiler, tmp_path):
|
|
262
|
+
slide = deck.kinds.modules()["slide"]
|
|
263
|
+
slide.create(tmp_path / "doc", slide.example(), preamble=OWN).save()
|
|
264
|
+
compiler.compile(tmp_path / "doc/slide.tex", tmp_path / "out").raise_for_status()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def locked_deck(deck, **changes): # noqa: ANN001, ANN003, ANN201
|
|
268
|
+
"""The deck declaration over a brand whose `expert` kind locks `LOCK`; `changes` go to its `brief` profile."""
|
|
269
|
+
import acme_deck # noqa: PLC0415 # importable once the `deck` fixture has built the brand
|
|
270
|
+
|
|
271
|
+
expert = dataclasses.replace(acme_brand.dist.kinds[0], locked_preamble=BASE_LOCK)
|
|
272
|
+
base = dataclasses.replace(acme_brand.dist, kinds=(expert, *acme_brand.dist.kinds[1:]))
|
|
273
|
+
brief = dataclasses.replace(acme_deck.dist.kind("brief"), **changes)
|
|
274
|
+
return dataclasses.replace(acme_deck.dist, base=base, kinds=(acme_deck.dist.kinds[0], brief))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_a_profile_writes_the_locked_entries_of_its_base_kind(deck):
|
|
278
|
+
wide = locked_deck(deck)
|
|
279
|
+
release = dataclasses.replace(deck._bound.RELEASE, dist=wide) # noqa: SLF001
|
|
280
|
+
brief = wide.kind("brief")
|
|
281
|
+
text = release.plan(brief, brief.example(), None).text
|
|
282
|
+
assert text.index("\\PassOptionsToPackage{draft}{graphicx}") < text.index("\\documentclass")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_a_profile_may_not_lock_or_allow_what_its_base_kind_locks(deck):
|
|
286
|
+
with pytest.raises(DeclarationError) as raised:
|
|
287
|
+
locked_deck(deck, locked_preamble=BASE_LOCK)
|
|
288
|
+
assert raised.value.code == "TP-K020" and "locked by the base kind too" in str(raised.value)
|
|
289
|
+
with pytest.raises(DeclarationError) as raised:
|
|
290
|
+
locked_deck(deck, preamble=(PassOption,))
|
|
291
|
+
assert raised.value.code == "TP-K020" and "locked by the base kind" in str(raised.value)
|
|
@@ -64,7 +64,7 @@ nothing_settable = vocab.create("v", vocab.example(), settings=[expert.Ink(CMYK(
|
|
|
64
64
|
def wheel_like(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path:
|
|
65
65
|
root = tmp_path_factory.mktemp("typing")
|
|
66
66
|
module = root / dist.module
|
|
67
|
-
for path, text in generate.sources(dist, "dist", "1.2.0", Contracts()).items():
|
|
67
|
+
for path, text in generate.sources(dist, "acme_brand:dist", "1.2.0", Contracts()).items():
|
|
68
68
|
(module / path).parent.mkdir(parents=True, exist_ok=True)
|
|
69
69
|
(module / path).write_text(text, encoding="utf-8")
|
|
70
70
|
shutil.copytree(FIXTURES / "acme_brand", module / "_declaration", ignore=shutil.ignore_patterns("__pycache__"))
|
|
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
|