entangled-cli 2.3.1__tar.gz → 2.4.0__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 (84) hide show
  1. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/.gitignore +1 -0
  2. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/PKG-INFO +6 -3
  3. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/commands/__init__.py +5 -11
  4. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/commands/brei.py +16 -10
  5. entangled_cli-2.4.0/entangled/commands/main.py +24 -0
  6. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/commands/new.py +35 -41
  7. entangled_cli-2.4.0/entangled/commands/reset.py +34 -0
  8. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/commands/status.py +11 -7
  9. entangled_cli-2.4.0/entangled/commands/stitch.py +31 -0
  10. entangled_cli-2.4.0/entangled/commands/sync.py +87 -0
  11. entangled_cli-2.4.0/entangled/commands/tangle.py +37 -0
  12. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/commands/watch.py +12 -7
  13. entangled_cli-2.4.0/entangled/config/__init__.py +83 -0
  14. entangled_cli-2.4.0/entangled/config/annotation_method.py +17 -0
  15. entangled_cli-2.4.0/entangled/config/config_data.py +105 -0
  16. entangled_cli-2.4.0/entangled/config/config_update.py +60 -0
  17. entangled_cli-2.4.0/entangled/config/document_style.py +18 -0
  18. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/config/language.py +2 -1
  19. entangled_cli-2.4.0/entangled/config/markers.py +21 -0
  20. entangled_cli-2.4.0/entangled/config/namespace_default.py +14 -0
  21. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/config/version.py +5 -6
  22. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/errors/user.py +20 -21
  23. entangled_cli-2.4.0/entangled/hooks/__init__.py +46 -0
  24. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/base.py +5 -1
  25. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/build.py +13 -14
  26. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/quarto_attributes.py +23 -15
  27. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/repl.py +7 -3
  28. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/shebang.py +5 -4
  29. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/spdx_license.py +5 -4
  30. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/hooks/task.py +25 -40
  31. entangled_cli-2.4.0/entangled/interface/__init__.py +4 -0
  32. entangled_cli-2.4.0/entangled/interface/context.py +56 -0
  33. entangled_cli-2.4.0/entangled/interface/document.py +105 -0
  34. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/io/__init__.py +2 -2
  35. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/io/filedb.py +9 -5
  36. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/io/transaction.py +26 -20
  37. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/io/virtual.py +62 -6
  38. entangled_cli-2.4.0/entangled/iterators/__init__.py +6 -0
  39. entangled_cli-2.4.0/entangled/iterators/lines.py +28 -0
  40. entangled_cli-2.4.0/entangled/iterators/peekable.py +43 -0
  41. entangled_cli-2.4.0/entangled/iterators/run_generator.py +14 -0
  42. entangled_cli-2.4.0/entangled/main.py +31 -0
  43. entangled_cli-2.4.0/entangled/model/__init__.py +23 -0
  44. entangled_cli-2.4.0/entangled/model/code_block.py +79 -0
  45. entangled_cli-2.4.0/entangled/model/content.py +34 -0
  46. {entangled_cli-2.3.1/entangled → entangled_cli-2.4.0/entangled/model}/properties.py +21 -1
  47. entangled_cli-2.4.0/entangled/model/reference_id.py +31 -0
  48. entangled_cli-2.4.0/entangled/model/reference_map.py +102 -0
  49. entangled_cli-2.4.0/entangled/model/reference_name.py +32 -0
  50. entangled_cli-2.4.0/entangled/model/tangle.py +154 -0
  51. entangled_cli-2.4.0/entangled/readers/__init__.py +16 -0
  52. entangled_cli-2.4.0/entangled/readers/code.py +100 -0
  53. entangled_cli-2.4.0/entangled/readers/delimiters.py +91 -0
  54. entangled_cli-2.4.0/entangled/readers/markdown.py +174 -0
  55. entangled_cli-2.4.0/entangled/readers/types.py +14 -0
  56. entangled_cli-2.4.0/entangled/readers/yaml_header.py +56 -0
  57. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/status.py +3 -12
  58. entangled_cli-2.4.0/entangled/text_location.py +21 -0
  59. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/pyproject.toml +15 -7
  60. entangled_cli-2.3.1/entangled/code_reader.py +0 -81
  61. entangled_cli-2.3.1/entangled/commands/reset.py +0 -62
  62. entangled_cli-2.3.1/entangled/commands/stitch.py +0 -64
  63. entangled_cli-2.3.1/entangled/commands/sync.py +0 -51
  64. entangled_cli-2.3.1/entangled/commands/tangle.py +0 -74
  65. entangled_cli-2.3.1/entangled/config/__init__.py +0 -198
  66. entangled_cli-2.3.1/entangled/document.py +0 -162
  67. entangled_cli-2.3.1/entangled/from_str.py +0 -27
  68. entangled_cli-2.3.1/entangled/hooks/__init__.py +0 -48
  69. entangled_cli-2.3.1/entangled/main.py +0 -54
  70. entangled_cli-2.3.1/entangled/markdown_reader.py +0 -229
  71. entangled_cli-2.3.1/entangled/tangle.py +0 -153
  72. entangled_cli-2.3.1/entangled/text_location.py +0 -12
  73. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/LICENSE +0 -0
  74. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/README.md +0 -0
  75. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/__init__.py +0 -0
  76. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/config/templates.py +0 -0
  77. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/errors/__init__.py +0 -0
  78. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/errors/internal.py +0 -0
  79. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/io/stat.py +0 -0
  80. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/logging.py +0 -0
  81. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/parsing.py +0 -0
  82. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/py.typed +0 -0
  83. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/utility.py +0 -0
  84. {entangled_cli-2.3.1 → entangled_cli-2.4.0}/entangled/version.py +0 -0
@@ -4,3 +4,4 @@ __pycache__
4
4
  coverage.*
5
5
  dist
6
6
  site
7
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entangled-cli
3
- Version: 2.3.1
3
+ Version: 2.4.0
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
@@ -10,17 +10,20 @@ License-File: LICENSE
10
10
  Requires-Python: <4,>=3.12
11
11
  Requires-Dist: argh<0.31,>=0.30
12
12
  Requires-Dist: brei<0.3,>=0.2.3
13
+ Requires-Dist: click>=8.3.0
13
14
  Requires-Dist: copier<10,>=9
14
15
  Requires-Dist: filelock<4,>=3.12.0
15
16
  Requires-Dist: mawk<0.2,>=0.1.7
16
- Requires-Dist: msgspec>=0.19.0
17
+ Requires-Dist: msgspec>=0.20.0
17
18
  Requires-Dist: pexpect<5,>=4.9.0
18
19
  Requires-Dist: pyyaml<7,>=6.0.1
19
20
  Requires-Dist: repl-session>=0.2.0
20
21
  Requires-Dist: rich-argparse<2,>=1.4.0
22
+ Requires-Dist: rich-click>=1.9.4
21
23
  Requires-Dist: rich<14,>=13.3.5
22
24
  Requires-Dist: tomlkit<0.13,>=0.12.1
23
- Requires-Dist: watchdog<4,>=3.0.0
25
+ Requires-Dist: typeguard>=4.4.4
26
+ Requires-Dist: watchdog>=6.0.0
24
27
  Description-Content-Type: text/markdown
25
28
 
26
29
  # 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,25 @@
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
9
8
  from brei import resolve_tasks, Phony
10
9
  from ..logging import logger
10
+ from .main import main
11
+
12
+ import click
11
13
 
12
14
  log = logger()
13
15
 
14
16
 
15
- async def main(target_strs: list[str], force_run: bool, throttle: int | None):
17
+ async def brei_main(target_strs: list[str], force_run: bool, throttle: int | None):
16
18
  if not Path(".entangled").exists():
17
19
  Path(".entangled").mkdir()
18
20
 
19
- db = await resolve_tasks(config.get.brei, Path(".entangled/brei_history"))
21
+ cfg = Config() | read_config()
22
+ db = await resolve_tasks(cfg.brei, Path(".entangled/brei_history"))
20
23
  if throttle:
21
24
  db.throttle = asyncio.Semaphore(throttle)
22
25
  db.force_run = force_run
@@ -33,10 +36,13 @@ async def main(target_strs: list[str], force_run: bool, throttle: int | None):
33
36
  log.error(msg)
34
37
 
35
38
 
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")
39
+ @main.command()
40
+ @click.argument("targets", nargs=-1)
41
+ @click.option("-B", "--force-run", is_flag=True, help="rebuild all dependencies")
42
+ @click.option("-j", "--throttle", is_flag=True, help="limit number of concurrent jobs")
39
43
  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))
44
+ """Build one of the configured targets.
45
+
46
+ TARGETS Names of the targets to run.
47
+ """
48
+ asyncio.run(brei_main(targets, force_run, throttle))
@@ -0,0 +1,24 @@
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().debug(f"Welcome to Entangled v{__version__}!")
24
+
@@ -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,34 @@
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, transaction
10
+ from ..errors.user import UserError
11
+ from ..interface import Document
12
+ from .main import main
13
+
14
+ import logging
15
+
16
+
17
+ @main.command(short_help="Reset the file database.")
18
+ def reset():
19
+ """
20
+ Resets the file database. This performs a tangle without actually
21
+ writing output to the files, but updating the database as if we were.
22
+ """
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))
@@ -1,7 +1,7 @@
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
5
  from pathlib import Path
6
6
 
7
7
  from rich.console import Console, Group
@@ -10,6 +10,8 @@ from rich.table import Table
10
10
  from rich.panel import Panel
11
11
  from rich.tree import Tree
12
12
 
13
+ from .main import main
14
+
13
15
 
14
16
  def tree_from_files(files: Iterable[Path]):
15
17
  tree = Tree(label=".")
@@ -28,23 +30,24 @@ def files_panel(file_list: Iterable[Path], title: str) -> Panel:
28
30
 
29
31
 
30
32
  def rich_status():
33
+ cfg = Config() | read_config()
31
34
  config_table = Table()
32
35
  config_table.add_column("name")
33
36
  config_table.add_column("value")
34
37
  config_table.add_row(
35
- "Watch list", ", ".join(f"'{pat}'" for pat in config.get.watch_list)
38
+ "Watch list", ", ".join(f"'{pat}'" for pat in cfg.watch_list)
36
39
  )
37
40
  config_table.add_row(
38
- "Ignore list", ", ".join(f"'{pat}'" for pat in config.get.ignore_list)
41
+ "Ignore list", ", ".join(f"'{pat}'" for pat in cfg.ignore_list)
39
42
  )
40
- config_table.add_row("Hooks enabled", ", ".join(config.get.hooks))
43
+ config_table.add_row("Hooks enabled", ", ".join(cfg.hooks))
41
44
 
42
45
  console = Console(color_system="auto")
43
46
  group = Group(
44
47
  Panel(config_table, title="config", border_style="dark_cyan"),
45
48
  Columns(
46
49
  [
47
- files_panel(list_input_files(), "input files"),
50
+ files_panel(get_input_files(cfg), "input files"),
48
51
  files_panel(list_dependent_files(), "dependent files"),
49
52
  ]
50
53
  ),
@@ -53,6 +56,7 @@ def rich_status():
53
56
  console.print(group)
54
57
 
55
58
 
59
+ @main.command()
56
60
  def status():
57
- config.read()
61
+ """Print a status overview."""
58
62
  rich_status()
@@ -0,0 +1,31 @@
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
+ try:
23
+ doc = Document()
24
+
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()
@@ -0,0 +1,87 @@
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
+ fs = FileCache()
21
+
22
+ with filedb(readonly=True) as db:
23
+ changed = set(db.changed_files(fs))
24
+
25
+ if not all(f in db for f in input_file_list):
26
+ return Action.TANGLE
27
+
28
+ if not changed:
29
+ return Action.NOTHING
30
+
31
+ if changed.isdisjoint(db.managed_files):
32
+ return Action.TANGLE
33
+
34
+ if changed.issubset(db.managed_files):
35
+ return Action.STITCH
36
+
37
+ logging.error("changed: %s", [str(p) for p in changed])
38
+ logging.error(
39
+ "Both markdown and code seem to have changed, don't know what to do now."
40
+ )
41
+ return Action.NOTHING
42
+
43
+
44
+ def tangle(doc: Document):
45
+ with transaction() as t:
46
+ doc.load(t)
47
+ doc.tangle(t)
48
+ t.clear_orphans()
49
+ for h in doc.context.all_hooks:
50
+ h.post_tangle(doc.reference_map)
51
+
52
+
53
+ def stitch(doc: Document):
54
+ with transaction() as t:
55
+ doc.load(t)
56
+ doc.load_all_code(t)
57
+ doc.stitch(t)
58
+ with transaction() as t:
59
+ doc.tangle(t)
60
+ for h in doc.context.all_hooks:
61
+ h.post_tangle(doc.reference_map)
62
+
63
+
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
+
72
+ case Action.STITCH:
73
+ logging.info("Stitching.")
74
+ stitch(doc)
75
+
76
+ case Action.NOTHING:
77
+ pass
78
+
79
+ except UserError as e:
80
+ e.handle()
81
+
82
+
83
+ @main.command()
84
+ def sync():
85
+ """Be smart wether to tangle or stich"""
86
+ run_sync()
87
+
@@ -0,0 +1,37 @@
1
+ import rich_click as click
2
+
3
+ from .main import main
4
+
5
+ from ..config import AnnotationMethod
6
+ from ..io import transaction, TransactionMode
7
+ from ..errors.user import UserError
8
+ from ..interface import 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
+ """Tangle codes from the documentation."""
18
+ if show:
19
+ mode = TransactionMode.SHOW
20
+ elif force:
21
+ mode = TransactionMode.FORCE
22
+ else:
23
+ mode = TransactionMode.FAIL
24
+
25
+ try:
26
+ doc = Document()
27
+
28
+ with transaction(mode) as t:
29
+ doc.load(t)
30
+ doc.tangle(t, annotate)
31
+ t.clear_orphans()
32
+
33
+ for h in doc.context.all_hooks:
34
+ h.post_tangle(doc.reference_map)
35
+
36
+ except UserError as e:
37
+ e.handle()
@@ -4,10 +4,11 @@ from threading import Event
4
4
  from watchdog.observers import Observer
5
5
  from watchdog.events import FileSystemEventHandler, FileSystemEvent
6
6
 
7
- from .sync import sync
8
- from ..config import config
9
7
  from ..status import find_watch_dirs
10
8
 
9
+ from .sync import run_sync
10
+ from .main import main
11
+
11
12
 
12
13
  class EventHandler(FileSystemEventHandler):
13
14
  def __init__(self):
@@ -19,12 +20,16 @@ class EventHandler(FileSystemEventHandler):
19
20
  def on_any_event(self, event: FileSystemEvent):
20
21
  if event.event_type == "opened":
21
22
  return
22
- config.read()
23
- path = Path(event.src_path)
23
+
24
+ if isinstance(event.src_path, bytes):
25
+ path = Path(event.src_path.decode("utf-8"))
26
+ else:
27
+ path = Path(event.src_path)
28
+
24
29
  if path.absolute().is_relative_to(Path("./.entangled").absolute()):
25
30
  return
26
31
  if any(path.absolute().is_relative_to(p.absolute()) for p in self.watched):
27
- sync()
32
+ run_sync()
28
33
  # os.sync()
29
34
  self.update_watched()
30
35
 
@@ -37,7 +42,7 @@ def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
37
42
  def stop() -> bool:
38
43
  return _stop_event is not None and _stop_event.is_set()
39
44
 
40
- sync()
45
+ run_sync()
41
46
 
42
47
  event_handler = EventHandler()
43
48
  observer = Observer()
@@ -55,7 +60,7 @@ def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
55
60
  observer.join()
56
61
 
57
62
 
63
+ @main.command()
58
64
  def watch():
59
65
  """Keep a loop running, watching for changes."""
60
- config.read()
61
66
  _watch()
@@ -0,0 +1,83 @@
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
+
24
+
25
+ log = logger()
26
+
27
+
28
+ def read_config_from_toml(
29
+ 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 not path.exists():
43
+ return None
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
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() -> ConfigUpdate | None:
63
+ if Path("./entangled.toml").exists():
64
+ return read_config_from_toml(Path("./entangled.toml"))
65
+ if Path("./pyproject.toml").exists():
66
+ return (
67
+ read_config_from_toml(Path("./pyproject.toml"), "tool.entangled")
68
+ )
69
+ return None
70
+
71
+
72
+ def get_input_files(cfg: Config) -> list[Path]:
73
+ 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
+ ]
79
+ log.debug("input file list %s", input_file_list)
80
+ return sorted(input_file_list)
81
+
82
+
83
+ __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"