entangled-cli 2.0.4__tar.gz → 2.1.0__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 (43) hide show
  1. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/PKG-INFO +46 -6
  2. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/README.md +44 -5
  3. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/stitch.py +5 -5
  4. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/tangle.py +3 -3
  5. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/__init__.py +2 -2
  6. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/document.py +13 -6
  7. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/__init__.py +6 -2
  8. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/base.py +3 -7
  9. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/build.py +23 -22
  10. entangled_cli-2.1.0/entangled/hooks/quarto_attributes.py +39 -0
  11. entangled_cli-2.1.0/entangled/hooks/shebang.py +10 -0
  12. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/task.py +29 -31
  13. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/markdown_reader.py +67 -64
  14. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/properties.py +2 -2
  15. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/tangle.py +3 -0
  16. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/pyproject.toml +5 -2
  17. entangled_cli-2.0.4/entangled/hooks/quarto_attributes.py +0 -28
  18. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/LICENSE +0 -0
  19. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/__init__.py +0 -0
  20. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/code_reader.py +0 -0
  21. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/__init__.py +0 -0
  22. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/brei.py +0 -0
  23. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/new.py +0 -0
  24. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/status.py +0 -0
  25. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/sync.py +0 -0
  26. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/watch.py +0 -0
  27. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/language.py +0 -0
  28. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/templates.py +0 -0
  29. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/version.py +0 -0
  30. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/construct.py +0 -0
  31. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/errors/__init__.py +0 -0
  32. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/errors/internal.py +0 -0
  33. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/errors/user.py +0 -0
  34. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/filedb.py +0 -0
  35. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/logging.py +0 -0
  36. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/main.py +0 -0
  37. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/parsing.py +0 -0
  38. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/py.typed +0 -0
  39. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/status.py +0 -0
  40. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/text_location.py +0 -0
  41. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/transaction.py +0 -0
  42. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/utility.py +0 -0
  43. {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: entangled-cli
3
- Version: 2.0.4
3
+ Version: 2.1.0
4
4
  Summary: Literate Programming toolbox
5
5
  Home-page: https://entangled.github.io/
6
6
  License: Apache 2
@@ -17,6 +17,7 @@ Requires-Dist: copier (>=9,<10)
17
17
  Requires-Dist: filelock (>=3.12.0,<4.0.0)
18
18
  Requires-Dist: mawk (>=0.1.4,<0.2.0)
19
19
  Requires-Dist: pexpect (>=4.9.0,<5.0.0)
20
+ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
20
21
  Requires-Dist: rich (>=13.3.5,<14.0.0)
21
22
  Requires-Dist: rich-argparse (>=1.4.0,<2.0.0)
22
23
  Requires-Dist: tomlkit (>=0.12.1,<0.13.0)
@@ -58,9 +59,6 @@ pip install entangled-cli
58
59
  ```
59
60
 
60
61
  ## Use
61
-
62
- > :warning: **This version of Entangled is still in beta.** In general things are working as they should, but there may still be some rough edges in the user experience.
63
-
64
62
  Run the `entangled watch` daemon in the root of your project folder. By default all Markdown files are monitored for fenced code blocks like so:
65
63
 
66
64
  ~~~markdown
@@ -171,9 +169,10 @@ Entangled has a system of *hooks*: these add actions to the tangling process:
171
169
 
172
170
  - `build` trigger actions in a generated `Makefile`
173
171
  - `brei` trigger actions (or tasks) using [Brei](https://entangled.github.io/brei), which is automatically installed along with Entangled. This is now prefered over the `build` hook.
174
- - `quatro_attributes` add attributes to the code block in Quatro style with `#|` comments at the top of the code block.
172
+ - `quarto_attributes` add attributes to the code block in Quatro style with `#|` comments at the top of the code block.
173
+ - `shebang` takes the first line if it starts with `#!` and puts it at the top of the file.
175
174
 
176
- ## `build` hook
175
+ ### `build` hook
177
176
  You can enable this hook in `entangled.toml`:
178
177
 
179
178
  ```toml
@@ -259,6 +258,34 @@ plt.savefig("docs/fig/triangle.svg")
259
258
  ![](fig/triangle.svg)
260
259
  ~~~
261
260
 
261
+ Using these attributes it is possible to write in Entangled using completely standard Markdown syntax. The following configuration disables the curly braces alltogether, though currently the quarto tags encoding the meta-data will end-up in the tangled code.
262
+
263
+ ```toml
264
+ #| file: entangled.toml
265
+ version="2.0"
266
+ watch_list=["*.typ"]
267
+ hooks=["quarto_attributes"]
268
+
269
+ [markers]
270
+ open="^(?P<indent>\\s*)```(?P<properties>.*)$"
271
+ close="^(?P<indent>\\s*)```\\s*$"
272
+ ```
273
+
274
+ Then you can write code like so:
275
+
276
+ ~~~markdown
277
+ ```python
278
+ #| id: hello
279
+ print("Hello, World!")
280
+ ```
281
+
282
+ ```python
283
+ #| file: test.py
284
+ if __name__ == "__main__":
285
+ <<hello>>
286
+ ```
287
+ ~~~
288
+
262
289
  ## Brei
263
290
  Entangled has a small build engine (similar to GNU Make) embedded, called Brei. You may give it a list of tasks (specified in TOML) that may depend on one another. Brei will run these when dependencies are newer than the target. Execution is lazy and in parallel. Brei supports:
264
291
 
@@ -389,6 +416,19 @@ Example: [Hello World in C++](https://entangled.github.io/examples/hello-world.h
389
416
 
390
417
  Example: TBD
391
418
 
419
+ ### Typst
420
+ [Typst](https://typst.app/) has a syntax that is similar to Markdown when it comes to code blocks. Set the code block markers in `entangled.toml` like so:
421
+
422
+ ```toml
423
+ version="2.0"
424
+ watch_list=["*.typ"]
425
+ hooks=["quarto_attributes"]
426
+
427
+ [markers]
428
+ open="^(?P<indent>\\s*)```(?P<properties>.*)$"
429
+ close="^(?P<indent>\\s*)```\\s*$"
430
+ ```
431
+
392
432
  ### Documenter.jl
393
433
  [Documenter.jl]() is the standard tool to write Julia documention in. It has internal support for evaluating code block contents.
394
434
 
@@ -32,9 +32,6 @@ pip install entangled-cli
32
32
  ```
33
33
 
34
34
  ## Use
35
-
36
- > :warning: **This version of Entangled is still in beta.** In general things are working as they should, but there may still be some rough edges in the user experience.
37
-
38
35
  Run the `entangled watch` daemon in the root of your project folder. By default all Markdown files are monitored for fenced code blocks like so:
39
36
 
40
37
  ~~~markdown
@@ -145,9 +142,10 @@ Entangled has a system of *hooks*: these add actions to the tangling process:
145
142
 
146
143
  - `build` trigger actions in a generated `Makefile`
147
144
  - `brei` trigger actions (or tasks) using [Brei](https://entangled.github.io/brei), which is automatically installed along with Entangled. This is now prefered over the `build` hook.
148
- - `quatro_attributes` add attributes to the code block in Quatro style with `#|` comments at the top of the code block.
145
+ - `quarto_attributes` add attributes to the code block in Quatro style with `#|` comments at the top of the code block.
146
+ - `shebang` takes the first line if it starts with `#!` and puts it at the top of the file.
149
147
 
150
- ## `build` hook
148
+ ### `build` hook
151
149
  You can enable this hook in `entangled.toml`:
152
150
 
153
151
  ```toml
@@ -233,6 +231,34 @@ plt.savefig("docs/fig/triangle.svg")
233
231
  ![](fig/triangle.svg)
234
232
  ~~~
235
233
 
234
+ Using these attributes it is possible to write in Entangled using completely standard Markdown syntax. The following configuration disables the curly braces alltogether, though currently the quarto tags encoding the meta-data will end-up in the tangled code.
235
+
236
+ ```toml
237
+ #| file: entangled.toml
238
+ version="2.0"
239
+ watch_list=["*.typ"]
240
+ hooks=["quarto_attributes"]
241
+
242
+ [markers]
243
+ open="^(?P<indent>\\s*)```(?P<properties>.*)$"
244
+ close="^(?P<indent>\\s*)```\\s*$"
245
+ ```
246
+
247
+ Then you can write code like so:
248
+
249
+ ~~~markdown
250
+ ```python
251
+ #| id: hello
252
+ print("Hello, World!")
253
+ ```
254
+
255
+ ```python
256
+ #| file: test.py
257
+ if __name__ == "__main__":
258
+ <<hello>>
259
+ ```
260
+ ~~~
261
+
236
262
  ## Brei
237
263
  Entangled has a small build engine (similar to GNU Make) embedded, called Brei. You may give it a list of tasks (specified in TOML) that may depend on one another. Brei will run these when dependencies are newer than the target. Execution is lazy and in parallel. Brei supports:
238
264
 
@@ -363,6 +389,19 @@ Example: [Hello World in C++](https://entangled.github.io/examples/hello-world.h
363
389
 
364
390
  Example: TBD
365
391
 
392
+ ### Typst
393
+ [Typst](https://typst.app/) has a syntax that is similar to Markdown when it comes to code blocks. Set the code block markers in `entangled.toml` like so:
394
+
395
+ ```toml
396
+ version="2.0"
397
+ watch_list=["*.typ"]
398
+ hooks=["quarto_attributes"]
399
+
400
+ [markers]
401
+ open="^(?P<indent>\\s*)```(?P<properties>.*)$"
402
+ close="^(?P<indent>\\s*)```\\s*$"
403
+ ```
404
+
366
405
  ### Documenter.jl
367
406
  [Documenter.jl]() is the standard tool to write Julia documention in. It has internal support for evaluating code block contents.
368
407
 
@@ -29,8 +29,9 @@ def stitch(*, force: bool = False, show: bool = False):
29
29
  config.read()
30
30
 
31
31
  # these imports depend on config being read
32
- from ..markdown_reader import MarkdownReader
32
+ from ..markdown_reader import read_markdown_file
33
33
  from ..code_reader import CodeReader
34
+ from ..hooks import get_hooks
34
35
 
35
36
  include_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
36
37
  exclude_file_list = list(
@@ -39,6 +40,7 @@ def stitch(*, force: bool = False, show: bool = False):
39
40
  input_file_list = [
40
41
  path for path in include_file_list if not path in exclude_file_list
41
42
  ]
43
+ hooks = get_hooks()
42
44
 
43
45
  if show:
44
46
  mode = TransactionMode.SHOW
@@ -52,10 +54,8 @@ def stitch(*, force: bool = False, show: bool = False):
52
54
  try:
53
55
  for path in input_file_list:
54
56
  logging.debug("reading `%s`", path)
55
- with open(path, "r") as f:
56
- mr = MarkdownReader(str(path), refs)
57
- mr.run(f.read())
58
- content[path] = mr.content
57
+ _, c = read_markdown_file(path, refs=refs, hooks=hooks)
58
+ content[path] = c
59
59
 
60
60
  with transaction(mode) as t:
61
61
  for path in t.db.managed:
@@ -25,7 +25,7 @@ def tangle(*, annotate: Optional[str] = None, force: bool = False, show: bool =
25
25
  config.read()
26
26
 
27
27
  # these imports depend on config being read
28
- from ..markdown_reader import MarkdownReader
28
+ from ..markdown_reader import read_markdown_file
29
29
  from ..tangle import tangle_ref
30
30
 
31
31
  if annotate is None:
@@ -43,6 +43,7 @@ def tangle(*, annotate: Optional[str] = None, force: bool = False, show: bool =
43
43
 
44
44
  refs = ReferenceMap()
45
45
  hooks = get_hooks()
46
+ logging.debug("tangling with hooks: %s", [h.__module__ for h in hooks])
46
47
 
47
48
  if show:
48
49
  mode = TransactionMode.SHOW
@@ -56,8 +57,7 @@ def tangle(*, annotate: Optional[str] = None, force: bool = False, show: bool =
56
57
  for path in input_file_list:
57
58
  logging.debug("reading `%s`", path)
58
59
  t.update(path)
59
- with open(path, "r") as f:
60
- MarkdownReader(str(path), refs, hooks).run(f.read())
60
+ read_markdown_file(path, refs=refs, hooks=hooks)
61
61
 
62
62
  for h in hooks:
63
63
  h.pre_tangle(refs)
@@ -83,11 +83,11 @@ class Config(threading.local):
83
83
  languages: list[Language] = field(default_factory=list)
84
84
  markers: Markers = field(default_factory=lambda: copy(markers))
85
85
  watch_list: list[str] = field(default_factory=lambda: ["**/*.md"])
86
- ignore_list: list[str] = field(default_factory=lambda: ["**/README.md"])
86
+ ignore_list: list[str] = field(default_factory=lambda: ["README.md"])
87
87
  annotation_format: Optional[str] = None
88
88
  annotation: AnnotationMethod = AnnotationMethod.STANDARD
89
89
  use_line_directives: bool = False
90
- hooks: list[str] = field(default_factory=list)
90
+ hooks: list[str] = field(default_factory=lambda: ["shebang"])
91
91
  hook: dict = field(default_factory=dict)
92
92
  brei: Program = field(default_factory=Program)
93
93
 
@@ -29,18 +29,19 @@ class PlainText:
29
29
  content: str
30
30
 
31
31
 
32
- Content = Union[PlainText, ReferenceId]
33
-
34
-
35
32
  @dataclass
36
33
  class CodeBlock:
37
- language: Language
38
34
  properties: list[Property]
39
35
  indent: str
40
- header: Optional[str]
41
36
  source: str
42
37
  origin: TextLocation
43
- mode: Optional[int]
38
+ language: Optional[Language] = None
39
+ header: Optional[str] = None
40
+ mode: Optional[int] = None
41
+
42
+
43
+ Content = Union[PlainText, ReferenceId]
44
+ RawContent = Union[PlainText, CodeBlock]
44
45
 
45
46
 
46
47
  @dataclass
@@ -57,11 +58,17 @@ class ReferenceMap:
57
58
  default_factory=lambda: defaultdict(list)
58
59
  )
59
60
  targets: set[str] = field(default_factory=set)
61
+ alias: dict[str, str] = field(default_factory=dict)
60
62
 
61
63
  def names(self) -> Iterable[str]:
62
64
  return self.index.keys()
63
65
 
64
66
  def by_name(self, n: str) -> Iterable[CodeBlock]:
67
+ if n not in self.index and n not in self.alias:
68
+ raise AttributeError(name=n, obj=self)
69
+ if n not in self.index:
70
+ return self.by_name(self.alias[n])
71
+
65
72
  return (self.map[r] for r in self.index[n])
66
73
 
67
74
  def new_id(self, filename: str, name: str) -> ReferenceId:
@@ -2,11 +2,14 @@ import logging
2
2
  from importlib.metadata import entry_points
3
3
 
4
4
  from .base import HookBase, PrerequisitesFailed
5
- from . import build, task, quarto_attributes
5
+ from . import build, task, quarto_attributes, shebang
6
6
  from ..config import config
7
7
  from ..construct import construct
8
+ from typing import TypeVar
8
9
 
9
10
 
11
+ AbstractHook = TypeVar('AbstractHook', bound=HookBase)
12
+
10
13
  discovered_hooks = entry_points(group="entangled.hooks")
11
14
 
12
15
 
@@ -18,6 +21,7 @@ external_hooks = {
18
21
  hooks: dict[str, type[HookBase]] = {
19
22
  "build": build.Hook,
20
23
  "brei": task.Hook,
24
+ "shebang": shebang.Hook,
21
25
  "quarto_attributes": quarto_attributes.Hook }
22
26
 
23
27
 
@@ -37,4 +41,4 @@ def get_hooks() -> list[HookBase]:
37
41
  return active_hooks
38
42
 
39
43
 
40
- __all__ = ["hooks", "PrerequisitesFailed"]
44
+ __all__ = ["hooks", "PrerequisitesFailed", "get_hooks"]
@@ -15,6 +15,7 @@ class PrerequisitesFailed(Exception):
15
15
 
16
16
 
17
17
  class HookBase:
18
+ @dataclass
18
19
  class Config:
19
20
  pass
20
21
 
@@ -25,13 +26,8 @@ class HookBase:
25
26
  """When prerequisites aren't met, raise PrerequisitesFailed."""
26
27
  return
27
28
 
28
- def condition(self, props: list[Property]):
29
- """Condition upon which the `on_read` is triggered. Maybe a bit superfluous
30
- could be removed in the future."""
31
- return True
32
-
33
- def on_read(self, refs: ReferenceMap, ref: ReferenceId, code: CodeBlock):
34
- """Called when the Markdown is being read, during the assembling of
29
+ def on_read(self, code: CodeBlock):
30
+ """Called when the Markdown is being read, before the assembling of
35
31
  the reference map."""
36
32
  pass
37
33
 
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  from dataclasses import dataclass, field
10
10
  from pathlib import Path
11
11
  from subprocess import run, SubprocessError, DEVNULL
12
+ import logging
12
13
 
13
14
  from entangled.config.language import Language
14
15
 
@@ -72,29 +73,29 @@ class Hook(HookBase):
72
73
  except (SubprocessError, FileNotFoundError):
73
74
  raise PrerequisitesFailed("GNU Make needs to be installed")
74
75
 
75
- def condition(self, props: list[Property]):
76
- """Condition by which a CodeBlock is processed: should have `.build` class
77
- and `target=` attribute."""
78
- return (
79
- "build" in get_classes(props) and get_attribute(props, "target") is not None
80
- )
81
-
82
- def on_read(self, refs: ReferenceMap, ref: ReferenceId, cb: CodeBlock):
76
+ def pre_tangle(self, refs: ReferenceMap):
83
77
  """Add a CodeBlock's target attribute to the list of targets."""
84
- target = get_attribute(cb.properties, "target")
85
- if target is None:
86
- return
87
-
88
- script_file_name = get_attribute(cb.properties, "file")
89
- if script_file_name is None:
90
- script_file_name = f".entangled/build/{ref.name}".replace(":", "_")
91
- refs.index[script_file_name].append(ref)
92
- refs.targets.add(script_file_name)
93
-
94
- deps = (get_attribute(cb.properties, "deps") or "").split()
95
- self.recipes.append(Hook.Recipe(target, deps, cb.language, script_file_name))
96
-
97
- def post_tangle(self, _: ReferenceMap):
78
+ for (ref, cb) in refs.map.items():
79
+ logging.debug("build hook: passing: %s", ref)
80
+ if "build" not in get_classes(cb.properties):
81
+ continue
82
+ target = get_attribute(cb.properties, "target")
83
+ if target is None:
84
+ continue
85
+ if cb.language is None:
86
+ continue
87
+
88
+ logging.debug("build hook: target: %s", target)
89
+ script_file_name = get_attribute(cb.properties, "file")
90
+ if script_file_name is None:
91
+ script_file_name = f".entangled/build/{ref.name}".replace(":", "_")
92
+ refs.index[script_file_name].append(ref)
93
+ refs.targets.add(script_file_name)
94
+
95
+ deps = (get_attribute(cb.properties, "deps") or "").split()
96
+ self.recipes.append(Hook.Recipe(target, deps, cb.language, script_file_name))
97
+
98
+ def post_tangle(self, refs: ReferenceMap):
98
99
  """After all code is tangled: retrieve the build scripts and run it."""
99
100
  targets = " ".join([r.target for r in self.recipes])
100
101
  makefile = preamble.format(
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from itertools import takewhile
4
+ import yaml
5
+
6
+ from ..properties import Attribute, Id
7
+ from ..document import ReferenceId, ReferenceMap, CodeBlock
8
+ from .base import HookBase
9
+ from ..logging import logger
10
+
11
+
12
+ log = logger()
13
+
14
+
15
+ class Hook(HookBase):
16
+ @dataclass
17
+ class Config(HookBase.Config):
18
+ pass
19
+
20
+ def __init__(self, config: Hook.Config):
21
+ self.config = config
22
+
23
+ def on_read(self, code: CodeBlock):
24
+ log.debug(f"quarto filter: %s", code)
25
+
26
+ if code.language is None:
27
+ return
28
+
29
+ trigger = f"{code.language.comment.open}|"
30
+ header = "\n".join(
31
+ line[len(trigger):] for line in code.source.splitlines()
32
+ if line.startswith(trigger))
33
+
34
+ attrs = yaml.safe_load(header)
35
+ if "id" in attrs:
36
+ code.properties.append(Id(attrs["id"]))
37
+ code.properties.extend(Attribute(k, v) for k, v in attrs.items())
38
+
39
+ log.debug(f"quarto attributes: {attrs}")
@@ -0,0 +1,10 @@
1
+ from ..document import CodeBlock
2
+ from .base import HookBase
3
+
4
+
5
+ class Hook(HookBase):
6
+ def on_read(self, code: CodeBlock):
7
+ lines = code.source.splitlines()
8
+ if lines[0].startswith("#!"):
9
+ code.header = lines[0]
10
+ code.source = "\n".join(lines[1:])
@@ -56,41 +56,40 @@ class Hook(HookBase):
56
56
  self.config = config
57
57
  self.sources: list[Path] = []
58
58
 
59
- def condition(self, props: list[Property]):
60
- return (
61
- "task" in get_classes(props)
62
- )
59
+ def pre_tangle(self, refs: ReferenceMap):
60
+ for ref, cb in refs.map.items():
61
+ if "task" not in get_classes(cb.properties):
62
+ continue
63
63
 
64
- def on_read(self, refs: ReferenceMap, ref: ReferenceId, cb: CodeBlock):
65
- self.sources.append(Path(ref.file))
64
+ self.sources.append(Path(ref.file))
66
65
 
67
- match cb.properties[0]:
68
- case Class("task"):
69
- runner = None
70
- case Class(lang_id):
71
- runner = lang_id
72
- case _:
73
- return
66
+ match cb.properties[0]:
67
+ case Class("task"):
68
+ runner = None
69
+ case Class(lang_id):
70
+ runner = lang_id
71
+ case _:
72
+ continue
74
73
 
75
- record: dict[str, Any] = {
76
- f.name: get_attribute(cb.properties, f.name)
77
- for f in fields(Hook.Recipe)
78
- }
74
+ record: dict[str, Any] = {
75
+ f.name: get_attribute(cb.properties, f.name)
76
+ for f in fields(Hook.Recipe)
77
+ }
78
+
79
+ record["runner"] = record["runner"] or runner
80
+ record["creates"] = record["creates"].split() if record["creates"] else None
81
+ record["requires"] = record["requires"].split() if record["requires"] else None
82
+ record["ref"] = ref
79
83
 
80
- record["runner"] = record["runner"] or runner
81
- record["creates"] = record["creates"].split() if record["creates"] else None
82
- record["requires"] = record["requires"].split() if record["requires"] else None
83
- record["ref"] = ref
84
+ log.debug(f"task: {record}")
85
+ recipe = Hook.Recipe(**record)
86
+ self.recipes.append(recipe)
87
+ if recipe.collect:
88
+ targets = recipe.creates or []
89
+ if recipe.stdout:
90
+ targets.append(recipe.stdout)
91
+ self.collections[recipe.collect].extend(targets)
84
92
 
85
- log.debug(f"task: {record}")
86
- recipe = Hook.Recipe(**record)
87
- self.recipes.append(recipe)
88
- if recipe.collect:
89
- targets = recipe.creates or []
90
- if recipe.stdout:
91
- targets.append(recipe.stdout)
92
- self.collections[recipe.collect].extend(targets)
93
-
94
93
  def on_tangle(self, t: Transaction, refs: ReferenceMap):
95
94
  collect = [{
96
95
  "name": k,
@@ -100,4 +99,3 @@ class Hook(HookBase):
100
99
  "task": [r.to_brei_task(refs) for r in self.recipes] + collect
101
100
  }, indent=2)
102
101
  t.write(Path(".entangled/tasks.json"), content, self.sources, mode=0o444)
103
-
@@ -8,35 +8,28 @@ import logging
8
8
 
9
9
  from .config import config
10
10
  from .utility import first
11
- from .document import TextLocation, CodeBlock, ReferenceMap, Content, PlainText
11
+ from .document import TextLocation, CodeBlock, ReferenceMap, Content, PlainText, RawContent
12
12
  from .properties import read_properties, get_attribute, get_classes, get_id
13
13
  from .hooks.base import HookBase
14
14
  from .errors.user import ParseError, IndentationError
15
15
  from . import parsing
16
16
 
17
17
 
18
- class MarkdownReader(mawk.RuleSet):
18
+ class MarkdownLexer(mawk.RuleSet):
19
19
  """Reads a Markdown file, and splits it up into code blocks and other
20
- content. The contents of the code blocks get stored in `reference_map`.
21
- """
22
-
20
+ content."""
23
21
  def __init__(
24
22
  self,
25
- filename: str,
26
- refs: Optional[ReferenceMap] = None,
27
- hooks: Optional[list[HookBase]] = None,
23
+ filename: str
28
24
  ):
29
25
  self.location = TextLocation(filename)
30
- self.reference_map = refs or ReferenceMap()
31
- self.content: list[Content] = []
26
+ self.raw_content: list[RawContent] = []
32
27
  self.inside_codeblock: bool = False
33
28
  self.current_content: list[str] = []
34
- self.current_header: list[str] = []
35
29
  self.ignore = False
36
- self.hooks = hooks or []
37
30
 
38
31
  def flush_plain_text(self):
39
- self.content.append(PlainText("\n".join(self.current_content)))
32
+ self.raw_content.append(PlainText("\n".join(self.current_content)))
40
33
  self.current_content = []
41
34
 
42
35
  @mawk.always
@@ -59,11 +52,13 @@ class MarkdownReader(mawk.RuleSet):
59
52
  return None
60
53
  if self.inside_codeblock:
61
54
  return None
55
+ logging.debug("triggered on codeblock: %s", m.group(0))
62
56
  self.current_codeblock_indent = m["indent"]
63
57
  self.current_codeblock_location = copy(self.location)
64
58
  self.current_content.append(m[0])
65
59
  try:
66
60
  self.current_codeblock_properties = read_properties(m["properties"])
61
+ logging.debug("properties: %s", self.current_codeblock_properties)
67
62
  self.flush_plain_text()
68
63
  self.inside_codeblock = True
69
64
  except parsing.Failure as f:
@@ -84,69 +79,32 @@ class MarkdownReader(mawk.RuleSet):
84
79
  if m["indent"] != self.current_codeblock_indent:
85
80
  return # treat this as code-block content
86
81
 
87
- # add block to reference-map
88
82
  language_class = first(get_classes(self.current_codeblock_properties))
89
- block_id = get_id(self.current_codeblock_properties)
90
- target_file = get_attribute(self.current_codeblock_properties, "file")
91
83
  language = config.get_language(language_class) if language_class else None
92
-
93
84
  if language_class and not language:
94
85
  logging.warning(f"Language `{language_class}` unknown at `{self.location}`.")
95
86
 
96
- header = (
97
- "\n".join(
98
- line.removeprefix(self.current_codeblock_indent)
99
- for line in self.current_header
100
- )
101
- if self.current_header
102
- else None
103
- )
87
+
104
88
  content = "\n".join(
105
89
  line.removeprefix(self.current_codeblock_indent)
106
90
  for line in self.current_content
107
91
  )
108
92
 
109
- ref_name = block_id or target_file
110
- if ref_name is None:
111
- ref_name = f"unnamed-{self.location}"
93
+ code = CodeBlock(
94
+ self.current_codeblock_properties,
95
+ self.current_codeblock_indent,
96
+ content,
97
+ self.current_codeblock_location,
98
+ language
99
+ )
112
100
 
113
- if language is None:
114
- self.flush_plain_text()
115
- else:
116
- ref = self.reference_map.new_id(
117
- self.current_codeblock_location.filename, ref_name
118
- )
119
- mode = get_attribute(self.current_codeblock_properties, "mode")
120
- code = CodeBlock(
121
- language,
122
- self.current_codeblock_properties,
123
- self.current_codeblock_indent,
124
- header,
125
- content,
126
- self.current_codeblock_location,
127
- int(mode, 8) if mode else None
128
- )
129
- # logging.debug(repr(code))
130
- self.reference_map[ref] = code
131
- if target_file is not None:
132
- self.reference_map.targets.add(target_file)
133
- self.content.append(ref)
134
- self.current_content = []
135
-
136
- for h in self.hooks:
137
- if h.condition(self.current_codeblock_properties):
138
- h.on_read(self.reference_map, ref, code)
101
+ self.raw_content.append(code)
102
+ self.current_content = []
139
103
 
140
104
  self.current_content.append(m[0])
141
105
  self.inside_codeblock = False
142
106
  return []
143
107
 
144
- @mawk.on_match(r"^\s*#!.*$")
145
- def on_shebang(self, m: re.Match):
146
- if self.inside_codeblock and not self.current_content:
147
- self.current_header.append(m[0])
148
- return []
149
-
150
108
  @mawk.always
151
109
  def add_line(self, line: str):
152
110
  self.current_content.append(line)
@@ -157,9 +115,54 @@ class MarkdownReader(mawk.RuleSet):
157
115
  return []
158
116
 
159
117
 
160
- def read_markdown(path: Path) -> tuple[ReferenceMap, list[Content]]:
118
+ def read_markdown_file(
119
+ path: Path,
120
+ refs: ReferenceMap | None = None,
121
+ hooks: list[HookBase] | None = None) \
122
+ -> tuple[ReferenceMap, list[Content]]:
123
+
161
124
  with open(path, "r") as f:
162
125
  path_str = str(path.resolve().relative_to(Path.cwd()))
163
- md = MarkdownReader(path_str)
164
- md.run(f.read())
165
- return md.reference_map, md.content
126
+ return read_markdown_string(f.read(), path_str, refs, hooks)
127
+
128
+
129
+ def read_markdown_string(
130
+ text: str,
131
+ path_str: str = "-",
132
+ refs: ReferenceMap | None = None,
133
+ hooks: list[HookBase] | None = None) \
134
+ -> tuple[ReferenceMap, list[Content]]:
135
+ md = MarkdownLexer(path_str)
136
+ md.run(text)
137
+
138
+ hooks = hooks if hooks is not None else []
139
+ refs = refs if refs is not None else ReferenceMap()
140
+
141
+ def process(r: RawContent) -> Content:
142
+ match r:
143
+ case CodeBlock():
144
+ for h in hooks: h.on_read(r)
145
+ block_id = get_id(r.properties)
146
+ target_file = get_attribute(r.properties, "file")
147
+
148
+ if mode := get_attribute(r.properties, "mode"):
149
+ r.mode = int(mode, 8)
150
+
151
+ ref_name = block_id or target_file
152
+ if ref_name is None:
153
+ ref_name = f"unnamed-{r.origin}"
154
+ ref = refs.new_id(r.origin.filename, ref_name)
155
+
156
+ refs[ref] = r
157
+ if target_file is not None:
158
+ refs.targets.add(target_file)
159
+ if target_file is not None and block_id is not None:
160
+ refs.alias[target_file] = block_id
161
+
162
+ return ref
163
+
164
+ case PlainText(): return r
165
+
166
+ content = list(map(process, md.raw_content))
167
+ logging.debug("found ids: %s", list(refs.map.keys()))
168
+ return refs, content
@@ -35,7 +35,7 @@ class Id(Parsable):
35
35
  @dataclass
36
36
  class Class(Parsable):
37
37
  value: str
38
- _pattern: ClassVar[Parser] = matching(r"\.([a-zA-Z]\S*)")
38
+ _pattern: ClassVar[Parser] = matching(r"\.?([a-zA-Z]\S*)")
39
39
 
40
40
  def __str__(self):
41
41
  return f".{self.value}"
@@ -74,7 +74,7 @@ def read_properties(inp: str) -> list[Property]:
74
74
  """
75
75
  # Explicit typing is needed to convince MyPy of correctness
76
76
  # parsers: list[Parser[Property]] = [Id, Class, Attribute]
77
- result, _ = many(tokenize(choice(Id, Class, Attribute))).read(inp)
77
+ result, _ = many(tokenize(choice(Id, Attribute, Class))).read(inp)
78
78
  return result
79
79
 
80
80
 
@@ -119,6 +119,9 @@ def tangle_ref(
119
119
  if annotation is None:
120
120
  annotation = config.annotation
121
121
 
122
+ if ref_name in refs.alias:
123
+ ref_name = refs.alias[ref_name]
124
+
122
125
  if ref_name not in refs:
123
126
  raise KeyError(ref_name)
124
127
  v = _visited or Visitor()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "entangled-cli"
3
- version = "2.0.4"
3
+ version = "2.1.0"
4
4
  description = "Literate Programming toolbox"
5
5
  repository = "https://github.com/entangled/entangled.py"
6
6
  homepage = "https://entangled.github.io/"
@@ -21,6 +21,7 @@ copier = "^9" # project templates
21
21
  brei = "^0.2.3" # build system
22
22
  rich-argparse = "^1.4.0" # colourful help messages
23
23
  pexpect = "^4.9.0"
24
+ pyyaml = "^6.0.1"
24
25
 
25
26
  [tool.poetry.group.dev.dependencies]
26
27
  pytest = "^7.3.1"
@@ -33,6 +34,9 @@ mkdocstrings = { extras = ["python"], version = "^0.21.2" }
33
34
  twine = "^4.0.2"
34
35
  pytest-asyncio = "^0.21.1"
35
36
  ruff = "^0.4.4"
37
+ types-pyyaml = "^6.0.12.20240311"
38
+ types-pygments = "^2.18.0.20240506"
39
+ types-colorama = "^0.4.15.20240311"
36
40
 
37
41
  [build-system]
38
42
  requires = ["poetry-core"]
@@ -43,7 +47,6 @@ testpaths = ["test"]
43
47
 
44
48
  [tool.mypy]
45
49
  packages = ["entangled"]
46
- enable_incomplete_feature = ["TypeVarTuple"]
47
50
 
48
51
  [tool.poetry.scripts]
49
52
  entangled = "entangled.main:cli"
@@ -1,28 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass
3
- from itertools import takewhile
4
-
5
- from ..properties import Attribute
6
- from ..document import ReferenceId, ReferenceMap, CodeBlock
7
- from .base import HookBase
8
- from ..logging import logger
9
-
10
-
11
- log = logger()
12
-
13
-
14
- class Hook(HookBase):
15
- @dataclass
16
- class Config(HookBase.Config):
17
- pass
18
-
19
- def __init__(self, config: Hook.Config):
20
- self.config = config
21
-
22
- def on_read(self, refs: ReferenceMap, ref: ReferenceId, cb: CodeBlock):
23
- trigger = f"{cb.language.comment.open}|"
24
- header = (line[len(trigger):] for line in cb.source.splitlines() if line.startswith(trigger))
25
- attrs = [Attribute(k.strip(), v.strip()) for k, v in map(lambda line: line.split(":"), header)]
26
- cb.properties.extend(attrs)
27
- log.debug(f"quarto attrs on {ref}: {attrs}")
28
-
File without changes