texmodel 0.3.0__tar.gz → 0.3.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.
- {texmodel-0.3.0 → texmodel-0.3.1}/PKG-INFO +20 -2
- {texmodel-0.3.0 → texmodel-0.3.1}/README.md +19 -1
- {texmodel-0.3.0 → texmodel-0.3.1}/pyproject.toml +1 -1
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/components.py +40 -16
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/package.py +14 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_parts.py +99 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/LICENSE +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/render.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/scan.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/values.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/body.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/bundle.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/cli.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/config.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/drift.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/errors.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/interface.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/keys.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/keyvals.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/plan.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/pointer.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/preamble.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/py.typed +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/spec.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/adjustbox.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/anyfontsize.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/calc.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/enumitem.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/eso_pic.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/etoolbox.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/fontspec.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/geometry.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/graphicx.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/hyphenat.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/latex.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/microtype.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/pdfrender.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/tcolorbox.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/tikz.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/xcolor.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/testing.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/types.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/converge.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/env.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/logparse.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/proofpdf.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/spawn.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/styscan.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/cli.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/compiler.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/config.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/diagnostics.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/engines.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/errors.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/py.typed +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/result.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/roots.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/targets.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/testkit.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/verify.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/cards.toml +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/convert.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/fit.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/legacy.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/model.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/old/card-deck.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/project.toml +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/res/card-deck.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/README.md +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/card.begin.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/card.end.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/contact.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/expected/ada.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/expected/business-card.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/expected/compact.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/name.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/expected/base-kit.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed-kit.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/expl3/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3-kit.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/hello/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/expected/invitation.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/expected/letter-kit.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/expected/terse.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/letter.begin.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/letter.end.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.begin.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.end.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/fact.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/generated/acme-cards.sty +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/name.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/slot.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.latexmk.txt +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/miss.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/ok.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/overset.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/pkg.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/sty.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/undefined_in_argument.log +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/bad.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/bad_font.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_file.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_package.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/ok.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/shell_escape.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/unstable.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_acme.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_values.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_fit.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_tex.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_docs.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_std/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_std/test_calc.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_std/test_headers.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/block.tex +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/conftest.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_adopted.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_body.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_build_check.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_bundle_drift.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_check_documents.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_choice.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_comment.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_cs.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_document.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_emitter.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_errors_config_cli.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_examples.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3_tex.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_grammar_hatches.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_import.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_interface.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_interface_tex.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_keyvals.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_pdf_string.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_plan.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_preamble.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_public_api.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_refs.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_render.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_schema.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_spec.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_stubs.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_text_values.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_types.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_value_kinds_and_meta.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_values.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/__init__.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/conftest.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_cli.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compile_tex.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compiler.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_config_doctor.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_diagnostics_logparse.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_doctor_sty.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_proofpdf.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_public_api.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_support.py +0 -0
- {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: texmodel
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Typed document-family models rendered to TeX, with an optional latexmk driver and verifier plugins.
|
|
5
5
|
Author-Email: Ilja Orlovs <ilja@release.art>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -205,6 +205,22 @@ texmodel check # exit 1 if a committed generated file is stale, missing or o
|
|
|
205
205
|
is not a field, an argument the body never uses, unbalanced braces, a name collision.
|
|
206
206
|
- **Typed**: `Fact("A soft woof.")` type-checks; fields are `Text`, `Raw` or either `| None`; values are `Length`, `CMYK`/`RGB`/`Gray` (components in [0, 1], no conversion between models) or `bool`.
|
|
207
207
|
|
|
208
|
+
### Shared bases
|
|
209
|
+
|
|
210
|
+
Components that repeat a body or a grammar share a base declared `abstract=True`. It carries fields, `body` (or `begin` and
|
|
211
|
+
`end`) and `children`; it needs no docstring and may have any Python name (`_Image`); it is never registered
|
|
212
|
+
(`TM-M043`), and its subclasses inherit all of it:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
class _Image(Command, abstract=True):
|
|
216
|
+
src: Text
|
|
217
|
+
def body(self): return r"\includegraphics{#src}"
|
|
218
|
+
|
|
219
|
+
@pkg.command
|
|
220
|
+
class Photo(_Image):
|
|
221
|
+
'''A photo.'''
|
|
222
|
+
```
|
|
223
|
+
|
|
208
224
|
### Generating a body
|
|
209
225
|
|
|
210
226
|
A body can also be built from pieces that are well-formed by construction, so a generator cannot write an unbalanced brace or
|
|
@@ -312,7 +328,9 @@ class Put(Command): # \put(1,2){x}
|
|
|
312
328
|
- **Slots.** `{}` `[]` `()` `<>` or your own `Delimiter`; a `Star` or other token; a slot left out is not written, and an
|
|
313
329
|
optional slot left out before a later optional one writes `fill=` (say `""` for empty brackets) or is refused
|
|
314
330
|
(`Gap.REFUSE`); declaring two optionals without saying which is `TM-M037`. A `SlotGroup` is written together or not at all.
|
|
315
|
-
Optional and token fields are keyword-only in Python wherever they stand
|
|
331
|
+
Optional and token fields are keyword-only in Python wherever they stand. Write an optional field with `arg(default=...)`
|
|
332
|
+
(`label: Text | None = arg(default=None)`): a type checker then reads it as keyword-only and accepts a required field after
|
|
333
|
+
it, where a plain `= None` is reported as a field-order error although it runs.
|
|
316
334
|
- **Value types.** `Text` (escaped), `PdfString`, `Raw`, `Length` (`Length("2mm")`, or `Length.expr(r"\linewidth")` for a TeX
|
|
317
335
|
expression), a colour model (`CMYK`, `RGB`, `Gray`) or a `ColourName` (`blue!50`), `int`, `float`, `bool` (`true`/`false`),
|
|
318
336
|
a `Choice`, a tuple, `NamedTuple`, dataclass or model of those, a `list`, and a `KeyVals`. A wrong value is a violation
|
|
@@ -186,6 +186,22 @@ texmodel check # exit 1 if a committed generated file is stale, missing or o
|
|
|
186
186
|
is not a field, an argument the body never uses, unbalanced braces, a name collision.
|
|
187
187
|
- **Typed**: `Fact("A soft woof.")` type-checks; fields are `Text`, `Raw` or either `| None`; values are `Length`, `CMYK`/`RGB`/`Gray` (components in [0, 1], no conversion between models) or `bool`.
|
|
188
188
|
|
|
189
|
+
### Shared bases
|
|
190
|
+
|
|
191
|
+
Components that repeat a body or a grammar share a base declared `abstract=True`. It carries fields, `body` (or `begin` and
|
|
192
|
+
`end`) and `children`; it needs no docstring and may have any Python name (`_Image`); it is never registered
|
|
193
|
+
(`TM-M043`), and its subclasses inherit all of it:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class _Image(Command, abstract=True):
|
|
197
|
+
src: Text
|
|
198
|
+
def body(self): return r"\includegraphics{#src}"
|
|
199
|
+
|
|
200
|
+
@pkg.command
|
|
201
|
+
class Photo(_Image):
|
|
202
|
+
'''A photo.'''
|
|
203
|
+
```
|
|
204
|
+
|
|
189
205
|
### Generating a body
|
|
190
206
|
|
|
191
207
|
A body can also be built from pieces that are well-formed by construction, so a generator cannot write an unbalanced brace or
|
|
@@ -293,7 +309,9 @@ class Put(Command): # \put(1,2){x}
|
|
|
293
309
|
- **Slots.** `{}` `[]` `()` `<>` or your own `Delimiter`; a `Star` or other token; a slot left out is not written, and an
|
|
294
310
|
optional slot left out before a later optional one writes `fill=` (say `""` for empty brackets) or is refused
|
|
295
311
|
(`Gap.REFUSE`); declaring two optionals without saying which is `TM-M037`. A `SlotGroup` is written together or not at all.
|
|
296
|
-
Optional and token fields are keyword-only in Python wherever they stand
|
|
312
|
+
Optional and token fields are keyword-only in Python wherever they stand. Write an optional field with `arg(default=...)`
|
|
313
|
+
(`label: Text | None = arg(default=None)`): a type checker then reads it as keyword-only and accepts a required field after
|
|
314
|
+
it, where a plain `= None` is reported as a field-order error although it runs.
|
|
297
315
|
- **Value types.** `Text` (escaped), `PdfString`, `Raw`, `Length` (`Length("2mm")`, or `Length.expr(r"\linewidth")` for a TeX
|
|
298
316
|
expression), a colour model (`CMYK`, `RGB`, `Gray`) or a `ColourName` (`blue!50`), `int`, `float`, `bool` (`true`/`false`),
|
|
299
317
|
a `Choice`, a tuple, `NamedTuple`, dataclass or model of those, a `list`, and a `KeyVals`. A wrong value is a violation
|
|
@@ -200,6 +200,7 @@ class _Meta(type):
|
|
|
200
200
|
|
|
201
201
|
|
|
202
202
|
def _document(cls: type) -> str:
|
|
203
|
+
"""The docstring of a component, which must be documented and named as a TeX control word would be."""
|
|
203
204
|
check_name(cls.__name__, "component")
|
|
204
205
|
doc = (cls.__dict__.get("__doc__") or "").strip()
|
|
205
206
|
if not doc:
|
|
@@ -208,13 +209,18 @@ def _document(cls: type) -> str:
|
|
|
208
209
|
|
|
209
210
|
|
|
210
211
|
def _source(cls: type, name: str) -> Source | None:
|
|
211
|
-
"""The class's
|
|
212
|
+
"""The class's `body` / `begin` / `end` as (pieces, origin), or None: its own, else the nearest base class's (a
|
|
213
|
+
shared base is declared `abstract=True`).
|
|
212
214
|
|
|
213
|
-
It is a string, a `TemplateRef` (the file is read now), a `TexRaw`, a `Ref`,
|
|
214
|
-
generator of those, or a method returning any of them. A field of that name
|
|
215
|
+
It is a string, a `TemplateRef` (the file is read now, relative to the module that wrote it), a `TexRaw`, a `Ref`,
|
|
216
|
+
a `Comment`, a sequence, iterator or generator of those, or a method returning any of them. A field of that name
|
|
217
|
+
is not a body.
|
|
215
218
|
"""
|
|
216
|
-
|
|
217
|
-
if
|
|
219
|
+
owner = next((k for k in cls.__mro__ if name in k.__dict__), None)
|
|
220
|
+
if owner is None:
|
|
221
|
+
return None
|
|
222
|
+
found: object = owner.__dict__[name]
|
|
223
|
+
if found is None or name in inspect.get_annotations(owner):
|
|
218
224
|
return None
|
|
219
225
|
if isinstance(found, types.FunctionType):
|
|
220
226
|
try:
|
|
@@ -238,7 +244,7 @@ def _source(cls: type, name: str) -> Source | None:
|
|
|
238
244
|
out: list[Resolved] = []
|
|
239
245
|
for piece in given:
|
|
240
246
|
if isinstance(piece, TemplateRef):
|
|
241
|
-
text = template_text(piece, sys.modules[
|
|
247
|
+
text = template_text(piece, sys.modules[owner.__module__].__file__ or ".", f"{cls.__qualname__}.{name}")
|
|
242
248
|
out.append(TexRaw(text, piece.whitespace, piece.syntax))
|
|
243
249
|
origins.append(piece.path.name)
|
|
244
250
|
else:
|
|
@@ -248,9 +254,15 @@ def _source(cls: type, name: str) -> Source | None:
|
|
|
248
254
|
|
|
249
255
|
|
|
250
256
|
def control_name(cls: type) -> str:
|
|
251
|
-
"""The control sequence of a registered component, without the backslash: its `cs`, else its package's
|
|
252
|
-
its class name."""
|
|
257
|
+
"""The control sequence of a registered component or value, without the backslash: its `cs`, else its package's
|
|
258
|
+
prefix and its class name. A colour is not a control sequence (`TM-M042`)."""
|
|
253
259
|
name: str | None = getattr(cls, "__tm_cs__", None)
|
|
260
|
+
if name is None and isinstance(getattr(cls, "__tm_kind__", None), ValueKind) and hasattr(cls, "__tm_prefix__"):
|
|
261
|
+
raise DeclarationError(
|
|
262
|
+
f"{cls.__qualname__}: a colour has no control sequence",
|
|
263
|
+
code="TM-M042",
|
|
264
|
+
hint="its name is `colour_name`, else prefix-kebab (xcolor's namespace)",
|
|
265
|
+
)
|
|
254
266
|
if name is None:
|
|
255
267
|
raise DeclarationError(
|
|
256
268
|
f"{cls.__qualname__}: not registered with a package", code="TM-M019", hint="decorate it with @pkg.command"
|
|
@@ -261,6 +273,9 @@ def control_name(cls: type) -> str:
|
|
|
261
273
|
class _Node(metaclass=_Meta):
|
|
262
274
|
"""What a `Command` and an `Environment` share: declaration data, `repr`, `validate` and `render`."""
|
|
263
275
|
|
|
276
|
+
__tm_abstract__: ClassVar[
|
|
277
|
+
bool
|
|
278
|
+
] # a shared base (`abstract=True`): fields and a body for subclasses, never registered
|
|
264
279
|
__tm_unused__: ClassVar[tuple[str, ...]]
|
|
265
280
|
__tm_args__: ClassVar[tuple[Arg, ...]]
|
|
266
281
|
__tm_doc__: ClassVar[str]
|
|
@@ -295,9 +310,11 @@ def _as_field(given: object) -> "dataclasses.Field[object]":
|
|
|
295
310
|
return typing.cast("dataclasses.Field[object]", dataclasses.field(default=given))
|
|
296
311
|
|
|
297
312
|
|
|
298
|
-
def _build(cls: type[_Node], unused: Sequence[str], *, init: bool) -> None:
|
|
299
|
-
"""Turn a declared class into a frozen dataclass whose fields are its arguments.
|
|
300
|
-
|
|
313
|
+
def _build(cls: type[_Node], unused: Sequence[str], *, init: bool, abstract: bool) -> None:
|
|
314
|
+
"""Turn a declared class into a frozen dataclass whose fields are its arguments. An abstract base needs no name a
|
|
315
|
+
control word could have and no docstring: it is never registered."""
|
|
316
|
+
cls.__tm_abstract__ = abstract
|
|
317
|
+
cls.__tm_doc__ = doc = "" if abstract else _document(cls)
|
|
301
318
|
cls.__tm_unused__ = tuple(unused)
|
|
302
319
|
if "cs" in cls.__dict__:
|
|
303
320
|
Cs(cls.__dict__["cs"], cls.syntax) # a control word of the class's syntax (TM-M027)
|
|
@@ -334,10 +351,10 @@ class Command(_Node):
|
|
|
334
351
|
|
|
335
352
|
__tm_inline__: ClassVar[Source | None]
|
|
336
353
|
|
|
337
|
-
def __init_subclass__(cls, *, unused: Sequence[str] = (), **kwargs: Any) -> None:
|
|
354
|
+
def __init_subclass__(cls, *, unused: Sequence[str] = (), abstract: bool = False, **kwargs: Any) -> None:
|
|
338
355
|
super().__init_subclass__(**kwargs)
|
|
339
356
|
cls.__tm_inline__ = _source(cls, "body")
|
|
340
|
-
_build(cls, unused, init=True)
|
|
357
|
+
_build(cls, unused, init=True, abstract=abstract)
|
|
341
358
|
|
|
342
359
|
@classmethod
|
|
343
360
|
def call(cls, *required: ArgValue, **optional: ArgValue) -> Ref:
|
|
@@ -356,11 +373,11 @@ class Environment(_Node):
|
|
|
356
373
|
__tm_inline__: ClassVar[tuple[Source | None, Source | None]]
|
|
357
374
|
__tm_grammar__: ClassVar[tuple[Repeat | Anywhere, ...]]
|
|
358
375
|
|
|
359
|
-
def __init_subclass__(cls, *, unused: Sequence[str] = (), **kwargs: Any) -> None:
|
|
376
|
+
def __init_subclass__(cls, *, unused: Sequence[str] = (), abstract: bool = False, **kwargs: Any) -> None:
|
|
360
377
|
super().__init_subclass__(**kwargs)
|
|
361
378
|
cls.__tm_inline__ = (_source(cls, "begin"), _source(cls, "end"))
|
|
362
379
|
cls.__tm_grammar__ = _grammar(cls)
|
|
363
|
-
_build(cls, unused, init=False)
|
|
380
|
+
_build(cls, unused, init=False, abstract=abstract)
|
|
364
381
|
|
|
365
382
|
@classmethod
|
|
366
383
|
def open(cls, **args: ArgValue) -> Ref:
|
|
@@ -466,7 +483,7 @@ def _repr(node: Command | Environment | Passthrough) -> str:
|
|
|
466
483
|
|
|
467
484
|
def _grammar(cls: type[Environment]) -> tuple[Repeat | Anywhere, ...]:
|
|
468
485
|
terms: list[Repeat | Anywhere] = []
|
|
469
|
-
for item in cls.
|
|
486
|
+
for item in cls.children: # inherited from a shared base, or its own
|
|
470
487
|
if isinstance(item, type) and issubclass(item, Command | Environment | Passthrough):
|
|
471
488
|
item = Repeat(item, 1, 1)
|
|
472
489
|
if not isinstance(item, Repeat | Anywhere) or not issubclass(item.kind, _Node):
|
|
@@ -801,6 +818,13 @@ class Value[T]:
|
|
|
801
818
|
__tm_doc__: ClassVar[str]
|
|
802
819
|
__tm_kind__: ClassVar[ValueKind]
|
|
803
820
|
__tm_prefix__: ClassVar[str]
|
|
821
|
+
__tm_cs__: ClassVar[str] # the control sequence, once registered (not for a colour); see `control_name`
|
|
822
|
+
|
|
823
|
+
@classmethod
|
|
824
|
+
def declared_default(cls) -> T | None:
|
|
825
|
+
"""The class's `default` (None when it has none): reading `default` through a class is ambiguous to a type
|
|
826
|
+
checker, this is not."""
|
|
827
|
+
return getattr(cls, "default", None)
|
|
804
828
|
|
|
805
829
|
def __init__(self, value: T) -> None:
|
|
806
830
|
"""A document's setting for this value (D1a): `FactGap(Length("3mm"))`, checked against the declared type."""
|
|
@@ -114,6 +114,16 @@ class _Defs(NamedTuple):
|
|
|
114
114
|
return [*(("control", n) for n in self.controls), *sorted(self.others)]
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def _concrete(cls: type[Command] | type[Environment]) -> None:
|
|
118
|
+
if cls.__tm_abstract__:
|
|
119
|
+
raise DeclarationError(
|
|
120
|
+
f"{cls.__qualname__}: an abstract base is not registered",
|
|
121
|
+
code="TM-M043",
|
|
122
|
+
path=cls.__name__,
|
|
123
|
+
hint="register the subclasses",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
117
127
|
def _shown(kind: str, name: str) -> str:
|
|
118
128
|
return f"\\{name}" if kind == "control" else f"{kind} {name}"
|
|
119
129
|
|
|
@@ -194,6 +204,7 @@ class Package:
|
|
|
194
204
|
|
|
195
205
|
def command[C: type[Command]](self, cls: C) -> C:
|
|
196
206
|
"""Register a command class."""
|
|
207
|
+
_concrete(cls)
|
|
197
208
|
spec = spec_of(cls, declared_only=self.adopted, whitespace=self.whitespace)
|
|
198
209
|
self._own_library(cls)
|
|
199
210
|
if not self.adopted and cls.defined_by is None:
|
|
@@ -254,6 +265,7 @@ class Package:
|
|
|
254
265
|
|
|
255
266
|
def environment[E: type[Environment]](self, cls: E) -> E:
|
|
256
267
|
"""Register an environment class: `\\newenvironment` with its begin and end bodies."""
|
|
268
|
+
_concrete(cls)
|
|
257
269
|
spec = env_spec_of(cls, declared_only=self.adopted, whitespace=self.whitespace)
|
|
258
270
|
self._own_library(cls)
|
|
259
271
|
if not self.adopted and cls.defined_by is None:
|
|
@@ -283,6 +295,8 @@ class Package:
|
|
|
283
295
|
for table, key, shown in spec.kind.claims(*addressed(self.prefix, spec.name, spec.cs), spec.alias)
|
|
284
296
|
]
|
|
285
297
|
self._add(spec, *claims, rendered=cls.defined_by is None)
|
|
298
|
+
if not spec.kind.named: # a colour is not a control sequence
|
|
299
|
+
cls.__tm_cs__ = "".join(addressed(self.prefix, spec.name, spec.cs))
|
|
286
300
|
cls.__tm_prefix__ = self.prefix
|
|
287
301
|
self._classes.append(cls)
|
|
288
302
|
return cls
|
|
@@ -302,3 +302,102 @@ def test_colour_components_are_written_to_five_decimals():
|
|
|
302
302
|
|
|
303
303
|
assert str(RGB(78.8 / 100, 0, 1 / 3)) == "0.788,0,0.33333"
|
|
304
304
|
assert str(CMYK(0.1 + 0.2, 1, 0.000004, 0.5)) == "0.3,1,0,0.5"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_a_shared_base_carries_fields_and_bodies_for_its_subclasses():
|
|
308
|
+
from texmodel.components import Environment
|
|
309
|
+
|
|
310
|
+
p = Package("p", prefix="ac")
|
|
311
|
+
|
|
312
|
+
@p.command
|
|
313
|
+
class Fact(Command, unused=("text",)):
|
|
314
|
+
"""A fact."""
|
|
315
|
+
|
|
316
|
+
text: Text
|
|
317
|
+
|
|
318
|
+
def body(self):
|
|
319
|
+
return r"#text"
|
|
320
|
+
|
|
321
|
+
class _Image(Command, abstract=True): # no docstring, and a name a control word could not have
|
|
322
|
+
src: Text
|
|
323
|
+
|
|
324
|
+
def body(self):
|
|
325
|
+
return r"\includegraphics{#src}"
|
|
326
|
+
|
|
327
|
+
class _Panel(Environment, abstract=True):
|
|
328
|
+
children = [Fact.times(2, 3)]
|
|
329
|
+
|
|
330
|
+
def begin(self):
|
|
331
|
+
return r"\begin{minipage}{\linewidth}"
|
|
332
|
+
|
|
333
|
+
def end(self):
|
|
334
|
+
return r"\end{minipage}"
|
|
335
|
+
|
|
336
|
+
@p.command
|
|
337
|
+
class Photo(_Image):
|
|
338
|
+
"""A photo."""
|
|
339
|
+
|
|
340
|
+
@p.command
|
|
341
|
+
class Icon(_Image):
|
|
342
|
+
"""An icon."""
|
|
343
|
+
|
|
344
|
+
@p.environment
|
|
345
|
+
class Card(_Panel):
|
|
346
|
+
"""A card."""
|
|
347
|
+
|
|
348
|
+
out = p.render()
|
|
349
|
+
assert out.count("\\includegraphics{#1}") == 2 and "\\newcommand{\\acPhoto}[1]{%" in out
|
|
350
|
+
assert out.count("\\begin{minipage}{\\linewidth}") == 1 and Photo("a.png").render() == "\\acPhoto{a.png}"
|
|
351
|
+
assert [v.code for v in Card(Fact("a")).validate()] == ["TM-C002"] # the base's grammar: 2 to 3 facts
|
|
352
|
+
with pytest.raises(DeclarationError, check=lambda e: e.code == "TM-M043"):
|
|
353
|
+
p.command(_Image)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_a_value_has_a_control_sequence_and_a_default_a_type_checker_can_read():
|
|
357
|
+
from texmodel.components import Value, control_name
|
|
358
|
+
from texmodel.types import RGB, Colour, Length
|
|
359
|
+
|
|
360
|
+
p = Package("p", prefix="ac")
|
|
361
|
+
|
|
362
|
+
@p.value
|
|
363
|
+
class Gap(Value[Length]):
|
|
364
|
+
"""Gap."""
|
|
365
|
+
|
|
366
|
+
default = Length("2mm")
|
|
367
|
+
|
|
368
|
+
@p.value
|
|
369
|
+
class Crop(Value[bool]):
|
|
370
|
+
"""Crop."""
|
|
371
|
+
|
|
372
|
+
cs = "acshowcrop"
|
|
373
|
+
default = True
|
|
374
|
+
|
|
375
|
+
@p.value
|
|
376
|
+
class Ink(Value[Colour]):
|
|
377
|
+
"""Ink."""
|
|
378
|
+
|
|
379
|
+
default = RGB(0, 0, 0)
|
|
380
|
+
|
|
381
|
+
assert control_name(Gap) == "acGap" and control_name(Crop) == "acshowcrop"
|
|
382
|
+
assert Gap.declared_default() == Length("2mm") and Crop.declared_default() is True
|
|
383
|
+
with pytest.raises(DeclarationError, check=lambda e: e.code == "TM-M042"):
|
|
384
|
+
control_name(Ink)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_a_field_called_body_is_a_field_not_a_body():
|
|
388
|
+
from texmodel.interface import arg
|
|
389
|
+
|
|
390
|
+
p = Package("p", prefix="ac")
|
|
391
|
+
|
|
392
|
+
class Note(Command, unused=("body",)):
|
|
393
|
+
"""A note."""
|
|
394
|
+
|
|
395
|
+
text: Text
|
|
396
|
+
body: Text | None = arg(default=None)
|
|
397
|
+
|
|
398
|
+
def end(self): # not a body either: only `body` is read for a command
|
|
399
|
+
return ""
|
|
400
|
+
|
|
401
|
+
assert Note.__tm_inline__ is None and [a.name for a in Note.__tm_args__] == ["text", "body"]
|
|
402
|
+
with pytest.raises(DeclarationError, check=lambda e: e.code == "TM-M006"):
|
|
403
|
+
p.command(Note)
|
|
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
|
|
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
|