entangled-cli 2.5.0__tar.gz → 2.6.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.5.0/README.md → entangled_cli-2.6.0/PKG-INFO +43 -0
- entangled_cli-2.5.0/PKG-INFO → entangled_cli-2.6.0/README.md +16 -25
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/__init__.py +2 -1
- entangled_cli-2.6.0/entangled/commands/weave.py +50 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/errors/user.py +12 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/repl.py +13 -3
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/interface/document.py +25 -5
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/io/stat.py +7 -4
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/logging.py +1 -1
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/tangle.py +3 -1
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/readers/markdown.py +5 -1
- entangled_cli-2.6.0/entangled/weave/__init__.py +9 -0
- entangled_cli-2.6.0/entangled/weave/base.py +66 -0
- entangled_cli-2.6.0/entangled/weave/filters.py +235 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/pyproject.toml +8 -2
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/.gitignore +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/LICENSE +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/brei.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/main.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/new.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/reset.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/serve.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/status.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/stitch.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/sync.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/tangle.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/commands/watch.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/annotation_method.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/config_data.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/config_update.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/document_style.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/language.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/markers.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/namespace_default.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/templates.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/config/version.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/errors/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/errors/internal.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/base.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/build.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/quarto_attributes.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/shebang.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/spdx_license.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/hooks/task.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/interface/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/interface/context.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/io/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/io/filedb.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/io/transaction.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/io/virtual.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/iterators/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/iterators/lines.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/iterators/peekable.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/iterators/run_generator.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/main.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/code_block.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/content.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/properties.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/reference_id.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/reference_map.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/model/reference_name.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/parsing.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/py.typed +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/readers/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/readers/code.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/readers/delimiters.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/readers/types.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/readers/yaml_header.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/server/__init__.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/server/server.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/status.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/text_location.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/utility.py +0 -0
- {entangled_cli-2.5.0 → entangled_cli-2.6.0}/entangled/version.py +0 -0
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: entangled-cli
|
|
3
|
+
Version: 2.6.0
|
|
4
|
+
Summary: Literate Programming toolbox
|
|
5
|
+
Project-URL: Homepage, https://entangled.github.io/
|
|
6
|
+
Project-URL: Repository, https://github.com/entangled/entangled.py
|
|
7
|
+
Author-email: Johan Hidding <j.hidding@esciencecenter.nl>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: <4,>=3.13
|
|
11
|
+
Requires-Dist: brei<0.3,>=0.2.3
|
|
12
|
+
Requires-Dist: click>=8.3.0
|
|
13
|
+
Requires-Dist: copier<10,>=9
|
|
14
|
+
Requires-Dist: filelock<5,>=3.12.0
|
|
15
|
+
Requires-Dist: msgspec>=0.20.0
|
|
16
|
+
Requires-Dist: pyyaml<7,>=6.0.1
|
|
17
|
+
Requires-Dist: repl-session>=0.3.0
|
|
18
|
+
Requires-Dist: rich-argparse<2,>=1.4.0
|
|
19
|
+
Requires-Dist: rich-click>=1.9.4
|
|
20
|
+
Requires-Dist: rich<16,>=13
|
|
21
|
+
Requires-Dist: tomlkit<0.16,>=0.12.1
|
|
22
|
+
Requires-Dist: typeguard>=4.4.4
|
|
23
|
+
Requires-Dist: watchfiles>=1.3
|
|
24
|
+
Provides-Extra: jupyter
|
|
25
|
+
Requires-Dist: repl-session[jupyter]>=0.3.0; extra == 'jupyter'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
1
28
|
# Entangled
|
|
2
29
|
|
|
3
30
|
[](https://github.com/entangled/entangled.py/actions/workflows/deploy.yml)
|
|
@@ -440,6 +467,22 @@ You can use `${variable}` syntax inside Brei tasks just as you would in a stand-
|
|
|
440
467
|
|
|
441
468
|
Entangled has been used successfully with the following document generators. Note that some of these examples were built using older versions of Entangled, but they should work just the same.
|
|
442
469
|
|
|
470
|
+
### Weaving
|
|
471
|
+
|
|
472
|
+
The `entangled weave` command transforms a Markdown document into Markdown that renders well in a documentation tool, by applying one or more filters to the code blocks:
|
|
473
|
+
|
|
474
|
+
```shell
|
|
475
|
+
entangled weave --list-filters # show available filters
|
|
476
|
+
entangled weave docs/index.md # apply the `label` filter, print to stdout
|
|
477
|
+
entangled weave -f title docs/index.md -o out/index.md # MkDocs/pymdownx style titles
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- `label` wraps each named code block in a `<div class="entangled-code-block">` with a label showing its name or target file. This renders in most Markdown previews (GitHub, Pandoc, Python-Markdown with `md_in_html`).
|
|
481
|
+
- `title` adds a `title` attribute to the code block header, as understood by `pymdownx.superfences`.
|
|
482
|
+
- `repl` inserts the results of a REPL session (see the `repl` hook) after each code block with the `repl` class. The results are read from the session file with its extension replaced by `.out.json`, e.g. `repl-session < session.json > session.out.json`. Text output becomes a `{.text .output}` code block; with the Jupyter backend, rich output (Markdown, HTML, SVG, PNG/JPEG) is included as well, with images embedded as data URIs. Set a `mime-type` attribute on the code block to choose a representation.
|
|
483
|
+
|
|
484
|
+
Filters can be combined by repeating `-f`; they are applied in the given order. The same transformation is available from Python through `entangled.weave.transform_markdown`, so that documentation tools only need a thin adapter.
|
|
485
|
+
|
|
443
486
|
### Pandoc
|
|
444
487
|
|
|
445
488
|
[Pandoc](https://pandoc.org/) is a very versatile tool for converting documents in any format. It specifically has very wide support for different forms of Markdown syntax out in the wild, including a filter system that lets you extend the workings of Pandoc. Those filters can be written in any language through an API, for instance Python filters can be written using `panflute`, but there is also native support for Lua.
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: entangled-cli
|
|
3
|
-
Version: 2.5.0
|
|
4
|
-
Summary: Literate Programming toolbox
|
|
5
|
-
Project-URL: Homepage, https://entangled.github.io/
|
|
6
|
-
Project-URL: Repository, https://github.com/entangled/entangled.py
|
|
7
|
-
Author-email: Johan Hidding <j.hidding@esciencecenter.nl>
|
|
8
|
-
License-Expression: Apache-2.0
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Python: <4,>=3.13
|
|
11
|
-
Requires-Dist: brei<0.3,>=0.2.3
|
|
12
|
-
Requires-Dist: click>=8.3.0
|
|
13
|
-
Requires-Dist: copier<10,>=9
|
|
14
|
-
Requires-Dist: filelock<5,>=3.12.0
|
|
15
|
-
Requires-Dist: msgspec>=0.20.0
|
|
16
|
-
Requires-Dist: pyyaml<7,>=6.0.1
|
|
17
|
-
Requires-Dist: repl-session>=0.2.0
|
|
18
|
-
Requires-Dist: rich-argparse<2,>=1.4.0
|
|
19
|
-
Requires-Dist: rich-click>=1.9.4
|
|
20
|
-
Requires-Dist: rich<16,>=13
|
|
21
|
-
Requires-Dist: tomlkit<0.16,>=0.12.1
|
|
22
|
-
Requires-Dist: typeguard>=4.4.4
|
|
23
|
-
Requires-Dist: watchfiles>=1.3
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
1
|
# Entangled
|
|
27
2
|
|
|
28
3
|
[](https://github.com/entangled/entangled.py/actions/workflows/deploy.yml)
|
|
@@ -465,6 +440,22 @@ You can use `${variable}` syntax inside Brei tasks just as you would in a stand-
|
|
|
465
440
|
|
|
466
441
|
Entangled has been used successfully with the following document generators. Note that some of these examples were built using older versions of Entangled, but they should work just the same.
|
|
467
442
|
|
|
443
|
+
### Weaving
|
|
444
|
+
|
|
445
|
+
The `entangled weave` command transforms a Markdown document into Markdown that renders well in a documentation tool, by applying one or more filters to the code blocks:
|
|
446
|
+
|
|
447
|
+
```shell
|
|
448
|
+
entangled weave --list-filters # show available filters
|
|
449
|
+
entangled weave docs/index.md # apply the `label` filter, print to stdout
|
|
450
|
+
entangled weave -f title docs/index.md -o out/index.md # MkDocs/pymdownx style titles
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
- `label` wraps each named code block in a `<div class="entangled-code-block">` with a label showing its name or target file. This renders in most Markdown previews (GitHub, Pandoc, Python-Markdown with `md_in_html`).
|
|
454
|
+
- `title` adds a `title` attribute to the code block header, as understood by `pymdownx.superfences`.
|
|
455
|
+
- `repl` inserts the results of a REPL session (see the `repl` hook) after each code block with the `repl` class. The results are read from the session file with its extension replaced by `.out.json`, e.g. `repl-session < session.json > session.out.json`. Text output becomes a `{.text .output}` code block; with the Jupyter backend, rich output (Markdown, HTML, SVG, PNG/JPEG) is included as well, with images embedded as data URIs. Set a `mime-type` attribute on the code block to choose a representation.
|
|
456
|
+
|
|
457
|
+
Filters can be combined by repeating `-f`; they are applied in the given order. The same transformation is available from Python through `entangled.weave.transform_markdown`, so that documentation tools only need a thin adapter.
|
|
458
|
+
|
|
468
459
|
### Pandoc
|
|
469
460
|
|
|
470
461
|
[Pandoc](https://pandoc.org/) is a very versatile tool for converting documents in any format. It specifically has very wide support for different forms of Markdown syntax out in the wild, including a filter system that lets you extend the workings of Pandoc. Those filters can be written in any language through an API, for instance Python filters can be written using `panflute`, but there is also native support for Lua.
|
|
@@ -8,7 +8,8 @@ from .watch import watch
|
|
|
8
8
|
from .status import status
|
|
9
9
|
from .brei import brei
|
|
10
10
|
from .serve import serve
|
|
11
|
+
from .weave import weave
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
|
-
"main", "stitch", "tangle", "new", "reset", "sync", "watch", "status", "brei", "serve"
|
|
14
|
+
"main", "stitch", "tangle", "new", "reset", "sync", "watch", "status", "brei", "serve", "weave"
|
|
14
15
|
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pathlib import Path, PurePath
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import rich_click as click
|
|
5
|
+
|
|
6
|
+
from .main import main
|
|
7
|
+
|
|
8
|
+
from ..config import read_config
|
|
9
|
+
from ..interface import Context
|
|
10
|
+
from ..weave import filters, filter_description, transform_markdown
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_filters():
|
|
14
|
+
width = max(map(len, filters))
|
|
15
|
+
for name, f in filters.items():
|
|
16
|
+
print(f"{name:<{width}} {filter_description(f)}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@main.command()
|
|
20
|
+
@click.argument("input", type=click.Path(dir_okay=False, allow_dash=True, path_type=Path),
|
|
21
|
+
default="-")
|
|
22
|
+
@click.option("-o", "--output", type=click.Path(dir_okay=False, allow_dash=True, path_type=Path),
|
|
23
|
+
default="-", help="output file, defaults to standard output")
|
|
24
|
+
@click.option("-f", "--filter", "filter_names", type=click.Choice(list(filters)), multiple=True,
|
|
25
|
+
help="filter to apply, can be repeated; defaults to `label`")
|
|
26
|
+
@click.option("--list-filters", is_flag=True, help="list available filters and exit")
|
|
27
|
+
def weave(*, input: Path, output: Path, filter_names: tuple[str, ...] = (),
|
|
28
|
+
list_filters: bool = False):
|
|
29
|
+
"""Transform Markdown for rendering by documentation tools."""
|
|
30
|
+
if list_filters:
|
|
31
|
+
print_filters()
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
context = Context()
|
|
35
|
+
context |= read_config(context.fs)
|
|
36
|
+
|
|
37
|
+
if input == Path("-"):
|
|
38
|
+
text = sys.stdin.read()
|
|
39
|
+
filename = PurePath("-")
|
|
40
|
+
else:
|
|
41
|
+
text = input.read_text()
|
|
42
|
+
filename = input
|
|
43
|
+
|
|
44
|
+
selected = [filters[n] for n in (filter_names or ("label",))]
|
|
45
|
+
result = "".join(transform_markdown(context, text, *selected, filename=filename))
|
|
46
|
+
|
|
47
|
+
if output == Path("-"):
|
|
48
|
+
_ = sys.stdout.write(result)
|
|
49
|
+
else:
|
|
50
|
+
_ = output.write_text(result)
|
|
@@ -73,3 +73,15 @@ class CodeAttributeError(UserError):
|
|
|
73
73
|
@override
|
|
74
74
|
def __str__(self) -> str:
|
|
75
75
|
return f"{self.origin}: Attribute error: {self.msg}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class StitchConflict(UserError):
|
|
80
|
+
reference: str
|
|
81
|
+
files: tuple[Path, Path]
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
a, b = self.files
|
|
86
|
+
return (f"code block `{self.reference}` was changed differently in "
|
|
87
|
+
f"`{a}` and `{b}`; resolve this by hand")
|
|
@@ -6,7 +6,7 @@ import msgspec
|
|
|
6
6
|
|
|
7
7
|
from entangled.config.language import Language
|
|
8
8
|
from .base import HookBase
|
|
9
|
-
from repl_session import ReplConfig, ReplSession, ReplCommand
|
|
9
|
+
from repl_session import JupyterConfig, ReplConfig, ReplSession, ReplCommand
|
|
10
10
|
from msgspec import Struct
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
@@ -18,6 +18,15 @@ from ..model.properties import Class, get_attribute, get_attribute_string, get_i
|
|
|
18
18
|
|
|
19
19
|
log = logger()
|
|
20
20
|
|
|
21
|
+
type BackendConfig = ReplConfig | JupyterConfig
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def backend_config(cfg: dict[str, object]) -> BackendConfig:
|
|
25
|
+
"""Convert a REPL config, defaulting to the `repl` (pexpect) backend when
|
|
26
|
+
no `backend` is given."""
|
|
27
|
+
return msgspec.convert({"backend": "repl"} | cfg, type=BackendConfig)
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
class Session(Struct):
|
|
22
31
|
filename: Path
|
|
23
32
|
language: str
|
|
@@ -35,7 +44,7 @@ def strip_comments(code: str, language: Language) -> str:
|
|
|
35
44
|
@final
|
|
36
45
|
class Hook(HookBase):
|
|
37
46
|
class Config(HookBase.Config):
|
|
38
|
-
config: dict[str,
|
|
47
|
+
config: dict[str, dict[str, object]] = msgspec.field(default_factory=dict)
|
|
39
48
|
|
|
40
49
|
@dataclass
|
|
41
50
|
class State(HookBase.State):
|
|
@@ -44,7 +53,8 @@ class Hook(HookBase):
|
|
|
44
53
|
def __init__(self, config: Config, state: State):
|
|
45
54
|
super().__init__(config, state)
|
|
46
55
|
log.debug(f"REPL hook config: {config}")
|
|
47
|
-
self.config =
|
|
56
|
+
self.config: dict[str, BackendConfig] = {
|
|
57
|
+
lang: backend_config(cfg) for lang, cfg in config.config.items()}
|
|
48
58
|
self.sessions: dict[str, Session] = state.sessions
|
|
49
59
|
|
|
50
60
|
@override
|
|
@@ -2,11 +2,12 @@ from dataclasses import dataclass, field
|
|
|
2
2
|
from pathlib import PurePath, Path
|
|
3
3
|
|
|
4
4
|
from ..config import Config, ConfigUpdate, get_input_files, read_config, AnnotationMethod
|
|
5
|
-
from ..model import ReferenceMap, tangle_ref, Content, content_to_text
|
|
5
|
+
from ..model import ReferenceMap, ReferenceId, tangle_ref, Content, content_to_text
|
|
6
6
|
from ..io import AbstractFileCache, FileCache, Transaction
|
|
7
7
|
from ..readers import code
|
|
8
8
|
from ..iterators import numbered_lines, run_generator
|
|
9
9
|
from ..logging import logger
|
|
10
|
+
from ..errors.user import StitchConflict
|
|
10
11
|
|
|
11
12
|
from .context import Context, markdown
|
|
12
13
|
|
|
@@ -65,18 +66,37 @@ class Document:
|
|
|
65
66
|
t.update(path)
|
|
66
67
|
return update
|
|
67
68
|
|
|
68
|
-
def load_code(self, t: Transaction, path: Path
|
|
69
|
+
def load_code(self, t: Transaction, path: Path,
|
|
70
|
+
loaded: dict[ReferenceId, Path] | None = None):
|
|
71
|
+
"""Read code blocks back from a tangled file. If `loaded` is given, it
|
|
72
|
+
records which file each block was read from, so that a block that
|
|
73
|
+
was read before with different content raises a `StitchConflict`."""
|
|
69
74
|
reader = code(numbered_lines(path, t.read(path)))
|
|
70
75
|
for block in reader:
|
|
71
|
-
|
|
76
|
+
ref = block.reference_id
|
|
77
|
+
if loaded is not None:
|
|
78
|
+
if ref in loaded and self.reference_map[ref].source != block.content:
|
|
79
|
+
raise StitchConflict(str(ref), (loaded[ref], path))
|
|
80
|
+
loaded[ref] = path
|
|
81
|
+
self.reference_map[ref].source = block.content
|
|
72
82
|
t.update(path)
|
|
73
83
|
|
|
74
84
|
def load_all_code(self, t: Transaction):
|
|
85
|
+
"""Read code blocks back from all targets that changed since they were
|
|
86
|
+
tangled. Unchanged targets are skipped: they match the documentation,
|
|
87
|
+
and when they share a block with a changed target, reading them would
|
|
88
|
+
undo the change."""
|
|
75
89
|
log.debug(f"Targets: {self.reference_map.targets()}")
|
|
90
|
+
loaded: dict[ReferenceId, Path] = {}
|
|
76
91
|
for tgt in self.reference_map.targets():
|
|
92
|
+
path = Path(tgt)
|
|
93
|
+
if path not in t.fs:
|
|
94
|
+
continue
|
|
95
|
+
if path in t.db and t.fs[path].stat == t.db[path]:
|
|
96
|
+
log.debug(f"Skipping unchanged code: `{tgt}`")
|
|
97
|
+
continue
|
|
77
98
|
log.debug(f"Reading code: `{tgt}`")
|
|
78
|
-
|
|
79
|
-
self.load_code(t, Path(tgt))
|
|
99
|
+
self.load_code(t, path, loaded)
|
|
80
100
|
|
|
81
101
|
def load(self, t: Transaction):
|
|
82
102
|
files = get_input_files(t.fs, self.config)
|
|
@@ -12,10 +12,13 @@ import time
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def hexdigest(s: str) -> str:
|
|
15
|
-
"""Creates a
|
|
16
|
-
|
|
17
|
-
is encoded as UTF-8.
|
|
18
|
-
|
|
15
|
+
"""Creates a SHA-256 hash digest from a string. Before hashing, the string
|
|
16
|
+
has carriage return `\\r` characters, trailing white space on each line
|
|
17
|
+
and trailing newlines removed, and the string is encoded as UTF-8. This
|
|
18
|
+
way, changes in white space only, like the ones made by editors that
|
|
19
|
+
strip trailing white space on save, don't count as changes."""
|
|
20
|
+
lines = s.replace("\r", "").split("\n")
|
|
21
|
+
content = "\n".join(line.rstrip() for line in lines).rstrip().encode()
|
|
19
22
|
return hashlib.sha256(content).hexdigest()
|
|
20
23
|
|
|
21
24
|
|
|
@@ -20,7 +20,7 @@ class WhitespaceStrippingConsole(Console):
|
|
|
20
20
|
return "\n".join(line.rstrip() for line in rendered.splitlines()) + newline_char
|
|
21
21
|
|
|
22
22
|
# Global rich console object
|
|
23
|
-
console: Console = WhitespaceStrippingConsole()
|
|
23
|
+
console: Console = WhitespaceStrippingConsole(stderr=True)
|
|
24
24
|
|
|
25
25
|
def logger():
|
|
26
26
|
return logging.getLogger("entangled")
|
|
@@ -75,7 +75,9 @@ type Tangler = Callable[[Tangler, Deps, ReferenceId, bool, bool], Iterator[str]]
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def indent(prefix: str, g: Iterator[str]) -> Iterator[str]:
|
|
78
|
-
|
|
78
|
+
"""Indent lines with `prefix`. Blank lines are not indented, so that they
|
|
79
|
+
don't get trailing white space."""
|
|
80
|
+
return map(lambda line: prefix + line if line.strip() else line.lstrip(" \t"), g)
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
def naked_tangler(refs: ReferenceMap) -> Tangler:
|
|
@@ -48,7 +48,11 @@ def code_block_guard(origin: TextLocation, open_match: re.Match[str], close_matc
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def dedent_line(location: TextLocation, indent: str, line: str):
|
|
51
|
-
|
|
51
|
+
# Blank lines lose all their white space, as they do when reading code
|
|
52
|
+
# back from a tangled file.
|
|
53
|
+
if line.strip() == "":
|
|
54
|
+
return line.lstrip(" \t")
|
|
55
|
+
if line.startswith(indent):
|
|
52
56
|
return line.removeprefix(indent)
|
|
53
57
|
raise IndentationError(location)
|
|
54
58
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .base import (
|
|
2
|
+
ContentFilter, CodeBlockFilter, codeblock_filter, compose_filters, iter_bind, transform_markdown
|
|
3
|
+
)
|
|
4
|
+
from .filters import filters, filter_description, block_title
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ContentFilter", "CodeBlockFilter", "codeblock_filter", "compose_filters", "iter_bind",
|
|
8
|
+
"transform_markdown", "filters", "filter_description", "block_title"
|
|
9
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic machinery for weaving: transforming the Markdown source of a page,
|
|
3
|
+
code block by code block, into Markdown that renders well in some target.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
7
|
+
from functools import partial, reduce, wraps
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from pathlib import PurePath
|
|
10
|
+
|
|
11
|
+
from ..interface import Context, markdown
|
|
12
|
+
from ..iterators import numbered_lines
|
|
13
|
+
from ..model import Content, PlainText, ReferenceId, ReferenceMap, content_to_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
type ContentFilter = Callable[[ReferenceMap, Content], Iterable[Content]]
|
|
17
|
+
type CodeBlockFilter = Callable[[ReferenceMap, ReferenceId], Iterable[Content]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def codeblock_filter(f: CodeBlockFilter) -> ContentFilter:
|
|
21
|
+
"""Lift a filter on code blocks to a filter on content, passing plain text
|
|
22
|
+
through unchanged."""
|
|
23
|
+
@wraps(f)
|
|
24
|
+
def _filter(rm: ReferenceMap, c: Content) -> Iterable[Content]:
|
|
25
|
+
match c:
|
|
26
|
+
case PlainText():
|
|
27
|
+
return [c]
|
|
28
|
+
case ReferenceId():
|
|
29
|
+
return f(rm, c)
|
|
30
|
+
return _filter
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def iter_bind[T, U](lst: Iterable[T], f: Callable[[T], Iterable[U]]) -> Iterator[U]:
|
|
34
|
+
return chain.from_iterable(map(f, lst))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compose_filter(a: ContentFilter, b: ContentFilter) -> ContentFilter:
|
|
38
|
+
def _joined(rm: ReferenceMap, c: Content) -> Iterable[Content]:
|
|
39
|
+
return iter_bind(a(rm, c), partial(b, rm))
|
|
40
|
+
return _joined
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compose_filters(*args: ContentFilter) -> ContentFilter:
|
|
44
|
+
"""Compose filters left to right: the first filter is applied first."""
|
|
45
|
+
identity: ContentFilter = lambda _, c: [c]
|
|
46
|
+
return reduce(compose_filter, args, identity)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def transform_markdown(
|
|
50
|
+
context: Context,
|
|
51
|
+
text: str,
|
|
52
|
+
*filters: ContentFilter,
|
|
53
|
+
filename: PurePath = PurePath("-"),
|
|
54
|
+
reference_map: ReferenceMap | None = None,
|
|
55
|
+
) -> Iterator[str]:
|
|
56
|
+
"""
|
|
57
|
+
Read a Markdown document, pass its content through the given filters,
|
|
58
|
+
and yield the resulting Markdown text.
|
|
59
|
+
|
|
60
|
+
Filters may modify code blocks in the reference map in place. If no
|
|
61
|
+
`reference_map` is given, a fresh one is used for this document only.
|
|
62
|
+
"""
|
|
63
|
+
refs = ReferenceMap() if reference_map is None else reference_map
|
|
64
|
+
content = markdown(context, refs, numbered_lines(filename, text))
|
|
65
|
+
for c in iter_bind(content, partial(compose_filters(*filters), refs)):
|
|
66
|
+
yield content_to_text(refs, c)[0]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filters for `entangled weave`. Each filter is registered by name in `filters`;
|
|
3
|
+
the first line of its docstring serves as description for `--list-filters`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from base64 import b64encode
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from html import escape
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from textwrap import indent
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
import msgspec
|
|
15
|
+
|
|
16
|
+
from repl_session import ReplCommand, ReplOutput, ReplSession, read_session
|
|
17
|
+
|
|
18
|
+
from ..hooks.repl import strip_comments
|
|
19
|
+
from ..logging import logger
|
|
20
|
+
from ..model import CodeBlock, Content, PlainText, ReferenceId, ReferenceMap
|
|
21
|
+
from ..model.properties import Attribute, Class, Id, get_attribute_string
|
|
22
|
+
from .base import ContentFilter, codeblock_filter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
log = logger()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def block_title(code_block: CodeBlock) -> str | None:
|
|
29
|
+
"""Compute a human readable title for a code block from its id and `file`
|
|
30
|
+
attribute. Returns `None` for anonymous code blocks."""
|
|
31
|
+
ids = [p.value for p in code_block.properties if isinstance(p, Id)]
|
|
32
|
+
filenames = [p.value for p in code_block.properties # pyright: ignore[reportAny]
|
|
33
|
+
if isinstance(p, Attribute) and p.key == "file"]
|
|
34
|
+
|
|
35
|
+
if len(ids) > 1 or len(filenames) > 1:
|
|
36
|
+
return "error: ambiguous code block title"
|
|
37
|
+
if ids and filenames:
|
|
38
|
+
return f"#{ids[0]} / file: {filenames[0]}"
|
|
39
|
+
if ids:
|
|
40
|
+
return f"#{ids[0]}"
|
|
41
|
+
if filenames:
|
|
42
|
+
return f"file: {filenames[0]}"
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def first_line(text: str) -> tuple[str, str]:
|
|
47
|
+
head, nl, rest = text.partition("\n")
|
|
48
|
+
return head, nl + rest
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@codeblock_filter
|
|
52
|
+
def title(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
|
|
53
|
+
"""Add a `title` attribute to the code block header (MkDocs/pymdownx style)."""
|
|
54
|
+
code_block = reference_map[r]
|
|
55
|
+
text = block_title(code_block)
|
|
56
|
+
ids = [p.value for p in code_block.properties if isinstance(p, Id)]
|
|
57
|
+
classes = [p.value for p in code_block.properties if isinstance(p, Class)]
|
|
58
|
+
|
|
59
|
+
head, rest = first_line(code_block.open_line)
|
|
60
|
+
fence_match = re.match(r"\s*(`{3,}|~{3,})", head)
|
|
61
|
+
fence = fence_match.group(1) if fence_match else "```"
|
|
62
|
+
|
|
63
|
+
attrs: list[str] = []
|
|
64
|
+
if ids:
|
|
65
|
+
attrs.append(str(Id(ids[0])))
|
|
66
|
+
attrs.extend(str(Class(c)) for c in classes[1:])
|
|
67
|
+
if text:
|
|
68
|
+
attrs.append(str(Attribute("title", text)))
|
|
69
|
+
|
|
70
|
+
head = fence + (classes[0] if classes else "")
|
|
71
|
+
if attrs:
|
|
72
|
+
head += " {" + " ".join(attrs) + "}"
|
|
73
|
+
|
|
74
|
+
code_block.open_line = head + (rest or "\n")
|
|
75
|
+
return [r]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@codeblock_filter
|
|
79
|
+
def label(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
|
|
80
|
+
"""Wrap code blocks in a `<div>` with a label; renders in most Markdown previews."""
|
|
81
|
+
code_block = reference_map[r]
|
|
82
|
+
text = block_title(code_block)
|
|
83
|
+
if text is None:
|
|
84
|
+
return [r]
|
|
85
|
+
|
|
86
|
+
ind = code_block.indent
|
|
87
|
+
header = (f"{ind}<div class=\"entangled-code-block\" markdown=\"1\">\n"
|
|
88
|
+
f"{ind}<div class=\"entangled-code-title\"><code>{escape(text)}</code></div>\n\n")
|
|
89
|
+
# The closing fence may lack a newline at the end of a file. The blank
|
|
90
|
+
# lines around `</div>` end the HTML block in CommonMark.
|
|
91
|
+
eol = "" if code_block.close_line.endswith("\n") else "\n"
|
|
92
|
+
footer = f"{eol}\n{ind}</div>\n\n"
|
|
93
|
+
return [PlainText(header), r, PlainText(footer)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Representations to pick from a MIME bundle, most preferred first.
|
|
97
|
+
MIME_PRIORITY = [
|
|
98
|
+
"text/markdown", "text/html", "image/svg+xml", "image/png", "image/jpeg", "image/gif",
|
|
99
|
+
"text/plain",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def fenced(text: str, classes: list[str]) -> str:
|
|
104
|
+
"""Put text in a fenced code block, with a fence longer than any backtick
|
|
105
|
+
run inside the text."""
|
|
106
|
+
longest = max((len(m) for m in re.findall(r"`+", text)), default=0)
|
|
107
|
+
fence = "`" * max(3, longest + 1)
|
|
108
|
+
attrs = " ".join(str(Class(c)) for c in classes)
|
|
109
|
+
return f"{fence} {{{attrs}}}\n{text}\n{fence}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def render_mime(mime: str, data: str, kind: str) -> str | None:
|
|
113
|
+
"""Render a single representation as Markdown, or `None` if there is
|
|
114
|
+
nothing to show."""
|
|
115
|
+
match mime:
|
|
116
|
+
case "text/plain":
|
|
117
|
+
text = data.strip("\n")
|
|
118
|
+
if not text.strip():
|
|
119
|
+
return None
|
|
120
|
+
return fenced(text, ["text", "output"] + ([] if kind == "result" else [kind]))
|
|
121
|
+
case "text/markdown" | "text/html":
|
|
122
|
+
return data.strip("\n") or None
|
|
123
|
+
case "image/svg+xml":
|
|
124
|
+
return f").decode()})"
|
|
125
|
+
case _ if mime.startswith("image/"):
|
|
126
|
+
return f")})"
|
|
127
|
+
case _:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def render_output(output: ReplOutput, output_type: str) -> str | None:
|
|
132
|
+
"""Render the most preferred representation in an output's MIME bundle.
|
|
133
|
+
A `mime-type` set on the code block, other than the default `text/plain`,
|
|
134
|
+
takes precedence."""
|
|
135
|
+
preferred = [] if output_type == "text/plain" else [output_type]
|
|
136
|
+
for mime in [*preferred, *MIME_PRIORITY]:
|
|
137
|
+
if mime in output.data:
|
|
138
|
+
return render_mime(mime, output.data[mime], output.kind)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def command_outputs(command: ReplCommand) -> list[ReplOutput]:
|
|
143
|
+
"""Outputs of a command; sessions from older versions of `repl-session`
|
|
144
|
+
only have the `output` field."""
|
|
145
|
+
if command.outputs is not None:
|
|
146
|
+
return command.outputs
|
|
147
|
+
if command.output is None:
|
|
148
|
+
return []
|
|
149
|
+
return [ReplOutput("result", {command.output_type: command.output})]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@lru_cache(maxsize=16)
|
|
153
|
+
def decode_session(text: str) -> ReplSession:
|
|
154
|
+
"""Decode the output of a REPL session. Every code block in the session
|
|
155
|
+
needs it, so we cache on the contents of the file; modification times
|
|
156
|
+
are too coarse to tell when a file was rewritten."""
|
|
157
|
+
return read_session(StringIO(text))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def session_output(path: Path) -> ReplSession | None:
|
|
161
|
+
try:
|
|
162
|
+
return decode_session(path.read_text())
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
log.warning("REPL session output `%s` not found; run `repl-session` first.", path)
|
|
165
|
+
except msgspec.MsgspecError as e:
|
|
166
|
+
log.warning("REPL session output `%s` could not be read: %s", path, e)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def find_command(reference_map: ReferenceMap, r: ReferenceId) -> ReplCommand | None:
|
|
171
|
+
"""Find the command for a code block in the output of its REPL session.
|
|
172
|
+
The session is opened by the first code block of the same name that has
|
|
173
|
+
the `repl` class and a `session` attribute; every code block of that name
|
|
174
|
+
from there on is a command in the session."""
|
|
175
|
+
refs = reference_map.select_by_name(r.name)
|
|
176
|
+
try:
|
|
177
|
+
start = next(i for i, ref in enumerate(refs)
|
|
178
|
+
if Class("repl") in reference_map[ref].properties
|
|
179
|
+
and get_attribute_string(reference_map[ref].properties, "session"))
|
|
180
|
+
except StopIteration:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
session_file = get_attribute_string(reference_map[refs[start]].properties, "session")
|
|
184
|
+
assert session_file is not None
|
|
185
|
+
session = session_output(Path(session_file).with_suffix(".out.json"))
|
|
186
|
+
if session is None:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
index = refs.index(r) - start
|
|
190
|
+
code_block = reference_map[r]
|
|
191
|
+
if not 0 <= index < len(session.commands):
|
|
192
|
+
log.warning("%s: no output for this code block in REPL session `%s`.",
|
|
193
|
+
code_block.origin, session_file)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
command = session.commands[index]
|
|
197
|
+
if code_block.language is not None \
|
|
198
|
+
and command.command.strip() != strip_comments(code_block.source, code_block.language).strip():
|
|
199
|
+
log.warning("%s: REPL session `%s` is out of date; tangle and run `repl-session` again.",
|
|
200
|
+
code_block.origin, session_file)
|
|
201
|
+
return None
|
|
202
|
+
return command
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@codeblock_filter
|
|
206
|
+
def repl(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
|
|
207
|
+
"""Insert the output of REPL sessions (from the `repl` hook) after code blocks with the `repl` class."""
|
|
208
|
+
code_block = reference_map[r]
|
|
209
|
+
if Class("repl") not in code_block.properties:
|
|
210
|
+
return [r]
|
|
211
|
+
|
|
212
|
+
command = find_command(reference_map, r)
|
|
213
|
+
if command is None:
|
|
214
|
+
return [r]
|
|
215
|
+
|
|
216
|
+
rendered = [text for out in command_outputs(command)
|
|
217
|
+
if (text := render_output(out, command.output_type)) is not None]
|
|
218
|
+
if not rendered:
|
|
219
|
+
return [r]
|
|
220
|
+
|
|
221
|
+
# The closing fence may lack a newline at the end of a file.
|
|
222
|
+
eol = "" if code_block.close_line.endswith("\n") else "\n"
|
|
223
|
+
body = "".join(indent(f"\n{text}\n", code_block.indent) for text in rendered)
|
|
224
|
+
return [r, PlainText(eol + body)]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
filters: dict[str, ContentFilter] = {
|
|
228
|
+
"label": label,
|
|
229
|
+
"repl": repl,
|
|
230
|
+
"title": title,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def filter_description(f: ContentFilter) -> str:
|
|
235
|
+
return (f.__doc__ or "").strip().partition("\n")[0]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "entangled-cli"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.6.0"
|
|
4
4
|
description = "Literate Programming toolbox"
|
|
5
5
|
authors = [{ name = "Johan Hidding", email = "j.hidding@esciencecenter.nl" }]
|
|
6
6
|
requires-python = ">=3.13,<4"
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
"brei>=0.2.3,<0.3",
|
|
15
15
|
"rich-argparse>=1.4.0,<2",
|
|
16
16
|
"pyyaml>=6.0.1,<7",
|
|
17
|
-
"repl-session>=0.
|
|
17
|
+
"repl-session>=0.3.0",
|
|
18
18
|
"msgspec>=0.20.0",
|
|
19
19
|
"click>=8.3.0",
|
|
20
20
|
"rich-click>=1.9.4",
|
|
@@ -22,6 +22,11 @@ dependencies = [
|
|
|
22
22
|
"watchfiles>=1.3",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
jupyter = [
|
|
27
|
+
"repl-session[jupyter]>=0.3.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
25
30
|
[project.urls]
|
|
26
31
|
Homepage = "https://entangled.github.io/"
|
|
27
32
|
Repository = "https://github.com/entangled/entangled.py"
|
|
@@ -47,6 +52,7 @@ dev = [
|
|
|
47
52
|
"types-pexpect>=4.9.0.20250916",
|
|
48
53
|
"types-PyYAML>=6.0.1,<7",
|
|
49
54
|
"pytest-timeout>=2.4.0",
|
|
55
|
+
"ipykernel>=7.0",
|
|
50
56
|
]
|
|
51
57
|
|
|
52
58
|
[tool.hatch.build.targets.sdist]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|