uv-pyproject-bump 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ uv.lock
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Reads `tool.uv.override-dependencies` as well, which brings the count to six tables. 0.1.0 read five, and the README wrongly said it read every table uv understands.
6
+ - Documents that `--check` exits 0 when a bound that is behind the index is held back by an upper bound or by a second anchor. That was already true in 0.1.0 and the README said the opposite.
7
+ - Published on PyPI under the distribution name `uv-pyproject-bump` (the `uvbump` name was taken by an unrelated project with the same normalized spelling). The command installed is still `uvbump`. The README no longer tells you to install from the git repository by default.
8
+
9
+ ## 0.1.0
10
+
11
+ First release.
12
+
13
+ - Upgrades declared version bounds in `pyproject.toml` by editing the file text, so comments, key order, quoting style and blank lines are preserved.
14
+ - Reads `project.dependencies`, `project.optional-dependencies`, `dependency-groups`, `tool.uv.dev-dependencies` and `tool.uv.constraint-dependencies`.
15
+ - Handles `>=`, `>`, `==`, `~=` and `==X.Y.*` bounds, keeps a compatible release clause at its original precision, and refuses to move a bound backwards.
16
+ - Skips a dependency held back by an upper bound or an exclusion, and says which clause held it, unless `--widen`.
17
+ - Passes over a version whose every file has been yanked, and over a requirement carrying two bounds to move.
18
+ - `--check` for CI: writes nothing, exits 1 when a bound is behind the index.
19
+ - `--only`, `--exclude`, `--group`, `--dry-run`, `--pre`, `--index-url`, `--no-lock`, `--quiet`.
20
+ - Runs `uv lock` after writing when uv is on PATH.
21
+ - No dependencies. Python 3.11 or later.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Younes Z.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.5
2
+ Name: uv-pyproject-bump
3
+ Version: 0.2.0
4
+ Summary: Upgrade the version bounds declared in pyproject.toml, in place, without touching comments or formatting.
5
+ Project-URL: Homepage, https://github.com/Rezarys/uvbump
6
+ Project-URL: Source, https://github.com/Rezarys/uvbump
7
+ Project-URL: Issues, https://github.com/Rezarys/uvbump/issues
8
+ Author: Younes Z.
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bounds,dependencies,packaging,pyproject,upgrade,uv
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Build Tools
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+
25
+ # uvbump
26
+
27
+ Upgrades the version bounds declared in your `pyproject.toml`, in place, without touching your comments or your formatting. For anyone whose `pyproject.toml` still says `requests>=2.28` while the lock file has been on 2.32.5 for a year.
28
+
29
+ ```
30
+ pip install uv-pyproject-bump
31
+ uvbump --help
32
+ ```
33
+
34
+ `uv lock` and `uv sync --upgrade` move your lock file. They leave the bounds you wrote in `pyproject.toml` exactly where they were, so a project can sit on `requests>=2.28` for two years while actually running 2.32.5. The usual fix is a `uv remove` then `uv add` cycle, which rewrites the file and drops every comment in it. uvbump edits the bound and nothing else.
35
+
36
+ ```console
37
+ $ uvbump
38
+ requests >=2.28 -> >=2.32.5
39
+ click >=8.0,<9 -> >=8.1.7,<9
40
+ attrs ~=23.1 -> ~=23.2
41
+ rich ==13.0.0 -> ==14.0.0
42
+ typing-extensions skipped: no version bound to update
43
+ pyproject.toml: 4 bound(s) updated
44
+ uv.lock updated
45
+ ```
46
+
47
+ The diff is one line per bound. Comments, key order, quoting style and blank lines come back byte for byte.
48
+
49
+ ## What it reads
50
+
51
+ The six tables it reads:
52
+
53
+ - `project.dependencies`
54
+ - `project.optional-dependencies.*`
55
+ - `dependency-groups.*`
56
+ - `tool.uv.dev-dependencies`
57
+ - `tool.uv.constraint-dependencies`
58
+ - `tool.uv.override-dependencies`
59
+
60
+ ## What it writes
61
+
62
+ - `requests>=2.28`, latest 2.32.5, becomes `requests>=2.32.5`.
63
+ - `rich==13.0.0`, latest 14.0.0, becomes `rich==14.0.0`.
64
+ - `attrs~=23.1`, latest 23.2.0, becomes `attrs~=23.2`, kept at the same precision so the ceiling still means something.
65
+ - `django==4.2.*`, latest 5.1.3, becomes `django==5.1.*`.
66
+ - `click>=8.0,<9`, latest 8.1.7, becomes `click>=8.1.7,<9`.
67
+ - `click>=8.0,<8.1`, latest 8.1.7, is left alone, and uvbump tells you the upper bound is what held it back.
68
+ - `typing-extensions` with no bound is left alone, there is nothing to move.
69
+ - `pkg @ https://...` is left alone, direct references are never touched.
70
+
71
+ It never moves a bound backwards, and it never invents a bound you did not write. A version whose every file has been yanked is not a candidate, the same way uv and pip pass over it. Pass `--widen` if you do want the upper bound raised to the next major instead of being skipped.
72
+
73
+ ## Limits
74
+
75
+ Worth knowing before you run it:
76
+
77
+ - It reads the index, not your environment. It offers the latest release, without checking that release against your `requires-python` or against what your other dependencies allow. `uv lock` is what tells you whether the set still resolves, which is why uvbump runs it for you unless you pass `--no-lock`.
78
+ - A requirement with two bounds to move, such as `pkg>=1.0,==2.0`, is left alone rather than guessed at.
79
+ - Anything left alone is invisible to `--check`, which is the one to know before you put it in CI. When a bound is held back by an upper bound (`click>=8.0,<8.1`) or by a second anchor (`pkg>=1.0,==2.0`), uvbump plans no change, so `--check` exits 0 even though the declared bound is behind the index. It reports the reason on stderr either way. `--check` answers "is there a bound I can move", not "is every bound current".
80
+ - A dependency with no bound at all stays without one. uvbump moves bounds, it does not add them.
81
+ - Markers, extras and direct URL references are carried through untouched, never interpreted.
82
+ - It edits `pyproject.toml` only. Your `requirements.txt` files are not its business.
83
+
84
+ ## In CI
85
+
86
+ ```yaml
87
+ - run: uvx --from uv-pyproject-bump uvbump --check
88
+ ```
89
+
90
+ `--check` writes nothing and exits 1 when it has a bound it can move forward, so a job can fail on a stale `pyproject.toml` the same way it fails on unformatted code. Exit 0 means it has nothing to move, which is not quite the same as every bound being current: read the two cases under [Limits](#limits) before you rely on it. Exit 2 means uvbump could not do its job.
91
+
92
+ ## Options
93
+
94
+ ```
95
+ uvbump [PATH] [options]
96
+
97
+ PATH pyproject.toml, or the directory holding it (default: ./pyproject.toml)
98
+ --check write nothing, exit 1 when a bound can be moved forward
99
+ -n, --dry-run show what would change, write nothing, exit 0
100
+ --only NAME only this dependency, repeatable
101
+ --exclude NAME never this dependency, repeatable
102
+ --group TABLE only tables whose name contains this, repeatable (for example: --group dev)
103
+ --widen raise an upper bound that holds a dependency back, instead of skipping it
104
+ --pre consider pre-releases
105
+ --index-url URL PEP 691 simple index (default: $UV_INDEX_URL, then $PIP_INDEX_URL, else PyPI)
106
+ --no-lock do not run `uv lock` after writing
107
+ --quiet only print what changed
108
+ ```
109
+
110
+ Examples:
111
+
112
+ ```console
113
+ # Two packages, nothing else.
114
+ $ uvbump --only requests --only httpx
115
+
116
+ # Dev tables only, and leave the lock file alone.
117
+ $ uvbump --group dev --no-lock
118
+
119
+ # Look first.
120
+ $ uvbump --dry-run
121
+
122
+ # A mirror or a private index.
123
+ $ uvbump --index-url https://my.index/simple
124
+ ```
125
+
126
+ ## Install
127
+
128
+ The distribution on PyPI is named `uv-pyproject-bump`; the command it installs is `uvbump`.
129
+
130
+ ```console
131
+ $ pip install uv-pyproject-bump
132
+ $ uvx --from uv-pyproject-bump uvbump
133
+ ```
134
+
135
+ Pinning to a commit instead of the PyPI release still works:
136
+
137
+ ```console
138
+ $ pip install git+https://github.com/Rezarys/uvbump@v0.2.0
139
+ ```
140
+
141
+ uvbump needs Python 3.11 or later and has no dependencies. It asks your index over the PEP 691 JSON API, which means a mirror or a private index works too. It sends nothing anywhere else and collects nothing.
142
+
143
+ ## Why the bounds matter
144
+
145
+ If you publish a library, the bounds in `pyproject.toml` are the contract your users resolve against, and a lock file does not reach them. If you ship an application, stale lower bounds quietly widen the set of resolutions your CI has never tried. Either way the file is the thing that has to be current, and it is the one thing no tool was updating.
146
+
147
+ ## Tests
148
+
149
+ ```console
150
+ $ python3 run_tests.py
151
+ ```
152
+
153
+ No test dependencies either.
154
+
155
+ ## Prior art
156
+
157
+ `uv` itself does not do this yet, and the request is one of the most upvoted on its tracker: [astral-sh/uv#6794](https://github.com/astral-sh/uv/issues/6794) and [astral-sh/uv#1419](https://github.com/astral-sh/uv/issues/1419). Several people in those threads have published their own take, and they are worth a look if uvbump is not the shape you want.
158
+
159
+ The closest match by name and by aim is [`uv-bump`](https://github.com/zundertj/uv-bump), around since February 2025: it bumps the minimum bounds in your `pyproject.toml` in sync with your `uv.lock`, keeps your formatting and your comments, and supports workspaces. It works from a clean project, and its Howto asks you for an up to date `uv.lock` and a synced `.venv` before you run it. Its README documents no check mode and no exit code for CI. uvbump reads the package index directly, so it needs neither a lock file nor a virtual environment to tell you what has moved, and `--check` is there for CI. Comments survive both tools, so that is not a reason to pick one over the other. If `uv-bump` is the shape you want, use it.
160
+
161
+ If uv ships this natively, uvbump has done its job and you should use uv.
162
+
163
+ Not affiliated with Astral or with the uv project.
164
+
165
+ ## License
166
+
167
+ MIT.
@@ -0,0 +1,143 @@
1
+ # uvbump
2
+
3
+ Upgrades the version bounds declared in your `pyproject.toml`, in place, without touching your comments or your formatting. For anyone whose `pyproject.toml` still says `requests>=2.28` while the lock file has been on 2.32.5 for a year.
4
+
5
+ ```
6
+ pip install uv-pyproject-bump
7
+ uvbump --help
8
+ ```
9
+
10
+ `uv lock` and `uv sync --upgrade` move your lock file. They leave the bounds you wrote in `pyproject.toml` exactly where they were, so a project can sit on `requests>=2.28` for two years while actually running 2.32.5. The usual fix is a `uv remove` then `uv add` cycle, which rewrites the file and drops every comment in it. uvbump edits the bound and nothing else.
11
+
12
+ ```console
13
+ $ uvbump
14
+ requests >=2.28 -> >=2.32.5
15
+ click >=8.0,<9 -> >=8.1.7,<9
16
+ attrs ~=23.1 -> ~=23.2
17
+ rich ==13.0.0 -> ==14.0.0
18
+ typing-extensions skipped: no version bound to update
19
+ pyproject.toml: 4 bound(s) updated
20
+ uv.lock updated
21
+ ```
22
+
23
+ The diff is one line per bound. Comments, key order, quoting style and blank lines come back byte for byte.
24
+
25
+ ## What it reads
26
+
27
+ The six tables it reads:
28
+
29
+ - `project.dependencies`
30
+ - `project.optional-dependencies.*`
31
+ - `dependency-groups.*`
32
+ - `tool.uv.dev-dependencies`
33
+ - `tool.uv.constraint-dependencies`
34
+ - `tool.uv.override-dependencies`
35
+
36
+ ## What it writes
37
+
38
+ - `requests>=2.28`, latest 2.32.5, becomes `requests>=2.32.5`.
39
+ - `rich==13.0.0`, latest 14.0.0, becomes `rich==14.0.0`.
40
+ - `attrs~=23.1`, latest 23.2.0, becomes `attrs~=23.2`, kept at the same precision so the ceiling still means something.
41
+ - `django==4.2.*`, latest 5.1.3, becomes `django==5.1.*`.
42
+ - `click>=8.0,<9`, latest 8.1.7, becomes `click>=8.1.7,<9`.
43
+ - `click>=8.0,<8.1`, latest 8.1.7, is left alone, and uvbump tells you the upper bound is what held it back.
44
+ - `typing-extensions` with no bound is left alone, there is nothing to move.
45
+ - `pkg @ https://...` is left alone, direct references are never touched.
46
+
47
+ It never moves a bound backwards, and it never invents a bound you did not write. A version whose every file has been yanked is not a candidate, the same way uv and pip pass over it. Pass `--widen` if you do want the upper bound raised to the next major instead of being skipped.
48
+
49
+ ## Limits
50
+
51
+ Worth knowing before you run it:
52
+
53
+ - It reads the index, not your environment. It offers the latest release, without checking that release against your `requires-python` or against what your other dependencies allow. `uv lock` is what tells you whether the set still resolves, which is why uvbump runs it for you unless you pass `--no-lock`.
54
+ - A requirement with two bounds to move, such as `pkg>=1.0,==2.0`, is left alone rather than guessed at.
55
+ - Anything left alone is invisible to `--check`, which is the one to know before you put it in CI. When a bound is held back by an upper bound (`click>=8.0,<8.1`) or by a second anchor (`pkg>=1.0,==2.0`), uvbump plans no change, so `--check` exits 0 even though the declared bound is behind the index. It reports the reason on stderr either way. `--check` answers "is there a bound I can move", not "is every bound current".
56
+ - A dependency with no bound at all stays without one. uvbump moves bounds, it does not add them.
57
+ - Markers, extras and direct URL references are carried through untouched, never interpreted.
58
+ - It edits `pyproject.toml` only. Your `requirements.txt` files are not its business.
59
+
60
+ ## In CI
61
+
62
+ ```yaml
63
+ - run: uvx --from uv-pyproject-bump uvbump --check
64
+ ```
65
+
66
+ `--check` writes nothing and exits 1 when it has a bound it can move forward, so a job can fail on a stale `pyproject.toml` the same way it fails on unformatted code. Exit 0 means it has nothing to move, which is not quite the same as every bound being current: read the two cases under [Limits](#limits) before you rely on it. Exit 2 means uvbump could not do its job.
67
+
68
+ ## Options
69
+
70
+ ```
71
+ uvbump [PATH] [options]
72
+
73
+ PATH pyproject.toml, or the directory holding it (default: ./pyproject.toml)
74
+ --check write nothing, exit 1 when a bound can be moved forward
75
+ -n, --dry-run show what would change, write nothing, exit 0
76
+ --only NAME only this dependency, repeatable
77
+ --exclude NAME never this dependency, repeatable
78
+ --group TABLE only tables whose name contains this, repeatable (for example: --group dev)
79
+ --widen raise an upper bound that holds a dependency back, instead of skipping it
80
+ --pre consider pre-releases
81
+ --index-url URL PEP 691 simple index (default: $UV_INDEX_URL, then $PIP_INDEX_URL, else PyPI)
82
+ --no-lock do not run `uv lock` after writing
83
+ --quiet only print what changed
84
+ ```
85
+
86
+ Examples:
87
+
88
+ ```console
89
+ # Two packages, nothing else.
90
+ $ uvbump --only requests --only httpx
91
+
92
+ # Dev tables only, and leave the lock file alone.
93
+ $ uvbump --group dev --no-lock
94
+
95
+ # Look first.
96
+ $ uvbump --dry-run
97
+
98
+ # A mirror or a private index.
99
+ $ uvbump --index-url https://my.index/simple
100
+ ```
101
+
102
+ ## Install
103
+
104
+ The distribution on PyPI is named `uv-pyproject-bump`; the command it installs is `uvbump`.
105
+
106
+ ```console
107
+ $ pip install uv-pyproject-bump
108
+ $ uvx --from uv-pyproject-bump uvbump
109
+ ```
110
+
111
+ Pinning to a commit instead of the PyPI release still works:
112
+
113
+ ```console
114
+ $ pip install git+https://github.com/Rezarys/uvbump@v0.2.0
115
+ ```
116
+
117
+ uvbump needs Python 3.11 or later and has no dependencies. It asks your index over the PEP 691 JSON API, which means a mirror or a private index works too. It sends nothing anywhere else and collects nothing.
118
+
119
+ ## Why the bounds matter
120
+
121
+ If you publish a library, the bounds in `pyproject.toml` are the contract your users resolve against, and a lock file does not reach them. If you ship an application, stale lower bounds quietly widen the set of resolutions your CI has never tried. Either way the file is the thing that has to be current, and it is the one thing no tool was updating.
122
+
123
+ ## Tests
124
+
125
+ ```console
126
+ $ python3 run_tests.py
127
+ ```
128
+
129
+ No test dependencies either.
130
+
131
+ ## Prior art
132
+
133
+ `uv` itself does not do this yet, and the request is one of the most upvoted on its tracker: [astral-sh/uv#6794](https://github.com/astral-sh/uv/issues/6794) and [astral-sh/uv#1419](https://github.com/astral-sh/uv/issues/1419). Several people in those threads have published their own take, and they are worth a look if uvbump is not the shape you want.
134
+
135
+ The closest match by name and by aim is [`uv-bump`](https://github.com/zundertj/uv-bump), around since February 2025: it bumps the minimum bounds in your `pyproject.toml` in sync with your `uv.lock`, keeps your formatting and your comments, and supports workspaces. It works from a clean project, and its Howto asks you for an up to date `uv.lock` and a synced `.venv` before you run it. Its README documents no check mode and no exit code for CI. uvbump reads the package index directly, so it needs neither a lock file nor a virtual environment to tell you what has moved, and `--check` is there for CI. Comments survive both tools, so that is not a reason to pick one over the other. If `uv-bump` is the shape you want, use it.
136
+
137
+ If uv ships this natively, uvbump has done its job and you should use uv.
138
+
139
+ Not affiliated with Astral or with the uv project.
140
+
141
+ ## License
142
+
143
+ MIT.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "uv-pyproject-bump"
7
+ version = "0.2.0"
8
+ description = "Upgrade the version bounds declared in pyproject.toml, in place, without touching comments or formatting."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Younes Z." }]
14
+ keywords = [
15
+ "pyproject",
16
+ "dependencies",
17
+ "upgrade",
18
+ "bounds",
19
+ "uv",
20
+ "packaging",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Environment :: Console",
25
+ "Intended Audience :: Developers",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Software Development :: Build Tools",
32
+ "Typing :: Typed",
33
+ ]
34
+ # No dependencies, on purpose: uvbump is meant to be run with `uvx` in a cold CI job.
35
+ dependencies = []
36
+
37
+ [project.scripts]
38
+ uvbump = "uvbump.cli:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Rezarys/uvbump"
42
+ Source = "https://github.com/Rezarys/uvbump"
43
+ Issues = "https://github.com/Rezarys/uvbump/issues"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/uvbump"]
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = ["src", "tests", "README.md", "LICENSE", "CHANGELOG.md", "run_tests.py"]
50
+
51
+ [tool.ruff]
52
+ line-length = 100
53
+ src = ["src", "tests"]
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Run the test suite with no dependencies and no installation:
3
+
4
+ python3 run_tests.py
5
+
6
+ Equivalent to `python3 -m unittest discover -s tests` with `src` on the path.
7
+ """
8
+
9
+ import sys
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ ROOT = Path(__file__).resolve().parent
14
+ sys.path.insert(0, str(ROOT / "src"))
15
+
16
+ if __name__ == "__main__":
17
+ tests = str(ROOT / "tests")
18
+ suite = unittest.defaultTestLoader.discover(tests, top_level_dir=tests)
19
+ result = unittest.TextTestRunner(verbosity=2).run(suite)
20
+ sys.exit(0 if result.wasSuccessful() else 1)
@@ -0,0 +1,5 @@
1
+ """uvbump: upgrade the version bounds declared in pyproject.toml, in place."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,185 @@
1
+ """The `uvbump` command line."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ from . import __version__
13
+ from .deps import Change, RewriteError, apply, canonical_name, collect, load, plan
14
+ from .index import DEFAULT_INDEX, IndexUnavailable, Resolver, make_resolver, resolve_all
15
+
16
+ EXIT_OK = 0
17
+ EXIT_OUTDATED = 1
18
+ EXIT_ERROR = 2
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ parser = argparse.ArgumentParser(
23
+ prog="uvbump",
24
+ description=(
25
+ "Upgrade the version bounds declared in pyproject.toml, in place, "
26
+ "without touching comments or formatting."
27
+ ),
28
+ )
29
+ parser.add_argument(
30
+ "path",
31
+ nargs="?",
32
+ default="pyproject.toml",
33
+ help="path to pyproject.toml, or to the directory holding it (default: ./pyproject.toml)",
34
+ )
35
+ parser.add_argument("--version", action="version", version=f"uvbump {__version__}")
36
+ parser.add_argument(
37
+ "--check",
38
+ action="store_true",
39
+ help="write nothing and exit 1 when a bound can be moved forward (for CI)",
40
+ )
41
+ parser.add_argument(
42
+ "-n", "--dry-run", action="store_true", help="show what would change and write nothing"
43
+ )
44
+ parser.add_argument(
45
+ "--only",
46
+ action="append",
47
+ default=[],
48
+ metavar="NAME",
49
+ help="only this dependency, repeatable",
50
+ )
51
+ parser.add_argument(
52
+ "--exclude",
53
+ action="append",
54
+ default=[],
55
+ metavar="NAME",
56
+ help="never this dependency, repeatable",
57
+ )
58
+ parser.add_argument(
59
+ "--group",
60
+ action="append",
61
+ default=[],
62
+ metavar="TABLE",
63
+ help="only tables whose name contains this, repeatable (for example: dev)",
64
+ )
65
+ parser.add_argument(
66
+ "--widen",
67
+ action="store_true",
68
+ help="raise an upper bound that holds a dependency back, instead of skipping it",
69
+ )
70
+ parser.add_argument("--pre", action="store_true", help="consider pre-releases")
71
+ parser.add_argument(
72
+ "--index-url",
73
+ default=os.environ.get("UV_INDEX_URL", os.environ.get("PIP_INDEX_URL", DEFAULT_INDEX)),
74
+ help=(
75
+ "PEP 691 simple index "
76
+ f"(default: $UV_INDEX_URL, then $PIP_INDEX_URL, else {DEFAULT_INDEX})"
77
+ ),
78
+ )
79
+ parser.add_argument(
80
+ "--no-lock", action="store_true", help="do not run `uv lock` after writing"
81
+ )
82
+ parser.add_argument("--quiet", action="store_true", help="only print what changed")
83
+ return parser
84
+
85
+
86
+ def resolve_path(raw: str) -> Path:
87
+ path = Path(raw)
88
+ if path.is_dir():
89
+ path = path / "pyproject.toml"
90
+ return path
91
+
92
+
93
+ def wanted(change_name: str, table: str, args: argparse.Namespace) -> bool:
94
+ name = canonical_name(change_name)
95
+ if args.only and name not in {canonical_name(o) for o in args.only}:
96
+ return False
97
+ if name in {canonical_name(e) for e in args.exclude}:
98
+ return False
99
+ if args.group and not any(g.lower() in table.lower() for g in args.group):
100
+ return False
101
+ return True
102
+
103
+
104
+ def compute(text: str, args: argparse.Namespace, resolver: Resolver) -> list[Change]:
105
+ document = load(text)
106
+ requirements = [r for r in collect(document) if wanted(r.name, r.table, args)]
107
+ latest = resolve_all((r.name for r in requirements), resolver)
108
+ changes = []
109
+ for requirement in requirements:
110
+ found = latest.get(requirement.canonical)
111
+ if isinstance(found, IndexUnavailable):
112
+ changes.append(Change(requirement, None, None, str(found)))
113
+ continue
114
+ changes.append(plan(requirement, found, widen=args.widen))
115
+ return changes
116
+
117
+
118
+ def report(changes: list[Change], out, quiet: bool) -> None:
119
+ width = max((len(c.requirement.name) for c in changes), default=0)
120
+ for change in changes:
121
+ name = change.requirement.name.ljust(width)
122
+ if change.bumped:
123
+ old = change.requirement.spec_text or "(unbounded)"
124
+ print(f" {name} {old} -> {change.new_spec}", file=out)
125
+ elif change.reason and not quiet:
126
+ print(f" {name} skipped: {change.reason}", file=out)
127
+
128
+
129
+ def run_lock(path: Path, out) -> None:
130
+ if shutil.which("uv") is None:
131
+ print("uv is not on PATH, skipping `uv lock`", file=out)
132
+ return
133
+ result = subprocess.run(
134
+ ["uv", "lock"], cwd=path.parent or Path("."), capture_output=True, text=True
135
+ )
136
+ if result.returncode != 0:
137
+ print(f"`uv lock` failed:\n{result.stderr.strip()}", file=out)
138
+ else:
139
+ print("uv.lock updated", file=out)
140
+
141
+
142
+ def main(argv: list[str] | None = None) -> int:
143
+ args = build_parser().parse_args(argv)
144
+ out = sys.stderr if args.check else sys.stdout
145
+ path = resolve_path(args.path)
146
+ if not path.is_file():
147
+ print(f"no such file: {path}", file=sys.stderr)
148
+ return EXIT_ERROR
149
+
150
+ text = path.read_text(encoding="utf-8")
151
+ resolver = make_resolver(args.index_url, allow_prereleases=args.pre)
152
+ try:
153
+ changes = compute(text, args, resolver)
154
+ except (ValueError, OSError) as error:
155
+ print(f"{path}: {error}", file=sys.stderr)
156
+ return EXIT_ERROR
157
+
158
+ bumped = [c for c in changes if c.bumped]
159
+ report(changes, out, args.quiet)
160
+
161
+ if not bumped:
162
+ if not args.quiet:
163
+ print("every bound is up to date", file=out)
164
+ return EXIT_OK
165
+
166
+ if args.check:
167
+ print(f"{len(bumped)} bound(s) behind the index", file=out)
168
+ return EXIT_OUTDATED
169
+ if args.dry_run:
170
+ print(f"{len(bumped)} bound(s) would change, nothing written", file=out)
171
+ return EXIT_OK
172
+
173
+ try:
174
+ path.write_text(apply(text, bumped), encoding="utf-8")
175
+ except RewriteError as error:
176
+ print(f"{path}: {error}", file=sys.stderr)
177
+ return EXIT_ERROR
178
+ print(f"{path}: {len(bumped)} bound(s) updated", file=out)
179
+ if not args.no_lock:
180
+ run_lock(path, out)
181
+ return EXIT_OK
182
+
183
+
184
+ if __name__ == "__main__":
185
+ raise SystemExit(main())