Files
pipenv/pipenv/utils.py
T
2018-02-04 22:27:18 -05:00

1243 lines
49 KiB
Python

# -*- coding: utf-8 -*-
import os
import hashlib
import tempfile
import sys
import shutil
import logging
import click
import crayons
import delegator
import parse
import fuzzywuzzy.process
import requests
import six
from time import time
logging.basicConfig(level=logging.ERROR)
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
from distutils.spawn import find_executable
from contextlib import contextmanager
from piptools.resolver import Resolver
from piptools.repositories.pypi import PyPIRepository
from piptools.scripts.compile import get_pip_command
from piptools import logging
from piptools.exceptions import NoCandidateFound
from pip.download import is_archive_file
from pip.exceptions import DistributionNotFound
from pip.index import Link
from requests.exceptions import HTTPError, ConnectionError
from .pep508checker import lookup
from .environments import SESSION_IS_INTERACTIVE, PIPENV_MAX_ROUNDS, PIPENV_CACHE_DIR
specifiers = [k for k in lookup.keys()]
# List of version control systems we support.
VCS_LIST = ('git', 'svn', 'hg', 'bzr')
SCHEME_LIST = ('http://', 'https://', 'ftp://', 'file://')
requests = requests.Session()
packages = [
'simplejson', 'six', 'botocore', 'python-dateutil', 'pyasn1', 'setuptools',
'requests', 'pyyaml', 'docutils', 's3transfer', 'futures', 'pip',
'jmespath', 'awscli', 'rsa', 'colorama', 'idna', 'certifi', 'urllib3',
'chardet', 'cffi', 'awscli-cwlogs', 'wheel', 'pycparser', 'enum34', 'pbr',
'cryptography', 'virtualenv', 'pytz', 'setuptools-scm', 'jinja2',
'ipaddress', 'markupsafe', 'boto3', 'asn1crypto', 'boto', 'paramiko',
'ptyprocess', 'pexpect', 'pytest-runner', 'psutil', 'flask', 'werkzeug',
'bcrypt', 'pynacl', 'sqlalchemy', 'click', 'numpy', 'pyparsing', 'lxml',
'pyopenssl', 'future', 'decorator', 'vcversioner', 'mock', 'argparse',
'pyasn1-modules', 'jsonschema', 'funcsigs', 'nose', 'tornado', 'httplib2',
'protobuf', 'pandas', 'coverage', 'psycopg2', 'pygments', 'oauth2client',
'singledispatch', 'itsdangerous', 'pytest', 'functools32', 'docopt',
'mccabe', 'babel', 'pillow', 'grpcio', 'backports-abc', 'public',
'query-string', 'redis', 'zope-interface',
'pyflakes', 'pycrypto', 'wrapt', 'django', 'selenium', 'flake8',
'html5lib', 'elasticsearch', 'markdown', 'pycodestyle',
'backports-ssl-match-hostname', 'scipy', 'websocket-client', 'lockfile',
'ipython', 'beautifulsoup4', 'gevent', 'uritemplate', 'pymysql',
'configparser', 'kombu', 'arrow', 'scikit-learn', 'greenlet', 'amqp',
'wcwidth', 'googleapis-common-protos', 'bleach',
'google-api-python-client', 'gunicorn', 'gitpython', 'typing',
'prompt-toolkit', 'google-cloud-core', 'google-gax', 'requests-oauthlib',
'stevedore', 'ordereddict', 'traitlets', 'packaging', 'pymongo',
'ipython-genutils', 'appdirs', 'celery', 'google-auth', 'cython',
'billiard', 'xmltodict', 'pickleshare', 'unittest2', 'simplegeneric',
'msgpack-python', 'snowballstemmer', 'sphinx', 'matplotlib', 'pep8',
'pylint', 'netaddr', 'flask-restful', 'oauthlib', 'linecache2', 'ply',
'traceback2', 'alabaster', 'monotonic', 'olefile', 'isort', 'astroid',
'pyjwt', 'lazy-object-proxy', 'imagesize', 'smmap2', 'gitdb2',
'incremental', 'contextlib2', 'ndg-httpsclient', 'ujson', 'unidecode',
'raven', 'blessings', 'docker-pycreds', 'ansible', 'vine', 'mako',
'netifaces', 'retrying', 'attrs', 'requests-toolbelt', 'supervisor',
'python-daemon', 'sqlparse', 'prettytable', 'iso8601', 'pytest-cov',
'cycler', 'cachetools', 'pyzmq', 'tabulate', 'google-cloud-logging',
'tqdm', 'mozsystemmonitor', 'gapic-google-cloud-logging-v2',
'blobuploader', 'tzlocal', 'tox', 'pluggy', 'xlrd', 'configobj',
'djangorestframework', 'webencodings', 'unicodecsv', 'grpcio-tools',
'pystache', 'meld3', 'mysql-python', 'uwsgi', 'oslo-utils',
'grpc-google-cloud-logging-v2', 'oslo-i18n', 'nbformat', 'statsd',
'debtcollector', 'docker-py', 'oslo-config', 'sphinxcontrib-websupport',
'pathlib2', 'parsedatetime', 'ecdsa', 'oslo-serialization',
'configargparse', 'backports-weakref', 'backports-functools-lru-cache',
'alembic', 'jupyter-core', 'cached-property', 'scandir', 'rfc3986',
'frida', 'subprocess32', 'keystoneauth1', 'thrift', 'jedi', 'ccxt',
'fabric', 'mistune', 'dnspython', 'service-identity', 'datadog',
'python-magic', 'altgraph', 'twisted', 'openpyxl', 'webob', 'macholib',
'docker', 'regex', 'python-keystoneclient',
'backports-shutil-get-terminal-size', 'zope-component', 'python-editor',
'zope-event', 'isodate', 'tensorflow', 'pika', 'anyjson', 'tldextract',
'tensorflow-tensorboard', 'pyrfc3339', 'requests-file', 'networkx',
'easyprocess', 'dockerpty', 'texttable', 'positional', 'python-augeas',
'acme', 'jdcal', 'mmh3', 'dill', 'certbot', 'termcolor', 'nbconvert',
'certbot-apache', 'ipykernel', 'python-mimeparse', 'ruamel-yaml',
'et-xmlfile', 'letsencrypt', 'opencv-python', 'cmd2', 'w3lib', 'cliff',
'jupyter-client', 'ipywidgets', 'passlib', 'gcloud', 'cssselect',
'notebook', 'python-swiftclient', 'widgetsnbextension', 'entrypoints',
'flask-sqlalchemy', 'kazoo', 'defusedxml', 'pandocfilters', 'python-gflags',
'testpath', 'python-memcached', 'keras', 'jsonpatch', 'python-novaclient',
'sympy', 'qtconsole', 'freezegun', 'whichcraft', 'docker-compose',
'binaryornot', 'blinker', 'cookiecutter', 'azure-common', 'jinja2-time',
'poyo', 'certbot-nginx', 'nltk', 'google-cloud-storage', 'sklearn',
'pyhocon', 'django-extensions', 'ua-parser', 'os-client-config',
'jupyter-console', 'inflection', 'newrelic', 'tempita', 'azure-nspkg',
'codecov', 'argh', 'sqlalchemy-migrate', 'requestsexceptions', 'geopy',
'azure-storage', 'pytest-xdist', 'jupyter', 'grpc-google-pubsub-v1',
'faker', 'execnet', 'constantly', 'grpc-google-logging-v2', 'automat',
'argcomplete', 'apipkg', 'wtforms', 'sphinx-rtd-theme', 'aiohttp',
'hyperlink', 'py4j', 'multidict', 'django-filter', 'coala', 'crcmod',
'jsonpointer', 'pytesseract', 'gax-google-pubsub-v1',
'gax-google-logging-v2', 'distribute', 'patsy', 'flask-wtf', 'waitress',
'coveralls', 'pyaml', 'bz2file', 'hjson', 'fake-useragent', 'terminado',
'pyperclip', 'repoze-lru', 'mysqlclient', 'smart-open', 'theano', 'pycurl',
'sqlobject', 'python-glanceclient', 'paste', 'python-cinderclient',
'pathspec', 'watchdog', 'testtools', 'plotly', 'python-openstackclient',
'scrapy-crawlera', 'pathtools', 'azure', 'flask-login', 'aniso8601',
'google-resumable-media', 'python-jenkins', 'slacker', 'xlsxwriter',
'async-timeout', 'pyserial', 'openstacksdk', 'python-jose', 'tenacity',
'python-slugify', 'keyring', 'pkginfo', 'pastedeploy', 'seaborn',
'eventlet', 'google-cloud-bigquery', 'h5py', 'aws-requests-auth',
'maxminddb', 's3cmd', 'django-debug-toolbar', 'flask-script',
'multi-key-dict', 'fuzzywuzzy', 'fasteners', 'youtube-dl',
'pycryptodome', 'smmap', 'gitdb', 'setuptools-git', 'pager',
'python-subunit', 'warlock', 'extras', 'capstone', 'httpretty',
'factory_boy', 'webtest', 'django-cors-headers', 'codeintel', 'suds',
'pyodbc', 'geoip2', 'filechunkio', 'fixtures', 'pysocks', 'statsmodels',
'google-auth-httplib2', 'kafka-python', 'applicationinsights', 'yarl',
'cassandra-driver', 'azure-mgmt-compute', 'pathlib', 'python-jwt', 'sh',
'flask-cors', 'shapely', 'twine', 'taskcluster', 'enum-compat',
'python-twitter', 'cookiejar', 'cookies', 'semantic-version', 'slugid',
'suds-jurko', 'joblib', 'azure-mgmt-network', 'azure-mgmt-resource',
'hiredis', 'pyhawk-with-a-single-extra-commit', 'jws', 'moto', 'bokeh',
'ipaddr', 'invoke', 'azure-mgmt-storage', 'pyxdg', 'azure-mgmt-nspkg',
'pytest-mock', 'google-cloud-pubsub', 'send2trash', 'yarg', 'subliminal',
'pydevd', 'xlwt', 'user-agents', 'python-fanart', 'bs4', 'rtorrent-python',
'django-storages', 'tmdbsimple', 'autopep8', 'pysftp', 'ipdb',
'setproctitle', 'osc-lib', 'importlib', 'validate-email', 'django-appconf',
'bottle', 'hgtools', 'stripe', 'azure-servicebus', 'marshmallow',
'voluptuous', 'ptvsd', 'jsonpickle', 'reportlab', 'python-geohash',
'dicttoxml', 'ddt', 'secretstorage', 'pytest-django', 'flexget',
'httpagentparser', 'beautifulsoup', 'azure-mgmt', 'haversine',
'flower', 'sortedcontainers', 'requests-mock',
'azure-servicemanagement-legacy', 'flask-migrate', 'pyinotify',
'carbon', 'zc-buildout', 'unittest-xml-reporting', 'parse', 'hacking',
'mxnet', 'qds-sdk', 'twilio', 'gspread', 'oslo-log', 'pytest-timeout',
'python-heatclient', 'oslo-context', 'numexpr', 'toolz', 'adal',
'troposphere', 'humanfriendly', 'path-py', 'dogpile-cache', 'plumbum',
'gapic-google-cloud-pubsub-v1', 'graphite-web', 'grpc-google-iam-v1',
'deprecation', 'mpmath', 'oslo-concurrency', 'feedparser', 'python-ldap',
'proto-google-cloud-pubsub-v1', 'pyzabbix', 'humanize', 'colorlog',
'msrestazure', 'msrest', 'python-ironicclient', 'pycountry',
'email-validator', 'hypothesis', 'coala-bears', 'phonenumbers',
'dj-database-url', 'elasticsearch-dsl', 'responses',
'python-neutronclient', 'sasl', 'django-nose', 'munch', 'pydns',
'proto-google-cloud-datastore-v1', 'apscheduler', 'django-redis',
'pytest-forked', 'python-levenshtein', 'dateparser',
'google-cloud-datastore', 'pytimeparse', 'pytest-html',
'virtualenv-clone', 'zope-deprecation', 'django-rest-swagger',
'whitenoise', 'gensim', 'python-consul', 'pypdf2', 'pydispatcher',
'scp', 'requires', 'cement', 'cx-oracle', 'graphviz', 'slackclient',
'hponeview', 'croniter', 'cssutils', 'appier', 'jsonpath-rw',
'requests-futures', 'mrjob', 'cachet', 'influxdb', 'virtualenvwrapper',
'appnope', 'pymssql', 'testfixtures', 'glob2', 'django-model-utils',
'awsebcli', 'tweepy', 'gapic-google-cloud-datastore-v1', 'coreapi',
'bkcharts', 'requests-ntlm', 'sqlalchemy-utils', 'more-itertools',
'testrepository', 'blessed', 'jsonfield', 'logilab-common',
'flake8-import-order', 'parse-type', 'clint', 'queuelib', 'robotframework',
'python-gnupg', 'tensorflow-gpu', 'jira', 'gcdt-bundler',
'azure-mgmt-redis', 'avro', 'args', 'pythonwhois', 'pyhamcrest',
'scrapy', 'ruamel-ordereddict', 'retry', 'azure-mgmt-batch',
'azure-batch', 'junit-xml', 'django-compressor', 'pyvirtualdisplay',
'python-openid', 'itypes', 'flask-cache', 'azure-mgmt-keyvault',
'pip-tools', 'apache-libcloud', 'inflect', 'django-celery', 'routes',
'google-apputils', 'bitarray', 'websockets', 'cherrypy', 'pyhive',
'os-testr', 'whoosh', 'django-braces', 'findspark', 'parsel',
'zope-exceptions', 'coreschema', 'ntlm-auth', 'fake-factory',
'enum', 'googleads', 'iptools', 'google-cloud-translate',
'google-cloud', 'pywinrm', 'google-cloud-vision', 'google-cloud-language',
'brotlipy', 'google-cloud-bigtable', 'google-cloud-error-reporting',
'oslo-messaging', 'zope-testrunner', 'google-cloud-monitoring', 'awacs',
'pydocstyle', 'lmdb', 'django-crispy-forms', 'jellyfish',
'google-cloud-speech', 'google-cloud-runtimeconfig', 'testscenarios',
'first', 'py-zabbix', 'bcdoc', 'azure-mgmt-web', 'google-cloud-dns',
'google-cloud-resource-manager', 'google-compute-engine', 'oslo-db',
'autobahn', 'ldap3', 'azure-mgmt-monitor', 'proto-google-cloud-logging-v2',
'azure-mgmt-trafficmanager', 'pypiwin32', 'azure-mgmt-cdn',
'oslo-middleware', 'azure-mgmt-authorization', 'google-cloud-spanner',
'python-json-logger', 'datetime', 'eggtestinfo', 'thriftpy', 'nosexcover',
'falcon', 'csvkit', 'ggplot', 'pyramid', 'pg8000', 'munkres', 'futurist',
'ciso8601', 'azure-graphrbac', 'python-dotenv', 'py2-ipaddress', 'peewee',
'brewer2mpl', 'dulwich', 'zeep', 'azure-mgmt-cognitiveservices',
'translationstring', 'sendgrid', 'xgboost', 'aws', 'prometheus-client',
'runcython', 'azure-mgmt-sql', 'kubernetes', 'oslo-service', 'annoy',
'oauth2', 'dbfread', 'mox3', 'wincertstore', 'initools', 'scikit-image',
'backport-collections', 'commonmark', 'pyproj', 'behave', 'qrcode',
'azure-mgmt-dns', 'azure-datalake-store',
'gapic-google-cloud-error-reporting-v1beta1', 'requests-aws4auth',
'flask-admin', 'pygame', 'cov-core', 'gapic-google-cloud-spanner-v1',
'agate', 'gapic-google-cloud-spanner-admin-database-v1',
'openstackdocstheme', 'azure-mgmt-containerregistry',
'djangorestframework-jwt',
'proto-google-cloud-error-reporting-v1beta1',
'proto-google-cloud-spanner-admin-database-v1',
'gapic-google-cloud-spanner-admin-instance-v1',
'azure-mgmt-datalake-store', 'proto-google-cloud-spanner-v1',
'proto-google-cloud-spanner-admin-instance-v1', 'runtime',
'azure-mgmt-datalake-analytics', 'oslotest', 'txaio', 'django-mptt',
'azure-keyvault', 'azure-mgmt-iothub', 'azure-mgmt-documentdb',
'oslo-policy', 'shade', 'pywavelets', 'flask-mail',
'azure-mgmt-devtestlabs', 'atx', 'azure-mgmt-scheduler', 'wand',
'azure-mgmt-datalake-nspkg', 'azure-mgmt-rdbms', 'empy',
'azure-mgmt-common', 'venusian', 'cairocffi', 'pysubnettree',
'agate-excel', 'toml', 'pyvmomi', 'oslosphinx', 'cchardet',
'requesocks', 'agate-dbf', 'openapi-codec', 'pylibmc', 'reno',
'httpbin', 'google-cloud-videointelligence', 'udatetime', 'pyroute2',
'flake8-docstrings', 'autograd', 'nodeenv', 'logutils', 'rq',
'azure-servicefabric', 'mongoengine', 'pycryptodomex', 'azure-mgmt-logic',
'leather', 'agate-sql', 'python-logstash', 'delorean', 'thrift-sasl',
'jpype1', 'shutit', 'wordsegment', 'flufl-enum', 'rjsmin', 'html2text',
'watchtower', 'pymeta3', 'netius', 'cairosvg', 'pybars3', 'recommonmark',
'uritemplate-py', 'fakeredis', 'python3-openid', 'filelock', 'jsmin',
'pipenv', 'django-environ', 'pyhs2', 'pep8-naming', 'typed-ast', 'pyusb',
'dedupe', 'dateutils', 'tablib', 'luigi', 'pysnmp', 'prettyplotlib',
'pre-commit', 'polib', 'jenkinsapi', 'rcssmin', 'ptable', 'multiprocess',
'pymc', 'pytest-metadata', 'django-oauth-toolkit', 'django-allauth',
'pygithub', 'python-crfsuite', 'python-cdb', 'pydas', 'pytest-cache',
'pyspin', 'pypi-publisher', 'pika-pool', 'pulp', 'pyinstaller',
'profilehooks', 'jenkins-job-builder', 'clickclick', 'urwid', 'pep257',
'sirepo', 'bandit', 'google-apitools', 'zope-proxy', 'cvxopt',
'pytest-catchlog', 'pybrain', 'gdata', 'toil', 'mypy',
'python2-pythondialog', 'pypng', 'sure', 'yamllint',
'robotframework-selenium2library', 'll-xist', 'tempora', 'webassets',
'pycadf', 'dropbox', 'pypandoc', 'django-taggit', 'paho-mqtt',
'keystonemiddleware', 'livereload', 'psycogreen', 'geocoder', 'ftfy',
'yapf', 'glances', 'grequests', 'coloredlogs', 'python-http-client',
'parsley', 'nose-exclude', 'transaction', 'flask-swagger', 'homeassistant',
'hvac', 'vcrpy', 'github3-py', 'schematics', 'tinycss',
'swagger-spec-validator', 'progressbar2', 'pydot', 'backoff', 'pytsite',
'scapy', 'attrdict', 'shellescape', 'impyla', 'flatten-dict',
'requests-kerberos', 'pykerberos', 'repoze-who', 'mxnet-mkl', 'cssmin',
'dask', 'cheroot', 'flake8-polyfill', 'pyotp', 'python-designateclient',
'simple-salesforce', 'hupper', 'neutron-lib', 'wavefront-cli', 'deepdiff',
'connexion', 'phonenumberslite', 'natsort', 'tox-travis', 'btrees',
'rednose', 'flask-testing', 'premailer', 'shortuuid', 'django-countries',
'ocflib', 'pylint-plugin-utils', 'pyenchant', 'logging', 'pysmi',
'appier-extras', 'zc-recipe-egg', 'oslo-rootwrap', 'flaky', 'libsass',
'oslo-versionedobjects', 'ipy', 'pecan', 'diff-match-patch',
'oslo-reports', 'google', 'aspen', 'rollbar', 'cobra',
'restructuredtext-lint', 'pythonnet', 'line-profiler', 'trollius',
'django-bootstrap3', 'pygeoip', 'django-picklefield', 'django-reversion',
'cytoolz', 'beaker', 'tooz', 'flask-assets', 'uuid', 'osprofiler',
'bitstring', 'naked', 'flask-babel', 'plac', 'semver', 'django-formtools',
'python-snappy', 'persistent', 'terminaltables', 'taskflow', 'boxsdk',
'cerberus', 'flask-principal', 'thinc', 'spacy', 'pycares', 'pylru',
'kafka', 'pkgconfig', 'couchbase', 'python-utils', 'django-localflavor',
'django-redis-cache', 'webapp2', 'sqlalchemy-redshift', 'salt',
'structlog', 'mandrill', 'googlemaps', 'easy-thumbnails', 'automaton',
'webcolors'
]
def get_requirement(dep):
import pip
import requirements
"""Pre-clean requirement strings passed to the requirements parser.
Ensures that we can accept both local and relative paths, file and VCS URIs,
remote URIs, and package names, and that we pass only valid requirement strings
to the requirements parser. Performs necessary modifications to requirements
object if the user input was a local relative path.
:param str dep: A requirement line
:returns: :class:`requirements.Requirement` object
"""
path = None
uri = None
editable = False
dep_link = None
# check for editable dep / vcs dep
if isinstance(dep, six.string_types) and dep.startswith('-e '):
editable = True
# Use the user supplied path as the written dependency
dep = dep.split(' ', 1)[1]
# Split out markers if they are present - similar to how pip does it
# See pip.req.req_install.InstallRequirement.from_line
if not any(dep.startswith(uri_prefix) for uri_prefix in SCHEME_LIST):
marker_sep = ';'
else:
marker_sep = '; '
if marker_sep in dep:
dep, markers = dep.split(marker_sep, 1)
markers = markers.strip()
if not markers:
markers = None
else:
markers = None
# Strip extras from the requirement so we can make a properly parseable req
dep, extras = pip.req.req_install._strip_extras(dep)
matches_uri = any(dep.startswith(uri_prefix) for uri_prefix in SCHEME_LIST)
# Only operate on local, existing, non-URI formatted paths which are installable
if is_file(dep) and isinstance(dep, six.string_types) and not matches_uri and is_installable_file(dep):
dep_path = Path(dep)
dep_link = Link(dep_path.absolute().as_uri())
if dep_path.is_absolute() or dep_path.as_posix() == '.':
path = dep_path.as_posix()
else:
path = get_converted_relative_path(dep)
dep = dep_link.egg_fragment if dep_link.egg_fragment else dep_link.url_without_fragment
elif is_vcs(dep):
# Generate a Link object for parsing egg fragments
dep_link = Link(dep)
# Save the original path to store in the pipfile
uri = dep_link.url
# Construct the requirement using proper git+ssh:// replaced uris or names if available
dep = clean_vcs_uri(dep)
if editable:
dep = '-e {0}'.format(dep)
req = [r for r in requirements.parse(dep)][0]
# if all we built was the requirement name and still need eveerything else
if req.name and not any([req.uri, req.path]):
if dep_link:
if dep_link.scheme.startswith('file') and path and not req.path:
req.path = path
req.local_file = True
req.uri = None
else:
req.uri = dep_link.url_without_fragment
# If the result is a local file with a URI and we have a local path, unset the URI
# and set the path instead -- note that local files may have 'path' set by accident
elif req.local_file and path and not req.vcs:
req.path = path
req.uri = None
elif req.vcs and req.uri and uri != req.uri:
if req.uri.startswith('git+ssh://') and uri.startswith('git+git'):
req.uri = req.uri.replace('git+ssh://', 'git+')
req.line = req.line.replace('git+ssh://', 'git+')
if editable and not req.editable:
req.editable = True
if markers:
req.markers = markers
if extras:
# Bizarrely this is also what pip does...
req.extras = [r for r in requirements.parse('fakepkg{0}'.format(extras))][0].extras
return req
def cleanup_toml(tml):
toml = tml.split('\n')
new_toml = []
# Remove all empty lines from TOML.
for line in toml:
if line.strip():
new_toml.append(line)
toml = '\n'.join(new_toml)
new_toml = []
# Add newlines between TOML sections.
for i, line in enumerate(toml.split('\n')):
after = False
# Skip the first line.
if line.startswith('['):
if i > 0:
# Insert a newline before the heading.
new_toml.append('\n')
after = True
new_toml.append(line)
# Insert a newline after the heading.
if after:
new_toml.append('')
# adding new line at the end of the TOML file
new_toml.append('')
toml = '\n'.join(new_toml)
return toml
def suggest_package(package):
"""Suggests a package name, given a package name."""
if SESSION_IS_INTERACTIVE:
if ('-' in package) or ('[' in package) or ('+' in package):
THRESHOLD = 90
else:
THRESHOLD = 86
# Bypass for speed.
if package in packages:
return package
result = fuzzywuzzy.process.extractOne(package, packages)
if result[1] > THRESHOLD:
return result[0]
def python_version(path_to_python):
if not path_to_python:
return None
try:
c = delegator.run([path_to_python, '--version'], block=False)
except Exception:
return None
output = c.out.strip() or c.err.strip()
@parse.with_pattern(r'.*')
def allow_empty(text):
return text
TEMPLATE = 'Python {}.{}.{:d}{:AllowEmpty}'
parsed = parse.parse(TEMPLATE, output, dict(AllowEmpty=allow_empty))
if parsed:
parsed = parsed.fixed
else:
return None
return u"{v[0]}.{v[1]}.{v[2]}".format(v=parsed)
def shellquote(s):
"""Prepares a string for the shell (on Windows too!)"""
if s is None:
return None
# Additional escaping for windows paths
if os.name == 'nt':
s = "{}".format(s.replace("\\", "\\\\"))
return '"' + s.replace("'", "'\\''") + '"'
def clean_pkg_version(version):
"""Uses pip to prepare a package version string, from our internal version."""
return six.u(pep440_version(str(version).replace('==', '')))
class HackedPythonVersion(object):
"""A Beautiful hack, which allows us to tell pip which version of Python we're using."""
def __init__(self, python_version, python_path):
self.python_version = python_version
self.python_path = python_path
def __enter__(self):
os.environ['PIP_PYTHON_VERSION'] = str(self.python_version)
os.environ['PIP_PYTHON_PATH'] = str(self.python_path)
def __exit__(self, *args):
# Restore original Python version information.
del os.environ['PIP_PYTHON_VERSION']
def prepare_pip_source_args(sources, pip_args=None):
if pip_args is None:
pip_args = []
if sources:
# Add the source to pip.
pip_args.extend(['-i', sources[0]['url']])
# Trust the host if it's not verified.
if not sources[0].get('verify_ssl', True):
pip_args.extend(['--trusted-host', urlparse(sources[0]['url']).netloc.split(':')[0]])
# Add additional sources as extra indexes.
if len(sources) > 1:
for source in sources[1:]:
pip_args.extend(['--extra-index-url', source['url']])
# Trust the host if it's not verified.
if not source.get('verify_ssl', True):
pip_args.extend(['--trusted-host', urlparse(source['url']).netloc.split(':')[0]])
return pip_args
def actually_resolve_reps(deps, index_lookup, markers_lookup, project, sources, verbose, clear, pre):
import pip
class PipCommand(pip.basecommand.Command):
"""Needed for pip-tools."""
name = 'PipCommand'
constraints = []
for dep in deps:
t = tempfile.mkstemp(prefix='pipenv-', suffix='-requirement.txt')[1]
with open(t, 'w') as f:
f.write(dep)
if dep.startswith('-e '):
constraint = pip.req.InstallRequirement.from_editable(dep[len('-e '):])
else:
constraint = [c for c in pip.req.parse_requirements(t, session=pip._vendor.requests)][0]
# extra_constraints = []
if ' -i ' in dep:
index_lookup[constraint.name] = project.get_source(url=dep.split(' -i ')[1]).get('name')
if constraint.markers:
markers_lookup[constraint.name] = str(constraint.markers).replace('"', "'")
constraints.append(constraint)
pip_command = get_pip_command()
pip_args = []
if sources:
pip_args = prepare_pip_source_args(sources, pip_args)
if verbose:
print('Using pip: {0}'.format(' '.join(pip_args)))
pip_options, _ = pip_command.parse_args(pip_args)
session = pip_command._build_session(pip_options)
pypi = PyPIRepository(pip_options=pip_options, session=session)
if verbose:
logging.log.verbose = True
resolved_tree = set()
resolver = Resolver(constraints=constraints, repository=pypi, clear_caches=clear, prereleases=pre)
# pre-resolve instead of iterating to avoid asking pypi for hashes of editable packages
try:
resolved_tree.update(resolver.resolve(max_rounds=PIPENV_MAX_ROUNDS))
except (NoCandidateFound, DistributionNotFound, HTTPError) as e:
click.echo(
'{0}: Your dependencies could not be resolved. You likely have a mismatch in your sub-dependencies.\n '
'You can use {1} to bypass this mechanism, then run {2} to inspect the situation.'
''.format(
crayons.red('Warning', bold=True),
crayons.red('$ pipenv install --skip-lock'),
crayons.red('$ pipenv graph')
),
err=True)
click.echo(crayons.blue(str(e)))
if 'no version found at all' in str(e):
click.echo(crayons.blue('Please check your version specifier and version number. See PEP440 for more information.'))
raise RuntimeError
return resolved_tree, resolver
def resolve_deps(deps, which, which_pip, project, sources=None, verbose=False, python=False, clear=False, pre=False, allow_global=False):
"""Given a list of dependencies, return a resolved list of dependencies,
using pip-tools -- and their hashes, using the warehouse API / pip.
"""
index_lookup = {}
markers_lookup = {}
python_path = which('python', allow_global=allow_global)
backup_python_path = sys.executable
results = []
# First (proper) attempt:
with HackedPythonVersion(python_version=python, python_path=python_path):
try:
resolved_tree, resolver = actually_resolve_reps(deps, index_lookup, markers_lookup, project, sources, verbose, clear, pre)
except RuntimeError:
# Don't exit here, like usual.
resolved_tree = None
# Second (last-resort) attempt:
if resolved_tree is None:
with HackedPythonVersion(python_version='.'.join([str(s) for s in sys.version_info[:3]]), python_path=backup_python_path):
try:
# Attempt to resolve again, with different Python version information,
# particularly for particularly particular packages.
resolved_tree, resolver = actually_resolve_reps(deps, index_lookup, markers_lookup, project, sources, verbose, clear, pre)
except RuntimeError:
sys.exit(1)
for result in resolved_tree:
if not result.editable:
name = pep423_name(result.name)
version = clean_pkg_version(result.specifier)
index = index_lookup.get(result.name)
if not markers_lookup.get(result.name):
markers = str(result.markers) if result.markers and 'extra' not in str(result.markers) else None
else:
markers = markers_lookup.get(result.name)
collected_hashes = []
if 'python.org' in '|'.join([source['url'] for source in sources]):
try:
# Grab the hashes from the new warehouse API.
r = requests.get('https://pypi.org/pypi/{0}/json'.format(name), timeout=10)
api_releases = r.json()['releases']
cleaned_releases = {}
for api_version, api_info in api_releases.items():
cleaned_releases[clean_pkg_version(api_version)] = api_info
for release in cleaned_releases[version]:
collected_hashes.append(release['digests']['sha256'])
collected_hashes = ['sha256:' + s for s in collected_hashes]
# Collect un-collectable hashes.
if not collected_hashes:
collected_hashes = list(list(resolver.resolve_hashes([result]).items())[0][1])
except (ValueError, KeyError, ConnectionError):
if verbose:
print('Error fetching {}'.format(name))
d = {'name': name, 'version': version, 'hashes': collected_hashes}
if index:
d.update({'index': index})
if markers:
d.update({'markers': markers.replace('"', "'")})
results.append(d)
return results
def multi_split(s, split):
"""Splits on multiple given separators."""
for r in split:
s = s.replace(r, '|')
return [i for i in s.split('|') if len(i) > 0]
def convert_deps_from_pip(dep):
""""Converts a pip-formatted dependency to a Pipfile-formatted one."""
dependency = {}
req = get_requirement(dep)
extras = {'extras': req.extras}
# File installs.
if (req.uri or req.path or (is_installable_file(req.name) if req.name else False)) and not req.vcs:
# Assign a package name to the file, last 7 of it's sha256 hex digest.
if not req.uri and not req.path:
req.path = os.path.abspath(req.name)
hashable_path = req.uri if req.uri else req.path
req.name = hashlib.sha256(hashable_path.encode('utf-8')).hexdigest()
req.name = req.name[len(req.name) - 7:]
# {path: uri} TOML (spec 4 I guess...)
if req.uri:
dependency[req.name] = {'file': hashable_path}
else:
dependency[req.name] = {'path': hashable_path}
if req.extras:
dependency[req.name].update(extras)
# Add --editable if applicable
if req.editable:
dependency[req.name].update({'editable': True})
# VCS Installs. Extra check for unparsed git over SSH
elif req.vcs or is_vcs(req.path):
if req.name is None:
raise ValueError('pipenv requires an #egg fragment for version controlled '
'dependencies. Please install remote dependency '
'in the form {0}#egg=<package-name>.'.format(req.uri))
# Set up this requirement as a proper VCS requirement if it was not
if not req.vcs and req.path.startswith(VCS_LIST):
req.vcs = [vcs for vcs in VCS_LIST if req.path.startswith(vcs)][0]
req.uri = '{0}'.format(req.path)
req.path = None
# Crop off the git+, etc part.
if req.uri.startswith('{0}+'.format(req.vcs)):
req.uri = req.uri[len(req.vcs) + 1:]
dependency.setdefault(req.name, {}).update({req.vcs: req.uri})
# Add --editable, if it's there.
if req.editable:
dependency[req.name].update({'editable': True})
# Add subdirectory, if it's there
if req.subdirectory:
dependency[req.name].update({'subdirectory': req.subdirectory})
# Add the specifier, if it was provided.
if req.revision:
dependency[req.name].update({'ref': req.revision})
# Extras: e.g. #egg=requests[security]
if req.extras:
dependency[req.name].update({'extras': req.extras})
elif req.extras or req.specs:
specs = None
# Comparison operators: e.g. Django>1.10
if req.specs:
r = multi_split(dep, '!=<>~')
specs = dep[len(r[0]):]
dependency[req.name] = specs
# Extras: e.g. requests[socks]
if req.extras:
dependency[req.name] = extras
if specs:
dependency[req.name].update({'version': specs})
# Bare dependencies: e.g. requests
else:
dependency[dep] = '*'
# Cleanup when there's multiple values, e.g. -e.
if len(dependency) > 1:
for key in dependency.copy():
if not hasattr(dependency[key], 'keys'):
del dependency[key]
return dependency
def convert_deps_to_pip(deps, project=None, r=True, include_index=False):
""""Converts a Pipfile-formatted dependency to a pip-formatted one."""
dependencies = []
for dep in deps.keys():
# Default (e.g. '>1.10').
extra = deps[dep] if isinstance(deps[dep], six.string_types) else ''
version = ''
index = ''
# Get rid of '*'.
if deps[dep] == '*' or str(extra) == '{}':
extra = ''
hash = ''
# Support for single hash (spec 1).
if 'hash' in deps[dep]:
hash = ' --hash={0}'.format(deps[dep]['hash'])
# Support for multiple hashes (spec 2).
if 'hashes' in deps[dep]:
hash = '{0} '.format(''.join([' --hash={0} '.format(h) for h in deps[dep]['hashes']]))
# Support for extras (e.g. requests[socks])
if 'extras' in deps[dep]:
extra = '[{0}]'.format(deps[dep]['extras'][0])
if 'version' in deps[dep]:
if not deps[dep]['version'] == '*':
version = deps[dep]['version']
# For lockfile format.
if 'markers' in deps[dep]:
specs = '; {0}'.format(deps[dep]['markers'])
else:
# For pipfile format.
specs = []
for specifier in specifiers:
if specifier in deps[dep]:
if not deps[dep][specifier] == '*':
specs.append('{0} {1}'.format(specifier, deps[dep][specifier]))
if specs:
specs = '; {0}'.format(' and '.join(specs))
else:
specs = ''
if include_index:
if 'index' in deps[dep]:
pip_args = prepare_pip_source_args([project.get_source(deps[dep]['index'])])
index = ' '.join(pip_args)
# Support for version control
maybe_vcs = [vcs for vcs in VCS_LIST if vcs in deps[dep]]
vcs = maybe_vcs[0] if maybe_vcs else None
# Support for files.
if 'file' in deps[dep]:
extra = '{1}{0}'.format(extra, deps[dep]['file']).strip()
# Flag the file as editable if it is a local relative path
if 'editable' in deps[dep]:
dep = '-e '
else:
dep = ''
# Support for paths.
elif 'path' in deps[dep]:
extra = '{1}{0}'.format(extra, deps[dep]['path']).strip()
# Flag the file as editable if it is a local relative path
if 'editable' in deps[dep]:
dep = '-e '
else:
dep = ''
if vcs:
extra = '{0}+{1}'.format(vcs, deps[dep][vcs])
# Support for @refs.
if 'ref' in deps[dep]:
extra += '@{0}'.format(deps[dep]['ref'])
extra += '#egg={0}'.format(dep)
# Support for subdirectory
if 'subdirectory' in deps[dep]:
extra += '&subdirectory={0}'.format(deps[dep]['subdirectory'])
# Support for editable.
if 'editable' in deps[dep]:
# Support for --egg.
dep = '-e '
else:
dep = ''
s = '{0}{1}{2}{3}{4} {5}'.format(dep, extra, version, specs, hash, index).strip()
dependencies.append(s)
if not r:
return dependencies
# Write requirements.txt to tmp directory.
f = tempfile.NamedTemporaryFile(suffix='-requirements.txt', delete=False)
f.write('\n'.join(dependencies).encode('utf-8'))
return f.name
def mkdir_p(newdir):
"""works the way a good mkdir should :)
- already exists, silently complete
- regular file in the way, raise an exception
- parent directory(ies) does not exist, make them as well
From: http://code.activestate.com/recipes/82465-a-friendly-mkdir/
"""
if os.path.isdir(newdir):
pass
elif os.path.isfile(newdir):
raise OSError("a file with the same name as the desired dir, '{0}', already exists.".format(newdir))
else:
head, tail = os.path.split(newdir)
if head and not os.path.isdir(head):
mkdir_p(head)
if tail:
os.mkdir(newdir)
def is_required_version(version, specified_version):
"""Check to see if there's a hard requirement for version
number provided in the Pipfile.
"""
# Certain packages may be defined with multiple values.
if isinstance(specified_version, dict):
specified_version = specified_version.get('version', '')
if specified_version.startswith('=='):
return version.strip() == specified_version.split('==')[1].strip()
return True
def clean_vcs_uri(uri):
"""Cleans VCS uris from pip format"""
if isinstance(uri, six.string_types):
# Add scheme for parsing purposes, this is also what pip does
if uri.startswith('git+') and '://' not in uri:
uri = uri.replace('git+', 'git+ssh://')
return uri
def is_vcs(pipfile_entry):
import requirements
"""Determine if dictionary entry from Pipfile is for a vcs dependency."""
if hasattr(pipfile_entry, 'keys'):
return any(key for key in pipfile_entry.keys() if key in VCS_LIST)
elif isinstance(pipfile_entry, six.string_types):
return bool(requirements.requirement.VCS_REGEX.match(clean_vcs_uri(pipfile_entry)))
return False
def is_installable_file(path):
import pip
"""Determine if a path can potentially be installed"""
if hasattr(path, 'keys') and any(key for key in path.keys() if key in ['file', 'path']):
path = urlparse(path['file']).path if 'file' in path else path['path']
if not isinstance(path, six.string_types) or path == '*':
return False
# If the string starts with a valid specifier operator, test if it is a valid
# specifier set before making a path object (to avoid breaking windows)
if any(path.startswith(spec) for spec in '!=<>~'):
try:
pip.utils.packaging.specifiers.SpecifierSet(path)
# If this is not a valid specifier, just move on and try it as a path
except pip.utils.packaging.specifiers.InvalidSpecifier:
pass
else:
return False
lookup_path = Path(path)
if not lookup_path.exists():
return False
lookup_link = Link(lookup_path.resolve().as_uri())
absolute_path = '{0}'.format(lookup_path.absolute())
return ((lookup_path.is_file() and (is_archive_file(absolute_path) or lookup_link.is_wheel)) or
(lookup_path.is_dir() and pip.utils.is_installable_dir(lookup_path.as_posix())))
def is_file(package):
"""Determine if a package name is for a File dependency."""
if hasattr(package, 'keys'):
return any(key for key in package.keys() if key in ['file', 'path'])
if os.path.exists(str(package)):
return True
for start in SCHEME_LIST:
if str(package).startswith(start):
return True
return False
def pep440_version(version):
"""Normalize version to PEP 440 standards"""
import pip
# Use pip built-in version parser.
return str(pip.index.parse_version(version))
def pep423_name(name):
"""Normalize package name to PEP 423 style standard."""
name = name.lower()
if any(i not in name for i in (VCS_LIST+SCHEME_LIST)):
return name.replace('_', '-')
else:
return name
def proper_case(package_name):
"""Properly case project name from pypi.org."""
# Hit the simple API.
r = requests.get('https://pypi.org/pypi/{0}/json'.format(package_name), timeout=0.3, stream=True)
if not r.ok:
raise IOError('Unable to find package {0} in PyPI repository.'.format(package_name))
r = parse.parse('https://pypi.org/pypi/{name}/json', r.url)
good_name = r['name']
return good_name
def split_section(input_file, section_suffix, test_function):
"""
Split a pipfile or a lockfile section out by section name and test function
:param dict input_file: A dictionary containing either a pipfile or lockfile
:param str section_suffix: A string of the name of the section
:param func test_function: A test function to test against the value in the key/value pair
>>> split_section(my_lockfile, 'vcs', is_vcs)
{
'default': {
"six": {
"hashes": [
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
],
"version": "==1.11.0"
}
},
'default-vcs': {
"e1839a8": {
"editable": true,
"path": "."
}
}
}
"""
pipfile_sections = ('packages', 'dev-packages')
lockfile_sections = ('default', 'develop')
if any(section in input_file for section in pipfile_sections):
sections = pipfile_sections
elif any(section in input_file for section in lockfile_sections):
sections = lockfile_sections
else:
# return the original file if we can't find any pipfile or lockfile sections
return input_file
for section in sections:
split_dict = {}
entries = input_file.get(section, {})
for k in list(entries.keys()):
if test_function(entries.get(k)):
split_dict[k] = entries.pop(k)
input_file['-'.join([section, section_suffix])] = split_dict
return input_file
def split_file(file_dict):
"""Split VCS and editable dependencies out from file."""
sections = {
'vcs': is_vcs,
'editable': lambda x: hasattr(x, 'keys') and x.get('editable')
}
for k, func in sections.items():
file_dict = split_section(file_dict, k, func)
return file_dict
def merge_deps(file_dict, project, dev=False, requirements=False, ignore_hashes=False, blocking=False, only=False):
"""
Given a file_dict, merges dependencies and converts them to pip dependency lists.
:param dict file_dict: The result of calling :func:`pipenv.utils.split_file`
:param :class:`pipenv.project.Project` project: Pipenv project
:param bool dev=False: Flag indicating whether dev dependencies are to be installed
:param bool requirements=False: Flag indicating whether to use a requirements file
:param bool ignore_hashes=False:
:param bool blocking=False:
:param bool only=False:
:return: Pip-converted 3-tuples of [deps, requirements_deps]
"""
deps = []
requirements_deps = []
for section in list(file_dict.keys()):
# Turn develop-vcs into ['develop', 'vcs']
section_name, suffix = section.rsplit('-', 1) if '-' in section and not section == 'dev-packages' else (section, None)
if not file_dict[section] or section_name not in ('dev-packages', 'packages', 'default', 'develop'):
continue
is_dev = section_name in ('dev-packages', 'develop')
if is_dev and not dev:
continue
if ignore_hashes:
for k, v in file_dict[section]:
if 'hash' in v:
del v['hash']
# Block and ignore hashes for all suffixed sections (vcs/editable)
no_hashes = True if suffix else ignore_hashes
block = True if suffix else blocking
include_index = True if not suffix else False
converted = convert_deps_to_pip(file_dict[section], project, r=False, include_index=include_index)
deps.extend((d, no_hashes, block) for d in converted)
if dev and is_dev and requirements:
requirements_deps.extend((d, no_hashes, block) for d in converted)
return deps, requirements_deps
def recase_file(file_dict):
"""Recase file before writing to output."""
if 'packages' in file_dict or 'dev-packages' in file_dict:
sections = ('packages', 'dev-packages')
elif 'default' in file_dict or 'develop' in file_dict:
sections = ('default', 'develop')
for section in sections:
file_section = file_dict.get(section, {})
# Try to properly case each key if we can.
for key in list(file_section.keys()):
try:
cased_key = proper_case(key)
except IOError:
cased_key = key
file_section[cased_key] = file_section.pop(key)
return file_dict
def get_windows_path(*args):
"""Sanitize a path for windows environments
Accepts an arbitrary list of arguments and makes a clean windows path"""
return os.path.normpath(os.path.join(*args))
def find_windows_executable(bin_path, exe_name):
"""Given an executable name, search the given location for an executable"""
requested_path = get_windows_path(bin_path, exe_name)
if os.path.exists(requested_path):
return requested_path
# Ensure we aren't adding two layers of file extensions
exe_name = os.path.splitext(exe_name)[0]
files = ['{0}.{1}'.format(exe_name, ext) for ext in ['', 'py', 'exe', 'bat']]
exec_paths = [get_windows_path(bin_path, f) for f in files]
exec_files = [filename for filename in exec_paths if os.path.isfile(filename)]
if exec_files:
return exec_files[0]
return find_executable(exe_name)
def get_converted_relative_path(path, relative_to=os.curdir):
"""Given a vague relative path, return the path relative to the given location"""
return os.path.join('.', os.path.relpath(path, start=relative_to))
def walk_up(bottom):
"""Mimic os.walk, but walk 'up' instead of down the directory tree.
From: https://gist.github.com/zdavkeos/1098474
"""
bottom = os.path.realpath(bottom)
# Get files in current dir.
try:
names = os.listdir(bottom)
except Exception:
return
dirs, nondirs = [], []
for name in names:
if os.path.isdir(os.path.join(bottom, name)):
dirs.append(name)
else:
nondirs.append(name)
yield bottom, dirs, nondirs
new_path = os.path.realpath(os.path.join(bottom, '..'))
# See if we are at the top.
if new_path == bottom:
return
for x in walk_up(new_path):
yield x
def find_requirements(max_depth=3):
"""Returns the path of a Pipfile in parent directories."""
i = 0
for c, d, f in walk_up(os.getcwd()):
i += 1
if i < max_depth:
if 'requirements.txt':
r = os.path.join(c, 'requirements.txt')
if os.path.isfile(r):
return r
raise RuntimeError('No requirements.txt found!')
# Borrowed from pew to avoid importing pew which imports psutil
# See https://github.com/berdario/pew/blob/master/pew/_utils.py#L82
@contextmanager
def temp_environ():
"""Allow the ability to set os.environ temporarily"""
environ = dict(os.environ)
try:
yield
finally:
os.environ.clear()
os.environ.update(environ)
def is_valid_url(url):
"""Checks if a given string is an url"""
pieces = urlparse(url)
return all([pieces.scheme, pieces.netloc])
def download_file(url, filename):
"""Downloads file from url to a path with filename"""
r = requests.get(url, stream=True)
if not r.ok:
raise IOError('Unable to download file')
with open(filename, 'wb') as f:
f.write(r.content)
def need_update_check():
"""Determines whether we need to check for updates."""
mkdir_p(PIPENV_CACHE_DIR)
p = os.sep.join((PIPENV_CACHE_DIR, '.pipenv_update_check'))
if not os.path.exists(p):
return True
out_of_date_time = time() - (24 * 60 * 60)
if os.path.isfile(p) and os.path.getmtime(p) <= out_of_date_time:
return True
else:
return False
def touch_update_stamp():
"""Touches PIPENV_CACHE_DIR/.pipenv_update_check"""
mkdir_p(PIPENV_CACHE_DIR)
p = os.sep.join((PIPENV_CACHE_DIR, '.pipenv_update_check'))
try:
os.utime(p, None)
except OSError:
with open(p, 'w') as fh:
fh.write('')
def normalize_drive(path):
"""Normalize drive in path so they stay consistent.
This currently only affects local drives on Windows, which can be
identified with either upper or lower cased drive names. The case is
always converted to uppercase because it seems to be preferred.
See: <https://github.com/pypa/pipenv/issues/1218>
"""
if os.name != 'nt' or not isinstance(path, six.string_types):
return path
drive, tail = os.path.splitdrive(path)
# Only match (lower cased) local drives (e.g. 'c:'), not UNC mounts.
if drive.islower() and len(drive) == 2 and drive[1] == ':':
return '{}{}'.format(drive.upper(), tail)
return path