texpkg 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- texpkg/__init__.py +18 -0
- texpkg/_internal/__init__.py +1 -0
- texpkg/_internal/adopt.py +119 -0
- texpkg/_internal/classify.py +190 -0
- texpkg/_internal/directory.py +48 -0
- texpkg/_internal/envelope.py +37 -0
- texpkg/_internal/export.py +49 -0
- texpkg/_internal/handedits.py +33 -0
- texpkg/_internal/history.py +91 -0
- texpkg/_internal/payload.py +118 -0
- texpkg/_internal/provenance_tex.py +65 -0
- texpkg/_internal/release.py +257 -0
- texpkg/_internal/snapshots.py +61 -0
- texpkg/_internal/state.py +113 -0
- texpkg/_internal/versions.py +61 -0
- texpkg/assets.py +56 -0
- texpkg/contracts.py +46 -0
- texpkg/declaration.py +379 -0
- texpkg/document.py +225 -0
- texpkg/errors.py +66 -0
- texpkg/layout.py +48 -0
- texpkg/migration.py +73 -0
- texpkg/protocol.py +61 -0
- texpkg/provenance.py +61 -0
- texpkg/py.typed +0 -0
- texpkg-0.1.0.dist-info/METADATA +30 -0
- texpkg-0.1.0.dist-info/RECORD +41 -0
- texpkg-0.1.0.dist-info/WHEEL +4 -0
- texpkg-0.1.0.dist-info/entry_points.txt +5 -0
- texpkg-0.1.0.dist-info/licenses/LICENSE +21 -0
- texpkg_build/__init__.py +1 -0
- texpkg_build/_internal/__init__.py +1 -0
- texpkg_build/_internal/checks.py +154 -0
- texpkg_build/_internal/embed.py +254 -0
- texpkg_build/_internal/generate.py +254 -0
- texpkg_build/_internal/isolate.py +86 -0
- texpkg_build/_internal/snapshot.py +319 -0
- texpkg_build/cli.py +83 -0
- texpkg_build/hook.py +76 -0
- texpkg_build/py.typed +0 -0
- texpkg_build/testing.py +176 -0
texpkg_build/testing.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Helpers for an author's tests: the declaration loaded in memory, the L3 law, the isolated compile (1.4, L7, A7)."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.abc
|
|
5
|
+
import importlib.machinery
|
|
6
|
+
import importlib.util
|
|
7
|
+
import pathlib
|
|
8
|
+
import pkgutil
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
|
|
14
|
+
from texmodel.components import Node
|
|
15
|
+
from texmodel_build import fonts
|
|
16
|
+
from texmodel_build.compiler import Compiler
|
|
17
|
+
|
|
18
|
+
from texpkg.contracts import Contracts
|
|
19
|
+
from texpkg.declaration import Distribution
|
|
20
|
+
from texpkg.errors import DeclarationError
|
|
21
|
+
|
|
22
|
+
from ._internal import generate
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_under(name: str, root: str) -> bool:
|
|
26
|
+
return name == root or name.startswith(root + ".")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _declared_modules(declared_in: str) -> dict[str, ModuleType]:
|
|
30
|
+
"""The declaration package and every module under it, imported."""
|
|
31
|
+
package = importlib.import_module(declared_in)
|
|
32
|
+
modules = {declared_in: package}
|
|
33
|
+
for info in pkgutil.walk_packages(package.__path__, f"{declared_in}."):
|
|
34
|
+
modules[info.name] = importlib.import_module(info.name)
|
|
35
|
+
return modules
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_declaration(pointer: str, *, version: str = "0.0.0", contracts: Contracts | None = None) -> ModuleType:
|
|
39
|
+
"""The module a wheel built from `pointer` (`"acme_decl:dist"`, as `[tool.texpkg] distribution` writes it) would
|
|
40
|
+
contain, made from the same generated source (1.4): no wheel, no embedding. The files are written to a temporary
|
|
41
|
+
directory that lives as long as the module; the declaration is the one already imported, shown as
|
|
42
|
+
`<module>._declaration`, so its classes are the ones the author's own tests use. Loading again replaces the
|
|
43
|
+
module."""
|
|
44
|
+
module_name, _, attr = pointer.partition(":")
|
|
45
|
+
dist = getattr(importlib.import_module(module_name), attr, None)
|
|
46
|
+
if not isinstance(dist, Distribution):
|
|
47
|
+
raise DeclarationError(
|
|
48
|
+
f"{pointer} is not a Distribution",
|
|
49
|
+
code="TP-B003",
|
|
50
|
+
path=pointer,
|
|
51
|
+
hint='write [tool.texpkg] distribution = "module:attribute"',
|
|
52
|
+
)
|
|
53
|
+
scratch = tempfile.TemporaryDirectory(prefix="texpkg-")
|
|
54
|
+
root = pathlib.Path(scratch.name) / dist.module
|
|
55
|
+
for loaded in [n for n in sys.modules if _is_under(n, dist.module)]:
|
|
56
|
+
del sys.modules[loaded]
|
|
57
|
+
_write(dist, attr, version, contracts or Contracts(), root)
|
|
58
|
+
spec = importlib.util.spec_from_file_location(
|
|
59
|
+
dist.module, root / "__init__.py", submodule_search_locations=[str(root)]
|
|
60
|
+
)
|
|
61
|
+
assert spec is not None and spec.loader is not None # a file we just wrote
|
|
62
|
+
module = importlib.util.module_from_spec(spec)
|
|
63
|
+
sys.modules[dist.module] = module
|
|
64
|
+
place, aliases = dist.module, dict[str, str]()
|
|
65
|
+
for member in dist.chain():
|
|
66
|
+
for name, real in _declared_modules(member.declared_in).items():
|
|
67
|
+
sys.modules[f"{place}._declaration{name.removeprefix(member.declared_in)}"] = real
|
|
68
|
+
if member.base:
|
|
69
|
+
nested = f"{place}._embedded.{member.base.module}"
|
|
70
|
+
if place != dist.module:
|
|
71
|
+
aliases[f"{dist.module}._embedded.{member.base.module}"] = nested
|
|
72
|
+
place = nested
|
|
73
|
+
module._scratch = scratch # pyright: ignore[reportAttributeAccessIssue] # the files go when the module does
|
|
74
|
+
finder = _Flatten(aliases)
|
|
75
|
+
sys.meta_path.insert(0, finder)
|
|
76
|
+
try:
|
|
77
|
+
spec.loader.exec_module(module)
|
|
78
|
+
finally:
|
|
79
|
+
sys.meta_path.remove(finder)
|
|
80
|
+
return module
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _Flatten(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
|
84
|
+
"""In a wheel the bases of a chain are siblings under `_embedded` (P19); written in memory each sits inside the one
|
|
85
|
+
above it. This makes `<module>._embedded.<grand base>` the same module as the nested one."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, aliases: dict[str, str]) -> None:
|
|
88
|
+
self.aliases = aliases
|
|
89
|
+
|
|
90
|
+
def find_spec(
|
|
91
|
+
self, fullname: str, path: object = None, target: object = None
|
|
92
|
+
) -> importlib.machinery.ModuleSpec | None:
|
|
93
|
+
if any(fullname == flat or fullname.startswith(flat + ".") for flat in self.aliases):
|
|
94
|
+
return importlib.util.spec_from_loader(fullname, self)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType:
|
|
98
|
+
flat = next(f for f in self.aliases if spec.name == f or spec.name.startswith(f + "."))
|
|
99
|
+
return importlib.import_module(self.aliases[flat] + spec.name.removeprefix(flat))
|
|
100
|
+
|
|
101
|
+
def exec_module(self, module: ModuleType) -> None:
|
|
102
|
+
"""The module is the nested one, already running."""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _attribute_of(dist: Distribution) -> str:
|
|
106
|
+
"""The name the declaration package gives `dist`, which a base's generated module imports it by."""
|
|
107
|
+
namespace = vars(importlib.import_module(dist.declared_in))
|
|
108
|
+
return next(name for name, value in namespace.items() if value is dist)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write(dist: Distribution, attr: str, version: str, contracts: Contracts, root: pathlib.Path) -> None:
|
|
112
|
+
"""The generated module of `dist` in `root`, and in `root/_embedded/<base>` that of each base: embedding without
|
|
113
|
+
copying, since the declarations are the modules already imported."""
|
|
114
|
+
for path, text in generate.sources(dist, attr, version, contracts).items():
|
|
115
|
+
(root / path).parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
(root / path).write_text(text, encoding="utf-8")
|
|
117
|
+
if dist.base:
|
|
118
|
+
(root / "_embedded").mkdir(exist_ok=True)
|
|
119
|
+
(root / "_embedded" / "__init__.py").write_text("", encoding="utf-8")
|
|
120
|
+
_write(
|
|
121
|
+
dist.base,
|
|
122
|
+
_attribute_of(dist.base),
|
|
123
|
+
dist.base.package.version,
|
|
124
|
+
contracts,
|
|
125
|
+
root / "_embedded" / dist.base.module,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_expansion(dist: Distribution, kind: str, examples: Iterable[Node] = ()) -> None:
|
|
130
|
+
"""The law of a profile kind (L3): every valid document of it expands to a valid document of the kind it expands to.
|
|
131
|
+
Checked on the kind's own example and on `examples`: run it over trees a hypothesis strategy builds (L6, L7), where
|
|
132
|
+
a profile that widens its base (a default or a value the base's type does not take) fails."""
|
|
133
|
+
profile = dist.kind(kind)
|
|
134
|
+
base = dist.base_kind(profile)
|
|
135
|
+
if profile.expand is None or base is None:
|
|
136
|
+
raise DeclarationError(f"kind {kind} is not a profile kind", code="TP-K009", path=kind)
|
|
137
|
+
for tree in (profile.example(), *examples):
|
|
138
|
+
if problems := tree.violations():
|
|
139
|
+
raise DeclarationError(f"kind {kind}: {problems[0].message}", code="TP-K010", path=kind)
|
|
140
|
+
if problem := dist.expansion_problem(profile, tree):
|
|
141
|
+
raise DeclarationError(
|
|
142
|
+
f"kind {kind}: {tree!r} expands to a tree that {problem}",
|
|
143
|
+
code="TP-K010",
|
|
144
|
+
path=kind,
|
|
145
|
+
hint="a profile narrows its base and never widens it (L6)",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def check_isolated(module: ModuleType, compiler: Compiler, workdir: pathlib.Path) -> None:
|
|
150
|
+
"""Each kind's example, in each of its variants, saved into a fresh directory that holds only that kind's payload
|
|
151
|
+
and compiled with no search path (no `TEXINPUTS`, no font directory): it fails on a file the payload lacks, and on a
|
|
152
|
+
font embedded from outside the document directory (A7). It proves what the example uses, so an example uses the
|
|
153
|
+
whole kind."""
|
|
154
|
+
problems: list[str] = []
|
|
155
|
+
for kind in module._bound.RELEASE.dist.kinds_offered(): # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]
|
|
156
|
+
kind_module = module.kinds.modules()[kind.name]
|
|
157
|
+
root = workdir / kind.name
|
|
158
|
+
kind_module.create(root, kind_module.example()).save()
|
|
159
|
+
for _, name in kind.documents():
|
|
160
|
+
result = compiler.compile(root / name, workdir / "out" / kind.name / name.removesuffix(".tex"))
|
|
161
|
+
problems += [f"{kind.name}/{name}: {e.code} {e.message}" for e in result.errors]
|
|
162
|
+
if result.log is not None:
|
|
163
|
+
problems += [
|
|
164
|
+
f"{kind.name}/{name}: font read from outside the document directory: {path}"
|
|
165
|
+
for path in fonts.outside(
|
|
166
|
+
result.log.read_text(encoding="utf-8", errors="replace"),
|
|
167
|
+
[pathlib.Path("/work/src"), root.resolve()],
|
|
168
|
+
root.resolve(),
|
|
169
|
+
)
|
|
170
|
+
]
|
|
171
|
+
if problems:
|
|
172
|
+
raise DeclarationError(
|
|
173
|
+
"a kind's payload is not whole:\n " + "\n ".join(problems),
|
|
174
|
+
code="TP-K011",
|
|
175
|
+
hint="an asset names every kind that needs it (Asset(kinds=)); fonts are assets, not system fonts",
|
|
176
|
+
)
|