dotx 3.2.0__tar.gz → 3.2.1__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.
- {dotx-3.2.0/src/dotx.egg-info → dotx-3.2.1}/PKG-INFO +11 -1
- {dotx-3.2.0 → dotx-3.2.1}/README.md +10 -0
- {dotx-3.2.0 → dotx-3.2.1}/pyproject.toml +1 -1
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/commands/install_cmd.py +5 -2
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/commands/uninstall_cmd.py +5 -2
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/install.py +2 -1
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/plan.py +2 -1
- {dotx-3.2.0 → dotx-3.2.1/src/dotx.egg-info}/PKG-INFO +11 -1
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_cli.py +51 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_install.py +83 -0
- {dotx-3.2.0 → dotx-3.2.1}/LICENSE +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/MANIFEST.in +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/setup.cfg +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/__init__.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/always-create +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/cli.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/commands/__init__.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/commands/database.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/commands/path_cmd.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/commands/progress.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/database.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/dotxignore +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/hierarchy.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/ignore.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/installed-schema.sql +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/options.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx/uninstall.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx.egg-info/SOURCES.txt +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx.egg-info/dependency_links.txt +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx.egg-info/entry_points.txt +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx.egg-info/requires.txt +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/src/dotx.egg-info/top_level.txt +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_always_create.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_cli_database.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_ignore.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_ignore_rules.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_options.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_path_which.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_plan.py +0 -0
- {dotx-3.2.0 → dotx-3.2.1}/tests/test_uninstall.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotx
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: A command-line tool to install a link-farm to your dotfiles
|
|
5
5
|
Author-email: Wolf <Wolf@zv.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -58,6 +58,16 @@ purpose tool. It's made for installing a link-farm to any kind of package from
|
|
|
58
58
|
it for other purposes, but it's tuned for its goal. It does the renaming task if you want it, but if your source files
|
|
59
59
|
are named simply `.bashrc` it works just as well.
|
|
60
60
|
|
|
61
|
+
### How to install
|
|
62
|
+
|
|
63
|
+
It's an ordinary PyPI-supplied Python package providing CLI entry points, so you can install it with `pip` or any other tool you like. I think the best way to install it is this:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv tool install dotx
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The current version is: v3.2.0
|
|
70
|
+
|
|
61
71
|
### The user interface
|
|
62
72
|
```
|
|
63
73
|
Usage: dotx [OPTIONS] COMMAND [ARGS]...
|
|
@@ -30,6 +30,16 @@ purpose tool. It's made for installing a link-farm to any kind of package from
|
|
|
30
30
|
it for other purposes, but it's tuned for its goal. It does the renaming task if you want it, but if your source files
|
|
31
31
|
are named simply `.bashrc` it works just as well.
|
|
32
32
|
|
|
33
|
+
### How to install
|
|
34
|
+
|
|
35
|
+
It's an ordinary PyPI-supplied Python package providing CLI entry points, so you can install it with `pip` or any other tool you like. I think the best way to install it is this:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv tool install dotx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The current version is: v3.2.0
|
|
42
|
+
|
|
33
43
|
### The user interface
|
|
34
44
|
```
|
|
35
45
|
Usage: dotx [OPTIONS] COMMAND [ARGS]...
|
|
@@ -10,7 +10,7 @@ from rich.console import Console
|
|
|
10
10
|
from dotx.commands.progress import execute_plans_with_progress
|
|
11
11
|
from dotx.database import InstallationDB
|
|
12
12
|
from dotx.install import plan_install
|
|
13
|
-
from dotx.options import is_verbose_mode
|
|
13
|
+
from dotx.options import is_dry_run, is_verbose_mode
|
|
14
14
|
from dotx.plan import Action, Plan, extract_plan, log_extracted_plan
|
|
15
15
|
|
|
16
16
|
|
|
@@ -92,7 +92,10 @@ def register_command(app: typer.Typer):
|
|
|
92
92
|
summary_parts.append(f"{total_dirs} dir(s)")
|
|
93
93
|
|
|
94
94
|
summary = " and ".join(summary_parts) if summary_parts else "nothing"
|
|
95
|
-
|
|
95
|
+
if is_dry_run(ctx):
|
|
96
|
+
console.print(f"\n[yellow][DRY RUN] Would install {summary} from {len(sources)} package(s)[/yellow]")
|
|
97
|
+
else:
|
|
98
|
+
console.print(f"\n[green]✓ Installed {summary} from {len(sources)} package(s)[/green]")
|
|
96
99
|
else:
|
|
97
100
|
console.print("[red]✗ Refusing to install - conflicts detected[/red]")
|
|
98
101
|
|
|
@@ -9,7 +9,7 @@ from rich.console import Console
|
|
|
9
9
|
|
|
10
10
|
from dotx.commands.progress import execute_plans_with_progress
|
|
11
11
|
from dotx.database import InstallationDB
|
|
12
|
-
from dotx.options import is_verbose_mode
|
|
12
|
+
from dotx.options import is_dry_run, is_verbose_mode
|
|
13
13
|
from dotx.plan import Action, Plan, extract_plan, log_extracted_plan
|
|
14
14
|
from dotx.uninstall import plan_uninstall
|
|
15
15
|
|
|
@@ -67,6 +67,9 @@ def register_command(app: typer.Typer):
|
|
|
67
67
|
len(extract_plan(plan, {Action.UNLINK}))
|
|
68
68
|
for _, plan in plans
|
|
69
69
|
)
|
|
70
|
-
|
|
70
|
+
if is_dry_run(ctx):
|
|
71
|
+
console.print(f"\n[yellow][DRY RUN] Would remove {total_removed} symlink(s) from {len(sources)} package(s)[/yellow]")
|
|
72
|
+
else:
|
|
73
|
+
console.print(f"\n[green]✓ Removed {total_removed} symlink(s) from {len(sources)} package(s)[/green]")
|
|
71
74
|
|
|
72
75
|
logger.info("uninstall finished")
|
|
@@ -193,8 +193,9 @@ def plan_install(source_package_root: Path, destination_root: Path) -> Plan:
|
|
|
193
193
|
plan=plan,
|
|
194
194
|
)
|
|
195
195
|
logger.debug(f"Directory {relative_destination_root_path} matches always-create pattern, will CREATE")
|
|
196
|
-
|
|
196
|
+
elif plan[relative_root_path].action != Action.CREATE:
|
|
197
197
|
# Directory doesn't exist and has no rename conflicts - we can link it
|
|
198
|
+
# But only if it wasn't already marked CREATE by a descendant's mark_all_ancestors
|
|
198
199
|
plan[relative_root_path].action = Action.LINK
|
|
199
200
|
|
|
200
201
|
# Third pass: mark children based on what we decided for this directory
|
|
@@ -183,11 +183,12 @@ def execute_plan(
|
|
|
183
183
|
# This does duplicate the loop code, but avoids thousands of repeated is_dry_run() calls
|
|
184
184
|
# in installations with many files.
|
|
185
185
|
if is_dry_run():
|
|
186
|
+
print("[DRY RUN] Would execute the equivalent of:")
|
|
186
187
|
for step in steps:
|
|
187
188
|
command = build_shell_command(step)
|
|
188
189
|
if command is not None:
|
|
189
190
|
logger.info(command)
|
|
190
|
-
print(command)
|
|
191
|
+
print(f" {command}")
|
|
191
192
|
else:
|
|
192
193
|
for step in steps:
|
|
193
194
|
command = build_shell_command(step)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotx
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: A command-line tool to install a link-farm to your dotfiles
|
|
5
5
|
Author-email: Wolf <Wolf@zv.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -58,6 +58,16 @@ purpose tool. It's made for installing a link-farm to any kind of package from
|
|
|
58
58
|
it for other purposes, but it's tuned for its goal. It does the renaming task if you want it, but if your source files
|
|
59
59
|
are named simply `.bashrc` it works just as well.
|
|
60
60
|
|
|
61
|
+
### How to install
|
|
62
|
+
|
|
63
|
+
It's an ordinary PyPI-supplied Python package providing CLI entry points, so you can install it with `pip` or any other tool you like. I think the best way to install it is this:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv tool install dotx
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The current version is: v3.2.0
|
|
70
|
+
|
|
61
71
|
### The user interface
|
|
62
72
|
```
|
|
63
73
|
Usage: dotx [OPTIONS] COMMAND [ARGS]...
|
|
@@ -246,3 +246,54 @@ def test_options_functions():
|
|
|
246
246
|
result = runner.invoke(app, ["--verbose", "--debug", "--dry-run", "debug"])
|
|
247
247
|
|
|
248
248
|
assert len(result.output) == 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_cli_install_dry_run_shows_indicator(tmp_path):
|
|
252
|
+
"""Test install with --dry-run clearly indicates dry-run mode in output."""
|
|
253
|
+
source = tmp_path / "source"
|
|
254
|
+
source.mkdir()
|
|
255
|
+
target = tmp_path / "target"
|
|
256
|
+
target.mkdir()
|
|
257
|
+
|
|
258
|
+
# Create source file
|
|
259
|
+
(source / "file1").write_text("content1")
|
|
260
|
+
|
|
261
|
+
runner = CliRunner()
|
|
262
|
+
|
|
263
|
+
# Install with --dry-run
|
|
264
|
+
result = runner.invoke(app, [f"--target={target}", "--dry-run", "install", str(source)])
|
|
265
|
+
|
|
266
|
+
assert result.exit_code == 0
|
|
267
|
+
# Should clearly indicate dry-run mode
|
|
268
|
+
assert "[DRY RUN]" in result.output
|
|
269
|
+
assert "Would install" in result.output
|
|
270
|
+
# File should NOT actually be created
|
|
271
|
+
assert not (target / "file1").exists()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_cli_uninstall_dry_run_shows_indicator(tmp_path, isolated_db):
|
|
275
|
+
"""Test uninstall with --dry-run clearly indicates dry-run mode in output."""
|
|
276
|
+
source = tmp_path / "source"
|
|
277
|
+
source.mkdir()
|
|
278
|
+
target = tmp_path / "target"
|
|
279
|
+
target.mkdir()
|
|
280
|
+
|
|
281
|
+
# Create and install source file
|
|
282
|
+
(source / "file1").write_text("content1")
|
|
283
|
+
|
|
284
|
+
runner = CliRunner()
|
|
285
|
+
|
|
286
|
+
# First install for real
|
|
287
|
+
result = runner.invoke(app, [f"--target={target}", "install", str(source)])
|
|
288
|
+
assert result.exit_code == 0
|
|
289
|
+
assert (target / "file1").exists()
|
|
290
|
+
|
|
291
|
+
# Uninstall with --dry-run
|
|
292
|
+
result = runner.invoke(app, [f"--target={target}", "--dry-run", "uninstall", str(source)])
|
|
293
|
+
|
|
294
|
+
assert result.exit_code == 0
|
|
295
|
+
# Should clearly indicate dry-run mode
|
|
296
|
+
assert "[DRY RUN]" in result.output
|
|
297
|
+
assert "Would remove" in result.output
|
|
298
|
+
# File should still exist after dry-run
|
|
299
|
+
assert (target / "file1").exists()
|
|
@@ -543,3 +543,86 @@ def test_install_renamed_dir_inside_renamed_dir(tmp_path):
|
|
|
543
543
|
Action.LINK, True, dir2_path, Path(".SIMPLE-DIR1/.SIMPLE-DIR2"), True
|
|
544
544
|
)
|
|
545
545
|
assert plan[file_path].action == Action.SKIP
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_install_deep_nesting_with_renamed_leaf(tmp_path):
|
|
549
|
+
"""
|
|
550
|
+
Test that deeply nested directories with a renamed file at the leaf install correctly.
|
|
551
|
+
|
|
552
|
+
This tests the fix for a bug where:
|
|
553
|
+
- Deep directory structure: a/b/c/d/e/f/dot-deepfile
|
|
554
|
+
- The leaf file needs renaming (dot-deepfile -> .deepfile)
|
|
555
|
+
- Directory f gets marked CREATE (because child needs renaming)
|
|
556
|
+
- All parent directories should also be CREATE (via mark_all_ancestors)
|
|
557
|
+
|
|
558
|
+
Bug was: parent dirs were overwritten to LINK when processed bottom-up,
|
|
559
|
+
causing FileExistsError when we tried to CREATE f under a symlinked parent.
|
|
560
|
+
|
|
561
|
+
Fix: preserve CREATE if already set by a descendant's mark_all_ancestors.
|
|
562
|
+
"""
|
|
563
|
+
source_package_root = tmp_path / "source"
|
|
564
|
+
destination_root = tmp_path / "dest"
|
|
565
|
+
source_package_root.mkdir()
|
|
566
|
+
destination_root.mkdir()
|
|
567
|
+
|
|
568
|
+
# Create deep nesting: a/b/c/d/e/f/dot-deepfile
|
|
569
|
+
deep_path = Path("a/b/c/d/e/f")
|
|
570
|
+
file_path = deep_path / "dot-deepfile"
|
|
571
|
+
(source_package_root / deep_path).mkdir(parents=True)
|
|
572
|
+
(source_package_root / file_path).write_text("deep content")
|
|
573
|
+
|
|
574
|
+
plan = plan_install(source_package_root, destination_root)
|
|
575
|
+
|
|
576
|
+
# All directories should be CREATE (to support the renamed file at the leaf)
|
|
577
|
+
for part_count in range(1, 7): # a, b, c, d, e, f
|
|
578
|
+
parts = ["a", "b", "c", "d", "e", "f"][:part_count]
|
|
579
|
+
subdir_path = Path("/".join(parts))
|
|
580
|
+
assert plan[subdir_path].action == Action.CREATE, f"{subdir_path} should be CREATE"
|
|
581
|
+
|
|
582
|
+
# The file should be LINK with renaming
|
|
583
|
+
assert plan[file_path].action == Action.LINK
|
|
584
|
+
assert plan[file_path].requires_rename
|
|
585
|
+
assert plan[file_path].relative_destination_path == Path("a/b/c/d/e/f/.deepfile")
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def test_install_deep_nesting_execute(tmp_path, isolated_db):
|
|
589
|
+
"""
|
|
590
|
+
Test that deeply nested directories with renamed leaf actually install without error.
|
|
591
|
+
|
|
592
|
+
This is an integration test that verifies the full install flow works,
|
|
593
|
+
not just the planning phase.
|
|
594
|
+
"""
|
|
595
|
+
from typer.testing import CliRunner
|
|
596
|
+
|
|
597
|
+
from dotx.cli import app
|
|
598
|
+
|
|
599
|
+
source_package_root = tmp_path / "source"
|
|
600
|
+
destination_root = tmp_path / "dest"
|
|
601
|
+
source_package_root.mkdir()
|
|
602
|
+
destination_root.mkdir()
|
|
603
|
+
|
|
604
|
+
# Create deep nesting: a/b/c/d/e/f/dot-deepfile
|
|
605
|
+
deep_path = Path("a/b/c/d/e/f")
|
|
606
|
+
file_path = deep_path / "dot-deepfile"
|
|
607
|
+
(source_package_root / deep_path).mkdir(parents=True)
|
|
608
|
+
(source_package_root / file_path).write_text("deep content")
|
|
609
|
+
|
|
610
|
+
runner = CliRunner()
|
|
611
|
+
|
|
612
|
+
# Execute install via CLI - should not raise an error
|
|
613
|
+
result = runner.invoke(app, [f"--target={destination_root}", "install", str(source_package_root)])
|
|
614
|
+
|
|
615
|
+
assert result.exit_code == 0, f"Install failed: {result.output}"
|
|
616
|
+
|
|
617
|
+
# Verify: all directories should be real directories (CREATE), not symlinks
|
|
618
|
+
for part_count in range(1, 7):
|
|
619
|
+
parts = ["a", "b", "c", "d", "e", "f"][:part_count]
|
|
620
|
+
dir_path = destination_root / "/".join(parts)
|
|
621
|
+
assert dir_path.is_dir(), f"{dir_path} should be a directory"
|
|
622
|
+
assert not dir_path.is_symlink(), f"{dir_path} should NOT be a symlink"
|
|
623
|
+
|
|
624
|
+
# The file should be a symlink with the renamed name (.deepfile, not dot-deepfile)
|
|
625
|
+
deep_file_dest = destination_root / "a/b/c/d/e/f/.deepfile"
|
|
626
|
+
assert deep_file_dest.is_symlink()
|
|
627
|
+
assert deep_file_dest.exists()
|
|
628
|
+
assert deep_file_dest.read_text() == "deep content"
|
|
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
|