From e949dc4522f81f738e4b446028cdfd4b9c6603b1 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Sun, 24 Jul 2016 18:06:08 +0300 Subject: [PATCH 1/2] add --save and --save-to options to install --- pip/commands/install.py | 32 +++++++++++++++++++++++++++++++- tests/functional/test_install.py | 8 ++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pip/commands/install.py b/pip/commands/install.py index 28d30c59f7b..cf53f530535 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -3,9 +3,10 @@ import logging import operator import os -import tempfile import shutil +import tempfile import warnings + try: import wheel except ImportError: @@ -159,6 +160,20 @@ def __init__(self, *args, **kw): help="Do not compile py files to pyc", ) + cmd_opts.add_option( + '--save', + action='store_true', + dest='save', + default=False, + help='Add package(s) to requirements file' + ) + + cmd_opts.add_option( + '--save-to', + dest='save_to', + help='Path to the requirements file' + ) + cmd_opts.add_option(cmdoptions.use_wheel()) cmd_opts.add_option(cmdoptions.no_use_wheel()) cmd_opts.add_option(cmdoptions.no_binary()) @@ -394,4 +409,19 @@ def run(self, options, args): target_item_dir ) shutil.rmtree(temp_target_dir) + + if options.save: + if options.save_to: + requirements_fpath = options.save_to + else: + requirements_fpath = 'requirements.txt' + + with open(requirements_fpath, 'a') as requirements_file: + for requirement in requirement_set.requirements.values(): + if not requirement.comes_from: # not a dependency of smth + requirements_file.write( + '{pkg}=={pkg_version}\n'.format( + pkg=requirement.name, + pkg_version=requirement.installed_version)) + return requirement_set diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1440f3335f9..04fc75459ff 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1026,3 +1026,11 @@ def test_double_install_fail(script, data): msg = ("Double requirement given: pip==7.1.2 (already in pip==*, " "name='pip')") assert msg in result.stderr + + +def test_save_installed_package_in_requirements(script, tmpdir): + requirements_fpath = tmpdir.join('requirements.txt') + script.pip('install', '--save', + '--save-to', requirements_fpath, 'pip') + with open(requirements_fpath, 'r') as requirements_file: + assert 'pip==' in requirements_file.read() From 0e4686beb4d4bf90af878f01c2c65fcbd9341876 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Mon, 25 Jul 2016 00:59:24 +0300 Subject: [PATCH 2/2] add more test to --save, preserve file content and update lines if exists --- pip/commands/install.py | 66 +++++++++++++++++++++------ tests/functional/test_install.py | 8 ---- tests/functional/test_install_save.py | 56 +++++++++++++++++++++++ 3 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 tests/functional/test_install_save.py diff --git a/pip/commands/install.py b/pip/commands/install.py index cf53f530535..03a95b4f06d 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -3,10 +3,12 @@ import logging import operator import os -import shutil import tempfile +import shutil import warnings +from pip._vendor import six + try: import wheel except ImportError: @@ -29,6 +31,11 @@ logger = logging.getLogger(__name__) +if six.PY2: + class FileNotFoundError(OSError): + pass + + class InstallCommand(RequirementCommand): """ Install packages from: @@ -171,6 +178,7 @@ def __init__(self, *args, **kw): cmd_opts.add_option( '--save-to', dest='save_to', + default='requirements.txt', help='Path to the requirements file' ) @@ -191,6 +199,18 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): + if options.save: + requirements_fpath = options.save_to + if not os.path.isabs(requirements_fpath): + requirements_fpath = os.path.join(os.getcwd(), + requirements_fpath) + + basedir = os.path.dirname(requirements_fpath) + if not os.path.exists(basedir): + raise FileNotFoundError( + 'Directory for the requirements file doesn\'t exist: ' + '{basedir}.'.format(basedir=basedir)) + cmdoptions.resolve_wheel_no_use_binary(options) cmdoptions.check_install_build_global(options) @@ -411,17 +431,37 @@ def run(self, options, args): shutil.rmtree(temp_target_dir) if options.save: - if options.save_to: - requirements_fpath = options.save_to - else: - requirements_fpath = 'requirements.txt' - - with open(requirements_fpath, 'a') as requirements_file: - for requirement in requirement_set.requirements.values(): - if not requirement.comes_from: # not a dependency of smth - requirements_file.write( - '{pkg}=={pkg_version}\n'.format( - pkg=requirement.name, - pkg_version=requirement.installed_version)) + lines = [] + if os.path.exists(requirements_fpath): + saved_packages = {} + with open(requirements_fpath, 'r') as requirements_file: + req_lines = requirements_file.readlines() + for line_number, line in enumerate(req_lines): + lines.append(line) + if '==' in line: + name, _ = line.rstrip().split('==') + saved_packages[name] = line_number + + for requirement in requirement_set.requirements.values(): + if not requirement.comes_from: + pkg_name = requirement.name + pkg_version = requirement.installed_version + pkg_output_line = '{pkg_name}=={pkg_version}\n'.format( + pkg_name=pkg_name, + pkg_version=pkg_version) + + if len(lines) == 0: + lines.append(pkg_output_line) + continue + + if pkg_name in saved_packages: + # if same version, nothing bad will happen + line_number = saved_packages[pkg_name] + lines[line_number] = pkg_output_line + else: + lines.append(pkg_output_line) + + with open(requirements_fpath, 'w') as requirements_file: + requirements_file.writelines(lines) return requirement_set diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 04fc75459ff..1440f3335f9 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1026,11 +1026,3 @@ def test_double_install_fail(script, data): msg = ("Double requirement given: pip==7.1.2 (already in pip==*, " "name='pip')") assert msg in result.stderr - - -def test_save_installed_package_in_requirements(script, tmpdir): - requirements_fpath = tmpdir.join('requirements.txt') - script.pip('install', '--save', - '--save-to', requirements_fpath, 'pip') - with open(requirements_fpath, 'r') as requirements_file: - assert 'pip==' in requirements_file.read() diff --git a/tests/functional/test_install_save.py b/tests/functional/test_install_save.py new file mode 100644 index 00000000000..0282166e628 --- /dev/null +++ b/tests/functional/test_install_save.py @@ -0,0 +1,56 @@ +def test_save_installed_package_in_requirements(script): + freezed_requests = 'requests==1.0.0' + script.pip('install', '--save', freezed_requests) + + requirements_fpath = script.cwd.join('requirements.txt') + with open(requirements_fpath, 'r') as requirements_file: + assert freezed_requests in requirements_file.read() + + +def test_save_can_specify_save_to_file(script, tmpdir): + requirements_fpath = tmpdir.join('base.txt') + freezed_requests = 'requests==1.0.0' + script.pip('install', '--save', + '--save-to', requirements_fpath, freezed_requests) + + with open(requirements_fpath, 'r') as requirements_file: + assert freezed_requests in requirements_file.read() + + +def test_save_fails_if_base_directory_not_exists(script, tmpdir): + requirements_fpath = tmpdir.join('base/requirements.txt') + freezed_requests = 'requests==1.0.0' + result = script.pip('install', '--save', + '--save-to', requirements_fpath, freezed_requests, + expect_error=True) + assert result.returncode == 2 + assert 'FileNotFoundError' in result.stderr + + +def test_save_only_updates_package_line_if_already_exists(script, tmpdir): + requirements_fpath = tmpdir.join('requirements.txt') + existing_requests_line = 'requests==1.0.0' + with open(requirements_fpath, 'w') as requirements_file: + requirements_file.write(existing_requests_line) + + updated_requests_line = 'requests==1.0.1' + script.pip('install', '--save', + '--save-to', requirements_fpath, updated_requests_line) + + with open(requirements_fpath, 'r') as requirements_file: + assert updated_requests_line in requirements_file.read() + + +def test_save_preserves_original_content_of_requirements_file(script, tmpdir): + requirements_fpath = tmpdir.join('dev-requirements.txt') + original_line = '-r requirements.txt' + with open(requirements_fpath, 'w') as requirements_file: + requirements_file.write(original_line + '\n') + + requests_line = 'requests==1.0.1' + script.pip('install', '--save', + '--save-to', requirements_fpath, requests_line) + with open(requirements_fpath, 'r') as requirements_file: + file_contents = requirements_file.read() + assert original_line in file_contents + assert requests_line in file_contents