Skip to content

Commit 87d9a97

Browse files
committed
Document lock workflows
1 parent fb0d9f8 commit 87d9a97

10 files changed

Lines changed: 509 additions & 2 deletions

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ repos:
3333
rev: 0.11.6
3434
hooks:
3535
- id: uv-lock
36+
- repo: local
37+
hooks:
38+
- id: check-termynal-line-lengths
39+
name: Check Termynal line lengths
40+
entry: uv run python scripts/check_termynal_line_lengths.py
41+
language: system
42+
files: ^docs/source/_static/md/.*\.md$
3643
- repo: https://github.com/executablebooks/mdformat
3744
rev: 1.0.0
3845
hooks:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="termy">
2+
3+
```console
4+
$ pytask lock accept -k train --dry-run
5+
6+
<span class="termynal-dim">──────────────────────────</span> Start pytask session <span class="termynal-dim">──────────────────────────</span>
7+
Platform: &lt;platform&gt; -- Python &lt;version&gt;, pytask &lt;version&gt;
8+
Root: &lt;path&gt;
9+
Collected 2 tasks.
10+
<span class="termynal-success">Would accept recorded state for task_train.py::task_train.</span>
11+
<span class="termynal-success">Would accept recorded state for task_evaluate.py::task_evaluate.</span>
12+
13+
<span class="termynal-dim">─────────────────────────────────────────────────────────────────────────</span>
14+
```
15+
16+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="termy">
2+
3+
```console
4+
$ pytask lock accept -k train
5+
<span class="termynal-dim">──────────────────────────</span> Start pytask session <span class="termynal-dim">──────────────────────────</span>
6+
Platform: &lt;platform&gt; -- Python &lt;version&gt;, pytask &lt;version&gt;
7+
Root: &lt;path&gt;
8+
Collected 2 tasks.
9+
# Accept recorded state for task_train.py::task_train? [y/N]: $ y
10+
# Accept recorded state for task_evaluate.py::task_evaluate? [y/N]: $ n
11+
<span class="termynal-success">Accept recorded state for task_train.py::task_train.</span>
12+
13+
<span class="termynal-dim">─────────────────────────────────────────────────────────────────────────</span>
14+
```
15+
16+
</div>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="termy">
2+
3+
```console
4+
$ pytask lock clean
5+
<span class="termynal-dim">──────────────────────────</span> Start pytask session <span class="termynal-dim">──────────────────────────</span>
6+
Platform: &lt;platform&gt; -- Python &lt;version&gt;, pytask &lt;version&gt;
7+
Root: &lt;path&gt;
8+
Collected 2 tasks.
9+
# Remove recorded state for task_old.py::task_train? [y/N]: $ y
10+
# Remove recorded state for task_old.py::task_evaluate? [y/N]: $ y
11+
<span class="termynal-success">Remove recorded state for task_old.py::task_train.</span>
12+
<span class="termynal-success">Remove recorded state for task_old.py::task_evaluate.</span>
13+
14+
<span class="termynal-dim">─────────────────────────────────────────────────────────────────────────</span>
15+
```
16+
17+
</div>

docs/source/how_to_guides/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ specific tasks with pytask.
1010
- [Migrating From Scripts To Pytask](migrating_from_scripts_to_pytask.md)
1111
- [Interfaces For Dependencies Products](interfaces_for_dependencies_products.md)
1212
- [Portability](portability.md)
13+
- [Reconciling Lockfile State](reconciling_lockfile_state.md)
1314
- [Remote Files](remote_files.md)
1415
- [Functional Interface](functional_interface.md)
1516
- [Capture Warnings](capture_warnings.md)

docs/source/how_to_guides/portability.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ Use this checklist when you move a project to another machine or environment.
1717
Run a normal build with [`pytask build`](../reference_guides/commands.md#pytask-build)
1818
so `pytask.lock` is up to date:
1919

20-
````
2120
```console
2221
$ pytask build
2322
```
2423

2524
If you already have a recent lockfile and up-to-date outputs, you can skip this step.
26-
````
2725

2826
1. **Ship the right files.**
2927

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Reconciling Lockfile State
2+
3+
Use [`pytask lock`](../reference_guides/commands.md#pytask-lock) when the current files
4+
in the project are already correct and only the recorded state in `pytask.lock` needs to
5+
catch up.
6+
7+
This is an advanced workflow. Most of the time,
8+
[`pytask build`](../reference_guides/commands.md#pytask-build) is the right command.
9+
Reach for `pytask lock` when you want to change the lockfile without executing tasks.
10+
11+
!!! warning
12+
13+
`pytask lock` is a sharp tool. It updates recorded state without proving that the files
14+
were produced by the current task definitions.
15+
16+
## When is this useful?
17+
18+
Typical situations are:
19+
20+
- You reformatted or reorganized a task file and do not want to rerun an expensive task.
21+
- You renamed or moved a task and want to accept the current outputs for the new task.
22+
- You produced outputs manually or elsewhere and now want to register them in the
23+
lockfile.
24+
- You deleted or renamed tasks and want to remove their stale lockfile entries.
25+
26+
## Preview changes first
27+
28+
By default, `pytask lock` runs interactively. It shows the planned changes and then asks
29+
for confirmation one by one. Only entries which would actually change appear in the
30+
prompt sequence.
31+
32+
To preview changes without writing them, use `--dry-run`:
33+
34+
--8<-- "docs/source/_static/md/lock-accept-dry-run.md"
35+
36+
To apply all planned changes without prompting, use `--yes`:
37+
38+
```console
39+
$ pytask lock accept -k train --yes
40+
```
41+
42+
## Accept the current state
43+
44+
Use [`pytask lock accept`](../reference_guides/commands.md#pytask-lock-accept) when the
45+
current dependencies, products, and task definition are already correct and should
46+
become the new recorded state.
47+
48+
--8<-- "docs/source/_static/md/lock-accept-interactive.md"
49+
50+
If no selectors are provided, `pytask lock accept` applies to all collected tasks in the
51+
provided paths.
52+
53+
If selectors are provided with `-k` or `-m`, `accept` automatically includes the
54+
ancestors of the selected tasks. This is useful when you target a downstream task and
55+
want the accepted state to stay consistent with its upstream dependencies.
56+
57+
```console
58+
$ pytask lock accept -k evaluate
59+
```
60+
61+
In this example, `pytask` accepts `evaluate` and its ancestors. It does not
62+
automatically include descendants. If you want to accept a wider part of the DAG, widen
63+
the task selection yourself.
64+
65+
```console
66+
$ pytask lock accept -k "train or evaluate"
67+
```
68+
69+
If a selected task is missing a required dependency or product, the command fails
70+
instead of accepting incomplete state.
71+
72+
## Reset recorded state
73+
74+
Use [`pytask lock reset`](../reference_guides/commands.md#pytask-lock-reset) to remove
75+
recorded state for selected tasks.
76+
77+
```console
78+
$ pytask lock reset -k train
79+
```
80+
81+
Unlike `accept`, `reset` works on the exact selected tasks. It does not automatically
82+
include ancestors.
83+
84+
On the next build, `pytask` determines again whether these tasks require execution. This
85+
is useful when state was accepted too broadly or when you want a specific task to be
86+
reconsidered from scratch.
87+
88+
## Remove stale lockfile entries
89+
90+
Use [`pytask lock clean`](../reference_guides/commands.md#pytask-lock-clean) to remove
91+
entries from the lockfile which no longer correspond to collected tasks in the current
92+
project.
93+
94+
--8<-- "docs/source/_static/md/lock-clean.md"
95+
96+
This is useful after deleting, renaming, or moving tasks when old entries should no
97+
longer remain in the lockfile.
98+
99+
## Example workflow
100+
101+
One common workflow looks like this:
102+
103+
1. Run a normal build once.
104+
1. Change a task file in a way that should not force a rerun.
105+
1. Accept the current state.
106+
1. Verify that a later build skips the task.
107+
1. Reset the task if you want `pytask` to reconsider it again.
108+
109+
```console
110+
$ pytask build
111+
$ pytask lock accept -k train --yes
112+
$ pytask build
113+
$ pytask lock reset -k train --yes
114+
$ pytask build
115+
```
116+
117+
After `accept`, the next build skips unchanged tasks according to the updated lockfile.
118+
After `reset`, the selected tasks are reconsidered on the next build.
119+
120+
## Be explicit about scope
121+
122+
Start with narrow task selections, preview changes with `--dry-run`, and widen the
123+
selection only when needed.
124+
125+
This is especially important for `accept`: it is often better to accept a small part of
126+
the DAG first and then inspect the result than to update the whole project at once.
127+
128+
## Related
129+
130+
- [`pytask lock`](../reference_guides/commands.md#pytask-lock)
131+
- [`pytask build`](../reference_guides/commands.md#pytask-build)
132+
- [Portability](portability.md)
133+
- [The lockfile](../reference_guides/lockfile.md)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ nav:
3838
- Migrating From Scripts To pytask: how_to_guides/migrating_from_scripts_to_pytask.md
3939
- Interfaces For Dependencies And Products: how_to_guides/interfaces_for_dependencies_products.md
4040
- Portability: how_to_guides/portability.md
41+
- Reconciling Lockfile State: how_to_guides/reconciling_lockfile_state.md
4142
- Remote Files: how_to_guides/remote_files.md
4243
- Functional Interface: how_to_guides/functional_interface.md
4344
- Capture Warnings: how_to_guides/capture_warnings.md
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Check visible line lengths in Termynal documentation snippets."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import re
7+
import sys
8+
from html import unescape
9+
from pathlib import Path
10+
11+
# Existing Termynal snippets top out at 78 visible characters.
12+
MAX_TERMYNAL_LINE_LENGTH = 78
13+
14+
_CONSOLE_BLOCK_PATTERN = re.compile(r"```console\n(.*?)\n```", re.DOTALL)
15+
_HTML_TAG_PATTERN = re.compile(r"<[^>]+>")
16+
17+
18+
def _visible_text(line: str) -> str:
19+
return unescape(_HTML_TAG_PATTERN.sub("", line))
20+
21+
22+
def _iter_violations(path: Path, max_length: int) -> list[str]:
23+
text = path.read_text(encoding="utf-8")
24+
if 'class="termy"' not in text:
25+
return []
26+
27+
violations = []
28+
for match in _CONSOLE_BLOCK_PATTERN.finditer(text):
29+
start_line = text.count("\n", 0, match.start(1)) + 1
30+
for offset, raw_line in enumerate(match.group(1).splitlines()):
31+
rendered_line = _visible_text(raw_line)
32+
line_length = len(rendered_line)
33+
if line_length > max_length:
34+
violations.append(
35+
f"{path}:{start_line + offset}: rendered line has "
36+
f"{line_length} characters, maximum is {max_length}:\n"
37+
f" {rendered_line}"
38+
)
39+
40+
return violations
41+
42+
43+
def main() -> int:
44+
parser = argparse.ArgumentParser()
45+
parser.add_argument(
46+
"paths",
47+
nargs="*",
48+
type=Path,
49+
default=sorted(Path("docs/source/_static/md").rglob("*.md")),
50+
help="Markdown files with Termynal snippets.",
51+
)
52+
parser.add_argument(
53+
"--max-length",
54+
type=int,
55+
default=MAX_TERMYNAL_LINE_LENGTH,
56+
help="Maximum visible line length for rendered terminal lines.",
57+
)
58+
args = parser.parse_args()
59+
60+
violations = [
61+
violation
62+
for path in args.paths
63+
if path.is_file()
64+
for violation in _iter_violations(path, args.max_length)
65+
]
66+
if violations:
67+
sys.stderr.write("\n\n".join(violations))
68+
sys.stderr.write("\n")
69+
return 1
70+
return 0
71+
72+
73+
if __name__ == "__main__":
74+
raise SystemExit(main())

0 commit comments

Comments
 (0)