diff --git a/Pipfile b/Pipfile index d9da9c57..581b96b4 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pytest-xdist = "*" click = "*" pytest-pypy = {path = "./tests/pytest-pypi", editable = true} pytest-tap = "*" -stdeb = {version="*", sys_platform="== 'linux'"} +stdeb = {version="*", markers="sys_platform == 'linux'"} white = {version="*", markers="python_version >= '3.6'"} [packages] @@ -23,4 +23,4 @@ tests = "bash ./run-tests.sh" [pipenv] -allow_prereleases = true \ No newline at end of file +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index df32d972..bf38293e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8acf60476c4efca4f1be83dc747f18d4a663c3479e664e9e6865469f7ab9b958" + "sha256": "eab749ae861993f9e4b71bea88782c806a02fa82e3f45c82c4550ee459d8b886" }, "pipfile-spec": 6, "requires": {}, @@ -29,6 +29,13 @@ ], "version": "==1.4" }, + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "version": "==0.24.0" + }, "attrs": { "hashes": [ "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", @@ -43,6 +50,14 @@ ], "version": "==2.5.3" }, + "black": { + "hashes": [ + "sha256:0461c7a52b5beb378936bf642753dec7a45305c96c6129d540b9c53227121a5a", + "sha256:7183263650ba3071034e90b40a1ea74abccbd32cf525cef6d7914479dbe7f2fb" + ], + "markers": "python_version > '3.5'", + "version": "==18.3a0" + }, "certifi": { "hashes": [ "sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", @@ -50,6 +65,39 @@ ], "version": "==2018.1.18" }, + "cffi": { + "hashes": [ + "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", + "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", + "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", + "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", + "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", + "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", + "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", + "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", + "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", + "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", + "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", + "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", + "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", + "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", + "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", + "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", + "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", + "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", + "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", + "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", + "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", + "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", + "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", + "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", + "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", + "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", + "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" + ], + "markers": "platform_python_implementation != 'pypy'", + "version": "==1.11.5" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -73,6 +121,35 @@ "markers": "sys_platform == 'win32'", "version": "==0.3.9" }, + "configparser": { + "hashes": [ + "sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a" + ], + "markers": "python_version < '3.2'", + "version": "==3.5.0" + }, + "cryptography": { + "hashes": [ + "sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd", + "sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04", + "sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f", + "sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd", + "sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb", + "sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2", + "sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037", + "sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd", + "sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531", + "sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63", + "sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e", + "sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351", + "sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a", + "sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563", + "sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab", + "sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471", + "sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887" + ], + "version": "==2.2.2" + }, "docutils": { "hashes": [ "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", @@ -81,6 +158,16 @@ ], "version": "==0.14" }, + "enum34": { + "hashes": [ + "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", + "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", + "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", + "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" + ], + "markers": "python_version < '3'", + "version": "==1.1.6" + }, "execnet": { "hashes": [ "sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a", @@ -103,6 +190,14 @@ ], "version": "==0.12.2" }, + "funcsigs": { + "hashes": [ + "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", + "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" + ], + "markers": "python_version < '3.0'", + "version": "==1.0.2" + }, "idna": { "hashes": [ "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", @@ -117,6 +212,13 @@ ], "version": "==1.0.0" }, + "ipaddress": { + "hashes": [ + "sha256:200d8686011d470b5e4de207d803445deee427455cd0cb7c982b68cf82524f81" + ], + "markers": "python_version < '3'", + "version": "==1.0.19" + }, "itsdangerous": { "hashes": [ "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" @@ -159,19 +261,26 @@ ], "version": "==4.1.0" }, - "pathlib": { + "ordereddict": { "hashes": [ - "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f" + "sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f" + ], + "version": "==1.1" + }, + "pathlib2": { + "hashes": [ + "sha256:24e0b33e1333b55e73c9d1e9a8342417d519f7789a9d3b440f4acd00ea45157e", + "sha256:deb3a960c1d55868dfbcac98432358b92ba89d95029cddd4040db1f27405055c" ], "markers": "python_version < '3.4'", - "version": "==1.0.1" + "version": "==2.1.0" }, "pbr": { "hashes": [ - "sha256:05f61c71aaefc02d8e37c0a3eeb9815ff526ea28b3b76324769e6158d7f95be1", - "sha256:60c25b7dfd054ef9bb0ae327af949dd4676aa09ac3a9471cdc871d8a9213f9ac" + "sha256:8b9a7c3704657cb0831b3ded0a6b61377947c8235b76649bb7de4bfe2e6cfa56", + "sha256:cf66675e22ae91a4f20e4b8354f117d3e3d1de651513051d109cc39645fb3672" ], - "version": "==3.1.1" + "version": "==4.0.0" }, "pipenv": { "editable": true, @@ -204,6 +313,12 @@ ], "version": "==2.3.1" }, + "pycparser": { + "hashes": [ + "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" + ], + "version": "==2.18" + }, "pyflakes": { "hashes": [ "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", @@ -218,6 +333,13 @@ ], "version": "==2.2.0" }, + "pyopenssl": { + "hashes": [ + "sha256:07a2de1a54de07448732a81e38a55df7da109b2f47f599f8bb35b0cbec69d4bd", + "sha256:2c10cfba46a52c0b0950118981d61e72c1e5b1aac451ca1bc77de1a679456773" + ], + "version": "==17.5.0" + }, "pytest": { "hashes": [ "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", @@ -328,10 +450,10 @@ }, "tqdm": { "hashes": [ - "sha256:05e991ecb0f874046ddcb374396a626afd046fb4d31f73633ea752b844458a7a", - "sha256:2aea9f81fdf127048667e0ba22f5fc10ebc879fb838dc52dcf055242037ec1f7" + "sha256:782aa84b61a5246c4f9e5b938875009e0b759d9a5c9d16b12e4f8deefdff7892", + "sha256:eaf9c32a2cf7b9623eb272134b934eb354ef6304299611aa71e3cf47cf83c22b" ], - "version": "==4.19.8" + "version": "==4.19.9" }, "twine": { "hashes": [ diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..d268576b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,77 @@ +build: off +version: 1.0.{build} + +skip_branch_with_pr: true + +init: + + - git config --global core.sharedRepository true + - git config --global core.longpaths true + - git config --global core.autocrlf input + +environment: + + PYPI_VENDOR_DIR: ".\\tests\\pypi\\" + GIT_ASK_YESNO: "false" + SHELL: "windows" + PYTHON_ARCH: "64" + PYTHONIOENCODING: "utf-8" + + matrix: + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + TEST_SUITE: "cli" + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + TEST_SUITE: '"dotvenv or check or unused or requirements"' + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + TEST_SUITE: "complex" + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + TEST_SUITE: '"markers or run or project or utils"' + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + TEST_SUITE: "install" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + TEST_SUITE: "cli" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + TEST_SUITE: '"dotvenv or check or unused or requirements"' + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + TEST_SUITE: "complex" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + TEST_SUITE: '"markers or run or project or utils"' + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "2.7.x" + TEST_SUITE: "install" + +install: + - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% + - "%PYTHON%\\python.exe -m pip install --upgrade pip" + - "%PYTHON%\\python.exe -m pip install -e ." + - "%PYTHON%\\python.exe -m pipenv install -e ." + - "%PYTHON%\\python.exe -m pipenv install --dev" + - echo "Running pipenv at:" + - "%PYTHON%\\python.exe -m pipenv --venv" + - echo "Using python:" + - "%PYTHON%\\python.exe -m pipenv --py" + +cache: + - "%LocalAppData%\\pip\\cache" + +test_script: + - cmd: set PYPI_VENDOR_DIR=".\tests\pypi\" && %PYTHON%\\python.exe -m pipenv run pytest -v -n auto -m %TEST_SUITE% tests diff --git a/pipenv/cli.py b/pipenv/cli.py index 449cd3c8..7baa3c6e 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -11,7 +11,7 @@ from click_didyoumean import DYMCommandCollection from .__version__ import __version__ -from .import environments +from . import environments from .environments import * # Enable shell completion. @@ -23,7 +23,7 @@ class PipenvGroup(click.Group): """Custom Group class provides formatted main help""" def get_help_option(self, ctx): - from .import core + from . import core """Override for showing formatted main help via --help and -h options""" help_options = self.get_help_option_names(ctx) @@ -148,7 +148,7 @@ def cli( ) sys.exit(1) sys.exit(0) - from .import core + from . import core if man: if core.system_which('man'): path = os.sep.join([os.path.dirname(__file__), 'pipenv.1']) @@ -344,7 +344,7 @@ def install( keep_outdated=False, selective_upgrade=False, ): - from .import core + from . import core core.do_install( package_name=package_name, @@ -426,7 +426,7 @@ def uninstall( verbose=False, keep_outdated=False, ): - from .import core + from . import core core.do_uninstall( package_name=package_name, @@ -499,7 +499,7 @@ def lock( pre=False, keep_outdated=False, ): - from .import core + from . import core # Ensure that virtualenv is available. core.ensure_project(three=three, python=python) @@ -542,7 +542,7 @@ def lock( def shell( three=None, python=False, fancy=False, shell_args=None, anyway=False ): - from .import core + from . import core # Prevent user from activating nested environments. if 'PIPENV_ACTIVE' in os.environ: @@ -594,7 +594,7 @@ def shell( help="Specify which version of Python virtualenv should use.", ) def run(command, args, three=None, python=False): - from .import core + from . import core core.do_run(command=command, args=args, three=three, python=python) @@ -633,7 +633,7 @@ def check( style=False, args=None, ): - from .import core + from . import core core.do_check( three=three, python=python, system=system, unused=unused, args=args @@ -719,7 +719,7 @@ def update( outdated=False, more_packages=None, ): - from .import core + from . import core core.ensure_project(three=three, python=python, warn=True) if not outdated: @@ -797,7 +797,7 @@ def update( '--reverse', is_flag=True, default=False, help="Reversed dependency graph." ) def graph(bare=False, json=False, reverse=False): - from .import core + from . import core core.do_graph(bare=bare, json=json, reverse=reverse) @@ -817,7 +817,7 @@ def graph(bare=False, json=False, reverse=False): ) @click.argument('module', nargs=1) def run_open(module, three=None, python=None): - from .import core + from . import core # Ensure that virtualenv is available. core.ensure_project(three=three, python=python, validate=False) @@ -895,7 +895,7 @@ def sync( package_name=None, sequential=False, ): - from .import core + from . import core core.do_sync( ctx=ctx, @@ -952,7 +952,7 @@ def clean( user=False, verbose=False, ): - from .import core + from . import core core.do_clean( ctx=ctx, three=three, python=python, dry_run=dry_run, verbose=verbose diff --git a/pipenv/core.py b/pipenv/core.py index 9967eed0..1a0deca8 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -25,7 +25,8 @@ from blindspin import spinner from requests.packages import urllib3 from requests.packages.urllib3.exceptions import InsecureRequestWarning -from .project import Project +from.project import Project +from .requirements import PipenvRequirement from .utils import ( convert_deps_from_pip, convert_deps_to_pip, @@ -1399,9 +1400,9 @@ def pip_install( f.write(package_name) # Install dependencies when a package is a VCS dependency. try: - req = get_requirement( + req = PipenvRequirement.from_line( package_name.split('--hash')[0].split('--trusted-host')[0] - ).vcs + ).requirement.vcs except (pip9._vendor.pyparsing.ParseException, ValueError) as e: click.echo('{0}: {1}'.format(crayons.red('WARNING'), e), err=True) click.echo( @@ -2198,13 +2199,16 @@ def inline_activate_virtualenv(): def do_run_nt(command, args): """Run command by appending space-joined args to it!""" import subprocess + try: + from shlex import quote as shellquote + except ImportError: + from pipenv.vendor.backports.shlex import quote as shellquote command = project.scripts.get(command, command) + command = [command] + [shellquote(a) for a in args] # if you've passed something with crazy quoting... # ...just don't. (or put it in a script!) - p = subprocess.Popen( - ' '.join([command] + list(args)), shell=True, universal_newlines=True - ) + p = subprocess.Popen(command, shell=True, universal_newlines=True) p.communicate() sys.exit(p.returncode) @@ -2486,7 +2490,8 @@ def do_clean( ) installed_package_names = [] for installed in installed_packages: - r = get_requirement(installed) + pipenvreq = PipenvRequirement.from_line(installed) + r = pipenvreq.requirement # Ignore editable installations. if not r.editable: installed_package_names.append(r.name.lower()) diff --git a/pipenv/patched/pew/_utils.py b/pipenv/patched/pew/_utils.py index d1d8e0ac..c45c37e0 100644 --- a/pipenv/patched/pew/_utils.py +++ b/pipenv/patched/pew/_utils.py @@ -6,7 +6,10 @@ from contextlib import contextmanager from subprocess import check_call, Popen, PIPE from collections import namedtuple from functools import partial, wraps -from pathlib import Path +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path from tempfile import NamedTemporaryFile as _ntf try: from shutil import which diff --git a/pipenv/patched/pew/pew.py b/pipenv/patched/pew/pew.py index c5c3b2b5..2000ed7e 100644 --- a/pipenv/patched/pew/pew.py +++ b/pipenv/patched/pew/pew.py @@ -8,7 +8,10 @@ import random import textwrap from functools import partial from subprocess import CalledProcessError -from pathlib import Path +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path try: from shutil import get_terminal_size diff --git a/pipenv/requirements.py b/pipenv/requirements.py new file mode 100644 index 00000000..cf733f57 --- /dev/null +++ b/pipenv/requirements.py @@ -0,0 +1,609 @@ +# -*- coding=utf-8 -*- +from __future__ import absolute_import +import sys +from pipenv import PIPENV_VENDOR, PIPENV_PATCHED + +sys.path.insert(0, PIPENV_VENDOR) +sys.path.insert(0, PIPENV_PATCHED) +import hashlib +import os +import requirements +import six +from attr import attrs, attrib, Factory, validators +from collections import defaultdict +from pip9.index import Link +from pip9.download import path_to_url, url_to_path +from pip9.req.req_install import _strip_extras +from pip9._vendor.distlib.markers import Evaluator +from pipenv.utils import SCHEME_LIST, VCS_LIST, is_installable_file, is_vcs, multi_split, get_converted_relative_path, is_star, is_pinned, is_valid_url +from first import first + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + +HASH_STRING = ' --hash={0}' + + +def _validate_vcs(instance, attr, value): + if value not in VCS_LIST: + raise ValueError('Invalid vcs {0}'.format(value)) + +_optional_instance_of = lambda cls: validators.optional(validators.instance_of(cls)) + + +@attrs +class Source(object): + #: URL to PyPI instance + url = attrib(default='') + #: If False, skip SSL checks + verify_ssl = attrib(default=True, validator=validators.optional(validators.instance_of(bool))) + #: human name to refer to this source (can be referenced in packages or dev-packages) + name = attrib(default='') + + +@attrs +class Requires(object): + """System-level requirements - see PEP508 for more detail""" + os_name = attrib(default=None) + sys_platform = attrib(default=None) + platform_machine = attrib(default=None) + platform_python_implementation = attrib(default=None) + platform_release = attrib(default=None) + platform_system = attrib(default=None) + platform_version = attrib(default=None) + python_version = attrib(default=None) + python_full_version = attrib(default=None) + implementation_name = attrib(default=None) + implementation_version = attrib(default=None) + + +@attrs +class VCSRequirement(object): + #: vcs reference name (branch / commit / tag) + ref = attrib(default=None) + #: path to hit - without any of the VCS prefixes (like git+ / http+ / etc) + uri = attrib(default=None) + subdirectory = attrib(default=None) + vcs = attrib(validator=validators.optional(_validate_vcs), default=None) + + +@attrs +class PipfileRequirement(object): + path = attrib(default=None) + uri = attrib(default=None) + name = attrib(default=None) + extras = attrib(default=Factory(list)) + markers = attrib(default='') + editable = attrib(default=False) + vcs = attrib(validator=validators.optional(_validate_vcs), default=None) + version = attrib(default='') + index = attrib(default=None) + _hash = attrib(default=None) + hashes = attrib(default=Factory(list)) + ref = attrib(default=None) + subdirectory = attrib(default=None) + _link = attrib() + + @_link.default + def _init_link(self): + if not self.vcs: + return None + + uri = self.uri if self.uri else path_to_url(os.path.abspath(self.path)) + return build_vcs_link( + self.vcs, + uri, + name=self.name, + ref=self.ref, + subdirectory=self.subdirectory, + ) + + def __attrs_post_init__(self): + if self._hash and not self.hashes: + self.hashes = [self._hash] + if self.vcs and self.uri and self._link: + self.uri = _strip_ssh_from_git_uri(self._link.url) + + @classmethod + def create(cls, name, pipfile): + _pipfile = {} + if hasattr(pipfile, 'keys'): + _pipfile = dict(pipfile).copy() + _pipfile['name'] = name + _pipfile['version'] = cls._get_version(pipfile) + editable = _pipfile.pop( + 'editable' + ) if 'editable' in _pipfile else False + vcs_type = first([vcs for vcs in VCS_LIST if vcs in _pipfile]) + vcs = _pipfile.pop(vcs_type) if vcs_type else None + _pipfile_vcs_key = None + if vcs: + _pipfile_vcs_key = 'uri' if is_valid_url(vcs) else 'path' + _pipfile['editable'] = editable + _pipfile['vcs'] = vcs_type + if _pipfile_vcs_key and not _pipfile.get(_pipfile_vcs_key): + _pipfile[_pipfile_vcs_key] = vcs + markers = _pipfile.get('markers') + _extra_markers = [k for k in _pipfile.keys() if k in Evaluator.allowed_values.keys()] + if _extra_markers: + markers = list(markers) if markers else [] + for marker in _extra_markers: + markers.append(_pipfile.pop(marker)) + _pipfile['markers'] = ' and '.join(markers) + return cls(**_pipfile) + + @staticmethod + def _get_version(pipfile_entry): + if str(pipfile_entry) == '{}' or is_star(pipfile_entry): + return '' + + elif isinstance(pipfile_entry, six.string_types): + return pipfile_entry + + return pipfile_entry.get('version', '') + + @property + def pip_version(self): + if is_star(self.version): + return self.name + + return '{0}{1}'.format(self.name, self.version) + + @property + def requirement(self): + req_uri = self.uri + if self.editable and self.path and not self.uri: + req_uri = path_to_url(os.path.abspath(self.path)) + return PipenvRequirement._create_requirement( + name=self.pip_version, + path=self.path, + uri=req_uri, + markers=self.markers, + extras=self.extras, + index=self.index, + hashes=self.hashes, + vcs=self.vcs, + editable=self.editable, + link=self._link, + line=None, + ) + + +@attrs +class PipenvRequirement(object): + """Requirement for Pipenv Use + + Provides the following methods: + - as_pipfile + - as_lockfile + - as_requirement + - from_line + - from_pipfile + - resolve + """ + _editable_prefix = '-e ' + path = attrib(default=None) + uri = attrib(default=None) + name = attrib(default=None) + extras = attrib(default=Factory(list)) + markers = attrib(default='') + editable = attrib(default=False) + vcs = attrib(validator=validators.optional(_validate_vcs), default=None) + link = attrib(default=None) + line = attrib(default=None) + requirement = attrib(default=None) + index = attrib(default=Factory(list)) + specs = attrib(default='') + hashes = attrib(default=Factory(list)) + + @classmethod + def create(cls, req): + creation_attrs = {'requirement': req} + for prop in [ + 'name', + 'extras', + 'markers', + 'line', + 'link', + 'vcs', + 'editable', + 'uri', + 'path', + 'hashes', + 'index', + ]: + creation_attrs[prop] = getattr(req, prop, None) + return cls(**creation_attrs) + + @property + def original_line(self): + _editable = '' + if self.editable: + _editable += self._editable_prefix + if self.line and (self.path or self.uri): + # original_line adds in -e if necessary + if self.line.startswith(self._editable_prefix): + return self.line + + return '{0}{1}'.format(_editable, self.line) + + return self.constructed_line + + @property + def constructed_line(self): + _editable = '' + if self.editable: + _editable += self._editable_prefix + line = '' + if self.link: + line = '{0}{1}'.format(_editable, self.link.url) + elif self.path or self.uri: + line = '{0}{1}'.format(_editable, self.path or self.uri) + else: + line += self.name + if not self.vcs: + line = '{0}{1}{2}{3}{4}'.format( + line, + self.extras_as_pip, + self.specifiers_as_pip, + self.markers_as_pip, + self.hashes_as_pip, + ) + else: + line = self.line + if _editable == self._editable_prefix and not self.line.startswith( + _editable + ): + line = '{0}{1}'.format(_editable, self.line) + line = '{0}{1}{2}{3}'.format( + line, + self.extras_as_pip, + self.markers_as_pip, + self.hashes_as_pip, + ) + return line + + @property + def extras_as_pip(self): + if self.extras: + return '[{0}]'.format(','.join(self.extras)) + + return '' + + @property + def markers_as_pip(self): + if self.markers: + return '; {0}'.format(self.markers) + + return '' + + @property + def specifiers_as_pip(self): + if hasattr(self.requirement, 'specs'): + return ','.join([''.join(spec) for spec in self.requirement.specs]) + + return '' + + @property + def hashes_as_pip(self): + if self.hashes: + if isinstance(self.hashes, six.string_types): + return HASH_STRING.format(self.hashes) + + return ''.join([HASH_STRING.format(h) for h in self.hashes]) + + return '' + + @classmethod + def from_pipfile(cls, name, indexes, pipfile_entry): + pipfile = PipfileRequirement.create(name, pipfile_entry) + return cls.create(pipfile.requirement) + + @classmethod + def from_line(cls, line): + """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 + """ + hashes = None + if '--hash=' in line: + hashes = line.split(' --hash=') + line, hashes = hashes[0], hashes[1:] + editable = False + original_line = line + _editable = '' + if line.startswith('-e '): + editable = True + _editable += cls._editable_prefix + line = line.split(' ', 1)[1] + line, markers = cls._split_markers(line) + line, extras = _strip_extras(line) + req_dict = defaultdict(None) + vcs = None + if is_installable_file(line): + req_dict = cls._prep_path(line) + req_dict['original_line'] = '{0}{1}'.format( + _editable, req_dict['original_line'] + ) + elif is_vcs(line): + req_dict = cls._prep_vcs(line) + vcs = first( + _split_vcs_method( + req_dict.get('uri', req_dict.get('path', line)) + ) + ) + req_dict['original_line'] = '{0}{1}'.format( + _editable, req_dict['original_line'] + ) + else: + req_dict = { + 'line': line, + 'original_line': original_line, + 'name': multi_split(line, '!=<>~')[0], + } + return cls.create( + cls._create_requirement( + line=req_dict['original_line'], + name=req_dict.get('name'), + path=req_dict.get('path'), + uri=req_dict.get('uri'), + link=req_dict.get('link'), + hashes=hashes, + markers=markers, + extras=extras, + editable=editable, + vcs=vcs, + ) + ) + + def as_pipfile(self): + """"Converts a requirement to a Pipfile-formatted one.""" + req_dict = {} + req = self.requirement + req_dict = {} + if req.local_file: + hashable_path = req.uri or req.path + dict_key = 'file' if req.uri else 'path' + hashed_path = hashlib.sha256( + hashable_path.encode('utf-8') + ).hexdigest( + ) + req_dict[dict_key] = hashable_path + req_dict['name'] = hashed_path[ + len(hashed_path) - 7: + ] if not req.vcs else req.name + elif req.vcs: + 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=.'.format(req.uri) + ) + + if req.uri and req.uri.startswith('{0}+'.format(req.vcs)): + if req_dict.get('uri'): + # req_dict['uri'] = req.uri[len(req.vcs) + 1:] + del req_dict['uri'] + req_dict.update( + { + req.vcs: req.uri[ + len(req.vcs) + 1: + ] if req.uri else req.path + } + ) + if req.subdirectory: + req_dict.update({'subdirectory': req.subdirectory}) + if req.revision: + req_dict.update({'ref': req.revision}) + elif req.specs: + # Comparison operators: e.g. Django>1.10 + specs = ','.join([''.join(spec) for spec in req.specs]) + req_dict.update({'version': specs}) + else: + req_dict.update({'version': '*'}) + if self.extras: + req_dict.update({'extras': self.extras}) + if req.editable: + req_dict.update({'editable': req.editable}) + if self.hashes: + hash_key = 'hashes' + hashes = self.hashes + if isinstance(hashes, six.string_types) or len(hashes) == 1: + hash_key = 'hash' + if len(hashes) == 1: + hashes = first(hashes) + req_dict.update({hash_key: hashes}) + if len(req_dict.keys()) == 1 and req_dict.get('version'): + return {req.name: req_dict.get('version')} + + return {req.name: req_dict} + + def as_requirement(self, project=None, include_index=False): + """Creates a requirements.txt compatible output of the current dependency. + + :param project: Pipenv Project, defaults to None + :param project: :class:`pipenv.project.Project`, optional + :param include_index: Whether to include the resolved index, defaults to False + :param include_index: bool, optional + """ + line = self.constructed_line + if include_index and not (self.requirement.local_file or self.vcs): + from .utils import prepare_pip_source_args + + if self.index: + pip_src_args = [project.get_source(self.index)] + else: + pip_src_args = project.sources + index_string = ' '.join(prepare_pip_source_args(pip_src_args)) + line = '{0} {1}'.format(line, index_string) + return line + + @staticmethod + def _split_markers(line): + """Split markers from a dependency""" + if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST): + marker_sep = ';' + else: + marker_sep = '; ' + markers = None + if marker_sep in line: + line, markers = line.split(marker_sep, 1) + markers = markers.strip() if markers else None + return line, markers + + @staticmethod + def _prep_path(line): + _path = Path(line) + link = Link(_path.absolute().as_uri()) + if _path.is_absolute() or _path.as_posix() == '.': + path = _path.as_posix() + else: + path = get_converted_relative_path(line) + name_or_url = link.egg_fragment if link.egg_fragment else link.url_without_fragment + name = link.egg_fragment or link.show_url or link.filename + return { + 'link': link, + 'path': path, + 'line': name_or_url, + 'original_line': line, + 'name': name, + } + + @staticmethod + def _prep_vcs(line): + # Generate a Link object for parsing egg fragments + link = Link(line) + # Save the original path to store in the pipfile + original_uri = link.url + # Construct the requirement using proper git+ssh:// replaced uris or names if available + formatted_line = _clean_git_uri(line) + return { + 'link': link, + 'uri': formatted_line, + 'line': original_uri, + 'original_line': line, + 'name': link.egg_fragment, + } + + @classmethod + def _create_requirement( + cls, + line=None, + name=None, + path=None, + uri=None, + extras=None, + markers=None, + editable=False, + vcs=None, + link=None, + hashes=None, + index=None, + ): + _editable = cls._editable_prefix if editable else '' + _line = line or uri or path or name + # We don't want to only use the name on properly + # formatted VCS inputs + if vcs or is_vcs(_line): + _line = uri or path or line + _line = '{0}{1}'.format(_editable, _line) + elif link and path and not uri: + _line = link.url + req = first(requirements.parse(_line)) + req.line = _line + if editable: + req.editable = True + if req.name and not any( + getattr(req, prop) for prop in ['uri', 'path'] + ): + if link and link.scheme.startswith('file') and path: + req.path = path + req.local_file = True + elif link and uri: + req.uri = link.url_without_fragment + elif req.local_file and path and not req.vcs: + req.uri = None + req.path = path + elif req.vcs and not req.local_file and uri != link.url: + req.uri = _strip_ssh_from_git_uri(req.uri) + req.line = line or _strip_ssh_from_git_uri(req.line) + if markers: + req.markers = markers + if extras: + # Bizarrely this is also what pip does... + req.extras = first( + requirements.parse( + 'fakepkg{0}'.format(_extras_to_string(extras)) + ) + ).extras + req.link = link + if hashes: + req.hashes = hashes + if index: + req.index = index + return req + + +def _strip_ssh_from_git_uri(uri): + """Return git+ssh:// formatted URI to git+git@ format""" + if isinstance(uri, six.string_types): + uri = uri.replace('git+ssh://', 'git+') + return uri + + +def _clean_git_uri(uri): + """Cleans VCS uris from pip9 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 _split_vcs_method(uri): + """Split a vcs+uri formatted uri into (vcs, uri)""" + vcs_start = '{0}+' + vcs = first( + [vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))] + ) + if vcs: + vcs, uri = uri.split('+', 1) + return vcs, uri + + +def _extras_to_string(extras): + """Turn a list of extras into a string""" + if isinstance(extras, six.string_types): + if extras.startswith('['): + return extras + + else: + extras = [extras] + return '[{0}]'.format(','.join(extras)) + + +def build_vcs_link( + vcs, uri, name=None, ref=None, subdirectory=None, extras= [] +): + vcs_start = '{0}+'.format(vcs) + if not uri.startswith(vcs_start): + uri = '{0}{1}'.format(vcs_start, uri) + uri = _clean_git_uri(uri) + if ref: + uri = '{0}@{1}'.format(uri, ref) + if name: + uri = '{0}#egg={1}'.format(uri, name) + if extras: + extras = _extras_to_string(extras) + uri = '{0}{1}'.format(uri, extras) + if subdirectory: + uri = '{0}&subdirectory={1}'.format(uri, subdirectory) + return Link(uri) diff --git a/pipenv/utils.py b/pipenv/utils.py index 7d60e440..3ab9b945 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -65,8 +65,6 @@ requests = requests.Session() def get_requirement(dep): - from pip9.req.req_install import _strip_extras - import requirements """Pre-clean requirement strings passed to the requirements parser. @@ -78,79 +76,8 @@ def get_requirement(dep): :param str dep: A requirement line :returns: :class:`requirements.Requirement` object """ - path = None - uri = None - cleaned_uri = None - editable = False - dep_link = None - # check for editable dep / vcs dep - if 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 pip9.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 = _strip_extras(dep) - # Only operate on local, existing, non-URI formatted paths which are installable - if 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 - cleaned_uri = clean_git_uri(dep) - dep = cleaned_uri - 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 everything 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 cleaned_uri and cleaned_uri != uri: - req.uri = strip_ssh_from_git_uri(req.uri) - req.line = strip_ssh_from_git_uri(req.line) - req.editable = editable - 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 + from .requirements import PipenvRequirement + return PipenvRequirement.from_line(dep).requirement def cleanup_toml(tml): @@ -359,7 +286,7 @@ def actually_resolve_reps( def venv_resolve_deps( deps, which, project, pre=False, verbose=False, clear=False ): - from .import resolver + from . import resolver import json resolver = escape_grouped_arguments(resolver.__file__.rstrip('co')) @@ -514,73 +441,10 @@ def multi_split(s, split): def convert_deps_from_pip(dep): """"Converts a pip-formatted dependency to a Pipfile-formatted one.""" + from .requirements import PipenvRequirement dependency = {} - req = get_requirement(dep) - extras = {'extras': req.extras} - # File installs. - if (req.uri or req.path or is_installable_file(req.name)) 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. - elif req.vcs: - 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=.'.format(req.uri) - ) - - # 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 + req = PipenvRequirement.from_line(dep) + return req.as_pipfile() def is_star(val): @@ -593,95 +457,16 @@ def is_pinned(val): def convert_deps_to_pip(deps, project=None, r=True, include_index=False): """"Converts a Pipfile-formatted dependency to a pip-formatted one.""" + from .requirements import PipenvRequirement 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 is_star(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(','.join(deps[dep]['extras'])) - if 'version' in deps[dep]: - if not is_star(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 is_star(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 and not is_file(deps[dep]) and not is_vcs(deps[dep]): - pip_src_args = [] - if 'index' in deps[dep]: - pip_src_args = [project.get_source(deps[dep]['index'])] - else: - pip_src_args = project.sources - pip_args = prepare_pip_source_args(pip_src_args) - 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) + for dep_name, dep in deps.items(): + indexes = project.sources if hasattr(project, 'sources') else None + if hasattr(dep, 'keys') and dep.get('index'): + indexes = project.get_source(dep['index']) + new_dep = PipenvRequirement.from_pipfile(dep_name, indexes, dep) + req = new_dep.as_requirement(project=project, include_index=include_index) + req = req.strip() + dependencies.append(req) if not r: return dependencies @@ -754,7 +539,7 @@ def is_editable(pipfile_entry): def is_vcs(pipfile_entry): - import requirements + from pipenv.vendor import requirements """Determine if dictionary entry from Pipfile is for a vcs dependency.""" if hasattr(pipfile_entry, 'keys'): @@ -1019,6 +804,10 @@ def find_windows_executable(bin_path, exe_name): return find_executable(exe_name) +def path_to_url(path): + return Path(normalize_drive(os.path.abspath(path))).as_uri() + + 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)) diff --git a/pipenv/vendor/attr/__init__.py b/pipenv/vendor/attr/__init__.py new file mode 100644 index 00000000..ea330007 --- /dev/null +++ b/pipenv/vendor/attr/__init__.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, division, print_function + +from functools import partial + +from . import converters, exceptions, filters, validators +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, evolve, has +from ._make import ( + NOTHING, Attribute, Factory, attrib, attrs, fields, fields_dict, + make_class, validate +) + + +__version__ = "18.1.0.dev0" + +__title__ = "attrs" +__description__ = "Classes Without Boilerplate" +__uri__ = "http://www.attrs.org/" +__doc__ = __description__ + " <" + __uri__ + ">" + +__author__ = "Hynek Schlawack" +__email__ = "hs@ox.cx" + +__license__ = "MIT" +__copyright__ = "Copyright (c) 2015 Hynek Schlawack" + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +__all__ = [ + "Attribute", + "Factory", + "NOTHING", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "converters", + "evolve", + "exceptions", + "fields", + "fields_dict", + "filters", + "get_run_validators", + "has", + "ib", + "make_class", + "s", + "set_run_validators", + "validate", + "validators", +] diff --git a/pipenv/vendor/attr/_compat.py b/pipenv/vendor/attr/_compat.py new file mode 100644 index 00000000..42a91ee5 --- /dev/null +++ b/pipenv/vendor/attr/_compat.py @@ -0,0 +1,146 @@ +from __future__ import absolute_import, division, print_function + +import platform +import sys +import types +import warnings + + +PY2 = sys.version_info[0] == 2 +PYPY = platform.python_implementation() == "PyPy" + + +if PYPY or sys.version_info[:2] >= (3, 6): + ordered_dict = dict +else: + from collections import OrderedDict + ordered_dict = OrderedDict + + +if PY2: + from UserDict import IterableUserDict + + # We 'bundle' isclass instead of using inspect as importing inspect is + # fairly expensive (order of 10-15 ms for a modern machine in 2016) + def isclass(klass): + return isinstance(klass, (type, types.ClassType)) + + # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. + TYPE = "type" + + def iteritems(d): + return d.iteritems() + + # Python 2 is bereft of a read-only dict proxy, so we make one! + class ReadOnlyDict(IterableUserDict): + """ + Best-effort read-only dict wrapper. + """ + + def __setitem__(self, key, val): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError("'mappingproxy' object does not support item " + "assignment") + + def update(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError("'mappingproxy' object has no attribute " + "'update'") + + def __delitem__(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError("'mappingproxy' object does not support item " + "deletion") + + def clear(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError("'mappingproxy' object has no attribute " + "'clear'") + + def pop(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError("'mappingproxy' object has no attribute " + "'pop'") + + def popitem(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError("'mappingproxy' object has no attribute " + "'popitem'") + + def setdefault(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError("'mappingproxy' object has no attribute " + "'setdefault'") + + def __repr__(self): + # Override to be identical to the Python 3 version. + return "mappingproxy(" + repr(self.data) + ")" + + def metadata_proxy(d): + res = ReadOnlyDict() + res.data.update(d) # We blocked update, so we have to do it like this. + return res + +else: + def isclass(klass): + return isinstance(klass, type) + + TYPE = "class" + + def iteritems(d): + return d.items() + + def metadata_proxy(d): + return types.MappingProxyType(dict(d)) + + +def import_ctypes(): + """ + Moved into a function for testability. + """ + import ctypes + return ctypes + + +if not PY2: + def just_warn(*args, **kw): + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + warnings.warn( + "Missing ctypes. Some features like bare super() or accessing " + "__class__ will not work with slots classes.", + RuntimeWarning, + stacklevel=2, + ) +else: + def just_warn(*args, **kw): # pragma: nocover + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + + +def make_set_closure_cell(): + """ + Moved into a function for testability. + """ + if PYPY: # pragma: no cover + def set_closure_cell(cell, value): + cell.__setstate__((value,)) + else: + try: + ctypes = import_ctypes() + + set_closure_cell = ctypes.pythonapi.PyCell_Set + set_closure_cell.argtypes = (ctypes.py_object, ctypes.py_object) + set_closure_cell.restype = ctypes.c_int + except Exception: + # We try best effort to set the cell, but sometimes it's not + # possible. For example on Jython or on GAE. + set_closure_cell = just_warn + return set_closure_cell + + +set_closure_cell = make_set_closure_cell() diff --git a/pipenv/vendor/attr/_config.py b/pipenv/vendor/attr/_config.py new file mode 100644 index 00000000..8ec92096 --- /dev/null +++ b/pipenv/vendor/attr/_config.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division, print_function + + +__all__ = ["set_run_validators", "get_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + """ + if not isinstance(run, bool): + raise TypeError("'run' must be bool.") + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + """ + return _run_validators diff --git a/pipenv/vendor/attr/_funcs.py b/pipenv/vendor/attr/_funcs.py new file mode 100644 index 00000000..798043af --- /dev/null +++ b/pipenv/vendor/attr/_funcs.py @@ -0,0 +1,212 @@ +from __future__ import absolute_import, division, print_function + +import copy + +from ._compat import iteritems +from ._make import NOTHING, _obj_setattr, fields +from .exceptions import AttrsAttributeNotFoundError + + +def asdict(inst, recurse=True, filter=None, dict_factory=dict, + retain_collection_types=False): + """ + Return the ``attrs`` attribute values of *inst* as a dict. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the :class:`attr.Attribute` as the first argument and the + value as the second argument. + :param callable dict_factory: A callable to produce dictionaries from. For + example, to produce ordered dictionaries instead of normal Python + dictionaries, pass in ``collections.OrderedDict``. + :param bool retain_collection_types: Do not convert to ``list`` when + encountering an attribute whose type is ``tuple`` or ``set``. Only + meaningful if ``recurse`` is ``True``. + + :rtype: return type of *dict_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + if recurse is True: + if has(v.__class__): + rv[a.name] = asdict(v, recurse=True, filter=filter, + dict_factory=dict_factory) + elif isinstance(v, (tuple, list, set)): + cf = v.__class__ if retain_collection_types is True else list + rv[a.name] = cf([ + asdict(i, recurse=True, filter=filter, + dict_factory=dict_factory) + if has(i.__class__) else i + for i in v + ]) + elif isinstance(v, dict): + df = dict_factory + rv[a.name] = df(( + asdict(kk, dict_factory=df) if has(kk.__class__) else kk, + asdict(vv, dict_factory=df) if has(vv.__class__) else vv) + for kk, vv in iteritems(v)) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def astuple(inst, recurse=True, filter=None, tuple_factory=tuple, + retain_collection_types=False): + """ + Return the ``attrs`` attribute values of *inst* as a tuple. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the :class:`attr.Attribute` as the first argument and the + value as the second argument. + :param callable tuple_factory: A callable to produce tuples from. For + example, to produce lists instead of tuples. + :param bool retain_collection_types: Do not convert to ``list`` + or ``dict`` when encountering an attribute which type is + ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is + ``True``. + + :rtype: return type of *tuple_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + if recurse is True: + if has(v.__class__): + rv.append(astuple(v, recurse=True, filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain)) + elif isinstance(v, (tuple, list, set)): + cf = v.__class__ if retain is True else list + rv.append(cf([ + astuple(j, recurse=True, filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain) + if has(j.__class__) else j + for j in v + ])) + elif isinstance(v, dict): + df = v.__class__ if retain is True else dict + rv.append(df( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain + ) if has(kk.__class__) else kk, + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain + ) if has(vv.__class__) else vv + ) + for kk, vv in iteritems(v))) + else: + rv.append(v) + else: + rv.append(v) + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with ``attrs`` attributes. + + :param type cls: Class to introspect. + :raise TypeError: If *cls* is not a class. + + :rtype: :class:`bool` + """ + return getattr(cls, "__attrs_attrs__", None) is not None + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't + be found on *cls*. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. deprecated:: 17.1.0 + Use :func:`evolve` instead. + """ + import warnings + warnings.warn("assoc is deprecated and will be removed after 2018/01.", + DeprecationWarning, stacklevel=2) + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in iteritems(changes): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + raise AttrsAttributeNotFoundError( + "{k} is not an attrs attribute on {cl}." + .format(k=k, cl=new.__class__) + ) + _obj_setattr(new, k, v) + return new + + +def evolve(inst, **changes): + """ + Create a new instance, based on *inst* with *changes* applied. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise TypeError: If *attr_name* couldn't be found in the class + ``__init__``. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 17.1.0 + """ + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + return cls(**changes) diff --git a/pipenv/vendor/attr/_make.py b/pipenv/vendor/attr/_make.py new file mode 100644 index 00000000..ebe12078 --- /dev/null +++ b/pipenv/vendor/attr/_make.py @@ -0,0 +1,1680 @@ +from __future__ import absolute_import, division, print_function + +import hashlib +import linecache +import sys +import threading +import warnings + +from operator import itemgetter + +from . import _config +from ._compat import ( + PY2, isclass, iteritems, metadata_proxy, ordered_dict, set_closure_cell +) +from .exceptions import ( + DefaultAlreadySetError, FrozenInstanceError, NotAnAttrsClassError, + UnannotatedAttributeError +) + + +# This is used at least twice, so cache it here. +_obj_setattr = object.__setattr__ +_init_converter_pat = "__attr_converter_{}" +_init_factory_pat = "__attr_factory_{}" +_tuple_property_pat = " {attr_name} = property(itemgetter({index}))" +_empty_metadata_singleton = metadata_proxy({}) + + +class _Nothing(object): + """ + Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + + All instances of `_Nothing` are equal. + """ + def __copy__(self): + return self + + def __deepcopy__(self, _): + return self + + def __eq__(self, other): + return other.__class__ == _Nothing + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return "NOTHING" + + def __hash__(self): + return 0xc0ffee + + +NOTHING = _Nothing() +""" +Sentinel to indicate the lack of a value when ``None`` is ambiguous. +""" + + +def attrib(default=NOTHING, validator=None, + repr=True, cmp=True, hash=None, init=True, + convert=None, metadata=None, type=None, converter=None, + factory=None): + """ + Create a new attribute on a class. + + .. warning:: + + Does *not* do anything unless the class is also decorated with + :func:`attr.s`! + + :param default: A value that is used if an ``attrs``-generated ``__init__`` + is used and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of :class:`Factory`, its callable will be + used to construct a new value (useful for mutable data types like lists + or dicts). + + If a default is not set (or set manually to ``attr.NOTHING``), a value + *must* be supplied when instantiating; otherwise a :exc:`TypeError` + will be raised. + + The default can also be set using decorator notation as shown below. + + :type default: Any value. + + :param callable factory: Syntactic sugar for + ``default=attr.Factory(callable)``. + + :param validator: :func:`callable` that is called by ``attrs``-generated + ``__init__`` methods after the instance has been initialized. They + receive the initialized instance, the :class:`Attribute`, and the + passed value. + + The return value is *not* inspected so the validator has to throw an + exception itself. + + If a ``list`` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + :func:`get_run_validators`. + + The validator can also be set using decorator notation as shown below. + + :type validator: ``callable`` or a ``list`` of ``callable``\ s. + + :param bool repr: Include this attribute in the generated ``__repr__`` + method. + :param bool cmp: Include this attribute in the generated comparison methods + (``__eq__`` et al). + :param hash: Include this attribute in the generated ``__hash__`` + method. If ``None`` (default), mirror *cmp*'s value. This is the + correct behavior according the Python spec. Setting this value to + anything else than ``None`` is *discouraged*. + :type hash: ``bool`` or ``None`` + :param bool init: Include this attribute in the generated ``__init__`` + method. It is possible to set this to ``False`` and set a default + value. In that case this attributed is unconditionally initialized + with the specified default value or factory. + :param callable converter: :func:`callable` that is called by + ``attrs``-generated ``__init__`` methods to converter attribute's value + to the desired format. It is given the passed-in value, and the + returned value will be used as the new value of the attribute. The + value is converted before being passed to the validator, if any. + :param metadata: An arbitrary mapping, to be used by third-party + components. See :ref:`extending_metadata`. + :param type: The type of the attribute. In Python 3.6 or greater, the + preferred method to specify the type is using a variable annotation + (see `PEP 526 `_). + This argument is provided for backward compatibility. + Regardless of the approach used, the type will be stored on + ``Attribute.type``. + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is ``None`` and therefore mirrors *cmp* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated + *convert* to achieve consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + """ + if hash is not None and hash is not True and hash is not False: + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + + if convert is not None: + if converter is not None: + raise RuntimeError( + "Can't pass both `convert` and `converter`. " + "Please use `converter` only." + ) + warnings.warn( + "The `convert` argument is deprecated in favor of `converter`. " + "It will be removed after 2019/01.", + DeprecationWarning, stacklevel=2 + ) + converter = convert + + if factory is not None: + if default is not NOTHING: + raise ValueError( + "The `default` and `factory` arguments are mutually " + "exclusive." + ) + if not callable(factory): + raise ValueError( + "The `factory` argument must be a callable." + ) + default = Factory(factory) + + if metadata is None: + metadata = {} + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=cmp, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + ) + + +def _make_attr_tuple_class(cls_name, attr_names): + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = "{}Attributes".format(cls_name) + attr_class_template = [ + "class {}(tuple):".format(attr_class_name), + " __slots__ = ()", + ] + if attr_names: + for i, attr_name in enumerate(attr_names): + attr_class_template.append(_tuple_property_pat.format( + index=i, + attr_name=attr_name, + )) + else: + attr_class_template.append(" pass") + globs = {"itemgetter": itemgetter} + eval(compile("\n".join(attr_class_template), "", "exec"), globs) + return globs[attr_class_name] + + +# Tuple class for extracted attributes from a class definition. +# `super_attrs` is a subset of `attrs`. +_Attributes = _make_attr_tuple_class("_Attributes", [ + "attrs", # all attributes to build dunder methods for + "super_attrs", # attributes that have been inherited + "super_attrs_map", # map inherited attributes to their originating classes +]) + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The implementation is gross but importing `typing` is slow and there are + discussions to remove it from the stdlib alltogether. + """ + return str(annot).startswith("typing.ClassVar") + + +def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + anns = getattr(cls, "__annotations__", None) + if anns is None: + return {} + + # Verify that the annotations aren't merely inherited. + for super_cls in cls.__mro__[1:]: + if anns is getattr(super_cls, "__annotations__", None): + return {} + + return anns + + +def _counter_getter(e): + """ + Key function for sorting to avoid re-creating a lambda for every class. + """ + return e[1].counter + + +def _transform_attrs(cls, these, auto_attribs): + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = [ + (name, ca) + for name, ca + in iteritems(these) + ] + + if not isinstance(these, ordered_dict): + ca_list.sort(key=_counter_getter) + elif auto_attribs is True: + ca_names = { + name + for name, attr + in cd.items() + if isinstance(attr, _CountingAttr) + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + if not isinstance(a, _CountingAttr): + if a is NOTHING: + a = attrib() + else: + a = attrib(default=a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if len(unannotated) > 0: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join(sorted( + unannotated, + key=lambda n: cd.get(n).counter + )) + "." + ) + else: + ca_list = sorted(( + (name, attr) + for name, attr + in cd.items() + if isinstance(attr, _CountingAttr) + ), key=lambda e: e[1].counter) + + own_attrs = [ + Attribute.from_counting_attr( + name=attr_name, + ca=ca, + type=anns.get(attr_name), + ) + for attr_name, ca + in ca_list + ] + + super_attrs = [] + super_attr_map = {} # A dictionary of superattrs to their classes. + taken_attr_names = {a.name: a for a in own_attrs} + + # Traverse the MRO and collect attributes. + for super_cls in cls.__mro__[1:-1]: + sub_attrs = getattr(super_cls, "__attrs_attrs__", None) + if sub_attrs is not None: + for a in sub_attrs: + prev_a = taken_attr_names.get(a.name) + # Only add an attribute if it hasn't been defined before. This + # allows for overwriting attribute definitions by subclassing. + if prev_a is None: + super_attrs.append(a) + taken_attr_names[a.name] = a + super_attr_map[a.name] = super_cls + + attr_names = [a.name for a in super_attrs + own_attrs] + + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + attrs = AttrsClass( + super_attrs + [ + Attribute.from_counting_attr( + name=attr_name, + ca=ca, + type=anns.get(attr_name) + ) + for attr_name, ca + in ca_list + ] + ) + + had_default = False + for a in attrs: + if had_default is True and a.default is NOTHING and a.init is True: + raise ValueError( + "No mandatory attributes allowed after an attribute with a " + "default value or factory. Attribute in question: {a!r}" + .format(a=a) + ) + elif had_default is False and \ + a.default is not NOTHING and \ + a.init is not False: + had_default = True + + return _Attributes((attrs, super_attrs, super_attr_map)) + + +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + raise FrozenInstanceError() + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + raise FrozenInstanceError() + + +class _ClassBuilder(object): + """ + Iteratively build *one* class. + """ + __slots__ = ( + "_cls", "_cls_dict", "_attrs", "_super_names", "_attr_names", "_slots", + "_frozen", "_has_post_init", "_delete_attribs", "_super_attr_map", + ) + + def __init__(self, cls, these, slots, frozen, auto_attribs): + attrs, super_attrs, super_map = _transform_attrs( + cls, these, auto_attribs + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if slots else {} + self._attrs = attrs + self._super_names = set(a.name for a in super_attrs) + self._super_attr_map = super_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = slots + self._frozen = frozen or _has_frozen_superclass(cls) + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + + self._cls_dict["__attrs_attrs__"] = self._attrs + + if frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + def __repr__(self): + return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used anymore after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + else: + return self._patch_original_class() + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + super_names = self._super_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if name not in super_names and \ + getattr(cls, name, None) is not None: + delattr(cls, name) + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + super_names = self._super_names + cd = { + k: v + for k, v in iteritems(self._cls_dict) + if k not in tuple(self._attr_names) + ("__dict__",) + } + + # We only add the names of attributes that aren't inherited. + # Settings __slots__ to inherited attributes wastes memory. + cd["__slots__"] = tuple( + name + for name in self._attr_names + if name not in super_names + ) + + qualname = getattr(self._cls, "__qualname__", None) + if qualname is not None: + cd["__qualname__"] = qualname + + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return tuple( + getattr(self, name) + for name in state_attr_names + ) + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + + # slots and frozen require __getstate__/__setstate__ to work + cd["__getstate__"] = slots_getstate + cd["__setstate__"] = slots_setstate + + # Create new class based on old class and our methods. + cls = type(self._cls)( + self._cls.__name__, + self._cls.__bases__, + cd, + ) + + # The following is a fix for + # https://github.com/python-attrs/attrs/issues/102. On Python 3, + # if a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in cls.__dict__.values(): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + if cell.cell_contents is self._cls: + set_closure_cell(cell, cls) + + return cls + + def add_repr(self, ns): + self._cls_dict["__repr__"] = self._add_method_dunders( + _make_repr(self._attrs, ns=ns) + ) + return self + + def add_str(self): + repr = self._cls_dict.get("__repr__") + if repr is None: + raise ValueError( + "__str__ can only be generated if a __repr__ exists." + ) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + self._cls_dict["__hash__"] = self._add_method_dunders( + _make_hash(self._attrs) + ) + + return self + + def add_init(self): + self._cls_dict["__init__"] = self._add_method_dunders( + _make_init( + self._attrs, + self._has_post_init, + self._frozen, + self._slots, + self._super_attr_map, + ) + ) + + return self + + def add_cmp(self): + cd = self._cls_dict + + cd["__eq__"], cd["__ne__"], cd["__lt__"], cd["__le__"], cd["__gt__"], \ + cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_cmp(self._attrs) + ) + + return self + + def _add_method_dunders(self, method): + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + try: + method.__module__ = self._cls.__module__ + except AttributeError: + pass + + try: + method.__qualname__ = ".".join( + (self._cls.__qualname__, method.__name__,) + ) + except AttributeError: + pass + + return method + + +def attrs(maybe_cls=None, these=None, repr_ns=None, + repr=True, cmp=True, hash=None, init=True, + slots=False, frozen=False, str=False, auto_attribs=False): + r""" + A class decorator that adds `dunder + `_\ -methods according to the + specified attributes using :func:`attr.ib` or the *these* argument. + + :param these: A dictionary of name to :func:`attr.ib` mappings. This is + useful to avoid the definition of your attributes within the class body + because you can't (e.g. if you want to add ``__repr__`` methods to + Django models) or don't want to. + + If *these* is not ``None``, ``attrs`` will *not* search the class body + for attributes and will *not* remove any attributes from it. + + If *these* is an ordered dict (:class:`dict` on Python 3.6+, + :class:`collections.OrderedDict` otherwise), the order is deduced from + the order of the attributes inside *these*. Otherwise the order + of the definition of the attributes is used. + + :type these: :class:`dict` of :class:`str` to :func:`attr.ib` + + :param str repr_ns: When using nested classes, there's no way in Python 2 + to automatically detect that. Therefore it's possible to set the + namespace explicitly for a more meaningful ``repr`` output. + :param bool repr: Create a ``__repr__`` method with a human readable + representation of ``attrs`` attributes.. + :param bool str: Create a ``__str__`` method that is identical to + ``__repr__``. This is usually not necessary except for + :class:`Exception`\ s. + :param bool cmp: Create ``__eq__``, ``__ne__``, ``__lt__``, ``__le__``, + ``__gt__``, and ``__ge__`` methods that compare the class as if it were + a tuple of its ``attrs`` attributes. But the attributes are *only* + compared, if the type of both classes is *identical*! + :param hash: If ``None`` (default), the ``__hash__`` method is generated + according how *cmp* and *frozen* are set. + + 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + 2. If *cmp* is True and *frozen* is False, ``__hash__`` will be set to + None, marking it unhashable (which it is). + 3. If *cmp* is False, ``__hash__`` will be left untouched meaning the + ``__hash__`` method of the superclass will be used (if superclass is + ``object``, this means it will fall back to id-based hashing.). + + Although not recommended, you can decide for yourself and force + ``attrs`` to create one (e.g. if the class is immutable even though you + didn't freeze it programmatically) by passing ``True`` or not. Both of + these cases are rather special and should be used carefully. + + See the `Python documentation \ + `_ + and the `GitHub issue that led to the default behavior \ + `_ for more details. + :type hash: ``bool`` or ``None`` + :param bool init: Create a ``__init__`` method that initializes the + ``attrs`` attributes. Leading underscores are stripped for the + argument name. If a ``__attrs_post_init__`` method exists on the + class, it will be called after the class is fully initialized. + :param bool slots: Create a slots_-style class that's more + memory-efficient. See :ref:`slots` for further ramifications. + :param bool frozen: Make instances immutable after initialization. If + someone attempts to modify a frozen instance, + :exc:`attr.exceptions.FrozenInstanceError` is raised. + + Please note: + + 1. This is achieved by installing a custom ``__setattr__`` method + on your class so you can't implement an own one. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance :ref:`impact + ` when initializing new instances. In other words: + ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You can + circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + .. _slots: https://docs.python.org/3/reference/datamodel.html#slots + :param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes + (Python 3.6 and later only) from the class body. + + In this case, you **must** annotate every field. If ``attrs`` + encounters a field that is set to an :func:`attr.ib` but lacks a type + annotation, an :exc:`attr.exceptions.UnannotatedAttributeError` is + raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't + want to set a type. + + If you assign a value to those attributes (e.g. ``x: int = 42``), that + value becomes the default value like if it were passed using + ``attr.ib(default=42)``. Passing an instance of :class:`Factory` also + works as expected. + + Attributes annotated as :data:`typing.ClassVar` are **ignored**. + + .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports ``None`` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + """ + def wrap(cls): + if getattr(cls, "__class__", None) is None: + raise TypeError("attrs only works with new-style classes.") + + builder = _ClassBuilder(cls, these, slots, frozen, auto_attribs) + + if repr is True: + builder.add_repr(repr_ns) + if str is True: + builder.add_str() + if cmp is True: + builder.add_cmp() + + if hash is not True and hash is not False and hash is not None: + # Can't use `hash in` because 1 == True for example. + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + elif hash is False or (hash is None and cmp is False): + pass + elif hash is True or (hash is None and cmp is True and frozen is True): + builder.add_hash() + else: + builder.make_unhashable() + + if init is True: + builder.add_init() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +if PY2: + def _has_frozen_superclass(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return ( + getattr( + cls.__setattr__, "__module__", None + ) == _frozen_setattrs.__module__ and + cls.__setattr__.__name__ == _frozen_setattrs.__name__ + ) +else: + def _has_frozen_superclass(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ == _frozen_setattrs + + +def _attrs_to_tuple(obj, attrs): + """ + Create a tuple of all values of *obj*'s *attrs*. + """ + return tuple(getattr(obj, a.name) for a in attrs) + + +def _make_hash(attrs): + attrs = tuple( + a + for a in attrs + if a.hash is True or (a.hash is None and a.cmp is True) + ) + + # We cache the generated hash methods for the same kinds of attributes. + sha1 = hashlib.sha1() + sha1.update(repr(attrs).encode("utf-8")) + unique_filename = "" % (sha1.hexdigest(),) + type_hash = hash(unique_filename) + lines = [ + "def __hash__(self):", + " return hash((", + " %d," % (type_hash,), + ] + for a in attrs: + lines.append(" self.%s," % (a.name)) + + lines.append(" ))") + + script = "\n".join(lines) + globs = {} + locs = {} + bytecode = compile(script, unique_filename, "exec") + eval(bytecode, globs, locs) + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + linecache.cache[unique_filename] = ( + len(script), + None, + script.splitlines(True), + unique_filename, + ) + + return locs["__hash__"] + + +def _add_hash(cls, attrs): + """ + Add a hash method to *cls*. + """ + cls.__hash__ = _make_hash(attrs) + return cls + + +def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or return the result + negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + +def _make_cmp(attrs): + attrs = [a for a in attrs if a.cmp] + + # We cache the generated eq methods for the same kinds of attributes. + sha1 = hashlib.sha1() + sha1.update(repr(attrs).encode("utf-8")) + unique_filename = "" % (sha1.hexdigest(),) + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + # We can't just do a big self.x = other.x and... clause due to + # irregularities like nan == nan is false but (nan,) == (nan,) is true. + if attrs: + lines.append(" return (") + others = [ + " ) == (", + ] + for a in attrs: + lines.append(" self.%s," % (a.name,)) + others.append(" other.%s," % (a.name,)) + + lines += others + [" )"] + else: + lines.append(" return True") + + script = "\n".join(lines) + globs = {} + locs = {} + bytecode = compile(script, unique_filename, "exec") + eval(bytecode, globs, locs) + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + linecache.cache[unique_filename] = ( + len(script), + None, + script.splitlines(True), + unique_filename, + ) + eq = locs["__eq__"] + ne = __ne__ + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return _attrs_to_tuple(obj, attrs) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if isinstance(other, self.__class__): + return attrs_to_tuple(self) < attrs_to_tuple(other) + else: + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if isinstance(other, self.__class__): + return attrs_to_tuple(self) <= attrs_to_tuple(other) + else: + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if isinstance(other, self.__class__): + return attrs_to_tuple(self) > attrs_to_tuple(other) + else: + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if isinstance(other, self.__class__): + return attrs_to_tuple(self) >= attrs_to_tuple(other) + else: + return NotImplemented + + return eq, ne, __lt__, __le__, __gt__, __ge__ + + +def _add_cmp(cls, attrs=None): + """ + Add comparison methods to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__eq__, cls.__ne__, cls.__lt__, cls.__le__, cls.__gt__, cls.__ge__ = \ + _make_cmp(attrs) + + return cls + + +_already_repring = threading.local() + + +def _make_repr(attrs, ns): + """ + Make a repr method for *attr_names* adding *ns* to the full name. + """ + attr_names = tuple( + a.name + for a in attrs + if a.repr + ) + + def __repr__(self): + """ + Automatically created by attrs. + """ + try: + working_set = _already_repring.working_set + except AttributeError: + working_set = set() + _already_repring.working_set = working_set + + if id(self) in working_set: + return "..." + real_cls = self.__class__ + if ns is None: + qualname = getattr(real_cls, "__qualname__", None) + if qualname is not None: + class_name = qualname.rsplit(">.", 1)[-1] + else: + class_name = real_cls.__name__ + else: + class_name = ns + "." + real_cls.__name__ + + # Since 'self' remains on the stack (i.e.: strongly referenced) for the + # duration of this call, it's safe to depend on id(...) stability, and + # not need to track the instance and therefore worry about properties + # like weakref- or hash-ability. + working_set.add(id(self)) + try: + result = [class_name, "("] + first = True + for name in attr_names: + if first: + first = False + else: + result.append(", ") + result.extend((name, "=", repr(getattr(self, name, NOTHING)))) + return "".join(result) + ")" + finally: + working_set.remove(id(self)) + return __repr__ + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__repr__ = _make_repr(attrs, ns) + return cls + + +def _make_init(attrs, post_init, frozen, slots, super_attr_map): + attrs = [ + a + for a in attrs + if a.init or a.default is not NOTHING + ] + + # We cache the generated init methods for the same kinds of attributes. + sha1 = hashlib.sha1() + sha1.update(repr(attrs).encode("utf-8")) + unique_filename = "".format( + sha1.hexdigest() + ) + + script, globs = _attrs_to_init_script( + attrs, + frozen, + slots, + post_init, + super_attr_map, + ) + locs = {} + bytecode = compile(script, unique_filename, "exec") + attr_dict = dict((a.name, a) for a in attrs) + globs.update({ + "NOTHING": NOTHING, + "attr_dict": attr_dict, + }) + if frozen is True: + # Save the lookup overhead in __init__ if we need to circumvent + # immutability. + globs["_cached_setattr"] = _obj_setattr + eval(bytecode, globs, locs) + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + linecache.cache[unique_filename] = ( + len(script), + None, + script.splitlines(True), + unique_filename, + ) + + return locs["__init__"] + + +def _add_init(cls, frozen): + """ + Add a __init__ method to *cls*. If *frozen* is True, make it immutable. + """ + cls.__init__ = _make_init( + cls.__attrs_attrs__, + getattr(cls, "__attrs_post_init__", False), + frozen, + _is_slot_cls(cls), + {}, + ) + return cls + + +def fields(cls): + """ + Return the tuple of ``attrs`` attributes for a class. + + The tuple also allows accessing the fields by their names (see below for + examples). + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: tuple (with name accessors) of :class:`attr.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of ``attrs`` attributes for a class, whose + keys are the attribute names. + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: an ordered dict where keys are attribute names and values are + :class:`attr.Attribute`\ s. This will be a :class:`dict` if it's + naturally ordered like on Python 3.6+ or an + :class:`~collections.OrderedDict` otherwise. + + .. versionadded:: 18.1.0 + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return ordered_dict(((a.name, a) for a in attrs)) + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + :param inst: Instance of a class with ``attrs`` attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_cls(cls): + return "__slots__" in cls.__dict__ + + +def _is_slot_attr(a_name, super_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + return a_name in super_attr_map and _is_slot_cls(super_attr_map[a_name]) + + +def _attrs_to_init_script(attrs, frozen, slots, post_init, super_attr_map): + """ + Return a script of an initializer for *attrs* and a dict of globals. + + The globals are expected by the generated script. + + If *frozen* is True, we cannot set the attributes directly so we use + a cached ``object.__setattr__``. + """ + lines = [] + any_slot_ancestors = any( + _is_slot_attr(a.name, super_attr_map) + for a in attrs + ) + if frozen is True: + if slots is True: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + "_setattr = _cached_setattr.__get__(self, self.__class__)" + ) + + def fmt_setter(attr_name, value_var): + return "_setattr('%(attr_name)s', %(value_var)s)" % { + "attr_name": attr_name, + "value_var": value_var, + } + + def fmt_setter_with_converter(attr_name, value_var): + conv_name = _init_converter_pat.format(attr_name) + return "_setattr('%(attr_name)s', %(conv)s(%(value_var)s))" % { + "attr_name": attr_name, + "value_var": value_var, + "conv": conv_name, + } + else: + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + lines.append( + "_inst_dict = self.__dict__" + ) + if any_slot_ancestors: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup + # per assignment. + "_setattr = _cached_setattr.__get__(self, self.__class__)" + ) + + def fmt_setter(attr_name, value_var): + if _is_slot_attr(attr_name, super_attr_map): + res = "_setattr('%(attr_name)s', %(value_var)s)" % { + "attr_name": attr_name, + "value_var": value_var, + } + else: + res = "_inst_dict['%(attr_name)s'] = %(value_var)s" % { + "attr_name": attr_name, + "value_var": value_var, + } + return res + + def fmt_setter_with_converter(attr_name, value_var): + conv_name = _init_converter_pat.format(attr_name) + if _is_slot_attr(attr_name, super_attr_map): + tmpl = "_setattr('%(attr_name)s', %(c)s(%(value_var)s))" + else: + tmpl = "_inst_dict['%(attr_name)s'] = %(c)s(%(value_var)s)" + return tmpl % { + "attr_name": attr_name, + "value_var": value_var, + "c": conv_name, + } + else: + # Not frozen. + def fmt_setter(attr_name, value): + return "self.%(attr_name)s = %(value)s" % { + "attr_name": attr_name, + "value": value, + } + + def fmt_setter_with_converter(attr_name, value_var): + conv_name = _init_converter_pat.format(attr_name) + return "self.%(attr_name)s = %(conv)s(%(value_var)s)" % { + "attr_name": attr_name, + "value_var": value_var, + "conv": conv_name, + } + + args = [] + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + attr_name = a.name + arg_name = a.name.lstrip("_") + has_factory = isinstance(a.default, Factory) + if has_factory and a.default.takes_self: + maybe_self = "self" + else: + maybe_self = "" + if a.init is False: + if has_factory: + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append(fmt_setter_with_converter( + attr_name, + init_factory_name + "({0})".format(maybe_self))) + conv_name = _init_converter_pat.format(a.name) + names_for_globals[conv_name] = a.converter + else: + lines.append(fmt_setter( + attr_name, + init_factory_name + "({0})".format(maybe_self) + )) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.converter is not None: + lines.append(fmt_setter_with_converter( + attr_name, + "attr_dict['{attr_name}'].default" + .format(attr_name=attr_name) + )) + conv_name = _init_converter_pat.format(a.name) + names_for_globals[conv_name] = a.converter + else: + lines.append(fmt_setter( + attr_name, + "attr_dict['{attr_name}'].default" + .format(attr_name=attr_name) + )) + elif a.default is not NOTHING and not has_factory: + args.append( + "{arg_name}=attr_dict['{attr_name}'].default".format( + arg_name=arg_name, + attr_name=attr_name, + ) + ) + if a.converter is not None: + lines.append(fmt_setter_with_converter(attr_name, arg_name)) + names_for_globals[_init_converter_pat.format(a.name)] = ( + a.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name)) + elif has_factory: + args.append("{arg_name}=NOTHING".format(arg_name=arg_name)) + lines.append("if {arg_name} is not NOTHING:" + .format(arg_name=arg_name)) + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append(" " + fmt_setter_with_converter( + attr_name, arg_name + )) + lines.append("else:") + lines.append(" " + fmt_setter_with_converter( + attr_name, + init_factory_name + "({0})".format(maybe_self) + )) + names_for_globals[_init_converter_pat.format(a.name)] = ( + a.converter + ) + else: + lines.append(" " + fmt_setter(attr_name, arg_name)) + lines.append("else:") + lines.append(" " + fmt_setter( + attr_name, + init_factory_name + "({0})".format(maybe_self) + )) + names_for_globals[init_factory_name] = a.default.factory + else: + args.append(arg_name) + if a.converter is not None: + lines.append(fmt_setter_with_converter(attr_name, arg_name)) + names_for_globals[_init_converter_pat.format(a.name)] = ( + a.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name)) + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_{}".format(a.name) + attr_name = "__attr_{}".format(a.name) + lines.append(" {}(self, {}, self.{})".format( + val_name, attr_name, a.name)) + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + if post_init: + lines.append("self.__attrs_post_init__()") + + return """\ +def __init__(self, {args}): + {lines} +""".format( + args=", ".join(args), + lines="\n ".join(lines) if lines else "pass", + ), names_for_globals + + +class Attribute(object): + """ + *Read-only* representation of an attribute. + + :attribute name: The name of the attribute. + + Plus *all* arguments of :func:`attr.ib`. + + For the version history of the fields, see :func:`attr.ib`. + """ + __slots__ = ( + "name", "default", "validator", "repr", "cmp", "hash", "init", + "metadata", "type", "converter", + ) + + def __init__(self, name, default, validator, repr, cmp, hash, init, + convert=None, metadata=None, type=None, converter=None): + # Cache this descriptor here to speed things up later. + bound_setattr = _obj_setattr.__get__(self, Attribute) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + if convert is not None: + if converter is not None: + raise RuntimeError( + "Can't pass both `convert` and `converter`. " + "Please use `converter` only." + ) + warnings.warn( + "The `convert` argument is deprecated in favor of `converter`." + " It will be removed after 2019/01.", + DeprecationWarning, stacklevel=2 + ) + converter = convert + + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("cmp", cmp) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr("metadata", ( + metadata_proxy(metadata) if metadata + else _empty_metadata_singleton + )) + bound_setattr("type", type) + + def __setattr__(self, name, value): + raise FrozenInstanceError() + + @property + def convert(self): + warnings.warn( + "The `convert` attribute is deprecated in favor of `converter`. " + "It will be removed after 2019/01.", + DeprecationWarning, stacklevel=2, + ) + return self.converter + + @classmethod + def from_counting_attr(cls, name, ca, type=None): + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + raise ValueError( + "Type annotation and type argument cannot both be present" + ) + inst_dict = { + k: getattr(ca, k) + for k + in Attribute.__slots__ + if k not in ( + "name", "validator", "default", "type", "convert", + ) # exclude methods and deprecated alias + } + return cls( + name=name, validator=ca._validator, default=ca._default, type=type, + **inst_dict + ) + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) if name != "metadata" + else dict(self.metadata) + for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in zip(self.__slots__, state): + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr(name, metadata_proxy(value) if value else + _empty_metadata_singleton) + + +_a = [ + Attribute(name=name, default=NOTHING, validator=None, + repr=True, cmp=True, hash=(name != "metadata"), init=True) + for name in Attribute.__slots__ + if name != "convert" # XXX: remove once `convert` is gone +] + +Attribute = _add_hash( + _add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a), + attrs=[a for a in _a if a.hash] +) + + +class _CountingAttr(object): + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + __slots__ = ("counter", "_default", "repr", "cmp", "hash", "init", + "metadata", "_validator", "converter", "type") + __attrs_attrs__ = tuple( + Attribute(name=name, default=NOTHING, validator=None, + repr=True, cmp=True, hash=True, init=True) + for name + in ("counter", "_default", "repr", "cmp", "hash", "init",) + ) + ( + Attribute(name="metadata", default=None, validator=None, + repr=True, cmp=True, hash=False, init=True), + ) + cls_counter = 0 + + def __init__(self, default, validator, repr, cmp, hash, init, converter, + metadata, type): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + # If validator is a list/tuple, wrap it using helper validator. + if validator and isinstance(validator, (list, tuple)): + self._validator = and_(*validator) + else: + self._validator = validator + self.repr = repr + self.cmp = cmp + self.hash = hash + self.init = init + self.converter = converter + self.metadata = metadata + self.type = type + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + :raises DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError() + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_cmp(_add_repr(_CountingAttr)) + + +@attrs(slots=True, init=False, hash=True) +class Factory(object): + """ + Stores a factory callable. + + If passed as the default value to :func:`attr.ib`, the factory is used to + generate a new value. + + :param callable factory: A callable that takes either none or exactly one + mandatory positional argument depending on *takes_self*. + :param bool takes_self: Pass the partially initialized instance that is + being initialized as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + factory = attrib() + takes_self = attrib() + + def __init__(self, factory, takes_self=False): + """ + `Factory` is part of the default machinery so if we want a default + value here, we have to implement it ourselves. + """ + self.factory = factory + self.takes_self = takes_self + + +def make_class(name, attrs, bases=(object,), **attributes_arguments): + """ + A quick way to create a new class called *name* with *attrs*. + + :param name: The name for the new class. + :type name: str + + :param attrs: A list of names or a dictionary of mappings of names to + attributes. + + If *attrs* is a list or an ordered dict (:class:`dict` on Python 3.6+, + :class:`collections.OrderedDict` otherwise), the order is deduced from + the order of the names or attributes inside *attrs*. Otherwise the + order of the definition of the attributes is used. + :type attrs: :class:`list` or :class:`dict` + + :param tuple bases: Classes that the new class will subclass. + + :param attributes_arguments: Passed unmodified to :func:`attr.s`. + + :return: A new class with *attrs*. + :rtype: type + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + """ + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = dict((a, attrib()) for a in attrs) + else: + raise TypeError("attrs argument must be a dict or a list.") + + post_init = cls_dict.pop("__attrs_post_init__", None) + type_ = type( + name, + bases, + {} if post_init is None else {"__attrs_post_init__": post_init} + ) + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + try: + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__", + ) + except (AttributeError, ValueError): + pass + + return _attrs(these=cls_dict, **attributes_arguments)(type_) + + +# These are required by within this module so we define them here and merely +# import into .validators. + + +@attrs(slots=True, hash=True) +class _AndValidator(object): + """ + Compose many validators to a single one. + """ + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + :param validators: Arbitrary number of validators. + :type validators: callables + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) diff --git a/pipenv/vendor/attr/converters.py b/pipenv/vendor/attr/converters.py new file mode 100644 index 00000000..3b3bac92 --- /dev/null +++ b/pipenv/vendor/attr/converters.py @@ -0,0 +1,24 @@ +""" +Commonly useful converters. +""" + +from __future__ import absolute_import, division, print_function + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to ``None``. + + :param callable converter: the converter that is used for non-``None`` + values. + + .. versionadded:: 17.1.0 + """ + + def optional_converter(val): + if val is None: + return None + return converter(val) + + return optional_converter diff --git a/pipenv/vendor/attr/exceptions.py b/pipenv/vendor/attr/exceptions.py new file mode 100644 index 00000000..f949f3c9 --- /dev/null +++ b/pipenv/vendor/attr/exceptions.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import, division, print_function + + +class FrozenInstanceError(AttributeError): + """ + A frozen/immutable instance has been attempted to be modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing :exc:`AttributeError`. + + .. versionadded:: 16.1.0 + """ + msg = "can't set attribute" + args = [msg] + + +class AttrsAttributeNotFoundError(ValueError): + """ + An ``attrs`` function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-``attrs`` class has been passed into an ``attrs`` function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set using ``attr.ib()`` and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type + annotation. + + .. versionadded:: 17.3.0 + """ diff --git a/pipenv/vendor/attr/filters.py b/pipenv/vendor/attr/filters.py new file mode 100644 index 00000000..d1bad35e --- /dev/null +++ b/pipenv/vendor/attr/filters.py @@ -0,0 +1,52 @@ +""" +Commonly useful filters for :func:`attr.asdict`. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import isclass +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isclass(cls)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Whitelist *what*. + + :param what: What to whitelist. + :type what: :class:`list` of :class:`type` or :class:`attr.Attribute`\ s + + :rtype: :class:`callable` + """ + cls, attrs = _split_what(what) + + def include_(attribute, value): + return value.__class__ in cls or attribute in attrs + + return include_ + + +def exclude(*what): + """ + Blacklist *what*. + + :param what: What to blacklist. + :type what: :class:`list` of classes or :class:`attr.Attribute`\ s. + + :rtype: :class:`callable` + """ + cls, attrs = _split_what(what) + + def exclude_(attribute, value): + return value.__class__ not in cls and attribute not in attrs + + return exclude_ diff --git a/pipenv/vendor/attr/validators.py b/pipenv/vendor/attr/validators.py new file mode 100644 index 00000000..f8892fcd --- /dev/null +++ b/pipenv/vendor/attr/validators.py @@ -0,0 +1,166 @@ +""" +Commonly useful validators. +""" + +from __future__ import absolute_import, division, print_function + +from ._make import _AndValidator, and_, attrib, attrs + + +__all__ = [ + "and_", + "in_", + "instance_of", + "optional", + "provides", +] + + +@attrs(repr=False, slots=True, hash=True) +class _InstanceOfValidator(object): + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + raise TypeError( + "'{name}' must be {type!r} (got {value!r} that is a " + "{actual!r})." + .format(name=attr.name, type=self.type, + actual=value.__class__, value=value), + attr, self.type, value, + ) + + def __repr__(self): + return ( + "" + .format(type=self.type) + ) + + +def instance_of(type): + """ + A validator that raises a :exc:`TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + :func:`isinstance` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type :class:`attr.Attribute`), the expected type, and the value it + got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, slots=True, hash=True) +class _ProvidesValidator(object): + interface = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.interface.providedBy(value): + raise TypeError( + "'{name}' must provide {interface!r} which {value!r} " + "doesn't." + .format(name=attr.name, interface=self.interface, value=value), + attr, self.interface, value, + ) + + def __repr__(self): + return ( + "" + .format(interface=self.interface) + ) + + +def provides(interface): + """ + A validator that raises a :exc:`TypeError` if the initializer is called + with an object that does not provide the requested *interface* (checks are + performed using ``interface.providedBy(value)`` (see `zope.interface + `_). + + :param zope.interface.Interface interface: The interface to check for. + + :raises TypeError: With a human readable error message, the attribute + (of type :class:`attr.Attribute`), the expected interface, and the + value it got. + """ + return _ProvidesValidator(interface) + + +@attrs(repr=False, slots=True, hash=True) +class _OptionalValidator(object): + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return ( + "" + .format(what=repr(self.validator)) + ) + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to ``None`` in addition to satisfying the requirements of + the sub-validator. + + :param validator: A validator (or a list of validators) that is used for + non-``None`` values. + :type validator: callable or :class:`list` of callables. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + """ + if isinstance(validator, list): + return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, hash=True) +class _InValidator(object): + options = attrib() + + def __call__(self, inst, attr, value): + if value not in self.options: + raise ValueError( + "'{name}' must be in {options!r} (got {value!r})" + .format(name=attr.name, options=self.options, value=value) + ) + + def __repr__(self): + return ( + "" + .format(options=self.options) + ) + + +def in_(options): + """ + A validator that raises a :exc:`ValueError` if the initializer is called + with a value that does not belong in the options provided. The check is + performed using ``value in options``. + + :param options: Allowed options. + :type options: list, tuple, :class:`enum.Enum`, ... + + :raises ValueError: With a human readable error message, the attribute (of + type :class:`attr.Attribute`), the expected options, and the value it + got. + + .. versionadded:: 17.1.0 + """ + return _InValidator(options) diff --git a/pipenv/vendor/backports/__init__.py b/pipenv/vendor/backports/__init__.py index b5ddec96..0eea729c 100644 --- a/pipenv/vendor/backports/__init__.py +++ b/pipenv/vendor/backports/__init__.py @@ -2,4 +2,5 @@ from pkgutil import extend_path __path__ = extend_path(__path__, __name__) from . import shutil_get_terminal_size -from . import weakref \ No newline at end of file +from . import weakref +from . import shlex diff --git a/pipenv/vendor/backports/shlex/__init__.py b/pipenv/vendor/backports/shlex/__init__.py new file mode 100644 index 00000000..942430d7 --- /dev/null +++ b/pipenv/vendor/backports/shlex/__init__.py @@ -0,0 +1,9 @@ +""" +Partial backport of python 3's shlex module. + +Include's only `shlex.quote()` for backwards compatible functionality. +""" + +__all__ = ['quote'] + +from .shlex import quote diff --git a/pipenv/vendor/backports/shlex/shlex.py b/pipenv/vendor/backports/shlex/shlex.py new file mode 100644 index 00000000..2a06d2ee --- /dev/null +++ b/pipenv/vendor/backports/shlex/shlex.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import re +import sys + +__all__ = ['quote'] + +if sys.version_info >= (3, 1): + _find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search +else: + _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search + + +def quote(s): + """Return a shell-escaped version of the string *s*.""" + if not s: + return "''" + if _find_unsafe(s) is None: + return s + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + s.replace("'", "'\"'\"'") + "'" diff --git a/pipenv/vendor/pathlib2.py b/pipenv/vendor/pathlib2.py index b67649c6..87917ff9 100644 --- a/pipenv/vendor/pathlib2.py +++ b/pipenv/vendor/pathlib2.py @@ -1,7 +1,3 @@ -# Copyright (c) 2014-2017 Matthias C. M. Troffaes -# Copyright (c) 2012-2014 Antoine Pitrou and contributors -# Distributed under the terms of the MIT License. - import ctypes import fnmatch import functools @@ -13,7 +9,8 @@ import re import six import sys from collections import Sequence -from errno import EINVAL, ENOENT, ENOTDIR, EEXIST, EPERM, EACCES +from contextlib import contextmanager +from errno import EINVAL, ENOENT, ENOTDIR, EEXIST from operator import attrgetter from stat import ( S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO) @@ -27,22 +24,23 @@ try: intern = intern except NameError: intern = sys.intern +try: + basestring = basestring +except NameError: + basestring = str supports_symlinks = True -if os.name == 'nt': +try: import nt +except ImportError: + nt = None +else: if sys.getwindowsversion()[:2] >= (6, 0) and sys.version_info >= (3, 2): from nt import _getfinalpathname else: supports_symlinks = False _getfinalpathname = None -else: - nt = None -try: - from os import scandir as os_scandir -except ImportError: - from scandir import scandir as os_scandir __all__ = [ "PurePath", "PurePosixPath", "PureWindowsPath", @@ -61,15 +59,12 @@ def _py2_fsencode(parts): else part for part in parts] -def _try_except_fileexistserror(try_func, except_func, else_func=None): +def _try_except_fileexistserror(try_func, except_func): if sys.version_info >= (3, 3): try: try_func() except FileExistsError as exc: except_func(exc) - else: - if else_func is not None: - else_func() else: try: try_func() @@ -78,45 +73,6 @@ def _try_except_fileexistserror(try_func, except_func, else_func=None): raise else: except_func(exc) - else: - if else_func is not None: - else_func() - - -def _try_except_filenotfounderror(try_func, except_func): - if sys.version_info >= (3, 3): - try: - try_func() - except FileNotFoundError as exc: - except_func(exc) - else: - try: - try_func() - except EnvironmentError as exc: - if exc.errno != ENOENT: - raise - else: - except_func(exc) - - -def _try_except_permissionerror_iter(try_iter, except_iter): - if sys.version_info >= (3, 3): - try: - for x in try_iter(): - yield x - except PermissionError as exc: - for x in except_iter(exc): - yield x - else: - try: - for x in try_iter(): - yield x - except EnvironmentError as exc: - if exc.errno not in (EPERM, EACCES): - raise - else: - for x in except_iter(exc): - yield x def _win32_get_unique_path_id(path): @@ -264,7 +220,10 @@ class _WindowsFlavour(_Flavour): is_supported = (os.name == 'nt') - drive_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') + drive_letters = ( + set(chr(x) for x in range(ord('a'), ord('z') + 1)) | + set(chr(x) for x in range(ord('A'), ord('Z') + 1)) + ) ext_namespace_prefix = '\\\\?\\' reserved_names = ( @@ -324,28 +283,12 @@ class _WindowsFlavour(_Flavour): def casefold_parts(self, parts): return [p.lower() for p in parts] - def resolve(self, path, strict=False): + def resolve(self, path): s = str(path) if not s: return os.getcwd() - previous_s = None if _getfinalpathname is not None: - if strict: - return self._ext_to_normal(_getfinalpathname(s)) - else: - # End of the path after the first one not found - tail_parts = [] - while True: - try: - s = self._ext_to_normal(_getfinalpathname(s)) - except FileNotFoundError: - previous_s = s - s, tail = os.path.split(s) - tail_parts.append(tail) - if previous_s == s: - return path - else: - return os.path.join(s, *reversed(tail_parts)) + return self._ext_to_normal(_getfinalpathname(s)) # Means fallback on absolute return None @@ -450,7 +393,7 @@ class _PosixFlavour(_Flavour): def casefold_parts(self, parts): return parts - def resolve(self, path, strict=False): + def resolve(self, path): sep = self.sep accessor = path._accessor seen = {} @@ -481,10 +424,9 @@ class _PosixFlavour(_Flavour): try: target = accessor.readlink(newpath) except OSError as e: - if e.errno != EINVAL and strict: + if e.errno != EINVAL: raise - # Not a symlink, or non-strict mode. We just leave the path - # untouched. + # Not a symlink path = newpath else: seen[newpath] = None # not resolved symlink @@ -521,7 +463,6 @@ class _PosixFlavour(_Flavour): raise RuntimeError("Can't determine home directory " "for %r" % username) - _windows_flavour = _WindowsFlavour() _posix_flavour = _PosixFlavour() @@ -554,8 +495,6 @@ class _NormalAccessor(_Accessor): listdir = _wrap_strfunc(os.listdir) - scandir = _wrap_strfunc(os_scandir) - chmod = _wrap_strfunc(os.chmod) if hasattr(os, "lchmod"): @@ -602,6 +541,27 @@ _normal_accessor = _NormalAccessor() # Globbing helpers # +@contextmanager +def _cached(func): + try: + func.__cached__ + yield func + except AttributeError: + cache = {} + + def wrapper(*args): + try: + return cache[args] + except KeyError: + value = cache[args] = func(*args) + return value + wrapper.__cached__ = True + try: + yield wrapper + finally: + cache.clear() + + def _make_selector(pattern_parts): pat = pattern_parts[0] child_parts = pattern_parts[1:] @@ -616,7 +576,6 @@ def _make_selector(pattern_parts): cls = _PreciseSelector return cls(pat, child_parts) - if hasattr(functools, "lru_cache"): _make_selector = functools.lru_cache()(_make_selector) @@ -630,10 +589,8 @@ class _Selector: self.child_parts = child_parts if child_parts: self.successor = _make_selector(child_parts) - self.dironly = True else: self.successor = _TerminatingSelector() - self.dironly = False def select_from(self, parent_path): """Iterate over all child paths of `parent_path` matched by this @@ -641,15 +598,13 @@ class _Selector: path_cls = type(parent_path) is_dir = path_cls.is_dir exists = path_cls.exists - scandir = parent_path._accessor.scandir - if not is_dir(parent_path): - return iter([]) - return self._select_from(parent_path, is_dir, exists, scandir) + listdir = parent_path._accessor.listdir + return self._select_from(parent_path, is_dir, exists, listdir) class _TerminatingSelector: - def _select_from(self, parent_path, is_dir, exists, scandir): + def _select_from(self, parent_path, is_dir, exists, listdir): yield parent_path @@ -659,20 +614,14 @@ class _PreciseSelector(_Selector): self.name = name _Selector.__init__(self, child_parts) - def _select_from(self, parent_path, is_dir, exists, scandir): - def try_iter(): - path = parent_path._make_child_relpath(self.name) - if (is_dir if self.dironly else exists)(path): - for p in self.successor._select_from( - path, is_dir, exists, scandir): - yield p - - def except_iter(exc): + def _select_from(self, parent_path, is_dir, exists, listdir): + if not is_dir(parent_path): return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x + path = parent_path._make_child_relpath(self.name) + if exists(path): + for p in self.successor._select_from( + path, is_dir, exists, listdir): + yield p class _WildcardSelector(_Selector): @@ -681,26 +630,17 @@ class _WildcardSelector(_Selector): self.pat = re.compile(fnmatch.translate(pat)) _Selector.__init__(self, child_parts) - def _select_from(self, parent_path, is_dir, exists, scandir): - def try_iter(): - cf = parent_path._flavour.casefold - entries = list(scandir(parent_path)) - for entry in entries: - if not self.dironly or entry.is_dir(): - name = entry.name - casefolded = cf(name) - if self.pat.match(casefolded): - path = parent_path._make_child_relpath(name) - for p in self.successor._select_from( - path, is_dir, exists, scandir): - yield p - - def except_iter(exc): + def _select_from(self, parent_path, is_dir, exists, listdir): + if not is_dir(parent_path): return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x + cf = parent_path._flavour.casefold + for name in listdir(parent_path): + casefolded = cf(name) + if self.pat.match(casefolded): + path = parent_path._make_child_relpath(name) + for p in self.successor._select_from( + path, is_dir, exists, listdir): + yield p class _RecursiveWildcardSelector(_Selector): @@ -708,46 +648,31 @@ class _RecursiveWildcardSelector(_Selector): def __init__(self, pat, child_parts): _Selector.__init__(self, child_parts) - def _iterate_directories(self, parent_path, is_dir, scandir): + def _iterate_directories(self, parent_path, is_dir, listdir): yield parent_path + for name in listdir(parent_path): + path = parent_path._make_child_relpath(name) + if is_dir(path): + for p in self._iterate_directories(path, is_dir, listdir): + yield p - def try_iter(): - entries = list(scandir(parent_path)) - for entry in entries: - if entry.is_dir() and not entry.is_symlink(): - path = parent_path._make_child_relpath(entry.name) - for p in self._iterate_directories(path, is_dir, scandir): - yield p - - def except_iter(exc): + def _select_from(self, parent_path, is_dir, exists, listdir): + if not is_dir(parent_path): return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x - - def _select_from(self, parent_path, is_dir, exists, scandir): - def try_iter(): + with _cached(listdir) as listdir: yielded = set() try: successor_select = self.successor._select_from for starting_point in self._iterate_directories( - parent_path, is_dir, scandir): + parent_path, is_dir, listdir): for p in successor_select( - starting_point, is_dir, exists, scandir): + starting_point, is_dir, exists, listdir): if p not in yielded: yield p yielded.add(p) finally: yielded.clear() - def except_iter(exc): - return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x - # # Public API @@ -818,25 +743,13 @@ class PurePath(object): for a in args: if isinstance(a, PurePath): parts += a._parts + elif isinstance(a, basestring): + # Force-cast str subclasses to str (issue #21127) + parts.append(str(a)) else: - if sys.version_info >= (3, 6): - a = os.fspath(a) - else: - # duck typing for older Python versions - if hasattr(a, "__fspath__"): - a = a.__fspath__() - if isinstance(a, str): - # Force-cast str subclasses to str (issue #21127) - parts.append(str(a)) - # also handle unicode for PY2 (six.text_type = unicode) - elif six.PY2 and isinstance(a, six.text_type): - # cast to str using filesystem encoding - parts.append(a.encode(sys.getfilesystemencoding())) - else: - raise TypeError( - "argument should be a str object or an os.PathLike " - "object returning str, not %r" - % type(a)) + raise TypeError( + "argument should be a path or str object, not %r" + % type(a)) return cls._flavour.parse_parts(parts) @classmethod @@ -870,7 +783,7 @@ class PurePath(object): return cls._flavour.join(parts) def _init(self): - # Overridden in concrete Path + # Overriden in concrete Path pass def _make_child(self, args): @@ -889,9 +802,6 @@ class PurePath(object): self._parts) or '.' return self._str - def __fspath__(self): - return str(self) - def as_posix(self): """Return the string representation of the path with forward (/) slashes.""" @@ -1072,7 +982,7 @@ class PurePath(object): cf = self._flavour.casefold_parts if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): formatted = self._format_parsed_parts(to_drv, to_root, to_parts) - raise ValueError("{0!r} does not start with {1!r}" + raise ValueError("{!r} does not start with {!r}" .format(str(self), str(formatted))) return self._from_parsed_parts('', root if n == 1 else '', abs_parts[n:]) @@ -1160,12 +1070,6 @@ class PurePath(object): return True -# Can't subclass os.PathLike from PurePath and keep the constructor -# optimizations in PurePath._parse_args(). -if sys.version_info >= (3, 6): - os.PathLike.register(PurePath) - - class PurePosixPath(PurePath): _flavour = _posix_flavour __slots__ = () @@ -1252,8 +1156,8 @@ class Path(PurePath): return cls(cls()._flavour.gethomedir(None)) def samefile(self, other_path): - """Return whether other_path is the same or not as this file - (as returned by os.path.samefile()). + """Return whether `other_file` is the same or not as this file. + (as returned by os.path.samefile(file, other_file)). """ if hasattr(os.path, "samestat"): st = self.stat() @@ -1287,8 +1191,6 @@ class Path(PurePath): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given pattern. """ - if not pattern: - raise ValueError("Unacceptable pattern: {0!r}".format(pattern)) pattern = self._flavour.casefold(pattern) drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) if drv or root: @@ -1327,7 +1229,7 @@ class Path(PurePath): obj._init(template=self) return obj - def resolve(self, strict=False): + def resolve(self): """ Make the path absolute, resolving all symlinks on the way and also normalizing it (for example turning slashes into backslashes under @@ -1335,7 +1237,7 @@ class Path(PurePath): """ if self._closed: self._raise_closed() - s = self._flavour.resolve(self, strict=strict) + s = self._flavour.resolve(self) if s is None: # No symlink resolution => for consistency, raise an error if # the path doesn't exist or is forbidden @@ -1405,7 +1307,7 @@ class Path(PurePath): if not isinstance(data, six.binary_type): raise TypeError( 'data must be %s, not %s' % - (six.binary_type.__name__, data.__class__.__name__)) + (six.binary_type.__class__.__name__, data.__class__.__name__)) with self.open(mode='wb') as f: return f.write(data) @@ -1416,7 +1318,7 @@ class Path(PurePath): if not isinstance(data, six.text_type): raise TypeError( 'data must be %s, not %s' % - (six.text_type.__name__, data.__class__.__name__)) + (six.text_type.__class__.__name__, data.__class__.__name__)) with self.open(mode='w', encoding=encoding, errors=errors) as f: return f.write(data) @@ -1444,26 +1346,27 @@ class Path(PurePath): os.close(fd) def mkdir(self, mode=0o777, parents=False, exist_ok=False): - """ - Create a new directory at this given path. - """ + + def helper(exc): + if not exist_ok or not self.is_dir(): + raise exc + if self._closed: self._raise_closed() - - def _try_func(): - self._accessor.mkdir(self, mode) - - def _exc_func(exc): - if not parents or self.parent == self: - raise exc - self.parent.mkdir(parents=True, exist_ok=True) - self.mkdir(mode, parents=False, exist_ok=exist_ok) - - try: - _try_except_filenotfounderror(_try_func, _exc_func) - except OSError: - if not exist_ok or not self.is_dir(): - raise + if not parents: + _try_except_fileexistserror( + lambda: self._accessor.mkdir(self, mode), + helper) + else: + try: + _try_except_fileexistserror( + lambda: self._accessor.mkdir(self, mode), + helper) + except OSError as e: + if e.errno != ENOENT: + raise + self.parent.mkdir(parents=True) + self._accessor.mkdir(self, mode) def chmod(self, mode): """ @@ -1661,9 +1564,3 @@ class PosixPath(Path, PurePosixPath): class WindowsPath(Path, PureWindowsPath): __slots__ = () - - def owner(self): - raise NotImplementedError("Path.owner() is unsupported on this system") - - def group(self): - raise NotImplementedError("Path.group() is unsupported on this system") diff --git a/setup.py b/setup.py index 27ef93b8..f4ebf98b 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ required = [ 'setuptools>=36.2.1', 'virtualenv-clone>=0.2.5', 'virtualenv', - 'pathlib;python_version<"3.4"', + 'pathlib2==2.1.0;python_version<"3.4"', 'requests[security];python_version<"3.0"', 'ordereddict;python_version<"3.0"', ] diff --git a/tests/pypi/ibm-db-sa-py3/ibm-db-sa-py3-0.3.0.tar.gz b/tests/pypi/ibm-db-sa-py3/ibm-db-sa-py3-0.3.0.tar.gz new file mode 100644 index 00000000..dd4e4a88 Binary files /dev/null and b/tests/pypi/ibm-db-sa-py3/ibm-db-sa-py3-0.3.0.tar.gz differ diff --git a/tests/pypi/ibm-db-sa-py3/ibm-db-sa-py3-0.3.1-1.tar.gz b/tests/pypi/ibm-db-sa-py3/ibm-db-sa-py3-0.3.1-1.tar.gz new file mode 100644 index 00000000..7770d23e Binary files /dev/null and b/tests/pypi/ibm-db-sa-py3/ibm-db-sa-py3-0.3.1-1.tar.gz differ diff --git a/tests/pypi/win-inet-pton/win_inet_pton-1.0.1.tar.gz b/tests/pypi/win-inet-pton/win_inet_pton-1.0.1.tar.gz new file mode 100644 index 00000000..dfb77887 Binary files /dev/null and b/tests/pypi/win-inet-pton/win_inet_pton-1.0.1.tar.gz differ diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index 5c12a9b4..d9262f9b 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -964,7 +964,7 @@ flask = "==0.12.2" click = "==6.7" [dev-packages] -requests = {git = "https://github.com/requests/requests", egg = "requests"} +requests = {git = "https://github.com/requests/requests.git"} """.strip() f.write(contents) diff --git a/tests/test_requirements.py b/tests/test_requirements.py new file mode 100644 index 00000000..38742f23 --- /dev/null +++ b/tests/test_requirements.py @@ -0,0 +1,34 @@ +# -*- coding=utf-8 -*- +import os +import pytest +from first import first +from pipenv import requirements +from pipenv.utils import get_requirement, convert_deps_from_pip, convert_deps_to_pip + + +class TestRequirements: + + @pytest.mark.requirement + @pytest.mark.parametrize( + 'line, pipfile', + [ + ['requests', {'requests': '*'}], + ['requests[socks]', {'requests': {'extras': ['socks'], 'version': '*'}}], + ['django>1.10', {'django': '>1.10'}], + ['requests[socks]>1.10', {'requests': {'extras': ['socks'], 'version': '>1.10'}}], + ['-e git+git://github.com/pinax/pinax.git@1.4#egg=pinax', {'pinax': {'git': 'git://github.com/pinax/pinax.git', 'ref': '1.4', 'editable': True}}], + ['git+git://github.com/pinax/pinax.git@1.4#egg=pinax', {'pinax': {'git': 'git://github.com/pinax/pinax.git', 'ref': '1.4'}}], + ['FooProject==1.2 --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', {'FooProject': {'version': '==1.2', 'hash': 'sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'}}], + ['FooProject[stuff]==1.2 --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', {'FooProject': {'version': '==1.2', 'extras': ['stuff'], 'hash': 'sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'}}], + ['git+https://github.com/requests/requests.git@master#egg=requests[security]', {'requests': {'git': 'https://github.com/requests/requests.git', 'ref': 'master', 'extras': ['security']}}], + ['-e svn+svn://svn.myproject.org/svn/MyProject#egg=MyProject', {u'MyProject': {u'svn': u'svn://svn.myproject.org/svn/MyProject', 'editable': True}}], + ['hg+http://hg.myproject.org/MyProject@da39a3ee5e6b#egg=MyProject', {'MyProject': {'hg': 'http://hg.myproject.org/MyProject', 'ref': 'da39a3ee5e6b'}}] + ] + ) + def test_pip_requirements(self, line, pipfile): + from_line = requirements.PipenvRequirement.from_line(line) + pipfile_pkgname = first([k for k in pipfile.keys()]) + pipfile_entry = pipfile[pipfile_pkgname] + from_pipfile = requirements.PipenvRequirement.from_pipfile(pipfile_pkgname, [], pipfile_entry) + assert from_line.as_pipfile() == pipfile + assert from_pipfile.as_requirement() == line diff --git a/tests/test_utils.py b/tests/test_utils.py index 26db37fb..6d99b74e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -86,7 +86,7 @@ class TestUtils: # requests[socks] dep = 'requests[socks]' dep = pipenv.utils.convert_deps_from_pip(dep) - assert dep == {'requests': {'extras': ['socks']}} + assert dep == {'requests': {'extras': ['socks'], 'version': '*'}} # requests[socks] w/ version dep = 'requests[socks]==1.10' dep = pipenv.utils.convert_deps_from_pip(dep)