entangled-cli 2.3.2__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.
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/.gitignore +1 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/PKG-INFO +5 -2
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/commands/__init__.py +5 -11
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/commands/brei.py +16 -10
- entangled_cli-2.4.0/entangled/commands/main.py +24 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/commands/new.py +35 -41
- entangled_cli-2.4.0/entangled/commands/reset.py +34 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/commands/status.py +11 -7
- entangled_cli-2.4.0/entangled/commands/stitch.py +31 -0
- entangled_cli-2.4.0/entangled/commands/sync.py +87 -0
- entangled_cli-2.4.0/entangled/commands/tangle.py +37 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/commands/watch.py +6 -6
- entangled_cli-2.4.0/entangled/config/__init__.py +83 -0
- entangled_cli-2.4.0/entangled/config/annotation_method.py +17 -0
- entangled_cli-2.4.0/entangled/config/config_data.py +105 -0
- entangled_cli-2.4.0/entangled/config/config_update.py +60 -0
- entangled_cli-2.4.0/entangled/config/document_style.py +18 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/config/language.py +2 -1
- entangled_cli-2.4.0/entangled/config/markers.py +21 -0
- entangled_cli-2.4.0/entangled/config/namespace_default.py +14 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/config/version.py +5 -6
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/errors/user.py +20 -21
- entangled_cli-2.4.0/entangled/hooks/__init__.py +46 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/base.py +5 -1
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/build.py +13 -14
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/quarto_attributes.py +23 -15
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/repl.py +7 -3
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/shebang.py +5 -4
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/spdx_license.py +5 -4
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/hooks/task.py +25 -40
- entangled_cli-2.4.0/entangled/interface/__init__.py +4 -0
- entangled_cli-2.4.0/entangled/interface/context.py +56 -0
- entangled_cli-2.4.0/entangled/interface/document.py +105 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/io/__init__.py +2 -2
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/io/filedb.py +9 -5
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/io/transaction.py +26 -20
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/io/virtual.py +62 -6
- entangled_cli-2.4.0/entangled/iterators/__init__.py +6 -0
- entangled_cli-2.4.0/entangled/iterators/lines.py +28 -0
- entangled_cli-2.4.0/entangled/iterators/peekable.py +43 -0
- entangled_cli-2.4.0/entangled/iterators/run_generator.py +14 -0
- entangled_cli-2.4.0/entangled/main.py +31 -0
- entangled_cli-2.4.0/entangled/model/__init__.py +23 -0
- entangled_cli-2.4.0/entangled/model/code_block.py +79 -0
- entangled_cli-2.4.0/entangled/model/content.py +34 -0
- {entangled_cli-2.3.2/entangled → entangled_cli-2.4.0/entangled/model}/properties.py +21 -1
- entangled_cli-2.4.0/entangled/model/reference_id.py +31 -0
- entangled_cli-2.4.0/entangled/model/reference_map.py +102 -0
- entangled_cli-2.4.0/entangled/model/reference_name.py +32 -0
- entangled_cli-2.4.0/entangled/model/tangle.py +154 -0
- entangled_cli-2.4.0/entangled/readers/__init__.py +16 -0
- entangled_cli-2.4.0/entangled/readers/code.py +100 -0
- entangled_cli-2.4.0/entangled/readers/delimiters.py +91 -0
- entangled_cli-2.4.0/entangled/readers/markdown.py +174 -0
- entangled_cli-2.4.0/entangled/readers/types.py +14 -0
- entangled_cli-2.4.0/entangled/readers/yaml_header.py +56 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/status.py +3 -12
- entangled_cli-2.4.0/entangled/text_location.py +21 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/pyproject.toml +14 -6
- entangled_cli-2.3.2/entangled/code_reader.py +0 -81
- entangled_cli-2.3.2/entangled/commands/reset.py +0 -62
- entangled_cli-2.3.2/entangled/commands/stitch.py +0 -64
- entangled_cli-2.3.2/entangled/commands/sync.py +0 -51
- entangled_cli-2.3.2/entangled/commands/tangle.py +0 -74
- entangled_cli-2.3.2/entangled/config/__init__.py +0 -198
- entangled_cli-2.3.2/entangled/document.py +0 -162
- entangled_cli-2.3.2/entangled/from_str.py +0 -27
- entangled_cli-2.3.2/entangled/hooks/__init__.py +0 -48
- entangled_cli-2.3.2/entangled/main.py +0 -54
- entangled_cli-2.3.2/entangled/markdown_reader.py +0 -229
- entangled_cli-2.3.2/entangled/tangle.py +0 -153
- entangled_cli-2.3.2/entangled/text_location.py +0 -12
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/LICENSE +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/README.md +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/__init__.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/config/templates.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/io/stat.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/logging.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/parsing.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/py.typed +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/utility.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.0}/entangled/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: entangled-cli
|
|
3
|
-
Version: 2.
|
|
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,16 +10,19 @@ 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.
|
|
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
|
|
25
|
+
Requires-Dist: typeguard>=4.4.4
|
|
23
26
|
Requires-Dist: watchdog>=6.0.0
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
from .
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
@
|
|
37
|
-
@
|
|
38
|
-
@
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
ctx = click.get_current_context()
|
|
44
|
+
click.echo(ctx.get_help())
|
|
45
|
+
ctx.exit()
|
|
48
46
|
|
|
49
47
|
|
|
50
|
-
@
|
|
51
|
-
"-
|
|
52
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
|
99
|
-
project_path: Path
|
|
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
|
|
4
|
-
from ..config import
|
|
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
|
|
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
|
|
41
|
+
"Ignore list", ", ".join(f"'{pat}'" for pat in cfg.ignore_list)
|
|
39
42
|
)
|
|
40
|
-
config_table.add_row("Hooks enabled", ", ".join(
|
|
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(
|
|
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
|
-
|
|
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,7 +20,6 @@ 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
23
|
|
|
24
24
|
if isinstance(event.src_path, bytes):
|
|
25
25
|
path = Path(event.src_path.decode("utf-8"))
|
|
@@ -29,7 +29,7 @@ class EventHandler(FileSystemEventHandler):
|
|
|
29
29
|
if path.absolute().is_relative_to(Path("./.entangled").absolute()):
|
|
30
30
|
return
|
|
31
31
|
if any(path.absolute().is_relative_to(p.absolute()) for p in self.watched):
|
|
32
|
-
|
|
32
|
+
run_sync()
|
|
33
33
|
# os.sync()
|
|
34
34
|
self.update_watched()
|
|
35
35
|
|
|
@@ -42,7 +42,7 @@ def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
|
|
|
42
42
|
def stop() -> bool:
|
|
43
43
|
return _stop_event is not None and _stop_event.is_set()
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
run_sync()
|
|
46
46
|
|
|
47
47
|
event_handler = EventHandler()
|
|
48
48
|
observer = Observer()
|
|
@@ -60,7 +60,7 @@ def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
|
|
|
60
60
|
observer.join()
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
@main.command()
|
|
63
64
|
def watch():
|
|
64
65
|
"""Keep a loop running, watching for changes."""
|
|
65
|
-
config.read()
|
|
66
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"
|