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.
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/.gitignore +1 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/PKG-INFO +8 -8
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/__init__.py +5 -11
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/brei.py +17 -10
- entangled_cli-2.4.2/entangled/commands/main.py +23 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/new.py +35 -41
- entangled_cli-2.4.2/entangled/commands/reset.py +20 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/commands/status.py +13 -7
- entangled_cli-2.4.2/entangled/commands/stitch.py +27 -0
- entangled_cli-2.4.2/entangled/commands/sync.py +81 -0
- entangled_cli-2.4.2/entangled/commands/tangle.py +48 -0
- entangled_cli-2.4.2/entangled/commands/watch.py +47 -0
- entangled_cli-2.4.2/entangled/config/__init__.py +93 -0
- entangled_cli-2.4.2/entangled/config/annotation_method.py +17 -0
- entangled_cli-2.4.2/entangled/config/config_data.py +105 -0
- entangled_cli-2.4.2/entangled/config/config_update.py +60 -0
- entangled_cli-2.4.2/entangled/config/document_style.py +18 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/config/language.py +2 -1
- entangled_cli-2.4.2/entangled/config/markers.py +21 -0
- entangled_cli-2.4.2/entangled/config/namespace_default.py +14 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/config/version.py +5 -6
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/errors/user.py +20 -21
- entangled_cli-2.4.2/entangled/hooks/__init__.py +47 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/base.py +9 -2
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/build.py +22 -19
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/quarto_attributes.py +25 -17
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/repl.py +18 -8
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/shebang.py +5 -4
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/spdx_license.py +5 -4
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/hooks/task.py +36 -45
- entangled_cli-2.4.2/entangled/interface/__init__.py +4 -0
- entangled_cli-2.4.2/entangled/interface/context.py +63 -0
- entangled_cli-2.4.2/entangled/interface/document.py +106 -0
- entangled_cli-2.4.2/entangled/io/__init__.py +11 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/filedb.py +38 -18
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/stat.py +2 -3
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/transaction.py +26 -20
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/io/virtual.py +93 -6
- entangled_cli-2.4.2/entangled/iterators/__init__.py +6 -0
- entangled_cli-2.4.2/entangled/iterators/lines.py +28 -0
- entangled_cli-2.4.2/entangled/iterators/peekable.py +43 -0
- entangled_cli-2.4.2/entangled/iterators/run_generator.py +14 -0
- entangled_cli-2.4.2/entangled/main.py +31 -0
- entangled_cli-2.4.2/entangled/model/__init__.py +23 -0
- entangled_cli-2.4.2/entangled/model/code_block.py +79 -0
- entangled_cli-2.4.2/entangled/model/content.py +34 -0
- {entangled_cli-2.3.2/entangled → entangled_cli-2.4.2/entangled/model}/properties.py +21 -1
- entangled_cli-2.4.2/entangled/model/reference_id.py +31 -0
- entangled_cli-2.4.2/entangled/model/reference_map.py +102 -0
- entangled_cli-2.4.2/entangled/model/reference_name.py +32 -0
- entangled_cli-2.4.2/entangled/model/tangle.py +158 -0
- entangled_cli-2.4.2/entangled/readers/__init__.py +16 -0
- entangled_cli-2.4.2/entangled/readers/code.py +107 -0
- entangled_cli-2.4.2/entangled/readers/delimiters.py +91 -0
- entangled_cli-2.4.2/entangled/readers/markdown.py +179 -0
- entangled_cli-2.4.2/entangled/readers/types.py +14 -0
- entangled_cli-2.4.2/entangled/readers/yaml_header.py +56 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/status.py +12 -17
- entangled_cli-2.4.2/entangled/text_location.py +21 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/utility.py +0 -4
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/pyproject.toml +19 -12
- 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/commands/watch.py +0 -66
- 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/io/__init__.py +0 -11
- 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.2}/LICENSE +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/README.md +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/__init__.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/config/templates.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/logging.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/parsing.py +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/py.typed +0 -0
- {entangled_cli-2.3.2 → entangled_cli-2.4.2}/entangled/version.py +0 -0
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: entangled-cli
|
|
3
|
-
Version: 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.
|
|
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:
|
|
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
|
|
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:
|
|
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 .
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
@
|
|
37
|
-
@
|
|
38
|
-
@
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
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,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
|
|
4
|
-
from ..config import
|
|
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
|
|
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
|
|
43
|
+
"Ignore list", ", ".join(f"'{pat}'" for pat in cfg.ignore_list)
|
|
39
44
|
)
|
|
40
|
-
config_table.add_row("Hooks enabled", ", ".join(
|
|
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(
|
|
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
|
-
|
|
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"
|