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.
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/PKG-INFO +46 -6
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/README.md +44 -5
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/stitch.py +5 -5
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/tangle.py +3 -3
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/__init__.py +2 -2
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/document.py +13 -6
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/__init__.py +6 -2
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/base.py +3 -7
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/build.py +23 -22
- entangled_cli-2.1.0/entangled/hooks/quarto_attributes.py +39 -0
- entangled_cli-2.1.0/entangled/hooks/shebang.py +10 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/hooks/task.py +29 -31
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/markdown_reader.py +67 -64
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/properties.py +2 -2
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/tangle.py +3 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/pyproject.toml +5 -2
- entangled_cli-2.0.4/entangled/hooks/quarto_attributes.py +0 -28
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/LICENSE +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/__init__.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/code_reader.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/__init__.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/brei.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/new.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/status.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/sync.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/commands/watch.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/language.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/templates.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/config/version.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/construct.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/errors/user.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/filedb.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/logging.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/main.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/parsing.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/py.typed +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/status.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/text_location.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/transaction.py +0 -0
- {entangled_cli-2.0.4 → entangled_cli-2.1.0}/entangled/utility.py +0 -0
- {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
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|

|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|

|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
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: ["
|
|
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=
|
|
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
|
-
|
|
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
|
|
29
|
-
"""
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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}")
|
|
@@ -56,41 +56,40 @@ class Hook(HookBase):
|
|
|
56
56
|
self.config = config
|
|
57
57
|
self.sources: list[Path] = []
|
|
58
58
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
"task" in get_classes(
|
|
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
|
-
|
|
65
|
-
self.sources.append(Path(ref.file))
|
|
64
|
+
self.sources.append(Path(ref.file))
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
18
|
+
class MarkdownLexer(mawk.RuleSet):
|
|
19
19
|
"""Reads a Markdown file, and splits it up into code blocks and other
|
|
20
|
-
content.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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"
|
|
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,
|
|
77
|
+
result, _ = many(tokenize(choice(Id, Attribute, Class))).read(inp)
|
|
78
78
|
return result
|
|
79
79
|
|
|
80
80
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "entangled-cli"
|
|
3
|
-
version = "2.0
|
|
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
|
|
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
|