entangled-cli 2.4.0__tar.gz → 2.4.3__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.4.0 → entangled_cli-2.4.3}/PKG-INFO +10 -7
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/README.md +6 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/brei.py +2 -1
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/main.py +1 -2
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/reset.py +3 -17
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/status.py +4 -2
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/stitch.py +5 -9
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/sync.py +13 -19
- entangled_cli-2.4.3/entangled/commands/tangle.py +48 -0
- entangled_cli-2.4.3/entangled/commands/watch.py +51 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/__init__.py +33 -23
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/__init__.py +4 -3
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/base.py +4 -1
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/build.py +11 -7
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/quarto_attributes.py +15 -4
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/repl.py +11 -5
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/task.py +12 -6
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/interface/context.py +18 -11
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/interface/document.py +11 -9
- entangled_cli-2.4.3/entangled/io/__init__.py +11 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/io/filedb.py +35 -16
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/io/stat.py +2 -3
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/io/transaction.py +8 -5
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/io/virtual.py +31 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/iterators/lines.py +2 -5
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/tangle.py +18 -10
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/parsing.py +13 -2
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/readers/code.py +22 -13
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/readers/markdown.py +8 -3
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/status.py +11 -7
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/utility.py +0 -4
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/pyproject.toml +6 -7
- entangled_cli-2.4.0/entangled/commands/tangle.py +0 -37
- entangled_cli-2.4.0/entangled/commands/watch.py +0 -66
- entangled_cli-2.4.0/entangled/io/__init__.py +0 -11
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/.gitignore +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/LICENSE +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/commands/new.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/annotation_method.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/config_data.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/config_update.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/document_style.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/language.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/markers.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/namespace_default.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/templates.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/config/version.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/errors/user.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/shebang.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/hooks/spdx_license.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/interface/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/iterators/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/iterators/peekable.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/iterators/run_generator.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/logging.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/main.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/code_block.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/content.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/properties.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/reference_id.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/reference_map.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/model/reference_name.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/py.typed +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/readers/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/readers/delimiters.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/readers/types.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/readers/yaml_header.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/text_location.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.3}/entangled/version.py +0 -0
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: entangled-cli
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.3
|
|
4
4
|
Summary: Literate Programming toolbox
|
|
5
5
|
Project-URL: Homepage, https://entangled.github.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/entangled/entangled.py
|
|
7
7
|
Author-email: Johan Hidding <j.hidding@esciencecenter.nl>
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Python: <4,>=3.
|
|
11
|
-
Requires-Dist: argh<0.31,>=0.30
|
|
10
|
+
Requires-Python: <4,>=3.13
|
|
12
11
|
Requires-Dist: brei<0.3,>=0.2.3
|
|
13
12
|
Requires-Dist: click>=8.3.0
|
|
14
13
|
Requires-Dist: copier<10,>=9
|
|
15
14
|
Requires-Dist: filelock<4,>=3.12.0
|
|
16
|
-
Requires-Dist: mawk<0.2,>=0.1.7
|
|
17
15
|
Requires-Dist: msgspec>=0.20.0
|
|
18
|
-
Requires-Dist: pexpect<5,>=4.9.0
|
|
19
16
|
Requires-Dist: pyyaml<7,>=6.0.1
|
|
20
17
|
Requires-Dist: repl-session>=0.2.0
|
|
21
18
|
Requires-Dist: rich-argparse<2,>=1.4.0
|
|
22
19
|
Requires-Dist: rich-click>=1.9.4
|
|
23
|
-
Requires-Dist: rich<
|
|
20
|
+
Requires-Dist: rich<15,>=13
|
|
24
21
|
Requires-Dist: tomlkit<0.13,>=0.12.1
|
|
25
22
|
Requires-Dist: typeguard>=4.4.4
|
|
26
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: watchfiles>=1.1.1
|
|
27
24
|
Description-Content-Type: text/markdown
|
|
28
25
|
|
|
29
26
|
# Entangled
|
|
@@ -61,6 +58,12 @@ Or, if you prefer plain old `pip`,
|
|
|
61
58
|
pip install entangled-cli
|
|
62
59
|
```
|
|
63
60
|
|
|
61
|
+
Or, if you prefer the nix package manager:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
nix run github:entangled/entangled.py
|
|
65
|
+
```
|
|
66
|
+
|
|
64
67
|
## Use
|
|
65
68
|
|
|
66
69
|
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:
|
|
@@ -33,6 +33,12 @@ Or, if you prefer plain old `pip`,
|
|
|
33
33
|
pip install entangled-cli
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Or, if you prefer the nix package manager:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
nix run github:entangled/entangled.py
|
|
40
|
+
```
|
|
41
|
+
|
|
36
42
|
## Use
|
|
37
43
|
|
|
38
44
|
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:
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import textwrap
|
|
6
6
|
|
|
7
7
|
from ..config import Config, read_config
|
|
8
|
+
from ..io import FileCache
|
|
8
9
|
from brei import resolve_tasks, Phony
|
|
9
10
|
from ..logging import logger
|
|
10
11
|
from .main import main
|
|
@@ -18,7 +19,7 @@ async def brei_main(target_strs: list[str], force_run: bool, throttle: int | Non
|
|
|
18
19
|
if not Path(".entangled").exists():
|
|
19
20
|
Path(".entangled").mkdir()
|
|
20
21
|
|
|
21
|
-
cfg = Config() | read_config()
|
|
22
|
+
cfg = Config() | read_config(FileCache())
|
|
22
23
|
db = await resolve_tasks(cfg.brei, Path(".entangled/brei_history"))
|
|
23
24
|
if throttle:
|
|
24
25
|
db.throttle = asyncio.Semaphore(throttle)
|
|
@@ -6,12 +6,9 @@ This command will read the markdown sources, then pretend to be tangling
|
|
|
6
6
|
without actually writing out to source files.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from ..io import TransactionMode
|
|
10
|
-
from ..errors.user import UserError
|
|
11
|
-
from ..interface import Document
|
|
9
|
+
from ..io import TransactionMode
|
|
12
10
|
from .main import main
|
|
13
|
-
|
|
14
|
-
import logging
|
|
11
|
+
from .tangle import do_tangle
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
@main.command(short_help="Reset the file database.")
|
|
@@ -20,15 +17,4 @@ def reset():
|
|
|
20
17
|
Resets the file database. This performs a tangle without actually
|
|
21
18
|
writing output to the files, but updating the database as if we were.
|
|
22
19
|
"""
|
|
23
|
-
|
|
24
|
-
try:
|
|
25
|
-
doc = Document()
|
|
26
|
-
mode = TransactionMode.RESETDB
|
|
27
|
-
|
|
28
|
-
with transaction(mode) as t:
|
|
29
|
-
doc.load(t)
|
|
30
|
-
doc.tangle(t)
|
|
31
|
-
t.clear_orphans()
|
|
32
|
-
|
|
33
|
-
except UserError as e:
|
|
34
|
-
logging.error(str(e))
|
|
20
|
+
do_tangle(mode=TransactionMode.RESETDB)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
from collections.abc import Iterable
|
|
3
3
|
from ..status import list_dependent_files
|
|
4
4
|
from ..config import Config, read_config, get_input_files
|
|
5
|
+
from ..io import FileCache
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from rich.console import Console, Group
|
|
@@ -30,7 +31,8 @@ def files_panel(file_list: Iterable[Path], title: str) -> Panel:
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def rich_status():
|
|
33
|
-
|
|
34
|
+
fs = FileCache()
|
|
35
|
+
cfg = Config() | read_config(fs)
|
|
34
36
|
config_table = Table()
|
|
35
37
|
config_table.add_column("name")
|
|
36
38
|
config_table.add_column("value")
|
|
@@ -47,7 +49,7 @@ def rich_status():
|
|
|
47
49
|
Panel(config_table, title="config", border_style="dark_cyan"),
|
|
48
50
|
Columns(
|
|
49
51
|
[
|
|
50
|
-
files_panel(get_input_files(cfg), "input files"),
|
|
52
|
+
files_panel(get_input_files(fs, cfg), "input files"),
|
|
51
53
|
files_panel(list_dependent_files(), "dependent files"),
|
|
52
54
|
]
|
|
53
55
|
),
|
|
@@ -19,13 +19,9 @@ def stitch(*, force: bool = False, show: bool = False):
|
|
|
19
19
|
else:
|
|
20
20
|
mode = TransactionMode.FAIL
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
doc = Document()
|
|
22
|
+
doc = Document()
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
except UserError as e:
|
|
31
|
-
e.handle()
|
|
24
|
+
with transaction(mode) as t:
|
|
25
|
+
doc.load(t)
|
|
26
|
+
doc.load_all_code(t)
|
|
27
|
+
doc.stitch(t)
|
|
@@ -17,10 +17,9 @@ class Action(Enum):
|
|
|
17
17
|
|
|
18
18
|
def sync_action(doc: Document) -> Action:
|
|
19
19
|
input_file_list = doc.input_files()
|
|
20
|
-
fs = FileCache()
|
|
21
20
|
|
|
22
|
-
with filedb(readonly=True) as db:
|
|
23
|
-
changed = set(db.changed_files(fs))
|
|
21
|
+
with filedb(readonly=True, fs=doc.context.fs) as db:
|
|
22
|
+
changed = set(db.changed_files(doc.context.fs))
|
|
24
23
|
|
|
25
24
|
if not all(f in db for f in input_file_list):
|
|
26
25
|
return Action.TANGLE
|
|
@@ -59,29 +58,24 @@ def stitch(doc: Document):
|
|
|
59
58
|
doc.tangle(t)
|
|
60
59
|
for h in doc.context.all_hooks:
|
|
61
60
|
h.post_tangle(doc.reference_map)
|
|
62
|
-
|
|
63
61
|
|
|
64
|
-
def run_sync():
|
|
65
|
-
try:
|
|
66
|
-
doc = Document()
|
|
67
|
-
match sync_action(doc):
|
|
68
|
-
case Action.TANGLE:
|
|
69
|
-
logging.info("Tangling.")
|
|
70
|
-
tangle(doc)
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
def run_sync():
|
|
64
|
+
doc = Document()
|
|
65
|
+
match sync_action(doc):
|
|
66
|
+
case Action.TANGLE:
|
|
67
|
+
logging.info("Tangling.")
|
|
68
|
+
tangle(doc)
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
case Action.STITCH:
|
|
71
|
+
logging.info("Stitching.")
|
|
72
|
+
stitch(doc)
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
case Action.NOTHING:
|
|
75
|
+
pass
|
|
81
76
|
|
|
82
77
|
|
|
83
78
|
@main.command()
|
|
84
79
|
def sync():
|
|
85
80
|
"""Be smart wether to tangle or stich"""
|
|
86
81
|
run_sync()
|
|
87
|
-
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import rich_click as click
|
|
2
|
+
|
|
3
|
+
from .main import main
|
|
4
|
+
|
|
5
|
+
from ..config import AnnotationMethod, Config
|
|
6
|
+
from ..io import AbstractFileCache, FileCache, transaction, TransactionMode
|
|
7
|
+
from ..errors.user import UserError
|
|
8
|
+
from ..interface import Context, Document
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@main.command()
|
|
12
|
+
@click.option("-a", "--annotate", type=click.Choice(AnnotationMethod, case_sensitive=False),
|
|
13
|
+
help="annotation method")
|
|
14
|
+
@click.option("-f", "--force", is_flag=True, help="force overwriting existing files")
|
|
15
|
+
@click.option("-s", "--show", is_flag=True, help="only show what would happen")
|
|
16
|
+
def tangle(*, annotate: AnnotationMethod | None = None, force: bool = False, show: bool = False):
|
|
17
|
+
if show:
|
|
18
|
+
mode = TransactionMode.SHOW
|
|
19
|
+
elif force:
|
|
20
|
+
mode = TransactionMode.FORCE
|
|
21
|
+
else:
|
|
22
|
+
mode = TransactionMode.FAIL
|
|
23
|
+
|
|
24
|
+
do_tangle(annotate=annotate, mode=mode, skip_post_tangle=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def do_tangle(*,
|
|
28
|
+
annotate: AnnotationMethod | None = None,
|
|
29
|
+
mode: TransactionMode = TransactionMode.FAIL,
|
|
30
|
+
fs: AbstractFileCache | None = None,
|
|
31
|
+
skip_post_tangle: bool = True):
|
|
32
|
+
"""Tangle codes from the documentation."""
|
|
33
|
+
|
|
34
|
+
if fs is None:
|
|
35
|
+
fs = FileCache()
|
|
36
|
+
|
|
37
|
+
doc = Document(context=Context(fs=fs))
|
|
38
|
+
|
|
39
|
+
with transaction(mode, fs=fs) as t:
|
|
40
|
+
doc.load(t)
|
|
41
|
+
doc.tangle(t, annotate)
|
|
42
|
+
t.clear_orphans()
|
|
43
|
+
|
|
44
|
+
if skip_post_tangle:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
for h in doc.context.all_hooks:
|
|
48
|
+
h.post_tangle(doc.reference_map)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from threading import Event
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ..status import find_watch_dirs
|
|
5
|
+
from ..logging import logger
|
|
6
|
+
|
|
7
|
+
from .sync import run_sync
|
|
8
|
+
from .main import main
|
|
9
|
+
from ..errors.user import UserError
|
|
10
|
+
|
|
11
|
+
import watchfiles
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
log = logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def watch_filter(change: watchfiles.Change, path: str) -> bool:
|
|
18
|
+
if Path(path).relative_to(Path.cwd()).as_posix().startswith(".entangled"):
|
|
19
|
+
return False
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
|
|
24
|
+
"""Keep a loop running, watching for changes. This interface is separated
|
|
25
|
+
from the CLI one, so that it can be tested using threading instead of
|
|
26
|
+
subprocess."""
|
|
27
|
+
|
|
28
|
+
def stop() -> bool:
|
|
29
|
+
return _stop_event is not None and _stop_event.is_set()
|
|
30
|
+
|
|
31
|
+
log.debug("Running daemon")
|
|
32
|
+
run_sync()
|
|
33
|
+
|
|
34
|
+
if _start_event is not None:
|
|
35
|
+
log.debug("Setting start event")
|
|
36
|
+
_start_event.set()
|
|
37
|
+
|
|
38
|
+
dirs = "." # find_watch_dirs()
|
|
39
|
+
|
|
40
|
+
for changes in watchfiles.watch(dirs, stop_event=_stop_event, watch_filter=watch_filter):
|
|
41
|
+
log.debug(changes)
|
|
42
|
+
try:
|
|
43
|
+
run_sync()
|
|
44
|
+
except UserError as e:
|
|
45
|
+
logger().error(e, exc_info=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@main.command()
|
|
49
|
+
def watch():
|
|
50
|
+
"""Keep a loop running, watching for changes."""
|
|
51
|
+
_watch()
|
|
@@ -20,13 +20,13 @@ from .namespace_default import NamespaceDefault
|
|
|
20
20
|
from ..logging import logger
|
|
21
21
|
from ..version import __version__
|
|
22
22
|
from ..errors.user import HelpfulUserError
|
|
23
|
-
|
|
23
|
+
from ..io import AbstractFileCache
|
|
24
24
|
|
|
25
25
|
log = logger()
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def read_config_from_toml(
|
|
29
|
-
path: Path, section: str | None = None
|
|
29
|
+
fs: AbstractFileCache, path: Path, section: str | None = None
|
|
30
30
|
) -> ConfigUpdate | None:
|
|
31
31
|
"""Read a config from given `path` in given `section`. The path should refer to
|
|
32
32
|
a TOML file that should decode to a `Config` object. If `section` is given, only
|
|
@@ -39,17 +39,17 @@ def read_config_from_toml(
|
|
|
39
39
|
read_config_from_toml(Path("./pyproject.toml"), "tool.entangled")
|
|
40
40
|
```
|
|
41
41
|
"""
|
|
42
|
-
if not
|
|
42
|
+
if path not in fs:
|
|
43
43
|
return None
|
|
44
44
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
content = fs[path].content
|
|
46
|
+
json: Any = tomllib.loads(content)
|
|
47
|
+
if section is not None:
|
|
48
|
+
for s in section.split("."):
|
|
49
|
+
json = json[s] # pyright: ignore[reportAny]
|
|
50
|
+
update = msgspec.convert(json, type=ConfigUpdate)
|
|
51
|
+
log.debug("Read config from `%s`", path)
|
|
52
|
+
return update
|
|
53
53
|
|
|
54
54
|
except (msgspec.ValidationError, tomllib.TOMLDecodeError) as e:
|
|
55
55
|
raise HelpfulUserError(f"Could not read config: {e}")
|
|
@@ -59,25 +59,35 @@ def read_config_from_toml(
|
|
|
59
59
|
return None
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def read_config() -> ConfigUpdate | None:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
def read_config(fs: AbstractFileCache) -> ConfigUpdate | None:
|
|
63
|
+
"""
|
|
64
|
+
Read configuration from any of the possible hard-coded locations:
|
|
65
|
+
|
|
66
|
+
- `./entangled.toml`
|
|
67
|
+
- `./pyproject.toml` section `[tool.entangled]`.
|
|
68
|
+
|
|
69
|
+
Returns a `ConfigUpdate` or `None`. To get the full `Config` object,
|
|
70
|
+
run `Config() | read_config(fs)`.
|
|
71
|
+
"""
|
|
72
|
+
if Path("./entangled.toml") in fs:
|
|
73
|
+
return read_config_from_toml(fs, Path("./entangled.toml"))
|
|
74
|
+
if Path("./pyproject.toml") in fs:
|
|
66
75
|
return (
|
|
67
|
-
read_config_from_toml(Path("./pyproject.toml"), "tool.entangled")
|
|
76
|
+
read_config_from_toml(fs, Path("./pyproject.toml"), "tool.entangled")
|
|
68
77
|
)
|
|
69
78
|
return None
|
|
70
79
|
|
|
71
80
|
|
|
72
|
-
def get_input_files(cfg: Config) -> list[Path]:
|
|
81
|
+
def get_input_files(fs: AbstractFileCache, cfg: Config) -> list[Path]:
|
|
82
|
+
"""
|
|
83
|
+
Get a sorted list of all input files for this project.
|
|
84
|
+
"""
|
|
73
85
|
log.debug("watch list: %s; ignoring: %s", cfg.watch_list, cfg.ignore_list)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if not any(path.match(pat) for pat in cfg.ignore_list) and path.is_file()
|
|
78
|
-
]
|
|
86
|
+
input_file_list = sorted(filter(
|
|
87
|
+
lambda p: not any(p.match(pat) for pat in cfg.ignore_list),
|
|
88
|
+
chain.from_iterable(map(fs.glob, cfg.watch_list))))
|
|
79
89
|
log.debug("input file list %s", input_file_list)
|
|
80
|
-
return
|
|
90
|
+
return input_file_list
|
|
81
91
|
|
|
82
92
|
|
|
83
93
|
__all__ = ["Config", "ConfigUpdate", "AnnotationMethod", "Markers", "NamespaceDefault"]
|
|
@@ -25,14 +25,15 @@ hooks: dict[str, type[HookBase]] = {
|
|
|
25
25
|
} | external_hooks
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def create_hook(cfg: Config, h: str) -> HookBase | None:
|
|
28
|
+
def create_hook(cfg: Config, h: str, state: HookBase.State) -> HookBase | None:
|
|
29
29
|
if h not in hooks:
|
|
30
30
|
logging.error("hook `%s` not found", h)
|
|
31
31
|
return None
|
|
32
32
|
|
|
33
33
|
try:
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
hook_cls = hooks[h]
|
|
35
|
+
hook_cfg = msgspec.convert(cfg.hook.get(h, {}), type=hook_cls.Config)
|
|
36
|
+
hook_instance = hook_cls(hook_cfg, state)
|
|
36
37
|
hook_instance.check_prerequisites()
|
|
37
38
|
return hook_instance
|
|
38
39
|
except PrerequisitesFailed as e:
|
|
@@ -6,13 +6,14 @@ directory.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from msgspec import field
|
|
9
|
+
from dataclasses import dataclass, field
|
|
11
10
|
from pathlib import Path, PurePath
|
|
12
11
|
from subprocess import run, SubprocessError, DEVNULL
|
|
13
12
|
import logging
|
|
14
13
|
from typing import final, override
|
|
15
14
|
|
|
15
|
+
import msgspec
|
|
16
|
+
|
|
16
17
|
from ..config.language import Language
|
|
17
18
|
from ..io import Transaction
|
|
18
19
|
from ..model.properties import Property, get_attribute, get_attribute_string, get_classes
|
|
@@ -45,7 +46,7 @@ EXEC_CMDS = {
|
|
|
45
46
|
@final
|
|
46
47
|
class Hook(HookBase):
|
|
47
48
|
class Config(HookBase.Config):
|
|
48
|
-
runners: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
runners: dict[str, str] = msgspec.field(default_factory=dict)
|
|
49
50
|
|
|
50
51
|
def __post_init__(self):
|
|
51
52
|
for k, v in EXEC_CMDS.items():
|
|
@@ -64,9 +65,13 @@ class Hook(HookBase):
|
|
|
64
65
|
exec_cmd = config.runners[self.language.name].format(script=self.scriptfile)
|
|
65
66
|
return f"{self.target}: {self.scriptfile} {dep_str}\n" + f"\t{exec_cmd}"
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
@dataclass
|
|
69
|
+
class State(HookBase.State):
|
|
70
|
+
recipes: list[Hook.Recipe] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
def __init__(self, config: Hook.Config, state: Hook.State):
|
|
73
|
+
super().__init__(config, state)
|
|
74
|
+
self.recipes: list[Hook.Recipe] = state.recipes
|
|
70
75
|
self.config = config
|
|
71
76
|
|
|
72
77
|
@override
|
|
@@ -108,4 +113,3 @@ class Hook(HookBase):
|
|
|
108
113
|
rules="\n\n".join(r.to_makefile(self.config) for r in self.recipes),
|
|
109
114
|
)
|
|
110
115
|
t.write(Path(".entangled/build/Makefile"), makefile, [])
|
|
111
|
-
|
|
@@ -14,16 +14,27 @@ from ..logging import logger
|
|
|
14
14
|
|
|
15
15
|
log = logger()
|
|
16
16
|
|
|
17
|
+
# Cache for compiled regex patterns (keyed by comment opener)
|
|
18
|
+
_yaml_header_pattern_cache: dict[str, re.Pattern[str]] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_yaml_header_pattern(comment_open: str) -> re.Pattern[str]:
|
|
22
|
+
"""Get or create a cached compiled pattern for YAML header matching."""
|
|
23
|
+
if comment_open not in _yaml_header_pattern_cache:
|
|
24
|
+
pattern = re.escape(comment_open) + r"\s*\|(.*)"
|
|
25
|
+
_yaml_header_pattern_cache[comment_open] = re.compile(pattern)
|
|
26
|
+
return _yaml_header_pattern_cache[comment_open]
|
|
27
|
+
|
|
17
28
|
|
|
18
29
|
def split_yaml_header(language: Language, source: str) -> tuple[str, str, object]:
|
|
19
30
|
"""Split source into YAML header and body."""
|
|
20
|
-
|
|
31
|
+
pattern = _get_yaml_header_pattern(language.comment.open)
|
|
21
32
|
lines = source.splitlines(keepends=True)
|
|
22
33
|
header_lines: list[str] = []
|
|
23
34
|
body_start: int = 0
|
|
24
35
|
|
|
25
36
|
for i, line in enumerate(lines):
|
|
26
|
-
if m :=
|
|
37
|
+
if m := pattern.match(line):
|
|
27
38
|
header_lines.append(m.group(1))
|
|
28
39
|
continue
|
|
29
40
|
|
|
@@ -80,8 +91,8 @@ def amend_code_properties(code_block: CodeBlock):
|
|
|
80
91
|
|
|
81
92
|
@final
|
|
82
93
|
class Hook(HookBase):
|
|
83
|
-
def __init__(self, config: Hook.Config):
|
|
84
|
-
super().__init__(config)
|
|
94
|
+
def __init__(self, config: Hook.Config, state: Hook.State):
|
|
95
|
+
super().__init__(config, state)
|
|
85
96
|
self.config = config
|
|
86
97
|
|
|
87
98
|
@override
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
1
2
|
import json
|
|
2
3
|
from typing import final, override
|
|
3
4
|
|
|
@@ -6,7 +7,7 @@ import msgspec
|
|
|
6
7
|
from entangled.config.language import Language
|
|
7
8
|
from .base import HookBase
|
|
8
9
|
from repl_session import ReplConfig, ReplSession, ReplCommand
|
|
9
|
-
from msgspec import Struct
|
|
10
|
+
from msgspec import Struct
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
from ..logging import logger
|
|
@@ -34,12 +35,17 @@ def strip_comments(code: str, language: Language) -> str:
|
|
|
34
35
|
@final
|
|
35
36
|
class Hook(HookBase):
|
|
36
37
|
class Config(HookBase.Config):
|
|
37
|
-
config: dict[str, ReplConfig] = field(default_factory=dict)
|
|
38
|
+
config: dict[str, ReplConfig] = msgspec.field(default_factory=dict)
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
@dataclass
|
|
41
|
+
class State(HookBase.State):
|
|
42
|
+
sessions: dict[str, Session] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
def __init__(self, config: Config, state: State):
|
|
45
|
+
super().__init__(config, state)
|
|
46
|
+
log.debug(f"REPL hook config: {config}")
|
|
41
47
|
self.config = config.config
|
|
42
|
-
self.sessions: dict[str, Session] =
|
|
48
|
+
self.sessions: dict[str, Session] = state.sessions
|
|
43
49
|
|
|
44
50
|
@override
|
|
45
51
|
def on_read(self, code: CodeBlock):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from collections import defaultdict
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import final, override
|
|
@@ -48,12 +48,18 @@ class Hook(HookBase):
|
|
|
48
48
|
"path": path
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
@dataclass
|
|
52
|
+
class State(HookBase.State):
|
|
53
|
+
recipes: list[Hook.Recipe] = field(default_factory=list)
|
|
54
|
+
collections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
55
|
+
sources: list[Path] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
def __init__(self, config: Hook.Config, state: Hook.State):
|
|
58
|
+
super().__init__(config, state)
|
|
59
|
+
self.recipes: list[Hook.Recipe] = state.recipes
|
|
60
|
+
self.collections: dict[str, list[str]] = state.collections
|
|
55
61
|
self.config = config
|
|
56
|
-
self.sources: list[Path] =
|
|
62
|
+
self.sources: list[Path] = state.sources
|
|
57
63
|
|
|
58
64
|
@override
|
|
59
65
|
def pre_tangle(self, refs: ReferenceMap):
|
|
@@ -3,31 +3,39 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from collections.abc import Generator, Iterable
|
|
5
5
|
|
|
6
|
+
from ..io import AbstractFileCache, FileCache
|
|
6
7
|
from ..config import Config, ConfigUpdate
|
|
7
|
-
from ..hooks import HookBase, create_hook
|
|
8
|
+
from ..hooks import HookBase, hooks, create_hook
|
|
8
9
|
from ..model import Content, ReferenceMap
|
|
9
10
|
from ..readers.yaml_header import get_config
|
|
10
11
|
from ..readers import read_yaml_header, process_token, collect_plain_text, raw_markdown, InputStream, run_reader
|
|
11
12
|
|
|
12
13
|
from functools import partial
|
|
13
|
-
import
|
|
14
|
+
from ..logging import logger
|
|
15
|
+
|
|
16
|
+
log = logger()
|
|
14
17
|
|
|
15
18
|
@dataclass
|
|
16
19
|
class Context:
|
|
20
|
+
fs: AbstractFileCache = field(default_factory=FileCache)
|
|
17
21
|
config: Config = Config()
|
|
22
|
+
_hook_states: dict[str, HookBase.State] = field(default_factory=dict)
|
|
18
23
|
_hooks: dict[str, HookBase] = field(default_factory=dict)
|
|
19
|
-
|
|
24
|
+
|
|
20
25
|
def __post_init__(self):
|
|
26
|
+
log.debug(f"context: hook config: {self.config.hook}")
|
|
21
27
|
for h in self.config.hooks:
|
|
22
|
-
if h not in self.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
if h not in self._hook_states:
|
|
29
|
+
self._hook_states[h] = hooks[h].State()
|
|
30
|
+
|
|
31
|
+
log.debug("context: loading hook %s", h)
|
|
32
|
+
hook = create_hook(self.config, h, self._hook_states[h])
|
|
33
|
+
if hook is None:
|
|
34
|
+
continue
|
|
35
|
+
self._hooks[h] = hook
|
|
28
36
|
|
|
29
37
|
def __or__(self, update: ConfigUpdate | None) -> Context:
|
|
30
|
-
return Context(self.config | update, self.
|
|
38
|
+
return Context(self.fs, self.config | update, self._hook_states)
|
|
31
39
|
|
|
32
40
|
@property
|
|
33
41
|
def hooks(self) -> list[HookBase]:
|
|
@@ -53,4 +61,3 @@ def markdown(context: Context, refs: ReferenceMap, input: InputStream) -> Genera
|
|
|
53
61
|
|
|
54
62
|
def read_markdown(context: Context, refs: ReferenceMap, input: str) -> tuple[list[Content], ConfigUpdate | None]:
|
|
55
63
|
return run_reader(partial(markdown, context, refs), input)
|
|
56
|
-
|