├── tests ├── __init__.py └── unit │ ├── __init__.py │ ├── helpers │ ├── __init__.py │ ├── test_cli.py │ └── test_requirements.py │ ├── conftest.py │ └── test_scaffold.py ├── .coveragerc ├── AUTHORS ├── setup.cfg ├── reqwire.sublime-project ├── src └── reqwire │ ├── helpers │ ├── __init__.py │ ├── cli.py │ └── requirements.py │ ├── __main__.py │ ├── __init__.py │ ├── config.py │ ├── errors.py │ ├── scaffold.py │ └── cli.py ├── docs ├── source │ ├── _static │ │ └── ribbon-slava-bowman.jpg │ ├── index.rst │ ├── api.rst │ ├── quickstart.rst │ ├── conf.py │ ├── primer.rst │ ├── release_notes.rst │ └── guide.rst ├── Makefile └── make.bat ├── MANIFEST.in ├── .travis.yml ├── mypy.ini ├── .editorconfig ├── requirements ├── src │ ├── docs.in │ ├── test.in │ ├── main.in │ └── qa.in └── lck │ ├── qa.txt │ ├── main.txt │ ├── test.txt │ └── docs.txt ├── LICENSE ├── .gitignore ├── tox.ini ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = src/reqwire 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | David Gidwani 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # -*- mode: ini -*- 2 | # vim: set ft=ini 3 | [bdist_wheel] 4 | universal = 1 5 | -------------------------------------------------------------------------------- /reqwire.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/reqwire/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | """Various helper utilities.""" 2 | from __future__ import absolute_import 3 | -------------------------------------------------------------------------------- /docs/source/_static/ribbon-slava-bowman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darvid/reqwire/HEAD/docs/source/_static/ribbon-slava-bowman.jpg -------------------------------------------------------------------------------- /src/reqwire/__main__.py: -------------------------------------------------------------------------------- 1 | """Provides the command-line entrypoint for reqwire.""" 2 | from __future__ import absolute_import 3 | 4 | import reqwire.cli 5 | 6 | 7 | reqwire.cli.main() 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include MANIFEST.in 4 | include README.rst 5 | graft docs 6 | graft tests 7 | graft requirements/lck 8 | global-exclude __pycache__ 9 | global-exclude *.py[co] 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | after_success: 4 | - coveralls 5 | python: 6 | - "2.7" 7 | - "3.3" 8 | - "3.4" 9 | - "3.5" 10 | - "3.6" 11 | install: pip install python-coveralls tox-travis 12 | script: tox 13 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.5 3 | fast_parser = True 4 | warn_unused_ignores = True 5 | 6 | [mypy-src/reqwire/*] 7 | disallow_untyped_calls = True 8 | disallow_untyped_defs = True 9 | ignore_missing_imports = True 10 | follow_imports = skip 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{ini,py,rst,yml}] 8 | indent_style = space 9 | 10 | [*.py] 11 | charset = utf-8 12 | indent_size = 4 13 | 14 | [*.{ini,rst,yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /requirements/src/docs.in: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # Generated by reqwire on Thu Dec 22 15:14:43 2016 4 | -r main.in 5 | --index-url https://pypi.python.org/simple 6 | Sphinx==1.5 7 | sphinx-md-theme==0.1a4 8 | sphinxcontrib-napoleon==0.6.0 9 | -------------------------------------------------------------------------------- /requirements/src/test.in: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # Generated by reqwire on Thu Dec 8 23:51:19 2016 4 | -r main.in 5 | --index-url https://pypi.python.org/simple 6 | future==0.16.0 7 | pytest-cov==2.4.0 8 | pytest-mock==1.5.0 9 | responses==0.5.1 10 | -------------------------------------------------------------------------------- /src/reqwire/__init__.py: -------------------------------------------------------------------------------- 1 | """reqwire: wire up Python requirements with pip-tools.""" 2 | from __future__ import absolute_import 3 | 4 | import pkg_resources 5 | 6 | 7 | try: # pragma: no cover 8 | __version__ = pkg_resources.get_distribution(__name__).version 9 | except: # noqa: B901 10 | __version__ = 'unknown' 11 | -------------------------------------------------------------------------------- /src/reqwire/config.py: -------------------------------------------------------------------------------- 1 | """Provides configuration and configuration defaults.""" 2 | from __future__ import absolute_import 3 | 4 | import pathlib 5 | 6 | import biome 7 | 8 | 9 | __all__ = ( 10 | 'env', 11 | 'lockfile', 12 | ) 13 | 14 | env = biome.reqwire 15 | lockfile = env.get_path('lockfile', pathlib.Path('.reqwire.lock')) 16 | -------------------------------------------------------------------------------- /requirements/src/main.in: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # Generated by reqwire on Thu Dec 22 15:15:25 2016 4 | --index-url https://pypi.python.org/simple 5 | atomicwrites==1.1.5 6 | biome==0.1.3 7 | emoji==0.3.9 8 | enum34==1.1.6 9 | fasteners==0.14.1 10 | ordered-set==2.0.1 11 | pathlib2==2.1.0 12 | pip-tools==1.9.0 13 | requests==2.12.3 14 | sh==1.12.7 15 | typing==3.5.2.2 16 | -------------------------------------------------------------------------------- /requirements/src/qa.in: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # Generated by reqwire on Sun Dec 11 20:53:10 2016 4 | --index-url https://pypi.python.org/simple 5 | flake8-blind-except==0.1.1 6 | flake8-commas==0.1.6 7 | flake8-comprehensions==1.2.1 8 | flake8-docstrings==1.0.2 9 | flake8-future-import==0.4.3 10 | flake8-import-order==0.11 11 | flake8-print==2.0.2 12 | flake8-quotes==0.8.1 13 | flake8==3.2.1 14 | -------------------------------------------------------------------------------- /src/reqwire/errors.py: -------------------------------------------------------------------------------- 1 | """Provides custom exception classes.""" 2 | from __future__ import absolute_import 3 | 4 | 5 | __all__ = ( 6 | 'IndexUrlMismatchError', 7 | 'ReqwireError', 8 | ) 9 | 10 | 11 | class ReqwireError(Exception): 12 | """Base class for all exceptions thrown by reqwire.""" 13 | 14 | 15 | class IndexUrlMismatchError(ReqwireError): 16 | """Indicates a conflict between CLI and requirement source file.""" 17 | -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from click.testing import CliRunner 4 | import pytest 5 | 6 | 7 | FAKE_TIME = datetime.datetime(2020, 1, 1, 0, 0, 0) 8 | 9 | 10 | @pytest.fixture 11 | def fake_time(): 12 | return FAKE_TIME 13 | 14 | 15 | @pytest.fixture 16 | def patch_datetime_now(fake_time, mocker): 17 | class mocked_datetime: 18 | @classmethod 19 | def now(cls): 20 | return fake_time 21 | 22 | mocker.patch('datetime.datetime', mocked_datetime) 23 | 24 | 25 | @pytest.fixture(scope='session') 26 | def cli_runner(): 27 | return CliRunner() 28 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. reqwire documentation master file, created by 2 | sphinx-quickstart on Wed Dec 7 14:29:01 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | reqwire 7 | ======= 8 | 9 | **reqwire** wires up your Python dependencies with pip-tools_. 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | quickstart 15 | primer 16 | guide 17 | api 18 | release_notes 19 | 20 | .. raw:: html 21 | 22 | 25 | 26 | .. _pip-tools: https://github.com/nvie/pip-tools 27 | -------------------------------------------------------------------------------- /requirements/lck/qa.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # 4 | # This file is autogenerated by pip-compile 5 | # To update, run: 6 | # 7 | # pip-compile --output-file /tmp/tmp2eVvBE requirements/src/qa.in 8 | # 9 | flake8-blind-except==0.1.1 10 | flake8-commas==0.1.6 11 | flake8-comprehensions==1.2.1 12 | flake8-docstrings==1.0.2 13 | flake8-future-import==0.4.3 14 | flake8-import-order==0.11 15 | flake8-print==2.0.2 16 | flake8-quotes==0.8.1 17 | flake8==3.2.1 18 | mccabe==0.5.3 # via flake8 19 | pep8==1.7.0 # via flake8-commas 20 | pycodestyle==2.2.0 # via flake8, flake8-import-order 21 | pydocstyle==1.1.1 # via flake8-docstrings 22 | pyflakes==1.3.0 # via flake8 23 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. contents:: 5 | :backlinks: none 6 | 7 | reqwire.helpers 8 | --------------- 9 | .. automodule:: reqwire.helpers 10 | :members: 11 | 12 | reqwire.helpers.cli 13 | ~~~~~~~~~~~~~~~~~~~ 14 | .. automodule:: reqwire.helpers.cli 15 | :members: 16 | 17 | reqwire.helpers.requirements 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | .. automodule:: reqwire.helpers.requirements 20 | :members: 21 | 22 | reqwire.config 23 | -------------- 24 | .. automodule:: reqwire.config 25 | :members: 26 | 27 | reqwire.errors 28 | -------------- 29 | .. automodule:: reqwire.errors 30 | :members: 31 | 32 | reqwire.scaffold 33 | ---------------- 34 | .. automodule:: reqwire.scaffold 35 | :members: 36 | -------------------------------------------------------------------------------- /requirements/lck/main.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # 4 | # This file is autogenerated by pip-compile 5 | # To update, run: 6 | # 7 | # pip-compile --output-file /tmp/tmpv6wgsy requirements/src/main.in 8 | # 9 | atomicwrites==1.1.5 10 | attrdict==2.0.0 # via biome 11 | biome==0.1.3 12 | click==6.7 # via pip-tools 13 | emoji==0.3.9 14 | enum34==1.1.6 15 | fasteners==0.14.1 16 | first==2.0.1 # via pip-tools 17 | monotonic==1.3 # via fasteners 18 | ordered-set==2.0.1 19 | pathlib2==2.1.0 20 | pathlib==1.0.1 # via biome 21 | pip-tools==1.9.0 22 | requests==2.12.3 23 | sh==1.12.7 24 | six==1.10.0 # via attrdict, fasteners, pathlib2, pip-tools 25 | typing==3.5.2.2 26 | -------------------------------------------------------------------------------- /requirements/lck/test.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # 4 | # This file is autogenerated by pip-compile 5 | # To update, run: 6 | # 7 | # pip-compile --output-file /tmp/tmpLUXXeN requirements/src/test.in 8 | # 9 | atomicwrites==1.1.5 10 | attrdict==2.0.0 # via biome 11 | biome==0.1.3 12 | click==6.7 # via pip-tools 13 | cookies==2.2.1 # via responses 14 | coverage==4.3.4 # via pytest-cov 15 | emoji==0.3.9 16 | enum34==1.1.6 17 | fasteners==0.14.1 18 | first==2.0.1 # via pip-tools 19 | future==0.16.0 20 | monotonic==1.3 # via fasteners 21 | ordered-set==2.0.1 22 | pathlib2==2.1.0 23 | pathlib==1.0.1 # via biome 24 | pip-tools==1.9.0 25 | py==1.4.33 # via pytest 26 | pytest-cov==2.4.0 27 | pytest-mock==1.5.0 28 | pytest==3.0.7 # via pytest-cov, pytest-mock 29 | requests==2.12.3 30 | responses==0.5.1 31 | sh==1.12.7 32 | six==1.10.0 # via attrdict, fasteners, pathlib2, pip-tools, responses 33 | typing==3.5.2.2 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 David Gidwani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | .. contents:: 5 | :backlinks: none 6 | 7 | Installation 8 | ------------ 9 | 10 | **reqwire** supports all versions of Python **above 2.7**. The 11 | recommneded way to install **reqwire** is with pip_: 12 | 13 | .. code-block:: shell 14 | 15 | $ pip install reqwire 16 | 17 | Source code is available on 18 | `GitHub `_. 19 | 20 | Usage 21 | ----- 22 | 23 | 1. Run ``reqwire init`` in the working directory of your Python project. 24 | This will scaffold out a requirements directory:: 25 | 26 | requirements/ 27 | ├── lck 28 | └── src 29 | ├── main.in 30 | ├── qa.in 31 | └── test.in 32 | 33 | 2. To add requirements during development, use 34 | ``reqwire add [-t ] ``. 35 | 36 | For example, ``reqwire add -t qa flake8`` will: 37 | 38 | * Resolve the latest version of ``flake8`` (e.g. ``flake8==3.2.1``). 39 | * Add ``flake8==3.2.1`` to ``requirements/src/qa.in``. 40 | 41 | .. _pip: https://pip.pypa.io/en/stable/ 42 | 43 | 3. To compile tags, use ``reqwire build -t ``. 44 | 45 | To quickly compile *all* tags, use ``reqwire build -a``. 46 | -------------------------------------------------------------------------------- /requirements/lck/docs.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: requirementstxt -*- 2 | # vim: set ft=requirements 3 | # 4 | # This file is autogenerated by pip-compile 5 | # To update, run: 6 | # 7 | # pip-compile --output-file /tmp/tmpVe8OI6 requirements/src/docs.in 8 | # 9 | alabaster==0.7.10 # via sphinx 10 | atomicwrites==1.1.5 11 | attrdict==2.0.0 # via biome 12 | babel==2.4.0 # via sphinx 13 | biome==0.1.3 14 | click==6.7 # via pip-tools 15 | docutils==0.13.1 # via sphinx 16 | emoji==0.3.9 17 | enum34==1.1.6 18 | fasteners==0.14.1 19 | first==2.0.1 # via pip-tools 20 | imagesize==0.7.1 # via sphinx 21 | jinja2==2.11.3 # via sphinx 22 | markupsafe==1.0 # via jinja2 23 | monotonic==1.3 # via fasteners 24 | ordered-set==2.0.1 25 | pathlib2==2.1.0 26 | pathlib==1.0.1 # via biome 27 | pip-tools==1.9.0 28 | pockets==0.3.2 # via sphinxcontrib-napoleon 29 | pygments==2.2.0 # via sphinx 30 | pytz==2017.2 # via babel 31 | requests==2.12.3 32 | sh==1.12.7 33 | six==1.10.0 # via attrdict, fasteners, pathlib2, pip-tools, pockets, sphinx, sphinxcontrib-napoleon 34 | snowballstemmer==1.2.1 # via sphinx 35 | sphinx-md-theme==0.1a4 36 | sphinx==1.5 37 | sphinxcontrib-napoleon==0.6.0 38 | typing==3.5.2.2 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | .reqwire.lock 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pkg_resources 3 | 4 | 5 | extensions = [ 6 | 'sphinx.ext.autodoc', 7 | 'sphinx.ext.autosectionlabel', 8 | 'sphinx.ext.doctest', 9 | 'sphinx.ext.intersphinx', 10 | 'sphinx.ext.todo', 11 | 'sphinx.ext.coverage', 12 | 'sphinx.ext.viewcode', 13 | 'sphinxcontrib.napoleon', 14 | ] 15 | templates_path = ['_templates'] 16 | source_suffix = '.rst' 17 | master_doc = 'index' 18 | 19 | project = u'reqwire' 20 | copyright = u'2016, David Gidwani' 21 | author = u'David Gidwani' 22 | version = pkg_resources.get_distribution('reqwire').version 23 | release = version 24 | language = None 25 | exclude_patterns = [] 26 | pygments_style = 'sphinx' 27 | todo_include_todos = True 28 | html_theme = 'material_design' 29 | html_theme_options = { 30 | 'pygments_theme': 'lovelace', 31 | 'ribbon_bg': 'ribbon-slava-bowman.jpg', 32 | # 'ribbon_bg_size': 'auto auto', 33 | 'ribbon_bg_position': 'left 60%', 34 | } 35 | html_static_path = ['_static'] 36 | htmlhelp_basename = 'reqwiredoc' 37 | latex_documents = [ 38 | (master_doc, 'reqwire.tex', u'reqwire Documentation', 39 | u'David Gidwani', 'manual'), 40 | ] 41 | man_pages = [ 42 | (master_doc, 'reqwire', u'reqwire Documentation', 43 | [author], 1) 44 | ] 45 | texinfo_documents = [ 46 | (master_doc, 'reqwire', u'reqwire Documentation', 47 | author, 'reqwire', 'Wire up Python requirements with pip-tools.', 48 | 'Miscellaneous'), 49 | ] 50 | intersphinx_mapping = { 51 | 'https://docs.python.org/': None, 52 | } 53 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | application-import-names = reqwire 3 | exclude = 4 | *.egg-info, 5 | *.pyc, 6 | .cache, 7 | .eggs 8 | .git, 9 | .tox, 10 | __pycache__, 11 | build, 12 | dist, 13 | docs/source/conf.py, 14 | src/stubs, 15 | tests/fixtures/*, 16 | ignore = 17 | D401, 18 | D403, 19 | FI10, 20 | FI12, 21 | FI13, 22 | FI14, 23 | FI15, 24 | FI16, 25 | FI17, 26 | FI51, 27 | H301 28 | import-order-style = google 29 | max-complexity = 11 30 | 31 | [tox] 32 | envlist = cov-init,py{27,33,34,35,36},py35-{mypy,lint},cov-report 33 | 34 | [testenv] 35 | usedevelop = true 36 | commands = pytest --cov --cov-report= {posargs} 37 | basepython = 38 | cov-{init,report}: python3.5 39 | py27: python2.7 40 | py33: python3.3 41 | py34: python3.4 42 | py35: python3.5 43 | py36: python3.6 44 | deps = 45 | -rrequirements/lck/test.txt 46 | setenv = 47 | COVERAGE_FILE = .coverage.{envname} 48 | 49 | [testenv:cov-init] 50 | commands = coverage erase 51 | setenv = 52 | COVERAGE_FILE = .coverage 53 | 54 | [testenv:cov-report] 55 | commands = 56 | coverage combine 57 | coverage report -m 58 | setenv = 59 | COVERAGE_FILE = .coverage 60 | 61 | [testenv:docs] 62 | basepython = python2.7 63 | changedir = docs 64 | deps = 65 | -rrequirements/lck/docs.txt 66 | commands = 67 | sphinx-build -W -b html -d {envtmpdir}/doctrees source {envtmpdir}/html 68 | 69 | [testenv:py35-lint] 70 | commands = flake8 src/reqwire 71 | deps = 72 | -rrequirements/lck/qa.txt 73 | 74 | [testenv:py35-mypy] 75 | commands = 76 | mypy --ignore-missing-imports --follow-imports=skip --fast-parser src/reqwire 77 | deps = 78 | mypy 79 | typed-ast 80 | setenv = 81 | MYPYPATH = src/stubs 82 | 83 | [travis] 84 | python = 85 | 2.7: py27, docs 86 | 3.3: py33 87 | 3.4: py34 88 | 3.5: cov-init, py35, py35-{lint,mypy}, cov-report 89 | 3.6: py36 90 | -------------------------------------------------------------------------------- /docs/source/primer.rst: -------------------------------------------------------------------------------- 1 | Primer 2 | ====== 3 | 4 | .. contents:: 5 | :backlinks: none 6 | 7 | Required Reading 8 | ---------------- 9 | 10 | (Pun absolutely intended. 👌) 11 | 12 | * Unless you're already familiar with `pip-tools`_, be sure to read 13 | Vincent Driessen's article on `Better Package Management`_. 14 | * After drinking the `pip-tools`_ kool-aid, read Kenneth Reitz's article 15 | on `A Better Pip Workflow™`_. 16 | * If by now you're unconvinced that your project could benefit in terms 17 | of maintainability and build determinism by adding a separate 18 | requirements.txt for top-level dependencies, then what are you still 19 | doing here? 20 | 21 | State of Python Package Management 22 | ---------------------------------- 23 | 24 | Although `new`__ and `improved`__ ways of managing Python requirements 25 | are on the horizon, the current standard of including and maintaining at 26 | least one requirements.txt file probably isn't going anywhere. 27 | 28 | Third-party tools like `pip-tools`_ aren't perfect solutions, but until 29 | an officially sanctioned alternative is implemented and released, Python 30 | package maintainers are going to have to decide the workflow that suits 31 | their needs. 32 | 33 | The Case for Tooling 34 | -------------------- 35 | 36 | Some would argue that installing additional tools to manage package 37 | requirements adds unnecessary overhead to the development process. 38 | This argument certainly has merit, but it should be noted that `pip`_ 39 | itself was an external tool until it came installed with Python after 40 | version **2.7.9**. And while Pipfile_ looks incredibly promising, it 41 | might be safe to assume that it wouldn't be included in a Python 42 | distribution in the *immediate* future. Oh, and `virtualenv`_ remains 43 | an external tool, although it was included in the `standard library`__ 44 | through `PEP 405`_ (Python 3.3+). 45 | 46 | Whether or not extra tooling for managing a project's requirements is 47 | necessary is up to the author(s), but historically, as well as in other 48 | spaces, managing software dependencies has frequently involved more than 49 | one tool. 50 | 51 | 52 | .. _pip-tools: https://github.com/nvie/pip-tools 53 | .. _Better Package Management: http://nvie.com/posts/better-package-management/ 54 | .. _virtual environments: http://docs.python-guide.org/en/latest/dev/virtualenvs/ 55 | .. _A Better Pip Workflow™: https://www.kennethreitz.org/essays/a-better-pip-workflow 56 | .. _PEP 518: https://www.python.org/dev/peps/pep-0518/ 57 | .. _Pipfile: https://github.com/pypa/pipfile 58 | .. _pip: https://pip.pypa.io/en/stable/installing/#do-i-need-to-install-pip 59 | .. _virtualenv: https://virtualenv.pypa.io/en/stable/ 60 | .. _venv: https://docs.python.org/3/library/venv.html 61 | .. _PEP 405: https://www.python.org/dev/peps/pep-0405/ 62 | 63 | __ PEP 518_ 64 | __ Pipfile_ 65 | __ venv_ 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """reqwire: wire up Python requirements with pip-tools.""" 3 | from __future__ import absolute_import 4 | 5 | import io 6 | import sys 7 | 8 | import setuptools 9 | 10 | 11 | __all__ = ('setup',) 12 | 13 | 14 | def readme(): 15 | with io.open('README.rst') as fp: 16 | return fp.read() 17 | 18 | 19 | def setup(): 20 | """Package setup entrypoint.""" 21 | extra_requirements = { 22 | ':python_version=="2.7"': ['enum34', 'pathlib2'], 23 | } 24 | install_requirements = [ 25 | 'atomicwrites', 26 | 'biome', 27 | 'emoji', 28 | 'fasteners', 29 | 'ordered-set', 30 | 'pip-tools', 31 | 'requests', 32 | 'sh', 33 | 'typing', 34 | ] 35 | setup_requirements = ['six', 'setuptools>=17.1', 'setuptools_scm'] 36 | needs_sphinx = { 37 | 'build_sphinx', 38 | 'docs', 39 | 'upload_docs', 40 | }.intersection(sys.argv) 41 | if needs_sphinx: 42 | setup_requirements.append('sphinx') 43 | setuptools.setup( 44 | author='David Gidwani', 45 | author_email='david.gidwani@gmail.com', 46 | classifiers=[ 47 | 'Development Status :: 2 - Pre-Alpha', 48 | 'Topic :: Software Development :: Libraries', 49 | 'Topic :: Software Development :: Libraries :: Python Modules', 50 | 'Topic :: Utilities', 51 | 'Programming Language :: Python', 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 2.7', 54 | 'Programming Language :: Python :: 3', 55 | 'Programming Language :: Python :: 3.3', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Environment :: Console', 60 | 'Intended Audience :: Developers', 61 | 'License :: OSI Approved :: BSD License', 62 | 'Operating System :: POSIX :: Linux', 63 | 'Operating System :: Unix', 64 | 'Operating System :: MacOS', 65 | 'Operating System :: Microsoft :: Windows', 66 | ], 67 | description=__doc__, 68 | entry_points={ 69 | 'console_scripts': [ 70 | 'reqwire = reqwire.cli:main', 71 | ], 72 | }, 73 | extras_require=extra_requirements, 74 | install_requires=install_requirements, 75 | license='MIT', 76 | long_description=readme(), 77 | name='reqwire', 78 | package_dir={'': 'src'}, 79 | packages=setuptools.find_packages('./src'), 80 | setup_requires=setup_requirements, 81 | url='https://github.com/darvid/reqwire', 82 | use_scm_version=True, 83 | zip_safe=False, 84 | ) 85 | 86 | 87 | if __name__ == '__main__': 88 | setup() 89 | -------------------------------------------------------------------------------- /tests/unit/test_scaffold.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import pip.models 3 | import pip.req 4 | import responses 5 | import six 6 | 7 | import reqwire.scaffold 8 | 9 | 10 | def test_build_filename(): 11 | filename = reqwire.scaffold.build_filename( 12 | '.', tag_name='test', extension='.in', prefix='build') 13 | assert filename == pathlib.Path('.') / 'build' / 'test.in' 14 | 15 | 16 | def test_build_source_header_timestamp(fake_time, patch_datetime_now): 17 | header = reqwire.scaffold.build_source_header() 18 | assert header == reqwire.scaffold.build_source_header(timestamp=fake_time) 19 | 20 | 21 | def test_build_source_header_format(fake_time, patch_datetime_now): 22 | header = reqwire.scaffold.build_source_header( 23 | index_url=pip.models.PyPI.simple_url, 24 | extra_index_urls=[pip.models.PyPI.simple_url], 25 | nested_cfiles={'constraints.txt'}, 26 | nested_rfiles={'requirements.txt'}) 27 | assert header == reqwire.scaffold.MODELINES_HEADER + six.text_type("""\ 28 | # Generated by reqwire on {} 29 | -c constraints.txt 30 | -r requirements.txt 31 | --index-url https://pypi.python.org/simple 32 | --extra-index-url https://pypi.python.org/simple 33 | """.format(fake_time.strftime('%c'))) 34 | 35 | 36 | @responses.activate 37 | def test_extend_source_file(mocker, tmpdir): 38 | responses.add( 39 | responses.GET, pip.models.PyPI.simple_url, 40 | body=b'Flask\n') 41 | src_dir = tmpdir.mkdir('src') 42 | src_file = src_dir.join('requirements.in') 43 | with mocker.patch('reqwire.helpers.requirements.resolve_specifier', 44 | return_value=pip.req.InstallRequirement.from_line( 45 | 'Flask==0.11.1')): 46 | reqwire.scaffold.extend_source_file( 47 | working_directory=str(tmpdir), 48 | tag_name='requirements', 49 | specifiers=['flask']) 50 | assert 'flask==0.11.1' in src_file.read() 51 | 52 | 53 | @responses.activate 54 | def test_extend_source_file_wo_resolve_versions(mocker, tmpdir): 55 | responses.add( 56 | responses.GET, pip.models.PyPI.simple_url, 57 | body=b'Flask\n') 58 | src_dir = tmpdir.mkdir('src') 59 | src_file = src_dir.join('requirements.in') 60 | with mocker.patch('reqwire.helpers.requirements.resolve_specifier', 61 | return_value=pip.req.InstallRequirement.from_line( 62 | 'Flask')): 63 | reqwire.scaffold.extend_source_file( 64 | working_directory=str(tmpdir), 65 | tag_name='requirements', 66 | resolve_versions=False, 67 | specifiers=['flask']) 68 | assert 'flask' in src_file.read(), src_file.read() 69 | 70 | 71 | def test_init_source_dir(tmpdir): 72 | reqwire.scaffold.init_source_dir(str(tmpdir)) 73 | assert tmpdir.join('src').exists() 74 | # assert tmpdir.join('src').stat().mode & 0o777 == 0o777 75 | -------------------------------------------------------------------------------- /tests/unit/helpers/test_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import sh 5 | 6 | import reqwire.helpers.cli 7 | 8 | 9 | log_methods = ( 10 | 'echo', 11 | 'error', 12 | 'fatal', 13 | 'info', 14 | 'warn', 15 | 'warning', 16 | ) 17 | 18 | 19 | def test_emojize_win32(mocker): 20 | mocker.patch('sys.platform', 'win32') 21 | assert reqwire.helpers.cli.emojize( 22 | ':thumbs_up_sign: foo').encode('utf-8') == b'foo' 23 | 24 | 25 | def test_emojize_linux(mocker): 26 | mocker.patch('sys.platform', 'linux') 27 | mocker.patch('io.open', mocker.mock_open( 28 | read_data='Linux version 4.4.0-31-generic (gcc version 5.3.1)')) 29 | assert reqwire.helpers.cli.emojize( 30 | ':thumbs_up_sign:').encode('utf-8') == b'\xf0\x9f\x91\x8d' 31 | 32 | 33 | def test_emojize_linux_ioerror(mocker): 34 | mocker.patch('sys.platform', 'linux') 35 | mocker.patch('io.open', side_effect=IOError) 36 | assert reqwire.helpers.cli.emojize( 37 | ':thumbs_up_sign:').encode('utf-8') == b'\xf0\x9f\x91\x8d' 38 | 39 | 40 | def test_emojize_wsl(mocker): 41 | mocker.patch('sys.platform', 'linux') 42 | mocker.patch('io.open', mocker.mock_open( 43 | read_data='Linux version 3.4.0-Microsoft (Microsoft@Microsoft.com)')) 44 | assert reqwire.helpers.cli.emojize( 45 | ':thumbs_up_sign: foo').encode('utf-8') == b'foo' 46 | 47 | 48 | def test_console_writer_quiet(mocker): 49 | click_echo = mocker.patch('click.echo') 50 | console = reqwire.helpers.cli.ConsoleWriter(verbose=False) 51 | for method in log_methods: 52 | getattr(console, method)('test') 53 | click_echo.assert_not_called() 54 | 55 | 56 | def test_console_writer_verbose(mocker): 57 | mocker.patch('sys.platform', 'linux') 58 | mocker.patch('io.open', mocker.mock_open( 59 | read_data='Linux version 4.4.0-31-generic (gcc version 5.3.1)')) 60 | click_echo = mocker.patch('click.echo') 61 | console = reqwire.helpers.cli.ConsoleWriter(verbose=True) 62 | for method in log_methods: 63 | getattr(console, method)('test') 64 | fmt = console.format_strings.get(method, '{msg}') 65 | message = reqwire.helpers.cli.emojize(fmt.format(msg='test')) 66 | click_echo.assert_called_once_with(message) 67 | click_echo.reset_mock() 68 | 69 | 70 | def test_build_with_pip_compile_options(cli_runner, mocker): 71 | from reqwire.cli import main 72 | pip_compile = mocker.patch.object(sh, 'pip_compile') 73 | result = cli_runner.invoke(main, ['build', '-t', 'main', '--', 74 | '--no-header']) 75 | assert result.exit_code == 0, result.output 76 | assert pip_compile.call_args[0][2] == '--no-header' 77 | 78 | 79 | def test_main_remove(cli_runner): 80 | from reqwire.cli import main 81 | result = cli_runner.invoke(main, ['remove', 'Flask']) 82 | assert result.exit_code == 0, result.output 83 | -------------------------------------------------------------------------------- /tests/unit/helpers/test_requirements.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pip.exceptions 4 | import pip.models 5 | import pip.req 6 | import pytest 7 | import responses 8 | 9 | import reqwire.helpers.requirements 10 | 11 | 12 | @pytest.fixture 13 | def ireq(): 14 | return pip.req.InstallRequirement.from_line('reqwire==0.1a1') 15 | 16 | 17 | def test_hashable_ireq_from_ireq(ireq): 18 | hireq = reqwire.helpers.requirements.HashableInstallRequirement.from_ireq( 19 | ireq=ireq) 20 | assert hireq.comes_from == ireq.comes_from 21 | assert hireq.source_dir == ireq.source_dir 22 | assert hireq.editable == ireq.editable 23 | assert hireq.link == ireq.link 24 | assert hireq.as_egg == ireq.as_egg 25 | assert hireq.update == ireq.update 26 | assert hireq.pycompile == ireq.pycompile 27 | assert hireq.markers == ireq.markers 28 | assert hireq.isolated == ireq.isolated 29 | assert hireq.options == ireq.options 30 | assert hireq._wheel_cache == ireq._wheel_cache 31 | assert hireq.constraint == ireq.constraint 32 | 33 | 34 | def test_hashable_ireq_hashable(ireq): 35 | ireq2 = pip.req.InstallRequirement.from_line(str(ireq)) 36 | assert hash(ireq) != hash(ireq2) 37 | hireq = reqwire.helpers.requirements.HashableInstallRequirement.from_ireq( 38 | ireq=ireq) 39 | hireq2 = reqwire.helpers.requirements.HashableInstallRequirement.from_ireq( 40 | ireq=ireq2) 41 | assert hash(hireq) == hash(hireq2) 42 | assert hireq == hireq2 43 | 44 | 45 | @responses.activate 46 | def test_get_canonical_name(): 47 | responses.add( 48 | responses.GET, pip.models.PyPI.simple_url, 49 | body=b'Flask\nJinja2') 50 | name = reqwire.helpers.requirements.get_canonical_name('jinja2') 51 | assert name == 'Jinja2' 52 | with pytest.raises(pip.exceptions.DistributionNotFound): 53 | name = reqwire.helpers.requirements.get_canonical_name('Jinja3') 54 | 55 | 56 | def test_requirements_from_line(): 57 | assert reqwire.helpers.requirements.HashableInstallRequirement.from_line( 58 | 'reqwire') is not None 59 | 60 | 61 | def test_resolve_specifier_wihout_resolve_versions(ireq): 62 | requirement = reqwire.helpers.requirements.resolve_specifier( 63 | 'reqwire', resolve_versions=False) 64 | assert not requirement.is_pinned 65 | 66 | 67 | def test_build_ireq_set(): 68 | specifiers = ['Flask'] 69 | result = reqwire.helpers.requirements.build_ireq_set(specifiers) 70 | assert result 71 | 72 | 73 | @pytest.mark.parametrize('req,expected', [ 74 | ('-e git+https://github.com/darvid/reqwire.git@master#egg=reqwire', 75 | '-e git+https://github.com/darvid/reqwire.git@master#egg=reqwire'), 76 | ('-e .', '-e file://{}'.format(os.path.abspath(os.path.dirname('.')))), 77 | ]) 78 | def test_format_requirement(req, expected): 79 | ireq = reqwire.helpers.requirements.HashableInstallRequirement.from_line(req) # noqa 80 | assert reqwire.helpers.requirements.format_requirement(ireq) == expected 81 | -------------------------------------------------------------------------------- /docs/source/release_notes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | .. contents:: 5 | :backlinks: none 6 | 7 | 0.2.1 (7/26/2017) 8 | ----------------- 9 | 10 | * Bugfix release. Fixes `#17 `_. 11 | 12 | 0.2.0 (4/18/2017) 13 | ----------------- 14 | 15 | * Merged `PR #14 `_ by `ticosax`_, 16 | drops support for pip-tools <1.9.0. 17 | 18 | 0.1.8 (2/27/2017) 19 | ----------------- 20 | 21 | * Allow preservation of top-level dependencies 22 | (`#12 `_). 23 | 24 | 25 | 0.1.7 (1/9/2017) 26 | ---------------- 27 | 28 | Bugfix release, thanks to contributions from `ticosax`_. 29 | 30 | * Merged `PR #6 `_ by `ticosax`_, 31 | fixes infinite recursion in 32 | :meth:`reqwire.helpers.requirements.HashableInstallRequirement.from_line`. 33 | 34 | * Merged `PR #8 `_ by `ticosax`_. 35 | fixes ``--no-resolve-versions``. 36 | 37 | * Merged `PR #9 `_ by `ticosax`_, 38 | removes ``--pin/--no-pin`` flag in favor of ``--no-resolve-versions``. 39 | 40 | 41 | .. _ticosax: https://github.com/ticosax 42 | 43 | 44 | 0.1.6 (1/7/17) 45 | -------------- 46 | 47 | * Fixed support for installing/adding editable projects from VCS that 48 | use `setuptools_scm`_. 49 | 50 | 51 | .. _setuptools_scm: https://github.com/pypa/setuptools_scm 52 | 53 | 54 | 0.1.5 (1/7/17) 55 | -------------- 56 | 57 | * Initial support for adding editable requirements. 58 | * Minor bugfixes. 59 | * Added typing as a dependency for Python 2.7 to setup script. 60 | 61 | 62 | 0.1.2 - 0.1.4 (12/23/16) 63 | ------------------------ 64 | 65 | * Various fixes for initialization and Python 2 compatibility. 66 | 67 | 68 | 0.1.1 (12/22/16) 69 | ---------------- 70 | 71 | * Fixed ``reqwire init``, uses user-defined source and build directory 72 | names. 73 | 74 | 75 | 0.1 (12/22/16) 76 | -------------- 77 | 78 | * Corrected package setup to include `sh `_ 79 | as an installation dependency. 80 | * Updated ``MANIFEST.in`` to include additional files in distribution. 81 | * Made source and build directories configurable through command-line 82 | and environment variables. 83 | * File headers now include modelines for Vim and Sublime Text (via 84 | `STEmacsModelines `_). 85 | * Added `reqwire remove` command. 86 | 87 | 0.1a3 (12/11/16) 88 | ---------------- 89 | 90 | * Adding requirements no longer includes requirements from nested 91 | requirement files (and possibly constraint files). 92 | * Added initial unit tests. 93 | * Added ``--pre`` option to the **add** command, allowing prerelease 94 | versions of packages to be installed and added to requirements. 95 | * Added ``-b|--build`` option to the **add** command, which invokes 96 | the **build** command upon successfully adding packages. 97 | * Added initial documentation. 98 | 99 | 0.1a2 (12/3/16) 100 | --------------- 101 | 102 | * Fixed support for Python 2.7. 103 | * Nonexistent requirement directories are now handled gracefully. 104 | 105 | 0.1a1 (12/1/16) 106 | --------------- 107 | 108 | * Initial alpha release, includes the **add**, **build**, and **init** 109 | commands. 110 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | reqwire 2 | ======= 3 | 4 | .. image:: https://api.codacy.com/project/badge/Grade/1130364b44eb4fddb0091e060f84351a 5 | :alt: Codacy Badge 6 | :target: https://www.codacy.com/app/darvid/reqwire?utm_source=github.com&utm_medium=referral&utm_content=darvid/reqwire&utm_campaign=badger 7 | 8 | .. image:: https://img.shields.io/pypi/v/reqwire.svg 9 | :target: https://pypi.python.org/pypi/reqwire 10 | 11 | .. image:: https://img.shields.io/pypi/pyversions/reqwire.svg 12 | :target: https://pypi.python.org/pypi/reqwire 13 | 14 | .. image:: https://travis-ci.org/darvid/reqwire.svg?branch=master 15 | :target: https://travis-ci.org/darvid/reqwire 16 | 17 | .. image:: https://img.shields.io/coveralls/darvid/reqwire.svg 18 | :target: https://coveralls.io/github/darvid/reqwire 19 | 20 | .. image:: https://badges.gitter.im/python-reqwire/Lobby.svg 21 | :alt: Join the chat at https://gitter.im/python-reqwire/Lobby 22 | :target: https://gitter.im/python-reqwire/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 23 | 24 | **reqwire** wires up your pip requirements with `pip-tools`_. 25 | 26 | .. image:: https://asciinema.org/a/97035.png 27 | :align: center 28 | :target: https://asciinema.org/a/97035 29 | 30 | Install 31 | ------- 32 | 33 | From **PyPI**:: 34 | 35 | $ pip install -U reqwire 36 | 37 | From **source**:: 38 | 39 | $ pip install -e git+https://github.com/darvid/reqwire.git#egg=reqwire 40 | 41 | Features 42 | -------- 43 | 44 | * Manage multiple requirement source files, or *tags* 45 | * Add requirements from the command line (à la ``npm --save``) 46 | * Compile tagged source files with **pip-compile** (from `pip-tools`_) 47 | 48 | Rationale 49 | --------- 50 | 51 | Until `PEP 518`_ and `Pipfile`_ become reality, maintaining one or more 52 | **requirements.txt** files for Python projects will continue to be 53 | subject to personal preference and differing opinions on best practices 54 | [#]_. Typical workflows involve maintaining multiple 55 | **requirements.txt** files, and many projects have some form of tooling, 56 | be it Makefile targets or external tools, such as Vincent Driessen's 57 | excellent `pip-tools`_. 58 | 59 | **reqwire** is a glorified wrapper around pip-tools, and imposes a 60 | slightly opinionated workflow: 61 | 62 | * Python requirements are split into *source files* and *built files*, 63 | with the built files being the output from **pip-compile**, containing 64 | pinned versions of the entire dependency graph. (Use ``reqwire init`` 65 | to quickly scaffold the necessary directory structure.) 66 | * *Source files* (with the ``.in`` extension) represent a project's 67 | immediate dependencies, and are always pinned to specific versions or 68 | version ranges. 69 | * Source filenames are synonymous with *tags*, which can be passed to 70 | ``reqwire add`` and ``reqwire build`` to maintain requirements for 71 | entirely separate environments. 72 | 73 | 74 | Disclaimer 75 | ---------- 76 | 77 | **reqwire** makes no claims about being designed specifically for 78 | humans, bonobos, cats, or any other land mammal. This project is **not** 79 | farm raised, free range, organic, low sugar, MSG free, gluten free, 80 | halal, kosher, vegan, or particularly of any nutritional value 81 | whatsoever. It just wraps `pip-tools`_ and helps you manage your freakin 82 | Python requirements. ☮ 🌈 83 | 84 | 85 | Links 86 | ----- 87 | 88 | * `Source code `_ 89 | * `Documentation `_ 90 | 91 | 92 | Roadmap 93 | ------- 94 | 95 | * Unit tests 96 | * Provide a command to update requirements (user-specified version) 97 | * Provide a command to freshen requirements 98 | * Provide a command to combine tags to a single output file 99 | (easily possible with **pip-compile**) 100 | * Provide a utility for generating setup and runtime requirements in 101 | setup.py scripts using setuptools. 102 | * Abandon in favor of `Pipfile`_ 👌 103 | 104 | 105 | .. _pip-tools: https://github.com/nvie/pip-tools 106 | .. _PEP 518: https://www.python.org/dev/peps/pep-0518/ 107 | .. _Pipfile: https://github.com/pypa/pipfile 108 | 109 | .. [#] 110 | 111 | - http://nvie.com/posts/pin-your-packages/ 112 | - https://www.kennethreitz.org/essays/a-better-pip-workflow 113 | - https://mail.python.org/pipermail/distutils-sig/2015-December/027954.html 114 | - https://devcenter.heroku.com/articles/python-pip#best-practices 115 | -------------------------------------------------------------------------------- /src/reqwire/helpers/cli.py: -------------------------------------------------------------------------------- 1 | """Helpers for command-line applications.""" 2 | from __future__ import absolute_import 3 | 4 | import io 5 | import re 6 | import sys 7 | 8 | import click 9 | import emoji 10 | 11 | 12 | MYPY = False 13 | if MYPY: # pragma: no cover 14 | from typing import Any, Iterator, Tuple # noqa: F401 15 | 16 | 17 | __all__ = ( 18 | 'ConsoleWriter', 19 | 'emojize', 20 | ) 21 | 22 | 23 | def emojize(message, **kwargs): 24 | # type: (str, Any) -> str 25 | """Wrapper around :func:`emoji.emojize` for Windows compatibility. 26 | 27 | Emoji are not well supported under Windows. This function not only 28 | checks :data:`sys.platform`, but the file ``/proc/version`` as well 29 | to prevent *"emojification"* on the Windows Subsystem for Linux 30 | (WSL, otherwise known as Ubuntu on Windows). 31 | 32 | Args: 33 | message: The format string. See :func:`emoji.emojize` for more 34 | information. Any emoji placeholders will be removed if 35 | Windows or WSL are detected. 36 | **kwargs: Passed to :func:`emoji.emojize`. 37 | 38 | """ 39 | emoji_pattern = r':(.+?):' 40 | if sys.platform == 'win32': 41 | return re.sub(emoji_pattern, '', message).strip() 42 | elif sys.platform.startswith('linux'): 43 | try: 44 | with io.open('/proc/version') as f: 45 | if 'Microsoft' in f.read(): 46 | return re.sub(emoji_pattern, '', message).strip() 47 | except IOError: 48 | pass 49 | return emoji.emojize(message, **kwargs) 50 | 51 | 52 | class ConsoleWriter(object): 53 | """Facilitates writing formatted, informational messages to a TTY.""" 54 | 55 | format_strings = { 56 | 'error': click.style(':skull: {msg}', fg='red'), 57 | 'fatal': click.style(':skull: {msg}', fg='red'), 58 | 'warn': click.style(':warning: {msg}', fg='yellow'), 59 | 'warning': click.style(':warning: {msg}', fg='yellow'), 60 | } 61 | 62 | def __init__(self, verbose=True): 63 | # type: (bool) -> None 64 | """Constructs a new :class:`ConsoleWriter`. 65 | 66 | Args: 67 | verbose: Sets verbosity for all future messages. 68 | 69 | """ 70 | self.verbose = verbose 71 | 72 | def echo(self, message, *args, **kwargs): 73 | # type: (str, Any, Any) -> None 74 | """Wraps :func:`click.echo`. 75 | 76 | Args: 77 | message: The message to write to stdout. 78 | *args: Used to format message. 79 | **kwargs: Used to format message. 80 | 81 | """ 82 | if self.verbose: 83 | click.echo(emojize(message.format(*args, **kwargs))) 84 | 85 | def error(self, message, *args, **kwargs): 86 | # type: (str, Any, Any) -> None 87 | """Prints an error message. 88 | 89 | Args: 90 | message: The message to write to stdout. 91 | *args: Used to format message. 92 | **kwargs: Used to format message. 93 | 94 | """ 95 | self._echo_formatted('error', message, *args, **kwargs) 96 | 97 | def fatal(self, message, *args, **kwargs): 98 | # type: (str, Any, Any) -> None 99 | """Prints a fatal message. 100 | 101 | Args: 102 | message: The message to write to stdout. 103 | *args: Used to format message. 104 | **kwargs: Used to format message. 105 | 106 | """ 107 | self._echo_formatted('fatal', message, *args, **kwargs) 108 | 109 | def info(self, message, *args, **kwargs): 110 | # type: (str, Any, Any) -> None 111 | """Prints an informational message. 112 | 113 | Args: 114 | message: The message to write to stdout. 115 | *args: Used to format message. 116 | **kwargs: Used to format message. 117 | 118 | """ 119 | self._echo_formatted('info', message, *args, **kwargs) 120 | 121 | def warn(self, message, *args, **kwargs): 122 | # type: (str, Any, Any) -> None 123 | """Prints a warning message. 124 | 125 | Args: 126 | message: The message to write to stdout. 127 | *args: Used to format message. 128 | **kwargs: Used to format message. 129 | 130 | """ 131 | self._echo_formatted('warn', message, *args, **kwargs) 132 | 133 | def warning(self, message, *args, **kwargs): 134 | # type: (str, Any, Any) -> None 135 | """Prints a warning message. 136 | 137 | Args: 138 | message: The message to write to stdout. 139 | *args: Used to format message. 140 | **kwargs: Used to format message. 141 | 142 | """ 143 | self._echo_formatted('warn', message, *args, **kwargs) 144 | 145 | def _echo_formatted(self, format_key, message, *args, **kwargs): 146 | # type: (str, str, Any, Any) -> None 147 | fmt = self.format_strings.get(format_key, '{msg}') 148 | self.echo(fmt.format(msg=message), *args, **kwargs) 149 | -------------------------------------------------------------------------------- /docs/source/guide.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | .. contents:: 5 | :backlinks: none 6 | 7 | Directory Structure 8 | ------------------- 9 | 10 | Reqwire introduces a **requirements base directory** to the root of your 11 | project, and two subdirectories: a **source** directory and a **build** 12 | directory. By default, the source directory is named *src*, and the 13 | build directory is named *lck* (for lock, as built requirements.txt 14 | files are analogous to the lock files of many other package managers). 15 | 16 | The names for all three of these directories are configurable, by 17 | passing the ``-d|--directory``, ``--source-directory``, and 18 | ``--build-directory`` options, respectively, to ``reqwire``, **before** 19 | any commands. For example: 20 | 21 | .. code-block:: shell 22 | 23 | $ reqwire -d req --source-directory=_src --build-directory=_build build -a 24 | # ...or... 25 | $ export REQWIRE_DIR_BASE=req 26 | $ export REQWIRE_DIR_SOURCE=_src 27 | $ export REQWIRE_DIR_BUILD=_build 28 | $ reqwire build -a 29 | 30 | Packaging and Version Control 31 | ----------------------------- 32 | 33 | Ideally, the requirements directory should be located in a project's 34 | root directory, and both source and build directories added to version 35 | control. Depending on the project and target audience, it might make 36 | sense to copy or symlink the primary, built requirements tag (usually 37 | ``main``) to a ``requirements.txt`` file in the project root. 38 | 39 | If you're distributing a Python package, it might be useful to Include 40 | the *build directory* in your `MANIFEST.in`_ file. This can be simply 41 | achieved with ``graft``:: 42 | 43 | graft requirements/lck 44 | 45 | Take care to ensure that the built requirements directory is not 46 | ignored by ``.gitignore``, ``.hgignore``, etc. This should not be a 47 | problem if using the default build directory name (*lck*). 48 | 49 | .. _MANIFEST.in: https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute 50 | 51 | Tag Organization 52 | ---------------- 53 | 54 | The purpose of **tags** in reqwire is to provide logical separation of 55 | package requirements based on the environment they target. For instance, 56 | `Sphinx`_ is likely only needed when building documentation, and not at 57 | runtime. `pytest`_ and pytest plugins are only required in a continuous 58 | integration (CI) environment, and so on. 59 | 60 | Traditionally, you would use tools like `tox`_, and end up maintaining 61 | requirements in more than one location, and likely not bother to 62 | pin versions or declare sub-dependencies. reqwire makes it convenient 63 | for package maintainers to quickly generate concrete, first-level 64 | requirements, which should hopefully encourage best practices across 65 | all environments. 66 | 67 | .. _Sphinx: http://www.sphinx-doc.org/en/1.5.1/ 68 | .. _pytest: http://doc.pytest.org/en/latest/ 69 | .. _tox: http://tox.readthedocs.io/en/latest/config.html?highlight=deps#confval-deps=MULTI-LINE-LIST 70 | 71 | Command Reference 72 | ----------------- 73 | 74 | reqwire add 75 | ~~~~~~~~~~~ 76 | 77 | * ``reqwire add [specifier]...`` 78 | 79 | Installs packages to the local environment and updates one or more 80 | tagged requirement source files. 81 | 82 | If no other parameters are given, this command will... 83 | 84 | * Resolve the latest version of the provided package(s), unless a 85 | pinned version is provided. 86 | * Install the package with **pip**. 87 | * Add the requirement to the ``main`` tag. 88 | 89 | * ``reqwire add -b [specifier]...`` 90 | 91 | Calls :ref:`reqwire build` for each tag provided (or ``main`` if no 92 | tags were provided). 93 | 94 | * ``reqwire add -e [path/url]`` 95 | 96 | Adds an editable project. 97 | 98 | * ``reqwire add [-t ]... [specifier]...`` 99 | 100 | Saves packages to the specified requirement tag(s). 101 | 102 | * ``reqwire add --no-install [specifier]...`` 103 | 104 | Skips package installation. 105 | 106 | * ``reqwire add --no-resolve-canonical-names [specifier]...`` 107 | 108 | By default, reqwire will search the Python package index for an exact 109 | match of package names, and use the canonical name (i.e. casing) for 110 | each specifier. 111 | 112 | Passing this flag results in the user-provided package being saved to 113 | requirement source files. 114 | 115 | * ``reqwire add --no-resolve-versions [specifier]...`` 116 | 117 | By default, reqwire will resolve the latest version for each specifier 118 | provided. 119 | 120 | Passing this flag allows for adding non-pinned packages to requirement 121 | source files. In most cases, this is not recommended even though the 122 | resulting requirement lock files will resolve to latest versions anyway. 123 | 124 | * ``reqwire add --pre [specifier]...`` 125 | 126 | Includes prerelease versions when resolving versions. 127 | 128 | reqwire build 129 | ~~~~~~~~~~~~~ 130 | 131 | * ``reqwire build -a`` 132 | 133 | Builds all tags. 134 | 135 | * ``reqwire build -t TAG`` 136 | 137 | Builds one or more tags. 138 | 139 | * ``reqwire build -a -- [pip-compile options]...`` 140 | 141 | Passes all additional options and arguments to **pip-compile**. 142 | 143 | For instance, to build requirements with hashes: 144 | 145 | .. code-block:: shell 146 | 147 | $ reqwire build -a -- --generate-hashes 148 | 149 | reqwire init 150 | ~~~~~~~~~~~~ 151 | 152 | * ``reqwire init`` 153 | 154 | Scaffolds a requirements directory in the current directory. 155 | 156 | * ``reqwire init -f`` 157 | 158 | Scaffolds a requirements directory and overwrites any default tag 159 | names, and ignores pre-existing directories. 160 | 161 | * ``reqwire init --index-url=INDEX_URL`` 162 | 163 | Changes the base URL written to requirement source files. 164 | 165 | * ``reqwire init -t TAG`` 166 | 167 | Creates the given tag names as requirement source files. 168 | 169 | If not provided, the tags ``docs``, ``main``, ``qa``, and ``test`` 170 | will get created. 171 | 172 | * ``reqwire init --extra-index-url INDEX_URL`` 173 | 174 | Adds ``extra-index-url`` options to requirement source files. 175 | 176 | reqwire remove 177 | ~~~~~~~~~~~~~~ 178 | 179 | * ``reqwire remove [specifier]...`` 180 | 181 | Removes the provided package name(s) from the main requirement source 182 | file. 183 | 184 | * ``reqwire remove -t TAG [specifier]...`` 185 | 186 | Removes the provided package name(s) from one or more tagged 187 | requirement source files. 188 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/reqwire.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/reqwire.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/reqwire" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/reqwire" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\reqwire.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\reqwire.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /src/reqwire/scaffold.py: -------------------------------------------------------------------------------- 1 | """Scaffold supporting files and directories.""" 2 | from __future__ import absolute_import 3 | 4 | import datetime 5 | import io 6 | import pathlib 7 | 8 | import fasteners.process_lock 9 | import ordered_set 10 | import six 11 | 12 | import reqwire.config 13 | import reqwire.errors 14 | import reqwire.helpers.requirements 15 | 16 | 17 | MYPY = False 18 | if MYPY: 19 | from typing import ( # noqa: F401 20 | Iterable, 21 | Iterator, 22 | Optional, 23 | Set, 24 | Type, 25 | ) 26 | 27 | 28 | __all__ = ( 29 | 'build_filename', 30 | 'build_source_header', 31 | 'DEFAULT_HEADER', 32 | 'extend_source_file', 33 | 'init_source_dir', 34 | 'init_source_file', 35 | ) 36 | 37 | 38 | MODELINES_HEADER = six.text_type("""\ 39 | # -*- mode: requirementstxt -*- 40 | # vim: set ft=requirements 41 | """) 42 | 43 | 44 | #: The default header format string for requirement source files. 45 | #: 46 | #: The first two lines provide modeline comments for Sublime Text and 47 | #: Vim. 48 | #: 49 | #: To enable requirements.txt completion and syntax highlighting in 50 | #: Sublime Text, install the following plugins with Package Control: 51 | #: 52 | #: * `requirementstxt `_ 53 | #: * `STEmacsModelines `_ 54 | #: 55 | #: For Vim, install the 56 | #: `requirements syntax `_. 57 | DEFAULT_HEADER = MODELINES_HEADER + six.text_type("""\ 58 | # Generated by reqwire on %c 59 | {nested_cfiles}\ 60 | {nested_rfiles}\ 61 | {index_url}\ 62 | {extra_index_urls} 63 | """) 64 | 65 | 66 | def build_filename(working_directory, # type: str 67 | tag_name, # type: str 68 | extension='.in', # type: str 69 | prefix='src', # type: str 70 | ): 71 | # type: (...) -> pathlib.Path 72 | """Constructs a path to a tagged requirement source file. 73 | 74 | Args: 75 | working_directory: The parent directory of the source file. 76 | tag_name: The tag name. 77 | extension: The file extension. Defaults to *.in*. 78 | prefix: The subdirectory under the working directory to create 79 | the source file in. Defaults to *src*. 80 | 81 | """ 82 | wd = pathlib.Path(working_directory) / prefix 83 | return wd / ''.join((tag_name, extension)) 84 | 85 | 86 | def build_source_header(format_string=None, # type: Optional[str] 87 | index_url=None, # type: Optional[str] 88 | extra_index_urls=None, # type: Optional[Iterable[str]] 89 | nested_cfiles=None, # type: Optional[Set[str]] 90 | nested_rfiles=None, # type: Optional[Set[str]] 91 | timestamp=None, # type: Optional[datetime.datetime] 92 | ): 93 | # type: (...) -> str 94 | """Builds a header for requirement source file. 95 | 96 | Args: 97 | format_string: A format string containing one or more of the 98 | following keys: **nested_cfiles**, **nested_rfiles**, 99 | **index_url**, and **extra_index_urls**. The format string 100 | may also include ``strftime`` formatting directives. 101 | Defaults to :const:`DEFAULT_HEADER`. 102 | index_url: A Python package index URL. 103 | extra_index_urls: Extra Python package index URLs. 104 | nested_cfiles: A set of nested constraint files. 105 | nested_rfiles: A set of nested requirement files. 106 | timestamp: A :class:`datetime.datetime` object. If not provided, 107 | defaults to the current datetime. 108 | 109 | Returns: 110 | The rendered header string. 111 | 112 | """ 113 | if format_string is None: 114 | format_string = DEFAULT_HEADER 115 | 116 | if timestamp is None: 117 | timestamp = datetime.datetime.now() 118 | 119 | components = { 120 | 'nested_cfiles': '', 121 | 'nested_rfiles': '', 122 | 'index_url': '', 123 | 'extra_index_urls': '', 124 | } 125 | 126 | if nested_cfiles is not None: 127 | components['nested_cfiles'] = '\n'.join( 128 | '-c {}'.format(rfile) 129 | for rfile in nested_cfiles) 130 | if nested_rfiles is not None: 131 | components['nested_rfiles'] = '\n'.join( 132 | '-r {}'.format(rfile) 133 | for rfile in nested_rfiles) 134 | if index_url is not None: 135 | components['index_url'] = ' '.join( 136 | ('--index-url', index_url.strip())) 137 | if extra_index_urls is not None: 138 | components['extra_index_urls'] = '\n'.join( 139 | ' '.join(('--extra-index-url', extra_index_url)) 140 | for extra_index_url in extra_index_urls) 141 | 142 | for key in components: 143 | if components[key]: 144 | components[key] += '\n' 145 | 146 | return timestamp.strftime(format_string).format( 147 | **components).strip() + '\n' 148 | 149 | 150 | @fasteners.process_lock.interprocess_locked(str(reqwire.config.lockfile)) 151 | def extend_source_file(working_directory, # type: str 152 | tag_name, # type: str 153 | specifiers, # type: Iterable[str] 154 | extension='.in', # type: str 155 | index_url=None, # type: Optional[str] 156 | extra_index_urls=None, # type: Optional[Set[str]] 157 | lookup_index_urls=None, # type: Optional[Set[str]] 158 | prereleases=False, # type: bool 159 | resolve_canonical_names=True, # type: bool 160 | resolve_versions=True, # type: bool 161 | ): 162 | # type: (...) -> None 163 | """Adds requirements to an existing requirement source file. 164 | 165 | Args: 166 | working_directory: The parent directory of the source file. 167 | tag_name: The tag name. 168 | specifiers: A list of specifiers. 169 | extension: The file extension. Defaults to *.in*. 170 | index_url: A Python package index URL. 171 | extra_index_urls: Extra Python package index URLs. 172 | lookup_index_urls: Python package index URLs used to search 173 | for packages during resolving. This parameter is only useful 174 | if an attempt is made to add packages found only in indexes 175 | that are only specified in nested requirement source files. 176 | prereleases: Whether or not to include prereleases. 177 | resolve_canonical_names: Queries package indexes provided by 178 | **index_urls** for the canonical name of each 179 | specifier. For example, *flask* will get resolved to 180 | *Flask*. 181 | resolve_versions: Queries package indexes for latest package 182 | versions. 183 | 184 | """ 185 | if extra_index_urls is None: 186 | extra_index_urls = ordered_set.OrderedSet() 187 | else: 188 | extra_index_urls = ordered_set.OrderedSet(extra_index_urls) 189 | 190 | filename = build_filename( 191 | working_directory=working_directory, 192 | tag_name=tag_name, 193 | extension=extension) 194 | req_file = reqwire.helpers.requirements.RequirementFile(str(filename)) 195 | if filename.exists(): 196 | if index_url is not None and index_url != req_file.index_url: 197 | raise reqwire.errors.IndexUrlMismatchError( 198 | '"{}" != "{}"'.format(index_url, req_file.index_url)) 199 | elif index_url is None: 200 | index_url = req_file.index_url 201 | extra_index_urls |= req_file.extra_index_urls 202 | 203 | if lookup_index_urls is None: 204 | lookup_index_urls = {index_url} if index_url is not None else set() 205 | if extra_index_urls is not None: 206 | lookup_index_urls |= set(extra_index_urls) 207 | 208 | if not lookup_index_urls and req_file.index_urls: 209 | lookup_index_urls |= req_file.index_urls 210 | 211 | requirements = req_file.requirements 212 | requirements |= reqwire.helpers.requirements.build_ireq_set( 213 | specifiers=specifiers, 214 | index_urls=lookup_index_urls, 215 | prereleases=prereleases, 216 | resolve_canonical_names=resolve_canonical_names, 217 | resolve_source_dir=str(pathlib.Path(working_directory).parent), 218 | resolve_versions=resolve_versions) 219 | 220 | if resolve_versions: 221 | requirements |= reqwire.helpers.requirements.resolve_ireqs( 222 | requirements=req_file.requirements, 223 | prereleases=prereleases, 224 | intersect=True) 225 | 226 | nested_cfiles = ordered_set.OrderedSet( 227 | str(cf.filename.relative_to(filename.parent)) 228 | for cf in req_file.nested_cfiles) 229 | nested_rfiles = ordered_set.OrderedSet( 230 | str(rf.filename.relative_to(filename.parent)) 231 | for rf in req_file.nested_rfiles) 232 | 233 | reqwire.helpers.requirements.write_requirements( 234 | filename=str(filename), 235 | requirements=requirements, 236 | header=build_source_header( 237 | index_url=index_url, 238 | extra_index_urls=extra_index_urls, 239 | nested_cfiles=nested_cfiles, 240 | nested_rfiles=nested_rfiles)) 241 | 242 | 243 | def init_source_dir(working_directory, # type: str 244 | mode=0o777, # type: int 245 | create_parents=True, # type: bool 246 | exist_ok=False, # type: bool 247 | name='src', # type: str 248 | ): 249 | # type: (...) -> pathlib.Path 250 | """Creates a requirements source directory. 251 | 252 | Args: 253 | working_directory: The parent directory of the source file. 254 | mode: Permissions with which to create source file. Defaults to 255 | `0o777`. 256 | create_parents: Whether or not parent directories should be 257 | created if missing. 258 | exist_ok: Does not raise an :class:`OSError` if directory 259 | creation failed. 260 | name: The name of the subdirectory to create under the working 261 | directory. Defaults to *src*. 262 | 263 | """ 264 | wd = pathlib.Path(working_directory) 265 | src = wd / name 266 | try: 267 | src.mkdir(mode=mode, parents=create_parents) 268 | except OSError: 269 | if not exist_ok: 270 | raise 271 | return src 272 | 273 | 274 | def init_source_file(working_directory, # type: str 275 | tag_name, # type: str 276 | extension='.in', # type: str 277 | index_url=None, # type: Optional[str] 278 | extra_index_urls=None, # type: Optional[Iterable[str]] 279 | encoding=None, # type: Optional[str] 280 | errors=None, # type: Optional[str] 281 | mode=0o666, # type: int 282 | overwrite=False, # type: bool 283 | ): 284 | # type: (...) -> pathlib.Path 285 | """Creates a requirements source file. 286 | 287 | Args: 288 | working_directory: The requirements working directory. 289 | tag_name: The tag name. 290 | extension: The file extension. Defaults to ".in". 291 | index_url: Base URL of Python package index. 292 | extra_index_urls: Iterable of URLs of secondary package indexes. 293 | encoding: Passed to :func:`io.open` when creating source file. 294 | errors: Passed to :func:`io.open` when creating source file. 295 | mode: Permissions with which to create source file. Defaults to 296 | `0o666`. 297 | overwrite: Whether or not to overwrite an existing source file. 298 | 299 | Returns: 300 | Path to source file. 301 | 302 | """ 303 | filename = build_filename( 304 | working_directory=working_directory, 305 | tag_name=tag_name, 306 | extension=extension) 307 | try: 308 | filename.touch(mode=mode) 309 | except OSError: 310 | if not overwrite: 311 | raise 312 | 313 | with io.open(str(filename), 'wb', encoding=encoding, errors=errors) as f: 314 | f.write(build_source_header().encode('utf-8')) 315 | 316 | return filename 317 | -------------------------------------------------------------------------------- /src/reqwire/cli.py: -------------------------------------------------------------------------------- 1 | """Provides the command-line entrypoint for reqwire.""" 2 | from __future__ import absolute_import 3 | 4 | import pathlib 5 | import shlex 6 | import tempfile 7 | 8 | import atomicwrites 9 | import click 10 | import piptools.exceptions 11 | import piptools.utils 12 | import sh 13 | 14 | import reqwire 15 | import reqwire.helpers.cli 16 | import reqwire.helpers.requirements 17 | import reqwire.scaffold 18 | 19 | 20 | MYPY = False 21 | if MYPY: 22 | from typing import Any, Dict, Iterable, Set, Tuple # noqa: F401 23 | 24 | 25 | __all__ = ('main',) 26 | 27 | 28 | console = reqwire.helpers.cli.ConsoleWriter() 29 | 30 | 31 | def pip_install(ctx, *specifiers): 32 | # type: (click.Context, str) -> None 33 | try: 34 | result = sh.pip.install(*specifiers, _err_to_out=True, _iter=True) 35 | for line in result: 36 | click.echo(line, nl=False) 37 | except sh.ErrorReturnCode: 38 | ctx.abort() 39 | 40 | 41 | @click.group() 42 | @click.option('-d', '--directory', default='requirements', 43 | envvar='REQWIRE_DIR_BASE', 44 | help='Requirements directory.', 45 | type=click.Path(file_okay=False, resolve_path=True)) 46 | @click.option('-q', '--quiet/--verbose', default=False, 47 | help='Suppress output.') 48 | @click.option('--extension', default='.in', 49 | help='File extension used for requirement source files. ' 50 | 'Defaults to ".in".') 51 | @click.option('--source-directory', default='src', envvar='REQWIRE_DIR_SOURCE', 52 | help='Source directory relative to requirements directory. ' 53 | 'Defaults to "src".') 54 | @click.option('--build-directory', default='lck', envvar='REQWIRE_DIR_BUILD', 55 | help='Build directory relative to requirements directory. ' 56 | 'Defaults to "lck".') 57 | @click.version_option(version=reqwire.__version__) 58 | @click.pass_context 59 | def main(ctx, # type: click.Context 60 | directory, # type: click.Path 61 | quiet, # type: bool 62 | extension, # type: str 63 | source_directory, # type: str 64 | build_directory, # type: str 65 | ): 66 | # type: (...) -> None 67 | """reqwire: micromanages your requirements.""" 68 | requirements_dir = pathlib.Path(str(directory)) 69 | console.verbose = not quiet 70 | ctx.obj = { 71 | 'build_dir': build_directory, 72 | 'directory': requirements_dir, 73 | 'extension': extension, 74 | 'source_dir': source_directory, 75 | } 76 | 77 | 78 | @main.command('add') 79 | @click.option('-b', '--build', default=False, is_flag=True, 80 | help='Builds the given tag(s) after adding packages.') 81 | @click.option('-e', '--editable', 82 | help='Installs the provided package in editable mode.', 83 | multiple=True) 84 | @click.option('-t', '--tag', 85 | help=('Target requirement tags. ' 86 | 'Multiple tags supported. ' 87 | 'Defaults to "main".'), 88 | multiple=True) 89 | @click.option('--install/--no-install', default=True, 90 | help='Installs packages with pip.') 91 | @click.option('--pre', default=False, is_flag=True, 92 | help='Include prerelease versions.') 93 | @click.option('--resolve-canonical-names/--no-resolve-canonical-names', 94 | default=True, 95 | help='Queries Python package index for canonical package names.') 96 | @click.option('--resolve-versions/--no-resolve-versions', 97 | default=True, 98 | help='Resolves and pins the latest package version.') 99 | @click.argument('specifiers', nargs=-1) 100 | @click.pass_obj 101 | @click.pass_context 102 | def main_add(ctx, # type: click.Context 103 | options, # type: Dict[str, Any] 104 | build, # type: bool 105 | editable, # type: Iterable[str] 106 | tag, # type: Iterable[str] 107 | install, # type: bool 108 | pre, # type: bool 109 | resolve_canonical_names, # type: bool 110 | resolve_versions, # type: bool 111 | specifiers, # type: Tuple[str, ...] 112 | ): 113 | # type: (...) -> None 114 | """Add packages to requirement source files.""" 115 | if not options['directory'].exists(): 116 | console.error('run `{} init\' first', ctx.find_root().info_name) 117 | ctx.abort() 118 | 119 | specifiers = tuple('-e {}'.format(e) for e in editable) + specifiers 120 | 121 | if install: 122 | pip_args = tuple(shlex.split(' '.join(specifiers))) 123 | if pre: 124 | pip_args = ('--pre',) + pip_args 125 | pip_install(ctx, *pip_args) 126 | 127 | if not tag: 128 | tag = ('main',) 129 | 130 | pip_options, session = reqwire.helpers.requirements.build_pip_session() 131 | src_dir = options['directory'] / options['source_dir'] 132 | lookup_index_urls = set() # type: Set[str] 133 | 134 | for tag_name in tag: 135 | filename = src_dir / ''.join((tag_name, options['extension'])) 136 | if not filename.exists(): 137 | continue 138 | _, finder = reqwire.helpers.requirements.parse_requirements( 139 | filename=str(filename)) 140 | lookup_index_urls |= set(finder.index_urls) 141 | 142 | try: 143 | for tag_name in tag: 144 | console.info('saving requirement(s) to {}', tag_name) 145 | reqwire.scaffold.extend_source_file( 146 | working_directory=str(options['directory']), 147 | tag_name=tag_name, 148 | specifiers=specifiers, 149 | extension=options['extension'], 150 | lookup_index_urls=lookup_index_urls, 151 | prereleases=pre, 152 | resolve_canonical_names=resolve_canonical_names, 153 | resolve_versions=resolve_versions) 154 | except piptools.exceptions.NoCandidateFound as err: 155 | console.error(str(err)) 156 | ctx.abort() 157 | 158 | if build: 159 | ctx.invoke(main_build, all=False, tag=tag) 160 | 161 | 162 | @main.command('build') 163 | @click.option('-a', '--all', is_flag=True, 164 | help='Builds all tags.') 165 | @click.option('-t', '--tag', help='Saves tagged requirement source files.', 166 | multiple=True) 167 | @click.argument('pip_compile_options', nargs=-1) 168 | @click.pass_obj 169 | @click.pass_context 170 | def main_build(ctx, # type: click.Context 171 | options, # type: Dict[str, Any] 172 | all, # type: bool 173 | tag, # type: Iterable[str] 174 | pip_compile_options, # type: Iterable[str] 175 | ): 176 | # type: (...) -> None 177 | """Build requirements with pip-compile.""" 178 | if not options['directory'].exists(): 179 | console.error('run `{} init\' first', ctx.find_root().info_name) 180 | ctx.abort() 181 | if not all and not tag: 182 | console.error('either --all or --tag must be provided.') 183 | ctx.abort() 184 | src_dir = options['directory'] / options['source_dir'] 185 | dest_dir = options['directory'] / options['build_dir'] 186 | if not dest_dir.exists(): 187 | dest_dir.mkdir() 188 | default_args = ['-r'] 189 | if not tag: 190 | pattern = '*{}'.format(options['extension']) 191 | tag = (path.stem for path in src_dir.glob(pattern)) 192 | for tag_name in tag: 193 | src = src_dir / ''.join((tag_name, options['extension'])) 194 | dest = dest_dir / '{}.txt'.format(tag_name) 195 | console.info('building {}', click.format_filename(str(dest))) 196 | args = default_args[:] 197 | args += [str(src)] 198 | args += list(pip_compile_options) 199 | with atomicwrites.AtomicWriter(str(dest), 'w', True).open() as f: 200 | f.write(reqwire.scaffold.MODELINES_HEADER) 201 | with tempfile.NamedTemporaryFile() as temp_file: 202 | args += ['-o', temp_file.name] 203 | sh.pip_compile(*args, _out=f, _tty_out=False) 204 | 205 | 206 | @main.command('init') 207 | @click.option('-f', '--force', help='Force initialization.', is_flag=True) 208 | @click.option('-i', '--index-url', envvar='PIP_INDEX_URL', 209 | help='Base URL of Python package index.') 210 | @click.option('-t', '--tag', 211 | help=('Tagged requirements files to create. ' 212 | 'Defaults to docs, main, qa, and test.'), 213 | multiple=True) 214 | @click.option('--extra-index-url', envvar='PIP_EXTRA_INDEX_URL', 215 | help='Extra URLs of package indexes', 216 | multiple=True) 217 | @click.pass_obj 218 | @click.pass_context 219 | def main_init(ctx, # type: click.Context 220 | options, # type: Dict[str, Any] 221 | force, # type: bool 222 | index_url, # type: str 223 | tag, # type: Iterable[str] 224 | extra_index_url, # type: Tuple[str] 225 | ): 226 | # type: (...) -> None 227 | """Initialize reqwire in the current directory.""" 228 | if not force and options['directory'].exists(): 229 | console.error('requirements directory already exists') 230 | ctx.abort() 231 | src_dir = reqwire.scaffold.init_source_dir( 232 | options['directory'], exist_ok=force, name=options['source_dir']) 233 | console.info('created {}', click.format_filename(str(src_dir))) 234 | 235 | build_dir = reqwire.scaffold.init_source_dir( 236 | options['directory'], exist_ok=force, name=options['build_dir']) 237 | console.info('created {}', click.format_filename(str(build_dir))) 238 | 239 | if not tag: 240 | tag = ('docs', 'main', 'qa', 'test') 241 | for tag_name in tag: 242 | filename = reqwire.scaffold.init_source_file( 243 | working_directory=options['directory'], 244 | tag_name=tag_name, 245 | extension=options['extension'], 246 | index_url=index_url, 247 | extra_index_urls=extra_index_url) 248 | console.info('created {}', click.format_filename(str(filename))) 249 | 250 | 251 | @main.command('remove') 252 | @click.option('-t', '--tag', 253 | help=('Tagged requirements files to create. ' 254 | 'Defaults to docs, main, qa, and test.'), 255 | multiple=True) 256 | @click.argument('specifiers', nargs=-1) 257 | @click.pass_obj 258 | @click.pass_context 259 | def main_remove(ctx, 260 | options, 261 | tag, 262 | specifiers, 263 | ): 264 | # type: (...) -> None 265 | """Remove packages from requirement source files.""" 266 | if not options['directory'].exists(): 267 | console.error('run `{} init\' first', ctx.find_root().info_name) 268 | ctx.abort() 269 | 270 | if not tag: 271 | tag = ('main',) 272 | 273 | for tag_name in tag: 274 | filename = reqwire.scaffold.build_filename( 275 | working_directory=options['directory'], 276 | tag_name=tag_name, 277 | extension=options['extension']) 278 | if not filename.exists(): 279 | console.warn('"{}" does not exist', 280 | click.format_filename(str(filename))) 281 | continue 282 | req_file = reqwire.helpers.requirements.RequirementFile( 283 | str(filename)) 284 | for specifier in specifiers: 285 | hireq = (reqwire.helpers.requirements.HashableInstallRequirement 286 | .from_line(specifier)) 287 | for requirement in req_file.requirements: 288 | src_req_name = requirement.name 289 | target_req_name = hireq.name 290 | if src_req_name == target_req_name: 291 | req_file.requirements.remove(requirement) 292 | console.info('removed "{}" from {}', 293 | src_req_name, tag_name) 294 | 295 | reqwire.helpers.requirements.write_requirements( 296 | filename=str(filename), 297 | requirements=req_file.requirements, 298 | header=reqwire.scaffold.build_source_header( 299 | index_url=req_file.index_url, 300 | extra_index_urls=req_file.extra_index_urls, 301 | nested_cfiles=req_file.nested_cfiles, 302 | nested_rfiles=req_file.nested_rfiles)) 303 | -------------------------------------------------------------------------------- /src/reqwire/helpers/requirements.py: -------------------------------------------------------------------------------- 1 | """Helpers for managing Python package requirements.""" 2 | from __future__ import absolute_import 3 | 4 | import enum 5 | import io 6 | import itertools 7 | import optparse 8 | import os 9 | import pathlib 10 | import shlex 11 | import typing 12 | 13 | import atomicwrites 14 | import ordered_set 15 | import pip.basecommand 16 | import pip.cmdoptions 17 | import pip.download 18 | import pip.exceptions 19 | import pip.index 20 | import pip.models 21 | import pip.req 22 | import pip.req.req_file 23 | import piptools.repositories 24 | import piptools.resolver 25 | import piptools.utils 26 | import requests 27 | import six 28 | import six.moves 29 | 30 | 31 | MYPY = False 32 | if MYPY: # pragma: no cover 33 | from typing import Any, Iterable, List, Optional, Set, Tuple # noqa: F401 34 | 35 | InstallReqIterable = Iterable['HashableInstallRequirement'] 36 | InstallReqSet = Set['HashableInstallRequirement'] 37 | InstallReqFileSet = Set['RequirementFile'] 38 | 39 | ParseResultType = Tuple[ 40 | InstallReqSet, 41 | Set[str], 42 | Set['RequirementFile'], 43 | Set['RequirementFile'], 44 | ] 45 | 46 | 47 | __all__ = ( 48 | 'build_ireq_set', 49 | 'get_canonical_name', 50 | 'HashableInstallRequirement', 51 | 'parse_requirements', 52 | 'PyPiHtmlParser', 53 | 'PyPiHtmlParserState', 54 | 'RequirementFile', 55 | 'resolve_ireqs', 56 | 'resolve_specifier', 57 | 'update_ireq_name', 58 | 'write_requirements', 59 | ) 60 | 61 | 62 | class _PipCommand(pip.basecommand.Command): 63 | 64 | name = '_PipCommand' 65 | 66 | 67 | class HashableInstallRequirement(typing.Hashable, pip.req.InstallRequirement): 68 | """A hashable version of :class:`pip.req.InstallRequirement`.""" 69 | 70 | @classmethod 71 | def from_ireq(cls, ireq): 72 | # type: (pip.req.InstallRequirement) -> HashableInstallRequirement 73 | """Builds a new instance from an existing install requirement.""" 74 | return cls( 75 | req=ireq.req, 76 | comes_from=ireq.comes_from, 77 | source_dir=ireq.source_dir, 78 | editable=ireq.editable, 79 | link=ireq.link, 80 | as_egg=ireq.as_egg, 81 | update=ireq.update, 82 | pycompile=ireq.pycompile, 83 | markers=ireq.markers, 84 | isolated=ireq.isolated, 85 | options=ireq.options, 86 | wheel_cache=ireq._wheel_cache, 87 | constraint=ireq.constraint) 88 | 89 | @classmethod 90 | def from_line(cls, name, **kwargs): 91 | # type: (str, Any) -> HashableInstallRequirement 92 | """Creates a hashable install requirement from a name.""" 93 | if name.startswith('-e'): 94 | name = name[2:].strip() 95 | ireq = cls.from_editable(name, **kwargs) 96 | else: 97 | ireq = super(HashableInstallRequirement, cls).from_line(name, 98 | **kwargs) 99 | return cls.from_ireq(ireq) 100 | 101 | def __eq__(self, other): # noqa: D105 102 | # type: (Any) -> bool 103 | return str(self.req) == str(other.req) 104 | 105 | def __hash__(self): # noqa: D105 106 | # type: () -> int 107 | return hash(str(self.req)) 108 | 109 | 110 | class PyPiHtmlParser(six.moves.html_parser.HTMLParser, object): 111 | """An HTML parse for Python package indexes.""" 112 | 113 | def __init__(self, search=None, *args, **kwargs): # noqa: D102 114 | super(PyPiHtmlParser, self).__init__(*args, **kwargs) 115 | self.search = search.lower() if search is not None else None 116 | self.state = PyPiHtmlParserState.waiting 117 | self.collected_packages = [] 118 | 119 | def handle_starttag(self, tag, attrs): # noqa: D102 120 | if tag == 'a': 121 | self.state = PyPiHtmlParserState.collecting_package_name 122 | 123 | def handle_endtag(self, tag): # noqa: D102 124 | if (tag == 'a' and 125 | self.state == PyPiHtmlParserState.collecting_package_name): 126 | self.state = PyPiHtmlParserState.waiting 127 | 128 | def handle_data(self, data): # noqa: D102 129 | if self.state == PyPiHtmlParserState.collecting_package_name: 130 | self.collected_packages.append(data) 131 | if self.search is not None and data.lower() == self.search: 132 | self.state = PyPiHtmlParserState.found_package_name 133 | 134 | 135 | class PyPiHtmlParserState(enum.IntEnum): 136 | """An enumeration of parsing states for :class:`PyPiHtmlParser`.""" 137 | 138 | #: The default parser state. 139 | waiting = 0 140 | #: The state indicating a package anchor tag is being visited. 141 | collecting_package_name = 1 142 | #: The state indicating a package matching a search was visited. 143 | found_package_name = 2 144 | 145 | 146 | class PyPiRepository(piptools.repositories.PyPIRepository): 147 | 148 | def get_dependencies(self, ireq): 149 | # type: (pip.req.InstallRequirement) -> Set[pip.req.InstallRequirement] 150 | if not (ireq.editable or piptools.utils.is_pinned_requirement(ireq)): 151 | raise TypeError( 152 | 'Expected pinned or editable InstallRequirement, ' 153 | 'got {}'.format(ireq)) 154 | 155 | if not os.path.isdir(self._download_dir): 156 | os.makedirs(self._download_dir) 157 | if not os.path.isdir(self._wheel_download_dir): 158 | os.makedirs(self._wheel_download_dir) 159 | 160 | download_dir = self._download_dir 161 | if ireq.editable and pip.download.is_vcs_url(ireq.link): 162 | download_dir = None 163 | reqset = pip.req.RequirementSet( 164 | self.build_dir, 165 | self.source_dir, 166 | download_dir=download_dir, 167 | wheel_download_dir=self._wheel_download_dir, 168 | session=self.session) 169 | dependencies = reqset._prepare_file(self.finder, ireq) 170 | return set(dependencies) 171 | 172 | 173 | class RequirementFile(object): 174 | """Represents a Python requirements.txt file.""" 175 | 176 | def __init__(self, 177 | filename, # type: str 178 | requirements=None, # type: Optional[InstallReqSet] 179 | nested_cfiles=None, # type: Optional[InstallReqFileSet] 180 | nested_rfiles=None, # type: Optional[InstallReqFileSet] 181 | index_urls=None, # type: Optional[List[str]] 182 | ): 183 | # type: (...) -> None 184 | """Constructs a new :class:`RequirementFile`. 185 | 186 | Args: 187 | filename: The path to a requirements file. The requirements 188 | file is not required to exist. 189 | requirements: A set of :class:`HashableInstallRequirement`. 190 | If **filename** points to a path that exists and 191 | **requirements** are not provided, then the requirements 192 | will be parsed from the target file. 193 | nested_cfiles: A set of :class:`RequirementFile`. 194 | nested_rfiles: A set of :class:`RequirementFile`. 195 | index_urls: A set of Python package index URLs. The first 196 | URL is assumed to be the primary index URL, while the 197 | rest are extra. 198 | 199 | """ 200 | self.filename = pathlib.Path(filename) 201 | self.requirements = requirements or ordered_set.OrderedSet() 202 | self.index_urls = ordered_set.OrderedSet(index_urls) 203 | self.nested_cfiles = nested_cfiles or ordered_set.OrderedSet() 204 | self.nested_rfiles = nested_rfiles or ordered_set.OrderedSet() 205 | 206 | if requirements is None and self.filename.exists(): 207 | self.reload() 208 | 209 | @property 210 | def index_url(self): 211 | # type: () -> Optional[str] 212 | """A Python package index URL.""" 213 | if len(self.index_urls): 214 | return self.index_urls[0] 215 | return None 216 | 217 | @property 218 | def extra_index_urls(self): 219 | # type: () -> Set[str] 220 | """Extra Python package index URLs.""" 221 | if len(self.index_urls) > 1: 222 | return self.index_urls[1:] 223 | return ordered_set.OrderedSet() 224 | 225 | def parse(self, *args): 226 | # type: (str) -> ParseResultType 227 | """Parses a requirements file. 228 | 229 | Args: 230 | *args: Command-line options and arguments passed to pip. 231 | 232 | Returns: 233 | A set of requirements, index URLs, nested constraint files, 234 | and nested requirements files. 235 | 236 | The nested constraint and requirements files are sets of 237 | :class:`RequirementFile` instances. 238 | 239 | """ 240 | self.nested_files = self.parse_nested_files() 241 | pip_options, session = build_pip_session(*args) 242 | repository = PyPiRepository(pip_options, session) 243 | requirements = pip.req.parse_requirements( 244 | str(self.filename), 245 | finder=repository.finder, 246 | session=repository.session, 247 | options=pip_options) 248 | requirements = ordered_set.OrderedSet(sorted( 249 | (HashableInstallRequirement.from_ireq(ireq) 250 | for ireq in requirements), 251 | key=lambda ireq: str(ireq))) 252 | index_urls = ordered_set.OrderedSet(repository.finder.index_urls) 253 | nested_cfiles, nested_rfiles = self.parse_nested_files() 254 | nested_requirements = set(itertools.chain( 255 | *(requirements_file.requirements 256 | for requirements_file in nested_rfiles))) 257 | requirements -= nested_requirements 258 | return requirements, index_urls, nested_cfiles, nested_rfiles 259 | 260 | def parse_nested_files(self): 261 | # type: () -> Tuple[InstallReqFileSet, InstallReqFileSet] 262 | """Parses a requirements file, looking for nested files. 263 | 264 | Returns: 265 | A set of constraint files and requirements files. 266 | 267 | """ 268 | nested_cfiles = ordered_set.OrderedSet() 269 | nested_rfiles = ordered_set.OrderedSet() 270 | parser = pip.req.req_file.build_parser() 271 | defaults = parser.get_default_values() 272 | defaults.index_url = None 273 | with io.open(str(self.filename), 'r') as f: 274 | for line in f: 275 | if line.startswith('#'): 276 | continue 277 | args_str, options_str = pip.req.req_file.break_args_options( 278 | line) 279 | opts, _ = parser.parse_args(shlex.split(options_str), defaults) 280 | if opts.requirements: 281 | filename = self.filename.parent / opts.requirements[0] 282 | nested_rfiles.add(self.__class__(str(filename))) 283 | elif opts.constraints: 284 | filename = self.filename.parent / opts.constraints[0] 285 | nested_cfiles.add(self.__class__(str(filename))) 286 | return nested_cfiles, nested_rfiles 287 | 288 | def reload(self): 289 | # type: () -> None 290 | """Reloads the current requirements file.""" 291 | parsed_requirements = self.parse() 292 | self.requirements = parsed_requirements[0] 293 | self.index_urls = parsed_requirements[1] 294 | self.nested_cfiles = parsed_requirements[2] 295 | self.nested_rfiles = parsed_requirements[3] 296 | 297 | def __str__(self): # noqa: D105 298 | # type: () -> str 299 | # TODO(dpg): serialize actual requirements.txt? 300 | return str(self.filename) 301 | 302 | def __repr__(self): # noqa: D105 303 | # type: () -> str 304 | return '<{}(filename={!r})>'.format( 305 | self.__class__.__name__, 306 | str(self.filename)) 307 | 308 | 309 | def build_ireq_set(specifiers, # type: Iterable[str] 310 | index_urls=None, # type: Optional[Iterable[str]] 311 | prereleases=False, # type: bool 312 | resolve_canonical_names=True, # type: bool 313 | resolve_source_dir=None, # type: str 314 | resolve_versions=True, # type: bool 315 | sort_specifiers=True, # type: bool 316 | ): 317 | # type: (...) -> InstallReqSet 318 | """Builds a set of install requirements. 319 | 320 | Args: 321 | specifiers: A list of specifier strings. 322 | index_urls: List of Python package indexes. Only used if 323 | **resolve_canonical_names** or **resolve_versions** is 324 | ``True``. 325 | prereleases: Whether or not to include prereleases. 326 | resolve_canonical_names: Queries package indexes provided by 327 | **index_urls** for the canonical name of each 328 | specifier. For example, *flask* will get resolved to 329 | *Flask*. 330 | resolve_source_dir: If an editable local directory is provided, 331 | rewrites the path to a path relative to the given 332 | absolute path. 333 | resolve_versions: Queries package indexes for latest package 334 | versions. 335 | sort_specifiers: Sorts specifiers alphabetically. 336 | 337 | Returns: 338 | A set of :class:`HashableInstallRequirement`. 339 | 340 | """ 341 | install_requirements = ordered_set.OrderedSet() 342 | if index_urls is None: 343 | index_urls = [] 344 | if sort_specifiers: 345 | specifiers = sorted(specifiers) 346 | for specifier in specifiers: 347 | if specifier.startswith('-e'): 348 | ireq = HashableInstallRequirement.from_line(specifier) 349 | else: 350 | args = [] 351 | for index_url in index_urls: 352 | args.extend(['--extra-index-url', index_url]) 353 | ireq = resolve_specifier(specifier, prereleases, resolve_versions, 354 | *args) 355 | if resolve_canonical_names and not ireq.editable: 356 | package_name = ireq.name 357 | canonical_name = get_canonical_name( 358 | package_name=package_name, index_urls=index_urls) 359 | update_ireq_name( 360 | install_requirement=ireq, package_name=canonical_name) 361 | elif resolve_source_dir is not None and ireq.source_dir: 362 | try: 363 | ireq.source_dir = str( 364 | pathlib.Path(ireq.source_dir) 365 | .relative_to(pathlib.Path(resolve_source_dir))) 366 | ireq.link = pip.index.Link('file://{}'.format( 367 | ireq.source_dir)) 368 | except ValueError: 369 | pass 370 | install_requirements.add(ireq) 371 | return install_requirements 372 | 373 | 374 | def build_pip_session(*args): 375 | # type: (str) -> Tuple[optparse.Values, pip.download.PipSession] 376 | pip_command = _PipCommand() 377 | index_opts = pip.cmdoptions.make_option_group( 378 | pip.cmdoptions.index_group, 379 | pip_command.parser, 380 | ) 381 | pip_command.parser.insert_option_group(0, index_opts) 382 | pip_command.parser.add_option(optparse.Option( 383 | '--pre', action='store_true', default=False)) 384 | pip_options, _ = pip_command.parse_args(list(args)) 385 | session = pip_command._build_session(pip_options) 386 | return pip_options, session 387 | 388 | 389 | def format_requirement(ireq, marker=None): 390 | # type: (HashableInstallRequirement, bool) -> str 391 | if ireq.editable: 392 | if ireq.source_dir and ireq.source_dir.startswith('.'): 393 | return '-e {}'.format(ireq.source_dir) 394 | if ireq.link: 395 | return '-e {}'.format(ireq.link) 396 | raise NotImplementedError('Unknown type of editable requirement') 397 | else: 398 | return piptools.utils.format_requirement(ireq, marker=marker) 399 | 400 | 401 | def get_canonical_name(package_name, index_urls=None, *args): 402 | # type: (str, Optional[Iterable[str]], str) -> str 403 | """Returns the canonical name of the given Python package. 404 | 405 | Args: 406 | package_name: The package name. 407 | index_urls: A list of Python package indexes. 408 | *args: Command-line options and arguments passed to pip. 409 | 410 | Returns: 411 | The canonical name of a package, or ``None`` if no packaging 412 | could be found in the given search indexes. 413 | 414 | """ 415 | if not index_urls: # pragma: no cover 416 | index_urls = {pip.models.PyPI.simple_url} 417 | for index_url in index_urls: 418 | response = requests.get(index_url, stream=True) 419 | parser = PyPiHtmlParser(search=package_name) 420 | for line in response.iter_lines(): 421 | parser.feed(six.text_type(line, response.encoding, 'ignore')) 422 | if parser.state == PyPiHtmlParserState.found_package_name: 423 | parser.close() 424 | return parser.collected_packages[-1] 425 | parser.close() 426 | raise pip.exceptions.DistributionNotFound( 427 | 'No matching distribution found for {}'.format(package_name)) 428 | 429 | 430 | def parse_requirements(filename, *args): # pragma: no cover 431 | # type: (str, str) -> Tuple[InstallReqSet, pip.index.PackageFinder] 432 | """Parses a requirements source file. 433 | 434 | Args: 435 | filename: The requirements source filename. 436 | *args: Command-line options and arguments passed to pip. 437 | 438 | Returns: 439 | A set of :class:`pip.req.InstallRequirement`, and a 440 | :class:`pip.index.PackageFinder` instance. 441 | 442 | """ 443 | pip_options, session = build_pip_session(*args) 444 | repository = PyPiRepository(pip_options, session) 445 | requirements = pip.req.parse_requirements( 446 | filename, 447 | finder=repository.finder, 448 | session=repository.session, 449 | options=pip_options) 450 | return set(requirements), repository.finder 451 | 452 | 453 | def resolve_ireqs(requirements, # type: InstallReqIterable 454 | prereleases=False, # type: bool 455 | intersect=False, # type: bool 456 | *args, # type: str 457 | **kwargs # type: Any 458 | ): # pragma: no cover 459 | # type: (...) -> InstallReqSet 460 | """Resolves install requirements with piptools. 461 | 462 | Args: 463 | requirements: An iterable of :class:`pip.req.InstallRequirement`. 464 | prereleases: Whether or not to include prereleases. 465 | intersect: Return only install requirements that intersect with 466 | **requirements**. Default behavior is to include the entire 467 | dependency graph as produced by piptools. 468 | *args: Command-line options and arguments passed to pip. 469 | **kwargs: Passed to :class:`piptools.resolver.Resolver`. 470 | 471 | Returns: 472 | A set of :class:`pip.req.InstallRequirement`. 473 | 474 | """ 475 | pip_options, session = build_pip_session(*args) 476 | repository = PyPiRepository(pip_options, session) 477 | resolver = piptools.resolver.Resolver( 478 | constraints=requirements, repository=repository, **kwargs) 479 | results = {HashableInstallRequirement.from_ireq(r) 480 | for r in resolver.resolve()} 481 | if intersect: 482 | results |= {HashableInstallRequirement.from_ireq(r) 483 | for r in requirements} 484 | return results 485 | 486 | 487 | def resolve_specifier(specifier, # type: str 488 | prereleases=False, # type: bool 489 | resolve_versions=True, # type: bool 490 | *args # type: str # noqa: C812 491 | ): 492 | # type: (...) -> HashableInstallRequirement 493 | """Resolves the given specifier. 494 | 495 | Args: 496 | specifier: A specifier string. 497 | prereleases: Whether or not to include prereleases. 498 | *args: Command-line options and arguments passed to pip. 499 | 500 | Returns: 501 | A set of :class:`pip.req.InstallRequirement`. 502 | 503 | """ 504 | ireq = HashableInstallRequirement.from_line(specifier) 505 | pip_options, session = build_pip_session(*args) 506 | repository = PyPiRepository(pip_options, session) 507 | if (ireq.editable or 508 | piptools.utils.is_pinned_requirement(ireq) or 509 | not resolve_versions): 510 | return ireq 511 | else: 512 | return HashableInstallRequirement.from_ireq( 513 | repository.find_best_match(ireq, prereleases=prereleases)) 514 | 515 | 516 | def update_ireq_name(install_requirement, package_name): 517 | # type: (pip.req.InstallRequirement, str) -> pip.req.InstallRequirement 518 | """Updates the name of an existing install requirement. 519 | 520 | Args: 521 | install_requirement: A :class:`pip.req.InstallRequirement` 522 | instance. 523 | package_name: The new package name. 524 | 525 | Returns: 526 | The **install_requirement** with mutated requirement name. 527 | 528 | """ 529 | requirement = install_requirement.req 530 | if hasattr(requirement, 'project_name'): 531 | requirement.project_name = package_name 532 | else: 533 | requirement.name = package_name 534 | return install_requirement 535 | 536 | 537 | def write_requirements(filename, # type: str 538 | requirements, # type: InstallReqIterable 539 | index_url=None, # type: Optional[str] 540 | extra_index_urls=None, # type: Optional[Iterable[str]] 541 | header=None, # type: Optional[str] 542 | ): 543 | # type: (...) -> None 544 | """Writes install requirements to a file. 545 | 546 | Note that this function is destructive and will overwrite any 547 | existing files. 548 | 549 | Args: 550 | filename: The output filename. 551 | requirements: An iterable of :class:`pip.req.InstallRequirement`. 552 | index_url: The primary Python package index URL. 553 | extra_index_urls: Extra package index URLs. 554 | header: A header string to prepend to the file. 555 | 556 | """ 557 | with atomicwrites.atomic_write(filename, overwrite=True) as f: 558 | if header: 559 | f.write(header) 560 | 561 | for ireq in sorted(requirements, key=lambda ireq: str(ireq)): 562 | f.write(format_requirement(ireq)) 563 | f.write('\n') 564 | --------------------------------------------------------------------------------