Skip to content

Commit 45e6878

Browse files
committed
Final and pre-release control options for package selection
1 parent 7e49dca commit 45e6878

File tree

15 files changed

+300
-22
lines changed

15 files changed

+300
-22
lines changed

docs/html/user_guide.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,55 @@ Example build constraints file (``build-constraints.txt``):
296296
# Pin Cython for packages that use it to build
297297
cython==0.29.24
298298
299+
Controlling Pre-release Installation
300+
=====================================
301+
302+
By default, pip installs stable versions of packages, unless their specifier includes
303+
a pre-release version (e.g., ``SomePackage>=1.0a1``) or if there are no stable versions
304+
available that satisfy the requirement. The ``--all-releases``and ``--only-final``
305+
options provide per-package control over pre-release selection.
306+
307+
Use ``--all-releases`` to allow pre-releases for specific packages:
308+
309+
.. tab:: Unix/macOS
310+
311+
.. code-block:: shell
312+
313+
python -m pip install --all-releases=DependencyPackage SomePackage
314+
python -m pip install --all-releases=:all: SomePackage
315+
316+
.. tab:: Windows
317+
318+
.. code-block:: shell
319+
320+
py -m pip install --all-releases=DependencyPackage SomePackage
321+
py -m pip install --all-releases=:all: SomePackage
322+
323+
Use ``--only-final`` to explicitly disable pre-releases for specific packages:
324+
325+
.. tab:: Unix/macOS
326+
327+
.. code-block:: shell
328+
329+
python -m pip install --only-final=DependencyPackage SomePackage
330+
python -m pip install --only-final=:all: SomePackage
331+
332+
.. tab:: Windows
333+
334+
.. code-block:: shell
335+
336+
py -m pip install --only-final=DependencyPackage SomePackage
337+
py -m pip install --only-final=:all: SomePackage
338+
339+
Both options accept ``:all:`` to apply to all packages, ``:none:`` to clear
340+
the setting, or comma-separated package names. Package-specific settings
341+
override ``:all:``. These options can also be used in requirements files.
342+
343+
.. note::
344+
345+
The ``--pre`` flag is equivalent to ``--all-releases :all:`` but cannot
346+
be combined with ``--all-releases`` or ``--only-final``.
347+
299348

300349
.. _`Dependency Groups`:
301350

src/pip/_internal/build_env.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ def install(
188188
)
189189
)
190190

191+
if finder.release_control is not None:
192+
# Use ordered args to preserve the user's original command-line order
193+
# This is important because later flags can override earlier ones
194+
for attr_name, value in finder.release_control.get_ordered_args():
195+
args.extend(("--" + attr_name.replace("_", "-"), value))
196+
191197
index_urls = finder.index_urls
192198
if index_urls:
193199
args.extend(["-i", index_urls[0]])
@@ -206,8 +212,6 @@ def install(
206212
args.extend(["--cert", finder.custom_cert])
207213
if finder.client_cert:
208214
args.extend(["--client-cert", finder.client_cert])
209-
if finder.allow_all_prereleases:
210-
args.append("--pre")
211215
if finder.prefer_binary:
212216
args.append("--prefer-binary")
213217

src/pip/_internal/cli/cmdoptions.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from pip._internal.locations import USER_CACHE_DIR, get_src_prefix
2828
from pip._internal.models.format_control import FormatControl
2929
from pip._internal.models.index import PyPI
30+
from pip._internal.models.release_control import ReleaseControl
3031
from pip._internal.models.target_python import TargetPython
3132
from pip._internal.utils.hashes import STRONG_HASHES
3233
from pip._internal.utils.misc import strtobool
@@ -580,6 +581,86 @@ def only_binary() -> Option:
580581
)
581582

582583

584+
def _get_release_control(values: Values, option: Option) -> Any:
585+
"""Get a release_control object."""
586+
return getattr(values, option.dest)
587+
588+
589+
def _handle_all_releases(
590+
option: Option, opt_str: str, value: str, parser: OptionParser
591+
) -> None:
592+
existing = _get_release_control(parser.values, option)
593+
existing.handle_mutual_excludes(
594+
value,
595+
existing.all_releases,
596+
existing.only_final,
597+
"all_releases",
598+
)
599+
600+
601+
def _handle_only_final(
602+
option: Option, opt_str: str, value: str, parser: OptionParser
603+
) -> None:
604+
existing = _get_release_control(parser.values, option)
605+
existing.handle_mutual_excludes(
606+
value,
607+
existing.only_final,
608+
existing.all_releases,
609+
"only_final",
610+
)
611+
612+
613+
def all_releases() -> Option:
614+
release_control = ReleaseControl(set(), set())
615+
return Option(
616+
"--all-releases",
617+
dest="release_control",
618+
action="callback",
619+
callback=_handle_all_releases,
620+
type="str",
621+
default=release_control,
622+
help="Allow all release types (including pre-releases) for a package. "
623+
"Can be supplied multiple times, and each time adds to the existing "
624+
'value. Accepts either ":all:" to allow pre-releases for all '
625+
'packages, ":none:" to empty the set (notice the colons), or one or '
626+
"more package names with commas between them (no colons). Cannot be "
627+
"used with --pre.",
628+
)
629+
630+
631+
def only_final() -> Option:
632+
release_control = ReleaseControl(set(), set())
633+
return Option(
634+
"--only-final",
635+
dest="release_control",
636+
action="callback",
637+
callback=_handle_only_final,
638+
type="str",
639+
default=release_control,
640+
help="Only allow final releases (no pre-releases) for a package. Can be "
641+
"supplied multiple times, and each time adds to the existing value. "
642+
'Accepts either ":all:" to disable pre-releases for all packages, '
643+
'":none:" to empty the set, or one or more package names with commas '
644+
"between them. Cannot be used with --pre.",
645+
)
646+
647+
648+
def check_release_control_exclusive(options: Values) -> None:
649+
"""
650+
Raise an error if --pre is used with --all-releases or --only-final,
651+
and transform --pre into --all-releases :all: if used alone.
652+
"""
653+
if not hasattr(options, "pre") or not options.pre:
654+
return
655+
656+
release_control = options.release_control
657+
if release_control.all_releases or release_control.only_final:
658+
raise CommandError("--pre cannot be used with --all-releases or --only-final.")
659+
660+
# Transform --pre into --all-releases :all:
661+
release_control.all_releases.add(":all:")
662+
663+
583664
platforms: Callable[..., Option] = partial(
584665
Option,
585666
"--platform",

src/pip/_internal/cli/req_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def _build_package_finder(
359359
selection_prefs = SelectionPreferences(
360360
allow_yanked=True,
361361
format_control=options.format_control,
362-
allow_all_prereleases=options.pre,
362+
release_control=options.release_control,
363363
prefer_binary=options.prefer_binary,
364364
ignore_requires_python=ignore_requires_python,
365365
)

src/pip/_internal/commands/download.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def add_options(self) -> None:
4343
self.cmd_opts.add_option(cmdoptions.prefer_binary())
4444
self.cmd_opts.add_option(cmdoptions.src())
4545
self.cmd_opts.add_option(cmdoptions.pre())
46+
self.cmd_opts.add_option(cmdoptions.all_releases())
47+
self.cmd_opts.add_option(cmdoptions.only_final())
4648
self.cmd_opts.add_option(cmdoptions.require_hashes())
4749
self.cmd_opts.add_option(cmdoptions.progress_bar())
4850
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
@@ -80,6 +82,7 @@ def run(self, options: Values, args: list[str]) -> int:
8082

8183
cmdoptions.check_dist_restriction(options)
8284
cmdoptions.check_build_constraints(options)
85+
cmdoptions.check_release_control_exclusive(options)
8386

8487
options.download_dir = normalize_path(options.download_dir)
8588
ensure_dir(options.download_dir)

src/pip/_internal/commands/index.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def add_options(self) -> None:
4141

4242
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
4343
self.cmd_opts.add_option(cmdoptions.pre())
44+
self.cmd_opts.add_option(cmdoptions.all_releases())
45+
self.cmd_opts.add_option(cmdoptions.only_final())
4446
self.cmd_opts.add_option(cmdoptions.json())
4547
self.cmd_opts.add_option(cmdoptions.no_binary())
4648
self.cmd_opts.add_option(cmdoptions.only_binary())
@@ -59,6 +61,8 @@ def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
5961
}
6062

6163
def run(self, options: Values, args: list[str]) -> int:
64+
cmdoptions.check_release_control_exclusive(options)
65+
6266
handler_map = self.handler_map()
6367

6468
# Determine action
@@ -95,7 +99,7 @@ def _build_package_finder(
9599
# Pass allow_yanked=False to ignore yanked versions.
96100
selection_prefs = SelectionPreferences(
97101
allow_yanked=False,
98-
allow_all_prereleases=options.pre,
102+
release_control=options.release_control,
99103
ignore_requires_python=ignore_requires_python,
100104
)
101105

src/pip/_internal/commands/install.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ def add_options(self) -> None:
8989
self.cmd_opts.add_option(cmdoptions.build_constraints())
9090
self.cmd_opts.add_option(cmdoptions.no_deps())
9191
self.cmd_opts.add_option(cmdoptions.pre())
92+
self.cmd_opts.add_option(cmdoptions.all_releases())
93+
self.cmd_opts.add_option(cmdoptions.only_final())
9294

9395
self.cmd_opts.add_option(cmdoptions.editable())
9496
self.cmd_opts.add_option(
@@ -303,6 +305,7 @@ def run(self, options: Values, args: list[str]) -> int:
303305

304306
cmdoptions.check_build_constraints(options)
305307
cmdoptions.check_dist_restriction(options, check_target=True)
308+
cmdoptions.check_release_control_exclusive(options)
306309

307310
logger.verbose("Using %s", get_pip_version())
308311
options.use_user_site = decide_user_install(

src/pip/_internal/commands/list.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ def add_options(self) -> None:
9999
"pip only finds stable versions."
100100
),
101101
)
102+
self.cmd_opts.add_option(cmdoptions.all_releases())
103+
self.cmd_opts.add_option(cmdoptions.only_final())
102104

103105
self.cmd_opts.add_option(
104106
"--format",
@@ -157,7 +159,7 @@ def _build_package_finder(
157159
# Pass allow_yanked=False to ignore yanked versions.
158160
selection_prefs = SelectionPreferences(
159161
allow_yanked=False,
160-
allow_all_prereleases=options.pre,
162+
release_control=options.release_control,
161163
)
162164

163165
return PackageFinder.create(
@@ -166,6 +168,8 @@ def _build_package_finder(
166168
)
167169

168170
def run(self, options: Values, args: list[str]) -> int:
171+
cmdoptions.check_release_control_exclusive(options)
172+
169173
if options.outdated and options.uptodate:
170174
raise CommandError("Options --outdated and --uptodate cannot be combined.")
171175

src/pip/_internal/commands/lock.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def add_options(self) -> None:
5959
self.cmd_opts.add_option(cmdoptions.build_constraints())
6060
self.cmd_opts.add_option(cmdoptions.no_deps())
6161
self.cmd_opts.add_option(cmdoptions.pre())
62+
self.cmd_opts.add_option(cmdoptions.all_releases())
63+
self.cmd_opts.add_option(cmdoptions.only_final())
6264

6365
self.cmd_opts.add_option(cmdoptions.editable())
6466

@@ -96,6 +98,7 @@ def run(self, options: Values, args: list[str]) -> int:
9698
)
9799

98100
cmdoptions.check_build_constraints(options)
101+
cmdoptions.check_release_control_exclusive(options)
99102

100103
session = self.get_default_session(options)
101104

src/pip/_internal/commands/wheel.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def add_options(self) -> None:
8585
"pip only finds stable versions."
8686
),
8787
)
88+
self.cmd_opts.add_option(cmdoptions.all_releases())
89+
self.cmd_opts.add_option(cmdoptions.only_final())
8890

8991
self.cmd_opts.add_option(cmdoptions.require_hashes())
9092

@@ -99,6 +101,7 @@ def add_options(self) -> None:
99101
@with_cleanup
100102
def run(self, options: Values, args: list[str]) -> int:
101103
cmdoptions.check_build_constraints(options)
104+
cmdoptions.check_release_control_exclusive(options)
102105

103106
session = self.get_default_session(options)
104107

0 commit comments

Comments
 (0)