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.
Files changed (178) hide show
  1. {texmodel-0.3.0 → texmodel-0.3.1}/PKG-INFO +20 -2
  2. {texmodel-0.3.0 → texmodel-0.3.1}/README.md +19 -1
  3. {texmodel-0.3.0 → texmodel-0.3.1}/pyproject.toml +1 -1
  4. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/components.py +40 -16
  5. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/package.py +14 -0
  6. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_parts.py +99 -0
  7. {texmodel-0.3.0 → texmodel-0.3.1}/LICENSE +0 -0
  8. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/__init__.py +0 -0
  9. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/__init__.py +0 -0
  10. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/render.py +0 -0
  11. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/scan.py +0 -0
  12. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/_internal/values.py +0 -0
  13. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/body.py +0 -0
  14. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/bundle.py +0 -0
  15. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/cli.py +0 -0
  16. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/config.py +0 -0
  17. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/drift.py +0 -0
  18. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/errors.py +0 -0
  19. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/interface.py +0 -0
  20. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/keys.py +0 -0
  21. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/keyvals.py +0 -0
  22. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/plan.py +0 -0
  23. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/pointer.py +0 -0
  24. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/preamble.py +0 -0
  25. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/py.typed +0 -0
  26. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/spec.py +0 -0
  27. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/__init__.py +0 -0
  28. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/adjustbox.py +0 -0
  29. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/anyfontsize.py +0 -0
  30. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/calc.py +0 -0
  31. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/enumitem.py +0 -0
  32. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/eso_pic.py +0 -0
  33. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/etoolbox.py +0 -0
  34. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/fontspec.py +0 -0
  35. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/geometry.py +0 -0
  36. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/graphicx.py +0 -0
  37. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/hyphenat.py +0 -0
  38. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/latex.py +0 -0
  39. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/microtype.py +0 -0
  40. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/pdfrender.py +0 -0
  41. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/tcolorbox.py +0 -0
  42. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/tikz.py +0 -0
  43. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/std/xcolor.py +0 -0
  44. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/testing.py +0 -0
  45. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel/types.py +0 -0
  46. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/__init__.py +0 -0
  47. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/__init__.py +0 -0
  48. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/converge.py +0 -0
  49. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/env.py +0 -0
  50. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/logparse.py +0 -0
  51. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/proofpdf.py +0 -0
  52. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/spawn.py +0 -0
  53. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/_internal/styscan.py +0 -0
  54. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/cli.py +0 -0
  55. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/compiler.py +0 -0
  56. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/config.py +0 -0
  57. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/diagnostics.py +0 -0
  58. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/engines.py +0 -0
  59. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/errors.py +0 -0
  60. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/py.typed +0 -0
  61. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/result.py +0 -0
  62. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/roots.py +0 -0
  63. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/targets.py +0 -0
  64. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/testkit.py +0 -0
  65. {texmodel-0.3.0 → texmodel-0.3.1}/src/texmodel_build/verify.py +0 -0
  66. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/__init__.py +0 -0
  67. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/cards.toml +0 -0
  68. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/convert.py +0 -0
  69. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/fit.py +0 -0
  70. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/legacy.py +0 -0
  71. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/model.py +0 -0
  72. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/old/card-deck.sty +0 -0
  73. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/project.toml +0 -0
  74. {texmodel-0.3.0 → texmodel-0.3.1}/tests/consumers/card_deck/res/card-deck.sty +0 -0
  75. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/README.md +0 -0
  76. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/__init__.py +0 -0
  77. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/card.begin.tex +0 -0
  78. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/card.end.tex +0 -0
  79. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/contact.tex +0 -0
  80. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/expected/ada.tex +0 -0
  81. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/expected/business-card.sty +0 -0
  82. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/expected/compact.tex +0 -0
  83. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/business_card/name.tex +0 -0
  84. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/__init__.py +0 -0
  85. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/expected/base-kit.sty +0 -0
  86. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed-kit.sty +0 -0
  87. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed.tex +0 -0
  88. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/expl3/__init__.py +0 -0
  89. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3-kit.sty +0 -0
  90. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3.tex +0 -0
  91. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/hello/__init__.py +0 -0
  92. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.sty +0 -0
  93. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.tex +0 -0
  94. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/__init__.py +0 -0
  95. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/expected/invitation.tex +0 -0
  96. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/expected/letter-kit.sty +0 -0
  97. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/expected/terse.tex +0 -0
  98. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/letter.begin.tex +0 -0
  99. {texmodel-0.3.0 → texmodel-0.3.1}/tests/examples/letter/letter.end.tex +0 -0
  100. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/__init__.py +0 -0
  101. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.begin.tex +0 -0
  102. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.end.tex +0 -0
  103. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/fact.tex +0 -0
  104. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/generated/acme-cards.sty +0 -0
  105. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/name.tex +0 -0
  106. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/slot.tex +0 -0
  107. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.latexmk.txt +0 -0
  108. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.log +0 -0
  109. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/miss.log +0 -0
  110. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/ok.log +0 -0
  111. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/overset.log +0 -0
  112. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/pkg.log +0 -0
  113. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/sty.log +0 -0
  114. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/logs/undefined_in_argument.log +0 -0
  115. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/bad.tex +0 -0
  116. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/bad_font.tex +0 -0
  117. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_file.tex +0 -0
  118. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_package.tex +0 -0
  119. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/ok.tex +0 -0
  120. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/shell_escape.tex +0 -0
  121. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/unstable.tex +0 -0
  122. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_acme.tex +0 -0
  123. {texmodel-0.3.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_values.tex +0 -0
  124. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/__init__.py +0 -0
  125. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck.py +0 -0
  126. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_fit.py +0 -0
  127. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_tex.py +0 -0
  128. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_docs.py +0 -0
  129. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_std/__init__.py +0 -0
  130. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_std/test_calc.py +0 -0
  131. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_std/test_headers.py +0 -0
  132. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/__init__.py +0 -0
  133. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/block.tex +0 -0
  134. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/conftest.py +0 -0
  135. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_adopted.py +0 -0
  136. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_body.py +0 -0
  137. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_build_check.py +0 -0
  138. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_bundle_drift.py +0 -0
  139. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_check_documents.py +0 -0
  140. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_choice.py +0 -0
  141. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_comment.py +0 -0
  142. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_cs.py +0 -0
  143. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_document.py +0 -0
  144. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_emitter.py +0 -0
  145. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_errors_config_cli.py +0 -0
  146. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_examples.py +0 -0
  147. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3.py +0 -0
  148. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3_tex.py +0 -0
  149. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_grammar_hatches.py +0 -0
  150. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_import.py +0 -0
  151. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_interface.py +0 -0
  152. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_interface_tex.py +0 -0
  153. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_keyvals.py +0 -0
  154. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_pdf_string.py +0 -0
  155. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_plan.py +0 -0
  156. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_preamble.py +0 -0
  157. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_public_api.py +0 -0
  158. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_refs.py +0 -0
  159. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_render.py +0 -0
  160. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_schema.py +0 -0
  161. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_spec.py +0 -0
  162. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_stubs.py +0 -0
  163. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_text_values.py +0 -0
  164. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_types.py +0 -0
  165. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_value_kinds_and_meta.py +0 -0
  166. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel/test_values.py +0 -0
  167. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/__init__.py +0 -0
  168. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/conftest.py +0 -0
  169. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_cli.py +0 -0
  170. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compile_tex.py +0 -0
  171. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compiler.py +0 -0
  172. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_config_doctor.py +0 -0
  173. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_diagnostics_logparse.py +0 -0
  174. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_doctor_sty.py +0 -0
  175. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_proofpdf.py +0 -0
  176. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_public_api.py +0 -0
  177. {texmodel-0.3.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_support.py +0 -0
  178. {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.0
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, so give an optional one `default=None`.
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, so give an optional one `default=None`.
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
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "typer>=0.27.3",
21
21
  "pydantic>=2.14.1",
22
22
  ]
23
- version = "0.3.0"
23
+ version = "0.3.1"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  proof-pdf = [
@@ -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 own `body` / `begin` / `end` as (pieces, origin), or None.
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`, a `Comment`, a sequence, iterator or
214
- generator of those, or a method returning any of them. A field of that name is not a body.
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
- found: object = cls.__dict__.get(name)
217
- if found is None or name in inspect.get_annotations(cls):
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[cls.__module__].__file__ or ".", f"{cls.__qualname__}.{name}")
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 prefix and
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
- cls.__tm_doc__ = doc = _document(cls)
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.__dict__.get("children", ()):
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