Skip to content

Commit 9b6bf6b

Browse files
authored
Merge branch 'master' into pip609-v2-sbi
2 parents d0d3d7f + 657cf25 commit 9b6bf6b

File tree

11 files changed

+137
-101
lines changed

11 files changed

+137
-101
lines changed

docs/html/reference/pip_install.rst

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -384,46 +384,52 @@ where ``setup.py`` is not in the root of project, the "subdirectory" component
384384
is used. The value of the "subdirectory" component should be a path starting
385385
from the root of the project to where ``setup.py`` is located.
386386

387-
So if your repository layout is:
387+
If your repository layout is::
388388

389-
- pkg_dir/
389+
pkg_dir
390+
├── setup.py # setup.py for package "pkg"
391+
└── some_module.py
392+
other_dir
393+
└── some_file
394+
some_other_file
390395

391-
- setup.py # setup.py for package ``pkg``
392-
- some_module.py
393-
- other_dir/
396+
Then, to install from this repository, the syntax would be::
394397

395-
- some_file
396-
- some_other_file
397-
398-
You'll need to use ``pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"``.
398+
$ pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"
399399

400400

401401
Git
402402
^^^
403403

404404
pip currently supports cloning over ``git``, ``git+http``, ``git+https``,
405-
``git+ssh``, ``git+git`` and ``git+file``:
405+
``git+ssh``, ``git+git`` and ``git+file``.
406+
407+
.. warning::
408+
409+
Note that the use of ``git``, ``git+git``, and ``git+http`` is discouraged.
410+
The former two use `the Git Protocol`_, which lacks authentication, and HTTP is
411+
insecure due to lack of TLS based encryption.
406412

407413
Here are the supported forms::
408414

409-
[-e] git://git.example.com/MyProject#egg=MyProject
410415
[-e] git+http://git.example.com/MyProject#egg=MyProject
411416
[-e] git+https://git.example.com/MyProject#egg=MyProject
412417
[-e] git+ssh://git.example.com/MyProject#egg=MyProject
413-
[-e] git+git://git.example.com/MyProject#egg=MyProject
414418
[-e] git+file:///home/user/projects/MyProject#egg=MyProject
415419

416420
Passing a branch name, a commit hash, a tag name or a git ref is possible like so::
417421

418-
[-e] git://git.example.com/MyProject.git@master#egg=MyProject
419-
[-e] git://git.example.com/[email protected]#egg=MyProject
420-
[-e] git://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject
421-
[-e] git://git.example.com/MyProject.git@refs/pull/123/head#egg=MyProject
422+
[-e] git+https://git.example.com/MyProject.git@master#egg=MyProject
423+
[-e] git+https://git.example.com/[email protected]#egg=MyProject
424+
[-e] git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject
425+
[-e] git+https://git.example.com/MyProject.git@refs/pull/123/head#egg=MyProject
422426

423427
When passing a commit hash, specifying a full hash is preferable to a partial
424428
hash because a full hash allows pip to operate more efficiently (e.g. by
425429
making fewer network calls).
426430

431+
.. _`the Git Protocol`: https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols
432+
427433
Mercurial
428434
^^^^^^^^^
429435

news/1983.doc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Emphasize that VCS URLs using git, git+git and git+http are insecure due to
2+
lack of authentication and encryption

news/7402.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reject VCS URLs with an empty revision.

news/7699.bugfix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Use better mechanism for handling temporary files, when recording metadata
2+
about installed files (RECORD) and the installer (INSTALLER).

src/pip/_internal/operations/install/wheel.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from pip._internal.exceptions import InstallationError
2929
from pip._internal.locations import get_major_minor_version
3030
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
31+
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
3132
from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file
3233
from pip._internal.utils.temp_dir import TempDirectory
3334
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@@ -37,7 +38,7 @@
3738
if MYPY_CHECK_RUNNING:
3839
from email.message import Message
3940
from typing import (
40-
Dict, List, Optional, Sequence, Tuple, IO, Text, Any,
41+
Dict, List, Optional, Sequence, Tuple, Any,
4142
Iterable, Callable, Set,
4243
)
4344

@@ -65,15 +66,15 @@ def rehash(path, blocksize=1 << 20):
6566
return (digest, str(length)) # type: ignore
6667

6768

68-
def open_for_csv(name, mode):
69-
# type: (str, Text) -> IO[Any]
70-
if sys.version_info[0] < 3:
71-
nl = {} # type: Dict[str, Any]
72-
bin = 'b'
69+
def csv_io_kwargs(mode):
70+
# type: (str) -> Dict[str, Any]
71+
"""Return keyword arguments to properly open a CSV file
72+
in the given mode.
73+
"""
74+
if sys.version_info.major < 3:
75+
return {'mode': '{}b'.format(mode)}
7376
else:
74-
nl = {'newline': ''} # type: Dict[str, Any]
75-
bin = ''
76-
return open(name, mode + bin, **nl)
77+
return {'mode': mode, 'newline': ''}
7778

7879

7980
def fix_script(path):
@@ -565,12 +566,11 @@ def is_entrypoint_wrapper(name):
565566
logger.warning(msg)
566567

567568
# Record pip as the installer
568-
installer = os.path.join(dest_info_dir, 'INSTALLER')
569-
temp_installer = os.path.join(dest_info_dir, 'INSTALLER.pip')
570-
with open(temp_installer, 'wb') as installer_file:
569+
installer_path = os.path.join(dest_info_dir, 'INSTALLER')
570+
with adjacent_tmp_file(installer_path) as installer_file:
571571
installer_file.write(b'pip\n')
572-
shutil.move(temp_installer, installer)
573-
generated.append(installer)
572+
replace(installer_file.name, installer_path)
573+
generated.append(installer_path)
574574

575575
# Record the PEP 610 direct URL reference
576576
if direct_url is not None:
@@ -584,20 +584,18 @@ def is_entrypoint_wrapper(name):
584584
generated.append(direct_url_path)
585585

586586
# Record details of all files installed
587-
record = os.path.join(dest_info_dir, 'RECORD')
588-
temp_record = os.path.join(dest_info_dir, 'RECORD.pip')
589-
with open_for_csv(record, 'r') as record_in:
590-
with open_for_csv(temp_record, 'w+') as record_out:
591-
reader = csv.reader(record_in)
592-
outrows = get_csv_rows_for_installed(
593-
reader, installed=installed, changed=changed,
594-
generated=generated, lib_dir=lib_dir,
595-
)
596-
writer = csv.writer(record_out)
597-
# Sort to simplify testing.
598-
for row in sorted_outrows(outrows):
599-
writer.writerow(row)
600-
shutil.move(temp_record, record)
587+
record_path = os.path.join(dest_info_dir, 'RECORD')
588+
with open(record_path, **csv_io_kwargs('r')) as record_file:
589+
rows = get_csv_rows_for_installed(
590+
csv.reader(record_file),
591+
installed=installed,
592+
changed=changed,
593+
generated=generated,
594+
lib_dir=lib_dir)
595+
with adjacent_tmp_file(record_path, **csv_io_kwargs('w')) as record_file:
596+
writer = csv.writer(record_file)
597+
writer.writerows(sorted_outrows(rows)) # sort to simplify testing
598+
replace(record_file.name, record_path)
601599

602600

603601
def install_wheel(

src/pip/_internal/utils/filesystem.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast
1818

1919
if MYPY_CHECK_RUNNING:
20-
from typing import BinaryIO, Iterator
20+
from typing import Any, BinaryIO, Iterator
2121

2222
class NamedTemporaryFileResult(BinaryIO):
2323
@property
@@ -85,16 +85,22 @@ def is_socket(path):
8585

8686

8787
@contextmanager
88-
def adjacent_tmp_file(path):
89-
# type: (str) -> Iterator[NamedTemporaryFileResult]
90-
"""Given a path to a file, open a temp file next to it securely and ensure
91-
it is written to disk after the context reaches its end.
88+
def adjacent_tmp_file(path, **kwargs):
89+
# type: (str, **Any) -> Iterator[NamedTemporaryFileResult]
90+
"""Return a file-like object pointing to a tmp file next to path.
91+
92+
The file is created securely and is ensured to be written to disk
93+
after the context reaches its end.
94+
95+
kwargs will be passed to tempfile.NamedTemporaryFile to control
96+
the way the temporary file will be opened.
9297
"""
9398
with NamedTemporaryFile(
9499
delete=False,
95100
dir=os.path.dirname(path),
96101
prefix=os.path.basename(path),
97102
suffix='.tmp',
103+
**kwargs
98104
) as f:
99105
result = cast('NamedTemporaryFileResult', f)
100106
try:

src/pip/_internal/vcs/versioncontrol.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pip._vendor import pkg_resources
1212
from pip._vendor.six.moves.urllib import parse as urllib_parse
1313

14-
from pip._internal.exceptions import BadCommand
14+
from pip._internal.exceptions import BadCommand, InstallationError
1515
from pip._internal.utils.compat import samefile
1616
from pip._internal.utils.misc import (
1717
ask_path_exists,
@@ -436,6 +436,12 @@ def get_url_rev_and_auth(cls, url):
436436
rev = None
437437
if '@' in path:
438438
path, rev = path.rsplit('@', 1)
439+
if not rev:
440+
raise InstallationError(
441+
"The URL {!r} has an empty revision (after @) "
442+
"which is not supported. Include a revision after @ "
443+
"or remove @ from the URL.".format(url)
444+
)
439445
url = urllib_parse.urlunsplit((scheme, netloc, path, query, ''))
440446
return url, rev, user_pass
441447

tests/functional/test_yaml.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
"""Tests for the resolver
1+
"""
2+
Tests for the resolver
23
"""
34

45
import os
56
import re
67

78
import pytest
9+
import yaml
810

911
from tests.lib import DATA_DIR, create_basic_wheel_for_package, path_to_url
10-
from tests.lib.yaml_helpers import generate_yaml_tests, id_func
1112

12-
_conflict_finder_re = re.compile(
13+
_conflict_finder_pat = re.compile(
1314
# Conflicting Requirements: \
1415
# A 1.0.0 requires B == 2.0.0, C 1.0.0 requires B == 1.0.0.
1516
r"""
@@ -24,7 +25,49 @@
2425
)
2526

2627

27-
def _convert_to_dict(string):
28+
def generate_yaml_tests(directory):
29+
"""
30+
Generate yaml test cases from the yaml files in the given directory
31+
"""
32+
for yml_file in directory.glob("*/*.yml"):
33+
data = yaml.safe_load(yml_file.read_text())
34+
assert "cases" in data, "A fixture needs cases to be used in testing"
35+
36+
# Strip the parts of the directory to only get a name without
37+
# extension and resolver directory
38+
base_name = str(yml_file)[len(str(directory)) + 1:-4]
39+
40+
base = data.get("base", {})
41+
cases = data["cases"]
42+
43+
for i, case_template in enumerate(cases):
44+
case = base.copy()
45+
case.update(case_template)
46+
47+
case[":name:"] = base_name
48+
if len(cases) > 1:
49+
case[":name:"] += "-" + str(i)
50+
51+
if case.pop("skip", False):
52+
case = pytest.param(case, marks=pytest.mark.xfail)
53+
54+
yield case
55+
56+
57+
def id_func(param):
58+
"""
59+
Give a nice parameter name to the generated function parameters
60+
"""
61+
if isinstance(param, dict) and ":name:" in param:
62+
return param[":name:"]
63+
64+
retval = str(param)
65+
if len(retval) > 25:
66+
retval = retval[:20] + "..." + retval[-2:]
67+
return retval
68+
69+
70+
def convert_to_dict(string):
2871

2972
def stripping_split(my_str, splitwith, count=None):
3073
if count is None:
@@ -89,7 +132,7 @@ def handle_install_request(script, requirement):
89132
message = result.stderr.rsplit("\n", 1)[-1]
90133

91134
# XXX: There might be a better way than parsing the message
92-
for match in re.finditer(message, _conflict_finder_re):
135+
for match in re.finditer(message, _conflict_finder_pat):
93136
di = match.groupdict()
94137
retval["conflicting"].append(
95138
{
@@ -119,7 +162,7 @@ def test_yaml_based(script, case):
119162
# XXX: This doesn't work because this isn't making an index of files.
120163
for package in available:
121164
if isinstance(package, str):
122-
package = _convert_to_dict(package)
165+
package = convert_to_dict(package)
123166

124167
assert isinstance(package, dict), "Needs to be a dictionary"
125168

tests/lib/yaml_helpers.py

Lines changed: 0 additions & 43 deletions
This file was deleted.

tests/unit/test_vcs.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from mock import patch
66
from pip._vendor.packaging.version import parse as parse_version
77

8-
from pip._internal.exceptions import BadCommand
8+
from pip._internal.exceptions import BadCommand, InstallationError
99
from pip._internal.utils.misc import hide_url, hide_value
1010
from pip._internal.vcs import make_vcs_requirement_url
1111
from pip._internal.vcs.bazaar import Bazaar
@@ -292,6 +292,21 @@ def test_version_control__get_url_rev_and_auth__missing_plus(url):
292292
assert 'malformed VCS url' in str(excinfo.value)
293293

294294

295+
@pytest.mark.parametrize('url', [
296+
# Test a URL with revision part as empty.
297+
'git+https://github.com/MyUser/myProject.git@#egg=py_pkg',
298+
])
299+
def test_version_control__get_url_rev_and_auth__no_revision(url):
300+
"""
301+
Test passing a URL to VersionControl.get_url_rev_and_auth() with
302+
empty revision
303+
"""
304+
with pytest.raises(InstallationError) as excinfo:
305+
VersionControl.get_url_rev_and_auth(url)
306+
307+
assert 'an empty revision (after @)' in str(excinfo.value)
308+
309+
295310
@pytest.mark.parametrize('url, expected', [
296311
# Test http.
297312
('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject',

tests/unit/test_wheel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def call_get_csv_rows_for_installed(tmpdir, text):
146146
generated = []
147147
lib_dir = '/lib/dir'
148148

149-
with wheel.open_for_csv(path, 'r') as f:
149+
with open(path, **wheel.csv_io_kwargs('r')) as f:
150150
reader = csv.reader(f)
151151
outrows = wheel.get_csv_rows_for_installed(
152152
reader, installed=installed, changed=changed,

0 commit comments

Comments
 (0)