entangled-cli 2.1.12__tar.gz → 2.2.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.1.12 → entangled_cli-2.2.0}/PKG-INFO +7 -3
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/README.md +2 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/code_reader.py +7 -6
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/status.py +3 -4
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/stitch.py +2 -3
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/tangle.py +4 -5
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/watch.py +1 -4
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/config/__init__.py +47 -38
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/config/language.py +6 -8
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/config/templates.py +9 -3
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/config/version.py +5 -3
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/document.py +70 -8
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/errors/user.py +11 -2
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/filedb.py +23 -11
- entangled_cli-2.2.0/entangled/from_str.py +27 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/hooks/__init__.py +14 -10
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/hooks/base.py +6 -6
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/hooks/build.py +11 -5
- entangled_cli-2.2.0/entangled/hooks/quarto_attributes.py +61 -0
- entangled_cli-2.2.0/entangled/hooks/repl.py +86 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/hooks/shebang.py +4 -2
- entangled_cli-2.2.0/entangled/hooks/spdx_license.py +12 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/hooks/task.py +5 -5
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/logging.py +1 -1
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/main.py +6 -4
- entangled_cli-2.2.0/entangled/markdown_reader.py +228 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/parsing.py +9 -5
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/properties.py +5 -4
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/status.py +4 -3
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/tangle.py +19 -14
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/text_location.py +3 -2
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/transaction.py +30 -13
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/utility.py +5 -10
- entangled_cli-2.2.0/pyproject.toml +67 -0
- entangled_cli-2.1.12/entangled/construct.py +0 -167
- entangled_cli-2.1.12/entangled/hooks/quarto_attributes.py +0 -51
- entangled_cli-2.1.12/entangled/markdown_reader.py +0 -168
- entangled_cli-2.1.12/pyproject.toml +0 -60
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/.gitignore +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/LICENSE +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/__init__.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/__init__.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/brei.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/new.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/commands/sync.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/py.typed +0 -0
- {entangled_cli-2.1.12 → entangled_cli-2.2.0}/entangled/version.py +0 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: entangled-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
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:
|
|
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
13
|
Requires-Dist: copier<10,>=9
|
|
14
14
|
Requires-Dist: filelock<4,>=3.12.0
|
|
15
|
-
Requires-Dist: mawk<0.2,>=0.1.
|
|
15
|
+
Requires-Dist: mawk<0.2,>=0.1.7
|
|
16
|
+
Requires-Dist: msgspec>=0.19.0
|
|
16
17
|
Requires-Dist: pexpect<5,>=4.9.0
|
|
17
18
|
Requires-Dist: pyyaml<7,>=6.0.1
|
|
19
|
+
Requires-Dist: repl-session>=0.2.0
|
|
18
20
|
Requires-Dist: rich-argparse<2,>=1.4.0
|
|
19
21
|
Requires-Dist: rich<14,>=13.3.5
|
|
20
22
|
Requires-Dist: tomlkit<0.13,>=0.12.1
|
|
@@ -218,6 +220,8 @@ Entangled has a system of *hooks*: these add actions to the tangling process:
|
|
|
218
220
|
- `brei` trigger actions (or tasks) using [Brei](https://entangled.github.io/brei), which is automatically installed along with Entangled. This is now prefered over the `build` hook.
|
|
219
221
|
- `quarto_attributes` add attributes to the code block in Quatro style with `#|` comments at the top of the code block.
|
|
220
222
|
- `shebang` takes the first line if it starts with `#!` and puts it at the top of the file.
|
|
223
|
+
- `spdx_license` takes the first line if it contains `SPDX-License-Identifier`
|
|
224
|
+
and puts it at the top of the file.
|
|
221
225
|
|
|
222
226
|
### `build` hook
|
|
223
227
|
|
|
@@ -195,6 +195,8 @@ Entangled has a system of *hooks*: these add actions to the tangling process:
|
|
|
195
195
|
- `brei` trigger actions (or tasks) using [Brei](https://entangled.github.io/brei), which is automatically installed along with Entangled. This is now prefered over the `build` hook.
|
|
196
196
|
- `quarto_attributes` add attributes to the code block in Quatro style with `#|` comments at the top of the code block.
|
|
197
197
|
- `shebang` takes the first line if it starts with `#!` and puts it at the top of the file.
|
|
198
|
+
- `spdx_license` takes the first line if it contains `SPDX-License-Identifier`
|
|
199
|
+
and puts it at the top of the file.
|
|
198
200
|
|
|
199
201
|
### `build` hook
|
|
200
202
|
|
|
@@ -4,7 +4,8 @@ from pathlib import Path, PurePath
|
|
|
4
4
|
import mawk
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
|
-
from .
|
|
7
|
+
from .text_location import TextLocation
|
|
8
|
+
from .document import ReferenceId, ReferenceMap
|
|
8
9
|
from .errors.user import IndentationError
|
|
9
10
|
|
|
10
11
|
|
|
@@ -19,7 +20,7 @@ class CodeReader(mawk.RuleSet):
|
|
|
19
20
|
"""Reads an annotated code file."""
|
|
20
21
|
|
|
21
22
|
def __init__(self, path: PurePath, refs: ReferenceMap):
|
|
22
|
-
self.location = TextLocation(path, 0)
|
|
23
|
+
self.location: TextLocation = TextLocation(path, 0)
|
|
23
24
|
self.stack: list[Frame] = [Frame(ReferenceId("#root#", PurePath("-"), -1), "")]
|
|
24
25
|
self.refs: ReferenceMap = refs
|
|
25
26
|
|
|
@@ -34,7 +35,7 @@ class CodeReader(mawk.RuleSet):
|
|
|
34
35
|
@mawk.on_match(
|
|
35
36
|
r"^(?P<indent>\s*).* ~/~ begin <<(?P<source>[^#<>]+)#(?P<ref_name>[^#<>]+)>>\[(?P<ref_count>init|\d+)\]"
|
|
36
37
|
)
|
|
37
|
-
def on_block_begin(self, m: re.Match):
|
|
38
|
+
def on_block_begin(self, m: re.Match[str]) -> list[str]:
|
|
38
39
|
ref_name = m["ref_name"]
|
|
39
40
|
|
|
40
41
|
# When there are lines above the first ref, say a shebang, swap
|
|
@@ -62,15 +63,15 @@ class CodeReader(mawk.RuleSet):
|
|
|
62
63
|
return []
|
|
63
64
|
|
|
64
65
|
@mawk.on_match(r"^(?P<indent>\s*).* ~/~ end")
|
|
65
|
-
def on_block_end(self, m: re.Match):
|
|
66
|
+
def on_block_end(self, m: re.Match[str]) -> list[str]:
|
|
66
67
|
if m["indent"] != self.current.indent:
|
|
67
68
|
raise IndentationError(self.location)
|
|
68
69
|
self.refs[self.current.ref].source = "\n".join(self.current.content)
|
|
69
|
-
self.stack.pop()
|
|
70
|
+
_ = self.stack.pop()
|
|
70
71
|
return []
|
|
71
72
|
|
|
72
73
|
@mawk.always
|
|
73
|
-
def otherwise(self, line: str):
|
|
74
|
+
def otherwise(self, line: str) -> list[str]:
|
|
74
75
|
if line.strip() == "":
|
|
75
76
|
self.current.content.append("")
|
|
76
77
|
return []
|
|
@@ -29,12 +29,12 @@ def rich_status():
|
|
|
29
29
|
config_table.add_column("name")
|
|
30
30
|
config_table.add_column("value")
|
|
31
31
|
config_table.add_row(
|
|
32
|
-
"Watch list", ", ".join(f"'{pat}'" for pat in config.watch_list)
|
|
32
|
+
"Watch list", ", ".join(f"'{pat}'" for pat in config.get.watch_list)
|
|
33
33
|
)
|
|
34
34
|
config_table.add_row(
|
|
35
|
-
"Ignore list", ", ".join(f"'{pat}'" for pat in config.ignore_list)
|
|
35
|
+
"Ignore list", ", ".join(f"'{pat}'" for pat in config.get.ignore_list)
|
|
36
36
|
)
|
|
37
|
-
config_table.add_row("Hooks enabled", ", ".join(config.hooks))
|
|
37
|
+
config_table.add_row("Hooks enabled", ", ".join(config.get.hooks))
|
|
38
38
|
|
|
39
39
|
console = Console(color_system="auto")
|
|
40
40
|
group = Group(
|
|
@@ -53,4 +53,3 @@ def rich_status():
|
|
|
53
53
|
def status():
|
|
54
54
|
config.read()
|
|
55
55
|
rich_status()
|
|
56
|
-
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from itertools import chain
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from textwrap import indent
|
|
4
3
|
|
|
5
4
|
import logging
|
|
6
5
|
import argh # type: ignore
|
|
@@ -18,7 +17,7 @@ def stitch_markdown(reference_map: ReferenceMap, content: list[Content]) -> str:
|
|
|
18
17
|
case PlainText(s):
|
|
19
18
|
return s
|
|
20
19
|
case ReferenceId():
|
|
21
|
-
return
|
|
20
|
+
return reference_map.get_codeblock(item).indented_text
|
|
22
21
|
|
|
23
22
|
return "\n".join(get(i) for i in content) + "\n"
|
|
24
23
|
|
|
@@ -33,7 +32,7 @@ def stitch(*, force: bool = False, show: bool = False):
|
|
|
33
32
|
from ..markdown_reader import read_markdown_file
|
|
34
33
|
from ..code_reader import CodeReader
|
|
35
34
|
from ..hooks import get_hooks
|
|
36
|
-
|
|
35
|
+
|
|
37
36
|
input_file_list = get_input_files()
|
|
38
37
|
hooks = get_hooks()
|
|
39
38
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
1
|
from itertools import chain
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
@@ -13,10 +12,10 @@ from ..errors.user import UserError
|
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def get_input_files() -> list[Path]:
|
|
16
|
-
include_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
|
|
15
|
+
include_file_list = chain.from_iterable(map(Path(".").glob, config.get.watch_list))
|
|
17
16
|
input_file_list = [
|
|
18
17
|
path for path in include_file_list
|
|
19
|
-
if not any(path.match(pat) for pat in config.ignore_list)
|
|
18
|
+
if not any(path.match(pat) for pat in config.get.ignore_list)
|
|
20
19
|
]
|
|
21
20
|
return input_file_list
|
|
22
21
|
|
|
@@ -30,7 +29,7 @@ def get_input_files() -> list[Path]:
|
|
|
30
29
|
@argh.arg("--force", help="force overwrite on conflict")
|
|
31
30
|
@argh.arg("-s", "--show", help="only show, don't act")
|
|
32
31
|
@argh.arg("-r", "--reset-db", help="resets database")
|
|
33
|
-
def tangle(*, annotate:
|
|
32
|
+
def tangle(*, annotate: str | None = None, force: bool = False, show: bool = False, reset_db: bool = False):
|
|
34
33
|
"""Tangle codes from Markdown"""
|
|
35
34
|
config.read()
|
|
36
35
|
|
|
@@ -39,7 +38,7 @@ def tangle(*, annotate: Optional[str] = None, force: bool = False, show: bool =
|
|
|
39
38
|
from ..tangle import tangle_ref
|
|
40
39
|
|
|
41
40
|
if annotate is None:
|
|
42
|
-
annotation_method = config.annotation
|
|
41
|
+
annotation_method = config.get.annotation
|
|
43
42
|
else:
|
|
44
43
|
annotation_method = AnnotationMethod[annotate.upper()]
|
|
45
44
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
1
|
from pathlib import Path
|
|
3
|
-
from itertools import chain
|
|
4
2
|
from threading import Event
|
|
5
|
-
import os
|
|
6
3
|
|
|
7
4
|
from watchdog.observers import Observer
|
|
8
5
|
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
@@ -32,7 +29,7 @@ class EventHandler(FileSystemEventHandler):
|
|
|
32
29
|
self.update_watched()
|
|
33
30
|
|
|
34
31
|
|
|
35
|
-
def _watch(_stop_event:
|
|
32
|
+
def _watch(_stop_event: Event | None = None, _start_event: Event | None = None):
|
|
36
33
|
"""Keep a loop running, watching for changes. This interface is separated
|
|
37
34
|
from the CLI one, so that it can be tested using threading instead of
|
|
38
35
|
subprocess."""
|
|
@@ -4,21 +4,21 @@ defaults and config loaded from `entangled.toml` in the work directory.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from functools import cached_property
|
|
7
8
|
import threading
|
|
8
9
|
from contextlib import contextmanager
|
|
9
|
-
from copy import copy
|
|
10
|
-
from
|
|
11
|
-
from enum import Enum
|
|
10
|
+
from copy import copy, deepcopy
|
|
11
|
+
from enum import StrEnum
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
14
14
|
|
|
15
|
+
import msgspec
|
|
16
|
+
from msgspec import Struct, field
|
|
15
17
|
import tomllib
|
|
16
|
-
from entangled.errors.internal import InternalError
|
|
17
|
-
|
|
18
|
-
from entangled.errors.user import UserError
|
|
19
18
|
|
|
20
19
|
from brei import Program
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
from entangled import from_str
|
|
22
22
|
from .language import Language, languages
|
|
23
23
|
from .version import Version
|
|
24
24
|
from ..logging import logger
|
|
@@ -27,7 +27,7 @@ from ..logging import logger
|
|
|
27
27
|
log = logger()
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class AnnotationMethod(
|
|
30
|
+
class AnnotationMethod(StrEnum):
|
|
31
31
|
"""Annotation methods.
|
|
32
32
|
|
|
33
33
|
- `STANDARD` is the default. Comments tell where a piece of code
|
|
@@ -38,13 +38,12 @@ class AnnotationMethod(Enum):
|
|
|
38
38
|
- `SUPPLEMENTED` adds extra information to the comment lines.
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
|
-
STANDARD =
|
|
42
|
-
NAKED =
|
|
43
|
-
SUPPLEMENTED =
|
|
41
|
+
STANDARD = "standard"
|
|
42
|
+
NAKED = "naked"
|
|
43
|
+
SUPPLEMENTED = "supplemented"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
class Markers:
|
|
46
|
+
class Markers(Struct):
|
|
48
47
|
"""Markers can be used to configure the Markdown dialect. Currently not used."""
|
|
49
48
|
|
|
50
49
|
open: str
|
|
@@ -58,8 +57,7 @@ markers = Markers(
|
|
|
58
57
|
)
|
|
59
58
|
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
class Config(threading.local):
|
|
60
|
+
class Config(Struct, dict=True):
|
|
63
61
|
"""Main config class.
|
|
64
62
|
|
|
65
63
|
Attributes:
|
|
@@ -79,35 +77,40 @@ class Config(threading.local):
|
|
|
79
77
|
|
|
80
78
|
This class is made thread-local to make it possible to test in parallel."""
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
_version: str = field(name = "version")
|
|
83
81
|
languages: list[Language] = field(default_factory=list)
|
|
84
82
|
markers: Markers = field(default_factory=lambda: copy(markers))
|
|
85
83
|
watch_list: list[str] = field(default_factory=lambda: ["**/*.md"])
|
|
86
|
-
ignore_list: list[str] = field(default_factory=
|
|
87
|
-
annotation_format:
|
|
84
|
+
ignore_list: list[str] = field(default_factory=list)
|
|
85
|
+
annotation_format: str | None = None
|
|
88
86
|
annotation: AnnotationMethod = AnnotationMethod.STANDARD
|
|
89
87
|
use_line_directives: bool = False
|
|
90
88
|
hooks: list[str] = field(default_factory=lambda: ["shebang"])
|
|
91
|
-
hook: dict = field(default_factory=dict)
|
|
89
|
+
hook: dict[str, Any] = field(default_factory=dict)
|
|
92
90
|
brei: Program = field(default_factory=Program)
|
|
93
91
|
|
|
92
|
+
language_index: dict[str, Language] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
@cached_property
|
|
95
|
+
def version(self) -> Version:
|
|
96
|
+
return Version.from_str(self._version)
|
|
97
|
+
|
|
94
98
|
def __post_init__(self):
|
|
95
99
|
self.languages = languages + self.languages
|
|
96
100
|
self.make_language_index()
|
|
97
101
|
|
|
98
102
|
def make_language_index(self):
|
|
99
|
-
self.language_index = dict()
|
|
100
103
|
for l in self.languages:
|
|
101
104
|
for i in l.identifiers:
|
|
102
105
|
self.language_index[i] = l
|
|
103
106
|
|
|
104
107
|
|
|
105
|
-
default = Config(Version.from_str("2.0"))
|
|
108
|
+
default = Config("2.0") # Version.from_str("2.0"))
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
def read_config_from_toml(
|
|
109
|
-
path: Path, section:
|
|
110
|
-
) ->
|
|
112
|
+
path: Path, section: str | None = None
|
|
113
|
+
) -> Config | None:
|
|
111
114
|
"""Read a config from given `path` in given `section`. The path should refer to
|
|
112
115
|
a TOML file that should decode to a `Config` object. If `section` is given, only
|
|
113
116
|
that section is decoded to a `Config` object. The `section` string may contain
|
|
@@ -127,7 +130,8 @@ def read_config_from_toml(
|
|
|
127
130
|
if section is not None:
|
|
128
131
|
for s in section.split("."):
|
|
129
132
|
json = json[s]
|
|
130
|
-
return
|
|
133
|
+
return msgspec.convert(json, type=Config, dec_hook=from_str.dec_hook)
|
|
134
|
+
|
|
131
135
|
except ValueError as e:
|
|
132
136
|
log.error("Could not read config: %s", e)
|
|
133
137
|
return None
|
|
@@ -147,30 +151,35 @@ def read_config():
|
|
|
147
151
|
return default
|
|
148
152
|
|
|
149
153
|
|
|
150
|
-
class ConfigWrapper:
|
|
151
|
-
def __init__(self, config = None):
|
|
152
|
-
self.config = config
|
|
154
|
+
class ConfigWrapper(threading.local):
|
|
155
|
+
def __init__(self, config: Config | None = None):
|
|
156
|
+
self.config: Config | None = config
|
|
153
157
|
|
|
154
|
-
def read(self, force=False):
|
|
158
|
+
def read(self, force: bool = False):
|
|
155
159
|
if self.config is None or force:
|
|
156
160
|
self.config = read_config()
|
|
157
161
|
|
|
158
|
-
|
|
162
|
+
@property
|
|
163
|
+
def get(self) -> Config:
|
|
159
164
|
if self.config is None:
|
|
160
|
-
raise
|
|
161
|
-
return
|
|
165
|
+
raise ValueError(f"No config loaded.")
|
|
166
|
+
return self.config
|
|
162
167
|
|
|
163
168
|
@contextmanager
|
|
164
169
|
def __call__(self, **kwargs):
|
|
165
|
-
backup =
|
|
170
|
+
backup = self.config
|
|
171
|
+
new_config = deepcopy(self.config)
|
|
166
172
|
for k, v in kwargs.items():
|
|
167
|
-
setattr(
|
|
173
|
+
setattr(new_config, k, v)
|
|
174
|
+
self.config = new_config
|
|
175
|
+
|
|
168
176
|
yield
|
|
169
|
-
for k in kwargs.keys():
|
|
170
|
-
setattr(self.config, k, backup[k])
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
self.config = backup
|
|
179
|
+
|
|
180
|
+
def get_language(self, lang_name: str) -> Language | None:
|
|
181
|
+
if self.config is None:
|
|
182
|
+
raise ValueError(f"No config loaded.")
|
|
174
183
|
return self.config.language_index.get(lang_name, None)
|
|
175
184
|
|
|
176
185
|
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
from
|
|
2
|
-
from dataclasses import dataclass
|
|
1
|
+
from msgspec import Struct
|
|
3
2
|
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
class Comment:
|
|
4
|
+
class Comment(Struct):
|
|
7
5
|
"""Comment method for a language. For example: `Comment("/*", "*/")` works
|
|
8
6
|
for C/C++ etc, `Comment("#")` works for Python, and so on.
|
|
9
7
|
"""
|
|
10
8
|
|
|
11
9
|
open: str
|
|
12
|
-
close:
|
|
10
|
+
close: str | None = None
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
class Language:
|
|
13
|
+
class Language(Struct):
|
|
17
14
|
"""Language information. Given a language we may have any number of short-hands
|
|
18
15
|
to indicate a code block is written in that language. If a language supports
|
|
19
16
|
line directives this can be used to redirect compiler messages directly to the
|
|
@@ -22,7 +19,7 @@ class Language:
|
|
|
22
19
|
name: str
|
|
23
20
|
identifiers: list[str]
|
|
24
21
|
comment: Comment
|
|
25
|
-
line_directive:
|
|
22
|
+
line_directive: str | None = None
|
|
26
23
|
|
|
27
24
|
|
|
28
25
|
languages = [
|
|
@@ -43,6 +40,7 @@ languages = [
|
|
|
43
40
|
Language("Make", ["make", "makefile"], Comment("#")),
|
|
44
41
|
Language("Gnuplot", ["gnuplot"], Comment("#")),
|
|
45
42
|
Language("TOML", ["toml"], Comment("#")),
|
|
43
|
+
Language("YAML", ["yaml"], Comment("#")),
|
|
46
44
|
Language("F#", ["fsharp"], Comment("//")),
|
|
47
45
|
Language("Javascript", ["javascript", "js", "ecma"], Comment("//")),
|
|
48
46
|
Language("Go", ["go", "golang"], Comment("//")),
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from msgspec import Struct
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
class Template:
|
|
4
|
+
class Template(Struct):
|
|
6
5
|
"""Officially supported templates. Templates are pulled from external GitHub
|
|
7
6
|
repositories: `entangled new <template name>`. These templates are the ones
|
|
8
7
|
we officially support. Other templates can be used with valid copier URL
|
|
@@ -32,5 +31,12 @@ templates: list[Template] = [
|
|
|
32
31
|
"gh:entangled/template-pandoc",
|
|
33
32
|
"pandoc",
|
|
34
33
|
"Supports HTML and PDF output, numbered equations and citations."
|
|
34
|
+
),
|
|
35
|
+
Template(
|
|
36
|
+
"one-pager",
|
|
37
|
+
"One-page README",
|
|
38
|
+
"gh:entangled/template-one-pager",
|
|
39
|
+
"none",
|
|
40
|
+
"Everything in a single README, to be rendered on Github/Gitlab/etc."
|
|
35
41
|
)
|
|
36
42
|
]
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
+
from ..from_str import FromStr
|
|
4
|
+
from typing import override
|
|
3
5
|
|
|
4
|
-
from ..construct import FromStr
|
|
5
6
|
|
|
6
7
|
@dataclass
|
|
7
8
|
class Version(FromStr):
|
|
8
9
|
numbers: tuple[int, ...]
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
@override
|
|
12
|
+
def to_str(self) -> str:
|
|
11
13
|
return ".".join(str(i) for i in self.numbers)
|
|
12
14
|
|
|
15
|
+
@override
|
|
13
16
|
@classmethod
|
|
14
17
|
def from_str(cls, s: str) -> Version:
|
|
15
18
|
return Version(tuple(int(sv) for sv in s.split(".")))
|
|
16
|
-
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from textwrap import indent
|
|
2
|
+
from typing import Optional, Union, Iterable, Any, override
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from collections import defaultdict
|
|
4
5
|
from functools import singledispatchmethod
|
|
@@ -11,7 +12,7 @@ from .errors.internal import InternalError
|
|
|
11
12
|
from .text_location import TextLocation
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def length(iter: Iterable[
|
|
15
|
+
def length[T](iter: Iterable[T]) -> int:
|
|
15
16
|
return sum(1 for _ in iter)
|
|
16
17
|
|
|
17
18
|
|
|
@@ -21,7 +22,8 @@ class ReferenceId:
|
|
|
21
22
|
file: PurePath
|
|
22
23
|
ref_count: int
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
@override
|
|
26
|
+
def __hash__(self) -> int:
|
|
25
27
|
return hash((self.name, self.file, self.ref_count))
|
|
26
28
|
|
|
27
29
|
|
|
@@ -34,15 +36,25 @@ class PlainText:
|
|
|
34
36
|
class CodeBlock:
|
|
35
37
|
properties: list[Property]
|
|
36
38
|
indent: str
|
|
39
|
+
open_line: str
|
|
40
|
+
close_line: str
|
|
37
41
|
source: str
|
|
38
42
|
origin: TextLocation
|
|
39
|
-
language:
|
|
40
|
-
header:
|
|
41
|
-
mode:
|
|
43
|
+
language: Language | None = None
|
|
44
|
+
header: str | None = None
|
|
45
|
+
mode: int | None = None
|
|
42
46
|
|
|
47
|
+
@property
|
|
48
|
+
def text(self) -> str:
|
|
49
|
+
return self.open_line + "\n" + self.source + "\n" + self.close_line
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
@property
|
|
52
|
+
def indented_text(self) -> str:
|
|
53
|
+
return indent(self.text, self.indent)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Content = PlainText | ReferenceId
|
|
57
|
+
RawContent = PlainText | CodeBlock
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
@dataclass
|
|
@@ -85,6 +97,9 @@ class ReferenceMap:
|
|
|
85
97
|
def __contains__(self, key: str) -> bool:
|
|
86
98
|
return key in self.index
|
|
87
99
|
|
|
100
|
+
def get_codeblock(self, key: ReferenceId) -> CodeBlock:
|
|
101
|
+
return self.map[key]
|
|
102
|
+
|
|
88
103
|
@singledispatchmethod
|
|
89
104
|
def __getitem__(self, key):
|
|
90
105
|
raise NotImplementedError(f"Invalid key: {type(key)}")
|
|
@@ -96,3 +111,50 @@ class ReferenceMap:
|
|
|
96
111
|
@__getitem__.register
|
|
97
112
|
def _(self, key: str) -> Iterable[CodeBlock]:
|
|
98
113
|
return self.by_name(key)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def content_to_text(r: ReferenceMap, c: Content) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Reconstruct original plain text from a piece of content.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
r: the reference map.
|
|
122
|
+
c: the content.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A string, usually not terminated by a newline.
|
|
126
|
+
"""
|
|
127
|
+
match c:
|
|
128
|
+
case PlainText(s): return s
|
|
129
|
+
case ReferenceId(): return r.get_codeblock(c).indented_text
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def document_to_text(r: ReferenceMap, cs: list[Content]) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Reconstruct original plain text content from a reference map and
|
|
135
|
+
list of content.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
r: the reference map.
|
|
139
|
+
cs: a list of content.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A string, including a final newline.
|
|
143
|
+
|
|
144
|
+
Usually this is the reconstructed content of a Markdown file. Most
|
|
145
|
+
editors have a convention to end a file with a newline, but this
|
|
146
|
+
newline is usually stripped when we read a file.
|
|
147
|
+
|
|
148
|
+
Context:
|
|
149
|
+
In Python `"foo".splitlines()` gives the same as `"foo\n".splitlines()`,
|
|
150
|
+
with the exception of `"\n".splitlines()` giving `['']`, while
|
|
151
|
+
`"".splitlines()` returns `[]`.
|
|
152
|
+
|
|
153
|
+
As an alternative, we could keep line endings by splitting with `keepends=True`,
|
|
154
|
+
and joining with `"".join(...)`.
|
|
155
|
+
"""
|
|
156
|
+
text = "\n".join(content_to_text(r, c) for c in cs)
|
|
157
|
+
if text[-1] != "\n":
|
|
158
|
+
return text + "\n"
|
|
159
|
+
else:
|
|
160
|
+
return text
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Any, Callable
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from ..text_location import TextLocation
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class UserError(Exception):
|
|
8
|
-
def __str__(self):
|
|
9
|
+
def __str__(self) -> str:
|
|
9
10
|
return "Unknown user error."
|
|
10
11
|
|
|
11
12
|
|
|
@@ -25,12 +26,20 @@ class HelpfulUserError(UserError):
|
|
|
25
26
|
Make sure to also execute e.func() in your error handling."""
|
|
26
27
|
|
|
27
28
|
msg: str
|
|
28
|
-
func: Callable = lambda: None
|
|
29
|
+
func: Callable[[], Any] = lambda: None
|
|
29
30
|
|
|
30
31
|
def __str__(self):
|
|
31
32
|
return f"error: {self.msg}"
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
@dataclass
|
|
36
|
+
class FileError(UserError):
|
|
37
|
+
filename: Path
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
return f"file not found `{self.filename}`"
|
|
41
|
+
|
|
42
|
+
|
|
34
43
|
@dataclass
|
|
35
44
|
class IndentationError(UserError):
|
|
36
45
|
location: TextLocation
|