Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/html/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,55 @@ Example build constraints file (``build-constraints.txt``):
# Pin Cython for packages that use it to build
cython==0.29.24

Controlling Pre-release Installation
=====================================

By default, pip installs stable versions of packages, unless their specifier includes
a pre-release version (e.g., ``SomePackage>=1.0a1``) or if there are no stable versions
available that satisfy the requirement. The ``--all-releases``and ``--only-final``
options provide per-package control over pre-release selection.

Use ``--all-releases`` to allow pre-releases for specific packages:

.. tab:: Unix/macOS

.. code-block:: shell

python -m pip install --all-releases=DependencyPackage SomePackage
python -m pip install --all-releases=:all: SomePackage

.. tab:: Windows

.. code-block:: shell

py -m pip install --all-releases=DependencyPackage SomePackage
py -m pip install --all-releases=:all: SomePackage

Use ``--only-final`` to explicitly disable pre-releases for specific packages:

.. tab:: Unix/macOS

.. code-block:: shell

python -m pip install --only-final=DependencyPackage SomePackage
python -m pip install --only-final=:all: SomePackage

.. tab:: Windows

.. code-block:: shell

py -m pip install --only-final=DependencyPackage SomePackage
py -m pip install --only-final=:all: SomePackage

Both options accept ``:all:`` to apply to all packages, ``:none:`` to clear
the setting, or comma-separated package names. Package-specific settings
override ``:all:``. These options can also be used in requirements files.

.. note::

The ``--pre`` flag is equivalent to ``--all-releases :all:`` but cannot
be combined with ``--all-releases`` or ``--only-final``.


.. _`Dependency Groups`:

Expand Down
2 changes: 2 additions & 0 deletions news/13221.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``--all-releases`` and ``--only-final`` options to control pre-release
and final release selection during package installation.
8 changes: 6 additions & 2 deletions src/pip/_internal/build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ def install(
)
)

if finder.release_control is not None:
# Use ordered args to preserve the user's original command-line order
# This is important because later flags can override earlier ones
for attr_name, value in finder.release_control.get_ordered_args():
args.extend(("--" + attr_name.replace("_", "-"), value))

index_urls = finder.index_urls
if index_urls:
args.extend(["-i", index_urls[0]])
Expand All @@ -206,8 +212,6 @@ def install(
args.extend(["--cert", finder.custom_cert])
if finder.client_cert:
args.extend(["--client-cert", finder.client_cert])
if finder.allow_all_prereleases:
args.append("--pre")
if finder.prefer_binary:
args.append("--prefer-binary")

Expand Down
81 changes: 81 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from pip._internal.locations import USER_CACHE_DIR, get_src_prefix
from pip._internal.models.format_control import FormatControl
from pip._internal.models.index import PyPI
from pip._internal.models.release_control import ReleaseControl
from pip._internal.models.target_python import TargetPython
from pip._internal.utils.hashes import STRONG_HASHES
from pip._internal.utils.misc import strtobool
Expand Down Expand Up @@ -580,6 +581,86 @@ def only_binary() -> Option:
)


def _get_release_control(values: Values, option: Option) -> Any:
"""Get a release_control object."""
return getattr(values, option.dest)


def _handle_all_releases(
option: Option, opt_str: str, value: str, parser: OptionParser
) -> None:
existing = _get_release_control(parser.values, option)
existing.handle_mutual_excludes(
value,
existing.all_releases,
existing.only_final,
"all_releases",
)


def _handle_only_final(
option: Option, opt_str: str, value: str, parser: OptionParser
) -> None:
existing = _get_release_control(parser.values, option)
existing.handle_mutual_excludes(
value,
existing.only_final,
existing.all_releases,
"only_final",
)


def all_releases() -> Option:
release_control = ReleaseControl(set(), set())
return Option(
"--all-releases",
dest="release_control",
action="callback",
callback=_handle_all_releases,
type="str",
default=release_control,
help="Allow all release types (including pre-releases) for a package. "
"Can be supplied multiple times, and each time adds to the existing "
'value. Accepts either ":all:" to allow pre-releases for all '
'packages, ":none:" to empty the set (notice the colons), or one or '
"more package names with commas between them (no colons). Cannot be "
"used with --pre.",
)


def only_final() -> Option:
release_control = ReleaseControl(set(), set())
return Option(
"--only-final",
dest="release_control",
action="callback",
callback=_handle_only_final,
type="str",
default=release_control,
help="Only allow final releases (no pre-releases) for a package. Can be "
"supplied multiple times, and each time adds to the existing value. "
'Accepts either ":all:" to disable pre-releases for all packages, '
'":none:" to empty the set, or one or more package names with commas '
"between them. Cannot be used with --pre.",
)


def check_release_control_exclusive(options: Values) -> None:
"""
Raise an error if --pre is used with --all-releases or --only-final,
and transform --pre into --all-releases :all: if used alone.
"""
if not hasattr(options, "pre") or not options.pre:
return

release_control = options.release_control
if release_control.all_releases or release_control.only_final:
raise CommandError("--pre cannot be used with --all-releases or --only-final.")

# Transform --pre into --all-releases :all:
release_control.all_releases.add(":all:")


platforms: Callable[..., Option] = partial(
Option,
"--platform",
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def _build_package_finder(
selection_prefs = SelectionPreferences(
allow_yanked=True,
format_control=options.format_control,
allow_all_prereleases=options.pre,
release_control=options.release_control,
prefer_binary=options.prefer_binary,
ignore_requires_python=ignore_requires_python,
)
Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.prefer_binary())
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.all_releases())
self.cmd_opts.add_option(cmdoptions.only_final())
self.cmd_opts.add_option(cmdoptions.require_hashes())
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
Expand Down Expand Up @@ -80,6 +82,7 @@ def run(self, options: Values, args: list[str]) -> int:

cmdoptions.check_dist_restriction(options)
cmdoptions.check_build_constraints(options)
cmdoptions.check_release_control_exclusive(options)

options.download_dir = normalize_path(options.download_dir)
ensure_dir(options.download_dir)
Expand Down
6 changes: 5 additions & 1 deletion src/pip/_internal/commands/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def add_options(self) -> None:

self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.all_releases())
self.cmd_opts.add_option(cmdoptions.only_final())
self.cmd_opts.add_option(cmdoptions.json())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
Expand All @@ -59,6 +61,8 @@ def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
}

def run(self, options: Values, args: list[str]) -> int:
cmdoptions.check_release_control_exclusive(options)

handler_map = self.handler_map()

# Determine action
Expand Down Expand Up @@ -95,7 +99,7 @@ def _build_package_finder(
# Pass allow_yanked=False to ignore yanked versions.
selection_prefs = SelectionPreferences(
allow_yanked=False,
allow_all_prereleases=options.pre,
release_control=options.release_control,
ignore_requires_python=ignore_requires_python,
)

Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.build_constraints())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.all_releases())
self.cmd_opts.add_option(cmdoptions.only_final())

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

cmdoptions.check_build_constraints(options)
cmdoptions.check_dist_restriction(options, check_target=True)
cmdoptions.check_release_control_exclusive(options)

logger.verbose("Using %s", get_pip_version())
options.use_user_site = decide_user_install(
Expand Down
6 changes: 5 additions & 1 deletion src/pip/_internal/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ def add_options(self) -> None:
"pip only finds stable versions."
),
)
self.cmd_opts.add_option(cmdoptions.all_releases())
self.cmd_opts.add_option(cmdoptions.only_final())

self.cmd_opts.add_option(
"--format",
Expand Down Expand Up @@ -157,7 +159,7 @@ def _build_package_finder(
# Pass allow_yanked=False to ignore yanked versions.
selection_prefs = SelectionPreferences(
allow_yanked=False,
allow_all_prereleases=options.pre,
release_control=options.release_control,
)

return PackageFinder.create(
Expand All @@ -166,6 +168,8 @@ def _build_package_finder(
)

def run(self, options: Values, args: list[str]) -> int:
cmdoptions.check_release_control_exclusive(options)

if options.outdated and options.uptodate:
raise CommandError("Options --outdated and --uptodate cannot be combined.")

Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.build_constraints())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.all_releases())
self.cmd_opts.add_option(cmdoptions.only_final())

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

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

cmdoptions.check_build_constraints(options)
cmdoptions.check_release_control_exclusive(options)

session = self.get_default_session(options)

Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def add_options(self) -> None:
"pip only finds stable versions."
),
)
self.cmd_opts.add_option(cmdoptions.all_releases())
self.cmd_opts.add_option(cmdoptions.only_final())

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

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

session = self.get_default_session(options)

Expand Down
Loading