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.
Files changed (74) hide show
  1. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/PKG-INFO +4 -7
  2. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/brei.py +2 -1
  3. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/main.py +1 -2
  4. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/reset.py +3 -17
  5. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/status.py +4 -2
  6. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/stitch.py +5 -9
  7. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/sync.py +13 -19
  8. entangled_cli-2.4.2/entangled/commands/tangle.py +48 -0
  9. entangled_cli-2.4.2/entangled/commands/watch.py +47 -0
  10. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/__init__.py +33 -23
  11. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/__init__.py +4 -3
  12. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/base.py +4 -1
  13. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/build.py +11 -7
  14. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/quarto_attributes.py +2 -2
  15. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/repl.py +11 -5
  16. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/task.py +12 -6
  17. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/interface/context.py +18 -11
  18. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/interface/document.py +6 -5
  19. entangled_cli-2.4.2/entangled/io/__init__.py +11 -0
  20. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/filedb.py +31 -15
  21. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/stat.py +2 -3
  22. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/transaction.py +1 -1
  23. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/io/virtual.py +31 -0
  24. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/tangle.py +5 -1
  25. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/code.py +11 -4
  26. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/markdown.py +8 -3
  27. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/status.py +11 -7
  28. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/utility.py +0 -4
  29. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/pyproject.toml +6 -7
  30. entangled_cli-2.4.0/entangled/commands/tangle.py +0 -37
  31. entangled_cli-2.4.0/entangled/commands/watch.py +0 -66
  32. entangled_cli-2.4.0/entangled/io/__init__.py +0 -11
  33. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/.gitignore +0 -0
  34. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/LICENSE +0 -0
  35. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/README.md +0 -0
  36. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/__init__.py +0 -0
  37. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/__init__.py +0 -0
  38. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/commands/new.py +0 -0
  39. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/annotation_method.py +0 -0
  40. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/config_data.py +0 -0
  41. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/config_update.py +0 -0
  42. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/document_style.py +0 -0
  43. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/language.py +0 -0
  44. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/markers.py +0 -0
  45. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/namespace_default.py +0 -0
  46. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/templates.py +0 -0
  47. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/config/version.py +0 -0
  48. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/errors/__init__.py +0 -0
  49. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/errors/internal.py +0 -0
  50. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/errors/user.py +0 -0
  51. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/shebang.py +0 -0
  52. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/hooks/spdx_license.py +0 -0
  53. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/interface/__init__.py +0 -0
  54. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/__init__.py +0 -0
  55. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/lines.py +0 -0
  56. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/peekable.py +0 -0
  57. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/iterators/run_generator.py +0 -0
  58. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/logging.py +0 -0
  59. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/main.py +0 -0
  60. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/__init__.py +0 -0
  61. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/code_block.py +0 -0
  62. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/content.py +0 -0
  63. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/properties.py +0 -0
  64. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/reference_id.py +0 -0
  65. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/reference_map.py +0 -0
  66. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/model/reference_name.py +0 -0
  67. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/parsing.py +0 -0
  68. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/py.typed +0 -0
  69. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/__init__.py +0 -0
  70. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/delimiters.py +0 -0
  71. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/types.py +0 -0
  72. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/readers/yaml_header.py +0 -0
  73. {entangled_cli-2.4.0 → entangled_cli-2.4.2}/entangled/text_location.py +0 -0
  74. {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.0
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.12
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<14,>=13.3.5
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: watchdog>=6.0.0
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)
@@ -20,5 +20,4 @@ def main(version: bool = False, debug: bool = False):
20
20
  sys.exit(0)
21
21
 
22
22
  configure(debug)
23
- logger().debug(f"Welcome to Entangled v{__version__}!")
24
-
23
+ logger().info(f"Welcome to Entangled v{__version__}!")
@@ -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, transaction
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
- cfg = Config() | read_config()
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
- try:
23
- doc = Document()
22
+ doc = Document()
24
23
 
25
- with transaction(mode) as t:
26
- doc.load(t)
27
- doc.load_all_code(t)
28
- doc.stitch(t)
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
- case Action.STITCH:
73
- logging.info("Stitching.")
74
- stitch(doc)
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
- case Action.NOTHING:
77
- pass
70
+ case Action.STITCH:
71
+ logging.info("Stitching.")
72
+ stitch(doc)
78
73
 
79
- except UserError as e:
80
- e.handle()
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 path.exists():
42
+ if path not in fs:
43
43
  return None
44
44
  try:
45
- with open(path, "rb") as f:
46
- json: Any = tomllib.load(f) # pyright: ignore[reportExplicitAny]
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
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
- if Path("./entangled.toml").exists():
64
- return read_config_from_toml(Path("./entangled.toml"))
65
- if Path("./pyproject.toml").exists():
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
- include_file_list = chain.from_iterable(map(Path(".").glob, cfg.watch_list))
75
- input_file_list = [
76
- path for path in include_file_list
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 sorted(input_file_list)
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
- hook_cfg = msgspec.convert(cfg.hook.get(h, {}), type=hooks[h].Config)
35
- hook_instance = hooks[h](hook_cfg)
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:
@@ -19,7 +19,10 @@ class HookBase:
19
19
  class Config(Struct):
20
20
  pass
21
21
 
22
- def __init__(self, config: Config):
22
+ class State:
23
+ pass
24
+
25
+ def __init__(self, config: Config, state: State):
23
26
  pass
24
27
 
25
28
  @staticmethod
@@ -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
- def __init__(self, config: Hook.Config):
68
- super().__init__(config)
69
- self.recipes: list[Hook.Recipe] = []
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, field
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
- def __init__(self, config: Config):
40
- super().__init__(config)
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
- def __init__(self, config: Hook.Config):
52
- super().__init__(config)
53
- self.recipes: list[Hook.Recipe] = []
54
- self.collections: dict[str, list[str]] = defaultdict(list)
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 logging
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._hooks:
23
- logging.debug("context: loading hook %s", h)
24
- hook = create_hook(self.config, h)
25
- if hook is None:
26
- continue
27
- self._hooks[h] = hook
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._hooks)
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"]