pyselfupdate 0.4.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyselfupdate
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -234,7 +234,7 @@ atomically:
234
234
  ```
235
235
 
236
236
  State, not config and not cache: it persists across runs, it is not authored by
237
- the user, and deleting it changes behaviour rather than merely costing a
237
+ the user, and deleting it changes behavior rather than merely costing a
238
238
  recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
239
239
  is where `gh` puts the same thing.
240
240
 
@@ -263,6 +263,6 @@ names it, and the `NO_AUTO_UPDATE` contract. They do not share an API, because
263
263
  Version precedence is deliberately identical across all three, so a tool and
264
264
  its siblings never disagree about which release is newer.
265
265
 
266
- ## Licence
266
+ ## License
267
267
 
268
268
  MIT
@@ -210,7 +210,7 @@ atomically:
210
210
  ```
211
211
 
212
212
  State, not config and not cache: it persists across runs, it is not authored by
213
- the user, and deleting it changes behaviour rather than merely costing a
213
+ the user, and deleting it changes behavior rather than merely costing a
214
214
  recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
215
215
  is where `gh` puts the same thing.
216
216
 
@@ -239,6 +239,6 @@ names it, and the `NO_AUTO_UPDATE` contract. They do not share an API, because
239
239
  Version precedence is deliberately identical across all three, so a tool and
240
240
  its siblings never disagree about which release is newer.
241
241
 
242
- ## Licence
242
+ ## License
243
243
 
244
244
  MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyselfupdate"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Self-update and update notification for Python CLIs installed with uv tool"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -66,6 +66,9 @@ skips = [
66
66
  [tool.codespell]
67
67
  skip = "*.lock"
68
68
  check-filenames = true
69
+ check-hidden = true
70
+ builtin = "clear,rare"
71
+ uri-ignore-words-list = "*"
69
72
 
70
73
  [tool.mypy]
71
74
  pretty = true
@@ -212,6 +215,18 @@ managed = [
212
215
  [
213
216
  "codespell",
214
217
  "check-filenames",
218
+ ],
219
+ [
220
+ "codespell",
221
+ "check-hidden",
222
+ ],
223
+ [
224
+ "codespell",
225
+ "builtin",
226
+ ],
227
+ [
228
+ "codespell",
229
+ "uri-ignore-words-list",
215
230
  ],
216
231
  [
217
232
  "pytest",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyselfupdate"
3
- version = "0.4.0"
3
+ version = "0.4.1"
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" }
@@ -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]
@@ -138,7 +141,7 @@ typeCheckingMode = "standard"
138
141
  analyzeUnannotatedFunctions = true # or pylance stops analyzing in vscode
139
142
 
140
143
  [tool.forge]
141
- # Keys the standard owns, written by `forge repos apply pyproject`.
144
+ # Keys the shared toolchain standard owns, written by its generator.
142
145
  # Dropping one from the template removes it here on the next sync; a key absent
143
146
  # from this list belongs to the project and is never touched. Do not hand-edit.
144
147
  managed = [
@@ -159,6 +162,9 @@ managed = [
159
162
  ["pyright", "typeCheckingMode"],
160
163
  ["pyright", "analyzeUnannotatedFunctions"],
161
164
  ["codespell", "check-filenames"],
165
+ ["codespell", "check-hidden"],
166
+ ["codespell", "builtin"],
167
+ ["codespell", "uri-ignore-words-list"],
162
168
  ["pytest", "ini_options", "verbosity_assertions"],
163
169
  ["pytest", "ini_options", "minversion"],
164
170
  ["pytest", "ini_options", "testpaths"],
@@ -18,7 +18,7 @@ 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 analogue of goselfupdate's
21
+ with no way to tell which is newer. This is the analog of goselfupdate's
22
22
  ErrDevBuild.
23
23
  """
24
24
 
@@ -6,6 +6,7 @@ this package is that adding it to a project adds nothing else.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import http.client
9
10
  import json
10
11
  import os
11
12
  import shlex
@@ -19,6 +20,7 @@ from dataclasses import dataclass
19
20
  from dataclasses import field
20
21
  from datetime import UTC
21
22
  from datetime import datetime
23
+ from typing import IO
22
24
 
23
25
  from pyselfupdate.errors import NoReleaseError
24
26
  from pyselfupdate.errors import SelfUpdateError
@@ -29,6 +31,49 @@ API = 'https://api.github.com'
29
31
  DEFAULT_TIMEOUT = 10.0
30
32
 
31
33
 
34
+ def _origin(url: str) -> tuple[str, str]:
35
+ """Scheme and authority, which is what decides whether a credential travels.
36
+
37
+ Port is part of it. Two ports on one host are two origins, and treating them
38
+ as one is how a redirect to a service sharing a hostname gets the token.
39
+ """
40
+ split = urllib.parse.urlsplit(url)
41
+ return split.scheme, split.netloc
42
+
43
+
44
+ class _CredentialScopedRedirects(urllib.request.HTTPRedirectHandler):
45
+ """Drop `Authorization` when a redirect leaves the origin it was sent to.
46
+
47
+ `urllib.request` carries the header to the new host instead. Measured across
48
+ 3.11, 3.13 and 3.14 with two local servers: a `Bearer` sent to the first
49
+ arrives intact at the second on every one of them. So this is the library's
50
+ to do, at every version it supports, rather than something a floor bump ends.
51
+
52
+ It matters here because `API` is a reassignable module attribute and
53
+ `headers` is caller-supplied, which is what makes a redirect off GitHub
54
+ something other than hypothetical.
55
+ """
56
+
57
+ def redirect_request(
58
+ self,
59
+ req: urllib.request.Request,
60
+ fp: IO[bytes],
61
+ code: int,
62
+ msg: str,
63
+ headers: http.client.HTTPMessage,
64
+ newurl: str,
65
+ ) -> urllib.request.Request | None:
66
+ redirected = super().redirect_request(req, fp, code, msg, headers, newurl)
67
+ if redirected is None or _origin(req.full_url) == _origin(newurl):
68
+ return redirected
69
+ redirected.headers = {name: value for name, value in redirected.headers.items() if name.lower() != 'authorization'}
70
+ return redirected
71
+
72
+
73
+ _OPENER = urllib.request.build_opener(_CredentialScopedRedirects)
74
+ """Built once. Passing a `HTTPRedirectHandler` subclass replaces the default one."""
75
+
76
+
32
77
  # B105 reads any name carrying "token" as a credential. Both hold the name of a
33
78
  # variable and the text of a command; neither ever holds a secret.
34
79
  TOKEN_COMMAND_ENV = 'GITHUB_TOKEN_COMMAND' # nosec B105
@@ -215,7 +260,7 @@ class GitHubSource:
215
260
  def _get(self, path: str):
216
261
  url = f'{API}{path}'
217
262
 
218
- # urlopen honours file:, ftp: and data: as readily as http:. API is a
263
+ # urlopen honors file:, ftp: and data: as readily as http:. API is a
219
264
  # constant here, but it is a module attribute a caller can reassign --
220
265
  # tests do exactly that -- so the scheme is checked rather than assumed.
221
266
  # Without this, setting it to a file: URL turns a release check into an
@@ -235,10 +280,7 @@ class GitHubSource:
235
280
  request.add_header(name, value)
236
281
 
237
282
  try:
238
- # B310 is a call blacklist rather than a dataflow check, so it fires
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
283
+ with _OPENER.open(request, timeout=self.timeout) as response:
242
284
  return json.load(response)
243
285
  except urllib.error.HTTPError as error:
244
286
  raise _http_error(self.owner, self.repo, error, authenticated=bool(token)) from error
@@ -1,6 +1,6 @@
1
1
  """Reading and rewriting a uv tool installation.
2
2
 
3
- This is the part with no analogue in goselfupdate. A Go tool updates by
3
+ This is the part with no analog in goselfupdate. A Go tool updates by
4
4
  replacing one file; a uv tool updates by rebuilding the virtual environment its
5
5
  own interpreter is running inside, which is why `update` must be the last thing
6
6
  a process does before it exits or re-execs.
@@ -5,7 +5,7 @@ any tool can read any other tool's state and a single dashboard can glob
5
5
  `~/.local/state/*/autoupdate-*.json` with no per-tool knowledge.
6
6
 
7
7
  State, not config and not cache: it persists across runs, it is not authored by
8
- the user, and deleting it changes behaviour rather than merely costing a
8
+ the user, and deleting it changes behavior rather than merely costing a
9
9
  recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
10
10
  is where `gh` puts the same thing.
11
11
  """
@@ -23,7 +23,7 @@ from pyselfupdate.source import Release
23
23
  class Result:
24
24
  """What an update found, whether or not it installed anything."""
25
25
 
26
- # The running version, canonicalised with a leading "v".
26
+ # The running version, canonicalized with a leading "v".
27
27
  current: str
28
28
 
29
29
  # The version now installed, or the one `update` would install after a
@@ -129,7 +129,7 @@ def changelog(config: Config, from_version: str, to_version: str) -> list[str]:
129
129
 
130
130
 
131
131
  def _current_version(config: Config) -> str:
132
- """The running version, canonicalised, rejecting one that is not a release.
132
+ """The running version, canonicalized, rejecting one that is not a release.
133
133
 
134
134
  An install that tracks a git branch rather than a tag reports whatever
135
135
  version was in pyproject.toml when it was built, which says nothing about
@@ -2,12 +2,12 @@
2
2
 
3
3
  Implemented here rather than taken from `packaging` so that this package has no
4
4
  third-party dependencies. `packaging` also implements PEP 440, whose precedence
5
- rules differ from semver's in ways that matter: it normalises `1.0.0-rc.1` to
5
+ rules differ from semver's in ways that matter: it normalizes `1.0.0-rc.1` to
6
6
  `1.0.0rc1` and orders `1.0.0.post1` above `1.0.0`, neither of which describes a
7
7
  git tag.
8
8
 
9
9
  Precedence follows https://semver.org section 11, and is byte-for-byte the same
10
- behaviour as goselfupdate's semver.go so that the two libraries never disagree
10
+ behavior as goselfupdate's semver.go so that the two libraries never disagree
11
11
  about which of two releases is newer.
12
12
  """
13
13