├── .python-version ├── .pipconfig ├── requirements_dev.txt ├── tests ├── fixtures │ ├── config_without_requirement_dev │ └── default_config └── test_pip_save.py ├── pip_save ├── __init__.py └── cli.py ├── AUTHORS.md ├── MANIFEST.in ├── setup.cfg ├── .editorconfig ├── LICENSE ├── .gitignore ├── README.md ├── Makefile └── setup.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.5.1 2 | -------------------------------------------------------------------------------- /.pipconfig: -------------------------------------------------------------------------------- 1 | [pip-save] 2 | requirement = requirements_dev.txt 3 | use_compatible = False -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | ipython==4.0.0 2 | mock==2.0.0 3 | pytest==3.0.2 4 | six==1.9.0 5 | twine==1.8.1 6 | -------------------------------------------------------------------------------- /tests/fixtures/config_without_requirement_dev: -------------------------------------------------------------------------------- 1 | [pip-save] 2 | requirement = requirements.txt 3 | use_compatible = False 4 | -------------------------------------------------------------------------------- /tests/fixtures/default_config: -------------------------------------------------------------------------------- 1 | [pip-save] 2 | requirement = requirements.txt 3 | use_compatible = False 4 | requirement_dev = requirements_dev.txt 5 | -------------------------------------------------------------------------------- /pip_save/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'Ritesh Kadmawala' 4 | __email__ = 'k.g.ritesh@gmail.com' 5 | __version__ = '0.2.0' 6 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ### Credits 2 | 3 | ######Development Lead 4 | 5 | * Ritesh Kadmawala 6 | 7 | ######Contributors 8 | 9 | None yet. Why not be the first? 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.md 2 | include LICENSE 3 | include README.md 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | 9 | recursive-include *.md conf.py Makefile make.bat 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pip_save/__init__.py] 7 | 8 | [wheel] 9 | universal = 1 10 | 11 | [flake8] 12 | exclude = docs 13 | 14 | [metadata] 15 | description-file = README.md 16 | 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Ritesh Kadmawala 2 | All rights reserved. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | .idea/ 61 | 62 | #MAC 63 | .DS_Store 64 | 65 | 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### pip-save 2 | 3 | [](https://pypi.python.org/pypi/pip-save) 4 | 5 | #### DEPREACATED 6 | I no longer use/support this library. A much better approach to solve this problem is now available at [pipenv](https://github.com/kennethreitz/pipenv). Strongly recommend to try that out 7 | 8 |
9 | 10 | pip-save is a simple wrapper around **pip** so as to add ```npm --save``` style functionality to pip. 11 | 12 | Currently its a big pain while installing new dependencies using pip. After installing the dependency, 13 | you need to figure out the version number and then manually add it to your requirements file. 14 | ``pip-save`` allows you to install/uninstall any dependecy and automatically add/remove 15 | it to/from your requirements file using one command only. 16 | 17 | Since its only a wrapper around pip install and uninstall commands, 18 | it accepts all options/config as these commands. 19 | 20 | #### Installation 21 | 22 | $ pip install pip-save 23 | 24 | #### Usage 25 | 26 | To Install a package and add it to your requirements.tx 27 | 28 | $ pip-save install [] 29 | 30 | To upgrade a package 31 | 32 | $ pip-save install --upgrade [] 33 | 34 | To uninstall a package and remove it from your requirements.txt 35 | 36 | $ pip-save uninstall [] 37 | 38 | To install a package from VCS and add it to your requirements file 39 | 40 | $ pip-save install -e 41 | 42 | 43 | #### Configuration 44 | 45 | For most users the default configuration of pip-save should be fine. If you do 46 | want to change pip-save's defaults you do so by adding configuration options to 47 | a configuration file. If a `.pipconfig` file exists in the current working 48 | directory, its automatically loaded. 49 | 50 | Here is an example of available options along with their default values. 51 | 52 | [pip-save] 53 | requirements = requirements.txt 54 | use_compatible = False 55 | 56 | 57 | ##### Configuration Options 58 | 59 | * requirements:- path to the requirements file to be used. Default value is `requirements.txt` 60 | Can be overwritten by using command line option `-r` or `--requirement` 61 | 62 | * use_compatible:- whether to use compatible version specifier instead of exact versions. 63 | Default value is `False`. Can be overwritten by using command line flag `--use-compatible` 64 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs clean 2 | define BROWSER_PYSCRIPT 3 | import os, webbrowser, sys 4 | try: 5 | from urllib import pathname2url 6 | except: 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 13 | 14 | help: 15 | @echo "clean - remove all build, test, coverage and Python artifacts" 16 | @echo "clean-build - remove build artifacts" 17 | @echo "clean-pyc - remove Python file artifacts" 18 | @echo "clean-test - remove test and coverage artifacts" 19 | @echo "lint - check style with flake8" 20 | @echo "test - run tests quickly with the default Python" 21 | @echo "test-all - run tests on every Python version with tox" 22 | @echo "coverage - check code coverage quickly with the default Python" 23 | @echo "docs - generate Sphinx HTML documentation, including API docs" 24 | @echo "release - package and upload a release" 25 | @echo "dist - package" 26 | @echo "install - install the package to the active Python's site-packages" 27 | 28 | clean: clean-build clean-pyc clean-test 29 | 30 | clean-build: 31 | rm -fr build/ 32 | rm -fr dist/ 33 | rm -fr .eggs/ 34 | find . -name '*.egg-info' -exec rm -fr {} + 35 | find . -name '*.egg' -exec rm -f {} + 36 | 37 | clean-pyc: 38 | find . -name '*.pyc' -exec rm -f {} + 39 | find . -name '*.pyo' -exec rm -f {} + 40 | find . -name '*~' -exec rm -f {} + 41 | find . -name '__pycache__' -exec rm -fr {} + 42 | 43 | clean-test: 44 | rm -fr .tox/ 45 | rm -f .coverage 46 | rm -fr htmlcov/ 47 | 48 | lint: 49 | flake8 pip-save tests 50 | 51 | test: 52 | python setup.py test 53 | 54 | test-all: 55 | tox 56 | 57 | coverage: 58 | coverage run --source pip-save setup.py test 59 | coverage report -m 60 | coverage html 61 | $(BROWSER) htmlcov/index.html 62 | 63 | docs: 64 | rm -f docs/pip-save.rst 65 | rm -f docs/modules.rst 66 | sphinx-apidoc -o docs/ pip-save 67 | $(MAKE) -C docs clean 68 | $(MAKE) -C docs html 69 | $(BROWSER) docs/_build/html/index.html 70 | 71 | servedocs: docs 72 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 73 | 74 | release: clean 75 | python setup.py sdist upload 76 | python setup.py bdist_wheel upload 77 | 78 | dist: clean 79 | python setup.py sdist 80 | python setup.py bdist_wheel 81 | ls -l dist 82 | 83 | install: clean 84 | python setup.py install 85 | -------------------------------------------------------------------------------- /tests/test_pip_save.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import absolute_import 4 | import os 5 | import mock 6 | import functools 7 | 8 | from pip import get_installed_distributions 9 | from six.moves.configparser import ConfigParser 10 | from pip_save.cli import parse_config, parse_requirement 11 | from pip.operations.freeze import freeze 12 | 13 | 14 | def get_installed_packages(): 15 | installed_packages = [] 16 | for line in freeze: 17 | installed_packages.append(line) 18 | 19 | os.chdir(os.path.abspath(os.path.dirname(__file__))) 20 | 21 | INSTALLED_PACKAGES = get_installed_distributions() 22 | 23 | 24 | def prepare_config_file(config_options): 25 | 26 | def read(config_parser, config_file): 27 | config_parser.add_section('pip-save') 28 | for key, value in config_options.items(): 29 | config_parser.set('pip-save', key, str(value)) 30 | 31 | return config_parser 32 | 33 | return read 34 | 35 | 36 | def test_parse_config_file_not_exists(): 37 | default_options = { 38 | 'requirement': 'requirements.txt', 39 | 'use_compatible': False, 40 | 'requirement_dev': 'requirements.txt' 41 | } 42 | 43 | config_dict = parse_config('xyz.txt') 44 | 45 | assert config_dict == default_options 46 | 47 | 48 | def test_parse_config_with_requirements_dev(): 49 | config_dict = parse_config('fixtures/default_config') 50 | assert config_dict == { 51 | 'requirement': 'requirements.txt', 52 | 'use_compatible': False, 53 | 'requirement_dev': 'requirements_dev.txt' 54 | } 55 | 56 | 57 | def test_parse_config_without_requirements_dev(): 58 | config_dict = parse_config('fixtures/config_without_requirement_dev') 59 | assert config_dict == { 60 | 'requirement': 'requirements.txt', 61 | 'use_compatible': False, 62 | 'requirement_dev': 'requirements.txt', 63 | } 64 | 65 | 66 | def test_parse_requirement_installed_package_name(): 67 | pkg = INSTALLED_PACKAGES[0] 68 | pkgname, specs = parse_requirement(pkg.key) 69 | assert pkgname == pkg.key 70 | assert specs == '=={}'.format(pkg.version) 71 | 72 | 73 | def test_parse_requirement_installed_with_specifier(): 74 | pkg = INSTALLED_PACKAGES[-1] 75 | pkgstring = '{}~={}'.format(pkg.key, pkg.version) 76 | pkgname, specs = parse_requirement(pkgstring) 77 | assert pkgname == pkg.key 78 | assert specs == '~={}'.format(pkg.version) 79 | 80 | 81 | def test_parse_requirement_uninstalled_without_specifier(): 82 | pkgname, specs = parse_requirement('xyz') 83 | assert pkgname == 'xyz' 84 | assert specs is '' 85 | 86 | 87 | def test_parse_requirement_uninstalled_with_specifier(): 88 | pkgname, specs = parse_requirement('xyz==1.0.2') 89 | assert pkgname == 'xyz' 90 | assert specs == '==1.0.2' 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | 6 | from setuptools import setup, find_packages 7 | 8 | 9 | def get_readme(): 10 | """Get the contents of the ``README.rst`` file as a Unicode string.""" 11 | try: 12 | import pypandoc 13 | description = pypandoc.convert('README.md', 'rst') 14 | except (IOError, ImportError): 15 | description = open('README.md').read() 16 | 17 | return description 18 | 19 | def get_absolute_path(*args): 20 | """Transform relative pathnames into absolute pathnames.""" 21 | directory = os.path.dirname(os.path.abspath(__file__)) 22 | return os.path.join(directory, *args) 23 | 24 | 25 | def get_version(): 26 | """Get the version of `package` (by extracting it from the source code).""" 27 | module_path = get_absolute_path('pip_save', '__init__.py') 28 | with open(module_path) as handle: 29 | for line in handle: 30 | match = re.match(r'^__version__\s*=\s*["\']([^"\']+)["\']$', line) 31 | if match: 32 | return match.group(1) 33 | raise Exception("Failed to extract version from %s!" % module_path) 34 | 35 | 36 | requirements = [ 37 | 'six == 1.9.0', 38 | ] 39 | 40 | test_requirements = [ 41 | ] 42 | 43 | setup( 44 | name='pip-save', 45 | version=get_version(), 46 | description="A wrapper around pip to add `npm --save` style functionality to pip", 47 | long_description=get_readme(), 48 | author="Ritesh Kadmawala", 49 | author_email='k.g.ritesh@gmail.com', 50 | url='https://github.com/kgritesh/pip-save', 51 | packages=find_packages(), 52 | entry_points={ 53 | 'console_scripts': ['pip-save = pip_save.cli:main'], 54 | }, 55 | include_package_data=True, 56 | install_requires=requirements, 57 | license="ISCL", 58 | zip_safe=False, 59 | keywords='pip-save', 60 | classifiers=[ 61 | 'Development Status :: 4 - Beta', 62 | 'Intended Audience :: Information Technology', 63 | 'Intended Audience :: System Administrators', 64 | 'License :: OSI Approved :: ISC License (ISCL)', 65 | 'Operating System :: MacOS :: MacOS X', 66 | 'Operating System :: Microsoft :: Windows', 67 | 'Operating System :: POSIX :: Linux', 68 | 'Operating System :: Unix', 69 | 'License :: OSI Approved :: ISC License (ISCL)', 70 | 'Natural Language :: English', 71 | 'Programming Language :: Python :: 2.6', 72 | 'Programming Language :: Python :: 2.7', 73 | 'Programming Language :: Python :: 3', 74 | 'Programming Language :: Python :: 3.3', 75 | 'Programming Language :: Python :: 3.4', 76 | 'Programming Language :: Python :: 3.5', 77 | 'Topic :: Software Development :: Build Tools', 78 | 'Topic :: Software Development :: Libraries :: Python Modules', 79 | 'Topic :: System :: Archiving :: Packaging', 80 | 'Topic :: System :: Installation/Setup', 81 | 'Topic :: System :: Software Distribution', 82 | ], 83 | test_suite='tests', 84 | tests_require=test_requirements 85 | ) 86 | -------------------------------------------------------------------------------- /pip_save/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import absolute_import 4 | 5 | import argparse 6 | import operator 7 | import os 8 | import subprocess 9 | import sys 10 | from collections import OrderedDict 11 | from six.moves.configparser import ConfigParser 12 | 13 | import functools 14 | from pip.req import InstallRequirement 15 | from pkg_resources import WorkingSet, Requirement 16 | 17 | DEFAULT_CONFIG_FILE = '.pipconfig' 18 | 19 | DEFAULT_OPTIONS = { 20 | 'requirement': 'requirements.txt', 21 | 'use_compatible': False, 22 | 'requirement_dev': '%(requirement)s' 23 | } 24 | 25 | 26 | def parse_arguments(): 27 | parser = argparse.ArgumentParser(description='Run Pip Command') 28 | parser.add_argument('command', 29 | choices=['install', 'uninstall'], 30 | help="command to execute") 31 | parser.add_argument('-e', '--editable', dest='editables', 32 | action='append', default=[], 33 | metavar='path/url', 34 | help=('Install a project in editable mode (i.e. setuptools ' 35 | '"develop mode") from a local project path or a ' 36 | 'VCS url.'), ) 37 | 38 | parser.add_argument('--config', dest='config_file', 39 | default=DEFAULT_CONFIG_FILE, 40 | help=( 41 | 'Config File To be used' 42 | )) 43 | 44 | parser.add_argument('--dev', dest='dev_requirement', 45 | default=False, action='store_true', 46 | help=('Mark the requirement as a dev requirement and hence its ' 47 | 'removed or added to the dev requirement file')) 48 | return parser 49 | 50 | 51 | def parse_config(config_file=DEFAULT_CONFIG_FILE): 52 | if not os.path.exists(config_file): 53 | config_dict = dict(DEFAULT_OPTIONS) 54 | config_dict['requirement_dev'] = config_dict['requirement'] 55 | return config_dict 56 | 57 | config_dict = {} 58 | config = ConfigParser(DEFAULT_OPTIONS) 59 | config.read(config_file) 60 | config_dict['requirement'] = config.get('pip-save', 'requirement') 61 | 62 | config_dict['use_compatible'] = config.getboolean('pip-save', 63 | 'use_compatible') 64 | 65 | config_dict['requirement_dev'] = config.get('pip-save', 'requirement_dev') 66 | return config_dict 67 | 68 | 69 | def execute_pip_command(command, args): 70 | pip_cmd = ['pip', command] 71 | pip_cmd.extend(args) 72 | return subprocess.call(pip_cmd) 73 | 74 | 75 | def parse_requirement(pkgstring, comparator='=='): 76 | ins = InstallRequirement.from_line(pkgstring) 77 | pkg_name, specs = ins.name, str(ins.specifier) 78 | if specs: 79 | return pkg_name, specs 80 | 81 | req = Requirement.parse(pkg_name) 82 | working_set = WorkingSet() 83 | dist = working_set.find(req) 84 | if dist: 85 | specs = "%s%s" % (comparator, dist.version) 86 | 87 | return req.project_name, specs 88 | 89 | 90 | def parse_editable_requirement(pkgstring): 91 | ins = InstallRequirement.from_editable(pkgstring) 92 | specs = '-e ' 93 | 94 | if ins.link: 95 | return ins.name, specs + str(ins.link) 96 | else: 97 | return ins.name, specs + str(ins.specifier) 98 | 99 | 100 | def sort_requirements(requirements_dict): 101 | def compare(pkg1, pkg2): 102 | name1, req_str1 = pkg1 103 | name2, req_str2 = pkg2 104 | 105 | if req_str2.startswith('-e'): 106 | return -1 107 | elif req_str1.startswith('-e'): 108 | return 1 109 | elif name1.lower() < name2.lower(): 110 | return -1 111 | else: 112 | return 1 113 | 114 | return sorted(requirements_dict.items(), 115 | key=functools.cmp_to_key(compare)) 116 | 117 | 118 | def read_requirements(requirement_file): 119 | existing_requirements = OrderedDict() 120 | with open(requirement_file, "r+") as fd: 121 | for line in fd.readlines(): 122 | line = line.strip() 123 | if not line or line.startswith('#') or line.startswith('-r'): 124 | continue 125 | editable = line.startswith('-e') 126 | line = line.replace('-e ', '').strip() 127 | if editable: 128 | pkg_name, link = parse_editable_requirement(line) 129 | existing_requirements[pkg_name] = link 130 | else: 131 | pkg_name, specifier = parse_requirement(line) 132 | existing_requirements[pkg_name] = '{}{}'.format(pkg_name, 133 | specifier) 134 | return existing_requirements 135 | 136 | 137 | def write_requirements(requirement_file, requirements_dict): 138 | with open(requirement_file, "w") as fd: 139 | for _, req_str in sort_requirements(requirements_dict): 140 | fd.write('{}\n'.format(req_str)) 141 | 142 | 143 | def update_requirement_file(config_dict, command, packages, editables, 144 | dev_requirement=False): 145 | requirement_file = config_dict['requirement_dev'] \ 146 | if dev_requirement else config_dict['requirement'] 147 | 148 | existing_requirements = read_requirements(requirement_file) 149 | update_requirements = OrderedDict() 150 | 151 | for pkg in packages: 152 | pkg_name, specs = parse_requirement(pkg) 153 | update_requirements[pkg_name] = '{}{}'.format(pkg_name, specs) 154 | 155 | for pkg in editables: 156 | pkg_name, link = parse_editable_requirement(pkg) 157 | update_requirements[pkg_name] = link 158 | 159 | if command == 'install': 160 | existing_requirements.update(update_requirements) 161 | else: 162 | for key in update_requirements: 163 | if key in existing_requirements: 164 | del existing_requirements[key] 165 | 166 | write_requirements(requirement_file, existing_requirements) 167 | 168 | 169 | def main(): 170 | parser = parse_arguments() 171 | 172 | args, remaining_args = parser.parse_known_args() 173 | 174 | packages = [] 175 | for arg in remaining_args: 176 | if not arg.startswith('-'): 177 | packages.append(arg) 178 | 179 | for editable in args.editables: 180 | remaining_args.extend(['-e', '{}'.format(editable)]) 181 | 182 | pip_output = execute_pip_command(args.command, remaining_args) 183 | 184 | if pip_output != 0: 185 | return 186 | 187 | config_dict = parse_config(args.config_file) 188 | 189 | update_requirement_file(config_dict, args.command, packages, 190 | args.editables, args.dev_requirement) 191 | 192 | return 0 193 | 194 | if __name__ == '__main__': 195 | sys.exit(main()) 196 | --------------------------------------------------------------------------------