pyselfupdate 0.2.0__tar.gz → 0.2.2__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.2.0 → pyselfupdate-0.2.2}/PKG-INFO +18 -3
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/README.md +17 -2
- pyselfupdate-0.2.2/pyproject.toml +239 -0
- pyselfupdate-0.2.0/pyproject.toml → pyselfupdate-0.2.2/pyproject.toml.orig +44 -15
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/__init__.py +6 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/install.py +19 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/notifier.py +2 -2
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/typercmd.py +33 -18
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/updater.py +31 -8
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/config.py +0 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/errors.py +0 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/github.py +0 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/py.typed +0 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/source.py +0 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/state.py +0 -0
- {pyselfupdate-0.2.0 → pyselfupdate-0.2.2}/src/pyselfupdate/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyselfupdate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -126,8 +126,23 @@ add_update_command(app, CONFIG) # gives you `mytool update [--check]`
|
|
|
126
126
|
environment the running interpreter lives in**. Unlike replacing a Unix binary
|
|
127
127
|
— where the process holds an inode and is untouched — this pulls modules out
|
|
128
128
|
from under a live process, so anything imported afterwards may fail in ways
|
|
129
|
-
that are hard to read. Make it the last thing your process does,
|
|
130
|
-
`update_and_reexec` to replace the process with the new
|
|
129
|
+
that are hard to read. Make it the last thing your process does, then call
|
|
130
|
+
`exit_now` — or use `update_and_reexec` to replace the process with the new
|
|
131
|
+
version immediately.
|
|
132
|
+
|
|
133
|
+
That cuts both ways: anything you want to *print* after the install has to be
|
|
134
|
+
fetched before it. `run_update` resolves its changelog first for exactly this
|
|
135
|
+
reason, and a caller that needs its own steps in between composes the three
|
|
136
|
+
pieces `update` is made of rather than working around it:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
installation = require_updatable(config) # refuses a checkout, costs nothing
|
|
140
|
+
result = check(config) # network, environment still intact
|
|
141
|
+
notes = changelog(config, result.current, result.latest)
|
|
142
|
+
install_release(config, result, installation)
|
|
143
|
+
print(notes)
|
|
144
|
+
exit_now()
|
|
145
|
+
```
|
|
131
146
|
|
|
132
147
|
## What will not be updated
|
|
133
148
|
|
|
@@ -102,8 +102,23 @@ add_update_command(app, CONFIG) # gives you `mytool update [--check]`
|
|
|
102
102
|
environment the running interpreter lives in**. Unlike replacing a Unix binary
|
|
103
103
|
— where the process holds an inode and is untouched — this pulls modules out
|
|
104
104
|
from under a live process, so anything imported afterwards may fail in ways
|
|
105
|
-
that are hard to read. Make it the last thing your process does,
|
|
106
|
-
`update_and_reexec` to replace the process with the new
|
|
105
|
+
that are hard to read. Make it the last thing your process does, then call
|
|
106
|
+
`exit_now` — or use `update_and_reexec` to replace the process with the new
|
|
107
|
+
version immediately.
|
|
108
|
+
|
|
109
|
+
That cuts both ways: anything you want to *print* after the install has to be
|
|
110
|
+
fetched before it. `run_update` resolves its changelog first for exactly this
|
|
111
|
+
reason, and a caller that needs its own steps in between composes the three
|
|
112
|
+
pieces `update` is made of rather than working around it:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
installation = require_updatable(config) # refuses a checkout, costs nothing
|
|
116
|
+
result = check(config) # network, environment still intact
|
|
117
|
+
notes = changelog(config, result.current, result.latest)
|
|
118
|
+
install_release(config, result, installation)
|
|
119
|
+
print(notes)
|
|
120
|
+
exit_now()
|
|
121
|
+
```
|
|
107
122
|
|
|
108
123
|
## What will not be updated
|
|
109
124
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyselfupdate"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Self-update and update notification for Python CLIs installed with uv tool"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
keywords = [
|
|
7
|
+
"uv",
|
|
8
|
+
"cli",
|
|
9
|
+
"self-update",
|
|
10
|
+
"update-notifier",
|
|
11
|
+
"release",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"Topic :: System :: Software Distribution",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "Chris Birch"
|
|
28
|
+
email = "datapointchris@gmail.com"
|
|
29
|
+
|
|
30
|
+
[project.license]
|
|
31
|
+
text = "MIT"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
typer = ["typer>=0.12.0"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Repository = "https://github.com/datapointchris/pyselfupdate"
|
|
38
|
+
Issues = "https://github.com/datapointchris/pyselfupdate/issues"
|
|
39
|
+
Changelog = "https://github.com/datapointchris/pyselfupdate/blob/main/CHANGELOG.md"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"bandit>=1.7.8",
|
|
44
|
+
"mypy>=1.10.0",
|
|
45
|
+
"pre-commit>=4.3.0",
|
|
46
|
+
"pytest>=8.0.0",
|
|
47
|
+
"ruff>=0.7.0",
|
|
48
|
+
"typer>=0.12.0",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.bandit]
|
|
52
|
+
exclude_dirs = [
|
|
53
|
+
".git",
|
|
54
|
+
".mypy_cache",
|
|
55
|
+
".pytest_cache",
|
|
56
|
+
".ruff_cache",
|
|
57
|
+
".venv",
|
|
58
|
+
"tests",
|
|
59
|
+
]
|
|
60
|
+
skips = [
|
|
61
|
+
"B311",
|
|
62
|
+
"B404",
|
|
63
|
+
"B603",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.codespell]
|
|
67
|
+
skip = "*.lock"
|
|
68
|
+
check-filenames = true
|
|
69
|
+
|
|
70
|
+
[tool.mypy]
|
|
71
|
+
pretty = true
|
|
72
|
+
ignore_missing_imports = true
|
|
73
|
+
check_untyped_defs = false
|
|
74
|
+
warn_return_any = false
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
addopts = "-vv"
|
|
78
|
+
minversion = "8.0"
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
|
|
81
|
+
[tool.refurb]
|
|
82
|
+
enable_all = true
|
|
83
|
+
|
|
84
|
+
[tool.ruff]
|
|
85
|
+
line-length = 140
|
|
86
|
+
exclude = [
|
|
87
|
+
".git",
|
|
88
|
+
"__pycache__",
|
|
89
|
+
".mypy_cache",
|
|
90
|
+
".ruff_cache",
|
|
91
|
+
".vscode",
|
|
92
|
+
".venv",
|
|
93
|
+
"build",
|
|
94
|
+
"dist",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.format]
|
|
98
|
+
quote-style = "single"
|
|
99
|
+
indent-style = "space"
|
|
100
|
+
skip-magic-trailing-comma = false
|
|
101
|
+
line-ending = "auto"
|
|
102
|
+
docstring-code-format = true
|
|
103
|
+
|
|
104
|
+
[tool.ruff.lint]
|
|
105
|
+
select = [
|
|
106
|
+
"E",
|
|
107
|
+
"F",
|
|
108
|
+
"UP",
|
|
109
|
+
"B",
|
|
110
|
+
"SIM",
|
|
111
|
+
"I",
|
|
112
|
+
]
|
|
113
|
+
ignore = ["SIM108"]
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.isort]
|
|
116
|
+
force-single-line = true
|
|
117
|
+
|
|
118
|
+
[tool.ruff.lint.per-file-ignores]
|
|
119
|
+
"__init__.py" = ["F401"]
|
|
120
|
+
|
|
121
|
+
[tool.semantic_release]
|
|
122
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
123
|
+
branch = "main"
|
|
124
|
+
commit_message = "build(release): {version}"
|
|
125
|
+
build_command = """
|
|
126
|
+
pip install uv
|
|
127
|
+
uv lock --upgrade-package pyselfupdate
|
|
128
|
+
git add uv.lock
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
[tool.pyright]
|
|
132
|
+
typeCheckingMode = "standard"
|
|
133
|
+
analyzeUnannotatedFunctions = true
|
|
134
|
+
|
|
135
|
+
[tool.forge]
|
|
136
|
+
managed = [
|
|
137
|
+
[
|
|
138
|
+
"ruff",
|
|
139
|
+
"line-length",
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"ruff",
|
|
143
|
+
"format",
|
|
144
|
+
"quote-style",
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"ruff",
|
|
148
|
+
"format",
|
|
149
|
+
"indent-style",
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
"ruff",
|
|
153
|
+
"format",
|
|
154
|
+
"skip-magic-trailing-comma",
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
"ruff",
|
|
158
|
+
"format",
|
|
159
|
+
"line-ending",
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
"ruff",
|
|
163
|
+
"format",
|
|
164
|
+
"docstring-code-format",
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"ruff",
|
|
168
|
+
"lint",
|
|
169
|
+
"select",
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"ruff",
|
|
173
|
+
"lint",
|
|
174
|
+
"ignore",
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"ruff",
|
|
178
|
+
"lint",
|
|
179
|
+
"isort",
|
|
180
|
+
"force-single-line",
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
"ruff",
|
|
184
|
+
"lint",
|
|
185
|
+
"per-file-ignores",
|
|
186
|
+
"__init__.py",
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
"mypy",
|
|
190
|
+
"pretty",
|
|
191
|
+
],
|
|
192
|
+
[
|
|
193
|
+
"mypy",
|
|
194
|
+
"ignore_missing_imports",
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
"mypy",
|
|
198
|
+
"check_untyped_defs",
|
|
199
|
+
],
|
|
200
|
+
[
|
|
201
|
+
"mypy",
|
|
202
|
+
"warn_return_any",
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
"pyright",
|
|
206
|
+
"typeCheckingMode",
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
"pyright",
|
|
210
|
+
"analyzeUnannotatedFunctions",
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
"codespell",
|
|
214
|
+
"check-filenames",
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
"pytest",
|
|
218
|
+
"ini_options",
|
|
219
|
+
"addopts",
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
"pytest",
|
|
223
|
+
"ini_options",
|
|
224
|
+
"minversion",
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"pytest",
|
|
228
|
+
"ini_options",
|
|
229
|
+
"testpaths",
|
|
230
|
+
],
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
[tool.uv.build-backend]
|
|
234
|
+
module-name = "pyselfupdate"
|
|
235
|
+
module-root = "src"
|
|
236
|
+
|
|
237
|
+
[build-system]
|
|
238
|
+
requires = ["uv_build>=0.11.32,<0.12.0"]
|
|
239
|
+
build-backend = "uv_build"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyselfupdate"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
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
6
|
license = { text = "MIT" }
|
|
@@ -66,6 +66,9 @@ ignore_missing_imports = true
|
|
|
66
66
|
check_untyped_defs = false
|
|
67
67
|
warn_return_any = false
|
|
68
68
|
|
|
69
|
+
# `standard` replaces the per-rule opt-outs repos used to accumulate; basedpyright
|
|
70
|
+
# defaults to `recommended`, which turns every one of them back on. Named
|
|
71
|
+
# `pyright` so both basedpyright and Pylance read it.
|
|
69
72
|
|
|
70
73
|
[tool.pytest.ini_options]
|
|
71
74
|
addopts = "-vv"
|
|
@@ -99,20 +102,16 @@ docstring-code-format = true
|
|
|
99
102
|
|
|
100
103
|
[tool.ruff.lint]
|
|
101
104
|
select = [
|
|
102
|
-
# pycodestyle
|
|
103
|
-
"
|
|
104
|
-
#
|
|
105
|
-
"
|
|
106
|
-
#
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
"SIM",
|
|
112
|
-
# isort
|
|
113
|
-
"I",
|
|
105
|
+
"E", # pycodestyle errors
|
|
106
|
+
"F", # pyflakes
|
|
107
|
+
"UP", # pyupgrade
|
|
108
|
+
"B", # flake8-bugbear
|
|
109
|
+
"SIM", # flake8-simplify
|
|
110
|
+
"I", # isort
|
|
111
|
+
]
|
|
112
|
+
ignore = [
|
|
113
|
+
"SIM108", # ternary operator — often less readable
|
|
114
114
|
]
|
|
115
|
-
ignore = ["SIM108"]
|
|
116
115
|
|
|
117
116
|
[tool.ruff.lint.isort]
|
|
118
117
|
force-single-line = true
|
|
@@ -120,7 +119,6 @@ force-single-line = true
|
|
|
120
119
|
[tool.ruff.lint.per-file-ignores]
|
|
121
120
|
"__init__.py" = ["F401"]
|
|
122
121
|
|
|
123
|
-
|
|
124
122
|
[tool.semantic_release]
|
|
125
123
|
version_toml = ["pyproject.toml:project.version"]
|
|
126
124
|
branch = "main"
|
|
@@ -135,6 +133,37 @@ build_command = """
|
|
|
135
133
|
# ---------- Build System ---------- #
|
|
136
134
|
|
|
137
135
|
|
|
136
|
+
[tool.pyright]
|
|
137
|
+
typeCheckingMode = "standard"
|
|
138
|
+
analyzeUnannotatedFunctions = true # or pylance stops analyzing in vscode
|
|
139
|
+
|
|
140
|
+
[tool.forge]
|
|
141
|
+
# Keys the standard owns, written by `forge dies run maintenance/sync-pyproject.sh`.
|
|
142
|
+
# Dropping one from the template removes it here on the next sync; a key absent
|
|
143
|
+
# from this list belongs to the project and is never touched. Do not hand-edit.
|
|
144
|
+
managed = [
|
|
145
|
+
["ruff", "line-length"],
|
|
146
|
+
["ruff", "format", "quote-style"],
|
|
147
|
+
["ruff", "format", "indent-style"],
|
|
148
|
+
["ruff", "format", "skip-magic-trailing-comma"],
|
|
149
|
+
["ruff", "format", "line-ending"],
|
|
150
|
+
["ruff", "format", "docstring-code-format"],
|
|
151
|
+
["ruff", "lint", "select"],
|
|
152
|
+
["ruff", "lint", "ignore"],
|
|
153
|
+
["ruff", "lint", "isort", "force-single-line"],
|
|
154
|
+
["ruff", "lint", "per-file-ignores", "__init__.py"],
|
|
155
|
+
["mypy", "pretty"],
|
|
156
|
+
["mypy", "ignore_missing_imports"],
|
|
157
|
+
["mypy", "check_untyped_defs"],
|
|
158
|
+
["mypy", "warn_return_any"],
|
|
159
|
+
["pyright", "typeCheckingMode"],
|
|
160
|
+
["pyright", "analyzeUnannotatedFunctions"],
|
|
161
|
+
["codespell", "check-filenames"],
|
|
162
|
+
["pytest", "ini_options", "addopts"],
|
|
163
|
+
["pytest", "ini_options", "minversion"],
|
|
164
|
+
["pytest", "ini_options", "testpaths"],
|
|
165
|
+
]
|
|
166
|
+
|
|
138
167
|
[build-system]
|
|
139
168
|
requires = ["uv_build>=0.11.32,<0.12.0"]
|
|
140
169
|
build-backend = "uv_build"
|
|
@@ -28,6 +28,7 @@ from pyselfupdate.errors import SourceError
|
|
|
28
28
|
from pyselfupdate.github import GitHubSource
|
|
29
29
|
from pyselfupdate.install import Installation
|
|
30
30
|
from pyselfupdate.install import InstallKind
|
|
31
|
+
from pyselfupdate.install import exit_now
|
|
31
32
|
from pyselfupdate.install import read_installation
|
|
32
33
|
from pyselfupdate.notifier import Outcome
|
|
33
34
|
from pyselfupdate.notifier import Skip
|
|
@@ -40,6 +41,8 @@ from pyselfupdate.state import read as read_state
|
|
|
40
41
|
from pyselfupdate.updater import Result
|
|
41
42
|
from pyselfupdate.updater import changelog
|
|
42
43
|
from pyselfupdate.updater import check
|
|
44
|
+
from pyselfupdate.updater import install_release
|
|
45
|
+
from pyselfupdate.updater import require_updatable
|
|
43
46
|
from pyselfupdate.updater import update
|
|
44
47
|
from pyselfupdate.updater import update_and_reexec
|
|
45
48
|
|
|
@@ -64,9 +67,12 @@ __all__ = [
|
|
|
64
67
|
'changelog',
|
|
65
68
|
'check',
|
|
66
69
|
'enabled',
|
|
70
|
+
'exit_now',
|
|
71
|
+
'install_release',
|
|
67
72
|
'notify',
|
|
68
73
|
'read_installation',
|
|
69
74
|
'read_state',
|
|
75
|
+
'require_updatable',
|
|
70
76
|
'update',
|
|
71
77
|
'update_and_reexec',
|
|
72
78
|
]
|
|
@@ -156,6 +156,25 @@ def run_install(requirement: str, *, quiet: bool = True) -> None:
|
|
|
156
156
|
raise InstallFailedError(f'uv tool install failed: {message}')
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def exit_now(code: int = 0) -> None:
|
|
160
|
+
"""End this process immediately, after flushing what it has written.
|
|
161
|
+
|
|
162
|
+
The sibling of `reexec` for when there is nothing left to run. Both exist
|
|
163
|
+
for the same reason: the environment has been rewritten, so no further
|
|
164
|
+
import can be trusted. `sys.exit` raises SystemExit, which unwinds through
|
|
165
|
+
whatever CLI framework called us and then through interpreter shutdown --
|
|
166
|
+
and both are free to import a module they had not needed yet, from a
|
|
167
|
+
directory that is no longer the one this process started in. `os._exit`
|
|
168
|
+
skips both, which is why the flushes are done here rather than left to the
|
|
169
|
+
shutdown that no longer happens.
|
|
170
|
+
|
|
171
|
+
Never returns.
|
|
172
|
+
"""
|
|
173
|
+
sys.stdout.flush()
|
|
174
|
+
sys.stderr.flush()
|
|
175
|
+
os._exit(code)
|
|
176
|
+
|
|
177
|
+
|
|
159
178
|
def reexec() -> None:
|
|
160
179
|
"""Replace this process with the newly installed one.
|
|
161
180
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This layer never installs. `<tool> update` installs, and `<tool> update` is
|
|
4
4
|
where errors are printed; a failure here is recorded in the state file and
|
|
5
5
|
swallowed. That single rule is what keeps a dev checkout from printing an
|
|
6
|
-
|
|
6
|
+
update failure on every invocation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -96,7 +96,7 @@ def notify(
|
|
|
96
96
|
def enabled(config: Config, *, interactive: bool | None = None) -> tuple[bool, Skip | None]:
|
|
97
97
|
"""Whether a check would run, without touching the network or the clock.
|
|
98
98
|
|
|
99
|
-
Backs `<tool> update --why
|
|
99
|
+
Backs a fleet dashboard and a `<tool> update --why`. The interval is
|
|
100
100
|
deliberately not consulted: this answers "is this tool opted in", not "is it
|
|
101
101
|
due".
|
|
102
102
|
"""
|
|
@@ -21,15 +21,16 @@ rather than at the first call.
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
-
import sys
|
|
25
|
-
|
|
26
24
|
import typer
|
|
27
25
|
|
|
28
26
|
from pyselfupdate.config import Config
|
|
29
27
|
from pyselfupdate.errors import SelfUpdateError
|
|
28
|
+
from pyselfupdate.install import Installation
|
|
29
|
+
from pyselfupdate.install import exit_now
|
|
30
30
|
from pyselfupdate.updater import changelog
|
|
31
31
|
from pyselfupdate.updater import check
|
|
32
|
-
from pyselfupdate.updater import
|
|
32
|
+
from pyselfupdate.updater import install_release
|
|
33
|
+
from pyselfupdate.updater import require_updatable
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def add_update_command(app: typer.Typer, config: Config, *, name: str = 'update') -> None:
|
|
@@ -50,37 +51,51 @@ def run_update(config: Config, *, check_only: bool = False, skip_changelog: bool
|
|
|
50
51
|
the command under a group, does not have to reimplement the reporting. The
|
|
51
52
|
output format is identical to goselfupdate's, because a fleet that reports
|
|
52
53
|
the same thing three different ways is a fleet you have to read carefully.
|
|
54
|
+
|
|
55
|
+
The step order is the load-bearing part. Everything that reaches the network
|
|
56
|
+
or imports a module happens before the install; after it, this process only
|
|
57
|
+
prints what it already holds and exits without unwinding. Both halves of
|
|
58
|
+
that are lessons from a real failure: syncer 4.0.0 fetched its changelog
|
|
59
|
+
after replacing its own environment, and died on httpx's lazy import of
|
|
60
|
+
httpcore -- a module the release it had just installed no longer depended
|
|
61
|
+
on, and so had just deleted.
|
|
53
62
|
"""
|
|
54
63
|
tool = config.tool
|
|
64
|
+
installation: Installation | None = None
|
|
55
65
|
try:
|
|
56
|
-
|
|
66
|
+
if not check_only:
|
|
67
|
+
installation = require_updatable(config)
|
|
68
|
+
result = check(config)
|
|
57
69
|
except SelfUpdateError as error:
|
|
58
|
-
typer.echo(f'✗ {tool}
|
|
70
|
+
typer.echo(f'✗ {tool} update failed: {error}', err=True)
|
|
59
71
|
raise typer.Exit(1) from error
|
|
60
72
|
|
|
61
73
|
if not result.update_available:
|
|
62
74
|
typer.echo(f'✓ {tool} already at latest: {result.latest}')
|
|
63
75
|
return
|
|
64
76
|
|
|
65
|
-
if result.
|
|
66
|
-
typer.echo(f'✓ {tool} upgraded: {result.current} → {result.latest}')
|
|
67
|
-
else:
|
|
68
|
-
typer.echo(f'✓ {tool} update available: {result.current} → {result.latest}')
|
|
77
|
+
subjects = [] if skip_changelog else changelog(config, result.current, result.latest)
|
|
69
78
|
|
|
70
|
-
if
|
|
79
|
+
if installation is None:
|
|
80
|
+
typer.echo(f'✓ {tool} update available: {result.current} → {result.latest}')
|
|
81
|
+
_echo_changes(subjects)
|
|
71
82
|
return
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
try:
|
|
85
|
+
install_release(config, result, installation)
|
|
86
|
+
except SelfUpdateError as error:
|
|
87
|
+
typer.echo(f'✗ {tool} update failed: {error}', err=True)
|
|
88
|
+
raise typer.Exit(1) from error
|
|
89
|
+
|
|
90
|
+
typer.echo(f'✓ {tool} updated: {result.current} → {result.latest}')
|
|
91
|
+
_echo_changes(subjects)
|
|
92
|
+
exit_now()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _echo_changes(subjects: list[str]) -> None:
|
|
74
96
|
if not subjects:
|
|
75
97
|
return
|
|
76
|
-
|
|
77
98
|
typer.echo('')
|
|
78
99
|
typer.echo('Changes:')
|
|
79
100
|
for subject in subjects:
|
|
80
101
|
typer.echo(f' • {subject}')
|
|
81
|
-
|
|
82
|
-
# The environment this interpreter is running in has just been replaced.
|
|
83
|
-
# Nothing more may be imported, so the process ends here rather than
|
|
84
|
-
# returning into a CLI that might touch a lazily-loaded module.
|
|
85
|
-
if result.applied:
|
|
86
|
-
sys.stdout.flush()
|
|
@@ -55,18 +55,27 @@ def check(config: Config) -> Result:
|
|
|
55
55
|
return Result(current=current, latest=latest, release=release)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def
|
|
59
|
-
"""
|
|
58
|
+
def require_updatable(config: Config) -> Installation:
|
|
59
|
+
"""The installation an update would rewrite, refusing one that must not be.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
Public because it is the only part of an update that costs nothing and can
|
|
62
|
+
still refuse outright. A caller that orders the steps itself -- as
|
|
63
|
+
`typercmd.run_update` does, to fetch a changelog while the environment is
|
|
64
|
+
still intact -- keeps the refusal ahead of the network by starting here.
|
|
64
65
|
"""
|
|
65
|
-
|
|
66
|
-
installation = read_installation(resolved.tool)
|
|
66
|
+
installation = read_installation(config.resolved().tool)
|
|
67
67
|
_require_updatable(installation)
|
|
68
|
+
return installation
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
|
|
71
|
+
def install_release(config: Config, result: Result, installation: Installation, *, quiet: bool = True) -> Result:
|
|
72
|
+
"""Install the release `result` names, over the running one.
|
|
73
|
+
|
|
74
|
+
A no-op returning the result unchanged when there is nothing newer. On
|
|
75
|
+
success this interpreter's environment has been rewritten underneath it, so
|
|
76
|
+
the caller may not import anything afterwards -- see
|
|
77
|
+
`pyselfupdate.install.reexec` and `pyselfupdate.install.exit_now`.
|
|
78
|
+
"""
|
|
70
79
|
if not result.update_available or result.release is None:
|
|
71
80
|
return result
|
|
72
81
|
|
|
@@ -81,6 +90,20 @@ def update(config: Config, *, quiet: bool = True) -> Result:
|
|
|
81
90
|
)
|
|
82
91
|
|
|
83
92
|
|
|
93
|
+
def update(config: Config, *, quiet: bool = True) -> Result:
|
|
94
|
+
"""Check for a newer release and install it, in one call.
|
|
95
|
+
|
|
96
|
+
A no-op returning `applied=False` when already current. The composition of
|
|
97
|
+
`require_updatable`, `check` and `install_release`, in the order that keeps
|
|
98
|
+
a refusal cheap; a caller needing to do work between the check and the
|
|
99
|
+
install calls those three itself.
|
|
100
|
+
"""
|
|
101
|
+
resolved = config.resolved()
|
|
102
|
+
installation = require_updatable(resolved)
|
|
103
|
+
result = check(resolved)
|
|
104
|
+
return install_release(resolved, result, installation, quiet=quiet)
|
|
105
|
+
|
|
106
|
+
|
|
84
107
|
def update_and_reexec(config: Config, *, quiet: bool = True) -> Result:
|
|
85
108
|
"""`update`, then replace this process when anything was installed.
|
|
86
109
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|