Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unstable Feature Flags #5727

Closed
wants to merge 6 commits into from
Closed
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
4 changes: 4 additions & 0 deletions docs/html/development/release-process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ document existing behavior with the intention of covering that behavior with
the above deprecation process are always acceptable, and will be considered on
their merits.

Any user-visible behavior enabled using a feature flag (i.e. the ``-X`` or
the ``--unstable-feature`` option) is not subject to these guarantees and can
be changed or be removed without a deprecation period.

.. note::

pip has a helper function for making deprecation easier for pip maintainers.
Expand Down
3 changes: 3 additions & 0 deletions news/5727.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add a ``-X``/``--unstable-feature`` flag as a way to incrementally introducing
new functionality. Any functionality provided behind this flag is exempted from
the regular deprecation policy.
7 changes: 7 additions & 0 deletions src/pip/_internal/cli/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pip._internal.utils.misc import get_prog, normalize_path
from pip._internal.utils.outdated import pip_version_check
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.utils.unstable import UnstableFeaturesHelper

if MYPY_CHECK_RUNNING:
from typing import Optional # noqa: F401
Expand All @@ -46,6 +47,9 @@ class Command(object):
ignore_require_venv = False # type: bool

def __init__(self, isolated=False):

self.unstable = UnstableFeaturesHelper()

parser_kw = {
'usage': self.usage,
'prog': '%s %s' % (get_prog(), self.name),
Expand Down Expand Up @@ -139,6 +143,9 @@ def main(self, args):
)
sys.exit(VIRTUALENV_NOT_FOUND)

# Raises an error if an unknown unstable feature is given.
self.unstable.validate(options.unstable_features)

try:
status = self.run(options, args)
# FIXME: all commands should return an exit status
Expand Down
13 changes: 13 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,18 @@ def _merge_hash(option, opt_str, value, parser):
) # type: Any


def unstable_features():
return Option(
'-X', '--unstable-feature',
dest='unstable_features',
metavar='feature',
action='append',
default=[],
help="Enable unstable functionality that is exempted from backwards "
"compatibility guarantees.",
)


##########
# groups #
##########
Expand Down Expand Up @@ -699,6 +711,7 @@ def _merge_hash(option, opt_str, value, parser):
no_cache,
disable_pip_version_check,
no_color,
unstable_features,
]
}

Expand Down
14 changes: 14 additions & 0 deletions src/pip/_internal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,17 @@ def hash_then_or(hash_name):
class UnsupportedPythonVersion(InstallationError):
"""Unsupported python version according to Requires-Python package
metadata."""


class UnknownUnstableFeatures(PipError):
"""Unstable feature names passed are not known.
"""

def __init__(self, unknown_names):
super(UnknownUnstableFeatures, self).__init__(unknown_names)
pradyunsg marked this conversation as resolved.
Show resolved Hide resolved
self.unknown_names = unknown_names

def __str__(self):
return "UnknownUnstableFeatures: {}".format(
", ".join(map(repr, sorted(self.unknown_names)))
)
26 changes: 26 additions & 0 deletions src/pip/_internal/utils/unstable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

from pip._internal.exceptions import UnknownUnstableFeatures


class UnstableFeaturesHelper(object):
"""Handles logic for registering/validating/checking
"""

def __init__(self):
super(UnstableFeaturesHelper, self).__init__()
pradyunsg marked this conversation as resolved.
Show resolved Hide resolved
self._names = set()
self._enabled_names = set()

def register(self, *names):
self._names.update(set(names))

def validate(self, given_names):
pradyunsg marked this conversation as resolved.
Show resolved Hide resolved
# Remember the given names as they are "enabled" features
self._enabled_names = set(given_names)

unknown_names = self._enabled_names - self._names
if unknown_names:
raise UnknownUnstableFeatures(unknown_names)

def is_enabled(self, name):
return name in self._enabled_names
10 changes: 10 additions & 0 deletions tests/unit/test_base_command.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging

from mock import Mock

from pip._internal.cli.base_command import Command


Expand Down Expand Up @@ -32,6 +34,14 @@ def run(self, options, args):
)


def test_base_command_unstable_validation_done_in_main():
cmd = FakeCommand()
cmd.unstable = Mock()
cmd.main(['fake'])

cmd.unstable.validate.assert_called_once_with([])


class Test_base_command_logging(object):
"""
Test `pip.base_command.Command` setting up logging consumers based on
Expand Down
31 changes: 30 additions & 1 deletion tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from pip._vendor.six import BytesIO

from pip._internal.exceptions import (
HashMismatch, HashMissing, InstallationError, UnsupportedPythonVersion,
HashMismatch, HashMissing, InstallationError, UnknownUnstableFeatures,
UnsupportedPythonVersion,
)
from pip._internal.utils.encoding import auto_decode
from pip._internal.utils.glibc import check_glibc_version
Expand All @@ -29,6 +30,7 @@
)
from pip._internal.utils.packaging import check_dist_requires_python
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.unstable import UnstableFeaturesHelper


class Tests_EgglinkPath:
Expand Down Expand Up @@ -665,3 +667,30 @@ def test_split_auth_from_netloc(netloc, expected):
def test_remove_auth_from_url(auth_url, expected_url):
url = remove_auth_from_url(auth_url)
assert url == expected_url


class TestUnstableFeaturesHelper(object):

def test_does_not_enable_on_registration(self):
unstable = UnstableFeaturesHelper()
unstable.register("name1", "name2")

assert not unstable.is_enabled("name1")
assert not unstable.is_enabled("name2")

def test_errors_when_validating_unregistered_name(self):
unstable = UnstableFeaturesHelper()
unstable.register("name1", "name2", "name3")

with pytest.raises(UnknownUnstableFeatures) as e:
unstable.validate(["name3", "name4", "name5"])

assert str(e.value) == "UnknownUnstableFeatures: 'name4', 'name5'"

def test_enables_names_on_validation(self):
unstable = UnstableFeaturesHelper()
unstable.register("name1", "name2")
unstable.validate(["name1"])

assert unstable.is_enabled("name1")
assert not unstable.is_enabled("name2")