entangled-cli 2.0.0b2__tar.gz → 2.0.0b5__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.0b2 → entangled_cli-2.0.0b5}/PKG-INFO +48 -17
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/README.md +46 -14
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/commands/__init__.py +2 -1
- entangled_cli-2.0.0b5/entangled/commands/status.py +60 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/commands/stitch.py +1 -1
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/commands/tangle.py +3 -1
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/commands/watch.py +7 -6
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/config/__init__.py +35 -7
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/config/language.py +3 -2
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/construct.py +3 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/document.py +6 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/filedb.py +12 -6
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/hooks/__init__.py +9 -5
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/hooks/base.py +12 -3
- entangled_cli-2.0.0b5/entangled/hooks/build.py +104 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/main.py +2 -2
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/markdown_reader.py +12 -8
- entangled_cli-2.0.0b5/entangled/status.py +25 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/transaction.py +6 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/utility.py +0 -6
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/pyproject.toml +2 -5
- entangled_cli-2.0.0b2/entangled/hooks/build.py +0 -73
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/LICENSE +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/__init__.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/code_reader.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/commands/init.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/commands/sync.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/config/version.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/errors/user.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/parsing.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/properties.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/py.typed +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/entangled/tangle.py +0 -0
- {entangled_cli-2.0.0b2 → entangled_cli-2.0.0b5}/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.0.0b5
|
|
4
4
|
Summary: Literate Programming toolbox
|
|
5
5
|
Home-page: https://entangled.github.io/
|
|
6
6
|
License: Apache 2
|
|
@@ -10,11 +10,10 @@ Requires-Python: >=3.11,<4.0
|
|
|
10
10
|
Classifier: License :: Other/Proprietary License
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Provides-Extra: rich
|
|
14
13
|
Requires-Dist: argh (>=0.28.1,<0.29.0)
|
|
15
14
|
Requires-Dist: filelock (>=3.12.0,<4.0.0)
|
|
16
15
|
Requires-Dist: mawk (>=0.1.4,<0.2.0)
|
|
17
|
-
Requires-Dist: rich (>=13.3.5,<14.0.0)
|
|
16
|
+
Requires-Dist: rich (>=13.3.5,<14.0.0)
|
|
18
17
|
Requires-Dist: watchdog (>=3.0.0,<4.0.0)
|
|
19
18
|
Project-URL: Repository, https://github.com/entangled/entangled.py
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
@@ -148,6 +147,11 @@ And so on...
|
|
|
148
147
|
~~~
|
|
149
148
|
|
|
150
149
|
### Using the `build` hook
|
|
150
|
+
|
|
151
|
+
:::warning
|
|
152
|
+
The `build` hook has changed from version 2.0.0b3 to 2.0.0b4; Previously you would have to enter Makefile code yourself. Now, `target` and `deps` are specified as attributes to the codeblock, and the Makefile is generated for you. This means less features, but also a simpler interface that may be backed by other build systems than Make.
|
|
153
|
+
:::
|
|
154
|
+
|
|
151
155
|
Entangled has a system of *hooks*, of which there exists currently only one: `build`. You can enable this hook in `entangled.toml`:
|
|
152
156
|
|
|
153
157
|
```toml
|
|
@@ -156,30 +160,34 @@ watch_list = ["docs/**/*.md"]
|
|
|
156
160
|
hooks = ["build"]
|
|
157
161
|
```
|
|
158
162
|
|
|
159
|
-
Then in your Markdown, you may enter
|
|
163
|
+
Then in your Markdown, you may enter code tagged with the `.build` tag.
|
|
160
164
|
|
|
161
165
|
~~~markdown
|
|
162
|
-
``` {.
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
``` {.python .build target=docs/fig/plot.svg}
|
|
167
|
+
from matplotlib import pyplot as plt
|
|
168
|
+
import numpy as np
|
|
169
|
+
|
|
170
|
+
x = np.linspace(-np.pi, np.pi, 100)
|
|
171
|
+
y = np.sin(x)
|
|
172
|
+
plt.plot(x, y)
|
|
173
|
+
plt.savefig("docs/fig/plot.svg")
|
|
165
174
|
```
|
|
166
175
|
~~~
|
|
167
176
|
|
|
168
|
-
|
|
177
|
+
This code will be saved into a Python script in the `.entangled/build` directory, or if you specify the `file=` attribute some other location. Second, a `Makefile` is generated in `.entangled/build`, that can be invoked as,
|
|
169
178
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
.RECIPEPREFIX = >
|
|
174
|
-
.PHONY = all
|
|
179
|
+
```shell
|
|
180
|
+
make -f .entangled/build/Makefile
|
|
181
|
+
```
|
|
175
182
|
|
|
176
|
-
|
|
183
|
+
You may configure how code from different languages is evaluated in `entangled.toml`. For example, to add Gnuplot support, and also make Julia code run through `DaemonMode.jl`, you may do the following:
|
|
177
184
|
|
|
178
|
-
|
|
185
|
+
```toml
|
|
186
|
+
[hook.build.runners]
|
|
187
|
+
Gnuplot = "gnuplot {script}"
|
|
188
|
+
Julia = "julia --project=. --startup-file=no -e 'using DaemonMode; runargs()' {script}"
|
|
179
189
|
```
|
|
180
190
|
|
|
181
|
-
changing the prefix from `TAB` to `>` (you really want this), and collecting all `target=...` attributes into the `all` target.
|
|
182
|
-
|
|
183
191
|
Once you have the code in place to generate figures and markdown tables, you can use the syntax at your disposal to include those into your Markdown. In this example that would be
|
|
184
192
|
|
|
185
193
|
~~~markdown
|
|
@@ -188,6 +196,29 @@ Once you have the code in place to generate figures and markdown tables, you can
|
|
|
188
196
|
|
|
189
197
|
In the case of tables or other rich content, Standard Markdown (or CommonMark) has no syntax for including other Markdown files, so you'll have to check with your own document generator how to do that. In MkDocs, you could use `mkdocs-macro-plugin`, Pandoc has `pandoc-include`, etc.
|
|
190
198
|
|
|
199
|
+
You can also specify intermediate data generation like so:
|
|
200
|
+
|
|
201
|
+
~~~markdown
|
|
202
|
+
``` {.python .build target="data/result.csv"}
|
|
203
|
+
import numpy as np
|
|
204
|
+
import pandas as pd
|
|
205
|
+
|
|
206
|
+
result = np.random.normal(0.0, 1.0, (100, 2))
|
|
207
|
+
df = pd.DataFrame(result, columns=["x", "y"])
|
|
208
|
+
df.to_csv("data/result.csv")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
``` {.python .build target="fig/plot.svg" deps="data/result.csv"}
|
|
212
|
+
import pandas as pd
|
|
213
|
+
|
|
214
|
+
df = pd.read_csv("data/result.csv")
|
|
215
|
+
plot = df.plot()
|
|
216
|
+
plot.savefig("fig/plot.svg")
|
|
217
|
+
```
|
|
218
|
+
~~~
|
|
219
|
+
|
|
220
|
+
The snippet for generating the data is given as a dependency for that data; to generate the figure, both `result.csv` and the code snippet are dependencies.
|
|
221
|
+
|
|
191
222
|
#### Ideas for other hooks
|
|
192
223
|
|
|
193
224
|
> A: Did you hack it together?
|
|
@@ -127,6 +127,11 @@ And so on...
|
|
|
127
127
|
~~~
|
|
128
128
|
|
|
129
129
|
### Using the `build` hook
|
|
130
|
+
|
|
131
|
+
:::warning
|
|
132
|
+
The `build` hook has changed from version 2.0.0b3 to 2.0.0b4; Previously you would have to enter Makefile code yourself. Now, `target` and `deps` are specified as attributes to the codeblock, and the Makefile is generated for you. This means less features, but also a simpler interface that may be backed by other build systems than Make.
|
|
133
|
+
:::
|
|
134
|
+
|
|
130
135
|
Entangled has a system of *hooks*, of which there exists currently only one: `build`. You can enable this hook in `entangled.toml`:
|
|
131
136
|
|
|
132
137
|
```toml
|
|
@@ -135,30 +140,34 @@ watch_list = ["docs/**/*.md"]
|
|
|
135
140
|
hooks = ["build"]
|
|
136
141
|
```
|
|
137
142
|
|
|
138
|
-
Then in your Markdown, you may enter
|
|
143
|
+
Then in your Markdown, you may enter code tagged with the `.build` tag.
|
|
139
144
|
|
|
140
145
|
~~~markdown
|
|
141
|
-
``` {.
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
``` {.python .build target=docs/fig/plot.svg}
|
|
147
|
+
from matplotlib import pyplot as plt
|
|
148
|
+
import numpy as np
|
|
149
|
+
|
|
150
|
+
x = np.linspace(-np.pi, np.pi, 100)
|
|
151
|
+
y = np.sin(x)
|
|
152
|
+
plt.plot(x, y)
|
|
153
|
+
plt.savefig("docs/fig/plot.svg")
|
|
144
154
|
```
|
|
145
155
|
~~~
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
This code will be saved into a Python script in the `.entangled/build` directory, or if you specify the `file=` attribute some other location. Second, a `Makefile` is generated in `.entangled/build`, that can be invoked as,
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
.RECIPEPREFIX = >
|
|
153
|
-
.PHONY = all
|
|
159
|
+
```shell
|
|
160
|
+
make -f .entangled/build/Makefile
|
|
161
|
+
```
|
|
154
162
|
|
|
155
|
-
|
|
163
|
+
You may configure how code from different languages is evaluated in `entangled.toml`. For example, to add Gnuplot support, and also make Julia code run through `DaemonMode.jl`, you may do the following:
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
```toml
|
|
166
|
+
[hook.build.runners]
|
|
167
|
+
Gnuplot = "gnuplot {script}"
|
|
168
|
+
Julia = "julia --project=. --startup-file=no -e 'using DaemonMode; runargs()' {script}"
|
|
158
169
|
```
|
|
159
170
|
|
|
160
|
-
changing the prefix from `TAB` to `>` (you really want this), and collecting all `target=...` attributes into the `all` target.
|
|
161
|
-
|
|
162
171
|
Once you have the code in place to generate figures and markdown tables, you can use the syntax at your disposal to include those into your Markdown. In this example that would be
|
|
163
172
|
|
|
164
173
|
~~~markdown
|
|
@@ -167,6 +176,29 @@ Once you have the code in place to generate figures and markdown tables, you can
|
|
|
167
176
|
|
|
168
177
|
In the case of tables or other rich content, Standard Markdown (or CommonMark) has no syntax for including other Markdown files, so you'll have to check with your own document generator how to do that. In MkDocs, you could use `mkdocs-macro-plugin`, Pandoc has `pandoc-include`, etc.
|
|
169
178
|
|
|
179
|
+
You can also specify intermediate data generation like so:
|
|
180
|
+
|
|
181
|
+
~~~markdown
|
|
182
|
+
``` {.python .build target="data/result.csv"}
|
|
183
|
+
import numpy as np
|
|
184
|
+
import pandas as pd
|
|
185
|
+
|
|
186
|
+
result = np.random.normal(0.0, 1.0, (100, 2))
|
|
187
|
+
df = pd.DataFrame(result, columns=["x", "y"])
|
|
188
|
+
df.to_csv("data/result.csv")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
``` {.python .build target="fig/plot.svg" deps="data/result.csv"}
|
|
192
|
+
import pandas as pd
|
|
193
|
+
|
|
194
|
+
df = pd.read_csv("data/result.csv")
|
|
195
|
+
plot = df.plot()
|
|
196
|
+
plot.savefig("fig/plot.svg")
|
|
197
|
+
```
|
|
198
|
+
~~~
|
|
199
|
+
|
|
200
|
+
The snippet for generating the data is given as a dependency for that data; to generate the figure, both `result.csv` and the code snippet are dependencies.
|
|
201
|
+
|
|
170
202
|
#### Ideas for other hooks
|
|
171
203
|
|
|
172
204
|
> A: Did you hack it together?
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from ..status import find_watch_dirs, list_input_files, list_dependent_files
|
|
3
|
+
from ..config import config
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from rich.console import Console, Group
|
|
8
|
+
from rich.columns import Columns
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
def tree_from_files(files):
|
|
14
|
+
tree = Tree(label=".")
|
|
15
|
+
dirs = { Path("."): tree }
|
|
16
|
+
for f in sorted(files):
|
|
17
|
+
for p in reversed(f.parents):
|
|
18
|
+
if p not in dirs:
|
|
19
|
+
dirs[p] = dirs[p.parent].add(p.name, style="repr.path")
|
|
20
|
+
dirs[f.parent].add(f.name, style="repr.filename")
|
|
21
|
+
return tree
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def files_panel(file_list: list[str], title: str) -> Panel:
|
|
25
|
+
tree = tree_from_files(file_list)
|
|
26
|
+
return Panel(tree, title=title, border_style="dark_cyan")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def rich_status():
|
|
30
|
+
cfg = config.config
|
|
31
|
+
config_table = Table()
|
|
32
|
+
config_table.add_column("name")
|
|
33
|
+
config_table.add_column("value")
|
|
34
|
+
config_table.add_row("Watch list", ", ".join(f"'{pat}'" for pat in cfg.watch_list))
|
|
35
|
+
config_table.add_row("Hooks enabled", ", ".join(cfg.hooks))
|
|
36
|
+
|
|
37
|
+
console = Console(color_system="auto")
|
|
38
|
+
group = Group(
|
|
39
|
+
Panel(config_table, title="config", border_style="dark_cyan"),
|
|
40
|
+
Columns([
|
|
41
|
+
files_panel([str(f) for f in list_input_files()], "input files"),
|
|
42
|
+
files_panel([str(f) for f in list_dependent_files()], "dependent files"),
|
|
43
|
+
]),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console.print(group)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
WITH_RICH = True
|
|
50
|
+
except ImportError:
|
|
51
|
+
WITH_RICH = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def status():
|
|
55
|
+
if WITH_RICH:
|
|
56
|
+
rich_status()
|
|
57
|
+
else:
|
|
58
|
+
print(config)
|
|
59
|
+
print("---")
|
|
60
|
+
print("input files:", sorted(list_input_files()))
|
|
@@ -50,7 +50,7 @@ def stitch(*, force: bool = False, show: bool = False):
|
|
|
50
50
|
with transaction(mode) as t:
|
|
51
51
|
for path in t.db.managed:
|
|
52
52
|
logging.debug("reading `%s`", path)
|
|
53
|
-
t.
|
|
53
|
+
t.update(path)
|
|
54
54
|
with open(path, "r") as f:
|
|
55
55
|
CodeReader(str(path), refs).run(f.read())
|
|
56
56
|
|
|
@@ -44,17 +44,19 @@ def tangle(*, annotate: Optional[str] = None, force: bool = False, show: bool =
|
|
|
44
44
|
with transaction(mode) as t:
|
|
45
45
|
for path in input_file_list:
|
|
46
46
|
logging.debug("reading `%s`", path)
|
|
47
|
-
t.
|
|
47
|
+
t.update(path)
|
|
48
48
|
with open(path, "r") as f:
|
|
49
49
|
MarkdownReader(str(path), refs, hooks).run(f.read())
|
|
50
50
|
|
|
51
51
|
for tgt in refs.targets:
|
|
52
52
|
result, deps = tangle_ref(refs, tgt, annotation_method)
|
|
53
53
|
t.write(Path(tgt), result, list(map(Path, deps)))
|
|
54
|
+
|
|
54
55
|
t.clear_orphans()
|
|
55
56
|
|
|
56
57
|
for h in hooks:
|
|
57
58
|
h.post_tangle(refs)
|
|
58
59
|
|
|
60
|
+
|
|
59
61
|
except UserError as e:
|
|
60
62
|
logging.error(str(e))
|
|
@@ -8,25 +8,26 @@ from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
|
8
8
|
|
|
9
9
|
from .sync import sync
|
|
10
10
|
from ..config import config
|
|
11
|
-
from ..
|
|
11
|
+
from ..status import find_watch_dirs
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class EventHandler(FileSystemEventHandler):
|
|
15
15
|
def __init__(self):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
self.watched = code_dirs.union(markdown_dirs)
|
|
16
|
+
self.update_watched()
|
|
17
|
+
|
|
18
|
+
def update_watched(self):
|
|
19
|
+
self.watched = find_watch_dirs()
|
|
21
20
|
|
|
22
21
|
def on_any_event(self, event: FileSystemEvent):
|
|
23
22
|
if event.event_type == "opened":
|
|
24
23
|
return
|
|
24
|
+
config.read()
|
|
25
25
|
path = Path(event.src_path)
|
|
26
26
|
if path.is_relative_to(Path("./.entangled")):
|
|
27
27
|
return
|
|
28
28
|
if any(path.is_relative_to(p) for p in self.watched):
|
|
29
29
|
sync()
|
|
30
|
+
self.update_watched()
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _watch(_stop_event: Optional[Event] = None):
|
|
@@ -4,7 +4,7 @@ defaults and config loaded from `entangled.toml` in the work directory.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import Optional, ClassVar, TypeVar
|
|
7
|
+
from typing import Any, Optional, ClassVar, TypeVar
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from copy import copy
|
|
@@ -67,6 +67,7 @@ class Config(threading.local):
|
|
|
67
67
|
annotation: AnnotationMethod = AnnotationMethod.STANDARD
|
|
68
68
|
use_line_directives: bool = False
|
|
69
69
|
hooks: list[str] = field(default_factory=list)
|
|
70
|
+
hook: dict = field(default_factory=dict)
|
|
70
71
|
|
|
71
72
|
def __post_init__(self):
|
|
72
73
|
self.languages = languages + self.languages
|
|
@@ -82,16 +83,42 @@ class Config(threading.local):
|
|
|
82
83
|
default = Config(Version.from_string("2.0"))
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
def read_config_from_toml(path: Path, section: Optional[str] = None) -> Optional[Config]:
|
|
87
|
+
"""Read a config from given `path` in given `section`. The path should refer to
|
|
88
|
+
a TOML file that should decode to a `Config` object. If `section` is given, only
|
|
89
|
+
that section is decoded to a `Config` object. The `section` string may contain
|
|
90
|
+
periods to indicate deeper nesting.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
read_config_from_toml(Path("./pyproject.toml"), "tool.entangled")
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
if not path.exists():
|
|
99
|
+
return None
|
|
88
100
|
try:
|
|
89
|
-
with open(
|
|
101
|
+
with open(path, "rb") as f:
|
|
90
102
|
json = tomllib.load(f)
|
|
91
|
-
|
|
103
|
+
if section is not None:
|
|
104
|
+
for s in section.split("."):
|
|
105
|
+
json = json[s]
|
|
106
|
+
return construct(Config, json)
|
|
92
107
|
except ValueError as e:
|
|
93
108
|
logging.error("Could not read config: %s", e)
|
|
94
|
-
return
|
|
109
|
+
return None
|
|
110
|
+
except KeyError as e:
|
|
111
|
+
logging.debug("%s", e)
|
|
112
|
+
logging.debug("The config file %s should contain a section %s", path, section)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def read_config():
|
|
117
|
+
if Path("./entangled.toml").exists():
|
|
118
|
+
return read_config_from_toml(Path("./entangled.toml")) or default
|
|
119
|
+
if Path("./pyproject.toml").exists():
|
|
120
|
+
return read_config_from_toml(Path("./pyproject.toml"), "tool.entangled") or default
|
|
121
|
+
return default
|
|
95
122
|
|
|
96
123
|
|
|
97
124
|
class ConfigWrapper:
|
|
@@ -120,3 +147,4 @@ class ConfigWrapper:
|
|
|
120
147
|
config = ConfigWrapper(read_config())
|
|
121
148
|
"""The `config.config` variable is changed when the `config` module is loaded.
|
|
122
149
|
Config is read from `entangled.toml` file."""
|
|
150
|
+
|
|
@@ -26,7 +26,7 @@ class Language:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
languages = [
|
|
29
|
-
Language("
|
|
29
|
+
Language("Bash", ["sh", "bash"], Comment("#")),
|
|
30
30
|
Language("C", ["c", "cpp", "c++"], Comment("/*", "*/")),
|
|
31
31
|
Language("Python", ["python"], Comment("#")),
|
|
32
32
|
Language("Rust", ["rust"], Comment("//")),
|
|
@@ -36,9 +36,10 @@ languages = [
|
|
|
36
36
|
),
|
|
37
37
|
Language("Julia", ["julia"], Comment("#")),
|
|
38
38
|
Language("Java", ["java"], Comment("//")),
|
|
39
|
-
Language("
|
|
39
|
+
Language("PureScript", ["pure", "purs", "purescript"], Comment("--")),
|
|
40
40
|
Language("CSS", ["css"], Comment("/*", "*/")),
|
|
41
41
|
Language("Lua", ["lua"], Comment("--")),
|
|
42
42
|
Language("Make", ["make", "makefile"], Comment("#")),
|
|
43
43
|
Language("Gnuplot", ["gnuplot"], Comment("#")),
|
|
44
|
+
Language("TOML", ["toml"], Comment("#")),
|
|
44
45
|
]
|
|
@@ -28,6 +28,9 @@ def construct(annot, json):
|
|
|
28
28
|
if isinstance(json, str) and isinstance(annot, Parser):
|
|
29
29
|
result, _ = annot.read(json)
|
|
30
30
|
return result
|
|
31
|
+
if annot is dict or isgeneric(annot) and typing.get_origin(annot) is dict:
|
|
32
|
+
assert isinstance(json, dict)
|
|
33
|
+
return json
|
|
31
34
|
if isgeneric(annot) and typing.get_origin(annot) is list:
|
|
32
35
|
assert isinstance(json, list)
|
|
33
36
|
return [construct(typing.get_args(annot)[0], item) for item in json]
|
|
@@ -51,6 +51,12 @@ class CodeBlock:
|
|
|
51
51
|
|
|
52
52
|
@dataclass
|
|
53
53
|
class ReferenceMap:
|
|
54
|
+
"""
|
|
55
|
+
Members:
|
|
56
|
+
`map`: maps references to actual code block content
|
|
57
|
+
`index`: maps names to list of references
|
|
58
|
+
`targets`: lists filenames; a target should be in `index`
|
|
59
|
+
"""
|
|
54
60
|
map: dict[ReferenceId, CodeBlock] = field(default_factory=dict)
|
|
55
61
|
index: defaultdict[str, list[ReferenceId]] = field(
|
|
56
62
|
default_factory=lambda: defaultdict(list)
|
|
@@ -16,6 +16,14 @@ from .version import __version__
|
|
|
16
16
|
from .utility import normal_relative, ensure_parent
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def hexdigest(s: str) -> str:
|
|
20
|
+
"""Creates a MD5 hash digest from a string. Before hashing, the string has
|
|
21
|
+
linefeed `\\r` characters and trailing newlines removed, and the string
|
|
22
|
+
is encoded as UTF-8."""
|
|
23
|
+
content = s.replace("\r", "").rstrip("\n").encode()
|
|
24
|
+
return hashlib.sha256(content).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
19
27
|
@dataclass
|
|
20
28
|
class FileStat:
|
|
21
29
|
path: Path
|
|
@@ -26,10 +34,10 @@ class FileStat:
|
|
|
26
34
|
@staticmethod
|
|
27
35
|
def from_path(path: Path, deps: Optional[list[Path]]):
|
|
28
36
|
stat = os.stat(path)
|
|
29
|
-
with open(path, "
|
|
30
|
-
|
|
37
|
+
with open(path, "r") as f:
|
|
38
|
+
digest = hexdigest(f.read())
|
|
31
39
|
return FileStat(
|
|
32
|
-
path, deps, datetime.fromtimestamp(stat.st_mtime),
|
|
40
|
+
path, deps, datetime.fromtimestamp(stat.st_mtime), digest
|
|
33
41
|
)
|
|
34
42
|
|
|
35
43
|
def __lt__(self, other: FileStat) -> bool:
|
|
@@ -136,9 +144,7 @@ class FileDB:
|
|
|
136
144
|
return self._files.keys()
|
|
137
145
|
|
|
138
146
|
def check(self, path: Path, content: str) -> bool:
|
|
139
|
-
return (
|
|
140
|
-
hashlib.sha256(content.encode()).hexdigest() == self._files[path].hexdigest
|
|
141
|
-
)
|
|
147
|
+
return hexdigest(content) == self._files[path].hexdigest
|
|
142
148
|
|
|
143
149
|
@staticmethod
|
|
144
150
|
def initialize() -> FileDB:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from
|
|
3
|
-
from .
|
|
2
|
+
from typing import Any
|
|
3
|
+
from .base import HookBase, PrerequisitesFailed
|
|
4
|
+
from . import build
|
|
4
5
|
from ..config import config
|
|
6
|
+
from ..construct import construct
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
hooks: dict[str, type[HookBase]] = {"build":
|
|
9
|
+
hooks: dict[str, type[HookBase]] = {"build": build.Hook}
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def get_hooks() -> list[HookBase]:
|
|
@@ -12,8 +14,10 @@ def get_hooks() -> list[HookBase]:
|
|
|
12
14
|
for h in config.hooks:
|
|
13
15
|
if h in hooks:
|
|
14
16
|
try:
|
|
15
|
-
hooks[h].
|
|
16
|
-
|
|
17
|
+
hook_cfg = construct(hooks[h].Config, config.hook.get(h, {}))
|
|
18
|
+
hook_instance = hooks[h](hook_cfg)
|
|
19
|
+
hook_instance.check_prerequisites()
|
|
20
|
+
active_hooks.append(hook_instance)
|
|
17
21
|
except PrerequisitesFailed as e:
|
|
18
22
|
logging.error("hook `%s`: %s", h, str(e))
|
|
19
23
|
else:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from ..properties import Property
|
|
4
5
|
from ..document import ReferenceMap, ReferenceId, CodeBlock
|
|
@@ -13,16 +14,24 @@ class PrerequisitesFailed(Exception):
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class HookBase:
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
class Config:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Config):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def check_prerequisites(self):
|
|
18
24
|
"""When prerequisites aren't met, raise PrerequisitesFailed."""
|
|
19
25
|
return
|
|
20
26
|
|
|
21
27
|
def condition(self, props: list[Property]):
|
|
22
28
|
raise NotImplementedError()
|
|
23
29
|
|
|
24
|
-
def on_read(self, refs: ReferenceId, code: CodeBlock):
|
|
30
|
+
def on_read(self, refs: ReferenceMap, ref: ReferenceId, code: CodeBlock):
|
|
25
31
|
raise NotImplementedError()
|
|
26
32
|
|
|
33
|
+
def pre_tangle(self, refs: ReferenceMap):
|
|
34
|
+
pass
|
|
35
|
+
|
|
27
36
|
def post_tangle(self, refs: ReferenceMap):
|
|
28
37
|
pass
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `build` hook collects code blocks that are tagged with the `#build`
|
|
3
|
+
identifier and have a `target=` attribute defined. These code blocks are put
|
|
4
|
+
together into a temporary Makefile that is run from the current working
|
|
5
|
+
directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from subprocess import run, SubprocessError, DEVNULL
|
|
12
|
+
|
|
13
|
+
from entangled.config.language import Language
|
|
14
|
+
|
|
15
|
+
from ..properties import Property, get_attribute, get_classes
|
|
16
|
+
from ..document import ReferenceId, ReferenceMap, CodeBlock
|
|
17
|
+
|
|
18
|
+
from .base import HookBase, PrerequisitesFailed
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
The Makefile is generated with `preamble` as a format string. The
|
|
22
|
+
`.RECIPEPREFIX` is set to `>` , and `target` attributes are collected into an
|
|
23
|
+
`all` target.
|
|
24
|
+
"""
|
|
25
|
+
preamble = """
|
|
26
|
+
# This Makefile is generated by Entangled. Modifications will be overwritten.
|
|
27
|
+
|
|
28
|
+
.RECIPEPREFIX = >
|
|
29
|
+
.PHONY = all
|
|
30
|
+
|
|
31
|
+
all: {targets}
|
|
32
|
+
|
|
33
|
+
{rules}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
EXEC_CMDS = {
|
|
37
|
+
"Python": "python {script}",
|
|
38
|
+
"Bash": "bash {script}",
|
|
39
|
+
"Julia": "julia -O0 --project=. {script}",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Hook(HookBase):
|
|
44
|
+
@dataclass
|
|
45
|
+
class Config(HookBase.Config):
|
|
46
|
+
runners: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
def __post_init__(self):
|
|
49
|
+
for k, v in EXEC_CMDS.items():
|
|
50
|
+
if k not in self.runners:
|
|
51
|
+
self.runners[k] = v
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Recipe:
|
|
55
|
+
target: str
|
|
56
|
+
dependencies: list[str]
|
|
57
|
+
language: Language
|
|
58
|
+
scriptfile: str
|
|
59
|
+
|
|
60
|
+
def to_makefile(self, config: Hook.Config):
|
|
61
|
+
dep_str = " ".join(self.dependencies)
|
|
62
|
+
exec_cmd = config.runners[self.language.name].format(script=self.scriptfile)
|
|
63
|
+
return f"{self.target}: {self.scriptfile} {dep_str}\n" \
|
|
64
|
+
f"> {exec_cmd}"
|
|
65
|
+
|
|
66
|
+
def __init__(self, config: Hook.Config):
|
|
67
|
+
self.recipes: list[Hook.Recipe] = []
|
|
68
|
+
self.config = config
|
|
69
|
+
|
|
70
|
+
def check_prerequisites(self):
|
|
71
|
+
"""Check that `make` is installed."""
|
|
72
|
+
try:
|
|
73
|
+
run(["make", "--version"], stdout=DEVNULL)
|
|
74
|
+
except (SubprocessError, FileNotFoundError):
|
|
75
|
+
raise PrerequisitesFailed("GNU Make needs to be installed")
|
|
76
|
+
|
|
77
|
+
def condition(self, props: list[Property]):
|
|
78
|
+
"""Condition by which a CodeBlock is processed: should have `.build` class
|
|
79
|
+
and `target=` attribute."""
|
|
80
|
+
return "build" in get_classes(props) \
|
|
81
|
+
and get_attribute(props, "target") is not None
|
|
82
|
+
|
|
83
|
+
def on_read(self, refs: ReferenceMap, ref: ReferenceId, cb: CodeBlock):
|
|
84
|
+
"""Add a CodeBlock's target attribute to the list of targets."""
|
|
85
|
+
target = get_attribute(cb.properties, "target")
|
|
86
|
+
if target is None:
|
|
87
|
+
return
|
|
88
|
+
|
|
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, _: ReferenceMap):
|
|
99
|
+
"""After all code is tangled: retrieve the build scripts and run it."""
|
|
100
|
+
targets = " ".join([r.target for r in self.recipes])
|
|
101
|
+
makefile = preamble.format(targets=targets, rules="\n\n".join(r.to_makefile(self.config) for r in self.recipes))
|
|
102
|
+
Path(".entangled/build").mkdir(exist_ok=True, parents=True)
|
|
103
|
+
Path(".entangled/build/Makefile").write_text(makefile)
|
|
104
|
+
|
|
@@ -12,7 +12,7 @@ except ImportError:
|
|
|
12
12
|
WITH_RICH = False
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
from .commands import tangle, stitch, sync, watch
|
|
15
|
+
from .commands import tangle, stitch, sync, watch, status
|
|
16
16
|
from .errors.internal import bug_contact
|
|
17
17
|
from .errors.user import UserError
|
|
18
18
|
from .version import __version__
|
|
@@ -57,7 +57,7 @@ def cli():
|
|
|
57
57
|
parser.add_argument(
|
|
58
58
|
"-v", "--version", action="store_true", help="show version number"
|
|
59
59
|
)
|
|
60
|
-
argh.add_commands(parser, [tangle, stitch, sync, watch])
|
|
60
|
+
argh.add_commands(parser, [tangle, stitch, sync, watch, status])
|
|
61
61
|
args = parser.parse_args()
|
|
62
62
|
|
|
63
63
|
if args.version:
|
|
@@ -87,10 +87,17 @@ class MarkdownReader(mawk.RuleSet):
|
|
|
87
87
|
language_class = first(get_classes(self.current_codeblock_properties))
|
|
88
88
|
block_id = get_id(self.current_codeblock_properties)
|
|
89
89
|
target_file = get_attribute(self.current_codeblock_properties, "file")
|
|
90
|
-
ref_name = block_id or target_file
|
|
91
90
|
language = config.get_language(language_class) if language_class else None
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
content = "\n".join(
|
|
93
|
+
line.removeprefix(self.current_codeblock_indent)
|
|
94
|
+
for line in self.current_content)
|
|
95
|
+
|
|
96
|
+
ref_name = block_id or target_file
|
|
97
|
+
if ref_name is None:
|
|
98
|
+
ref_name = f"unnamed-{self.location}"
|
|
99
|
+
|
|
100
|
+
if language is None:
|
|
94
101
|
self.flush_plain_text()
|
|
95
102
|
else:
|
|
96
103
|
ref = self.reference_map.new_id(
|
|
@@ -100,13 +107,10 @@ class MarkdownReader(mawk.RuleSet):
|
|
|
100
107
|
language,
|
|
101
108
|
self.current_codeblock_properties,
|
|
102
109
|
self.current_codeblock_indent,
|
|
103
|
-
|
|
104
|
-
line.removeprefix(self.current_codeblock_indent)
|
|
105
|
-
for line in self.current_content
|
|
106
|
-
),
|
|
110
|
+
content,
|
|
107
111
|
self.current_codeblock_location,
|
|
108
112
|
)
|
|
109
|
-
logging.debug(repr(code))
|
|
113
|
+
# logging.debug(repr(code))
|
|
110
114
|
self.reference_map[ref] = code
|
|
111
115
|
if target_file is not None:
|
|
112
116
|
self.reference_map.targets.add(target_file)
|
|
@@ -115,7 +119,7 @@ class MarkdownReader(mawk.RuleSet):
|
|
|
115
119
|
|
|
116
120
|
for h in self.hooks:
|
|
117
121
|
if h.condition(self.current_codeblock_properties):
|
|
118
|
-
h.on_read(ref, code)
|
|
122
|
+
h.on_read(self.reference_map, ref, code)
|
|
119
123
|
|
|
120
124
|
self.current_content.append(m[0])
|
|
121
125
|
self.inside_codeblock = False
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .config import config
|
|
2
|
+
from .filedb import file_db
|
|
3
|
+
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_watch_dirs():
|
|
9
|
+
"""List all directories that contain files that need watching."""
|
|
10
|
+
input_file_list = list_input_files()
|
|
11
|
+
markdown_dirs = set(p.parent for p in input_file_list)
|
|
12
|
+
with file_db(readonly=True) as db:
|
|
13
|
+
code_dirs = set(p.parent for p in db.managed)
|
|
14
|
+
return code_dirs.union(markdown_dirs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_input_files():
|
|
18
|
+
"""List all input files."""
|
|
19
|
+
return chain.from_iterable(map(Path(".").glob, config.watch_list))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_dependent_files():
|
|
23
|
+
with file_db(readonly=True) as db:
|
|
24
|
+
result = list(db.managed)
|
|
25
|
+
return result
|
|
@@ -105,9 +105,13 @@ class Delete(Action):
|
|
|
105
105
|
@dataclass
|
|
106
106
|
class Transaction:
|
|
107
107
|
db: FileDB
|
|
108
|
+
updates: list[Path] = field(default_factory=list)
|
|
108
109
|
actions: list[Action] = field(default_factory=list)
|
|
109
110
|
passed: set[Path] = field(default_factory=set)
|
|
110
111
|
|
|
112
|
+
def update(self, path: Path):
|
|
113
|
+
self.updates.append(path)
|
|
114
|
+
|
|
111
115
|
def write(self, path: Path, content: str, sources: list[Path]):
|
|
112
116
|
if path in self.passed:
|
|
113
117
|
raise InternalError("Path is being written to twice", [path])
|
|
@@ -147,6 +151,8 @@ class Transaction:
|
|
|
147
151
|
def run(self):
|
|
148
152
|
for a in self.actions:
|
|
149
153
|
a.run(self.db)
|
|
154
|
+
for f in self.updates:
|
|
155
|
+
self.db.update(f)
|
|
150
156
|
|
|
151
157
|
|
|
152
158
|
class TransactionMode(Enum):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "entangled-cli"
|
|
3
|
-
version = "2.0.0-beta-
|
|
3
|
+
version = "2.0.0-beta-5"
|
|
4
4
|
description = "Literate Programming toolbox"
|
|
5
5
|
repository = "https://github.com/entangled/entangled.py"
|
|
6
6
|
homepage = "https://entangled.github.io/"
|
|
@@ -17,7 +17,7 @@ mawk = "^0.1.4" # use mawk as line parsing engine
|
|
|
17
17
|
watchdog = "^3.0.0" # file watching
|
|
18
18
|
filelock = "^3.12.0" # file lock for json db
|
|
19
19
|
argh = "^0.28.1"
|
|
20
|
-
rich =
|
|
20
|
+
rich = "^13.3.5"
|
|
21
21
|
|
|
22
22
|
[tool.poetry.group.dev.dependencies]
|
|
23
23
|
pytest = "^7.3.1"
|
|
@@ -29,9 +29,6 @@ mkdocs-material = "^9.1.13"
|
|
|
29
29
|
mkdocstrings = {extras = ["python"], version = "^0.21.2"}
|
|
30
30
|
twine = "^4.0.2"
|
|
31
31
|
|
|
32
|
-
[tool.poetry.extras]
|
|
33
|
-
rich = ["rich"]
|
|
34
|
-
|
|
35
32
|
[build-system]
|
|
36
33
|
requires = ["poetry-core"]
|
|
37
34
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The `build` hook collects code blocks that are tagged with the `#build`
|
|
3
|
-
identifier and have a `target=` attribute defined. These code blocks are put
|
|
4
|
-
together into a temporary Makefile that is run from the current working
|
|
5
|
-
directory.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from tempfile import TemporaryDirectory
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from subprocess import run, SubprocessError, DEVNULL
|
|
11
|
-
|
|
12
|
-
import logging
|
|
13
|
-
import os
|
|
14
|
-
|
|
15
|
-
from ..properties import Property, get_id, get_attribute
|
|
16
|
-
from ..tangle import tangle_ref
|
|
17
|
-
from ..document import ReferenceMap, CodeBlock
|
|
18
|
-
|
|
19
|
-
from .base import HookBase, PrerequisitesFailed
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
The Makefile is generated with `preamble` as a format string. The
|
|
23
|
-
`.RECIPEPREFIX` is set to `>` , and `target` attributes are collected into an
|
|
24
|
-
`all` target.
|
|
25
|
-
"""
|
|
26
|
-
preamble = """
|
|
27
|
-
.RECIPEPREFIX = >
|
|
28
|
-
.PHONY = all
|
|
29
|
-
|
|
30
|
-
all: {targets}
|
|
31
|
-
|
|
32
|
-
{rules}
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class BuildHook(HookBase):
|
|
37
|
-
def __init__(self):
|
|
38
|
-
self.targets = []
|
|
39
|
-
|
|
40
|
-
@staticmethod
|
|
41
|
-
def check_prerequisites():
|
|
42
|
-
"""Check that `make` is installed."""
|
|
43
|
-
try:
|
|
44
|
-
run(["make", "--version"], stdout=DEVNULL)
|
|
45
|
-
except (SubprocessError, FileNotFoundError):
|
|
46
|
-
raise PrerequisitesFailed("GNU Make needs to be installed")
|
|
47
|
-
|
|
48
|
-
def condition(self, props: list[Property]):
|
|
49
|
-
"""Condition by which a CodeBlock is processed: should have `#build` id
|
|
50
|
-
and `target=` attribute."""
|
|
51
|
-
return get_id(props) == "build" and get_attribute(props, "target") is not None
|
|
52
|
-
|
|
53
|
-
def on_read(self, _, code: CodeBlock):
|
|
54
|
-
"""Add a CodeBlock's target attribute to the list of targets."""
|
|
55
|
-
target = get_attribute(code.properties, "target")
|
|
56
|
-
if target is None:
|
|
57
|
-
return
|
|
58
|
-
self.targets.append(target)
|
|
59
|
-
|
|
60
|
-
def post_tangle(self, refs: ReferenceMap):
|
|
61
|
-
"""After all code is tangled: retrieve the `#build` script and run it."""
|
|
62
|
-
if "CI" in os.environ:
|
|
63
|
-
logging.info("CI run detected, skipping `build` hook.")
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
logging.info("Building artifacts with `make`.")
|
|
67
|
-
with TemporaryDirectory() as _pwd:
|
|
68
|
-
pwd = Path(_pwd)
|
|
69
|
-
script = preamble.format(
|
|
70
|
-
targets=" ".join(self.targets), rules=tangle_ref(refs, "build")[0]
|
|
71
|
-
)
|
|
72
|
-
(pwd / "Makefile").write_text(script)
|
|
73
|
-
run(["make", "-f", str(pwd / "Makefile")], stdout=DEVNULL)
|
|
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
|