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

Fix #5499: Include in pip's User-Agent whether it looks like pip is in CI #6273

Merged
merged 2 commits into from
Feb 24, 2019
Merged
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
2 changes: 2 additions & 0 deletions news/5499.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Include in pip's User-Agent string whether it looks like pip is running
under CI.
34 changes: 34 additions & 0 deletions src/pip/_internal/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,34 @@
logger = logging.getLogger(__name__)


# These are environment variables present when running under various
# CI systems. For each variable, some CI systems that use the variable
# are indicated. The collection was chosen so that for each of a number
# of popular systems, at least one of the environment variables is used.
# This list is used to provide some indication of and lower bound for
# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive.
# For more background, see: https://github.com/pypa/pip/issues/5499
CI_ENVIRONMENT_VARIABLES = (
# Azure Pipelines
'BUILD_BUILDID',
# Jenkins
'BUILD_ID',
# AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI
'CI',
)


def looks_like_ci():
# type: () -> bool
"""
Return whether it looks like pip is running under CI.
"""
# We don't use the method of checking for a tty (e.g. using isatty())
# because some CI systems mimic a tty (e.g. Travis CI). Thus that
# method doesn't provide definitive information in either direction.
return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES)


def user_agent():
"""
Return a string representing the user agent.
Expand Down Expand Up @@ -135,6 +163,12 @@ def user_agent():
if setuptools_version is not None:
data["setuptools_version"] = setuptools_version

# Use None rather than False so as not to give the impression that
# pip knows it is not being run under CI. Rather, it is a null or
# inconclusive result. Also, we include some value rather than no
# value to make it easier to know that the check has been run.
data["ci"] = True if looks_like_ci() else None

return "{data[installer][name]}/{data[installer][version]} {json}".format(
data=data,
json=json.dumps(data, separators=(",", ":"), sort_keys=True),
Expand Down
39 changes: 36 additions & 3 deletions tests/unit/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import pip
from pip._internal.download import (
PipSession, SafeFileCache, path_to_url, unpack_file_url, unpack_http_url,
url_to_path,
CI_ENVIRONMENT_VARIABLES, PipSession, SafeFileCache, path_to_url,
unpack_file_url, unpack_http_url, url_to_path,
)
from pip._internal.exceptions import HashMismatch
from pip._internal.models.link import Link
Expand Down Expand Up @@ -50,8 +50,41 @@ def _fake_session_get(*args, **kwargs):
rmtree(temp_dir)


def get_user_agent():
return PipSession().headers["User-Agent"]


def test_user_agent():
PipSession().headers["User-Agent"].startswith("pip/%s" % pip.__version__)
user_agent = get_user_agent()

assert user_agent.startswith("pip/%s" % pip.__version__)


@pytest.mark.parametrize('name, expected_like_ci', [
('BUILD_BUILDID', True),
('BUILD_ID', True),
('CI', True),
# Test a prefix substring of one of the variable names we use.
('BUILD', False),
])
def test_user_agent__ci(monkeypatch, name, expected_like_ci):
# Delete the variable names we use to check for CI to prevent the
# detection from always returning True in case the tests are being run
# under actual CI. It is okay to depend on CI_ENVIRONMENT_VARIABLES
# here (part of the code under test) because this setup step can only
# prevent false test failures. It can't cause a false test passage.
for ci_name in CI_ENVIRONMENT_VARIABLES:
monkeypatch.delenv(ci_name, raising=False)

# Confirm the baseline before setting the environment variable.
user_agent = get_user_agent()
assert '"ci":null' in user_agent
assert '"ci":true' not in user_agent

monkeypatch.setenv(name, 'true')
user_agent = get_user_agent()
assert ('"ci":true' in user_agent) == expected_like_ci
assert ('"ci":null' in user_agent) == (not expected_like_ci)


class FakeStream(object):
Expand Down