entangled-cli 2.3.2__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 (86) hide show
  1. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/.gitignore +1 -0
  2. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/PKG-INFO +8 -8
  3. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/__init__.py +5 -11
  4. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/brei.py +17 -10
  5. entangled_cli-2.4.2/entangled/commands/main.py +23 -0
  6. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/new.py +35 -41
  7. entangled_cli-2.4.2/entangled/commands/reset.py +20 -0
  8. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/status.py +13 -7
  9. entangled_cli-2.4.2/entangled/commands/stitch.py +27 -0
  10. entangled_cli-2.4.2/entangled/commands/sync.py +81 -0
  11. entangled_cli-2.4.2/entangled/commands/tangle.py +48 -0
  12. entangled_cli-2.4.2/entangled/commands/watch.py +47 -0
  13. entangled_cli-2.4.2/entangled/config/__init__.py +93 -0
  14. entangled_cli-2.4.2/entangled/config/annotation_method.py +17 -0
  15. entangled_cli-2.4.2/entangled/config/config_data.py +105 -0
  16. entangled_cli-2.4.2/entangled/config/config_update.py +60 -0
  17. entangled_cli-2.4.2/entangled/config/document_style.py +18 -0
  18. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/config/language.py +2 -1
  19. entangled_cli-2.4.2/entangled/config/markers.py +21 -0
  20. entangled_cli-2.4.2/entangled/config/namespace_default.py +14 -0
  21. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/config/version.py +5 -6
  22. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/errors/user.py +20 -21
  23. entangled_cli-2.4.2/entangled/hooks/__init__.py +47 -0
  24. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/base.py +9 -2
  25. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/build.py +22 -19
  26. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/quarto_attributes.py +25 -17
  27. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/repl.py +18 -8
  28. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/shebang.py +5 -4
  29. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/spdx_license.py +5 -4
  30. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/task.py +36 -45
  31. entangled_cli-2.4.2/entangled/interface/__init__.py +4 -0
  32. entangled_cli-2.4.2/entangled/interface/context.py +63 -0
  33. entangled_cli-2.4.2/entangled/interface/document.py +106 -0
  34. entangled_cli-2.4.2/entangled/io/__init__.py +11 -0
  35. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/filedb.py +38 -18
  36. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/stat.py +2 -3
  37. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/transaction.py +26 -20
  38. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/virtual.py +93 -6
  39. entangled_cli-2.4.2/entangled/iterators/__init__.py +6 -0
  40. entangled_cli-2.4.2/entangled/iterators/lines.py +28 -0
  41. entangled_cli-2.4.2/entangled/iterators/peekable.py +43 -0
  42. entangled_cli-2.4.2/entangled/iterators/run_generator.py +14 -0
  43. entangled_cli-2.4.2/entangled/main.py +31 -0
  44. entangled_cli-2.4.2/entangled/model/__init__.py +23 -0
  45. entangled_cli-2.4.2/entangled/model/code_block.py +79 -0
  46. entangled_cli-2.4.2/entangled/model/content.py +34 -0
  47. {entangled_cli-2.3.2/entangled → entangled_cli-2.4.2/entangled/model}/properties.py +21 -1
  48. entangled_cli-2.4.2/entangled/model/reference_id.py +31 -0
  49. entangled_cli-2.4.2/entangled/model/reference_map.py +102 -0
  50. entangled_cli-2.4.2/entangled/model/reference_name.py +32 -0
  51. entangled_cli-2.4.2/entangled/model/tangle.py +158 -0
  52. entangled_cli-2.4.2/entangled/readers/__init__.py +16 -0
  53. entangled_cli-2.4.2/entangled/readers/code.py +107 -0
  54. entangled_cli-2.4.2/entangled/readers/delimiters.py +91 -0
  55. entangled_cli-2.4.2/entangled/readers/markdown.py +179 -0
  56. entangled_cli-2.4.2/entangled/readers/types.py +14 -0
  57. entangled_cli-2.4.2/entangled/readers/yaml_header.py +56 -0
  58. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/status.py +12 -17
  59. entangled_cli-2.4.2/entangled/text_location.py +21 -0
  60. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/utility.py +0 -4
  61. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/pyproject.toml +19 -12
  62. entangled_cli-2.3.2/entangled/code_reader.py +0 -81
  63. entangled_cli-2.3.2/entangled/commands/reset.py +0 -62
  64. entangled_cli-2.3.2/entangled/commands/stitch.py +0 -64
  65. entangled_cli-2.3.2/entangled/commands/sync.py +0 -51
  66. entangled_cli-2.3.2/entangled/commands/tangle.py +0 -74
  67. entangled_cli-2.3.2/entangled/commands/watch.py +0 -66
  68. entangled_cli-2.3.2/entangled/config/__init__.py +0 -198
  69. entangled_cli-2.3.2/entangled/document.py +0 -162
  70. entangled_cli-2.3.2/entangled/from_str.py +0 -27
  71. entangled_cli-2.3.2/entangled/hooks/__init__.py +0 -48
  72. entangled_cli-2.3.2/entangled/io/__init__.py +0 -11
  73. entangled_cli-2.3.2/entangled/main.py +0 -54
  74. entangled_cli-2.3.2/entangled/markdown_reader.py +0 -229
  75. entangled_cli-2.3.2/entangled/tangle.py +0 -153
  76. entangled_cli-2.3.2/entangled/text_location.py +0 -12
  77. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/LICENSE +0 -0
  78. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/README.md +0 -0
  79. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/__init__.py +0 -0
  80. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/config/templates.py +0 -0
  81. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/errors/__init__.py +0 -0
  82. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/errors/internal.py +0 -0
  83. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/logging.py +0 -0
  84. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/parsing.py +0 -0
  85. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/py.typed +0 -0
  86. {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/version.py +0 -0
@@ -4,3 +4,4 @@ __pycache__
4
4
  coverage.*
5
5
  dist
6
6
  site
7
+
@@ -1,26 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entangled-cli
3
- Version: 2.3.2
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
12
+ Requires-Dist: click>=8.3.0
13
13
  Requires-Dist: copier<10,>=9
14
14
  Requires-Dist: filelock<4,>=3.12.0
15
- Requires-Dist: mawk<0.2,>=0.1.7
16
- Requires-Dist: msgspec>=0.19.0
17
- Requires-Dist: pexpect<5,>=4.9.0
15
+ Requires-Dist: msgspec>=0.20.0
18
16
  Requires-Dist: pyyaml<7,>=6.0.1
19
17
  Requires-Dist: repl-session>=0.2.0
20
18
  Requires-Dist: rich-argparse<2,>=1.4.0
21
- Requires-Dist: rich<14,>=13.3.5
19
+ Requires-Dist: rich-click>=1.9.4
20
+ Requires-Dist: rich<15,>=13
22
21
  Requires-Dist: tomlkit<0.13,>=0.12.1
23
- Requires-Dist: watchdog>=6.0.0
22
+ Requires-Dist: typeguard>=4.4.4
23
+ Requires-Dist: watchfiles>=1.1.1
24
24
  Description-Content-Type: text/markdown
25
25
 
26
26
  # Entangled
@@ -1,19 +1,13 @@
1
- from .new import new
2
- from .status import status
1
+ from .main import main
3
2
  from .stitch import stitch
4
- from .sync import sync
5
3
  from .tangle import tangle
4
+ from .new import new
6
5
  from .reset import reset
6
+ from .sync import sync
7
7
  from .watch import watch
8
+ from .status import status
8
9
  from .brei import brei
9
10
 
10
11
  __all__ = [
11
- "new",
12
- "brei",
13
- "reset",
14
- "status",
15
- "stitch",
16
- "sync",
17
- "tangle",
18
- "watch",
12
+ "main", "stitch", "tangle", "new", "reset", "sync", "watch", "status", "brei"
19
13
  ]
@@ -1,22 +1,26 @@
1
1
  from pathlib import Path
2
2
  from collections.abc import Awaitable
3
3
  from typing import Any
4
- import argh # type: ignore
5
4
  import asyncio
6
5
  import textwrap
7
6
 
8
- from ..config import config
7
+ from ..config import Config, read_config
8
+ from ..io import FileCache
9
9
  from brei import resolve_tasks, Phony
10
10
  from ..logging import logger
11
+ from .main import main
12
+
13
+ import click
11
14
 
12
15
  log = logger()
13
16
 
14
17
 
15
- async def main(target_strs: list[str], force_run: bool, throttle: int | None):
18
+ async def brei_main(target_strs: list[str], force_run: bool, throttle: int | None):
16
19
  if not Path(".entangled").exists():
17
20
  Path(".entangled").mkdir()
18
21
 
19
- db = await resolve_tasks(config.get.brei, Path(".entangled/brei_history"))
22
+ cfg = Config() | read_config(FileCache())
23
+ db = await resolve_tasks(cfg.brei, Path(".entangled/brei_history"))
20
24
  if throttle:
21
25
  db.throttle = asyncio.Semaphore(throttle)
22
26
  db.force_run = force_run
@@ -33,10 +37,13 @@ async def main(target_strs: list[str], force_run: bool, throttle: int | None):
33
37
  log.error(msg)
34
38
 
35
39
 
36
- @argh.arg("targets", nargs="+", help="name of target to run")
37
- @argh.arg("-B", "--force-run", help="rebuild all dependencies")
38
- @argh.arg("-j", "--throttle", help="limit number of concurrent jobs")
40
+ @main.command()
41
+ @click.argument("targets", nargs=-1)
42
+ @click.option("-B", "--force-run", is_flag=True, help="rebuild all dependencies")
43
+ @click.option("-j", "--throttle", is_flag=True, help="limit number of concurrent jobs")
39
44
  def brei(targets: list[str], *, force_run: bool = False, throttle: int | None = None):
40
- """Build one of the configured targets."""
41
- config.read()
42
- asyncio.run(main(targets, force_run, throttle))
45
+ """Build one of the configured targets.
46
+
47
+ TARGETS Names of the targets to run.
48
+ """
49
+ asyncio.run(brei_main(targets, force_run, throttle))
@@ -0,0 +1,23 @@
1
+ from ..logging import configure, logger
2
+ from ..version import __version__
3
+
4
+ import sys
5
+ import rich_click as click
6
+
7
+
8
+ @click.group(
9
+ invoke_without_command=True,
10
+ epilog="See https://entangled.github.io/ for more help and tutorials."
11
+ )
12
+ @click.rich_config({"commands_before_options": True, "theme": "nord-modern"})
13
+ @click.option("-v", "--version", is_flag=True, help="Show version.")
14
+ @click.option("-d", "--debug", is_flag=True, help="Enable debugging.")
15
+ def main(version: bool = False, debug: bool = False):
16
+ """Your literate programming toolkit.
17
+ """
18
+ if version:
19
+ print(f"Entangled {__version__}")
20
+ sys.exit(0)
21
+
22
+ configure(debug)
23
+ logger().info(f"Welcome to Entangled v{__version__}!")
@@ -1,13 +1,11 @@
1
- import argh # type: ignore
2
- from argh.utils import get_subparsers
3
- import argparse
1
+ import click
4
2
 
5
3
  from pathlib import Path
6
- from rich_argparse import RichHelpFormatter
7
4
  from rich.console import Console
8
5
  from rich.table import Table
9
6
 
10
7
  from ..errors.user import HelpfulUserError
8
+ from .main import main
11
9
 
12
10
  description = """Create a new entangled project from a template.
13
11
 
@@ -42,61 +40,52 @@ def print_help() -> None:
42
40
  For this we first have to create a parser and attach this `new` function
43
41
  to it. Then we have a parser object that we can `print_help()` from.
44
42
  """
45
- parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
46
- argh.add_commands(parser, [new], func_kwargs={"formatter_class": RichHelpFormatter})
47
- get_subparsers(parser).choices["new"].print_help()
43
+ ctx = click.get_current_context()
44
+ click.echo(ctx.get_help())
45
+ ctx.exit()
48
46
 
49
47
 
50
- @argh.arg(
51
- "-a",
52
- "--answers-file",
48
+ @main.command(
49
+ epilog="This command, and the options provided are a front-end for `copier`. " +
50
+ "See https://copier.readthedocs.io/en/stable/ for more information."
51
+ )
52
+ @click.option(
53
+ "-a", "--answers-file",
53
54
  help="Update using this path (relative to [project_path]) to find the answers file for `copier`",
54
55
  )
55
- @argh.arg(
56
- "-d",
57
- "--data",
56
+ @click.option(
57
+ "-d", "--data",
58
58
  help='"VARIABLE1=VALUE1;VARIABLE1=VALUE2" Make VARIABLEs available as VALUEs when rendering the template; make sure to use quotation marks for multiple variables/values',
59
59
  )
60
- @argh.arg(
61
- "-D",
62
- "--defaults",
60
+ @click.option(
61
+ "-D", "--defaults", is_flag=True,
63
62
  help="Use default answers to questions, which might be null if not specified; overwrites when combined with -d!",
64
63
  )
65
- @argh.arg(
66
- "-p",
67
- "--pretend",
64
+ @click.option(
65
+ "-p", "--pretend", is_flag=True,
68
66
  help="Run but do not make any changes",
69
67
  )
70
- @argh.arg(
71
- "-o",
72
- "--overwrite",
68
+ @click.option(
69
+ "-o", "--overwrite", is_flag=True,
73
70
  help="Overwrite files that already exist, without asking.",
74
71
  )
75
- @argh.arg(
76
- "-l",
77
- "--list-templates",
72
+ @click.option(
73
+ "-l", "--list-templates", is_flag=True,
78
74
  help="List all official templates and exit",
79
75
  )
80
- @argh.arg(
81
- "-t",
82
- "--trust",
76
+ @click.option(
77
+ "-t", "--trust", is_flag=True,
83
78
  help='Allow templates with unsafe features (Jinja extensions, migrations, tasks); "True" for officially supported templates',
84
79
  )
85
- @argh.arg(
86
- "template",
87
- # default=None,
88
- nargs="?",
89
- help="Template handle or URL; initialize a new project from this template",
80
+ @click.argument(
81
+ "template"
90
82
  )
91
- @argh.arg(
92
- "project-path",
93
- # default=None,
94
- nargs="?",
95
- help="Initialize a new project at this path",
83
+ @click.argument(
84
+ "project-path"
96
85
  )
97
86
  def new(
98
- template: str | None,
99
- project_path: Path | None, *,
87
+ template: str,
88
+ project_path: Path, *,
100
89
  answers_file: str | None = None,
101
90
  data: str = "",
102
91
  defaults: bool = False,
@@ -105,7 +94,12 @@ def new(
105
94
  list_templates: bool = False,
106
95
  trust: bool = False,
107
96
  ) -> None:
108
- """Create a new entangled project from a template."""
97
+ """Create a new entangled project from a template.
98
+
99
+ TEMPLATE Template handle or URL; initialize a new project from this template.
100
+
101
+ PROJECT_PATH Initialize a new project at this path.
102
+ """
109
103
 
110
104
  from ..config.templates import templates as AVAILABLE_TEMPLATES
111
105
  from ..config.templates import Template
@@ -0,0 +1,20 @@
1
+ """
2
+ The `reset` command resets the file database in `.entangled/filedb.json`.
3
+ This database gets updated every time you tangle or stitch, but sometimes
4
+ its contents may become invalid, for instance when switching branches.
5
+ This command will read the markdown sources, then pretend to be tangling
6
+ without actually writing out to source files.
7
+ """
8
+
9
+ from ..io import TransactionMode
10
+ from .main import main
11
+ from .tangle import do_tangle
12
+
13
+
14
+ @main.command(short_help="Reset the file database.")
15
+ def reset():
16
+ """
17
+ Resets the file database. This performs a tangle without actually
18
+ writing output to the files, but updating the database as if we were.
19
+ """
20
+ do_tangle(mode=TransactionMode.RESETDB)
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
  from collections.abc import Iterable
3
- from ..status import list_input_files, list_dependent_files
4
- from ..config import config
3
+ from ..status import list_dependent_files
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
@@ -10,6 +11,8 @@ from rich.table import Table
10
11
  from rich.panel import Panel
11
12
  from rich.tree import Tree
12
13
 
14
+ from .main import main
15
+
13
16
 
14
17
  def tree_from_files(files: Iterable[Path]):
15
18
  tree = Tree(label=".")
@@ -28,23 +31,25 @@ def files_panel(file_list: Iterable[Path], title: str) -> Panel:
28
31
 
29
32
 
30
33
  def rich_status():
34
+ fs = FileCache()
35
+ cfg = Config() | read_config(fs)
31
36
  config_table = Table()
32
37
  config_table.add_column("name")
33
38
  config_table.add_column("value")
34
39
  config_table.add_row(
35
- "Watch list", ", ".join(f"'{pat}'" for pat in config.get.watch_list)
40
+ "Watch list", ", ".join(f"'{pat}'" for pat in cfg.watch_list)
36
41
  )
37
42
  config_table.add_row(
38
- "Ignore list", ", ".join(f"'{pat}'" for pat in config.get.ignore_list)
43
+ "Ignore list", ", ".join(f"'{pat}'" for pat in cfg.ignore_list)
39
44
  )
40
- config_table.add_row("Hooks enabled", ", ".join(config.get.hooks))
45
+ config_table.add_row("Hooks enabled", ", ".join(cfg.hooks))
41
46
 
42
47
  console = Console(color_system="auto")
43
48
  group = Group(
44
49
  Panel(config_table, title="config", border_style="dark_cyan"),
45
50
  Columns(
46
51
  [
47
- files_panel(list_input_files(), "input files"),
52
+ files_panel(get_input_files(fs, cfg), "input files"),
48
53
  files_panel(list_dependent_files(), "dependent files"),
49
54
  ]
50
55
  ),
@@ -53,6 +58,7 @@ def rich_status():
53
58
  console.print(group)
54
59
 
55
60
 
61
+ @main.command()
56
62
  def status():
57
- config.read()
63
+ """Print a status overview."""
58
64
  rich_status()
@@ -0,0 +1,27 @@
1
+ import rich_click as click
2
+
3
+ from .main import main
4
+
5
+ from ..interface import Document
6
+ from ..io import transaction, TransactionMode
7
+ from ..errors.user import UserError
8
+
9
+
10
+ @main.command()
11
+ @click.option("-f", "--force", is_flag=True, help="force overwrite on conflict")
12
+ @click.option("-s", "--show", is_flag=True, help="only show, don't act")
13
+ def stitch(*, force: bool = False, show: bool = False):
14
+ """Stitch code changes back into the documentation."""
15
+ if show:
16
+ mode = TransactionMode.SHOW
17
+ elif force:
18
+ mode = TransactionMode.FORCE
19
+ else:
20
+ mode = TransactionMode.FAIL
21
+
22
+ doc = Document()
23
+
24
+ with transaction(mode) as t:
25
+ doc.load(t)
26
+ doc.load_all_code(t)
27
+ doc.stitch(t)
@@ -0,0 +1,81 @@
1
+ from enum import Enum
2
+
3
+ from ..io import filedb, FileCache, transaction
4
+ from ..interface import Document
5
+ from ..errors.user import UserError
6
+
7
+ from .main import main
8
+
9
+ import logging
10
+
11
+
12
+ class Action(Enum):
13
+ NOTHING = 0
14
+ TANGLE = 1
15
+ STITCH = 2
16
+
17
+
18
+ def sync_action(doc: Document) -> Action:
19
+ input_file_list = doc.input_files()
20
+
21
+ with filedb(readonly=True, fs=doc.context.fs) as db:
22
+ changed = set(db.changed_files(doc.context.fs))
23
+
24
+ if not all(f in db for f in input_file_list):
25
+ return Action.TANGLE
26
+
27
+ if not changed:
28
+ return Action.NOTHING
29
+
30
+ if changed.isdisjoint(db.managed_files):
31
+ return Action.TANGLE
32
+
33
+ if changed.issubset(db.managed_files):
34
+ return Action.STITCH
35
+
36
+ logging.error("changed: %s", [str(p) for p in changed])
37
+ logging.error(
38
+ "Both markdown and code seem to have changed, don't know what to do now."
39
+ )
40
+ return Action.NOTHING
41
+
42
+
43
+ def tangle(doc: Document):
44
+ with transaction() as t:
45
+ doc.load(t)
46
+ doc.tangle(t)
47
+ t.clear_orphans()
48
+ for h in doc.context.all_hooks:
49
+ h.post_tangle(doc.reference_map)
50
+
51
+
52
+ def stitch(doc: Document):
53
+ with transaction() as t:
54
+ doc.load(t)
55
+ doc.load_all_code(t)
56
+ doc.stitch(t)
57
+ with transaction() as t:
58
+ doc.tangle(t)
59
+ for h in doc.context.all_hooks:
60
+ h.post_tangle(doc.reference_map)
61
+
62
+
63
+ def run_sync():
64
+ doc = Document()
65
+ match sync_action(doc):
66
+ case Action.TANGLE:
67
+ logging.info("Tangling.")
68
+ tangle(doc)
69
+
70
+ case Action.STITCH:
71
+ logging.info("Stitching.")
72
+ stitch(doc)
73
+
74
+ case Action.NOTHING:
75
+ pass
76
+
77
+
78
+ @main.command()
79
+ def sync():
80
+ """Be smart wether to tangle or stich"""
81
+ run_sync()
@@ -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()
@@ -0,0 +1,93 @@
1
+ """Configuration. The variable `config` should be automatically populated with
2
+ defaults and config loaded from `entangled.toml` in the work directory.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from itertools import chain
10
+
11
+ import msgspec
12
+ import tomllib
13
+
14
+ from .annotation_method import AnnotationMethod
15
+ from .markers import Markers
16
+ from .config_data import Config
17
+ from .config_update import ConfigUpdate
18
+ from .namespace_default import NamespaceDefault
19
+
20
+ from ..logging import logger
21
+ from ..version import __version__
22
+ from ..errors.user import HelpfulUserError
23
+ from ..io import AbstractFileCache
24
+
25
+ log = logger()
26
+
27
+
28
+ def read_config_from_toml(
29
+ fs: AbstractFileCache, path: Path, section: str | None = None
30
+ ) -> ConfigUpdate | None:
31
+ """Read a config from given `path` in given `section`. The path should refer to
32
+ a TOML file that should decode to a `Config` object. If `section` is given, only
33
+ that section is decoded to a `Config` object. The `section` string may contain
34
+ periods to indicate deeper nesting.
35
+
36
+ Example:
37
+
38
+ ```python
39
+ read_config_from_toml(Path("./pyproject.toml"), "tool.entangled")
40
+ ```
41
+ """
42
+ if path not in fs:
43
+ return None
44
+ try:
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
+
54
+ except (msgspec.ValidationError, tomllib.TOMLDecodeError) as e:
55
+ raise HelpfulUserError(f"Could not read config: {e}")
56
+ except KeyError as e:
57
+ log.debug("%s", str(e))
58
+ log.debug("The config file %s should contain a section %s", path, section)
59
+ return None
60
+
61
+
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:
75
+ return (
76
+ read_config_from_toml(fs, Path("./pyproject.toml"), "tool.entangled")
77
+ )
78
+ return None
79
+
80
+
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
+ """
85
+ log.debug("watch list: %s; ignoring: %s", cfg.watch_list, cfg.ignore_list)
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))))
89
+ log.debug("input file list %s", input_file_list)
90
+ return input_file_list
91
+
92
+
93
+ __all__ = ["Config", "ConfigUpdate", "AnnotationMethod", "Markers", "NamespaceDefault"]
@@ -0,0 +1,17 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class AnnotationMethod(StrEnum):
5
+ """Annotation methods.
6
+
7
+ - `STANDARD` is the default. Comments tell where a piece of code
8
+ came from in enough detail to reconstruct the markdown if some
9
+ of the code is changed.
10
+ - `NAKED` adds no comments to the tangled files. Stitching is not
11
+ possible with this setting.
12
+ - `SUPPLEMENTED` adds extra information to the comment lines.
13
+ """
14
+
15
+ STANDARD = "standard"
16
+ NAKED = "naked"
17
+ SUPPLEMENTED = "supplemented"