dploy 0.1.2__tar.gz → 0.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.
Files changed (111) hide show
  1. dploy-0.2.0/.gitattributes +5 -0
  2. dploy-0.2.0/.github/workflows/ci.yml +54 -0
  3. dploy-0.2.0/.github/workflows/release.yaml +50 -0
  4. dploy-0.2.0/.gitignore +16 -0
  5. dploy-0.2.0/AGENTS.md +21 -0
  6. {dploy-0.1.2 → dploy-0.2.0}/CHANGELOG.md +75 -3
  7. dploy-0.2.0/CLAUDE.md +1 -0
  8. dploy-0.2.0/CONTEXT.md +105 -0
  9. dploy-0.2.0/PKG-INFO +121 -0
  10. dploy-0.2.0/README.md +101 -0
  11. dploy-0.2.0/docs/adr/0001-windows-permission-validation.md +67 -0
  12. dploy-0.2.0/docs/agents/domain.md +51 -0
  13. dploy-0.2.0/docs/agents/issue-tracker.md +45 -0
  14. dploy-0.2.0/docs/agents/triage-labels.md +13 -0
  15. dploy-0.2.0/dploy/__init__.py +94 -0
  16. {dploy-0.1.2 → dploy-0.2.0}/dploy/__main__.py +2 -4
  17. dploy-0.2.0/dploy/_winaccess.py +203 -0
  18. dploy-0.2.0/dploy/actions.py +207 -0
  19. dploy-0.2.0/dploy/cli.py +173 -0
  20. dploy-0.2.0/dploy/error.py +175 -0
  21. dploy-0.2.0/dploy/ignore.py +96 -0
  22. dploy-0.2.0/dploy/linkcmd.py +119 -0
  23. dploy-0.2.0/dploy/main.py +138 -0
  24. dploy-0.2.0/dploy/stowcmd.py +711 -0
  25. dploy-0.2.0/dploy/utils.py +225 -0
  26. dploy-0.2.0/dploy/version.py +5 -0
  27. dploy-0.2.0/pyproject.toml +100 -0
  28. dploy-0.2.0/tasks.py +123 -0
  29. dploy-0.2.0/tests/__init__.py +0 -0
  30. dploy-0.2.0/tests/conftest.py +231 -0
  31. dploy-0.2.0/tests/test_clean.py +140 -0
  32. dploy-0.2.0/tests/test_cli.py +159 -0
  33. dploy-0.2.0/tests/test_ignore.py +228 -0
  34. dploy-0.2.0/tests/test_link.py +129 -0
  35. dploy-0.2.0/tests/test_stow.py +499 -0
  36. dploy-0.2.0/tests/test_unstow.py +517 -0
  37. dploy-0.2.0/tests/test_utils.py +271 -0
  38. dploy-0.2.0/tests/utils.py +249 -0
  39. dploy-0.2.0/uv.lock +476 -0
  40. dploy-0.1.2/.pylintrc +0 -7
  41. dploy-0.1.2/MANIFEST.in +0 -12
  42. dploy-0.1.2/PKG-INFO +0 -76
  43. dploy-0.1.2/README.md +0 -58
  44. dploy-0.1.2/dploy/__init__.py +0 -35
  45. dploy-0.1.2/dploy/__init__.py.orig +0 -45
  46. dploy-0.1.2/dploy/__init__.pyc +0 -0
  47. dploy-0.1.2/dploy/__pycache__/__init__.cpython-35.pyc +0 -0
  48. dploy-0.1.2/dploy/__pycache__/__init__.cpython-36.pyc +0 -0
  49. dploy-0.1.2/dploy/__pycache__/__main__.cpython-35.pyc +0 -0
  50. dploy-0.1.2/dploy/__pycache__/__main__.cpython-36.pyc +0 -0
  51. dploy-0.1.2/dploy/__pycache__/actions.cpython-35.pyc +0 -0
  52. dploy-0.1.2/dploy/__pycache__/actions.cpython-36.pyc +0 -0
  53. dploy-0.1.2/dploy/__pycache__/cli.cpython-35.pyc +0 -0
  54. dploy-0.1.2/dploy/__pycache__/cli.cpython-36.pyc +0 -0
  55. dploy-0.1.2/dploy/__pycache__/error.cpython-35.pyc +0 -0
  56. dploy-0.1.2/dploy/__pycache__/error.cpython-36.pyc +0 -0
  57. dploy-0.1.2/dploy/__pycache__/ignore.cpython-35.pyc +0 -0
  58. dploy-0.1.2/dploy/__pycache__/ignore.cpython-36.pyc +0 -0
  59. dploy-0.1.2/dploy/__pycache__/linkcmd.cpython-35.pyc +0 -0
  60. dploy-0.1.2/dploy/__pycache__/linkcmd.cpython-36.pyc +0 -0
  61. dploy-0.1.2/dploy/__pycache__/main.cpython-35.pyc +0 -0
  62. dploy-0.1.2/dploy/__pycache__/main.cpython-36.pyc +0 -0
  63. dploy-0.1.2/dploy/__pycache__/stowcmd.cpython-35.pyc +0 -0
  64. dploy-0.1.2/dploy/__pycache__/stowcmd.cpython-36.pyc +0 -0
  65. dploy-0.1.2/dploy/__pycache__/utils.cpython-35.pyc +0 -0
  66. dploy-0.1.2/dploy/__pycache__/utils.cpython-36.pyc +0 -0
  67. dploy-0.1.2/dploy/__pycache__/version.cpython-35.pyc +0 -0
  68. dploy-0.1.2/dploy/__pycache__/version.cpython-36.pyc +0 -0
  69. dploy-0.1.2/dploy/actions.py +0 -213
  70. dploy-0.1.2/dploy/cli.py +0 -101
  71. dploy-0.1.2/dploy/error.py +0 -121
  72. dploy-0.1.2/dploy/ignore.py +0 -67
  73. dploy-0.1.2/dploy/linkcmd.py +0 -87
  74. dploy-0.1.2/dploy/main.py +0 -127
  75. dploy-0.1.2/dploy/stowcmd.py +0 -376
  76. dploy-0.1.2/dploy/utils.py +0 -105
  77. dploy-0.1.2/dploy/version.py +0 -18
  78. dploy-0.1.2/dploy.egg-info/PKG-INFO +0 -76
  79. dploy-0.1.2/dploy.egg-info/SOURCES.txt +0 -73
  80. dploy-0.1.2/dploy.egg-info/dependency_links.txt +0 -1
  81. dploy-0.1.2/dploy.egg-info/entry_points.txt +0 -3
  82. dploy-0.1.2/dploy.egg-info/top_level.txt +0 -1
  83. dploy-0.1.2/requirements.txt +0 -10
  84. dploy-0.1.2/setup.cfg +0 -8
  85. dploy-0.1.2/setup.py +0 -41
  86. dploy-0.1.2/tasks.py +0 -108
  87. dploy-0.1.2/tests/__pycache__/conftest.cpython-35-PYTEST.pyc +0 -0
  88. dploy-0.1.2/tests/__pycache__/conftest.cpython-36-PYTEST.pyc +0 -0
  89. dploy-0.1.2/tests/__pycache__/conftest.cpython-36.pyc +0 -0
  90. dploy-0.1.2/tests/__pycache__/test_cli.cpython-35-PYTEST.pyc +0 -0
  91. dploy-0.1.2/tests/__pycache__/test_cli.cpython-36-PYTEST.pyc +0 -0
  92. dploy-0.1.2/tests/__pycache__/test_ignore.cpython-35-PYTEST.pyc +0 -0
  93. dploy-0.1.2/tests/__pycache__/test_ignore.cpython-36-PYTEST.pyc +0 -0
  94. dploy-0.1.2/tests/__pycache__/test_link.cpython-35-PYTEST.pyc +0 -0
  95. dploy-0.1.2/tests/__pycache__/test_link.cpython-36-PYTEST.pyc +0 -0
  96. dploy-0.1.2/tests/__pycache__/test_stow.cpython-35-PYTEST.pyc +0 -0
  97. dploy-0.1.2/tests/__pycache__/test_stow.cpython-36-PYTEST.pyc +0 -0
  98. dploy-0.1.2/tests/__pycache__/test_unstow.cpython-35-PYTEST.pyc +0 -0
  99. dploy-0.1.2/tests/__pycache__/test_unstow.cpython-36-PYTEST.pyc +0 -0
  100. dploy-0.1.2/tests/__pycache__/utils.cpython-35.pyc +0 -0
  101. dploy-0.1.2/tests/__pycache__/utils.cpython-36.pyc +0 -0
  102. dploy-0.1.2/tests/conftest.py +0 -173
  103. dploy-0.1.2/tests/test_cli.py +0 -84
  104. dploy-0.1.2/tests/test_ignore.py +0 -40
  105. dploy-0.1.2/tests/test_link.py +0 -67
  106. dploy-0.1.2/tests/test_stow.py +0 -232
  107. dploy-0.1.2/tests/test_unstow.py +0 -228
  108. dploy-0.1.2/tests/utils.py +0 -101
  109. {dploy-0.1.2 → dploy-0.2.0}/LICENSE.txt +0 -0
  110. /dploy-0.1.2/conftest.py → /dploy-0.2.0/dploy/py.typed +0 -0
  111. {dploy-0.1.2 → dploy-0.2.0}/invoke.yaml +0 -0
@@ -0,0 +1,5 @@
1
+ # This checkout is shared between Windows and Linux (via Dropbox), so the same
2
+ # physical files are opened by both. Normalize to LF everywhere -- including in
3
+ # the working tree on Windows -- so that editing from one side does not rewrite
4
+ # every file from the other side's point of view.
5
+ * text=auto eol=lf
@@ -0,0 +1,54 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test:
12
+ name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
13
+ runs-on: ${{ matrix.os }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ubuntu-latest, macos-latest, windows-latest]
18
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ # hatch-vcs reads the version from tags, so it needs the full history.
24
+ fetch-depth: 0
25
+
26
+ - name: Enable symlinks on Windows
27
+ if: runner.os == 'Windows'
28
+ run: |
29
+ reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /v "AllowDevelopmentWithoutDevLicense" /d 1 /f
30
+ git config --global core.symlinks true
31
+
32
+ - name: Install uv
33
+ uses: astral-sh/setup-uv@v5
34
+ with:
35
+ enable-cache: true
36
+ cache-dependency-glob: "uv.lock"
37
+
38
+ - name: Set up Python ${{ matrix.python-version }}
39
+ run: uv python install ${{ matrix.python-version }}
40
+
41
+ - name: Install dependencies
42
+ run: uv sync --all-groups
43
+
44
+ - name: Lint (pylint, ruff, ty)
45
+ run: uv run invoke lint
46
+
47
+ - name: Reformat Check (ruff)
48
+ run: uv run invoke reformat-check
49
+
50
+ - name: Metrics (radon)
51
+ run: uv run invoke metrics
52
+
53
+ - name: Test (pytest)
54
+ run: uv run invoke test
@@ -0,0 +1,50 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI when a version tag is pushed, for example:
4
+ # git tag 0.2.0 && git push origin 0.2.0
5
+ # The package version comes from the tag (hatch-vcs), so there is no version
6
+ # to edit by hand. Before tagging, set the date in CHANGELOG.md.
7
+ #
8
+ # One-time setup on PyPI: add this repository and workflow (release.yaml) as a
9
+ # trusted publisher for the project, with the environment name "pypi".
10
+
11
+ on:
12
+ push:
13
+ tags:
14
+ - "[0-9]+.[0-9]+.[0-9]+*"
15
+
16
+ jobs:
17
+ build:
18
+ name: Build distributions
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ # hatch-vcs reads the version from tags, so it needs the full history.
24
+ fetch-depth: 0
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v5
28
+
29
+ - name: Build
30
+ run: uv build
31
+
32
+ - uses: actions/upload-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ publish:
38
+ name: Publish to PyPI
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment: pypi
42
+ permissions:
43
+ id-token: write # required for PyPI trusted publishing
44
+ steps:
45
+ - uses: actions/download-artifact@v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+
50
+ - uses: pypa/gh-action-pypi-publish@release/v1
dploy-0.2.0/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ # python files
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+
6
+ # pyinstaller files
7
+ dist/
8
+ build/
9
+ *.spec
10
+
11
+ # pytest-cov files
12
+ .coverage
13
+ .pytest_cache/
14
+
15
+ # git worktrees
16
+ .worktree/
dploy-0.2.0/AGENTS.md ADDED
@@ -0,0 +1,21 @@
1
+ # dploy
2
+
3
+ A command-line stow/link/unstow utility for managing dotfiles and other
4
+ symlinked file trees.
5
+
6
+ ## Agent skills
7
+
8
+ ### Issue tracker
9
+
10
+ Issues live as GitHub issues in `arecarn/dploy`, managed with the `gh` CLI. See
11
+ `docs/agents/issue-tracker.md`.
12
+
13
+ ### Triage labels
14
+
15
+ Default canonical role names (`needs-triage`, `needs-info`, `ready-for-agent`,
16
+ `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
17
+
18
+ ### Domain docs
19
+
20
+ Single-context: `CONTEXT.md` and `docs/adr/` at the repo root (created lazily,
21
+ absence is not an error). See `docs/agents/domain.md`.
@@ -6,16 +6,90 @@ All changes documented here should be written with the goal give the end-user
6
6
  (not other developers) a summarized view of the changes since the last release,
7
7
  organized by order of importance.
8
8
 
9
- ## [Unreleased] - YYYY-MM-DD
9
+ ## [0.2.0] - 2026-11-14
10
10
 
11
11
  ### Added
12
12
 
13
+ - Added a `--skip-conflicts` flag to `stow` that links everything that
14
+ doesn't conflict with an existing, unmanaged file instead of aborting the
15
+ whole command. Dploy exits with status `2` when some files were skipped.
16
+ - Added a `--no-folding` flag to `stow` and `unstow`, matching GNU Stow's
17
+ flag of the same name, that keeps each file as its own individual link
18
+ instead of folding a destination directory into one link.
19
+ - Added a `--dotfiles` flag to `stow` and `unstow`, matching GNU Stow's flag
20
+ of the same name: a package entry named `dot-foo` is linked as `.foo`.
21
+ `unstow` removes links by what they point at, so it does not need the flag.
22
+
13
23
  ### Changed
14
24
 
25
+ - **Breaking:** Renamed `source`/`sources`/`dest` to `package`/`packages`/
26
+ `destination` across the public API and the CLI, to match the terms
27
+ `CONTEXT.md` defines. This affects the `stow`, `unstow`, and `clean`
28
+ function parameters and CLI arguments. The `link` command's argument stays
29
+ `source`, since it takes a single file or directory, not a package.
30
+ - Switch to uv for dependency management instead of Poetry.
31
+ - Use hatchling as the build system.
32
+ - Switch CI from Travis CI and AppVeyor to GitHub Actions.
33
+ - Move Pylint configuration to pyproject.toml and remove in-code suppressions.
34
+
15
35
  ### Fixed
16
36
 
37
+ - On Windows, a link to a directory is now created as a directory symlink
38
+ instead of a file symlink, which did not work.
39
+ - On Windows, existing links are now recognized as pointing at their
40
+ package. Windows reports link targets with a `\\?\` prefix, which made
41
+ every comparison with the package path fail.
42
+ - On Windows, dploy now decides whether two paths are the same file by asking
43
+ the file system instead of comparing path text, so a different spelling of
44
+ one path (letter case, short names) no longer counts as a different file.
45
+ - An ignore pattern the file system rejects, which Windows does for some
46
+ patterns POSIX accepts, is now skipped instead of crashing stow, unstow
47
+ and clean.
48
+ - On Windows, permission checks now read the file's ACL instead of only the
49
+ read-only flag, so an unreadable or unwritable path is reported before
50
+ anything changes rather than raising `PermissionError` part way through.
51
+ - Blank lines in `.dploystowignore`, or an empty `--ignore` pattern, no
52
+ longer crash stow/unstow/clean.
53
+ - Stowing a second package that shares a directory two or more levels deep
54
+ with an already-stowed package no longer writes into the first package's
55
+ source tree.
56
+ - Recursive ignore patterns, like `**/*.swp`, no longer cause dploy to skip
57
+ the whole folder a matching file lives in, including when that folder is
58
+ shared with another package being stowed.
59
+ - `unstow` no longer leaves behind an empty directory when all of its
60
+ contents are removed within the same run, for example a subdirectory
61
+ cleared while its parent directory only contained that one entry.
62
+ - `clean` now removes dangling links. It previously removed nothing, because
63
+ it could not match a link to its package unless the package sat directly in
64
+ the working directory and the link's target had no `..` in it. An unreadable
65
+ directory is now reported as an error instead of crashing.
66
+ - `link` now reports a clean error when the destination's parent is a file
67
+ instead of a directory. It previously crashed with a traceback.
68
+
69
+ ## [0.1.3] - 2026-02-13
70
+
71
+ ### Added
72
+
73
+ - Added a `dploy clean` subcommand.
74
+
75
+ ### Changed
76
+
77
+ - Switch to Poetry for packaging instead of setuptools.
78
+ - Require at least Python >= 3.10.
79
+ - Use black for auto formatting instead of yapf.
80
+ - Use custom *DployError* exceptions.
81
+ - Version is now sourced from pyproject.toml via importlib.metadata.
82
+
83
+ ### Fixed
84
+
85
+ - Print syntax help if no valid subcommand was provided.
86
+ - Fixed pytest.
87
+ - Fixed invoke tasks to work with Poetry and Python 3.13.
88
+
17
89
  ### Removed
18
90
 
91
+ - Removed requirements.txt in favor of pyproject.toml dependency groups.
92
+
19
93
  ## [0.1.2] - 2017-10-26
20
94
 
21
95
  ### Changed
@@ -43,8 +117,6 @@ organized by order of importance.
43
117
  Since the symbolic links can't use a relative path fallback to the absolute
44
118
  version of the path passed in.
45
119
 
46
- ### Removed
47
-
48
120
  ## [0.1.1] - 2016-12-29
49
121
 
50
122
  ### Fix
dploy-0.2.0/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
dploy-0.2.0/CONTEXT.md ADDED
@@ -0,0 +1,105 @@
1
+ # dploy
2
+
3
+ dploy creates and removes symbolic links so that the contents of one directory
4
+ appear inside another, in the manner of GNU Stow, on Windows, Linux, and macOS.
5
+
6
+ ## Language
7
+
8
+ ### What gets linked
9
+
10
+ **Package**:
11
+ A directory whose contents are linked into a destination as a unit. Packages are
12
+ always named explicitly on the command line; dploy has no directory it searches
13
+ for them.
14
+ _Avoid_: source, source directory, stow package
15
+
16
+ **Destination**:
17
+ The directory a package's contents are linked into.
18
+ _Avoid_: target, target directory, dest
19
+
20
+ **Target**:
21
+ The path a symbolic link points at. A link's target is what it refers to, never
22
+ where it lives.
23
+ _Avoid_: referent, linked-to path
24
+
25
+ **Dangling link**:
26
+ A symbolic link whose target no longer exists.
27
+ _Avoid_: broken link, orphaned link, stale link
28
+
29
+ ### Dot-file names
30
+
31
+ **Dotfile translation**:
32
+ With `--dotfiles`, linking a package entry named `dot-foo` under the destination
33
+ name `.foo`. A name where `dot-` is followed by nothing or by a dot is never
34
+ translated.
35
+ _Avoid_: dot renaming, dot expansion
36
+
37
+ **Literal name**:
38
+ A package entry's own name, such as `dot-foo`.
39
+ _Avoid_: original name, source name
40
+
41
+ **Translated name**:
42
+ The name dotfile translation gives an entry, such as `.foo`. For an entry that is
43
+ not translated, it is the same as the literal name.
44
+ _Avoid_: dot name, renamed name
45
+
46
+ **Matching by target**:
47
+ How unstow finds the links to remove: a link is removed because its target is
48
+ inside the package, whichever of the entry's two names it carries. A file or link
49
+ that does not point into the package is never touched.
50
+ _Avoid_: name matching, flag matching
51
+
52
+ ### Operations
53
+
54
+ **Stow**:
55
+ Linking the contents of a package into a destination. Stowing is idempotent:
56
+ stowing an already-stowed package reports the links as already in place.
57
+ _Avoid_: install, deploy, apply
58
+
59
+ **Unstow**:
60
+ Removing the links a stow produced, recognised by their target rather than by
61
+ their name. Also idempotent.
62
+ _Avoid_: uninstall, remove, undeploy
63
+
64
+ **Clean**:
65
+ Removing dangling links in a destination that point into a given package. Unlike
66
+ unstow, it acts on links whose targets are already gone.
67
+ _Avoid_: prune, sweep, garbage collect
68
+
69
+ **Link**:
70
+ Creating a single symbolic link at an exact path, rather than linking a whole
71
+ package's contents. Where the distinction matters, say "the link command" for
72
+ the operation and "a symbolic link" for the thing.
73
+ _Avoid_: symlink (as the name of the operation)
74
+
75
+ **Folding**:
76
+ Replacing a destination directory whose links all point into one package
77
+ directory with a single link to that directory.
78
+ _Avoid_: collapsing, compacting
79
+
80
+ **Unfolding**:
81
+ Replacing a link to a directory with a real directory containing links to that
82
+ directory's contents, so a second package can contribute entries alongside the
83
+ first.
84
+ _Avoid_: expanding, exploding
85
+
86
+ ### How work is described
87
+
88
+ **Action**:
89
+ A single filesystem change dploy intends to make: creating a link, removing a
90
+ link, creating a directory, removing a directory. Actions are collected and
91
+ validated as a set before any of them run, so a command either applies fully or
92
+ not at all.
93
+ _Avoid_: operation, step, task, change
94
+
95
+ **Conflict**:
96
+ A reason a collected set of actions cannot be applied, such as a destination
97
+ entry that is not a link dploy created, or two packages claiming the same
98
+ destination path. Conflicts are reported together and abort the command before
99
+ any action runs.
100
+ _Avoid_: error, clash, collision
101
+
102
+ **Ignore pattern**:
103
+ A glob that excludes matching entries of a package from being linked. Patterns
104
+ come from the command line or from a `.dploystowignore` file beside the package.
105
+ _Avoid_: exclude pattern, skip pattern, filter
dploy-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.5
2
+ Name: dploy
3
+ Version: 0.2.0
4
+ Summary: Provides functionality similar to GNU Stow as a cross platform CLI tool and Python 3 module
5
+ Project-URL: Homepage, https://github.com/arecarn/dploy
6
+ Author-email: Ryan Carney <arecarn@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE.txt
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Dploy
22
+
23
+ Dploy is a tool for creating symbolic links similarly to [GNU
24
+ Stow](https://www.gnu.org/software/stow/). It is provided as a CLI tool and
25
+ Python 3.10+ module and supports Windows, Linux, and OSX.
26
+
27
+ Dploy's command `stow` creates symbolic links to the contents of packages in a
28
+ specified destination directory. Repeating the
29
+ `stow` command with the same arguments will confirm that the contents of the
30
+ package have been symbolically linked.
31
+
32
+ Dploy's command `unstow` removes symbolic links that resulted from `stow`
33
+ commands. Repeating the `unstow` command with the same arguments will confirm
34
+ that the links to stowed packages have been removed.
35
+
36
+ ## Installation
37
+
38
+ * Latest Release: `pip install dploy`
39
+ * Development Version: `pip install git+https://github.com/arecarn/dploy.git`
40
+
41
+ ## Basic CLI Usage
42
+
43
+ * `dploy stow <package-directory>... <destination-directory>`
44
+ * `dploy unstow <package-directory>... <destination-directory>`
45
+ * `dploy clean <package-directory>... <destination-directory>`
46
+ * `dploy --help`
47
+
48
+ ### Dot-file names
49
+
50
+ `stow --dotfiles` stows a package entry named `dot-something` as a destination
51
+ entry named `.something`:
52
+
53
+ ```
54
+ dploy stow --dotfiles ~/dotfiles/bash ~
55
+ # ~/dotfiles/bash/dot-bashrc -> ~/.bashrc
56
+ ```
57
+
58
+ This keeps the files visible in the package directory while they land as
59
+ dot-files in the destination.
60
+
61
+ `unstow` does not need `--dotfiles`. It finds links by what they point at, so it
62
+ removes them whether they are named `dot-something` or `.something`. It still
63
+ accepts the flag, for symmetry with `stow`; with it, an unrelated file already
64
+ named `.something` is reported as a conflict. `clean` is unaffected for the
65
+ same reason.
66
+
67
+ An entry whose name is `dot-` followed by nothing or by a dot (`dot-`,
68
+ `dot-.`, `dot-..`, `dot-.hidden`) is left untranslated, as GNU Stow does; the
69
+ first two would otherwise resolve to the destination directory or its parent.
70
+
71
+ ## Rationale
72
+
73
+ Dploy started out as simple Python script to create symbolic links to my
74
+ dotfiles for Windows, Mac, and Linux. Over time I keep improving and tweaking my
75
+ script to suit my needs, but I was running into a problem. Keeping all the
76
+ files I wanted to link in a config file was becoming a real pain in the neck.
77
+
78
+ I started looking for another solution to solve my problem, and found many
79
+ alternatives but none of them seemed to be a good fit. The solution that seemed
80
+ the most promising was using GNU Stow. It seemed like the most simple elegant
81
+ solution to the problem. The only issue was that it didn't support Windows.
82
+
83
+ Then I thought to myself, why can't I just create my own version of Stow that
84
+ work on Windows, Linux and OSX. So after that my I started morphing
85
+ simple python script into what would become Dploy and learned a lot more about
86
+ python in the process.
87
+
88
+ ## How does it compare with GNU Stow?
89
+
90
+ Below are just a few few major points of comparison between GNU stow and Dploy.
91
+
92
+ * Like GNU Stow Dploy runs in two passes. First by collecting the actions
93
+ required to complete the command and verifying that the command can
94
+ completed without any issues. If no issues are detected then the second
95
+ pass executes these actions are execute to complete the command. Otherwise
96
+ Dploy will exit and indicate why the command can not be completed. This way a
97
+ stow or unstow operation is atomic and never partially done.
98
+
99
+ * Like Stow, Dploy supports tree folding and tree unfolding. Like Stow's
100
+ `--no-folding`, passing `--no-folding` to `stow` or `unstow` disables
101
+ folding, so each file gets its own link inside a real directory.
102
+
103
+ * Unlike Stow, Dploy requires an explicit package(s) and a destination
104
+ directory.
105
+
106
+ * Unlike Stow, Dploy does not have any concept of ownership, but will only
107
+ operate on symbolic links and the creation or removal of directories for these
108
+ symbolic links.
109
+
110
+ * Unlike Stow, Dploy's `unstow` removes a destination directory that becomes
111
+ empty as a result of removing its symbolic links, even if that directory
112
+ existed before `stow` created any links inside it. Dploy keeps no state
113
+ across invocations, so it cannot distinguish a directory it created from
114
+ one that already existed.
115
+
116
+ * `stow`'s `--skip-conflicts` flag is the one opt-in exception to the
117
+ atomicity described above: files that conflict with an existing,
118
+ unmanaged destination entry are skipped and reported, while every other
119
+ file in the package is still linked. `dploy` exits with status `2` when
120
+ this happens, so a script can tell a partial stow apart from a full
121
+ success (status `0`) or a fatal error (status `1`).
dploy-0.2.0/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Dploy
2
+
3
+ Dploy is a tool for creating symbolic links similarly to [GNU
4
+ Stow](https://www.gnu.org/software/stow/). It is provided as a CLI tool and
5
+ Python 3.10+ module and supports Windows, Linux, and OSX.
6
+
7
+ Dploy's command `stow` creates symbolic links to the contents of packages in a
8
+ specified destination directory. Repeating the
9
+ `stow` command with the same arguments will confirm that the contents of the
10
+ package have been symbolically linked.
11
+
12
+ Dploy's command `unstow` removes symbolic links that resulted from `stow`
13
+ commands. Repeating the `unstow` command with the same arguments will confirm
14
+ that the links to stowed packages have been removed.
15
+
16
+ ## Installation
17
+
18
+ * Latest Release: `pip install dploy`
19
+ * Development Version: `pip install git+https://github.com/arecarn/dploy.git`
20
+
21
+ ## Basic CLI Usage
22
+
23
+ * `dploy stow <package-directory>... <destination-directory>`
24
+ * `dploy unstow <package-directory>... <destination-directory>`
25
+ * `dploy clean <package-directory>... <destination-directory>`
26
+ * `dploy --help`
27
+
28
+ ### Dot-file names
29
+
30
+ `stow --dotfiles` stows a package entry named `dot-something` as a destination
31
+ entry named `.something`:
32
+
33
+ ```
34
+ dploy stow --dotfiles ~/dotfiles/bash ~
35
+ # ~/dotfiles/bash/dot-bashrc -> ~/.bashrc
36
+ ```
37
+
38
+ This keeps the files visible in the package directory while they land as
39
+ dot-files in the destination.
40
+
41
+ `unstow` does not need `--dotfiles`. It finds links by what they point at, so it
42
+ removes them whether they are named `dot-something` or `.something`. It still
43
+ accepts the flag, for symmetry with `stow`; with it, an unrelated file already
44
+ named `.something` is reported as a conflict. `clean` is unaffected for the
45
+ same reason.
46
+
47
+ An entry whose name is `dot-` followed by nothing or by a dot (`dot-`,
48
+ `dot-.`, `dot-..`, `dot-.hidden`) is left untranslated, as GNU Stow does; the
49
+ first two would otherwise resolve to the destination directory or its parent.
50
+
51
+ ## Rationale
52
+
53
+ Dploy started out as simple Python script to create symbolic links to my
54
+ dotfiles for Windows, Mac, and Linux. Over time I keep improving and tweaking my
55
+ script to suit my needs, but I was running into a problem. Keeping all the
56
+ files I wanted to link in a config file was becoming a real pain in the neck.
57
+
58
+ I started looking for another solution to solve my problem, and found many
59
+ alternatives but none of them seemed to be a good fit. The solution that seemed
60
+ the most promising was using GNU Stow. It seemed like the most simple elegant
61
+ solution to the problem. The only issue was that it didn't support Windows.
62
+
63
+ Then I thought to myself, why can't I just create my own version of Stow that
64
+ work on Windows, Linux and OSX. So after that my I started morphing
65
+ simple python script into what would become Dploy and learned a lot more about
66
+ python in the process.
67
+
68
+ ## How does it compare with GNU Stow?
69
+
70
+ Below are just a few few major points of comparison between GNU stow and Dploy.
71
+
72
+ * Like GNU Stow Dploy runs in two passes. First by collecting the actions
73
+ required to complete the command and verifying that the command can
74
+ completed without any issues. If no issues are detected then the second
75
+ pass executes these actions are execute to complete the command. Otherwise
76
+ Dploy will exit and indicate why the command can not be completed. This way a
77
+ stow or unstow operation is atomic and never partially done.
78
+
79
+ * Like Stow, Dploy supports tree folding and tree unfolding. Like Stow's
80
+ `--no-folding`, passing `--no-folding` to `stow` or `unstow` disables
81
+ folding, so each file gets its own link inside a real directory.
82
+
83
+ * Unlike Stow, Dploy requires an explicit package(s) and a destination
84
+ directory.
85
+
86
+ * Unlike Stow, Dploy does not have any concept of ownership, but will only
87
+ operate on symbolic links and the creation or removal of directories for these
88
+ symbolic links.
89
+
90
+ * Unlike Stow, Dploy's `unstow` removes a destination directory that becomes
91
+ empty as a result of removing its symbolic links, even if that directory
92
+ existed before `stow` created any links inside it. Dploy keeps no state
93
+ across invocations, so it cannot distinguish a directory it created from
94
+ one that already existed.
95
+
96
+ * `stow`'s `--skip-conflicts` flag is the one opt-in exception to the
97
+ atomicity described above: files that conflict with an existing,
98
+ unmanaged destination entry are skipped and reported, while every other
99
+ file in the package is still linked. `dploy` exits with status `2` when
100
+ this happens, so a script can tell a partial stow apart from a full
101
+ success (status `0`) or a fatal error (status `1`).
@@ -0,0 +1,67 @@
1
+ # Validate permissions on Windows with an ACL check, not os.access
2
+
3
+ Status: accepted (issue #28)
4
+
5
+ ## Context
6
+
7
+ dploy runs in two passes. The collect pass validates the whole command and the
8
+ execute pass applies it, so a command either completes or changes nothing. The
9
+ README states this as a reason to use dploy over GNU Stow.
10
+
11
+ The collect pass checks permissions with `os.access`. On Windows that call only
12
+ reflects the read-only attribute and ignores ACLs, so a directory the user
13
+ cannot read passes validation and raises `PermissionError` during execution,
14
+ after actions have started applying.
15
+
16
+ The guarantee is cross-platform. A check that cannot be made honest on one
17
+ platform narrows the guarantee there instead of being quietly excepted.
18
+
19
+ ## Decision
20
+
21
+ On Windows the five permission helpers in `dploy/utils.py` ask the OS the
22
+ question it asks when a path is opened: Win32 `AccessCheck` over the path's
23
+ security descriptor, using the current user's token. POSIX keeps `os.access`.
24
+
25
+ The Win32 calls are made through `ctypes` in `dploy/_winaccess.py`, so dploy
26
+ keeps no runtime dependencies.
27
+
28
+ ## Options considered
29
+
30
+ - **ACL check via `pywin32`.** Rejected: `pywin32` does not expose
31
+ `AccessCheck`. Its `GetEffectiveRightsFromAcl` ignores privileges and
32
+ logon-session groups, and fails on an ACL with an inherited deny entry, which
33
+ is the common case.
34
+ - **Try the operation and handle the failure.** Rejected: the collect pass
35
+ exists so that nothing is touched before the command is known to succeed.
36
+ - **Probe with a cheap reversible access attempt.** Rejected: it makes the
37
+ collect pass write to the filesystem, and still races with the execute pass.
38
+ - **Accept and document the gap.** Rejected: it makes the atomicity claim
39
+ conditional on the platform.
40
+
41
+ ## Consequences
42
+
43
+ - The Windows permission code is one `ctypes` module that only `windows-latest`
44
+ CI exercises. Wrong struct layouts fail there, not on Linux. Tests in
45
+ `tests/test_utils.py` deny access with real ACL entries through `icacls`.
46
+ - The check is a point-in-time answer. A permission changed between the two
47
+ passes can still fail execution, on every platform.
48
+ - A path that cannot be examined (missing, bad name, no ACL support) counts as
49
+ not accessible, as with `os.access`. Failures of the token or `AccessCheck`
50
+ calls raise `OSError`, because the answer is then unknown rather than "no".
51
+ - A writable directory needs both the add-file and the add-subdirectory right,
52
+ because stow creates files, directories and directory links. Removing an
53
+ entry is not checked: it depends on `DELETE` on the entry itself, which the
54
+ parent's ACL does not show.
55
+ - The execute (traverse) check is stricter than the OS. Users hold the
56
+ "bypass traverse checking" privilege, which `AccessCheck` does not model, so
57
+ the OS reaches through a directory whose traverse right is denied. dploy
58
+ refuses it instead, because the ACL says no. A refusal in the collect pass
59
+ never breaks the all-or-nothing promise; only a failure in the execute pass
60
+ would.
61
+ - The permission tests run on Windows. Their `remove_*_permission` helpers set
62
+ a deny entry with `icacls` there, and teardown clears them first. Three
63
+ tests stay skipped because they need the OS itself to refuse traversal,
64
+ which the bypass above prevents (`skip_on_windows_traverse_bypass`).
65
+ - Each check opens and duplicates a process token. This is not cached because
66
+ the cost is small next to the file system calls; cache it if profiling says
67
+ otherwise.