pyselfupdate 0.4.0__tar.gz → 0.5.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.
- pyselfupdate-0.5.0/LICENSE +21 -0
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/PKG-INFO +38 -13
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/README.md +34 -9
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/pyproject.toml +69 -8
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/pyproject.toml.orig +30 -7
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/__init__.py +2 -0
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/errors.py +15 -2
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/github.py +59 -12
- pyselfupdate-0.5.0/src/pyselfupdate/install.py +426 -0
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/notifier.py +8 -8
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/state.py +4 -5
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/typercmd.py +3 -1
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/updater.py +38 -5
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/version.py +2 -2
- pyselfupdate-0.4.0/src/pyselfupdate/install.py +0 -195
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/config.py +0 -0
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/py.typed +0 -0
- {pyselfupdate-0.4.0 → pyselfupdate-0.5.0}/src/pyselfupdate/source.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Birch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyselfupdate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Self-update and update notification for Python CLIs installed with uv tool
|
|
5
5
|
Keywords: uv,cli,self-update,update-notifier,release
|
|
6
6
|
Author: Chris Birch
|
|
7
7
|
Author-email: Chris Birch <datapointchris@gmail.com>
|
|
8
|
-
License: MIT
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
9
10
|
Classifier: Development Status :: 4 - Beta
|
|
10
11
|
Classifier: Environment :: Console
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries
|
|
15
15
|
Classifier: Topic :: System :: Software Distribution
|
|
@@ -56,9 +56,9 @@ version exists, so it silently drifts. The usual fix drags an HTTP client, a
|
|
|
56
56
|
TOML parser and a version library into a tool that had none of them.
|
|
57
57
|
|
|
58
58
|
This package has zero runtime dependencies — `urllib` for the network,
|
|
59
|
-
`tomllib` for uv's receipt, and its own semver implementation — and
|
|
60
|
-
that by importing every module into a virtual environment
|
|
61
|
-
else.
|
|
59
|
+
`tomllib` for uv's receipt and lock, and its own semver implementation — and
|
|
60
|
+
CI enforces that by importing every module into a virtual environment
|
|
61
|
+
containing nothing else.
|
|
62
62
|
|
|
63
63
|
## notify
|
|
64
64
|
|
|
@@ -128,7 +128,8 @@ environment the running interpreter lives in**. Unlike replacing a Unix binary
|
|
|
128
128
|
from under a live process, so anything imported afterwards may fail in ways
|
|
129
129
|
that are hard to read. Make it the last thing your process does, then call
|
|
130
130
|
`exit_now` — or use `update_and_reexec` to replace the process with the new
|
|
131
|
-
version immediately.
|
|
131
|
+
version immediately. Its caller never sees the `Result`, so it writes
|
|
132
|
+
`lock_warning` to stderr itself before the re-exec.
|
|
132
133
|
|
|
133
134
|
That cuts both ways: anything you want to *print* after the install has to be
|
|
134
135
|
fetched before it. `run_update` resolves its changelog first for exactly this
|
|
@@ -139,11 +140,34 @@ pieces `update` is made of rather than working around it:
|
|
|
139
140
|
installation = require_updatable(config) # refuses a checkout, costs nothing
|
|
140
141
|
result = check(config) # network, environment still intact
|
|
141
142
|
notes = changelog(config, result.current, result.latest)
|
|
142
|
-
install_release(config, result, installation)
|
|
143
|
+
installed = install_release(config, result, installation)
|
|
143
144
|
print(notes)
|
|
145
|
+
if installed.lock_warning:
|
|
146
|
+
print(installed.lock_warning, file=sys.stderr)
|
|
144
147
|
exit_now()
|
|
145
148
|
```
|
|
146
149
|
|
|
150
|
+
### A git install is held to the release's `uv.lock`
|
|
151
|
+
|
|
152
|
+
`uv tool install` never reads a lock. Handed a git requirement, it resolves
|
|
153
|
+
every dependency to the newest version, so a tool would run on a set its CI
|
|
154
|
+
never tested. `update` therefore clones the release's tag, exports its
|
|
155
|
+
`uv.lock`, and passes registry pins as `--constraints` and git pins as
|
|
156
|
+
`--overrides`. uv records both in the tool's receipt, so a later
|
|
157
|
+
`uv tool upgrade` stays held to them.
|
|
158
|
+
|
|
159
|
+
| The tag | What `update` does |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| Carries a `uv.lock` | Installs the locked versions |
|
|
162
|
+
| Carries none | Installs unlocked and sets `lock_missing`. `lock_warning` names the tag, and `run_update` and `update_and_reexec` print it |
|
|
163
|
+
| Will not clone, or uv will not export its lock | Raises `LockUnreadableError` and installs nothing |
|
|
164
|
+
|
|
165
|
+
An install from an index reads no lock, because a wheel carries none.
|
|
166
|
+
|
|
167
|
+
An update reinstalls the tool's own extras, as in `mytool[fast]`, with their
|
|
168
|
+
dependencies held to the lock. Every `--with` and `--with-editable`
|
|
169
|
+
requirement is passed back to uv as well.
|
|
170
|
+
|
|
147
171
|
## What will not be updated
|
|
148
172
|
|
|
149
173
|
Read from uv's own receipt, written at install time, rather than guessed at
|
|
@@ -155,6 +179,7 @@ runtime:
|
|
|
155
179
|
| `name = "mytool"` (from an index) | Updatable |
|
|
156
180
|
| `git = "...git"` with no `rev` | Refused — tracks a branch, so its version says nothing about how far behind it is |
|
|
157
181
|
| `directory` / `path` / `editable` | Refused — reinstalling would discard a working copy |
|
|
182
|
+
| A `--with` requirement no requirements-file line reproduces, such as a relative path | Refused with `NotInstalledError` — the update would drop it |
|
|
158
183
|
|
|
159
184
|
A tool that cannot be identified at all is treated as local and left alone.
|
|
160
185
|
|
|
@@ -206,8 +231,8 @@ It never raises. A command that is not installed, exits non-zero, or takes
|
|
|
206
231
|
longer than ten seconds degrades to an unauthenticated request, which still
|
|
207
232
|
works against a public repository.
|
|
208
233
|
|
|
209
|
-
`token_func` is
|
|
210
|
-
|
|
234
|
+
`token_func` is for a credential neither the environment nor a command can
|
|
235
|
+
produce. It is called lazily, for the same reason the command is: the notify
|
|
211
236
|
gate resolves a `Config` on every invocation and declines most of them without
|
|
212
237
|
reaching the network, and a subprocess in front of that gate is the entire cost
|
|
213
238
|
worth avoiding.
|
|
@@ -234,7 +259,7 @@ atomically:
|
|
|
234
259
|
```
|
|
235
260
|
|
|
236
261
|
State, not config and not cache: it persists across runs, it is not authored by
|
|
237
|
-
the user, and deleting it changes
|
|
262
|
+
the user, and deleting it changes behavior rather than merely costing a
|
|
238
263
|
recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
|
|
239
264
|
is where `gh` puts the same thing.
|
|
240
265
|
|
|
@@ -263,6 +288,6 @@ names it, and the `NO_AUTO_UPDATE` contract. They do not share an API, because
|
|
|
263
288
|
Version precedence is deliberately identical across all three, so a tool and
|
|
264
289
|
its siblings never disagree about which release is newer.
|
|
265
290
|
|
|
266
|
-
##
|
|
291
|
+
## License
|
|
267
292
|
|
|
268
293
|
MIT
|
|
@@ -32,9 +32,9 @@ version exists, so it silently drifts. The usual fix drags an HTTP client, a
|
|
|
32
32
|
TOML parser and a version library into a tool that had none of them.
|
|
33
33
|
|
|
34
34
|
This package has zero runtime dependencies — `urllib` for the network,
|
|
35
|
-
`tomllib` for uv's receipt, and its own semver implementation — and
|
|
36
|
-
that by importing every module into a virtual environment
|
|
37
|
-
else.
|
|
35
|
+
`tomllib` for uv's receipt and lock, and its own semver implementation — and
|
|
36
|
+
CI enforces that by importing every module into a virtual environment
|
|
37
|
+
containing nothing else.
|
|
38
38
|
|
|
39
39
|
## notify
|
|
40
40
|
|
|
@@ -104,7 +104,8 @@ environment the running interpreter lives in**. Unlike replacing a Unix binary
|
|
|
104
104
|
from under a live process, so anything imported afterwards may fail in ways
|
|
105
105
|
that are hard to read. Make it the last thing your process does, then call
|
|
106
106
|
`exit_now` — or use `update_and_reexec` to replace the process with the new
|
|
107
|
-
version immediately.
|
|
107
|
+
version immediately. Its caller never sees the `Result`, so it writes
|
|
108
|
+
`lock_warning` to stderr itself before the re-exec.
|
|
108
109
|
|
|
109
110
|
That cuts both ways: anything you want to *print* after the install has to be
|
|
110
111
|
fetched before it. `run_update` resolves its changelog first for exactly this
|
|
@@ -115,11 +116,34 @@ pieces `update` is made of rather than working around it:
|
|
|
115
116
|
installation = require_updatable(config) # refuses a checkout, costs nothing
|
|
116
117
|
result = check(config) # network, environment still intact
|
|
117
118
|
notes = changelog(config, result.current, result.latest)
|
|
118
|
-
install_release(config, result, installation)
|
|
119
|
+
installed = install_release(config, result, installation)
|
|
119
120
|
print(notes)
|
|
121
|
+
if installed.lock_warning:
|
|
122
|
+
print(installed.lock_warning, file=sys.stderr)
|
|
120
123
|
exit_now()
|
|
121
124
|
```
|
|
122
125
|
|
|
126
|
+
### A git install is held to the release's `uv.lock`
|
|
127
|
+
|
|
128
|
+
`uv tool install` never reads a lock. Handed a git requirement, it resolves
|
|
129
|
+
every dependency to the newest version, so a tool would run on a set its CI
|
|
130
|
+
never tested. `update` therefore clones the release's tag, exports its
|
|
131
|
+
`uv.lock`, and passes registry pins as `--constraints` and git pins as
|
|
132
|
+
`--overrides`. uv records both in the tool's receipt, so a later
|
|
133
|
+
`uv tool upgrade` stays held to them.
|
|
134
|
+
|
|
135
|
+
| The tag | What `update` does |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| Carries a `uv.lock` | Installs the locked versions |
|
|
138
|
+
| Carries none | Installs unlocked and sets `lock_missing`. `lock_warning` names the tag, and `run_update` and `update_and_reexec` print it |
|
|
139
|
+
| Will not clone, or uv will not export its lock | Raises `LockUnreadableError` and installs nothing |
|
|
140
|
+
|
|
141
|
+
An install from an index reads no lock, because a wheel carries none.
|
|
142
|
+
|
|
143
|
+
An update reinstalls the tool's own extras, as in `mytool[fast]`, with their
|
|
144
|
+
dependencies held to the lock. Every `--with` and `--with-editable`
|
|
145
|
+
requirement is passed back to uv as well.
|
|
146
|
+
|
|
123
147
|
## What will not be updated
|
|
124
148
|
|
|
125
149
|
Read from uv's own receipt, written at install time, rather than guessed at
|
|
@@ -131,6 +155,7 @@ runtime:
|
|
|
131
155
|
| `name = "mytool"` (from an index) | Updatable |
|
|
132
156
|
| `git = "...git"` with no `rev` | Refused — tracks a branch, so its version says nothing about how far behind it is |
|
|
133
157
|
| `directory` / `path` / `editable` | Refused — reinstalling would discard a working copy |
|
|
158
|
+
| A `--with` requirement no requirements-file line reproduces, such as a relative path | Refused with `NotInstalledError` — the update would drop it |
|
|
134
159
|
|
|
135
160
|
A tool that cannot be identified at all is treated as local and left alone.
|
|
136
161
|
|
|
@@ -182,8 +207,8 @@ It never raises. A command that is not installed, exits non-zero, or takes
|
|
|
182
207
|
longer than ten seconds degrades to an unauthenticated request, which still
|
|
183
208
|
works against a public repository.
|
|
184
209
|
|
|
185
|
-
`token_func` is
|
|
186
|
-
|
|
210
|
+
`token_func` is for a credential neither the environment nor a command can
|
|
211
|
+
produce. It is called lazily, for the same reason the command is: the notify
|
|
187
212
|
gate resolves a `Config` on every invocation and declines most of them without
|
|
188
213
|
reaching the network, and a subprocess in front of that gate is the entire cost
|
|
189
214
|
worth avoiding.
|
|
@@ -210,7 +235,7 @@ atomically:
|
|
|
210
235
|
```
|
|
211
236
|
|
|
212
237
|
State, not config and not cache: it persists across runs, it is not authored by
|
|
213
|
-
the user, and deleting it changes
|
|
238
|
+
the user, and deleting it changes behavior rather than merely costing a
|
|
214
239
|
recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
|
|
215
240
|
is where `gh` puts the same thing.
|
|
216
241
|
|
|
@@ -239,6 +264,6 @@ names it, and the `NO_AUTO_UPDATE` contract. They do not share an API, because
|
|
|
239
264
|
Version precedence is deliberately identical across all three, so a tool and
|
|
240
265
|
its siblings never disagree about which release is newer.
|
|
241
266
|
|
|
242
|
-
##
|
|
267
|
+
## License
|
|
243
268
|
|
|
244
269
|
MIT
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyselfupdate"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Self-update and update notification for Python CLIs installed with uv tool"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
5
7
|
readme = "README.md"
|
|
6
8
|
keywords = [
|
|
7
9
|
"uv",
|
|
@@ -14,7 +16,6 @@ classifiers = [
|
|
|
14
16
|
"Development Status :: 4 - Beta",
|
|
15
17
|
"Environment :: Console",
|
|
16
18
|
"Intended Audience :: Developers",
|
|
17
|
-
"License :: OSI Approved :: MIT License",
|
|
18
19
|
"Programming Language :: Python :: 3 :: Only",
|
|
19
20
|
"Topic :: Software Development :: Libraries",
|
|
20
21
|
"Topic :: System :: Software Distribution",
|
|
@@ -27,9 +28,6 @@ dependencies = []
|
|
|
27
28
|
name = "Chris Birch"
|
|
28
29
|
email = "datapointchris@gmail.com"
|
|
29
30
|
|
|
30
|
-
[project.license]
|
|
31
|
-
text = "MIT"
|
|
32
|
-
|
|
33
31
|
[project.optional-dependencies]
|
|
34
32
|
typer = ["typer>=0.12.0"]
|
|
35
33
|
|
|
@@ -44,7 +42,7 @@ dev = [
|
|
|
44
42
|
"mypy>=1.10.0",
|
|
45
43
|
"pre-commit>=4.3.0",
|
|
46
44
|
"pytest>=8.0.0",
|
|
47
|
-
"ruff
|
|
45
|
+
"ruff==0.12.5",
|
|
48
46
|
"typer>=0.12.0",
|
|
49
47
|
]
|
|
50
48
|
|
|
@@ -66,6 +64,9 @@ skips = [
|
|
|
66
64
|
[tool.codespell]
|
|
67
65
|
skip = "*.lock"
|
|
68
66
|
check-filenames = true
|
|
67
|
+
check-hidden = true
|
|
68
|
+
builtin = "clear,rare"
|
|
69
|
+
uri-ignore-words-list = "*"
|
|
69
70
|
|
|
70
71
|
[tool.mypy]
|
|
71
72
|
pretty = true
|
|
@@ -109,6 +110,7 @@ select = [
|
|
|
109
110
|
"B",
|
|
110
111
|
"SIM",
|
|
111
112
|
"I",
|
|
113
|
+
"ICN",
|
|
112
114
|
]
|
|
113
115
|
ignore = ["SIM108"]
|
|
114
116
|
|
|
@@ -118,9 +120,21 @@ force-single-line = true
|
|
|
118
120
|
[tool.ruff.lint.per-file-ignores]
|
|
119
121
|
"__init__.py" = ["F401"]
|
|
120
122
|
|
|
123
|
+
[tool.ruff.lint.flake8-import-conventions]
|
|
124
|
+
banned-from = [
|
|
125
|
+
"datetime",
|
|
126
|
+
"pyspark.sql.functions",
|
|
127
|
+
"pyspark.sql.types",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
|
|
131
|
+
datetime = "dt"
|
|
132
|
+
sqlalchemy = "sa"
|
|
133
|
+
"pyspark.sql.functions" = "sf"
|
|
134
|
+
"pyspark.sql.types" = "st"
|
|
135
|
+
|
|
121
136
|
[tool.semantic_release]
|
|
122
137
|
version_toml = ["pyproject.toml:project.version"]
|
|
123
|
-
branch = "main"
|
|
124
138
|
commit_message = "build(release): {version}"
|
|
125
139
|
build_command = """
|
|
126
140
|
pip install uv
|
|
@@ -178,6 +192,40 @@ managed = [
|
|
|
178
192
|
"lint",
|
|
179
193
|
"isort",
|
|
180
194
|
"force-single-line",
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
"ruff",
|
|
198
|
+
"lint",
|
|
199
|
+
"flake8-import-conventions",
|
|
200
|
+
"banned-from",
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"ruff",
|
|
204
|
+
"lint",
|
|
205
|
+
"flake8-import-conventions",
|
|
206
|
+
"extend-aliases",
|
|
207
|
+
"datetime",
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
"ruff",
|
|
211
|
+
"lint",
|
|
212
|
+
"flake8-import-conventions",
|
|
213
|
+
"extend-aliases",
|
|
214
|
+
"sqlalchemy",
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
"ruff",
|
|
218
|
+
"lint",
|
|
219
|
+
"flake8-import-conventions",
|
|
220
|
+
"extend-aliases",
|
|
221
|
+
"pyspark.sql.functions",
|
|
222
|
+
],
|
|
223
|
+
[
|
|
224
|
+
"ruff",
|
|
225
|
+
"lint",
|
|
226
|
+
"flake8-import-conventions",
|
|
227
|
+
"extend-aliases",
|
|
228
|
+
"pyspark.sql.types",
|
|
181
229
|
],
|
|
182
230
|
[
|
|
183
231
|
"ruff",
|
|
@@ -212,6 +260,18 @@ managed = [
|
|
|
212
260
|
[
|
|
213
261
|
"codespell",
|
|
214
262
|
"check-filenames",
|
|
263
|
+
],
|
|
264
|
+
[
|
|
265
|
+
"codespell",
|
|
266
|
+
"check-hidden",
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
"codespell",
|
|
270
|
+
"builtin",
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"codespell",
|
|
274
|
+
"uri-ignore-words-list",
|
|
215
275
|
],
|
|
216
276
|
[
|
|
217
277
|
"pytest",
|
|
@@ -229,11 +289,12 @@ managed = [
|
|
|
229
289
|
"testpaths",
|
|
230
290
|
],
|
|
231
291
|
]
|
|
292
|
+
pinned = ["ruff"]
|
|
232
293
|
|
|
233
294
|
[tool.uv.build-backend]
|
|
234
295
|
module-name = "pyselfupdate"
|
|
235
296
|
module-root = "src"
|
|
236
297
|
|
|
237
298
|
[build-system]
|
|
238
|
-
requires = ["uv_build>=0.
|
|
299
|
+
requires = ["uv_build>=0.12.2,<0.13.0"]
|
|
239
300
|
build-backend = "uv_build"
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyselfupdate"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Self-update and update notification for Python CLIs installed with uv tool"
|
|
5
5
|
authors = [{ name = "Chris Birch", email = "datapointchris@gmail.com" }]
|
|
6
|
-
license =
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
7
8
|
readme = "README.md"
|
|
8
9
|
keywords = ["uv", "cli", "self-update", "update-notifier", "release"]
|
|
9
10
|
classifiers = [
|
|
10
11
|
"Development Status :: 4 - Beta",
|
|
11
12
|
"Environment :: Console",
|
|
12
13
|
"Intended Audience :: Developers",
|
|
13
|
-
"License :: OSI Approved :: MIT License",
|
|
14
14
|
"Programming Language :: Python :: 3 :: Only",
|
|
15
15
|
"Topic :: Software Development :: Libraries",
|
|
16
16
|
"Topic :: System :: Software Distribution",
|
|
@@ -42,7 +42,7 @@ dev = [
|
|
|
42
42
|
"mypy>=1.10.0",
|
|
43
43
|
"pre-commit>=4.3.0",
|
|
44
44
|
"pytest>=8.0.0",
|
|
45
|
-
"ruff
|
|
45
|
+
"ruff==0.12.5",
|
|
46
46
|
"typer>=0.12.0",
|
|
47
47
|
]
|
|
48
48
|
|
|
@@ -58,6 +58,9 @@ skips = ["B311", "B404", "B603"]
|
|
|
58
58
|
[tool.codespell]
|
|
59
59
|
skip = '*.lock'
|
|
60
60
|
check-filenames = true
|
|
61
|
+
check-hidden = true
|
|
62
|
+
builtin = "clear,rare"
|
|
63
|
+
uri-ignore-words-list = "*"
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
[tool.mypy]
|
|
@@ -108,6 +111,7 @@ select = [
|
|
|
108
111
|
"B", # flake8-bugbear
|
|
109
112
|
"SIM", # flake8-simplify
|
|
110
113
|
"I", # isort
|
|
114
|
+
"ICN", # flake8-import-conventions
|
|
111
115
|
]
|
|
112
116
|
ignore = [
|
|
113
117
|
"SIM108", # ternary operator — often less readable
|
|
@@ -119,9 +123,17 @@ force-single-line = true
|
|
|
119
123
|
[tool.ruff.lint.per-file-ignores]
|
|
120
124
|
"__init__.py" = ["F401"]
|
|
121
125
|
|
|
126
|
+
[tool.ruff.lint.flake8-import-conventions]
|
|
127
|
+
banned-from = ["datetime", "pyspark.sql.functions", "pyspark.sql.types"]
|
|
128
|
+
|
|
129
|
+
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
|
|
130
|
+
datetime = "dt"
|
|
131
|
+
sqlalchemy = "sa"
|
|
132
|
+
"pyspark.sql.functions" = "sf"
|
|
133
|
+
"pyspark.sql.types" = "st"
|
|
134
|
+
|
|
122
135
|
[tool.semantic_release]
|
|
123
136
|
version_toml = ["pyproject.toml:project.version"]
|
|
124
|
-
branch = "main"
|
|
125
137
|
commit_message = "build(release): {version}"
|
|
126
138
|
build_command = """
|
|
127
139
|
pip install uv
|
|
@@ -138,7 +150,7 @@ typeCheckingMode = "standard"
|
|
|
138
150
|
analyzeUnannotatedFunctions = true # or pylance stops analyzing in vscode
|
|
139
151
|
|
|
140
152
|
[tool.forge]
|
|
141
|
-
# Keys the standard owns, written by
|
|
153
|
+
# Keys the shared toolchain standard owns, written by its generator.
|
|
142
154
|
# Dropping one from the template removes it here on the next sync; a key absent
|
|
143
155
|
# from this list belongs to the project and is never touched. Do not hand-edit.
|
|
144
156
|
managed = [
|
|
@@ -151,6 +163,11 @@ managed = [
|
|
|
151
163
|
["ruff", "lint", "select"],
|
|
152
164
|
["ruff", "lint", "ignore"],
|
|
153
165
|
["ruff", "lint", "isort", "force-single-line"],
|
|
166
|
+
["ruff", "lint", "flake8-import-conventions", "banned-from"],
|
|
167
|
+
["ruff", "lint", "flake8-import-conventions", "extend-aliases", "datetime"],
|
|
168
|
+
["ruff", "lint", "flake8-import-conventions", "extend-aliases", "sqlalchemy"],
|
|
169
|
+
["ruff", "lint", "flake8-import-conventions", "extend-aliases", "pyspark.sql.functions"],
|
|
170
|
+
["ruff", "lint", "flake8-import-conventions", "extend-aliases", "pyspark.sql.types"],
|
|
154
171
|
["ruff", "lint", "per-file-ignores", "__init__.py"],
|
|
155
172
|
["mypy", "pretty"],
|
|
156
173
|
["mypy", "ignore_missing_imports"],
|
|
@@ -159,13 +176,19 @@ managed = [
|
|
|
159
176
|
["pyright", "typeCheckingMode"],
|
|
160
177
|
["pyright", "analyzeUnannotatedFunctions"],
|
|
161
178
|
["codespell", "check-filenames"],
|
|
179
|
+
["codespell", "check-hidden"],
|
|
180
|
+
["codespell", "builtin"],
|
|
181
|
+
["codespell", "uri-ignore-words-list"],
|
|
162
182
|
["pytest", "ini_options", "verbosity_assertions"],
|
|
163
183
|
["pytest", "ini_options", "minversion"],
|
|
164
184
|
["pytest", "ini_options", "testpaths"],
|
|
165
185
|
]
|
|
186
|
+
# Dependencies held at the version the pre-commit hook runs, in every group
|
|
187
|
+
# and extra that names them, so a local run and CI cannot disagree.
|
|
188
|
+
pinned = ["ruff"]
|
|
166
189
|
|
|
167
190
|
[build-system]
|
|
168
|
-
requires = ["uv_build>=0.
|
|
191
|
+
requires = ["uv_build>=0.12.2,<0.13.0"]
|
|
169
192
|
build-backend = "uv_build"
|
|
170
193
|
|
|
171
194
|
[tool.uv.build-backend]
|
|
@@ -21,6 +21,7 @@ from pyselfupdate.config import Config
|
|
|
21
21
|
from pyselfupdate.errors import InstallFailedError
|
|
22
22
|
from pyselfupdate.errors import InvalidConfigError
|
|
23
23
|
from pyselfupdate.errors import LocalInstallError
|
|
24
|
+
from pyselfupdate.errors import LockUnreadableError
|
|
24
25
|
from pyselfupdate.errors import NoReleaseError
|
|
25
26
|
from pyselfupdate.errors import NotInstalledError
|
|
26
27
|
from pyselfupdate.errors import SelfUpdateError
|
|
@@ -54,6 +55,7 @@ __all__ = [
|
|
|
54
55
|
'Installation',
|
|
55
56
|
'InvalidConfigError',
|
|
56
57
|
'LocalInstallError',
|
|
58
|
+
'LockUnreadableError',
|
|
57
59
|
'NoReleaseError',
|
|
58
60
|
'NotInstalledError',
|
|
59
61
|
'Outcome',
|
|
@@ -18,13 +18,17 @@ class LocalInstallError(SelfUpdateError):
|
|
|
18
18
|
"""The tool was installed from a local path or as an editable checkout.
|
|
19
19
|
|
|
20
20
|
Reinstalling would discard a working copy for a release that may be older,
|
|
21
|
-
with no way to tell which is newer. This is the
|
|
21
|
+
with no way to tell which is newer. This is the analog of goselfupdate's
|
|
22
22
|
ErrDevBuild.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class NotInstalledError(SelfUpdateError):
|
|
27
|
-
"""
|
|
27
|
+
"""uv's receipt is missing, unreadable, or names no requirement for the tool.
|
|
28
|
+
|
|
29
|
+
`update` raises it too when the receipt lists a `--with` requirement that no
|
|
30
|
+
requirements-file line reproduces, because reinstalling would drop it.
|
|
31
|
+
"""
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class NoReleaseError(SelfUpdateError):
|
|
@@ -37,3 +41,12 @@ class SourceError(SelfUpdateError):
|
|
|
37
41
|
|
|
38
42
|
class InstallFailedError(SelfUpdateError):
|
|
39
43
|
"""The install command ran and failed."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LockUnreadableError(SelfUpdateError):
|
|
47
|
+
"""The release's `uv.lock` could not be read, so nothing was installed.
|
|
48
|
+
|
|
49
|
+
Raised when git or uv is not on PATH, the tag will not clone, or `uv export`
|
|
50
|
+
refuses the lock. A tag with no `uv.lock` at all is not this error: it
|
|
51
|
+
installs unlocked and sets `Result.lock_missing`.
|
|
52
|
+
"""
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""GitHub as a release source.
|
|
2
2
|
|
|
3
|
-
Uses `urllib.request` rather than
|
|
4
|
-
|
|
3
|
+
Uses `urllib.request` rather than httpx2. The package makes one JSON request and
|
|
4
|
+
downloads nothing, and tools with no HTTP client of their own take it for
|
|
5
|
+
self-update, so a client library would land in their dependency tree for this
|
|
6
|
+
one call. `_CredentialScopedRedirects` covers the redirect hazard a client would.
|
|
7
|
+
|
|
8
|
+
It moves to httpx2 when this package needs a second request shape — a client
|
|
9
|
+
object, streaming, an asset download — or when every consumer already declares
|
|
10
|
+
httpx2.
|
|
5
11
|
"""
|
|
6
12
|
|
|
7
13
|
from __future__ import annotations
|
|
8
14
|
|
|
15
|
+
import datetime as dt
|
|
16
|
+
import http.client
|
|
9
17
|
import json
|
|
10
18
|
import os
|
|
11
19
|
import shlex
|
|
@@ -17,8 +25,7 @@ import urllib.request
|
|
|
17
25
|
from collections.abc import Callable
|
|
18
26
|
from dataclasses import dataclass
|
|
19
27
|
from dataclasses import field
|
|
20
|
-
from
|
|
21
|
-
from datetime import datetime
|
|
28
|
+
from typing import IO
|
|
22
29
|
|
|
23
30
|
from pyselfupdate.errors import NoReleaseError
|
|
24
31
|
from pyselfupdate.errors import SelfUpdateError
|
|
@@ -29,6 +36,49 @@ API = 'https://api.github.com'
|
|
|
29
36
|
DEFAULT_TIMEOUT = 10.0
|
|
30
37
|
|
|
31
38
|
|
|
39
|
+
def _origin(url: str) -> tuple[str, str]:
|
|
40
|
+
"""Scheme and authority, which is what decides whether a credential travels.
|
|
41
|
+
|
|
42
|
+
Port is part of it. Two ports on one host are two origins, and treating them
|
|
43
|
+
as one is how a redirect to a service sharing a hostname gets the token.
|
|
44
|
+
"""
|
|
45
|
+
split = urllib.parse.urlsplit(url)
|
|
46
|
+
return split.scheme, split.netloc
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _CredentialScopedRedirects(urllib.request.HTTPRedirectHandler):
|
|
50
|
+
"""Drop `Authorization` when a redirect leaves the origin it was sent to.
|
|
51
|
+
|
|
52
|
+
`urllib.request` carries the header to the new host instead. Measured across
|
|
53
|
+
3.11, 3.13 and 3.14 with two local servers: a `Bearer` sent to the first
|
|
54
|
+
arrives intact at the second on every one of them. So this is the library's
|
|
55
|
+
to do, at every version it supports, rather than something a floor bump ends.
|
|
56
|
+
|
|
57
|
+
It matters here because `API` is a reassignable module attribute and
|
|
58
|
+
`headers` is caller-supplied, which is what makes a redirect off GitHub
|
|
59
|
+
something other than hypothetical.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def redirect_request(
|
|
63
|
+
self,
|
|
64
|
+
req: urllib.request.Request,
|
|
65
|
+
fp: IO[bytes],
|
|
66
|
+
code: int,
|
|
67
|
+
msg: str,
|
|
68
|
+
headers: http.client.HTTPMessage,
|
|
69
|
+
newurl: str,
|
|
70
|
+
) -> urllib.request.Request | None:
|
|
71
|
+
redirected = super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
72
|
+
if redirected is None or _origin(req.full_url) == _origin(newurl):
|
|
73
|
+
return redirected
|
|
74
|
+
redirected.headers = {name: value for name, value in redirected.headers.items() if name.lower() != 'authorization'}
|
|
75
|
+
return redirected
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_OPENER = urllib.request.build_opener(_CredentialScopedRedirects)
|
|
79
|
+
"""Built once. Passing a `HTTPRedirectHandler` subclass replaces the default one."""
|
|
80
|
+
|
|
81
|
+
|
|
32
82
|
# B105 reads any name carrying "token" as a credential. Both hold the name of a
|
|
33
83
|
# variable and the text of a command; neither ever holds a secret.
|
|
34
84
|
TOKEN_COMMAND_ENV = 'GITHUB_TOKEN_COMMAND' # nosec B105
|
|
@@ -123,8 +173,8 @@ class GitHubSource:
|
|
|
123
173
|
# notify gate declines to check at all. That gate is otherwise free, and a
|
|
124
174
|
# spawn in front of it is the entire cost.
|
|
125
175
|
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
176
|
+
# `gh` needs no `token_func`, because it is the default below. This is for a
|
|
177
|
+
# credential neither the environment nor a command can produce.
|
|
128
178
|
token_func: Callable[[], str] | None = None
|
|
129
179
|
|
|
130
180
|
timeout: float = DEFAULT_TIMEOUT
|
|
@@ -215,7 +265,7 @@ class GitHubSource:
|
|
|
215
265
|
def _get(self, path: str):
|
|
216
266
|
url = f'{API}{path}'
|
|
217
267
|
|
|
218
|
-
# urlopen
|
|
268
|
+
# urlopen honors file:, ftp: and data: as readily as http:. API is a
|
|
219
269
|
# constant here, but it is a module attribute a caller can reassign --
|
|
220
270
|
# tests do exactly that -- so the scheme is checked rather than assumed.
|
|
221
271
|
# Without this, setting it to a file: URL turns a release check into an
|
|
@@ -235,10 +285,7 @@ class GitHubSource:
|
|
|
235
285
|
request.add_header(name, value)
|
|
236
286
|
|
|
237
287
|
try:
|
|
238
|
-
|
|
239
|
-
# on urlopen regardless of the scheme guard above. That guard, and
|
|
240
|
-
# test_a_non_http_scheme_is_refused, are the actual defence.
|
|
241
|
-
with urllib.request.urlopen(request, timeout=self.timeout) as response: # noqa: S310 # nosec B310
|
|
288
|
+
with _OPENER.open(request, timeout=self.timeout) as response:
|
|
242
289
|
return json.load(response)
|
|
243
290
|
except urllib.error.HTTPError as error:
|
|
244
291
|
raise _http_error(self.owner, self.repo, error, authenticated=bool(token)) from error
|
|
@@ -301,7 +348,7 @@ def _reset_clock(error: urllib.error.HTTPError) -> str:
|
|
|
301
348
|
"""
|
|
302
349
|
header = error.headers.get('x-ratelimit-reset') if error.headers else None
|
|
303
350
|
try:
|
|
304
|
-
moment = datetime.fromtimestamp(int(header or ''), tz=UTC)
|
|
351
|
+
moment = dt.datetime.fromtimestamp(int(header or ''), tz=dt.UTC)
|
|
305
352
|
except (TypeError, ValueError, OSError, OverflowError):
|
|
306
353
|
return ''
|
|
307
354
|
return f'; resets at {moment:%H:%M} UTC'
|