diff --git a/.cirrus.yml b/.cirrus.yml index 98088aa6e02..e9fc243b332 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -16,7 +16,8 @@ test_task: - pkg install -y git-lite $PYPACKAGE $SQLPACKAGE pip_script: - $PYTHON -m ensurepip - - $PYTHON -m pip install -U pip tox poetry + - $PYTHON -m pip install -U pip tox + - $PYTHON -m pip install -U --pre poetry - poetry config virtualenvs.in-project true tox_script: $PYTHON -m tox -e py -- -q --junitxml=junit.xml tests on_failure: diff --git a/.flake8 b/.flake8 index 5c25c0bb778..130c44c6752 100644 --- a/.flake8 +++ b/.flake8 @@ -16,6 +16,7 @@ exclude = .vscode .github poetry/utils/_compat.py + poetry/utils/env_scripts/tags.py tests/fixtures/ tests/repositories/fixtures/ tests/utils/fixtures/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 20efe023bf1..77589dc8907 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,7 +49,7 @@ jobs: shell: bash run: | curl -fsS -o get-poetry.py https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py - python get-poetry.py -y + python get-poetry.py -y --preview echo "::set-env name=PATH::$HOME/.poetry/bin:$PATH" - name: Configure poetry diff --git a/poetry.lock b/poetry.lock index 6339c9b0a56..ed7f5b5024c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -9,7 +9,6 @@ version = "1.4.4" [[package]] category = "dev" description = "Atomic file writes." -marker = "python_version >= \"3.5\" and sys_platform == \"win32\" or sys_platform == \"win32\" or python_version < \"3.5\"" name = "atomicwrites" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -32,7 +31,6 @@ tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.i [[package]] category = "dev" description = "Backport of functools.lru_cache" -marker = "python_version < \"3.2\"" name = "backports.functools-lru-cache" optional = false python-versions = ">=2.6" @@ -45,7 +43,6 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt [[package]] category = "dev" description = "The uncompromising code formatter." -marker = "python_version >= \"3.6\" and python_version < \"4.0\"" name = "black" optional = false python-versions = ">=3.6" @@ -74,10 +71,7 @@ version = "0.12.6" [package.dependencies] msgpack = ">=0.5.2" requests = "*" - -[package.dependencies.lockfile] -optional = true -version = ">=0.9" +lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} [package.extras] filecache = ["lockfile (>=0.9)"] @@ -107,7 +101,6 @@ version = "2020.6.20" [[package]] category = "main" description = "Foreign Function Interface for Python calling C code." -marker = "python_version >= \"2.7\" and python_version < \"2.8\" and (sys_platform == \"linux2\" or sys_platform == \"linux\") or python_version >= \"3.5\" and python_version < \"3.6\" and sys_platform == \"linux\" or python_version >= \"3.6\" and python_version < \"4.0\" and sys_platform == \"linux\"" name = "cffi" optional = false python-versions = "*" @@ -119,11 +112,10 @@ pycparser = "*" [[package]] category = "dev" description = "Validate configuration and produce human readable error messages." -marker = "python_version >= \"3.6.1\" and python_version < \"4.0.0\"" name = "cfgv" optional = false -python-versions = ">=3.6" -version = "3.0.0" +python-versions = ">=3.6.1" +version = "3.1.0" [[package]] category = "main" @@ -147,7 +139,6 @@ clikit = ">=0.6.0,<0.7.0" [[package]] category = "dev" description = "Composable command line interface toolkit" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\" or python_version >= \"3.6\" and python_version < \"4.0\"" name = "click" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -164,27 +155,14 @@ version = "0.6.2" [package.dependencies] pastel = ">=0.2.0,<0.3.0" pylev = ">=1.3,<2.0" - -[package.dependencies.crashtest] -python = ">=3.6,<4.0" -version = ">=0.3.0,<0.4.0" - -[package.dependencies.enum34] -python = ">=2.7,<2.8" -version = ">=1.1,<2.0" - -[package.dependencies.typing] -python = ">=2.7,<2.8 || >=3.4,<3.5" -version = ">=3.6,<4.0" - -[package.dependencies.typing-extensions] -python = ">=3.5,<3.5.4" -version = ">=3.6,<4.0" +crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} +enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} +typing-extensions = {version = ">=3.6,<4.0", markers = "python_version >= \"3.5\" and python_full_version < \"3.5.4\""} [[package]] category = "dev" description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\" and python_version != \"3.4\" and python_version < \"3.5\" or platform_system == \"Windows\" or python_version >= \"3.5\" and sys_platform == \"win32\" or sys_platform == \"win32\"" name = "colorama" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -193,7 +171,6 @@ version = "0.4.3" [[package]] category = "main" description = "Updated configparser from Python 3.7 for Python 2.6+." -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version == \"2.7\" and python_version == \"2.7\" or python_version < \"3\"" name = "configparser" optional = false python-versions = ">=2.6" @@ -206,7 +183,6 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytes [[package]] category = "main" description = "Backports and enhancements for the contextlib module" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.4\"" name = "contextlib2" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -226,7 +202,6 @@ toml = ["toml"] [[package]] category = "main" description = "Manage Python errors with ease" -marker = "python_version >= \"3.6\" and python_version < \"4.0\"" name = "crashtest" optional = false python-versions = ">=3.6,<4.0" @@ -235,7 +210,6 @@ version = "0.3.0" [[package]] category = "main" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -marker = "python_version >= \"2.7\" and python_version < \"2.8\" and (sys_platform == \"linux2\" or sys_platform == \"linux\") or python_version >= \"3.5\" and python_version < \"3.6\" and sys_platform == \"linux\" or python_version >= \"3.6\" and python_version < \"4.0\" and sys_platform == \"linux\"" name = "cryptography" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" @@ -244,14 +218,8 @@ version = "2.9.2" [package.dependencies] cffi = ">=1.8,<1.11.3 || >1.11.3" six = ">=1.4.1" - -[package.dependencies.enum34] -python = "<3" -version = "*" - -[package.dependencies.ipaddress] -python = "<3" -version = "*" +enum34 = {version = "*", markers = "python_version < \"3\""} +ipaddress = {version = "*", markers = "python_version < \"3\""} [package.extras] docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"] @@ -266,29 +234,22 @@ description = "Distribution utilities" name = "distlib" optional = false python-versions = "*" -version = "0.3.0" - -[package.dependencies] -nut = "*" +version = "0.3.1" [[package]] category = "main" description = "Discover and load entry points from installed packages." -marker = "python_version >= \"2.7\" and python_version < \"2.8\"" name = "entrypoints" optional = false python-versions = ">=2.7" version = "0.3" [package.dependencies] -[package.dependencies.configparser] -python = ">=2.7,<2.8" -version = ">=3.5" +configparser = {version = ">=3.5", markers = "python_version == \"2.7\""} [[package]] category = "main" description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"2.7\" and python_version < \"2.8\" and (sys_platform == \"linux2\" or sys_platform == \"linux\")" name = "enum34" optional = false python-versions = "*" @@ -305,7 +266,6 @@ version = "3.0.12" [[package]] category = "dev" description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" -marker = "python_version < \"3.0\"" name = "funcsigs" optional = false python-versions = "*" @@ -314,25 +274,22 @@ version = "1.0.2" [[package]] category = "main" description = "Backport of the functools module from Python 3.2.3 for use on 2.7 and PyPy." -marker = "python_version >= \"2.7\" and python_version < \"2.8\"" name = "functools32" optional = false python-versions = "*" version = "3.2.3-2" [[package]] -category = "dev" -description = "Clean single-source support for Python 3 and 2" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "future" +category = "main" +description = "Backport of the concurrent.futures package from Python 3" +name = "futures" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.18.2" +python-versions = ">=2.6, <3" +version = "3.3.0" [[package]] category = "main" description = "Version of the glob module that can capture patterns and supports recursive wildcards" -marker = "python_version >= \"2.7\" and python_version < \"2.8\"" name = "glob2" optional = false python-versions = "*" @@ -370,11 +327,10 @@ six = "*" [[package]] category = "dev" description = "File identification library for Python" -marker = "python_version >= \"3.6.1\" and python_version < \"4.0.0\"" name = "identify" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "1.4.20" +version = "1.4.21" [package.extras] license = ["editdistance"] @@ -385,31 +341,21 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.9" +version = "2.10" [[package]] category = "main" description = "Read metadata from Python packages" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.8\" or python_version >= \"3.5\" and python_version < \"3.6\" or python_version >= \"3.5\" and python_version < \"3.8\" or python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"3.8\" or python_version >= \"3.6\" and python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.6.1" +version = "1.7.0" [package.dependencies] zipp = ">=0.5" - -[package.dependencies.configparser] -python = "<3" -version = ">=3.5" - -[package.dependencies.contextlib2] -python = "<3" -version = "*" - -[package.dependencies.pathlib2] -python = "<3" -version = "*" +configparser = {version = ">=3.5", markers = "python_version < \"3\""} +contextlib2 = {version = "*", markers = "python_version < \"3\""} +pathlib2 = {version = "*", markers = "python_version < \"3\""} [package.extras] docs = ["sphinx", "rst.linker"] @@ -418,36 +364,17 @@ testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] category = "main" description = "Read resources from Python packages" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.7\" or python_version >= \"3.6.1\" and python_version < \"3.7\"" name = "importlib-resources" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "2.0.1" +version = "3.0.0" [package.dependencies] -[package.dependencies.contextlib2] -python = "<3" -version = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - -[package.dependencies.pathlib2] -python = "<3" -version = "*" - -[package.dependencies.singledispatch] -python = "<3.4" -version = "*" - -[package.dependencies.typing] -python = "<3.5" -version = "*" - -[package.dependencies.zipp] -python = "<3.8" -version = ">=0.4" +contextlib2 = {version = "*", markers = "python_version < \"3\""} +pathlib2 = {version = "*", markers = "python_version < \"3\""} +singledispatch = {version = "*", markers = "python_version < \"3.4\""} +typing = {version = "*", markers = "python_version < \"3.5\""} +zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx", "rst.linker", "jaraco.packaging"] @@ -455,7 +382,6 @@ docs = ["sphinx", "rst.linker", "jaraco.packaging"] [[package]] category = "main" description = "IPv4/IPv6 manipulation library" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" and (sys_platform == \"linux2\" or sys_platform == \"linux\")" name = "ipaddress" optional = false python-versions = "*" @@ -464,7 +390,6 @@ version = "1.0.23" [[package]] category = "main" description = "Low-level, pure Python DBus protocol wrapper." -marker = "python_version >= \"3.5\" and python_version < \"3.6\" and sys_platform == \"linux\" or python_version >= \"3.6\" and python_version < \"4.0\" and sys_platform == \"linux\"" name = "jeepney" optional = false python-versions = ">=3.5" @@ -476,7 +401,6 @@ dev = ["testpath"] [[package]] category = "dev" description = "A very fast and expressive template engine." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" name = "jinja2" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -488,28 +412,9 @@ MarkupSafe = ">=0.23" [package.extras] i18n = ["Babel (>=0.8)"] -[[package]] -category = "dev" -description = "Lightweight pipelining: using Python functions as pipeline jobs." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "joblib" -optional = false -python-versions = "*" -version = "0.14.1" - -[[package]] -category = "dev" -description = "Lightweight pipelining: using Python functions as pipeline jobs." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "joblib" -optional = false -python-versions = ">=3.6" -version = "0.15.1" - [[package]] category = "main" description = "Store and access your passwords safely." -marker = "python_version >= \"2.7\" and python_version < \"2.8\"" name = "keyring" optional = false python-versions = ">=2.7" @@ -517,11 +422,8 @@ version = "18.0.1" [package.dependencies] entrypoints = "*" -pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1" - -[package.dependencies.secretstorage] -python = "<3.5" -version = "<3" +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +secretstorage = {version = "<3", markers = "(sys_platform == \"linux2\" or sys_platform == \"linux\") and python_version < \"3.5\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -530,19 +432,15 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs", "pytest-flake8 [[package]] category = "main" description = "Store and access your passwords safely." -marker = "python_version >= \"3.5\" and python_version < \"3.6\"" name = "keyring" optional = false python-versions = ">=3.5" version = "20.0.1" [package.dependencies] -pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1" -secretstorage = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +secretstorage = {version = "*", markers = "sys_platform == \"linux\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -551,20 +449,16 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt [[package]] category = "main" description = "Store and access your passwords safely." -marker = "python_version >= \"3.6\" and python_version < \"4.0\"" name = "keyring" optional = false python-versions = ">=3.6" version = "21.2.1" [package.dependencies] -SecretStorage = ">=3" -jeepney = ">=0.4.2" -pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +SecretStorage = {version = ">=3", markers = "sys_platform == \"linux\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -573,7 +467,6 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt [[package]] category = "dev" description = "Python LiveReload is an awesome tool for web developers" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" name = "livereload" optional = false python-versions = "*" @@ -582,8 +475,12 @@ version = "2.6.2" [package.dependencies] six = "*" -[package.dependencies.tornado] -python = ">=2.8" +[[package.dependencies.tornado]] +markers = "python_version == \"2.7\"" +version = "<6" + +[[package.dependencies.tornado]] +markers = "python_version > \"2.7\"" version = "*" [[package]] @@ -594,27 +491,6 @@ optional = false python-versions = "*" version = "0.12.2" -[[package]] -category = "dev" -description = "A Python implementation of Lunr.js" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "lunr" -optional = false -python-versions = "*" -version = "0.5.8" - -[package.dependencies] -future = ">=0.16.0" -six = ">=1.11.0" - -[package.dependencies.nltk] -optional = true -python = ">=2.8" -version = ">=3.2.5" - -[package.extras] -languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] - [[package]] category = "dev" description = "Python implementation of Markdown." @@ -623,25 +499,6 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" version = "3.1.1" -[package.dependencies] -setuptools = ">=36" - -[package.extras] -testing = ["coverage", "pyyaml"] - -[[package]] -category = "dev" -description = "Python implementation of Markdown." -name = "markdown" -optional = false -python-versions = ">=3.5" -version = "3.2.2" - -[package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - [package.extras] testing = ["coverage", "pyyaml"] @@ -659,7 +516,6 @@ markdown = "*" [[package]] category = "dev" description = "Safely add untrusted strings to HTML/XML markup." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" name = "markupsafe" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" @@ -668,7 +524,6 @@ version = "1.1.1" [[package]] category = "dev" description = "Project documentation with Markdown." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" name = "mkdocs" optional = false python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" @@ -682,31 +537,9 @@ click = ">=3.3" livereload = ">=2.5.1" tornado = ">=5.0" -[[package]] -category = "dev" -description = "Project documentation with Markdown." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "mkdocs" -optional = false -python-versions = ">=3.5" -version = "1.1.2" - -[package.dependencies] -Jinja2 = ">=2.10.1" -Markdown = ">=3.2.1" -PyYAML = ">=3.10" -click = ">=3.3" -livereload = ">=2.5.1" -tornado = ">=5.0" - -[package.dependencies.lunr] -extras = ["languages"] -version = "0.5.8" - [[package]] category = "dev" description = "Rolling backport of unittest.mock for all Pythons" -marker = "python_version < \"3.0\"" name = "mock" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -714,10 +547,7 @@ version = "3.0.5" [package.dependencies] six = "*" - -[package.dependencies.funcsigs] -python = "<3.3" -version = ">=1" +funcsigs = {version = ">=1", markers = "python_version < \"3.3\""} [package.extras] build = ["twine", "wheel", "blurb"] @@ -727,7 +557,6 @@ test = ["pytest", "pytest-cov"] [[package]] category = "dev" description = "More routines for operating on iterables, beyond itertools" -marker = "python_version <= \"2.7\"" name = "more-itertools" optional = false python-versions = "*" @@ -752,33 +581,9 @@ optional = false python-versions = "*" version = "1.0.0" -[[package]] -category = "dev" -description = "Natural Language Toolkit" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "nltk" -optional = false -python-versions = "*" -version = "3.5" - -[package.dependencies] -click = "*" -joblib = "*" -regex = "*" -tqdm = "*" - -[package.extras] -all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"] -corenlp = ["requests"] -machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"] -plot = ["matplotlib"] -tgrep = ["pyparsing"] -twitter = ["twython"] - [[package]] category = "dev" description = "Node.js virtual environment builder" -marker = "python_version >= \"3.6.1\" and python_version < \"4.0.0\"" name = "nodeenv" optional = false python-versions = "*" @@ -786,14 +591,6 @@ version = "1.4.0" [[package]] category = "main" -description = "Network utility... things like a UDP/TCP relay." -name = "nut" -optional = false -python-versions = "*" -version = "0.2.0" - -[[package]] -category = "dev" description = "Core utilities for Python packages" name = "packaging" optional = false @@ -815,7 +612,6 @@ version = "0.2.0" [[package]] category = "main" description = "Object-oriented filesystem paths" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"2.7\" and python_version < \"2.8\" and sys_platform != \"win32\" or python_version < \"3.5\" or python_version >= \"3.5\" and python_version < \"3.6\"" name = "pathlib2" optional = false python-versions = "*" @@ -823,15 +619,11 @@ version = "2.3.5" [package.dependencies] six = "*" - -[package.dependencies.scandir] -python = "<3.5" -version = "*" +scandir = {version = "*", markers = "python_version < \"3.5\""} [[package]] category = "dev" description = "Utility library for gitignore style pattern matching of file paths." -marker = "python_version >= \"3.6\" and python_version < \"4.0\"" name = "pathspec" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -876,9 +668,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.13.1" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] @@ -892,30 +682,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "1.0.0a8" [package.dependencies] -[package.dependencies.enum34] -python = ">=2.7,<2.8" -version = ">=1.1.10,<2.0.0" - -[package.dependencies.functools32] -python = ">=2.7,<2.8" -version = ">=3.2.3-2,<4.0.0" - -[package.dependencies.pathlib2] -python = ">=2.7,<2.8" -version = ">=2.3.5,<3.0.0" - -[package.dependencies.typing] -python = ">=2.7,<2.8" -version = ">=3.7.4.1,<4.0.0.0" +enum34 = {version = ">=1.1.10,<2.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +functools32 = {version = ">=3.2.3-2,<4.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +pathlib2 = {version = ">=2.3.5,<3.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +typing = {version = ">=3.7.4.1,<4.0.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} [[package]] category = "dev" description = "A framework for managing and maintaining multi-language pre-commit hooks." -marker = "python_version >= \"3.6.1\" and python_version < \"4.0.0\"" name = "pre-commit" optional = false python-versions = ">=3.6.1" -version = "2.5.1" +version = "2.6.0" [package.dependencies] cfgv = ">=2.0.0" @@ -924,14 +702,8 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - -[package.dependencies.importlib-resources] -python = "<3.7" -version = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} [[package]] category = "main" @@ -952,7 +724,6 @@ version = "1.9.0" [[package]] category = "main" description = "C parser in Python" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" and (sys_platform == \"linux2\" or sys_platform == \"linux\") or python_version >= \"3.5\" and python_version < \"3.6\" and sys_platform == \"linux\" or python_version >= \"3.6\" and python_version < \"4.0\" and sys_platform == \"linux\"" name = "pycparser" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -966,14 +737,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "2.5.2" -[[package]] -category = "dev" -description = "Pygments is a syntax highlighting package written in Python." -name = "pygments" -optional = false -python-versions = ">=3.5" -version = "2.6.1" - [[package]] category = "dev" description = "Pygments Github custom lexers." @@ -1006,18 +769,7 @@ Markdown = ">=3.0.1" pep562 = "*" [[package]] -category = "dev" -description = "Extension pack for Python Markdown." -name = "pymdown-extensions" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "6.3" - -[package.dependencies] -Markdown = ">=3.2" - -[[package]] -category = "dev" +category = "main" description = "Python parsing module" name = "pyparsing" optional = false @@ -1040,26 +792,18 @@ pluggy = ">=0.12,<1.0" py = ">=1.5.0" six = ">=1.10.0" wcwidth = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\" and python_version != \"3.4\""} +funcsigs = {version = ">=1.0", markers = "python_version < \"3.0\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} -[package.dependencies.colorama] -python = "<3.4.0 || >=3.5.0" -version = "*" - -[package.dependencies.funcsigs] -python = "<3.0" -version = ">=1.0" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.dependencies.more-itertools] -python = "<2.8" +[[package.dependencies.more-itertools]] +markers = "python_version <= \"2.7\"" version = ">=4.0.0,<6.0.0" -[package.dependencies.pathlib2] -python = "<3.6" -version = ">=2.2.0" +[[package.dependencies.more-itertools]] +markers = "python_version > \"2.7\"" +version = ">=4.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] @@ -1073,22 +817,16 @@ python-versions = ">=3.5" version = "5.4.3" [package.dependencies] -atomicwrites = ">=1.0" attrs = ">=17.4.0" -colorama = "*" more-itertools = ">=4.0.0" packaging = "*" pluggy = ">=0.12,<1.0" py = ">=1.5.0" wcwidth = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.dependencies.pathlib2] -python = "<3.6" -version = ">=2.2.0" +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} [package.extras] checkqa-mypy = ["mypy (v0.761)"] @@ -1119,10 +857,7 @@ version = "1.13.0" [package.dependencies] pytest = ">=2.7" - -[package.dependencies.mock] -python = "<3.0" -version = "*" +mock = {version = "*", markers = "python_version < \"3.0\""} [package.extras] dev = ["pre-commit", "tox"] @@ -1143,7 +878,6 @@ termcolor = ">=1.1.0" [[package]] category = "main" description = "" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" and sys_platform == \"win32\" or python_version >= \"3.5\" and python_version < \"3.6\" and sys_platform == \"win32\" or python_version >= \"3.6\" and python_version < \"4.0\" and sys_platform == \"win32\"" name = "pywin32-ctypes" optional = false python-versions = "*" @@ -1152,7 +886,6 @@ version = "0.2.0" [[package]] category = "dev" description = "YAML parser and emitter for Python" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\" or python_version >= \"3.6.1\" and python_version < \"4.0.0\"" name = "pyyaml" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -1161,7 +894,6 @@ version = "5.3.1" [[package]] category = "dev" description = "Alternative regular expression module, to replace re." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\" or python_version >= \"3.6\" and python_version < \"4.0\"" name = "regex" optional = false python-versions = "*" @@ -1199,7 +931,6 @@ requests = ">=2.0.1,<3.0.0" [[package]] category = "main" description = "scandir, a better directory iterator and faster os.walk()" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"2.7\" and python_version < \"2.8\" and sys_platform != \"win32\"" name = "scandir" optional = false python-versions = "*" @@ -1208,7 +939,6 @@ version = "1.10.0" [[package]] category = "main" description = "Python bindings to FreeDesktop.org Secret Service API" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" and (sys_platform == \"linux2\" or sys_platform == \"linux\")" name = "secretstorage" optional = false python-versions = "*" @@ -1223,7 +953,6 @@ dbus-python = ["dbus-python"] [[package]] category = "main" description = "Python bindings to FreeDesktop.org Secret Service API" -marker = "python_version >= \"3.5\" and python_version < \"3.6\" and sys_platform == \"linux\" or python_version >= \"3.6\" and python_version < \"4.0\" and sys_platform == \"linux\"" name = "secretstorage" optional = false python-versions = ">=3.5" @@ -1244,7 +973,6 @@ version = "1.3.2" [[package]] category = "main" description = "This library brings functools.singledispatch from Python 3.4 to Python 2.6-3.3." -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.4\" or python_version >= \"2.7\" and python_version < \"2.8\" and (python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.4\")" name = "singledispatch" optional = false python-versions = "*" @@ -1264,7 +992,6 @@ version = "1.15.0" [[package]] category = "main" description = "A backport of the subprocess module from Python 3 for use on 2.x." -marker = "python_version >= \"2.7\" and python_version < \"2.8\"" name = "subprocess32" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" @@ -1295,22 +1022,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.5.11" [package.dependencies] -[package.dependencies.enum34] -python = ">=2.7,<2.8" -version = ">=1.1,<2.0" - -[package.dependencies.functools32] -python = ">=2.7,<2.8" -version = ">=3.2.3,<4.0.0" - -[package.dependencies.typing] -python = ">=2.7,<2.8 || >=3.4,<3.5" -version = ">=3.6,<4.0" +enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +functools32 = {version = ">=3.2.3,<4.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} +typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} [[package]] category = "dev" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" name = "tornado" optional = false python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, != 3.3.*" @@ -1322,10 +1040,9 @@ description = "tox is a generic virtualenv management and test command line tool name = "tox" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.15.2" +version = "3.16.1" [package.dependencies] -colorama = ">=0.4.1" filelock = ">=3.0.0" packaging = ">=14" pluggy = ">=0.12.0" @@ -1333,31 +1050,16 @@ py = ">=1.4.17" six = ">=1.14.0" toml = ">=0.9.4" virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12,<2" +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx (>=2.0.0)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] testing = ["freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-xdist (>=1.22.2)", "pytest-randomly (>=1.0.0)", "flaky (>=3.4.0)", "psutil (>=5.6.1)"] -[[package]] -category = "dev" -description = "Fast, Extensible Progress Meter" -marker = "python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"4.0\"" -name = "tqdm" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.46.1" - -[package.extras] -dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] - [[package]] category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" -marker = "python_version >= \"3.6\" and python_version < \"4.0\"" name = "typed-ast" optional = false python-versions = "*" @@ -1366,7 +1068,6 @@ version = "1.4.1" [[package]] category = "main" description = "Type Hints for Python" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\" or python_version >= \"2.7\" and python_version < \"2.8\" and (python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.5\") or python_version < \"3.5\"" name = "typing" optional = false python-versions = "*" @@ -1375,7 +1076,6 @@ version = "3.7.4.1" [[package]] category = "main" description = "Backported and Experimental Type Hints for Python 3.5+" -marker = "python_version >= \"3.5\" and python_full_version < \"3.5.4\"" name = "typing-extensions" optional = false python-versions = "*" @@ -1407,18 +1107,9 @@ appdirs = ">=1.4.3,<2" distlib = ">=0.3.0,<1" filelock = ">=3.0.0,<4" six = ">=1.9.0,<2" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12,<2" - -[package.dependencies.importlib-resources] -python = "<3.7" -version = ">=1.0" - -[package.dependencies.pathlib2] -python = "<3.4" -version = ">=2.3.3,<3" +importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +pathlib2 = {version = ">=2.3.3,<3", markers = "python_version < \"3.4\" and sys_platform != \"win32\""} [package.extras] docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] @@ -1433,9 +1124,7 @@ python-versions = "*" version = "0.2.5" [package.dependencies] -[package.dependencies."backports.functools-lru-cache"] -python = "<3.2" -version = ">=1.2.1" +"backports.functools-lru-cache" = {version = ">=1.2.1", markers = "python_version < \"3.2\""} [[package]] category = "main" @@ -1448,36 +1137,20 @@ version = "0.5.1" [[package]] category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.8\" or python_version >= \"3.5\" and python_version < \"3.6\" or python_version >= \"3.5\" and python_version < \"3.8\" or python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"3.8\"" name = "zipp" optional = false python-versions = ">=2.7" version = "1.2.0" [package.dependencies] -[package.dependencies.contextlib2] -python = "<3.4" -version = "*" +contextlib2 = {version = "*", markers = "python_version < \"3.4\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] -[[package]] -category = "main" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.6\" and python_version < \"3.8\" or python_version < \"3.8\" or python_version >= \"3.5\" and python_version < \"3.8\" or python_version >= \"2.7.9\" and python_version < \"2.8.0\" or python_version >= \"3.4\" and python_version < \"3.8\"" -name = "zipp" -optional = false -python-versions = ">=3.6" -version = "3.1.0" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] - [metadata] -content-hash = "78c1880171165781164556d08eb2cdd58ed2c708b73dfa6a92acd2dd1df3739c" +content-hash = "363cbb8480f9bbda72b318c6edfcaa1de130ceecba7c7d0e774005a3bca91df5" python-versions = "~2.7 || ^3.5" [metadata.files] @@ -1544,8 +1217,8 @@ cffi = [ {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"}, ] cfgv = [ - {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, - {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, + {file = "cfgv-3.1.0-py2.py3-none-any.whl", hash = "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53"}, + {file = "cfgv-3.1.0.tar.gz", hash = "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -1634,7 +1307,8 @@ cryptography = [ {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"}, ] distlib = [ - {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, @@ -1657,8 +1331,9 @@ functools32 = [ {file = "functools32-3.2.3-2.tar.gz", hash = "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d"}, {file = "functools32-3.2.3-2.zip", hash = "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0"}, ] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +futures = [ + {file = "futures-3.3.0-py2-none-any.whl", hash = "sha256:49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16"}, + {file = "futures-3.3.0.tar.gz", hash = "sha256:7e033af76a5e35f58e56da7a91e687706faf4e7bdfb2cbc3f2cca6b9bcda9794"}, ] glob2 = [ {file = "glob2-0.6.tar.gz", hash = "sha256:f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"}, @@ -1671,20 +1346,20 @@ httpretty = [ {file = "httpretty-0.9.7.tar.gz", hash = "sha256:66216f26b9d2c52e81808f3e674a6fb65d4bf719721394a1a9be926177e55fbe"}, ] identify = [ - {file = "identify-1.4.20-py2.py3-none-any.whl", hash = "sha256:acf0712ab4042642e8f44e9532d95c26fbe60c0ab8b6e5b654dd1bc6512810e0"}, - {file = "identify-1.4.20.tar.gz", hash = "sha256:b2cd24dece806707e0b50517c1b3bcf3044e0b1cb13a72e7d34aa31c91f2a55a"}, + {file = "identify-1.4.21-py2.py3-none-any.whl", hash = "sha256:dac33eff90d57164e289fb20bf4e131baef080947ee9bf45efcd0da8d19064bf"}, + {file = "identify-1.4.21.tar.gz", hash = "sha256:c4d07f2b979e3931894170a9e0d4b8281e6905ea6d018c326f7ffefaf20db680"}, ] idna = [ - {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, - {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, - {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, ] importlib-resources = [ - {file = "importlib_resources-2.0.1-py2.py3-none-any.whl", hash = "sha256:83985739b3a6679702f9ab33f0ad016ad564664d0568a31ac14d7c64789453e6"}, - {file = "importlib_resources-2.0.1.tar.gz", hash = "sha256:f5edfcece1cc9435d0979c19e08739521f4cf1aa1adaf6e571f732df6f568962"}, + {file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, + {file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, ] ipaddress = [ {file = "ipaddress-1.0.23-py2.py3-none-any.whl", hash = "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc"}, @@ -1698,12 +1373,6 @@ jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] -joblib = [ - {file = "joblib-0.14.1-py2.py3-none-any.whl", hash = "sha256:bdb4fd9b72915ffb49fde2229ce482dd7ae79d842ed8c2b4c932441495af1403"}, - {file = "joblib-0.14.1.tar.gz", hash = "sha256:0630eea4f5664c463f23fbf5dcfc54a2bc6168902719fa8e19daf033022786c8"}, - {file = "joblib-0.15.1-py3-none-any.whl", hash = "sha256:6825784ffda353cc8a1be573118085789e5b5d29401856b35b756645ab5aecb5"}, - {file = "joblib-0.15.1.tar.gz", hash = "sha256:61e49189c84b3c5d99a969d314853f4d1d263316cc694bec17548ebaa9c47b6e"}, -] keyring = [ {file = "keyring-18.0.1-py2.py3-none-any.whl", hash = "sha256:7b29ebfcf8678c4da531b2478a912eea01e80007e5ddca9ee0c7038cb3489ec6"}, {file = "keyring-18.0.1.tar.gz", hash = "sha256:67d6cc0132bd77922725fae9f18366bb314fd8f95ff4d323a4df41890a96a838"}, @@ -1719,15 +1388,9 @@ lockfile = [ {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, ] -lunr = [ - {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, - {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, -] markdown = [ {file = "Markdown-3.1.1-py2.py3-none-any.whl", hash = "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"}, {file = "Markdown-3.1.1.tar.gz", hash = "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a"}, - {file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"}, - {file = "Markdown-3.2.2.tar.gz", hash = "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17"}, ] markdown-include = [ {file = "markdown-include-0.5.1.tar.gz", hash = "sha256:72a45461b589489a088753893bc95c5fa5909936186485f4ed55caa57d10250f"}, @@ -1760,18 +1423,11 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mkdocs = [ {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, - {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, - {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mock = [ {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, @@ -1804,15 +1460,9 @@ msgpack = [ {file = "msgpack-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d"}, {file = "msgpack-1.0.0.tar.gz", hash = "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0"}, ] -nltk = [ - {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, -] nodeenv = [ {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, ] -nut = [ - {file = "nut-0.2.0.tar.gz", hash = "sha256:427bd08d2b47d0c1c8f0a76783586df8b31c3427a0800d7930f010c06ff81e85"}, -] packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, @@ -1850,8 +1500,8 @@ poetry-core = [ {file = "poetry_core-1.0.0a8-py2.py3-none-any.whl", hash = "sha256:d7d9e702fbae06c05abb1ada63678e940d746b2696f4601b951b27ac7c2c5337"}, ] pre-commit = [ - {file = "pre_commit-2.5.1-py2.py3-none-any.whl", hash = "sha256:c5c8fd4d0e1c363723aaf0a8f9cba0f434c160b48c4028f4bae6d219177945b3"}, - {file = "pre_commit-2.5.1.tar.gz", hash = "sha256:da463cf8f0e257f9af49047ba514f6b90dbd9b4f92f4c8847a3ccd36834874c7"}, + {file = "pre_commit-2.6.0-py2.py3-none-any.whl", hash = "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626"}, + {file = "pre_commit-2.6.0.tar.gz", hash = "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915"}, ] ptyprocess = [ {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, @@ -1868,8 +1518,6 @@ pycparser = [ pygments = [ {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, - {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, - {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, ] pygments-github-lexers = [ {file = "pygments-github-lexers-0.0.5.tar.gz", hash = "sha256:aaca57e77cd6fcfce8d6ee97a998962eebf7fbb810519a8ebde427c62823e133"}, @@ -1882,8 +1530,6 @@ pylev = [ pymdown-extensions = [ {file = "pymdown-extensions-6.2.1.tar.gz", hash = "sha256:3bbe6048275f8a0d13a0fe44e0ea201e67268aa7bb40c2544eef16abbf168f7b"}, {file = "pymdown_extensions-6.2.1-py2.py3-none-any.whl", hash = "sha256:dce5e17b93be0572322b7d06c9a13c13a9d98694d6468277911d50ca87d26f29"}, - {file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"}, - {file = "pymdown_extensions-6.3-py2.py3-none-any.whl", hash = "sha256:66fae2683c7a1dac53184f7de57f51f8dad73f9ead2f453e94e85096cb811335"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -1986,7 +1632,6 @@ six = [ ] subprocess32 = [ {file = "subprocess32-3.5.4-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:88e37c1aac5388df41cc8a8456bb49ebffd321a3ad4d70358e3518176de3a56b"}, - {file = "subprocess32-3.5.4-cp27-cp27mu-manylinux2014_x86_64.whl", hash = "sha256:e45d985aef903c5b7444d34350b05da91a9e0ea015415ab45a21212786c649d0"}, {file = "subprocess32-3.5.4.tar.gz", hash = "sha256:eb2937c80497978d181efa1b839ec2d9622cf9600a039a79d0e108d1f9aec79d"}, ] termcolor = [ @@ -2010,12 +1655,8 @@ tornado = [ {file = "tornado-5.1.1.tar.gz", hash = "sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409"}, ] tox = [ - {file = "tox-3.15.2-py2.py3-none-any.whl", hash = "sha256:50a188b8e17580c1fb931f494a754e6507d4185f54fb18aca5ba3e12d2ffd55e"}, - {file = "tox-3.15.2.tar.gz", hash = "sha256:c696d36cd7c6a28ada2da780400e44851b20ee19ef08cfe73344a1dcebbbe9f3"}, -] -tqdm = [ - {file = "tqdm-4.46.1-py2.py3-none-any.whl", hash = "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c"}, - {file = "tqdm-4.46.1.tar.gz", hash = "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f"}, + {file = "tox-3.16.1-py2.py3-none-any.whl", hash = "sha256:60c3793f8ab194097ec75b5a9866138444f63742b0f664ec80be1222a40687c5"}, + {file = "tox-3.16.1.tar.gz", hash = "sha256:9a746cda9cadb9e1e05c7ab99f98cfcea355140d2ecac5f97520be94657c3bc7"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -2069,6 +1710,4 @@ webencodings = [ zipp = [ {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, ] diff --git a/poetry/config/config.py b/poetry/config/config.py index d7fc62d35f9..dbece199237 100644 --- a/poetry/config/config.py +++ b/poetry/config/config.py @@ -37,6 +37,7 @@ class Config(object): "in-project": False, "path": os.path.join("{cache-dir}", "virtualenvs"), }, + "experimental": {"new-installer": True}, } def __init__( diff --git a/poetry/console/commands/add.py b/poetry/console/commands/add.py index 5747fe386df..3cc505c0dcf 100644 --- a/poetry/console/commands/add.py +++ b/poetry/console/commands/add.py @@ -1,11 +1,11 @@ from cleo import argument from cleo import option -from .env_command import EnvCommand from .init import InitCommand +from .installer_command import InstallerCommand -class AddCommand(EnvCommand, InitCommand): +class AddCommand(InstallerCommand, InitCommand): name = "add" description = "Adds a new dependency to pyproject.toml." @@ -56,7 +56,6 @@ class AddCommand(EnvCommand, InitCommand): loggers = ["poetry.repositories.pypi_repository"] def handle(self): - from poetry.installation.installer import Installer from poetry.core.semver import parse_constraint from tomlkit import inline_table @@ -149,18 +148,17 @@ def handle(self): # Update packages self.reset_poetry() - installer = Installer( - self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool - ) - - installer.dry_run(self.option("dry-run")) - installer.update(True) + self._installer.set_package(self.poetry.package) + self._installer.dry_run(self.option("dry-run")) + self._installer.verbose(self._io.is_verbose()) + self._installer.update(True) if self.option("lock"): - installer.lock() - installer.whitelist([r["name"] for r in requirements]) + self._installer.lock() + + self._installer.whitelist([r["name"] for r in requirements]) try: - status = installer.run() + status = self._installer.run() except Exception: self.poetry.file.write(original_content) @@ -169,10 +167,10 @@ def handle(self): if status != 0 or self.option("dry-run"): # Revert changes if not self.option("dry-run"): - self.error( + self.line_error( "\n" - "Addition failed, reverting pyproject.toml " - "to its original content." + "Failed to add packages, reverting the pyproject.toml file " + "to its original content." ) self.poetry.file.write(original_content) diff --git a/poetry/console/commands/config.py b/poetry/console/commands/config.py index 68154296ee9..6dcd509dc13 100644 --- a/poetry/console/commands/config.py +++ b/poetry/console/commands/config.py @@ -57,6 +57,11 @@ def unique_config_values(self): lambda val: str(Path(val)), str(Path(CACHE_DIR) / "virtualenvs"), ), + "experimental.new-installer": ( + boolean_validator, + boolean_normalizer, + True, + ), } return unique_config_values diff --git a/poetry/console/commands/install.py b/poetry/console/commands/install.py index d4fc00ea501..1a932ecb7cf 100644 --- a/poetry/console/commands/install.py +++ b/poetry/console/commands/install.py @@ -1,9 +1,9 @@ from cleo import option -from .env_command import EnvCommand +from .installer_command import InstallerCommand -class InstallCommand(EnvCommand): +class InstallCommand(InstallerCommand): name = "install" description = "Installs the project dependencies." @@ -48,12 +48,11 @@ class InstallCommand(EnvCommand): _loggers = ["poetry.repositories.pypi_repository"] def handle(self): - from poetry.installation.installer import Installer from poetry.masonry.builders import EditableBuilder from poetry.core.masonry.utils.module import ModuleOrPackageNotFound - installer = Installer( - self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool + self._installer.use_executor( + self.poetry.config.get("experimental.new-installer", False) ) extras = [] @@ -63,13 +62,13 @@ def handle(self): else: extras.append(extra) - installer.extras(extras) - installer.dev_mode(not self.option("no-dev")) - installer.dry_run(self.option("dry-run")) - installer.remove_untracked(self.option("remove-untracked")) - installer.verbose(self.option("verbose")) + self._installer.extras(extras) + self._installer.dev_mode(not self.option("no-dev")) + self._installer.dry_run(self.option("dry-run")) + self._installer.remove_untracked(self.option("remove-untracked")) + self._installer.verbose(self._io.is_verbose()) - return_code = installer.run() + return_code = self._installer.run() if return_code != 0: return return_code @@ -85,15 +84,32 @@ def handle(self): # If this is a true error it will be picked up later by build anyway. return 0 - self.line( - " - Installing {} ({})".format( - self.poetry.package.pretty_name, self.poetry.package.pretty_version + self.line("") + if not self._io.supports_ansi() or self.io.is_debug(): + self.line( + "Installing the current project: {} ({})".format( + self.poetry.package.pretty_name, self.poetry.package.pretty_version + ) + ) + else: + self.write( + "Installing the current project: {} ({})".format( + self.poetry.package.pretty_name, self.poetry.package.pretty_version + ) ) - ) if self.option("dry-run"): + self.line("") return 0 builder.build() + if self._io.supports_ansi() and not self.io.is_debug(): + self.overwrite( + "Installing the current project: {} ({})".format( + self.poetry.package.pretty_name, self.poetry.package.pretty_version + ) + ) + self.line("") + return 0 diff --git a/poetry/console/commands/installer_command.py b/poetry/console/commands/installer_command.py new file mode 100644 index 00000000000..57df3c957f0 --- /dev/null +++ b/poetry/console/commands/installer_command.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING + +from .env_command import EnvCommand + + +if TYPE_CHECKING: + from poetry.installation.installer import Installer + + +class InstallerCommand(EnvCommand): + def __init__(self): + self._installer = None + + super(InstallerCommand, self).__init__() + + @property + def installer(self): # type: () -> Installer + return self._installer + + def set_installer(self, installer): # type: (Installer) -> None + self._installer = installer diff --git a/poetry/console/commands/lock.py b/poetry/console/commands/lock.py index 8639425b446..ddedb055d87 100644 --- a/poetry/console/commands/lock.py +++ b/poetry/console/commands/lock.py @@ -1,7 +1,7 @@ -from .env_command import EnvCommand +from .installer_command import InstallerCommand -class LockCommand(EnvCommand): +class LockCommand(InstallerCommand): name = "lock" description = "Locks the project dependencies." @@ -17,12 +17,10 @@ class LockCommand(EnvCommand): loggers = ["poetry.repositories.pypi_repository"] def handle(self): - from poetry.installation.installer import Installer - - installer = Installer( - self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool + self._installer.use_executor( + self.poetry.config.get("experimental.new-installer", False) ) - installer.lock() + self._installer.lock() - return installer.run() + return self._installer.run() diff --git a/poetry/console/commands/remove.py b/poetry/console/commands/remove.py index c713b8a000b..a85d7ea86c7 100644 --- a/poetry/console/commands/remove.py +++ b/poetry/console/commands/remove.py @@ -1,10 +1,10 @@ from cleo import argument from cleo import option -from .env_command import EnvCommand +from .installer_command import InstallerCommand -class RemoveCommand(EnvCommand): +class RemoveCommand(InstallerCommand): name = "remove" description = "Removes a package from the project dependencies." @@ -28,8 +28,6 @@ class RemoveCommand(EnvCommand): loggers = ["poetry.repositories.pypi_repository"] def handle(self): - from poetry.installation.installer import Installer - packages = self.argument("packages") is_dev = self.option("dev") @@ -62,16 +60,18 @@ def handle(self): # Update packages self.reset_poetry() - installer = Installer( - self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool + self._installer.set_package(self.poetry.package) + self._installer.use_executor( + self.poetry.config.get("experimental.new-installer", False) ) - installer.dry_run(self.option("dry-run")) - installer.update(True) - installer.whitelist(requirements) + self._installer.dry_run(self.option("dry-run")) + self._installer.verbose(self._io.is_verbose()) + self._installer.update(True) + self._installer.whitelist(requirements) try: - status = installer.run() + status = self._installer.run() except Exception: self.poetry.file.write(original_content) @@ -80,7 +80,7 @@ def handle(self): if status != 0 or self.option("dry-run"): # Revert changes if not self.option("dry-run"): - self.error( + self.line_error( "\n" "Removal failed, reverting pyproject.toml " "to its original content." diff --git a/poetry/console/commands/update.py b/poetry/console/commands/update.py index 5c00f27ba61..9e18feb78b6 100644 --- a/poetry/console/commands/update.py +++ b/poetry/console/commands/update.py @@ -1,10 +1,10 @@ from cleo import argument from cleo import option -from .env_command import EnvCommand +from .installer_command import InstallerCommand -class UpdateCommand(EnvCommand): +class UpdateCommand(InstallerCommand): name = "update" description = ( @@ -28,22 +28,20 @@ class UpdateCommand(EnvCommand): loggers = ["poetry.repositories.pypi_repository"] def handle(self): - from poetry.installation.installer import Installer - packages = self.argument("packages") - installer = Installer( - self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool + self._installer.use_executor( + self.poetry.config.get("experimental.new-installer", False) ) if packages: - installer.whitelist({name: "*" for name in packages}) + self._installer.whitelist({name: "*" for name in packages}) - installer.dev_mode(not self.option("no-dev")) - installer.dry_run(self.option("dry-run")) - installer.execute_operations(not self.option("lock")) + self._installer.dev_mode(not self.option("no-dev")) + self._installer.dry_run(self.option("dry-run")) + self._installer.execute_operations(not self.option("lock")) # Force update - installer.update(True) + self._installer.update(True) - return installer.run() + return self._installer.run() diff --git a/poetry/console/config/application_config.py b/poetry/console/config/application_config.py index 328d88d0ba5..593c29b4377 100644 --- a/poetry/console/config/application_config.py +++ b/poetry/console/config/application_config.py @@ -27,6 +27,7 @@ from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand +from poetry.console.commands.installer_command import InstallerCommand from poetry.console.logging.io_formatter import IOFormatter from poetry.console.logging.io_handler import IOHandler from poetry.utils._compat import PY36 @@ -37,15 +38,22 @@ def configure(self): super(ApplicationConfig, self).configure() self.add_style(Style("c1").fg("cyan")) - self.add_style(Style("c2").fg("green")) + self.add_style(Style("c2").fg("default").bold()) self.add_style(Style("info").fg("blue")) self.add_style(Style("comment").fg("green")) self.add_style(Style("error").fg("red").bold()) self.add_style(Style("warning").fg("yellow").bold()) self.add_style(Style("debug").fg("default").dark()) + self.add_style(Style("success").fg("green")) + + # Dark variants + self.add_style(Style("c1_dark").fg("cyan").dark()) + self.add_style(Style("c2_dark").fg("default").bold().dark()) + self.add_style(Style("success_dark").fg("green").dark()) self.add_event_listener(PRE_HANDLE, self.register_command_loggers) self.add_event_listener(PRE_HANDLE, self.set_env) + self.add_event_listener(PRE_HANDLE, self.set_installer) if PY36: from poetry.mixology.solutions.providers import ( @@ -93,6 +101,9 @@ def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, Any) -> if not isinstance(command, EnvCommand): return + if command.env is not None: + return + io = event.io poetry = command.poetry @@ -104,6 +115,32 @@ def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, Any) -> command.set_env(env) + def set_installer( + self, event, event_name, _ + ): # type: (PreHandleEvent, str, Any) -> None + command = event.command.config.handler # type: InstallerCommand + if not isinstance(command, InstallerCommand): + return + + # If the command already has an installer + # we skip this step + if command.installer is not None: + return + + from poetry.installation.installer import Installer + + poetry = command.poetry + installer = Installer( + event.io, + command.env, + poetry.package, + poetry.locker, + poetry.pool, + poetry.config, + ) + installer.use_executor(poetry.config.get("experimental.new-installer", False)) + command.set_installer(installer) + def resolve_help_command( self, event, event_name, dispatcher ): # type: (PreResolveEvent, str, EventDispatcher) -> None diff --git a/poetry/installation/authenticator.py b/poetry/installation/authenticator.py new file mode 100644 index 00000000000..795a72719f6 --- /dev/null +++ b/poetry/installation/authenticator.py @@ -0,0 +1,133 @@ +from typing import TYPE_CHECKING + +from poetry.utils._compat import urlparse +from poetry.utils.password_manager import PasswordManager + + +if TYPE_CHECKING: + from typing import Any + from typing import Optional + from typing import Tuple + + from clikit.api.io import IO + from requests import Request # noqa + from requests import Response # noqa + from requests import Session # noqa + + from poetry.config.config import Config + + +class Authenticator(object): + def __init__(self, config, io): # type: (Config, IO) -> None + self._config = config + self._io = io + self._session = None + self._credentials = {} + self._password_manager = PasswordManager(self._config) + + @property + def session(self): # type: () -> Session + from requests import Session # noqa + + if self._session is None: + self._session = Session() + + return self._session + + def request(self, method, url, **kwargs): # type: (str, str, Any) -> Response + from requests import Request # noqa + from requests.auth import HTTPBasicAuth + + request = Request(method, url) + + username, password = self._get_credentials_for_url(url) + + if username is not None and password is not None: + request = HTTPBasicAuth(username, password)(request) + + session = self.session + prepared_request = session.prepare_request(request) + + proxies = kwargs.get("proxies", {}) + stream = kwargs.get("stream") + verify = kwargs.get("verify") + cert = kwargs.get("cert") + + settings = session.merge_environment_settings( + prepared_request.url, proxies, stream, verify, cert + ) + + # Send the request. + send_kwargs = { + "timeout": kwargs.get("timeout"), + "allow_redirects": kwargs.get("allow_redirects", True), + } + send_kwargs.update(settings) + resp = session.send(prepared_request, **send_kwargs) + + resp.raise_for_status() + + return resp + + def _get_credentials_for_url( + self, url + ): # type: (str) -> Tuple[Optional[str], Optional[str]] + parsed_url = urlparse.urlsplit(url) + + netloc = parsed_url.netloc + + credentials = self._credentials.get(netloc, (None, None)) + + if credentials == (None, None): + if "@" not in netloc: + credentials = self._get_credentials_for_netloc_from_config(netloc) + else: + # Split from the right because that's how urllib.parse.urlsplit() + # behaves if more than one @ is present (which can be checked using + # the password attribute of urlsplit()'s return value). + auth, netloc = netloc.rsplit("@", 1) + if ":" in auth: + # Split from the left because that's how urllib.parse.urlsplit() + # behaves if more than one : is present (which again can be checked + # using the password attribute of the return value) + credentials = auth.split(":", 1) + else: + credentials = auth, None + + credentials = tuple( + None if x is None else urlparse.unquote(x) for x in credentials + ) + + if credentials[0] is not None or credentials[1] is not None: + credentials = (credentials[0] or "", credentials[1] or "") + + self._credentials[netloc] = credentials + + return credentials[0], credentials[1] + + def _get_credentials_for_netloc_from_config( + self, netloc + ): # type: (str) -> Tuple[Optional[str], Optional[str]] + credentials = (None, None) + for repository_name in self._config.get("http-basic", {}): + repository_config = self._config.get( + "repositories.{}".format(repository_name) + ) + if not repository_config: + continue + + url = repository_config.get("url") + if not url: + continue + + parsed_url = urlparse.urlsplit(url) + + if netloc == parsed_url.netloc: + auth = self._password_manager.get_http_auth(repository_name) + + if auth is None: + continue + + return auth["username"], auth["password"] + + return credentials diff --git a/poetry/installation/chef.py b/poetry/installation/chef.py new file mode 100644 index 00000000000..9e556d7690b --- /dev/null +++ b/poetry/installation/chef.py @@ -0,0 +1,110 @@ +import hashlib +import json + +from typing import TYPE_CHECKING + +from poetry.core.packages.utils.link import Link +from poetry.utils._compat import Path + +from .chooser import InvalidWheelName +from .chooser import Wheel + + +if TYPE_CHECKING: + from typing import List + from typing import Optional + + from poetry.config.config import Config + from poetry.utils.env import Env + + +class Chef: + def __init__(self, config, env): # type: (Config, Env) -> None + self._config = config + self._env = env + self._cache_dir = ( + Path(config.get("cache-dir")).expanduser().joinpath("artifacts") + ) + + def prepare(self, archive): # type: (Path) -> Path + return archive + + def prepare_sdist(self, archive): # type: (Path) -> Path + return archive + + def prepare_wheel(self, archive): # type: (Path) -> Path + return archive + + def should_prepare(self, archive): # type: (Path) -> bool + return not self.is_wheel(archive) + + def is_wheel(self, archive): # type: (Path) -> bool + return archive.suffix == ".whl" + + def get_cached_archive_for_link(self, link): # type: (Link) -> Optional[Link] + # If the archive is already a wheel, there is no need to cache it. + if link.is_wheel: + pass + + archives = self.get_cached_archives_for_link(link) + + if not archives: + return link + + candidates = [] + for archive in archives: + if not archive.is_wheel: + candidates.append((float("inf"), archive)) + continue + + try: + wheel = Wheel(archive.filename) + except InvalidWheelName: + continue + + if not wheel.is_supported_by_environment(self._env): + continue + + candidates.append( + (wheel.get_minimum_supported_index(self._env.supported_tags), archive), + ) + + if not candidates: + return link + + return min(candidates)[1] + + def get_cached_archives_for_link(self, link): # type: (Link) -> List[Link] + cache_dir = self.get_cache_directory_for_link(link) + + archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] + links = [] + for archive_type in archive_types: + for archive in cache_dir.glob("*.{}".format(archive_type)): + links.append(Link("file://{}".format(str(archive)))) + + return links + + def get_cache_directory_for_link(self, link): # type: (Link) -> Path + key_parts = {"url": link.url_without_fragment} + + if link.hash_name is not None and link.hash is not None: + key_parts[link.hash_name] = link.hash + + if link.subdirectory_fragment: + key_parts["subdirectory"] = link.subdirectory_fragment + + key_parts["interpreter_name"] = self._env.marker_env["interpreter_name"] + key_parts["interpreter_version"] = "".join( + self._env.marker_env["interpreter_version"].split(".")[:2] + ) + + key = hashlib.sha256( + json.dumps( + key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("ascii") + ).hexdigest() + + split_key = [key[:2], key[2:4], key[4:6], key[6:]] + + return self._cache_dir.joinpath(*split_key) diff --git a/poetry/installation/chooser.py b/poetry/installation/chooser.py new file mode 100644 index 00000000000..ee825bc9740 --- /dev/null +++ b/poetry/installation/chooser.py @@ -0,0 +1,179 @@ +import re + +from typing import List +from typing import Tuple + +from packaging.tags import Tag +from poetry.core.packages.package import Package +from poetry.core.packages.utils.link import Link +from poetry.repositories.pool import Pool +from poetry.utils.env import Env +from poetry.utils.patterns import wheel_file_re + + +class InvalidWheelName(Exception): + pass + + +class Wheel(object): + def __init__(self, filename): # type: (str) -> None + wheel_info = wheel_file_re.match(filename) + if not wheel_info: + raise InvalidWheelName("{} is not a valid wheel filename.".format(filename)) + + self.filename = filename + self.name = wheel_info.group("name").replace("_", "-") + self.version = wheel_info.group("ver").replace("_", "-") + self.build_tag = wheel_info.group("build") + self.pyversions = wheel_info.group("pyver").split(".") + self.abis = wheel_info.group("abi").split(".") + self.plats = wheel_info.group("plat").split(".") + + self.tags = { + Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats + } + + def get_minimum_supported_index(self, tags): + indexes = [tags.index(t) for t in self.tags if t in tags] + + return min(indexes) if indexes else None + + def is_supported_by_environment(self, env): + return bool(set(env.supported_tags).intersection(self.tags)) + + +class Chooser: + """ + A Chooser chooses an appropriate release archive for packages. + """ + + def __init__(self, pool, env): # type: (Pool, Env) -> None + self._pool = pool + self._env = env + + def choose_for(self, package): # type: (Package) -> Link + """ + Return the url of the selected archive for a given package. + """ + links = [] + for link in self._get_links(package): + if link.is_wheel and not Wheel(link.filename).is_supported_by_environment( + self._env + ): + continue + + if link.ext == ".egg": + continue + + links.append(link) + + if not links: + raise RuntimeError( + "Unable to find installation candidates for {}".format(package) + ) + + # Get the best link + chosen = max(links, key=lambda link: self._sort_key(package, link)) + if not chosen: + raise RuntimeError( + "Unable to find installation candidates for {}".format(package) + ) + + return chosen + + def _get_links(self, package): # type: (Package) -> List[Link] + if not package.source_type: + if not self._pool.has_repository("pypi"): + repository = self._pool.repositories[0] + else: + repository = self._pool.repository("pypi") + else: + repository = self._pool.repository(package.source_reference) + + links = repository.find_links_for_package(package) + + hashes = [f["hash"] for f in package.files] + if not hashes: + return links + + selected_links = [] + for link in links: + if not link.hash: + selected_links.append(link) + continue + + h = link.hash_name + ":" + link.hash + if h not in hashes: + continue + + selected_links.append(link) + + return selected_links + + def _sort_key(self, package, link): # type: (Package, Link) -> Tuple + """ + Function to pass as the `key` argument to a call to sorted() to sort + InstallationCandidates by preference. + Returns a tuple such that tuples sorting as greater using Python's + default comparison operator are more preferred. + The preference is as follows: + First and foremost, candidates with allowed (matching) hashes are + always preferred over candidates without matching hashes. This is + because e.g. if the only candidate with an allowed hash is yanked, + we still want to use that candidate. + Second, excepting hash considerations, candidates that have been + yanked (in the sense of PEP 592) are always less preferred than + candidates that haven't been yanked. Then: + If not finding wheels, they are sorted by version only. + If finding wheels, then the sort order is by version, then: + 1. existing installs + 2. wheels ordered via Wheel.support_index_min(self._supported_tags) + 3. source archives + If prefer_binary was set, then all wheels are sorted above sources. + Note: it was considered to embed this logic into the Link + comparison operators, but then different sdist links + with the same version, would have to be considered equal + """ + support_num = len(self._env.supported_tags) + build_tag = () + binary_preference = 0 + if link.is_wheel: + wheel = Wheel(link.filename) + if not wheel.is_supported_by_environment(self._env): + raise RuntimeError( + "{} is not a supported wheel for this platform. It " + "can't be sorted.".format(wheel.filename) + ) + + # TODO: Binary preference + pri = -(wheel.get_minimum_supported_index(self._env.supported_tags)) + if wheel.build_tag is not None: + match = re.match(r"^(\d+)(.*)$", wheel.build_tag) + build_tag_groups = match.groups() + build_tag = (int(build_tag_groups[0]), build_tag_groups[1]) + else: # sdist + pri = -support_num + + has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package)) + + # TODO: Proper yank value + yank_value = 0 + + return ( + has_allowed_hash, + yank_value, + binary_preference, + package.version, + build_tag, + pri, + ) + + def _is_link_hash_allowed_for_package( + self, link, package + ): # type: (Link, Package) -> bool + if not link.hash: + return True + + h = link.hash_name + ":" + link.hash + + return h in {f["hash"] for f in package.files} diff --git a/poetry/installation/executor.py b/poetry/installation/executor.py new file mode 100644 index 00000000000..56a6c4cba21 --- /dev/null +++ b/poetry/installation/executor.py @@ -0,0 +1,654 @@ +# -*- coding: utf-8 -*- +from __future__ import division + +import itertools +import os +import threading + +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import wait +from subprocess import CalledProcessError + +from poetry.core.packages.file_dependency import FileDependency +from poetry.core.packages.utils.link import Link +from poetry.io.null_io import NullIO +from poetry.utils._compat import OrderedDict +from poetry.utils._compat import Path +from poetry.utils._compat import cpu_count +from poetry.utils._compat import decode +from poetry.utils.env import EnvCommandError +from poetry.utils.helpers import safe_rmtree + +from .authenticator import Authenticator +from .chef import Chef +from .chooser import Chooser +from .operations.install import Install +from .operations.operation import Operation +from .operations.uninstall import Uninstall +from .operations.update import Update + + +class Executor(object): + def __init__(self, env, pool, config, io, parallel=None): + self._env = env + self._io = io + self._dry_run = False + self._enabled = True + self._verbose = False + self._authenticator = Authenticator(config, self._io) + self._chef = Chef(config, self._env) + self._chooser = Chooser(pool, self._env) + + if parallel is None: + parallel = self.supports_fancy_output() + + if parallel: + # This should be directly handled by ThreadPoolExecutor + # however, on some systems the number of CPUs cannot be determined + # (it raises a NotImplementedError), so, in this case, we assume + # that the system only has one CPU. + try: + self._max_workers = cpu_count() + 4 + except NotImplementedError: + self._max_workers = 5 + else: + self._max_workers = 1 + + self._executor = ThreadPoolExecutor(max_workers=self._max_workers) + self._total_operations = 0 + self._executed_operations = 0 + self._executed = {"install": 0, "update": 0, "uninstall": 0} + self._skipped = {"install": 0, "update": 0, "uninstall": 0} + self._sections = OrderedDict() + self._lock = threading.Lock() + self._shutdown = False + + @property + def installations_count(self): # type: () -> int + return self._executed["install"] + + @property + def updates_count(self): # type: () -> int + return self._executed["update"] + + @property + def removals_count(self): # type: () -> int + return self._executed["uninstall"] + + def supports_fancy_output(self): # type: () -> bool + return self._io.supports_ansi() and not self._dry_run + + def disable(self): + self._enabled = False + + return self + + def dry_run(self, dry_run=True): + self._dry_run = dry_run + + return self + + def verbose(self, verbose=True): + self._verbose = verbose + + return self + + def execute(self, operations): # type: (Operation) -> int + self._total_operations = len(operations) + for job_type in self._executed: + self._executed[job_type] = 0 + self._skipped[job_type] = 0 + + if operations and (self._enabled or self._dry_run): + self._display_summary(operations) + + # We group operations by priority + groups = itertools.groupby(operations, key=lambda o: -o.priority) + self._sections = OrderedDict() + for _, group in groups: + tasks = [] + for operation in group: + if self._shutdown: + break + + tasks.append(self._executor.submit(self._execute_operation, operation)) + + try: + wait(tasks) + except KeyboardInterrupt: + self._shutdown = True + + if self._shutdown: + self._executor.shutdown(wait=True) + + break + + return self._shutdown + + def _write(self, operation, line): + if not self.supports_fancy_output() or not self._should_write_operation( + operation + ): + return + + if self._io.is_debug(): + self._lock.acquire() + section = self._sections[id(operation)] + section.write_line(line) + self._lock.release() + + return + + self._lock.acquire() + section = self._sections[id(operation)] + section.output.clear() + section.write(line) + self._lock.release() + + def _execute_operation(self, operation): + try: + if self.supports_fancy_output(): + if id(operation) not in self._sections: + if self._should_write_operation(operation): + self._lock.acquire() + self._sections[id(operation)] = self._io.section() + self._sections[id(operation)].write_line( + " • {message}: Pending...".format( + message=self.get_operation_message(operation), + ), + ) + self._lock.release() + else: + if self._should_write_operation(operation): + if not operation.skipped: + self._io.write_line( + " • {message}".format( + message=self.get_operation_message(operation), + ), + ) + else: + self._io.write_line( + " • {message}: " + "Skipped " + "for the following reason: " + "{reason}".format( + message=self.get_operation_message(operation), + reason=operation.skip_reason, + ) + ) + + try: + result = self._do_execute_operation(operation) + except EnvCommandError as e: + if e.e.returncode == -2: + result = -2 + else: + raise + + # If we have a result of -2 it means a KeyboardInterrupt + # in the any python subprocess, so we raise a KeyboardInterrupt + # error to be picked up by the error handler. + if result == -2: + raise KeyboardInterrupt + except Exception as e: + from clikit.ui.components.exception_trace import ExceptionTrace + + if not self.supports_fancy_output(): + io = self._io + else: + message = " • {message}: Failed".format( + message=self.get_operation_message(operation, error=True), + ) + self._write(operation, message) + io = self._sections.get(id(operation), self._io) + + self._lock.acquire() + + trace = ExceptionTrace(e) + trace.render(io) + io.write_line("") + + self._shutdown = True + self._lock.release() + except KeyboardInterrupt: + message = " • {message}: Cancelled".format( + message=self.get_operation_message(operation, warning=True), + ) + if not self.supports_fancy_output(): + self._io.write_line(message) + else: + self._write(operation, message) + + self._lock.acquire() + self._shutdown = True + self._lock.release() + + def _do_execute_operation(self, operation): + method = operation.job_type + + operation_message = self.get_operation_message(operation) + if operation.skipped: + if self.supports_fancy_output(): + self._write( + operation, + " • {message}: " + "Skipped " + "for the following reason: " + "{reason}".format( + message=operation_message, reason=operation.skip_reason, + ), + ) + + self._skipped[operation.job_type] += 1 + + return 0 + + if not self._enabled or self._dry_run: + self._io.write_line( + " • {message}".format( + message=operation_message, + ) + ) + + return 0 + + result = getattr(self, "_execute_{}".format(method))(operation) + + if result != 0: + return result + + message = " • {message}".format( + message=self.get_operation_message(operation, done=True), + ) + self._write(operation, message) + + self._increment_operations_count(operation, True) + + return result + + def _increment_operations_count(self, operation, executed): + self._lock.acquire() + if executed: + self._executed_operations += 1 + self._executed[operation.job_type] += 1 + else: + self._skipped[operation.job_type] += 1 + + self._lock.release() + + def run_pip(self, *args, **kwargs): # type: (...) -> int + try: + self._env.run("python", "-m", "pip", *args, **kwargs) + except EnvCommandError as e: + output = decode(e.e.output) + if ( + "KeyboardInterrupt" in output + or "ERROR: Operation cancelled by user" in output + ): + return -2 + + raise + + return 0 + + def get_operation_message(self, operation, done=False, error=False, warning=False): + base_tag = "fg=default" + operation_color = "c2" + source_operation_color = "c2" + package_color = "c1" + + if error: + operation_color = "error" + elif warning: + operation_color = "warning" + elif done: + operation_color = "success" + + if operation.skipped: + base_tag = "fg=default;options=dark" + operation_color += "_dark" + source_operation_color += "_dark" + package_color += "_dark" + + if operation.job_type == "install": + return "<{}>Installing <{}>{} (<{}>{})".format( + base_tag, + package_color, + operation.package.name, + package_color, + operation_color, + operation.package.full_pretty_version, + ) + + if operation.job_type == "uninstall": + return "<{}>Removing <{}>{} (<{}>{})".format( + base_tag, + package_color, + operation.package.name, + package_color, + operation_color, + operation.package.full_pretty_version, + ) + + if operation.job_type == "update": + return "<{}>Updating <{}>{} (<{}>{} -> <{}>{})".format( + base_tag, + package_color, + operation.initial_package.name, + package_color, + source_operation_color, + operation.initial_package.full_pretty_version, + source_operation_color, + operation_color, + operation.target_package.full_pretty_version, + ) + + return "" + + def _display_summary(self, operations): + installs = 0 + updates = 0 + uninstalls = 0 + skipped = 0 + for op in operations: + if op.skipped: + skipped += 1 + continue + + if op.job_type == "install": + installs += 1 + elif op.job_type == "update": + updates += 1 + elif op.job_type == "uninstall": + uninstalls += 1 + + if not installs and not updates and not uninstalls and not self._verbose: + self._io.write_line("") + self._io.write_line("No dependencies to install or update") + + return + + self._io.write_line("") + self._io.write_line( + "Package operations: " + "{} install{}, " + "{} update{}, " + "{} removal{}" + "{}".format( + installs, + "" if installs == 1 else "s", + updates, + "" if updates == 1 else "s", + uninstalls, + "" if uninstalls == 1 else "s", + ", {} skipped".format(skipped) + if skipped and self._verbose + else "", + ) + ) + self._io.write_line("") + + def _execute_install(self, operation): # type: (Install) -> None + return self._install(operation) + + def _execute_update(self, operation): # type: (Update) -> None + return self._update(operation) + + def _execute_uninstall(self, operation): # type: (Uninstall) -> None + message = " • {message}: Removing...".format( + message=self.get_operation_message(operation), + ) + self._write(operation, message) + + return self._remove(operation) + + def _install(self, operation): + package = operation.package + if package.source_type == "directory": + return self._install_directory(operation) + + if package.source_type == "git": + return self._install_git(operation) + + if package.source_type == "file": + archive = self._prepare_file(operation) + elif package.source_type == "url": + archive = self._download_link(operation, Link(package.source_url)) + else: + archive = self._download(operation) + + operation_message = self.get_operation_message(operation) + message = " • {message}: Installing...".format( + message=operation_message, + ) + self._write(operation, message) + + args = ["install", "--no-deps", str(archive)] + if operation.job_type == "update": + args.insert(2, "-U") + + return self.run_pip(*args) + + def _update(self, operation): + return self._install(operation) + + def _remove(self, operation): + package = operation.package + + # If we have a VCS package, remove its source directory + if package.source_type == "git": + src_dir = self._env.path / "src" / package.name + if src_dir.exists(): + safe_rmtree(str(src_dir)) + + try: + return self.run_pip("uninstall", package.name, "-y") + except CalledProcessError as e: + if "not installed" in str(e): + return 0 + + raise + + def _prepare_file(self, operation): + package = operation.package + + message = " • {message}: Preparing...".format( + message=self.get_operation_message(operation), + ) + self._write(operation, message) + + archive = Path(package.source_url) + if not Path(package.source_url).is_absolute() and package.root_dir: + archive = package.root_dir / archive + + archive = self._chef.prepare(archive) + + return archive + + def _install_directory(self, operation): + from poetry.factory import Factory + from poetry.utils.toml_file import TomlFile + + package = operation.package + operation_message = self.get_operation_message(operation) + + message = " • {message}: Building...".format( + message=operation_message, + ) + self._write(operation, message) + + if package.root_dir: + req = os.path.join(str(package.root_dir), package.source_url) + else: + req = os.path.realpath(package.source_url) + + args = ["install", "--no-deps", "-U"] + + pyproject = TomlFile(os.path.join(req, "pyproject.toml")) + + has_poetry = False + has_build_system = False + if pyproject.exists(): + pyproject_content = pyproject.read() + has_poetry = ( + "tool" in pyproject_content and "poetry" in pyproject_content["tool"] + ) + # Even if there is a build system specified + # some versions of pip (< 19.0.0) don't understand it + # so we need to check the version of pip to know + # if we can rely on the build system + pip_version = self._env.pip_version + pip_version_with_build_system_support = pip_version.__class__(19, 0, 0) + has_build_system = ( + "build-system" in pyproject_content + and pip_version >= pip_version_with_build_system_support + ) + + if has_poetry: + package_poetry = Factory().create_poetry(pyproject.parent) + if package.develop and not package_poetry.package.build_script: + from poetry.masonry.builders.editable import EditableBuilder + + # This is a Poetry package in editable mode + # we can use the EditableBuilder without going through pip + # to install it, unless it has a build script. + builder = EditableBuilder(package_poetry, self._env, NullIO()) + builder.build() + + return 0 + elif not has_build_system or package_poetry.package.build_script: + from poetry.core.masonry.builders.sdist import SdistBuilder + + # We need to rely on creating a temporary setup.py + # file since the version of pip does not support + # build-systems + # We also need it for non-PEP-517 packages + builder = SdistBuilder(package_poetry) + + with builder.setup_py(): + if package.develop: + args.append("-e") + + args.append(req) + + return self.run_pip(*args) + + if package.develop: + args.append("-e") + + args.append(req) + + return self.run_pip(*args) + + def _install_git(self, operation): + from poetry.core.vcs import Git + + package = operation.package + operation_message = self.get_operation_message(operation) + + message = " • {message}: Cloning...".format( + message=operation_message, + ) + self._write(operation, message) + + src_dir = self._env.path / "src" / package.name + if src_dir.exists(): + safe_rmtree(str(src_dir)) + + src_dir.parent.mkdir(exist_ok=True) + + git = Git() + git.clone(package.source_url, src_dir) + git.checkout(package.source_reference, src_dir) + + # Now we just need to install from the source directory + package.source_url = str(src_dir) + + return self._install_directory(operation) + + def _download(self, operation): # type: (Operation) -> Path + link = self._chooser.choose_for(operation.package) + + return self._download_link(operation, link) + + def _download_link(self, operation, link): + package = operation.package + + archive = self._chef.get_cached_archive_for_link(link) + if archive is link: + # No cached distributions was found, so we download and prepare it + try: + archive = self._download_archive(operation, link) + except BaseException: + cache_directory = self._chef.get_cache_directory_for_link(link) + cache_directory.joinpath(link.filename).unlink(missing_ok=True) + + raise + + # TODO: Check readability of the created archive + + if not link.is_wheel: + archive = self._chef.prepare(archive) + + if package.files: + archive_hash = "sha256:" + FileDependency(package.name, archive).hash() + if archive_hash not in {f["hash"] for f in package.files}: + raise RuntimeError( + "Invalid hash for {} using archive {}".format(package, archive.name) + ) + + return archive + + def _download_archive(self, operation, link): # type: (Operation, Link) -> Path + response = self._authenticator.request("get", link.url, stream=True) + wheel_size = response.headers.get("content-length") + operation_message = self.get_operation_message(operation) + message = " • {message}: Downloading...".format( + message=operation_message, + ) + progress = None + if self.supports_fancy_output(): + if wheel_size is None: + self._write(operation, message) + else: + from clikit.ui.components.progress_bar import ProgressBar + + progress = ProgressBar( + self._sections[id(operation)].output, max=int(wheel_size) + ) + progress.set_format(message + " %percent%%") + + if progress: + self._lock.acquire() + progress.start() + self._lock.release() + + done = 0 + archive = self._chef.get_cache_directory_for_link(link) / link.filename + archive.parent.mkdir(parents=True, exist_ok=True) + with archive.open("wb") as f: + for chunk in response.iter_content(chunk_size=4096): + if not chunk: + break + + done += len(chunk) + + if progress: + self._lock.acquire() + progress.set_progress(done) + self._lock.release() + + f.write(chunk) + + if progress: + self._lock.acquire() + progress.finish() + self._lock.release() + + return archive + + def _should_write_operation(self, operation): # type: (Operation) -> bool + if not operation.skipped: + return True + + return self._dry_run or self._verbose diff --git a/poetry/installation/installer.py b/poetry/installation/installer.py index c8df4204254..49015730615 100644 --- a/poetry/installation/installer.py +++ b/poetry/installation/installer.py @@ -1,16 +1,13 @@ from typing import List +from typing import Optional from typing import Union from clikit.api.io import IO +from poetry.config.config import Config from poetry.core.packages.project_package import ProjectPackage from poetry.io.null_io import NullIO from poetry.packages import Locker -from poetry.puzzle import Solver -from poetry.puzzle.operations import Install -from poetry.puzzle.operations import Uninstall -from poetry.puzzle.operations import Update -from poetry.puzzle.operations.operation import Operation from poetry.repositories import Pool from poetry.repositories import Repository from poetry.repositories.installed_repository import InstalledRepository @@ -18,6 +15,11 @@ from poetry.utils.helpers import canonicalize_name from .base_installer import BaseInstaller +from .executor import Executor +from .operations import Install +from .operations import Uninstall +from .operations import Update +from .operations.operation import Operation from .pip_installer import PipInstaller @@ -29,7 +31,9 @@ def __init__( package, # type: ProjectPackage locker, # type: Locker pool, # type: Pool - installed=None, # type: (Union[InstalledRepository, None]) + config, # type: Config + installed=None, # type: Union[InstalledRepository, None] + executor=None, # type: Optional[Executor] ): self._io = io self._env = env @@ -50,16 +54,31 @@ def __init__( self._extras = [] + if executor is None: + executor = Executor(self._env, self._pool, config, self._io) + + self._executor = executor + self._use_executor = False + self._installer = self._get_installer() if installed is None: installed = self._get_installed() self._installed_repository = installed + @property + def executor(self): + return self._executor + @property def installer(self): return self._installer + def set_package(self, package): # type: (ProjectPackage) -> Installer + self._package = package + + return self + def run(self): # Force update if there is no lock file present if not self._update and not self._locker.is_locked(): @@ -71,12 +90,12 @@ def run(self): self._execute_operations = False local_repo = Repository() - self._do_install(local_repo) - return 0 + return self._do_install(local_repo) def dry_run(self, dry_run=True): # type: (bool) -> Installer self._dry_run = dry_run + self._executor.dry_run(dry_run) return self @@ -93,6 +112,7 @@ def is_remove_untracked(self): # type: () -> bool def verbose(self, verbose=True): # type: (bool) -> Installer self._verbose = verbose + self._executor.verbose(verbose) return self @@ -128,6 +148,9 @@ def is_updating(self): # type: () -> bool def execute_operations(self, execute=True): # type: (bool) -> Installer self._execute_operations = execute + if not execute: + self._executor.disable() + return self def whitelist(self, packages): # type: (dict) -> Installer @@ -140,7 +163,14 @@ def extras(self, extras): # type: (list) -> Installer return self + def use_executor(self, use_executor=True): # type: (bool) -> Installer + self._use_executor = use_executor + + return self + def _do_install(self, local_repo): + from poetry.puzzle import Solver + locked_repository = Repository() if self._update: if self._locker.is_locked() and not self._lock: @@ -247,19 +277,30 @@ def _do_install(self, local_repo): # or optional and not requested, are dropped self._filter_operations(ops, local_repo) - self._io.write_line("") - # Execute operations - actual_ops = [op for op in ops if not op.skipped] - if not actual_ops and (self._execute_operations or self._dry_run): + return self._execute(ops) + + def _write_lock_file(self, repo): # type: (Repository) -> None + if self._update and self._write_lock: + updated_lock = self._locker.set_lock_data(self._package, repo.packages) + + if updated_lock: + self._io.write_line("") + self._io.write_line("Writing lock file") + + def _execute(self, operations): + if self._use_executor: + return self._executor.execute(operations) + + if not operations and (self._execute_operations or self._dry_run): self._io.write_line("No dependencies to install or update") - if actual_ops and (self._execute_operations or self._dry_run): + if operations and (self._execute_operations or self._dry_run): installs = 0 updates = 0 uninstalls = 0 skipped = 0 - for op in ops: + for op in operations: if op.skipped: skipped += 1 elif op.job_type == "install": @@ -289,18 +330,13 @@ def _do_install(self, local_repo): ) self._io.write_line("") - for op in ops: - self._execute(op) - def _write_lock_file(self, repo): # type: (Repository) -> None - if self._update and self._write_lock: - updated_lock = self._locker.set_lock_data(self._package, repo.packages) + for op in operations: + self._execute_operation(op) - if updated_lock: - self._io.write_line("") - self._io.write_line("Writing lock file") + return 0 - def _execute(self, operation): # type: (Operation) -> None + def _execute_operation(self, operation): # type: (Operation) -> None """ Execute a given operation. """ diff --git a/poetry/puzzle/operations/__init__.py b/poetry/installation/operations/__init__.py similarity index 100% rename from poetry/puzzle/operations/__init__.py rename to poetry/installation/operations/__init__.py diff --git a/poetry/puzzle/operations/install.py b/poetry/installation/operations/install.py similarity index 81% rename from poetry/puzzle/operations/install.py rename to poetry/installation/operations/install.py index 3d6344781fd..48097c7c6ce 100644 --- a/poetry/puzzle/operations/install.py +++ b/poetry/installation/operations/install.py @@ -2,8 +2,8 @@ class Install(Operation): - def __init__(self, package, reason=None): - super(Install, self).__init__(reason) + def __init__(self, package, reason=None, priority=0): + super(Install, self).__init__(reason, priority=priority) self._package = package diff --git a/poetry/puzzle/operations/operation.py b/poetry/installation/operations/operation.py similarity index 81% rename from poetry/puzzle/operations/operation.py rename to poetry/installation/operations/operation.py index 9b14c4a248e..0c72cc8c044 100644 --- a/poetry/puzzle/operations/operation.py +++ b/poetry/installation/operations/operation.py @@ -4,11 +4,14 @@ class Operation(object): - def __init__(self, reason=None): # type: (Union[str, None]) -> None + def __init__( + self, reason=None, priority=0 + ): # type: (Union[str, None], int) -> None self._reason = reason self._skipped = False self._skip_reason = None + self._priority = priority @property def job_type(self): # type: () -> str @@ -26,6 +29,10 @@ def skipped(self): # type: () -> bool def skip_reason(self): # type: () -> Union[str, None] return self._skip_reason + @property + def priority(self): # type: () -> int + return self._priority + @property def package(self): raise NotImplementedError() diff --git a/poetry/puzzle/operations/uninstall.py b/poetry/installation/operations/uninstall.py similarity index 79% rename from poetry/puzzle/operations/uninstall.py rename to poetry/installation/operations/uninstall.py index 0a8c75747ee..b7e40bc606e 100644 --- a/poetry/puzzle/operations/uninstall.py +++ b/poetry/installation/operations/uninstall.py @@ -2,8 +2,8 @@ class Uninstall(Operation): - def __init__(self, package, reason=None): - super(Uninstall, self).__init__(reason) + def __init__(self, package, reason=None, priority=float("inf")): + super(Uninstall, self).__init__(reason, priority=priority) self._package = package diff --git a/poetry/puzzle/operations/update.py b/poetry/installation/operations/update.py similarity index 88% rename from poetry/puzzle/operations/update.py rename to poetry/installation/operations/update.py index c52e9e8046f..87803fd7a23 100644 --- a/poetry/puzzle/operations/update.py +++ b/poetry/installation/operations/update.py @@ -2,11 +2,11 @@ class Update(Operation): - def __init__(self, initial, target, reason=None): + def __init__(self, initial, target, reason=None, priority=0): self._initial_package = initial self._target_package = target - super(Update, self).__init__(reason) + super(Update, self).__init__(reason, priority=priority) @property def initial_package(self): diff --git a/poetry/puzzle/solver.py b/poetry/puzzle/solver.py index a404971f0fe..99b8fccdb03 100644 --- a/poetry/puzzle/solver.py +++ b/poetry/puzzle/solver.py @@ -9,6 +9,10 @@ from poetry.core.packages import Package from poetry.core.packages.project_package import ProjectPackage +from poetry.installation.operations import Install +from poetry.installation.operations import Uninstall +from poetry.installation.operations import Update +from poetry.installation.operations.operation import Operation from poetry.mixology import resolve_version from poetry.mixology.failure import SolveFailure from poetry.packages import DependencyPackage @@ -18,10 +22,6 @@ from .exceptions import OverrideNeeded from .exceptions import SolverProblemError -from .operations import Install -from .operations import Uninstall -from .operations import Update -from .operations.operation import Operation from .provider import Provider @@ -72,7 +72,7 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation] ) operations = [] - for package in packages: + for i, package in enumerate(packages): installed = False for pkg in self._installed.packages: if package.name == pkg.name: @@ -107,23 +107,27 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation] package.source_reference ) ): - operations.append(Update(pkg, package)) + operations.append(Update(pkg, package, priority=depths[i])) else: operations.append( Install(package).skip("Already installed") ) elif package.version != pkg.version: # Checking version - operations.append(Update(pkg, package)) + operations.append(Update(pkg, package, priority=depths[i])) elif pkg.source_type and package.source_type != pkg.source_type: - operations.append(Update(pkg, package)) + operations.append(Update(pkg, package, priority=depths[i])) else: - operations.append(Install(package).skip("Already installed")) + operations.append( + Install(package, priority=depths[i]).skip( + "Already installed" + ) + ) break if not installed: - operations.append(Install(package)) + operations.append(Install(package, priority=depths[i])) # Checking for removals for pkg in self._locked.packages: @@ -159,15 +163,7 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation] operations.append(Uninstall(installed)) return sorted( - operations, - key=lambda o: ( - o.job_type == "uninstall", - # Packages to be uninstalled have no depth so we default to 0 - # since it actually doesn't matter since removals are always on top. - -depths[packages.index(o.package)] if o.job_type != "uninstall" else 0, - o.package.name, - o.package.version, - ), + operations, key=lambda o: (-o.priority, o.package.name, o.package.version,), ) def solve_in_compatibility_mode(self, overrides, use_latest=None): diff --git a/poetry/repositories/installed_repository.py b/poetry/repositories/installed_repository.py index 27feacffad7..296b5c9ecec 100644 --- a/poetry/repositories/installed_repository.py +++ b/poetry/repositories/installed_repository.py @@ -90,6 +90,7 @@ def load(cls, env): # type: (Env) -> InstalledRepository # TODO: handle multiple source directories? package.source_type = "directory" package.source_url = paths.pop().as_posix() + continue src_path = env.path / "src" diff --git a/poetry/repositories/legacy_repository.py b/poetry/repositories/legacy_repository.py index 9c5553be66b..299ebd663dd 100644 --- a/poetry/repositories/legacy_repository.py +++ b/poetry/repositories/legacy_repository.py @@ -304,6 +304,13 @@ def package(self, name, version, extras=None): # type: (...) -> Package return package + def find_links_for_package(self, package): + page = self._get("/{}/".format(package.name.replace(".", "-"))) + if page is None: + return [] + + return list(page.links_for_version(package.version)) + def _get_release_info(self, name, version): # type: (str, str) -> dict page = self._get("/{}/".format(canonicalize_name(name).replace(".", "-"))) if page is None: diff --git a/poetry/repositories/pool.py b/poetry/repositories/pool.py index 35904a27b8a..952890fcab3 100644 --- a/poetry/repositories/pool.py +++ b/poetry/repositories/pool.py @@ -33,7 +33,15 @@ def repositories(self): # type: () -> List[Repository] def has_default(self): # type: () -> bool return self._default + def has_repository(self, name): # type: (str) -> bool + name = name.lower() if name is not None else None + + return name in self._lookup + def repository(self, name): # type: (str) -> Repository + if name is not None: + name = name.lower() + if name in self._lookup: return self._repositories[self._lookup[name]] @@ -45,6 +53,9 @@ def add_repository( """ Adds a repository to the pool. """ + repository_name = ( + repository.name.lower() if repository.name is not None else None + ) if default: if self.has_default(): raise ValueError("Only one repository can be the default") @@ -57,17 +68,17 @@ def add_repository( if self._secondary_start_idx is not None: self._secondary_start_idx += 1 - self._lookup[repository.name] = 0 + self._lookup[repository_name] = 0 elif secondary: if self._secondary_start_idx is None: self._secondary_start_idx = len(self._repositories) self._repositories.append(repository) - self._lookup[repository.name] = len(self._repositories) - 1 + self._lookup[repository_name] = len(self._repositories) - 1 else: if self._secondary_start_idx is None: self._repositories.append(repository) - self._lookup[repository.name] = len(self._repositories) - 1 + self._lookup[repository_name] = len(self._repositories) - 1 else: self._repositories.insert(self._secondary_start_idx, repository) @@ -77,12 +88,15 @@ def add_repository( self._lookup[name] += 1 - self._lookup[repository.name] = self._secondary_start_idx + self._lookup[repository_name] = self._secondary_start_idx self._secondary_start_idx += 1 return self def remove_repository(self, repository_name): # type: (str) -> Pool + if repository_name is not None: + repository_name = repository_name.lower() + idx = self._lookup.get(repository_name) if idx is not None: del self._repositories[idx] @@ -95,6 +109,9 @@ def has_package(self, package): def package( self, name, version, extras=None, repository=None ): # type: (str, str, List[str], str) -> "Package" + if repository is not None: + repository = repository.lower() + if ( repository is not None and repository not in self._lookup @@ -104,9 +121,7 @@ def package( if repository is not None and not self._ignore_repository_names: try: - return self._repositories[self._lookup[repository]].package( - name, version, extras=extras - ) + return self.repository(repository).package(name, version, extras=extras) except PackageNotFound: pass else: @@ -131,6 +146,9 @@ def find_packages( allow_prereleases=False, repository=None, ): + if repository is not None: + repository = repository.lower() + if ( repository is not None and repository not in self._lookup @@ -139,7 +157,7 @@ def find_packages( raise ValueError('Repository "{}" does not exist.'.format(repository)) if repository is not None and not self._ignore_repository_names: - return self._repositories[self._lookup[repository]].find_packages( + return self.repository(repository).find_packages( name, constraint, extras=extras, allow_prereleases=allow_prereleases ) diff --git a/poetry/repositories/pypi_repository.py b/poetry/repositories/pypi_repository.py index e53a2af555e..727dc184977 100644 --- a/poetry/repositories/pypi_repository.py +++ b/poetry/repositories/pypi_repository.py @@ -241,6 +241,18 @@ def get_release_info(self, name, version): # type: (str, str) -> PackageInfo return PackageInfo.load(cached) + def find_links_for_package(self, package): + json_data = self._get("pypi/{}/{}/json".format(package.name, package.version)) + if json_data is None: + return [] + + links = [] + for url in json_data["urls"]: + h = "sha256={}".format(url["digests"]["sha256"]) + links.append(Link(url["url"] + "#" + h)) + + return links + def _get_release_info(self, name, version): # type: (str, str) -> dict self._log("Getting info for {} ({}) from PyPI".format(name, version), "debug") diff --git a/poetry/repositories/repository.py b/poetry/repositories/repository.py index 00b43207d62..cea554ef47d 100644 --- a/poetry/repositories/repository.py +++ b/poetry/repositories/repository.py @@ -6,10 +6,10 @@ class Repository(BaseRepository): - def __init__(self, packages=None): + def __init__(self, packages=None, name=None): super(Repository, self).__init__() - self._name = None + self._name = name if packages is None: packages = [] @@ -115,6 +115,9 @@ def remove_package(self, package): if index is not None: del self._packages[index] + def find_links_for_package(self, package): + return [] + def search(self, query): results = [] diff --git a/poetry/utils/_compat.py b/poetry/utils/_compat.py index 6f8d5b4c372..79457967144 100644 --- a/poetry/utils/_compat.py +++ b/poetry/utils/_compat.py @@ -23,6 +23,11 @@ except ImportError: import urlparse +try: + from os import cpu_count +except ImportError: # Python 2 + from multiprocessing import cpu_count + try: # Python 2 long = long unicode = unicode @@ -50,6 +55,14 @@ shell_quote = shlex.quote +if PY34: + from importlib.machinery import EXTENSION_SUFFIXES +else: + from imp import get_suffixes + + EXTENSION_SUFFIXES = [suffix[0] for suffix in get_suffixes()] + + if PY35: from pathlib import Path else: diff --git a/poetry/utils/env.py b/poetry/utils/env.py index a40b00baede..f06e0689c3d 100644 --- a/poetry/utils/env.py +++ b/poetry/utils/env.py @@ -7,7 +7,7 @@ import shutil import sys import sysconfig -import warnings +import textwrap from contextlib import contextmanager from typing import Any @@ -20,6 +20,12 @@ from clikit.api.io import IO +import packaging.tags + +from packaging.tags import Tag +from packaging.tags import interpreter_name +from packaging.tags import interpreter_version +from packaging.tags import sys_tags from poetry.core.semver import parse_constraint from poetry.core.semver.version import Version from poetry.core.version.markers import BaseMarker @@ -39,6 +45,36 @@ import os import platform import sys +import sysconfig + +INTERPRETER_SHORT_NAMES = { + "python": "py", + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +def interpreter_version(): + version = sysconfig.get_config_var("interpreter_version") + if version: + version = str(version) + else: + version = _version_nodot(sys.version_info[:2]) + + return version + + +def _version_nodot(version): + # type: (PythonVersion) -> str + if any(v >= 10 for v in version): + sep = "_" + else: + sep = "" + + return sep.join(map(str, version)) + if hasattr(sys, "implementation"): info = sys.implementation.version @@ -50,7 +86,7 @@ implementation_name = sys.implementation.name else: iver = "0" - implementation_name = "" + implementation_name = platform.python_implementation().lower() env = { "implementation_name": implementation_name, @@ -65,6 +101,9 @@ "python_version": platform.python_version()[:3], "sys_platform": sys.platform, "version_info": tuple(sys.version_info), + # Extra information + "interpreter_name": INTERPRETER_SHORT_NAMES.get(implementation_name, implementation_name), + "interpreter_version": interpreter_version(), } print(json.dumps(env)) @@ -82,12 +121,6 @@ print(sys.prefix) """ -GET_CONFIG_VAR = """\ -import sysconfig - -print(sysconfig.get_config_var("{config_var}")), -""" - GET_PYTHON_VERSION = """\ import sys @@ -742,6 +775,7 @@ def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None self._pip_version = None self._site_packages = None self._paths = None + self._supported_tags = None @property def path(self): # type: () -> Path @@ -813,6 +847,13 @@ def paths(self): # type: () -> Dict[str, str] return self._paths + @property + def supported_tags(self): # type: () -> List[Tag] + if self._supported_tags is None: + self._supported_tags = self.get_supported_tags() + + return self._supported_tags + @classmethod def get_base_prefix(cls): # type: () -> Path if hasattr(sys, "real_prefix"): @@ -835,7 +876,7 @@ def get_marker_env(self): # type: () -> Dict[str, Any] def get_pip_command(self): # type: () -> List[str] raise NotImplementedError() - def config_var(self, var): # type: (str) -> Any + def get_supported_tags(self): # type: () -> List[Tag] raise NotImplementedError() def get_pip_version(self): # type: () -> Version @@ -987,6 +1028,9 @@ def get_paths(self): # type: () -> Dict[str, str] return paths + def get_supported_tags(self): # type: () -> List[Tag] + return list(sys_tags()) + def get_marker_env(self): # type: () -> Dict[str, Any] if hasattr(sys, "implementation"): info = sys.implementation.version @@ -1015,16 +1059,11 @@ def get_marker_env(self): # type: () -> Dict[str, Any] ), "sys_platform": sys.platform, "version_info": sys.version_info, + # Extra information + "interpreter_name": interpreter_name(), + "interpreter_version": interpreter_version(), } - def config_var(self, var): # type: (str) -> Any - try: - return sysconfig.get_config_var(var) - except IOError as e: - warnings.warn("{0}".format(e), RuntimeWarning) - - return - def get_pip_version(self): # type: () -> Version from pip import __version__ @@ -1068,29 +1107,41 @@ def get_pip_command(self): # type: () -> List[str] # so assume that we have a functional pip return [self._bin("pip")] + def get_supported_tags(self): # type: () -> List[Tag] + file_path = Path(packaging.tags.__file__) + if file_path.suffix == ".pyc": + # Python 2 + file_path = file_path.with_suffix(".py") + + with file_path.open(encoding="utf-8") as f: + script = decode(f.read()) + + script = script.replace( + "from ._typing import TYPE_CHECKING, cast", + "TYPE_CHECKING = False\ncast = lambda type_, value: value", + ) + script = script.replace( + "from ._typing import MYPY_CHECK_RUNNING, cast", + "MYPY_CHECK_RUNNING = False\ncast = lambda type_, value: value", + ) + + script += textwrap.dedent( + """ + import json + + print(json.dumps([(t.interpreter, t.abi, t.platform) for t in sys_tags()])) + """ + ) + + output = self.run("python", "-", input_=script) + + return [Tag(*t) for t in json.loads(output)] + def get_marker_env(self): # type: () -> Dict[str, Any] output = self.run("python", "-", input_=GET_ENVIRONMENT_INFO) return json.loads(output) - def config_var(self, var): # type: (str) -> Any - try: - value = self.run( - "python", "-", input_=GET_CONFIG_VAR.format(config_var=var) - ).strip() - except EnvCommandError as e: - warnings.warn("{0}".format(e), RuntimeWarning) - return None - - if value == "None": - value = None - elif value == "1": - value = 1 - elif value == "0": - value = 0 - - return value - def get_pip_version(self): # type: () -> Version output = self.run_pip("--version").strip() m = re.match("pip (.+?)(?: from .+)?$", output) @@ -1188,7 +1239,7 @@ def __init__( pip_version="19.1", sys_path=None, marker_env=None, - config_vars=None, + supported_tags=None, **kwargs ): super(MockEnv, self).__init__(**kwargs) @@ -1201,15 +1252,7 @@ def __init__( self._pip_version = Version.parse(pip_version) self._sys_path = sys_path self._mock_marker_env = marker_env - self._config_vars = config_vars - - @property - def version_info(self): # type: () -> Tuple[int] - return self._version_info - - @property - def python_implementation(self): # type: () -> str - return self._python_implementation + self._supported_tags = supported_tags @property def platform(self): # type: () -> str @@ -1239,17 +1282,12 @@ def get_marker_env(self): # type: () -> Dict[str, Any] marker_env["version_info"] = self._version_info marker_env["python_version"] = ".".join(str(v) for v in self._version_info[:2]) marker_env["sys_platform"] = self._platform + marker_env["interpreter_name"] = self._python_implementation.lower() + marker_env["interpreter_version"] = "cp" + "".join( + str(v) for v in self._version_info[:2] + ) return marker_env def is_venv(self): # type: () -> bool return self._is_venv - - def config_var(self, var): # type: (str) -> Any - if self._config_vars is None: - return super().config_var(var) - else: - try: - return self._config_vars[var] - except KeyError: - return None diff --git a/pyproject.toml b/pyproject.toml index 9073ff58266..a7969afbe64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,12 +37,15 @@ html5lib = "^1.0" shellingham = "^1.1" tomlkit = "^0.5.11" pexpect = "^4.7.0" +packaging = "^20.4" # The typing module is not in the stdlib in Python 2.7 typing = { version = "^3.6", python = "~2.7" } # Use pathlib2 for Python 2.7 pathlib2 = { version = "^2.3", python = "~2.7" } +# Use futures on Python 2.7 +futures = { version = "^3.3.0", python = "~2.7" } # Use glob2 for Python 2.7 and 3.4 glob2 = { version = "^0.6", python = "~2.7" } # Use virtualenv for Python 2.7 since venv does not exist diff --git a/tests/conftest.py b/tests/conftest.py index 0dc637ded13..db5f96250f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,11 +104,13 @@ def git_mock(mocker): @pytest.fixture def http(): - httpretty.enable() + httpretty.reset() + httpretty.enable(allow_net_connect=False) yield httpretty - httpretty.disable() + httpretty.activate() + httpretty.reset() @pytest.fixture diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 866715163ae..9572427c81b 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -1,24 +1,800 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + import sys -import pytest +import pytest + +from cleo.testers import CommandTester + +from poetry.core.semver import Version +from poetry.installation.installer import Installer +from poetry.repositories.legacy_repository import LegacyRepository +from poetry.utils._compat import Path +from tests.helpers import get_dependency +from tests.helpers import get_package + + +@pytest.fixture() +def tester(app, poetry, config, executor, env): + tester = CommandTester(app.find("add")) + + executor._io = tester.io + + installer = Installer( + tester.io, + env, + poetry.package, + poetry.locker, + poetry.pool, + config, + executor=executor, + ) + installer.use_executor(True) + tester._command.set_installer(installer) + tester._command.set_env(env) + + return tester + + +@pytest.fixture() +def old_tester(tester): + tester._command.installer.use_executor(False) + + return tester + + +def test_add_no_constraint(app, repo, tester): + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("cachy") + + expected = """\ +Using version ^0.2.0 for cachy + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "cachy" in content["dependencies"] + assert content["dependencies"]["cachy"] == "^0.2.0" + + +def test_add_equal_constraint(app, repo, tester): + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("cachy==0.1.0") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing cachy (0.1.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + +def test_add_greater_constraint(app, repo, tester): + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("cachy>=0.1.0") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + +def test_add_constraint_with_extras(app, repo, tester): + cachy1 = get_package("cachy", "0.1.0") + cachy1.extras = {"msgpack": [get_dependency("msgpack-python")]} + msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) + cachy1.requires = [msgpack_dep] + + repo.add_package(get_package("cachy", "0.2.0")) + repo.add_package(cachy1) + repo.add_package(get_package("msgpack-python", "0.5.3")) + + tester.execute("cachy[msgpack]>=0.1.0,<0.2.0") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing msgpack-python (0.5.3) + • Installing cachy (0.1.0) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + +def test_add_constraint_dependencies(app, repo, tester): + cachy2 = get_package("cachy", "0.2.0") + msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6") + cachy2.requires = [msgpack_dep] + + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(cachy2) + repo.add_package(get_package("msgpack-python", "0.5.3")) + + tester.execute("cachy=0.2.0") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing msgpack-python (0.5.3) + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + +def test_add_git_constraint(app, repo, tester, tmp_venv): + tester._command.set_env(tmp_venv) + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cleo", "0.6.5")) + + tester.execute("git+https://github.com/demo/demo.git") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.2 9cf87a2) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "git": "https://github.com/demo/demo.git" + } + + +def test_add_git_constraint_with_poetry(app, repo, tester, tmp_venv): + tester._command.set_env(tmp_venv) + + repo.add_package(get_package("pendulum", "1.4.4")) + + tester.execute("git+https://github.com/demo/pyproject-demo.git") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.2 9cf87a2) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + +def test_add_git_constraint_with_extras(app, repo, tester, tmp_venv): + tester._command.set_env(tmp_venv) + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cleo", "0.6.5")) + repo.add_package(get_package("tomlkit", "0.5.5")) + + tester.execute("git+https://github.com/demo/demo.git[foo,bar]") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 4 installs, 0 updates, 0 removals + + • Installing cleo (0.6.5) + • Installing pendulum (1.4.4) + • Installing tomlkit (0.5.5) + • Installing demo (0.1.2 9cf87a2) +""" + + assert expected == tester.io.fetch_output() + assert 4 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "git": "https://github.com/demo/demo.git", + "extras": ["foo", "bar"], + } + + +def test_add_git_ssh_constraint(app, repo, tester, tmp_venv): + tester._command.set_env(tmp_venv) + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cleo", "0.6.5")) + + tester.execute("git+ssh://git@github.com/demo/demo.git@develop") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.2 9cf87a2) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "git": "ssh://git@github.com/demo/demo.git", + "rev": "develop", + } + + +def test_add_directory_constraint(app, repo, tester, mocker): + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__) / ".." + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cleo", "0.6.5")) + + tester.execute("../git/github.com/demo/demo") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.2 ../git/github.com/demo/demo) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == {"path": "../git/github.com/demo/demo"} + + +def test_add_directory_with_poetry(app, repo, tester, mocker): + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__) / ".." + + repo.add_package(get_package("pendulum", "1.4.4")) + + tester.execute("../git/github.com/demo/pyproject-demo") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.2 ../git/github.com/demo/pyproject-demo) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + +def test_add_file_constraint_wheel(app, repo, tester, mocker, poetry): + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = poetry.file.parent + + repo.add_package(get_package("pendulum", "1.4.4")) + + tester.execute("../distributions/demo-0.1.0-py2.py3-none-any.whl") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.0 ../distributions/demo-0.1.0-py2.py3-none-any.whl) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "path": "../distributions/demo-0.1.0-py2.py3-none-any.whl" + } + + +def test_add_file_constraint_sdist(app, repo, tester, mocker): + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__) / ".." + + repo.add_package(get_package("pendulum", "1.4.4")) + + tester.execute("../distributions/demo-0.1.0.tar.gz") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.0 ../distributions/demo-0.1.0.tar.gz) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "path": "../distributions/demo-0.1.0.tar.gz" + } + + +def test_add_constraint_with_extras_option(app, repo, tester): + cachy2 = get_package("cachy", "0.2.0") + cachy2.extras = {"msgpack": [get_dependency("msgpack-python")]} + msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) + cachy2.requires = [msgpack_dep] + + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(cachy2) + repo.add_package(get_package("msgpack-python", "0.5.3")) + + tester.execute("cachy=0.2.0 --extras msgpack") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing msgpack-python (0.5.3) + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "cachy" in content["dependencies"] + assert content["dependencies"]["cachy"] == { + "version": "0.2.0", + "extras": ["msgpack"], + } + + +def test_add_url_constraint_wheel(app, repo, tester, mocker): + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__) / ".." + + repo.add_package(get_package("pendulum", "1.4.4")) + + tester.execute( + "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" + ) + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 2 installs, 0 updates, 0 removals + + • Installing pendulum (1.4.4) + • Installing demo (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) +""" + + assert expected == tester.io.fetch_output() + assert 2 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "url": "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" + } + + +def test_add_url_constraint_wheel_with_extras(app, repo, tester, mocker): + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cleo", "0.6.5")) + repo.add_package(get_package("tomlkit", "0.5.5")) + + tester.execute( + "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl[foo,bar]" + ) + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 4 installs, 0 updates, 0 removals + + • Installing cleo (0.6.5) + • Installing pendulum (1.4.4) + • Installing tomlkit (0.5.5) + • Installing demo (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) +""" + + assert expected == tester.io.fetch_output() + assert 4 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "demo" in content["dependencies"] + assert content["dependencies"]["demo"] == { + "url": "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl", + "extras": ["foo", "bar"], + } + + +def test_add_constraint_with_python(app, repo, tester): + cachy2 = get_package("cachy", "0.2.0") + + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(cachy2) + + tester.execute("cachy=0.2.0 --python >=2.7") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "cachy" in content["dependencies"] + assert content["dependencies"]["cachy"] == {"version": "0.2.0", "python": ">=2.7"} + + +def test_add_constraint_with_platform(app, repo, tester, env): + platform = sys.platform + env._platform = platform + + cachy2 = get_package("cachy", "0.2.0") + + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(cachy2) + + tester.execute("cachy=0.2.0 --platform {} -vvv".format(platform)) + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "cachy" in content["dependencies"] + assert content["dependencies"]["cachy"] == { + "version": "0.2.0", + "platform": platform, + } + + +def test_add_constraint_with_source(app, poetry, tester): + repo = LegacyRepository(name="my-index", url="https://my-index.fake") + repo.add_package(get_package("cachy", "0.2.0")) + repo._cache.store("matches").put("cachy:0.2.0", [Version.parse("0.2.0")], 5) + + poetry.pool.add_repository(repo) + + tester.execute("cachy=0.2.0 --source my-index") + + expected = """\ + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "cachy" in content["dependencies"] + assert content["dependencies"]["cachy"] == { + "version": "0.2.0", + "source": "my-index", + } + + +def test_add_constraint_with_source_that_does_not_exist(app, tester): + with pytest.raises(ValueError) as e: + tester.execute("foo --source i-dont-exist") + + assert 'Repository "i-dont-exist" does not exist.' == str(e.value) + + +def test_add_constraint_not_found_with_source(app, poetry, mocker, tester): + repo = LegacyRepository(name="my-index", url="https://my-index.fake") + mocker.patch.object(repo, "find_packages", return_value=[]) + + poetry.pool.add_repository(repo) + + pypi = poetry.pool.repositories[0] + pypi.add_package(get_package("cachy", "0.2.0")) + + with pytest.raises(ValueError) as e: + tester.execute("cachy --source my-index") + + assert "Could not find a matching version of package cachy" == str(e.value) + + +def test_add_to_section_that_does_no_exist_yet(app, repo, tester): + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(get_package("cachy", "0.2.0")) + + tester.execute("cachy --dev") + + expected = """\ +Using version ^0.2.0 for cachy + +Updating dependencies +Resolving dependencies... + +Writing lock file -from cleo.testers import CommandTester +Package operations: 1 install, 0 updates, 0 removals -from poetry.core.semver import Version -from poetry.repositories.legacy_repository import LegacyRepository -from poetry.utils._compat import Path -from tests.helpers import get_dependency -from tests.helpers import get_package + • Installing cachy (0.2.0) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + content = app.poetry.file.read()["tool"]["poetry"] -def test_add_no_constraint(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) + assert "cachy" in content["dev-dependencies"] + assert content["dev-dependencies"]["cachy"] == "^0.2.0" - repo.add_package(get_package("cachy", "0.1.0")) + +def test_add_should_not_select_prereleases(app, repo, tester): + repo.add_package(get_package("pyyaml", "3.13")) + repo.add_package(get_package("pyyaml", "4.2b2")) + + tester.execute("pyyaml") + + expected = """\ +Using version ^3.13 for pyyaml + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing pyyaml (3.13) +""" + + assert expected == tester.io.fetch_output() + assert 1 == tester._command.installer.executor.installations_count + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "pyyaml" in content["dependencies"] + assert content["dependencies"]["pyyaml"] == "^3.13" + + +def test_add_should_display_an_error_when_adding_existing_package_with_no_constraint( + app, repo, tester +): + content = app.poetry.file.read() + content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + app.poetry.file.write(content) + + repo.add_package(get_package("foo", "1.1.2")) + + with pytest.raises(ValueError) as e: + tester.execute("foo") + + assert "Package foo is already present" == str(e.value) + + +def test_add_should_work_when_adding_existing_package_with_latest_constraint( + app, repo, tester +): + content = app.poetry.file.read() + content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" + app.poetry.file.write(content) + + repo.add_package(get_package("foo", "1.1.2")) + + tester.execute("foo@latest") + + expected = """\ +Using version ^1.1.2 for foo + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing foo (1.1.2) +""" + + assert expected in tester.io.fetch_output() + + content = app.poetry.file.read()["tool"]["poetry"] + + assert "foo" in content["dependencies"] + assert content["dependencies"]["foo"] == "^1.1.2" + + +def test_add_chooses_prerelease_if_only_prereleases_are_available(app, repo, tester): + repo.add_package(get_package("foo", "1.2.3b0")) + repo.add_package(get_package("foo", "1.2.3b1")) + + tester.execute("foo") + + expected = """\ +Using version ^1.2.3-beta.1 for foo + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing foo (1.2.3b1) +""" + + assert expected in tester.io.fetch_output() + + +def test_add_prefers_stable_releases(app, repo, tester): + repo.add_package(get_package("foo", "1.2.3")) + repo.add_package(get_package("foo", "1.2.4b1")) + + tester.execute("foo") + + expected = """\ +Using version ^1.2.3 for foo + +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 1 install, 0 updates, 0 removals + + • Installing foo (1.2.3) +""" + + assert expected in tester.io.fetch_output() + + +def test_add_with_lock(app, repo, tester): repo.add_package(get_package("cachy", "0.2.0")) - tester.execute("cachy") + tester.execute("cachy --lock") expected = """\ Using version ^0.2.0 for cachy @@ -27,14 +803,31 @@ def test_add_no_constraint(app, repo, installer): Resolving dependencies... Writing lock file +""" + + assert expected == tester.io.fetch_output() + + +def test_add_no_constraint_old_installer(app, repo, installer, old_tester): + repo.add_package(get_package("cachy", "0.1.0")) + repo.add_package(get_package("cachy", "0.2.0")) + + old_tester.execute("cachy") + expected = """\ +Using version ^0.2.0 for cachy + +Updating dependencies +Resolving dependencies... + +Writing lock file Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 @@ -44,14 +837,11 @@ def test_add_no_constraint(app, repo, installer): assert content["dependencies"]["cachy"] == "^0.2.0" -def test_add_equal_constraint(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_equal_constraint_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) - tester.execute("cachy==0.1.0") + old_tester.execute("cachy==0.1.0") expected = """\ @@ -60,25 +850,21 @@ def test_add_equal_constraint(app, repo, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.1.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 -def test_add_greater_constraint(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_greater_constraint_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) - tester.execute("cachy>=0.1.0") + old_tester.execute("cachy>=0.1.0") expected = """\ @@ -87,21 +873,17 @@ def test_add_greater_constraint(app, repo, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 -def test_add_constraint_with_extras(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_constraint_with_extras_old_installer(app, repo, installer, old_tester): cachy1 = get_package("cachy", "0.1.0") cachy1.extras = {"msgpack": [get_dependency("msgpack-python")]} msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) @@ -111,7 +893,7 @@ def test_add_constraint_with_extras(app, repo, installer): repo.add_package(cachy1) repo.add_package(get_package("msgpack-python", "0.5.3")) - tester.execute("cachy[msgpack]>=0.1.0,<0.2.0") + old_tester.execute("cachy[msgpack]>=0.1.0,<0.2.0") expected = """\ @@ -120,22 +902,18 @@ def test_add_constraint_with_extras(app, repo, installer): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.1.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 -def test_add_constraint_dependencies(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_constraint_dependencies_old_installer(app, repo, installer, old_tester): cachy2 = get_package("cachy", "0.2.0") msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6") cachy2.requires = [msgpack_dep] @@ -144,7 +922,7 @@ def test_add_constraint_dependencies(app, repo, installer): repo.add_package(cachy2) repo.add_package(get_package("msgpack-python", "0.5.3")) - tester.execute("cachy=0.2.0") + old_tester.execute("cachy=0.2.0") expected = """\ @@ -153,26 +931,22 @@ def test_add_constraint_dependencies(app, repo, installer): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 -def test_add_git_constraint(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_git_constraint_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) - tester.execute("git+https://github.com/demo/demo.git") + old_tester.execute("git+https://github.com/demo/demo.git") expected = """\ @@ -181,14 +955,13 @@ def test_add_git_constraint(app, repo, installer): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -200,13 +973,10 @@ def test_add_git_constraint(app, repo, installer): } -def test_add_git_constraint_with_poetry(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_git_constraint_with_poetry_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("pendulum", "1.4.4")) - tester.execute("git+https://github.com/demo/pyproject-demo.git") + old_tester.execute("git+https://github.com/demo/pyproject-demo.git") expected = """\ @@ -215,27 +985,23 @@ def test_add_git_constraint_with_poetry(app, repo, installer): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 -def test_add_git_constraint_with_extras(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_git_constraint_with_extras_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) repo.add_package(get_package("tomlkit", "0.5.5")) - tester.execute("git+https://github.com/demo/demo.git[foo,bar]") + old_tester.execute("git+https://github.com/demo/demo.git[foo,bar]") expected = """\ @@ -244,7 +1010,6 @@ def test_add_git_constraint_with_extras(app, repo, installer): Writing lock file - Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) @@ -253,7 +1018,7 @@ def test_add_git_constraint_with_extras(app, repo, installer): - Installing demo (0.1.2 9cf87a2) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 4 @@ -266,14 +1031,11 @@ def test_add_git_constraint_with_extras(app, repo, installer): } -def test_add_git_ssh_constraint(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_git_ssh_constraint_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) - tester.execute("git+ssh://git@github.com/demo/demo.git@develop") + old_tester.execute("git+ssh://git@github.com/demo/demo.git@develop") expected = """\ @@ -282,14 +1044,13 @@ def test_add_git_ssh_constraint(app, repo, installer): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -302,17 +1063,16 @@ def test_add_git_ssh_constraint(app, repo, installer): } -def test_add_directory_constraint(app, repo, installer, mocker): +def test_add_directory_constraint_old_installer( + app, repo, installer, mocker, old_tester +): p = mocker.patch("poetry.utils._compat.Path.cwd") p.return_value = Path(__file__) / ".." - command = app.find("add") - tester = CommandTester(command) - repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) - tester.execute("../git/github.com/demo/demo") + old_tester.execute("../git/github.com/demo/demo") expected = """\ @@ -321,14 +1081,13 @@ def test_add_directory_constraint(app, repo, installer, mocker): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 ../git/github.com/demo/demo) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -338,16 +1097,15 @@ def test_add_directory_constraint(app, repo, installer, mocker): assert content["dependencies"]["demo"] == {"path": "../git/github.com/demo/demo"} -def test_add_directory_with_poetry(app, repo, installer, mocker): +def test_add_directory_with_poetry_old_installer( + app, repo, installer, mocker, old_tester +): p = mocker.patch("poetry.utils._compat.Path.cwd") p.return_value = Path(__file__) / ".." - command = app.find("add") - tester = CommandTester(command) - repo.add_package(get_package("pendulum", "1.4.4")) - tester.execute("../git/github.com/demo/pyproject-demo") + old_tester.execute("../git/github.com/demo/pyproject-demo") expected = """\ @@ -356,28 +1114,26 @@ def test_add_directory_with_poetry(app, repo, installer, mocker): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 ../git/github.com/demo/pyproject-demo) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 -def test_add_file_constraint_wheel(app, repo, installer, mocker): +def test_add_file_constraint_wheel_old_installer( + app, repo, installer, mocker, old_tester +): p = mocker.patch("poetry.utils._compat.Path.cwd") p.return_value = Path(__file__) / ".." - command = app.find("add") - tester = CommandTester(command) - repo.add_package(get_package("pendulum", "1.4.4")) - tester.execute("../distributions/demo-0.1.0-py2.py3-none-any.whl") + old_tester.execute("../distributions/demo-0.1.0-py2.py3-none-any.whl") expected = """\ @@ -386,14 +1142,13 @@ def test_add_file_constraint_wheel(app, repo, installer, mocker): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 ../distributions/demo-0.1.0-py2.py3-none-any.whl) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -405,16 +1160,15 @@ def test_add_file_constraint_wheel(app, repo, installer, mocker): } -def test_add_file_constraint_sdist(app, repo, installer, mocker): +def test_add_file_constraint_sdist_old_installer( + app, repo, installer, mocker, old_tester +): p = mocker.patch("poetry.utils._compat.Path.cwd") p.return_value = Path(__file__) / ".." - command = app.find("add") - tester = CommandTester(command) - repo.add_package(get_package("pendulum", "1.4.4")) - tester.execute("../distributions/demo-0.1.0.tar.gz") + old_tester.execute("../distributions/demo-0.1.0.tar.gz") expected = """\ @@ -423,14 +1177,13 @@ def test_add_file_constraint_sdist(app, repo, installer, mocker): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 ../distributions/demo-0.1.0.tar.gz) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -442,10 +1195,9 @@ def test_add_file_constraint_sdist(app, repo, installer, mocker): } -def test_add_constraint_with_extras_option(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_constraint_with_extras_option_old_installer( + app, repo, installer, old_tester +): cachy2 = get_package("cachy", "0.2.0") cachy2.extras = {"msgpack": [get_dependency("msgpack-python")]} msgpack_dep = get_dependency("msgpack-python", ">=0.5 <0.6", optional=True) @@ -455,7 +1207,7 @@ def test_add_constraint_with_extras_option(app, repo, installer): repo.add_package(cachy2) repo.add_package(get_package("msgpack-python", "0.5.3")) - tester.execute("cachy=0.2.0 --extras msgpack") + old_tester.execute("cachy=0.2.0 --extras msgpack") expected = """\ @@ -464,14 +1216,13 @@ def test_add_constraint_with_extras_option(app, repo, installer): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -484,16 +1235,15 @@ def test_add_constraint_with_extras_option(app, repo, installer): } -def test_add_url_constraint_wheel(app, repo, installer, mocker): +def test_add_url_constraint_wheel_old_installer( + app, repo, installer, mocker, old_tester +): p = mocker.patch("poetry.utils._compat.Path.cwd") p.return_value = Path(__file__) / ".." - command = app.find("add") - tester = CommandTester(command) - repo.add_package(get_package("pendulum", "1.4.4")) - tester.execute( + old_tester.execute( "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" ) @@ -504,14 +1254,13 @@ def test_add_url_constraint_wheel(app, repo, installer, mocker): Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 2 @@ -523,15 +1272,14 @@ def test_add_url_constraint_wheel(app, repo, installer, mocker): } -def test_add_url_constraint_wheel_with_extras(app, repo, installer, mocker): - command = app.find("add") - tester = CommandTester(command) - +def test_add_url_constraint_wheel_with_extras_old_installer( + app, repo, installer, old_tester +): repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) repo.add_package(get_package("tomlkit", "0.5.5")) - tester.execute( + old_tester.execute( "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl[foo,bar]" ) @@ -542,7 +1290,6 @@ def test_add_url_constraint_wheel_with_extras(app, repo, installer, mocker): Writing lock file - Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) @@ -551,7 +1298,7 @@ def test_add_url_constraint_wheel_with_extras(app, repo, installer, mocker): - Installing demo (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 4 @@ -564,16 +1311,13 @@ def test_add_url_constraint_wheel_with_extras(app, repo, installer, mocker): } -def test_add_constraint_with_python(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_constraint_with_python_old_installer(app, repo, installer, old_tester): cachy2 = get_package("cachy", "0.2.0") repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(cachy2) - tester.execute("cachy=0.2.0 --python >=2.7") + old_tester.execute("cachy=0.2.0 --python >=2.7") expected = """\ @@ -582,13 +1326,12 @@ def test_add_constraint_with_python(app, repo, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 @@ -598,18 +1341,18 @@ def test_add_constraint_with_python(app, repo, installer): assert content["dependencies"]["cachy"] == {"version": "0.2.0", "python": ">=2.7"} -def test_add_constraint_with_platform(app, repo, installer, env): +def test_add_constraint_with_platform_old_installer( + app, repo, installer, env, old_tester +): platform = sys.platform env._platform = platform - command = app.find("add") - tester = CommandTester(command) cachy2 = get_package("cachy", "0.2.0") repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(cachy2) - tester.execute("cachy=0.2.0 --platform {} -vvv".format(platform)) + old_tester.execute("cachy=0.2.0 --platform {} -vvv".format(platform)) expected = """\ @@ -618,13 +1361,12 @@ def test_add_constraint_with_platform(app, repo, installer, env): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 @@ -637,17 +1379,14 @@ def test_add_constraint_with_platform(app, repo, installer, env): } -def test_add_constraint_with_source(app, poetry, installer): +def test_add_constraint_with_source_old_installer(app, poetry, installer, old_tester): repo = LegacyRepository(name="my-index", url="https://my-index.fake") repo.add_package(get_package("cachy", "0.2.0")) repo._cache.store("matches").put("cachy:0.2.0", [Version.parse("0.2.0")], 5) poetry.pool.add_repository(repo) - command = app.find("add") - tester = CommandTester(command) - - tester.execute("cachy=0.2.0 --source my-index") + old_tester.execute("cachy=0.2.0 --source my-index") expected = """\ @@ -656,13 +1395,12 @@ def test_add_constraint_with_source(app, poetry, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 @@ -675,17 +1413,16 @@ def test_add_constraint_with_source(app, poetry, installer): } -def test_add_constraint_with_source_that_does_not_exist(app): - command = app.find("add") - tester = CommandTester(command) - +def test_add_constraint_with_source_that_does_not_exist_old_installer(app, old_tester): with pytest.raises(ValueError) as e: - tester.execute("foo --source i-dont-exist") + old_tester.execute("foo --source i-dont-exist") assert 'Repository "i-dont-exist" does not exist.' == str(e.value) -def test_add_constraint_not_found_with_source(app, poetry, mocker): +def test_add_constraint_not_found_with_source_old_installer( + app, poetry, mocker, old_tester +): repo = LegacyRepository(name="my-index", url="https://my-index.fake") mocker.patch.object(repo, "find_packages", return_value=[]) @@ -694,23 +1431,19 @@ def test_add_constraint_not_found_with_source(app, poetry, mocker): pypi = poetry.pool.repositories[0] pypi.add_package(get_package("cachy", "0.2.0")) - command = app.find("add") - tester = CommandTester(command) - with pytest.raises(ValueError) as e: - tester.execute("cachy --source my-index") + old_tester.execute("cachy --source my-index") assert "Could not find a matching version of package cachy" == str(e.value) -def test_add_to_section_that_does_no_exist_yet(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_to_section_that_does_no_exist_yet_old_installer( + app, repo, installer, old_tester +): repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) - tester.execute("cachy --dev") + old_tester.execute("cachy --dev") expected = """\ Using version ^0.2.0 for cachy @@ -720,13 +1453,12 @@ def test_add_to_section_that_does_no_exist_yet(app, repo, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 @@ -736,14 +1468,13 @@ def test_add_to_section_that_does_no_exist_yet(app, repo, installer): assert content["dev-dependencies"]["cachy"] == "^0.2.0" -def test_add_should_not_select_prereleases(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_should_not_select_prereleases_old_installer( + app, repo, installer, old_tester +): repo.add_package(get_package("pyyaml", "3.13")) repo.add_package(get_package("pyyaml", "4.2b2")) - tester.execute("pyyaml") + old_tester.execute("pyyaml") expected = """\ Using version ^3.13 for pyyaml @@ -753,13 +1484,12 @@ def test_add_should_not_select_prereleases(app, repo, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing pyyaml (3.13) """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() assert len(installer.installs) == 1 @@ -769,35 +1499,31 @@ def test_add_should_not_select_prereleases(app, repo, installer): assert content["dependencies"]["pyyaml"] == "^3.13" -def test_add_should_display_an_error_when_adding_existing_package_with_no_constraint( - app, repo, installer +def test_add_should_display_an_error_when_adding_existing_package_with_no_constraint_old_installer( + app, repo, installer, old_tester ): content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" app.poetry.file.write(content) - command = app.find("add") - tester = CommandTester(command) repo.add_package(get_package("foo", "1.1.2")) with pytest.raises(ValueError) as e: - tester.execute("foo") + old_tester.execute("foo") assert "Package foo is already present" == str(e.value) -def test_add_should_work_when_adding_existing_package_with_latest_constraint( - app, repo, installer +def test_add_should_work_when_adding_existing_package_with_latest_constraint_old_installer( + app, repo, installer, old_tester ): content = app.poetry.file.read() content["tool"]["poetry"]["dependencies"]["foo"] = "^1.0" app.poetry.file.write(content) - command = app.find("add") - tester = CommandTester(command) repo.add_package(get_package("foo", "1.1.2")) - tester.execute("foo@latest") + old_tester.execute("foo@latest") expected = """\ Using version ^1.1.2 for foo @@ -807,13 +1533,12 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.1.2) """ - assert expected in tester.io.fetch_output() + assert expected in old_tester.io.fetch_output() content = app.poetry.file.read()["tool"]["poetry"] @@ -821,14 +1546,13 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( assert content["dependencies"]["foo"] == "^1.1.2" -def test_add_chooses_prerelease_if_only_prereleases_are_available(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) - +def test_add_chooses_prerelease_if_only_prereleases_are_available_old_installer( + app, repo, installer, old_tester +): repo.add_package(get_package("foo", "1.2.3b0")) repo.add_package(get_package("foo", "1.2.3b1")) - tester.execute("foo") + old_tester.execute("foo") expected = """\ Using version ^1.2.3-beta.1 for foo @@ -838,23 +1562,19 @@ def test_add_chooses_prerelease_if_only_prereleases_are_available(app, repo, ins Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3b1) """ - assert expected in tester.io.fetch_output() - + assert expected in old_tester.io.fetch_output() -def test_add_preferes_stable_releases(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) +def test_add_preferes_stable_releases_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("foo", "1.2.3")) repo.add_package(get_package("foo", "1.2.4b1")) - tester.execute("foo") + old_tester.execute("foo") expected = """\ Using version ^1.2.3 for foo @@ -864,22 +1584,18 @@ def test_add_preferes_stable_releases(app, repo, installer): Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3) """ - assert expected in tester.io.fetch_output() - + assert expected in old_tester.io.fetch_output() -def test_add_with_lock(app, repo, installer): - command = app.find("add") - tester = CommandTester(command) +def test_add_with_lock_old_installer(app, repo, installer, old_tester): repo.add_package(get_package("cachy", "0.2.0")) - tester.execute("cachy --lock") + old_tester.execute("cachy --lock") expected = """\ Using version ^0.2.0 for cachy @@ -890,4 +1606,4 @@ def test_add_with_lock(app, repo, installer): Writing lock file """ - assert expected == tester.io.fetch_output() + assert expected == old_tester.io.fetch_output() diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index fe331d777fc..14048090146 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -13,6 +13,7 @@ def test_list_displays_default_value_if_not_set(app, config): tester.execute("--list") expected = """cache-dir = "/foo" +experimental.new-installer = true virtualenvs.create = true virtualenvs.in-project = false virtualenvs.path = {path} # /foo{sep}virtualenvs @@ -32,6 +33,7 @@ def test_list_displays_set_get_setting(app, config): tester.execute("--list") expected = """cache-dir = "/foo" +experimental.new-installer = true virtualenvs.create = false virtualenvs.in-project = false virtualenvs.path = {path} # /foo{sep}virtualenvs @@ -79,6 +81,7 @@ def test_list_displays_set_get_local_setting(app, config): tester.execute("--list") expected = """cache-dir = "/foo" +experimental.new-installer = true virtualenvs.create = false virtualenvs.in-project = false virtualenvs.path = {path} # /foo{sep}virtualenvs diff --git a/tests/console/conftest.py b/tests/console/conftest.py index e96a4a9d52d..2707dd1f071 100644 --- a/tests/console/conftest.py +++ b/tests/console/conftest.py @@ -1,12 +1,18 @@ import os +import re import pytest from cleo import ApplicationTester from poetry.console import Application as BaseApplication +from poetry.core.masonry.utils.helpers import escape_name +from poetry.core.masonry.utils.helpers import escape_version +from poetry.core.packages.utils.link import Link from poetry.factory import Factory +from poetry.installation.executor import Executor as BaseExecutor from poetry.installation.noop_installer import NoopInstaller +from poetry.io.null_io import NullIO from poetry.packages import Locker as BaseLocker from poetry.poetry import Poetry as BasePoetry from poetry.repositories import Pool @@ -18,6 +24,42 @@ from tests.helpers import mock_clone +class Executor(BaseExecutor): + def __init__(self, *args, **kwargs): + super(Executor, self).__init__(*args, **kwargs) + + self._installs = [] + self._updates = [] + self._uninstalls = [] + + @property + def installations(self): + return self._installs + + @property + def updates(self): + return self._updates + + @property + def removals(self): + return self._uninstalls + + def _do_execute_operation(self, operation): + super(Executor, self)._do_execute_operation(operation) + + if not operation.skipped: + getattr(self, "_{}s".format(operation.job_type)).append(operation.package) + + def _execute_install(self, operation): + return 0 + + def _execute_update(self, operation): + return 0 + + def _execute_remove(self, operation): + return 0 + + @pytest.fixture() def installer(): return NoopInstaller() @@ -39,6 +81,9 @@ def setup(mocker, installer, installed, config, env): p = mocker.patch("poetry.installation.installer.Installer._get_installer") p.return_value = installer + # Do not run pip commands of the executor + mocker.patch("poetry.installation.executor.Executor.run_pip") + p = mocker.patch("poetry.installation.installer.Installer._get_installed") p.return_value = installed @@ -144,10 +189,22 @@ def find_packages( raise PackageNotFound("Package [{}] not found.".format(name)) return packages + def find_links_for_package(self, package): + return [ + Link( + "https://foo.bar/files/{}-{}-py2.py3-none-any.whl".format( + escape_name(package.name), escape_version(package.version.text) + ) + ) + ] + @pytest.fixture -def repo(): - return Repository() +def repo(http): + http.register_uri( + http.GET, re.compile("^https?://foo.bar/(.+?)$"), + ) + return Repository(name="foo") @pytest.fixture @@ -188,3 +245,13 @@ def app(poetry): @pytest.fixture def app_tester(app): return ApplicationTester(app) + + +@pytest.fixture +def new_installer_disabled(config): + config.merge({"experimental": {"new-installer": False}}) + + +@pytest.fixture() +def executor(poetry, config, env): + return Executor(env, poetry.pool, config, NullIO()) diff --git a/tests/installation/test_authenticator.py b/tests/installation/test_authenticator.py new file mode 100644 index 00000000000..bd09ad86663 --- /dev/null +++ b/tests/installation/test_authenticator.py @@ -0,0 +1,115 @@ +import re + +import pytest + +from poetry.installation.authenticator import Authenticator +from poetry.io.null_io import NullIO + + +@pytest.fixture() +def mock_remote(http): + http.register_uri( + http.GET, re.compile("^https?://foo.bar/(.+?)$"), + ) + + +def test_authenticator_uses_url_provided_credentials(config, mock_remote, http): + config.merge( + { + "repositories": {"foo": {"url": "https://foo.bar/simple/"}}, + "http-basic": {"foo": {"username": "bar", "password": "baz"}}, + } + ) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://foo001:bar002@foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic Zm9vMDAxOmJhcjAwMg==" == request.headers["Authorization"] + + +def test_authenticator_uses_credentials_from_config_if_not_provided( + config, mock_remote, http +): + config.merge( + { + "repositories": {"foo": {"url": "https://foo.bar/simple/"}}, + "http-basic": {"foo": {"username": "bar", "password": "baz"}}, + } + ) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic YmFyOmJheg==" == request.headers["Authorization"] + + +def test_authenticator_uses_username_only_credentials(config, mock_remote, http): + config.merge( + { + "repositories": {"foo": {"url": "https://foo.bar/simple/"}}, + "http-basic": {"foo": {"username": "bar", "password": "baz"}}, + } + ) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://foo001@foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic Zm9vMDAxOg==" == request.headers["Authorization"] + + +def test_authenticator_uses_password_only_credentials(config, mock_remote, http): + config.merge( + { + "repositories": {"foo": {"url": "https://foo.bar/simple/"}}, + "http-basic": {"foo": {"username": "bar", "password": "baz"}}, + } + ) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://:bar002@foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic OmJhcjAwMg==" == request.headers["Authorization"] + + +def test_authenticator_uses_empty_strings_as_default_password( + config, mock_remote, http +): + config.merge( + { + "repositories": {"foo": {"url": "https://foo.bar/simple/"}}, + "http-basic": {"foo": {"username": "bar"}}, + } + ) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic YmFyOg==" == request.headers["Authorization"] + + +def test_authenticator_uses_empty_strings_as_default_username( + config, mock_remote, http +): + config.merge( + { + "repositories": {"foo": {"url": "https://foo.bar/simple/"}}, + "http-basic": {"foo": {"username": None, "password": "bar"}}, + } + ) + + authenticator = Authenticator(config, NullIO()) + authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") + + request = http.last_request() + + assert "Basic OmJhcg==" == request.headers["Authorization"] diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py new file mode 100644 index 00000000000..d44df3b8894 --- /dev/null +++ b/tests/installation/test_chef.py @@ -0,0 +1,75 @@ +from packaging.tags import Tag +from poetry.core.packages.utils.link import Link +from poetry.installation.chef import Chef +from poetry.utils._compat import Path +from poetry.utils.env import MockEnv + + +def test_get_cached_archive_for_link(config, mocker): + chef = Chef( + config, + MockEnv( + version_info=(3, 8, 3), + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, + supported_tags=[ + Tag("cp38", "cp38", "macosx_10_15_x86_64"), + Tag("py3", "none", "any"), + ], + ), + ) + + mocker.patch.object( + chef, + "get_cached_archives_for_link", + return_value=[ + Link("file:///foo/demo-0.1.0-py2.py3-none-any"), + Link("file:///foo/demo-0.1.0.tar.gz"), + Link("file:///foo/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), + Link("file:///foo/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), + ], + ) + + archive = chef.get_cached_archive_for_link( + Link("https://files.python-poetry.org/demo-0.1.0.tar.gz") + ) + + assert Link("file:///foo/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl") == archive + + +def test_get_cached_archives_for_link(config, mocker): + chef = Chef( + config, + MockEnv( + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"} + ), + ) + + mocker.patch.object( + chef, + "get_cache_directory_for_link", + return_value=Path(__file__).parent.parent.joinpath("fixtures/distributions"), + ) + + archives = chef.get_cached_archives_for_link( + Link("https://files.python-poetry.org/demo-0.1.0.tar.gz") + ) + + assert 2 == len(archives) + + +def test_get_cache_directory_for_link(config): + chef = Chef( + config, + MockEnv( + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"} + ), + ) + + directory = chef.get_cache_directory_for_link( + Link("https://files.python-poetry.org/poetry-1.1.0.tar.gz") + ) + expected = Path( + "/foo/artifacts/ba/63/13/283a3b3b7f95f05e9e6f84182d276f7bb0951d5b0cc24422b33f7a4648" + ) + + assert expected == directory diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py new file mode 100644 index 00000000000..5683831a8d3 --- /dev/null +++ b/tests/installation/test_chooser.py @@ -0,0 +1,174 @@ +import re + +import pytest + +from packaging.tags import Tag +from poetry.core.packages.package import Package +from poetry.installation.chooser import Chooser +from poetry.repositories.legacy_repository import LegacyRepository +from poetry.repositories.pool import Pool +from poetry.repositories.pypi_repository import PyPiRepository +from poetry.utils._compat import Path +from poetry.utils.env import MockEnv + + +JSON_FIXTURES = ( + Path(__file__).parent.parent / "repositories" / "fixtures" / "pypi.org" / "json" +) + +LEGACY_FIXTURES = Path(__file__).parent.parent / "repositories" / "fixtures" / "legacy" + + +@pytest.fixture() +def env(): + return MockEnv( + supported_tags=[ + Tag("cp37", "cp37", "macosx_10_15_x86_64"), + Tag("py3", "none", "any"), + ] + ) + + +@pytest.fixture() +def mock_pypi(http): + def callback(request, uri, headers): + parts = uri.rsplit("/") + + name = parts[-3] + version = parts[-2] + + fixture = JSON_FIXTURES / name / (version + ".json") + if not fixture.exists(): + fixture = JSON_FIXTURES / (name + ".json") + + if not fixture.exists(): + return + + with fixture.open(encoding="utf-8") as f: + return [200, headers, f.read()] + + http.register_uri( + http.GET, re.compile("^https://pypi.org/(.+?)/(.+?)/json$"), body=callback, + ) + + +@pytest.fixture() +def mock_legacy(http): + def callback(request, uri, headers): + parts = uri.rsplit("/") + name = parts[-2] + + fixture = LEGACY_FIXTURES / (name + ".html") + + with fixture.open(encoding="utf-8") as f: + return [200, headers, f.read()] + + http.register_uri( + http.GET, re.compile("^https://foo.bar/simple/(.+?)$"), body=callback, + ) + + +@pytest.fixture() +def pool(): + pool = Pool() + + pool.add_repository(PyPiRepository(disable_cache=True)) + pool.add_repository( + LegacyRepository("foo", "https://foo.bar/simple/", disable_cache=True) + ) + + return pool + + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_chooses_universal_wheel_link_if_available( + env, mock_pypi, mock_legacy, source_type, pool +): + chooser = Chooser(pool, env) + + package = Package("pytest", "3.5.0") + if source_type == "legacy": + package.source_type = "legacy" + package.source_reference = "foo" + package.source_url = "https://foo.bar/simple/" + + link = chooser.choose_for(package) + + assert "pytest-3.5.0-py2.py3-none-any.whl" == link.filename + + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_chooses_specific_python_universal_wheel_link_if_available( + env, mock_pypi, mock_legacy, source_type, pool +): + chooser = Chooser(pool, env) + + package = Package("isort", "4.3.4") + if source_type == "legacy": + package.source_type = "legacy" + package.source_reference = "foo" + package.source_url = "https://foo.bar/simple/" + + link = chooser.choose_for(package) + + assert "isort-4.3.4-py3-none-any.whl" == link.filename + + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_chooses_system_specific_wheel_link_if_available( + mock_pypi, mock_legacy, source_type, pool +): + env = MockEnv( + supported_tags=[Tag("cp37", "cp37m", "win32"), Tag("py3", "none", "any")] + ) + chooser = Chooser(pool, env) + + package = Package("pyyaml", "3.13.0") + if source_type == "legacy": + package.source_type = "legacy" + package.source_reference = "foo" + package.source_url = "https://foo.bar/simple/" + + link = chooser.choose_for(package) + + assert "PyYAML-3.13-cp37-cp37m-win32.whl" == link.filename + + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_chooses_sdist_if_no_compatible_wheel_link_is_available( + env, mock_pypi, mock_legacy, source_type, pool, +): + chooser = Chooser(pool, env) + + package = Package("pyyaml", "3.13.0") + if source_type == "legacy": + package.source_type = "legacy" + package.source_reference = "foo" + package.source_url = "https://foo.bar/simple/" + + link = chooser.choose_for(package) + + assert "PyYAML-3.13.tar.gz" == link.filename + + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_chooses_distributions_that_match_the_package_hashes( + env, mock_pypi, mock_legacy, source_type, pool, +): + chooser = Chooser(pool, env) + + package = Package("isort", "4.3.4") + package.files = [ + { + "hash": "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "filename": "isort-4.3.4.tar.gz", + } + ] + if source_type == "legacy": + package.source_type = "legacy" + package.source_reference = "foo" + package.source_url = "https://foo.bar/simple/" + + link = chooser.choose_for(package) + + assert "isort-4.3.4.tar.gz" == link.filename diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py new file mode 100644 index 00000000000..7f9b20a8aa9 --- /dev/null +++ b/tests/installation/test_executor.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import re + +import pytest + +from clikit.api.formatter.style import Style +from clikit.io.buffered_io import BufferedIO + +from poetry.config.config import Config +from poetry.core.packages.package import Package +from poetry.installation.executor import Executor +from poetry.installation.operations import Install +from poetry.installation.operations import Uninstall +from poetry.installation.operations import Update +from poetry.repositories.pool import Pool +from poetry.utils._compat import PY36 +from poetry.utils._compat import Path +from poetry.utils.env import MockEnv +from tests.repositories.test_pypi_repository import MockRepository + + +@pytest.fixture() +def io(): + io = BufferedIO() + io.formatter.add_style(Style("c1_dark").fg("cyan").dark()) + io.formatter.add_style(Style("c2_dark").fg("default").bold().dark()) + io.formatter.add_style(Style("success_dark").fg("green").dark()) + io.formatter.add_style(Style("warning").fg("yellow")) + + return io + + +@pytest.fixture() +def pool(): + pool = Pool() + pool.add_repository(MockRepository()) + + return pool + + +@pytest.fixture() +def mock_file_downloads(http): + def callback(request, uri, headers): + fixture = Path(__file__).parent.parent.joinpath( + "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" + ) + + with fixture.open("rb") as f: + return [200, headers, f.read()] + + http.register_uri( + http.GET, re.compile("^https://files.pythonhosted.org/.*$"), body=callback, + ) + + +def test_execute_executes_a_batch_of_operations( + config, pool, io, tmp_dir, mock_file_downloads +): + config = Config() + config.merge({"cache-dir": tmp_dir}) + + env = MockEnv(path=Path(tmp_dir)) + executor = Executor(env, pool, config, io) + + file_package = Package("demo", "0.1.0") + file_package.source_type = "file" + file_package.source_url = str( + Path(__file__) + .parent.parent.joinpath( + "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" + ) + .resolve() + ) + + directory_package = Package("simple-project", "1.2.3") + directory_package.source_type = "directory" + directory_package.source_url = str( + Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() + ) + + git_package = Package("demo", "0.1.0") + git_package.source_type = "git" + git_package.source_reference = "master" + git_package.source_url = "https://github.com/demo/demo.git" + + assert 0 == executor.execute( + [ + Install(Package("pytest", "3.5.2")), + Uninstall(Package("attrs", "17.4.0")), + Update(Package("requests", "2.18.3"), Package("requests", "2.18.4")), + Uninstall(Package("clikit", "0.2.3")).skip("Not currently installed"), + Install(file_package), + Install(directory_package), + Install(git_package), + ] + ) + + expected = """ +Package operations: 4 installs, 1 update, 1 removal + + • Installing pytest (3.5.2) + • Removing attrs (17.4.0) + • Updating requests (2.18.3 -> 2.18.4) + • Installing demo (0.1.0 {}) + • Installing simple-project (1.2.3 {}) + • Installing demo (0.1.0 master) +""".format( + file_package.source_url, directory_package.source_url + ) + assert expected == io.fetch_output() + assert 5 == len(env.executed) + + +def test_execute_shows_skipped_operations_if_verbose(config, pool, io): + config = Config() + config.merge({"cache-dir": "/foo"}) + + env = MockEnv() + executor = Executor(env, pool, config, io) + executor.verbose() + + assert 0 == executor.execute( + [Uninstall(Package("clikit", "0.2.3")).skip("Not currently installed")] + ) + + expected = """ +Package operations: 0 installs, 0 updates, 0 removals, 1 skipped + + • Removing clikit (0.2.3): Skipped for the following reason: Not currently installed +""" + assert expected == io.fetch_output() + assert 0 == len(env.executed) + + +@pytest.mark.skipif( + not PY36, reason="Improved error rendering is only available on Python >=3.6" +) +def test_execute_should_show_errors(config, mocker, io): + env = MockEnv() + executor = Executor(env, pool, config, io) + executor.verbose() + + mocker.patch.object(executor, "_install", side_effect=Exception("It failed!")) + + assert 1 == executor.execute([Install(Package("clikit", "0.2.3"))]) + + expected = """ +Package operations: 1 install, 0 updates, 0 removals + + • Installing clikit (0.2.3) + + Exception + + It failed! +""" + + assert expected in io.fetch_output() + + +def test_execute_should_show_operation_as_cancelled_on_subprocess_keyboard_interrupt( + config, mocker, io +): + env = MockEnv() + executor = Executor(env, pool, config, io) + executor.verbose() + + # A return code of -2 means KeyboardInterrupt in the pip subprocess + mocker.patch.object(executor, "_install", return_value=-2) + + assert 1 == executor.execute([Install(Package("clikit", "0.2.3"))]) + + expected = """ +Package operations: 1 install, 0 updates, 0 removals + + • Installing clikit (0.2.3) + • Installing clikit (0.2.3): Cancelled +""" + + assert expected == io.fetch_output() diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 822dac484e2..1e76197ce7c 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -8,6 +8,7 @@ from poetry.core.packages import ProjectPackage from poetry.installation import Installer as BaseInstaller +from poetry.installation.executor import Executor as BaseExecutor from poetry.installation.noop_installer import NoopInstaller from poetry.packages import Locker as BaseLocker from poetry.repositories import Pool @@ -34,6 +35,42 @@ def _get_installer(self): return NoopInstaller() +class Executor(BaseExecutor): + def __init__(self, *args, **kwargs): + super(Executor, self).__init__(*args, **kwargs) + + self._installs = [] + self._updates = [] + self._uninstalls = [] + + @property + def installations(self): + return self._installs + + @property + def updates(self): + return self._updates + + @property + def removals(self): + return self._uninstalls + + def _do_execute_operation(self, operation): + super(Executor, self)._do_execute_operation(operation) + + if not operation.skipped: + getattr(self, "_{}s".format(operation.job_type)).append(operation.package) + + def _execute_install(self, operation): + return 0 + + def _execute_update(self, operation): + return 0 + + def _execute_uninstall(self, operation): + return 0 + + class CustomInstalledRepository(InstalledRepository): @classmethod def load(cls, env): @@ -122,8 +159,20 @@ def env(): @pytest.fixture() -def installer(package, pool, locker, env, installed): - return Installer(NullIO(), env, package, locker, pool, installed=installed) +def installer(package, pool, locker, env, installed, config): + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor(True) + + return installer def fixture(name): @@ -217,14 +266,9 @@ def test_run_update_after_removing_dependencies( assert locker.written_data == expected - installs = installer.installer.installs - assert len(installs) == 0 - - updates = installer.installer.updates - assert len(updates) == 0 - - removals = installer.installer.removals - assert len(removals) == 1 + assert 0 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 1 == installer.executor.removals_count def test_run_install_no_dev(installer, locker, repo, package, installed): @@ -286,14 +330,9 @@ def test_run_install_no_dev(installer, locker, repo, package, installed): installer.dev_mode(False) installer.run() - installs = installer.installer.installs - assert len(installs) == 0 - - updates = installer.installer.updates - assert len(updates) == 0 - - removals = installer.installer.removals - assert len(removals) == 1 + assert 0 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 1 == installer.executor.removals_count def test_run_install_remove_untracked(installer, locker, repo, package, installed): @@ -339,14 +378,10 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe installer.dev_mode(True).remove_untracked(True) installer.run() - installs = installer.installer.installs - assert len(installs) == 0 - - updates = installer.installer.updates - assert len(updates) == 0 - - removals = installer.installer.removals - assert set(r.name for r in removals) == {"b", "c"} + assert 0 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 2 == installer.executor.removals_count + assert {"b", "c"} == set(r.name for r in installer.executor.removals) def test_run_whitelist_add(installer, locker, repo, package): @@ -438,9 +473,9 @@ def test_run_whitelist_remove(installer, locker, repo, package, installed): expected = fixture("remove") assert locker.written_data == expected - assert len(installer.installer.installs) == 1 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 1 + assert 1 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 1 == installer.executor.removals_count def test_add_with_sub_dependencies(installer, locker, repo, package): @@ -518,13 +553,12 @@ def test_run_with_optional_and_python_restricted_dependencies( assert locker.written_data == expected - installer = installer.installer # We should only have 2 installs: # C,D since python version is not compatible # with B's python constraint and A is optional - assert len(installer.installs) == 2 - assert installer.installs[0].name == "d" - assert installer.installs[1].name == "c" + assert 2 == installer.executor.installations_count + assert "d" == installer.executor.installations[0].name + assert "c" == installer.executor.installations[1].name def test_run_with_optional_and_platform_restricted_dependencies( @@ -555,13 +589,12 @@ def test_run_with_optional_and_platform_restricted_dependencies( assert locker.written_data == expected - installer = installer.installer # We should only have 2 installs: # C,D since the mocked python version is not compatible # with B's python constraint and A is optional - assert len(installer.installs) == 2 - assert installer.installs[0].name == "d" - assert installer.installs[1].name == "c" + assert 2 == installer.executor.installations_count + assert "d" == installer.executor.installations[0].name + assert "c" == installer.executor.installations[1].name def test_run_with_dependencies_extras(installer, locker, repo, package): @@ -609,8 +642,7 @@ def test_run_does_not_install_extras_if_not_requested(installer, locker, repo, p assert locker.written_data == expected # But should not be installed - installer = installer.installer - assert len(installer.installs) == 3 # A, B, C + assert 3 == installer.executor.installations_count # A, B, C def test_run_installs_extras_if_requested(installer, locker, repo, package): @@ -638,8 +670,7 @@ def test_run_installs_extras_if_requested(installer, locker, repo, package): assert locker.written_data == expected # But should not be installed - installer = installer.installer - assert len(installer.installs) == 4 # A, B, C, D + assert 4 == installer.executor.installations_count # A, B, C, D def test_run_installs_extras_with_deps_if_requested(installer, locker, repo, package): @@ -668,8 +699,7 @@ def test_run_installs_extras_with_deps_if_requested(installer, locker, repo, pac assert locker.written_data == expected # But should not be installed - installer = installer.installer - assert len(installer.installs) == 4 # A, B, C, D + assert 4 == installer.executor.installations_count # A, B, C, D def test_run_installs_extras_with_deps_if_requested_locked( @@ -698,16 +728,15 @@ def test_run_installs_extras_with_deps_if_requested_locked( installer.run() # But should not be installed - installer = installer.installer - assert len(installer.installs) == 4 # A, B, C, D + assert 4 == installer.executor.installations_count # A, B, C, D -def test_installer_with_pypi_repository(package, locker, installed): +def test_installer_with_pypi_repository(package, locker, installed, config): pool = Pool() pool.add_repository(MockRepository()) installer = Installer( - NullIO(), NullEnv(), package, locker, pool, installed=installed + NullIO(), NullEnv(), package, locker, pool, config, installed=installed ) package.add_dependency("pytest", "^3.5", category="dev") @@ -730,7 +759,7 @@ def test_run_installs_with_local_file(installer, locker, repo, package): assert locker.written_data == expected - assert len(installer.installer.installs) == 2 + assert 2 == installer.executor.installations_count def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, package): @@ -745,7 +774,7 @@ def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, packa assert locker.written_data == expected - assert len(installer.installer.installs) == 1 + assert 1 == installer.executor.installations_count def test_run_installs_with_local_poetry_directory_and_extras( @@ -764,7 +793,7 @@ def test_run_installs_with_local_poetry_directory_and_extras( assert locker.written_data == expected - assert len(installer.installer.installs) == 2 + assert 2 == installer.executor.installations_count def test_run_installs_with_local_poetry_directory_transitive( @@ -788,7 +817,7 @@ def test_run_installs_with_local_poetry_directory_transitive( assert locker.written_data == expected - assert len(installer.installer.installs) == 6 + assert 6 == installer.executor.installations_count def test_run_installs_with_local_poetry_file_transitive( @@ -812,7 +841,7 @@ def test_run_installs_with_local_poetry_file_transitive( assert locker.written_data == expected - assert len(installer.installer.installs) == 4 + assert 4 == installer.executor.installations_count def test_run_installs_with_local_setuptools_directory( @@ -830,7 +859,7 @@ def test_run_installs_with_local_setuptools_directory( assert locker.written_data == expected - assert len(installer.installer.installs) == 3 + assert 3 == installer.executor.installations_count def test_run_with_prereleases(installer, locker, repo, package): @@ -1049,16 +1078,14 @@ def test_run_install_duplicate_dependencies_different_constraints( assert locker.written_data == expected - installs = installer.installer.installs - assert len(installs) == 3 + installs = installer.executor.installations + assert 3 == installer.executor.installations_count assert installs[0] == package_c12 assert installs[1] == package_b10 assert installs[2] == package_a - updates = installer.installer.updates - assert len(updates) == 0 - removals = installer.installer.removals - assert len(removals) == 0 + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count def test_run_install_duplicate_dependencies_different_constraints_with_lock( @@ -1159,12 +1186,9 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock( assert locker.written_data == expected - installs = installer.installer.installs - assert len(installs) == 3 - updates = installer.installer.updates - assert len(updates) == 0 - removals = installer.installer.removals - assert len(removals) == 0 + assert 3 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count def test_run_update_uninstalls_after_removal_transient_dependency( @@ -1218,12 +1242,9 @@ def test_run_update_uninstalls_after_removal_transient_dependency( installer.update(True) installer.run() - installs = installer.installer.installs - assert len(installs) == 0 - updates = installer.installer.updates - assert len(updates) == 0 - removals = installer.installer.removals - assert len(removals) == 1 + assert 0 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 1 == installer.executor.removals_count def test_run_install_duplicate_dependencies_different_constraints_with_lock_update( @@ -1326,12 +1347,9 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda assert locker.written_data == expected - installs = installer.installer.installs - assert len(installs) == 2 - updates = installer.installer.updates - assert len(updates) == 1 - removals = installer.installer.removals - assert len(removals) == 0 + assert 2 == installer.executor.installations_count + assert 1 == installer.executor.updates_count + assert 0 == installer.executor.removals_count @pytest.mark.skip( @@ -1357,16 +1375,14 @@ def test_installer_test_solver_finds_compatible_package_for_dependency_python_no expected = fixture("with-conditional-dependency") assert locker.written_data == expected - installs = installer.installer.installs - if sys.version_info >= (3, 5, 0): - assert len(installs) == 1 + assert 1 == installer.executor.installations_count else: - assert len(installs) == 0 + assert 0 == installer.executor.installations_count def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency( - installer, locker, repo, package, installed, env, pool + installer, locker, repo, package, installed, env, pool, config ): package.add_dependency("A", {"version": "^1.0"}) @@ -1388,9 +1404,9 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.update(True) installer.run() - assert len(installer.installer.installs) == 3 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert 3 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count package.add_dependency("D", "^1.0") locker.locked(True) @@ -1400,62 +1416,102 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installed.add_package(package_b) installed.add_package(package_c) - installer = Installer(NullIO(), env, package, locker, pool, installed=installed) + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor() installer.update(True) installer.whitelist(["D"]) installer.run() - assert len(installer.installer.installs) == 1 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert 1 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency_pypi_repository( - locker, repo, package, installed, env, mocker + locker, repo, package, installed, env, mocker, config ): mocker.patch("sys.platform", "darwin") pool = Pool() pool.add_repository(MockRepository()) - installer = Installer(NullIO(), env, package, locker, pool, installed=installed) + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor() package.add_dependency("poetry", {"version": "^0.12.0"}) installer.update(True) installer.run() - assert len(installer.installer.installs) == 3 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert 3 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count package.add_dependency("pytest", "^3.5") locker.locked(True) locker.mock_lock_data(locker.written_data) - for pkg in installer.installer.installs: + for pkg in installer.executor.installations: installed.add_package(pkg) - installer = Installer(NullIO(), env, package, locker, pool, installed=installed) + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor() installer.update(True) installer.whitelist(["pytest"]) installer.run() - assert len(installer.installer.installs) == 6 if not PY2 else 7 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert (6 if not PY2 else 7) == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count def test_installer_required_extras_should_be_installed( - locker, repo, package, installed, env, mocker + locker, repo, package, installed, env, config ): pool = Pool() pool.add_repository(MockRepository()) - installer = Installer(NullIO(), env, package, locker, pool, installed=installed) + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor() package.add_dependency( "cachecontrol", {"version": "^0.12.5", "extras": ["filecache"]} @@ -1464,21 +1520,31 @@ def test_installer_required_extras_should_be_installed( installer.update(True) installer.run() - assert len(installer.installer.installs) == 2 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert 2 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count locker.locked(True) locker.mock_lock_data(locker.written_data) - installer = Installer(NullIO(), env, package, locker, pool, installed=installed) + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor() installer.update(True) installer.run() - assert len(installer.installer.installs) == 2 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert 2 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count def test_update_multiple_times_with_split_dependencies_is_idempotent( @@ -1557,7 +1623,7 @@ def test_update_multiple_times_with_split_dependencies_is_idempotent( def test_installer_can_install_dependencies_from_forced_source( - locker, package, installed, env + locker, package, installed, env, config ): package.python_versions = "^3.7" package.add_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) @@ -1566,14 +1632,24 @@ def test_installer_can_install_dependencies_from_forced_source( pool.add_repository(MockLegacyRepository()) pool.add_repository(MockRepository()) - installer = Installer(NullIO(), env, package, locker, pool, installed=installed) + installer = Installer( + NullIO(), + env, + package, + locker, + pool, + config, + installed=installed, + executor=Executor(env, pool, config, NullIO()), + ) + installer.use_executor() installer.update(True) installer.run() - assert len(installer.installer.installs) == 1 - assert len(installer.installer.updates) == 0 - assert len(installer.installer.removals) == 0 + assert 1 == installer.executor.installations_count + assert 0 == installer.executor.updates_count + assert 0 == installer.executor.removals_count def test_run_installs_with_url_file(installer, locker, repo, package): @@ -1588,7 +1664,7 @@ def test_run_installs_with_url_file(installer, locker, repo, package): assert locker.written_data == expected - assert len(installer.installer.installs) == 2 + assert 2 == installer.executor.installations_count def test_installer_uses_prereleases_if_they_are_compatible( @@ -1616,11 +1692,11 @@ def test_installer_uses_prereleases_if_they_are_compatible( installer.update(True) installer.run() - assert len(installer.installer.installs) == 2 + assert 2 == installer.executor.installations_count def test_installer_can_handle_old_lock_files( - installer, locker, package, repo, installed + installer, locker, package, repo, installed, config ): pool = Pool() pool.add_repository(MockRepository()) @@ -1631,12 +1707,20 @@ def test_installer_can_handle_old_lock_files( locker.mock_lock_data(fixture("old-lock")) installer = Installer( - NullIO(), MockEnv(), package, locker, pool, installed=installed + NullIO(), + MockEnv(), + package, + locker, + pool, + config, + installed=installed, + executor=Executor(MockEnv(), pool, config, NullIO(),), ) + installer.use_executor() installer.run() - assert 6 == len(installer.installer.installs) + assert 6 == installer.executor.installations_count installer = Installer( NullIO(), @@ -1644,13 +1728,16 @@ def test_installer_can_handle_old_lock_files( package, locker, pool, + config, installed=installed, + executor=Executor(MockEnv(version_info=(2, 7, 18)), pool, config, NullIO(),), ) + installer.use_executor() installer.run() # funcsigs will be added - assert 7 == len(installer.installer.installs) + assert 7 == installer.executor.installations_count installer = Installer( NullIO(), @@ -1658,10 +1745,15 @@ def test_installer_can_handle_old_lock_files( package, locker, pool, + config, installed=installed, + executor=Executor( + MockEnv(version_info=(2, 7, 18), platform="win32"), pool, config, NullIO(), + ), ) + installer.use_executor() installer.run() # colorama will be added - assert 8 == len(installer.installer.installs) + assert 8 == installer.executor.installations_count diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py new file mode 100644 index 00000000000..e50f8cc0202 --- /dev/null +++ b/tests/installation/test_installer_old.py @@ -0,0 +1,1681 @@ +from __future__ import unicode_literals + +import sys + +import pytest + +from clikit.io import NullIO + +from poetry.core.packages import ProjectPackage +from poetry.installation import Installer as BaseInstaller +from poetry.installation.noop_installer import NoopInstaller +from poetry.packages import Locker as BaseLocker +from poetry.repositories import Pool +from poetry.repositories import Repository +from poetry.repositories.installed_repository import InstalledRepository +from poetry.utils._compat import PY2 +from poetry.utils._compat import Path +from poetry.utils.env import MockEnv +from poetry.utils.env import NullEnv +from poetry.utils.toml_file import TomlFile +from tests.helpers import get_dependency +from tests.helpers import get_package +from tests.repositories.test_legacy_repository import ( + MockRepository as MockLegacyRepository, +) +from tests.repositories.test_pypi_repository import MockRepository + + +fixtures_dir = Path("tests/fixtures") + + +class Installer(BaseInstaller): + def _get_installer(self): + return NoopInstaller() + + +class CustomInstalledRepository(InstalledRepository): + @classmethod + def load(cls, env): + return cls() + + +class Locker(BaseLocker): + def __init__(self): + self._written_data = None + self._locked = False + self._content_hash = self._get_content_hash() + + @property + def written_data(self): + return self._written_data + + def locked(self, is_locked=True): + self._locked = is_locked + + return self + + def mock_lock_data(self, data): + self._lock_data = data + + def is_locked(self): + return self._locked + + def is_fresh(self): + return True + + def _get_content_hash(self): + return "123456789" + + def _write_lock_data(self, data): + for package in data["package"]: + python_versions = str(package["python-versions"]) + if PY2: + python_versions = python_versions.decode() + if "requirements" in package: + requirements = {} + for key, value in package["requirements"].items(): + requirements[key.decode()] = value.decode() + + package["requirements"] = requirements + + package["python-versions"] = python_versions + + self._written_data = data + self._lock_data = data + + +@pytest.fixture() +def package(): + p = ProjectPackage("root", "1.0") + p.root_dir = Path.cwd() + + return p + + +@pytest.fixture() +def repo(): + return Repository() + + +@pytest.fixture() +def pool(repo): + pool = Pool() + pool.add_repository(repo) + + return pool + + +@pytest.fixture() +def installed(): + return CustomInstalledRepository() + + +@pytest.fixture() +def locker(): + return Locker() + + +@pytest.fixture() +def env(): + return NullEnv() + + +@pytest.fixture() +def installer(package, pool, locker, env, installed, config): + return Installer(NullIO(), env, package, locker, pool, config, installed=installed) + + +def fixture(name): + file = TomlFile(Path(__file__).parent / "fixtures" / "{}.test".format(name)) + + return file.read() + + +def test_run_no_dependencies(installer, locker): + installer.run() + expected = fixture("no-dependencies") + + assert locker.written_data == expected + + +def test_run_with_dependencies(installer, locker, repo, package): + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + repo.add_package(package_a) + repo.add_package(package_b) + + package.add_dependency("A", "~1.0") + package.add_dependency("B", "^1.0") + + installer.run() + expected = fixture("with-dependencies") + + assert locker.written_data == expected + + +def test_run_update_after_removing_dependencies( + installer, locker, repo, package, installed +): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "B", + "version": "1.1", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "C", + "version": "1.2", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": [], "C": []}, + }, + } + ) + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_c = get_package("C", "1.2") + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + installed.add_package(package_a) + installed.add_package(package_b) + installed.add_package(package_c) + + package.add_dependency("A", "~1.0") + package.add_dependency("B", "~1.1") + + installer.update(True) + installer.run() + expected = fixture("with-dependencies") + + assert locker.written_data == expected + + installs = installer.installer.installs + assert len(installs) == 0 + + updates = installer.installer.updates + assert len(updates) == 0 + + removals = installer.installer.removals + assert len(removals) == 1 + + +def test_run_install_no_dev(installer, locker, repo, package, installed): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "B", + "version": "1.1", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "C", + "version": "1.2", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": [], "C": []}, + }, + } + ) + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_c = get_package("C", "1.2") + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + installed.add_package(package_a) + installed.add_package(package_b) + installed.add_package(package_c) + + package.add_dependency("A", "~1.0") + package.add_dependency("B", "~1.1") + package.add_dependency("C", "~1.2", category="dev") + + installer.dev_mode(False) + installer.run() + + installs = installer.installer.installs + assert len(installs) == 0 + + updates = installer.installer.updates + assert len(updates) == 0 + + removals = installer.installer.removals + assert len(removals) == 1 + + +def test_run_install_remove_untracked(installer, locker, repo, package, installed): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "a", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + } + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"a": []}, + }, + } + ) + package_a = get_package("a", "1.0") + package_b = get_package("b", "1.1") + package_c = get_package("c", "1.2") + package_pip = get_package("pip", "20.0.0") + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_pip) + + installed.add_package(package_a) + installed.add_package(package_b) + installed.add_package(package_c) + installed.add_package(package_pip) # Always required and never removed. + installed.add_package(package) # Root package never removed. + + package.add_dependency("A", "~1.0") + + installer.dev_mode(True).remove_untracked(True) + installer.run() + + installs = installer.installer.installs + assert len(installs) == 0 + + updates = installer.installer.updates + assert len(updates) == 0 + + removals = installer.installer.removals + assert set(r.name for r in removals) == {"b", "c"} + + +def test_run_whitelist_add(installer, locker, repo, package): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + } + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": []}, + }, + } + ) + package_a = get_package("A", "1.0") + package_a_new = get_package("A", "1.1") + package_b = get_package("B", "1.1") + repo.add_package(package_a) + repo.add_package(package_a_new) + repo.add_package(package_b) + + package.add_dependency("A", "~1.0") + package.add_dependency("B", "^1.0") + + installer.update(True) + installer.whitelist(["B"]) + + installer.run() + expected = fixture("with-dependencies") + + assert locker.written_data == expected + + +def test_run_whitelist_remove(installer, locker, repo, package, installed): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "B", + "version": "1.1", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": []}, + }, + } + ) + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + repo.add_package(package_a) + repo.add_package(package_b) + installed.add_package(package_b) + + package.add_dependency("A", "~1.0") + + installer.update(True) + installer.whitelist(["B"]) + + installer.run() + expected = fixture("remove") + + assert locker.written_data == expected + assert len(installer.installer.installs) == 1 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 1 + + +def test_add_with_sub_dependencies(installer, locker, repo, package): + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_c = get_package("C", "1.2") + package_d = get_package("D", "1.3") + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_d) + + package.add_dependency("A", "~1.0") + package.add_dependency("B", "^1.0") + + package_a.add_dependency("D", "^1.0") + package_b.add_dependency("C", "~1.2") + + installer.run() + expected = fixture("with-sub-dependencies") + + assert locker.written_data == expected + + +def test_run_with_python_versions(installer, locker, repo, package): + package.python_versions = "~2.7 || ^3.4" + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_c12 = get_package("C", "1.2") + package_c12.python_versions = "~2.7 || ^3.3" + package_c13 = get_package("C", "1.3") + package_c13.python_versions = "~3.3" + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c12) + repo.add_package(package_c13) + + package.add_dependency("A", "~1.0") + package.add_dependency("B", "^1.0") + package.add_dependency("C", "^1.0") + + installer.run() + expected = fixture("with-python-versions") + + assert locker.written_data == expected + + +def test_run_with_optional_and_python_restricted_dependencies( + installer, locker, repo, package +): + package.python_versions = "~2.7 || ^3.4" + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_c12 = get_package("C", "1.2") + package_c13 = get_package("C", "1.3") + package_d = get_package("D", "1.4") + package_c13.add_dependency("D", "^1.2") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c12) + repo.add_package(package_c13) + repo.add_package(package_d) + + package.extras = {"foo": [get_dependency("A", "~1.0")]} + package.add_dependency("A", {"version": "~1.0", "optional": True}) + package.add_dependency("B", {"version": "^1.0", "python": "~2.4"}) + package.add_dependency("C", {"version": "^1.0", "python": "~2.7 || ^3.4"}) + + installer.run() + expected = fixture("with-optional-dependencies") + + assert locker.written_data == expected + + installer = installer.installer + # We should only have 2 installs: + # C,D since python version is not compatible + # with B's python constraint and A is optional + assert len(installer.installs) == 2 + assert installer.installs[0].name == "d" + assert installer.installs[1].name == "c" + + +def test_run_with_optional_and_platform_restricted_dependencies( + installer, locker, repo, package, mocker +): + mocker.patch("sys.platform", "darwin") + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_c12 = get_package("C", "1.2") + package_c13 = get_package("C", "1.3") + package_d = get_package("D", "1.4") + package_c13.add_dependency("D", "^1.2") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c12) + repo.add_package(package_c13) + repo.add_package(package_d) + + package.extras = {"foo": [get_dependency("A", "~1.0")]} + package.add_dependency("A", {"version": "~1.0", "optional": True}) + package.add_dependency("B", {"version": "^1.0", "platform": "custom"}) + package.add_dependency("C", {"version": "^1.0", "platform": "darwin"}) + + installer.run() + expected = fixture("with-platform-dependencies") + + assert locker.written_data == expected + + installer = installer.installer + # We should only have 2 installs: + # C,D since the mocked python version is not compatible + # with B's python constraint and A is optional + assert len(installer.installs) == 2 + assert installer.installs[0].name == "d" + assert installer.installs[1].name == "c" + + +def test_run_with_dependencies_extras(installer, locker, repo, package): + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + + package_b.extras = {"foo": [get_dependency("C", "^1.0")]} + package_b.add_dependency("C", {"version": "^1.0", "optional": True}) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + package.add_dependency("A", "^1.0") + package.add_dependency("B", {"version": "^1.0", "extras": ["foo"]}) + + installer.run() + expected = fixture("with-dependencies-extras") + + assert locker.written_data == expected + + +def test_run_does_not_install_extras_if_not_requested(installer, locker, repo, package): + package.extras["foo"] = [get_dependency("D")] + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + package_d = get_package("D", "1.1") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_d) + + package.add_dependency("A", "^1.0") + package.add_dependency("B", "^1.0") + package.add_dependency("C", "^1.0") + package.add_dependency("D", {"version": "^1.0", "optional": True}) + + installer.run() + expected = fixture("extras") + + # Extras are pinned in lock + assert locker.written_data == expected + + # But should not be installed + installer = installer.installer + assert len(installer.installs) == 3 # A, B, C + + +def test_run_installs_extras_if_requested(installer, locker, repo, package): + package.extras["foo"] = [get_dependency("D")] + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + package_d = get_package("D", "1.1") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_d) + + package.add_dependency("A", "^1.0") + package.add_dependency("B", "^1.0") + package.add_dependency("C", "^1.0") + package.add_dependency("D", {"version": "^1.0", "optional": True}) + + installer.extras(["foo"]) + installer.run() + expected = fixture("extras") + + # Extras are pinned in lock + assert locker.written_data == expected + + # But should not be installed + installer = installer.installer + assert len(installer.installs) == 4 # A, B, C, D + + +def test_run_installs_extras_with_deps_if_requested(installer, locker, repo, package): + package.extras["foo"] = [get_dependency("C")] + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + package_d = get_package("D", "1.1") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_d) + + package.add_dependency("A", "^1.0") + package.add_dependency("B", "^1.0") + package.add_dependency("C", {"version": "^1.0", "optional": True}) + + package_c.add_dependency("D", "^1.0") + + installer.extras(["foo"]) + installer.run() + expected = fixture("extras-with-dependencies") + + # Extras are pinned in lock + assert locker.written_data == expected + + # But should not be installed + installer = installer.installer + assert len(installer.installs) == 4 # A, B, C, D + + +def test_run_installs_extras_with_deps_if_requested_locked( + installer, locker, repo, package +): + locker.locked(True) + locker.mock_lock_data(fixture("extras-with-dependencies")) + package.extras["foo"] = [get_dependency("C")] + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + package_d = get_package("D", "1.1") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_d) + + package.add_dependency("A", "^1.0") + package.add_dependency("B", "^1.0") + package.add_dependency("C", {"version": "^1.0", "optional": True}) + + package_c.add_dependency("D", "^1.0") + + installer.extras(["foo"]) + installer.run() + + # But should not be installed + installer = installer.installer + assert len(installer.installs) == 4 # A, B, C, D + + +def test_installer_with_pypi_repository(package, locker, installed, config): + pool = Pool() + pool.add_repository(MockRepository()) + + installer = Installer( + NullIO(), NullEnv(), package, locker, pool, config, installed=installed + ) + + package.add_dependency("pytest", "^3.5", category="dev") + installer.run() + + expected = fixture("with-pypi-repository") + + assert locker.written_data == expected + + +def test_run_installs_with_local_file(installer, locker, repo, package): + file_path = fixtures_dir / "distributions/demo-0.1.0-py2.py3-none-any.whl" + package.add_dependency("demo", {"file": str(file_path)}) + + repo.add_package(get_package("pendulum", "1.4.4")) + + installer.run() + + expected = fixture("with-file-dependency") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 2 + + +def test_run_installs_wheel_with_no_requires_dist(installer, locker, repo, package): + file_path = ( + fixtures_dir / "wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.whl" + ) + package.add_dependency("demo", {"file": str(file_path)}) + + installer.run() + + expected = fixture("with-wheel-dependency-no-requires-dist") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 1 + + +def test_run_installs_with_local_poetry_directory_and_extras( + installer, locker, repo, package, tmpdir +): + file_path = fixtures_dir / "project_with_extras" + package.add_dependency( + "project-with-extras", {"path": str(file_path), "extras": ["extras_a"]} + ) + + repo.add_package(get_package("pendulum", "1.4.4")) + + installer.run() + + expected = fixture("with-directory-dependency-poetry") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 2 + + +def test_run_installs_with_local_poetry_directory_transitive( + installer, locker, repo, package, tmpdir +): + package.root_dir = fixtures_dir.joinpath("directory") + directory = fixtures_dir.joinpath("directory").joinpath( + "project_with_transitive_directory_dependencies" + ) + package.add_dependency( + "project-with-transitive-directory-dependencies", + {"path": str(directory.relative_to(fixtures_dir.joinpath("directory")))}, + ) + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cachy", "0.2.0")) + + installer.run() + + expected = fixture("with-directory-dependency-poetry-transitive") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 6 + + +def test_run_installs_with_local_poetry_file_transitive( + installer, locker, repo, package, tmpdir +): + package.root_dir = fixtures_dir.joinpath("directory") + directory = fixtures_dir.joinpath("directory").joinpath( + "project_with_transitive_file_dependencies" + ) + package.add_dependency( + "project-with-transitive-file-dependencies", + {"path": str(directory.relative_to(fixtures_dir.joinpath("directory")))}, + ) + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cachy", "0.2.0")) + + installer.run() + + expected = fixture("with-file-dependency-transitive") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 4 + + +def test_run_installs_with_local_setuptools_directory( + installer, locker, repo, package, tmpdir +): + file_path = fixtures_dir / "project_with_setup/" + package.add_dependency("my-package", {"path": str(file_path)}) + + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("cachy", "0.2.0")) + + installer.run() + + expected = fixture("with-directory-dependency-setuptools") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 3 + + +def test_run_with_prereleases(installer, locker, repo, package): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0a2", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + } + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": []}, + }, + } + ) + package_a = get_package("A", "1.0a2") + package_b = get_package("B", "1.1") + repo.add_package(package_a) + repo.add_package(package_b) + + package.add_dependency("A", {"version": "*", "allows-prereleases": True}) + package.add_dependency("B", "^1.1") + + installer.update(True) + installer.whitelist({"B": "^1.1"}) + + installer.run() + expected = fixture("with-prereleases") + + assert locker.written_data == expected + + +def test_run_changes_category_if_needed(installer, locker, repo, package): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "dev", + "optional": True, + "platform": "*", + "python-versions": "*", + "checksum": [], + } + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": []}, + }, + } + ) + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.1") + package_b.add_dependency("A", "^1.0") + repo.add_package(package_a) + repo.add_package(package_b) + + package.add_dependency("A", {"version": "^1.0", "optional": True}, category="dev") + package.add_dependency("B", "^1.1") + + installer.update(True) + installer.whitelist(["B"]) + + installer.run() + expected = fixture("with-category-change") + + assert locker.written_data == expected + + +def test_run_update_all_with_lock(installer, locker, repo, package): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "dev", + "optional": True, + "platform": "*", + "python-versions": "*", + "checksum": [], + } + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": []}, + }, + } + ) + package_a = get_package("A", "1.1") + repo.add_package(get_package("A", "1.0")) + repo.add_package(package_a) + + package.add_dependency("A") + + installer.update(True) + + installer.run() + expected = fixture("update-with-lock") + + assert locker.written_data == expected + + +def test_run_update_with_locked_extras(installer, locker, repo, package): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"B": "^1.0", "C": "^1.0"}, + }, + { + "name": "B", + "version": "1.0", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "C", + "version": "1.1", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "requirements": {"python": "~2.7"}, + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": [], "C": []}, + }, + } + ) + package_a = get_package("A", "1.0") + package_a.extras["foo"] = [get_dependency("B")] + b_dependency = get_dependency("B", "^1.0", optional=True) + b_dependency.in_extras.append("foo") + c_dependency = get_dependency("C", "^1.0") + c_dependency.python_versions = "~2.7" + package_a.requires.append(b_dependency) + package_a.requires.append(c_dependency) + + repo.add_package(package_a) + repo.add_package(get_package("B", "1.0")) + repo.add_package(get_package("C", "1.1")) + repo.add_package(get_package("D", "1.1")) + + package.add_dependency("A", {"version": "^1.0", "extras": ["foo"]}) + package.add_dependency("D", "^1.0") + + installer.update(True) + installer.whitelist("D") + + installer.run() + expected = fixture("update-with-locked-extras") + + assert locker.written_data == expected + + +def test_run_install_duplicate_dependencies_different_constraints( + installer, locker, repo, package +): + package.add_dependency("A") + + package_a = get_package("A", "1.0") + package_a.add_dependency("B", {"version": "^1.0", "python": "<4.0"}) + package_a.add_dependency("B", {"version": "^2.0", "python": ">=4.0"}) + + package_b10 = get_package("B", "1.0") + package_b20 = get_package("B", "2.0") + package_b10.add_dependency("C", "1.2") + package_b20.add_dependency("C", "1.5") + + package_c12 = get_package("C", "1.2") + package_c15 = get_package("C", "1.5") + + repo.add_package(package_a) + repo.add_package(package_b10) + repo.add_package(package_b20) + repo.add_package(package_c12) + repo.add_package(package_c15) + + installer.run() + + expected = fixture("with-duplicate-dependencies") + + assert locker.written_data == expected + + installs = installer.installer.installs + assert len(installs) == 3 + assert installs[0] == package_c12 + assert installs[1] == package_b10 + assert installs[2] == package_a + + updates = installer.installer.updates + assert len(updates) == 0 + removals = installer.installer.removals + assert len(removals) == 0 + + +def test_run_install_duplicate_dependencies_different_constraints_with_lock( + installer, locker, repo, package +): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": { + "B": [ + {"version": "^1.0", "python": "<4.0"}, + {"version": "^2.0", "python": ">=4.0"}, + ] + }, + }, + { + "name": "B", + "version": "1.0", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"C": "1.2"}, + "requirements": {"python": "<4.0"}, + }, + { + "name": "B", + "version": "2.0", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"C": "1.5"}, + "requirements": {"python": ">=4.0"}, + }, + { + "name": "C", + "version": "1.2", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "C", + "version": "1.5", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": [], "C": []}, + }, + } + ) + package.add_dependency("A") + + package_a = get_package("A", "1.0") + package_a.add_dependency("B", {"version": "^1.0", "python": "<4.0"}) + package_a.add_dependency("B", {"version": "^2.0", "python": ">=4.0"}) + + package_b10 = get_package("B", "1.0") + package_b20 = get_package("B", "2.0") + package_b10.add_dependency("C", "1.2") + package_b20.add_dependency("C", "1.5") + + package_c12 = get_package("C", "1.2") + package_c15 = get_package("C", "1.5") + + repo.add_package(package_a) + repo.add_package(package_b10) + repo.add_package(package_b20) + repo.add_package(package_c12) + repo.add_package(package_c15) + + installer.update(True) + installer.run() + + expected = fixture("with-duplicate-dependencies") + + assert locker.written_data == expected + + installs = installer.installer.installs + assert len(installs) == 3 + updates = installer.installer.updates + assert len(updates) == 0 + removals = installer.installer.removals + assert len(removals) == 0 + + +def test_run_update_uninstalls_after_removal_transient_dependency( + installer, locker, repo, package, installed +): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"B": {"version": "^1.0", "python": "<2.0"}}, + }, + { + "name": "B", + "version": "1.0", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": []}, + }, + } + ) + package.add_dependency("A") + + package_a = get_package("A", "1.0") + package_a.add_dependency("B", {"version": "^1.0", "python": "<2.0"}) + + package_b10 = get_package("B", "1.0") + + repo.add_package(package_a) + repo.add_package(package_b10) + + installed.add_package(get_package("A", "1.0")) + installed.add_package(get_package("B", "1.0")) + + installer.update(True) + installer.run() + + installs = installer.installer.installs + assert len(installs) == 0 + updates = installer.installer.updates + assert len(updates) == 0 + removals = installer.installer.removals + assert len(removals) == 1 + + +def test_run_install_duplicate_dependencies_different_constraints_with_lock_update( + installer, locker, repo, package, installed +): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": { + "B": [ + {"version": "^1.0", "python": "<2.7"}, + {"version": "^2.0", "python": ">=2.7"}, + ] + }, + }, + { + "name": "B", + "version": "1.0", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"C": "1.2"}, + "requirements": {"python": "<2.7"}, + }, + { + "name": "B", + "version": "2.0", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"C": "1.5"}, + "requirements": {"python": ">=2.7"}, + }, + { + "name": "C", + "version": "1.2", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + { + "name": "C", + "version": "1.5", + "category": "dev", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": [], "C": []}, + }, + } + ) + package.add_dependency("A") + + package_a = get_package("A", "1.1") + package_a.add_dependency("B", "^2.0") + + package_b10 = get_package("B", "1.0") + package_b20 = get_package("B", "2.0") + package_b10.add_dependency("C", "1.2") + package_b20.add_dependency("C", "1.5") + + package_c12 = get_package("C", "1.2") + package_c15 = get_package("C", "1.5") + + repo.add_package(package_a) + repo.add_package(package_b10) + repo.add_package(package_b20) + repo.add_package(package_c12) + repo.add_package(package_c15) + + installed.add_package(get_package("A", "1.0")) + + installer.update(True) + installer.whitelist(["A"]) + installer.run() + + expected = fixture("with-duplicate-dependencies-update") + + assert locker.written_data == expected + + installs = installer.installer.installs + assert len(installs) == 2 + updates = installer.installer.updates + assert len(updates) == 1 + removals = installer.installer.removals + assert len(removals) == 0 + + +@pytest.mark.skip( + "This is not working at the moment due to limitations in the resolver" +) +def test_installer_test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python( + installer, locker, repo, package, installed +): + package.python_versions = "~2.7 || ^3.4" + package.add_dependency("A", {"version": "^1.0", "python": "^3.5"}) + + package_a101 = get_package("A", "1.0.1") + package_a101.python_versions = ">=3.6" + + package_a100 = get_package("A", "1.0.0") + package_a100.python_versions = ">=3.5" + + repo.add_package(package_a100) + repo.add_package(package_a101) + + installer.run() + + expected = fixture("with-conditional-dependency") + assert locker.written_data == expected + + installs = installer.installer.installs + + if sys.version_info >= (3, 5, 0): + assert len(installs) == 1 + else: + assert len(installs) == 0 + + +def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency( + installer, locker, repo, package, installed, env, pool, config +): + package.add_dependency("A", {"version": "^1.0"}) + + package_a = get_package("A", "1.0.0") + package_a.add_dependency("B", {"version": "^1.0", "extras": ["foo"]}) + + package_b = get_package("B", "1.0.0") + package_b.add_dependency("C", {"version": "^1.0", "optional": True}) + package_b.extras = {"foo": [get_dependency("C")]} + + package_c = get_package("C", "1.0.0") + package_d = get_package("D", "1.0.0") + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + repo.add_package(package_d) + + installer.update(True) + installer.run() + + assert len(installer.installer.installs) == 3 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + package.add_dependency("D", "^1.0") + locker.locked(True) + locker.mock_lock_data(locker.written_data) + + installed.add_package(package_a) + installed.add_package(package_b) + installed.add_package(package_c) + + installer = Installer( + NullIO(), env, package, locker, pool, config, installed=installed + ) + + installer.update(True) + installer.whitelist(["D"]) + installer.run() + + assert len(installer.installer.installs) == 1 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + +def test_installer_required_extras_should_not_be_removed_when_updating_single_dependency_pypi_repository( + locker, repo, package, installed, env, mocker, config +): + mocker.patch("sys.platform", "darwin") + + pool = Pool() + pool.add_repository(MockRepository()) + + installer = Installer( + NullIO(), env, package, locker, pool, config, installed=installed + ) + + package.add_dependency("poetry", {"version": "^0.12.0"}) + + installer.update(True) + installer.run() + + assert len(installer.installer.installs) == 3 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + package.add_dependency("pytest", "^3.5") + + locker.locked(True) + locker.mock_lock_data(locker.written_data) + + for pkg in installer.installer.installs: + installed.add_package(pkg) + + installer = Installer( + NullIO(), env, package, locker, pool, config, installed=installed + ) + + installer.update(True) + installer.whitelist(["pytest"]) + installer.run() + + assert len(installer.installer.installs) == 6 if not PY2 else 7 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + +def test_installer_required_extras_should_be_installed( + locker, repo, package, installed, env, config +): + pool = Pool() + pool.add_repository(MockRepository()) + + installer = Installer( + NullIO(), env, package, locker, pool, config, installed=installed + ) + + package.add_dependency( + "cachecontrol", {"version": "^0.12.5", "extras": ["filecache"]} + ) + + installer.update(True) + installer.run() + + assert len(installer.installer.installs) == 2 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + locker.locked(True) + locker.mock_lock_data(locker.written_data) + + installer = Installer( + NullIO(), env, package, locker, pool, config, installed=installed + ) + + installer.update(True) + installer.run() + + assert len(installer.installer.installs) == 2 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + +def test_update_multiple_times_with_split_dependencies_is_idempotent( + installer, locker, repo, package +): + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"B": ">=1.0"}, + }, + { + "name": "B", + "version": "1.0.1", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", + "checksum": [], + "dependencies": {}, + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "hashes": {"A": [], "B": []}, + }, + } + ) + + package.python_versions = "~2.7 || ^3.4" + package.add_dependency("A", "^1.0") + + a10 = get_package("A", "1.0") + a11 = get_package("A", "1.1") + a11.add_dependency("B", ">=1.0.1") + a11.add_dependency("C", {"version": "^1.0", "python": "~2.7"}) + a11.add_dependency("C", {"version": "^2.0", "python": "^3.4"}) + b101 = get_package("B", "1.0.1") + b110 = get_package("B", "1.1.0") + repo.add_package(a10) + repo.add_package(a11) + repo.add_package(b101) + repo.add_package(b110) + repo.add_package(get_package("C", "1.0")) + repo.add_package(get_package("C", "2.0")) + expected = fixture("with-multiple-updates") + + installer.update(True) + installer.run() + + assert expected == locker.written_data + + locker.mock_lock_data(locker.written_data) + + installer.update(True) + installer.run() + + assert expected == locker.written_data + + locker.mock_lock_data(locker.written_data) + + installer.update(True) + installer.run() + + assert expected == locker.written_data + + +def test_installer_can_install_dependencies_from_forced_source( + locker, package, installed, env, config +): + package.python_versions = "^3.7" + package.add_dependency("tomlkit", {"version": "^0.5", "source": "legacy"}) + + pool = Pool() + pool.add_repository(MockLegacyRepository()) + pool.add_repository(MockRepository()) + + installer = Installer( + NullIO(), env, package, locker, pool, config, installed=installed + ) + + installer.update(True) + installer.run() + + assert len(installer.installer.installs) == 1 + assert len(installer.installer.updates) == 0 + assert len(installer.installer.removals) == 0 + + +def test_run_installs_with_url_file(installer, locker, repo, package): + url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" + package.add_dependency("demo", {"url": url}) + + repo.add_package(get_package("pendulum", "1.4.4")) + + installer.run() + + expected = fixture("with-url-dependency") + + assert locker.written_data == expected + + assert len(installer.installer.installs) == 2 + + +def test_installer_uses_prereleases_if_they_are_compatible( + installer, locker, package, repo +): + package.python_versions = "~2.7 || ^3.4" + package.add_dependency( + "prerelease", {"git": "https://github.com/demo/prerelease.git"} + ) + + package_b = get_package("b", "2.0.0") + package_b.add_dependency("prerelease", ">=0.19") + + repo.add_package(package_b) + + installer.run() + + del installer.installer.installs[:] + locker.locked(True) + locker.mock_lock_data(locker.written_data) + + package.add_dependency("b", "^2.0.0") + + installer.whitelist(["b"]) + installer.update(True) + installer.run() + + assert len(installer.installer.installs) == 2 + + +def test_installer_can_handle_old_lock_files( + installer, locker, package, repo, installed, config +): + pool = Pool() + pool.add_repository(MockRepository()) + + package.add_dependency("pytest", "^3.5", category="dev") + + locker.locked() + locker.mock_lock_data(fixture("old-lock")) + + installer = Installer( + NullIO(), MockEnv(), package, locker, pool, config, installed=installed + ) + + installer.run() + + assert 6 == len(installer.installer.installs) + + installer = Installer( + NullIO(), + MockEnv(version_info=(2, 7, 18)), + package, + locker, + pool, + config, + installed=installed, + ) + + installer.run() + + # funcsigs will be added + assert 7 == len(installer.installer.installs) + + installer = Installer( + NullIO(), + MockEnv(version_info=(2, 7, 18), platform="win32"), + package, + locker, + pool, + config, + installed=installed, + ) + + installer.run() + + # colorama will be added + assert 8 == len(installer.installer.installs) diff --git a/tests/repositories/fixtures/legacy/pytest.html b/tests/repositories/fixtures/legacy/pytest.html new file mode 100644 index 00000000000..487444cc0ce --- /dev/null +++ b/tests/repositories/fixtures/legacy/pytest.html @@ -0,0 +1,11 @@ + + + + Links for pytest + + +

Links for pytest

pytest-3.5.0-py2.py3-none-any.whl
+ pytest-3.5.0.tar.gz
+ + + diff --git a/tests/repositories/fixtures/legacy/pyyaml.html b/tests/repositories/fixtures/legacy/pyyaml.html index f77abc1a161..a5007a9866c 100644 --- a/tests/repositories/fixtures/legacy/pyyaml.html +++ b/tests/repositories/fixtures/legacy/pyyaml.html @@ -5,6 +5,7 @@

Links for python-language-server

+ PyYAML-3.13-cp37-cp37m-win32.whl
PyYAML-3.13.tar.gz
PyYAML-4.2b2.tar.gz
diff --git a/tox.ini b/tox.ini index 1f0a4b9ede4..8a36104610f 100644 --- a/tox.ini +++ b/tox.ini @@ -7,5 +7,5 @@ envlist = py27, py35, py36, py37, py38 whitelist_externals = poetry skip_install = true commands = - poetry install -vvv + poetry install -vv poetry run pytest {posargs} tests/