entangled-cli 2.4.0__tar.gz → 2.4.2__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.2}/PKG-INFO +4 -7
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/brei.py +2 -1
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/main.py +1 -2
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/reset.py +3 -17
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/status.py +4 -2
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/stitch.py +5 -9
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/sync.py +13 -19
- entangled_cli-2.4.2/entangled/commands/tangle.py +48 -0
- entangled_cli-2.4.2/entangled/commands/watch.py +47 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/__init__.py +33 -23
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/__init__.py +4 -3
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/base.py +4 -1
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/build.py +11 -7
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/quarto_attributes.py +2 -2
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/repl.py +11 -5
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/task.py +12 -6
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/interface/context.py +18 -11
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/interface/document.py +6 -5
- entangled_cli-2.4.2/entangled/io/__init__.py +11 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/filedb.py +31 -15
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/stat.py +2 -3
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/transaction.py +1 -1
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/virtual.py +31 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/tangle.py +5 -1
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/code.py +11 -4
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/markdown.py +8 -3
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/status.py +11 -7
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/utility.py +0 -4
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/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.2}/.gitignore +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/LICENSE +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/README.md +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/new.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/annotation_method.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/config_data.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/config_update.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/document_style.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/language.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/markers.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/namespace_default.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/templates.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/version.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/errors/user.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/shebang.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/spdx_license.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/interface/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/lines.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/peekable.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/run_generator.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/logging.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/main.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/code_block.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/content.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/properties.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/reference_id.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/reference_map.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/reference_name.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/parsing.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/py.typed +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/__init__.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/delimiters.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/types.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/yaml_header.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/text_location.py +0 -0
- {entangled_cli-2.4.0 → entangled_cli-2.4.2}/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.2
|
|
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
|
|
@@ -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,47 @@
|
|
|
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
|
+
|
|
10
|
+
import watchfiles
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
log = logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def watch_filter(change: watchfiles.Change, path: str) -> bool:
|
|
17
|
+
if Path(path).relative_to(Path.cwd()).as_posix().startswith(".entangled"):
|
|
18
|
+
return False
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
|
|
23
|
+
"""Keep a loop running, watching for changes. This interface is separated
|
|
24
|
+
from the CLI one, so that it can be tested using threading instead of
|
|
25
|
+
subprocess."""
|
|
26
|
+
|
|
27
|
+
def stop() -> bool:
|
|
28
|
+
return _stop_event is not None and _stop_event.is_set()
|
|
29
|
+
|
|
30
|
+
log.debug("Running daemon")
|
|
31
|
+
run_sync()
|
|
32
|
+
|
|
33
|
+
if _start_event is not None:
|
|
34
|
+
log.debug("Setting start event")
|
|
35
|
+
_start_event.set()
|
|
36
|
+
|
|
37
|
+
dirs = "." # find_watch_dirs()
|
|
38
|
+
|
|
39
|
+
for changes in watchfiles.watch(dirs, stop_event=_stop_event, watch_filter=watch_filter):
|
|
40
|
+
log.debug(changes)
|
|
41
|
+
run_sync()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@main.command()
|
|
45
|
+
def watch():
|
|
46
|
+
"""Keep a loop running, watching for changes."""
|
|
47
|
+
_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
|
-
|
|
@@ -80,8 +80,8 @@ def amend_code_properties(code_block: CodeBlock):
|
|
|
80
80
|
|
|
81
81
|
@final
|
|
82
82
|
class Hook(HookBase):
|
|
83
|
-
def __init__(self, config: Hook.Config):
|
|
84
|
-
super().__init__(config)
|
|
83
|
+
def __init__(self, config: Hook.Config, state: Hook.State):
|
|
84
|
+
super().__init__(config, state)
|
|
85
85
|
self.config = config
|
|
86
86
|
|
|
87
87
|
@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
|
-
|
|
@@ -3,7 +3,7 @@ from pathlib import PurePath, Path
|
|
|
3
3
|
|
|
4
4
|
from ..config import Config, ConfigUpdate, get_input_files, read_config, AnnotationMethod
|
|
5
5
|
from ..model import ReferenceMap, tangle_ref, Content, content_to_text
|
|
6
|
-
from ..io import Transaction
|
|
6
|
+
from ..io import AbstractFileCache, FileCache, Transaction
|
|
7
7
|
from ..readers import code
|
|
8
8
|
from ..iterators import numbered_lines, run_generator
|
|
9
9
|
from ..logging import logger
|
|
@@ -29,10 +29,10 @@ class Document:
|
|
|
29
29
|
self.context.config = new_config
|
|
30
30
|
|
|
31
31
|
def __post_init__(self):
|
|
32
|
-
self.config |= read_config()
|
|
32
|
+
self.config |= read_config(self.context.fs)
|
|
33
33
|
|
|
34
34
|
def input_files(self):
|
|
35
|
-
return get_input_files(self.config)
|
|
35
|
+
return get_input_files(self.context.fs, self.config)
|
|
36
36
|
|
|
37
37
|
def source_text(self, path: Path) -> tuple[str, set[PurePath]]:
|
|
38
38
|
deps = set()
|
|
@@ -71,12 +71,14 @@ class Document:
|
|
|
71
71
|
t.update(path)
|
|
72
72
|
|
|
73
73
|
def load_all_code(self, t: Transaction):
|
|
74
|
+
log.debug(f"Targets: {self.reference_map.targets()}")
|
|
74
75
|
for tgt in self.reference_map.targets():
|
|
76
|
+
log.debug(f"Reading code: `{tgt}`")
|
|
75
77
|
if Path(tgt) in t.fs:
|
|
76
78
|
self.load_code(t, Path(tgt))
|
|
77
79
|
|
|
78
80
|
def load(self, t: Transaction):
|
|
79
|
-
files = get_input_files(self.config)
|
|
81
|
+
files = get_input_files(t.fs, self.config)
|
|
80
82
|
if len(files) == 1:
|
|
81
83
|
log.debug(f"single input file `{files[0]}`")
|
|
82
84
|
self.context |= self.load_source(t, files[0])
|
|
@@ -102,4 +104,3 @@ class Document:
|
|
|
102
104
|
for path in self.content:
|
|
103
105
|
text, deps = self.source_text(path)
|
|
104
106
|
t.write(path, text, map(Path, deps))
|
|
105
|
-
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In Entangled all file IO should pass through a transaction.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .transaction import transaction, Transaction, TransactionMode
|
|
7
|
+
from .filedb import filedb
|
|
8
|
+
from .virtual import AbstractFileCache, FileCache, VirtualFS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["AbstractFileCache", "FileCache", "filedb", "Transaction", "TransactionMode", "transaction", "VirtualFS"]
|