From 993eaac126653fbac484ec2d3dd7c234afce98cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Fri, 12 Jul 2019 17:52:44 +0200 Subject: [PATCH] Add the ability to add complete dependencies in one go --- docs/docs/cli.md | 12 +++- poetry/console/commands/init.py | 50 ++++++++++++-- tests/console/commands/test_add.py | 106 ++++++++++++++++++++++++++++- 3 files changed, 159 insertions(+), 9 deletions(-) diff --git a/docs/docs/cli.md b/docs/docs/cli.md index 75aa2f922c2..e6310d82672 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -186,6 +186,7 @@ You also can specify a constraint when adding a package, like so: ```bash poetry add pendulum@^2.0.5 +poetry add "pendulum>=2.0.5" ``` If you try to add a package that is already present, you will get an error. @@ -224,11 +225,20 @@ It means that changes in the local directory will be reflected directly in envir If you don't want the dependency to be installed in editable mode you can specify it in the `pyproject.toml` file: -``` +```toml [tool.poetry.dependencies] my-package = {path = "../my/path", develop = false} ``` +If the package(s) you want to install provide extras, you can specify them +when adding the package: + +```bash +poetry add requests[security,socks] +poetry add "requests[security,socks]~=2.22.0" +poetry add "git+https://github.com/pallets/flask.git@1.1.1[dotenv,dev]" +``` + ### Options * `--dev (-D)`: Add package as development dependency. diff --git a/poetry/console/commands/init.py b/poetry/console/commands/init.py index 52e1d2568f3..46c42bf03fb 100644 --- a/poetry/console/commands/init.py +++ b/poetry/console/commands/init.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import os import re from typing import Dict @@ -10,7 +11,6 @@ from cleo import option from tomlkit import inline_table -from tomlkit import table from poetry.utils._compat import Path from poetry.utils._compat import OrderedDict @@ -336,6 +336,13 @@ def _parse_requirements( cwd = Path.cwd() for requirement in requirements: + requirement = requirement.strip() + extras = [] + extras_m = re.search(r"\[([\w\d,-_]+)\]$", requirement) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + requirement, _ = requirement.split("[") + if requirement.startswith(("git+https://", "git+ssh://")): url = requirement.lstrip("git+") rev = None @@ -348,6 +355,9 @@ def _parse_requirements( if rev: pair["rev"] = rev + if extras: + pair["extras"] = extras + package = Provider.get_package_from_vcs( "git", url, reference=pair.get("rev") ) @@ -355,7 +365,9 @@ def _parse_requirements( result.append(pair) continue - elif cwd.joinpath(requirement).exists(): + elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath( + requirement + ).exists(): path = cwd.joinpath(requirement) if path.is_file(): package = Provider.get_package_from_file(path.resolve()) @@ -368,19 +380,47 @@ def _parse_requirements( ("name", package.name), ("path", path.relative_to(cwd).as_posix()), ] + + ([("extras", extras)] if extras else []) ) ) continue - pair = re.sub("^([^=: ]+)[@=: ](.*)$", "\\1 \\2", requirement.strip()) + pair = re.sub( + "^([^@=: ]+)(?:@|==|(?~!])=|:| )(.*)$", "\\1 \\2", requirement + ) pair = pair.strip() + require = OrderedDict() if " " in pair: name, version = pair.split(" ", 2) - result.append({"name": name, "version": version}) + require["name"] = name + require["version"] = version else: - result.append({"name": pair}) + m = re.match( + "^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip() + ) + if m: + name, constraint = m.group(1), m.group(2) + extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + name, _ = name.split("[") + + require["name"] = name + require["version"] = constraint + else: + extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + pair, _ = pair.split("[") + + require["name"] = pair + + if extras: + require["extras"] = extras + + result.append(require) return result diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 58526da6509..0ed94470446 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -42,14 +42,14 @@ def test_add_no_constraint(app, repo, installer): assert content["dependencies"]["cachy"] == "^0.2.0" -def test_add_constraint(app, repo, installer): +def test_add_equal_constraint(app, repo, installer): command = app.find("add") tester = CommandTester(command) repo.add_package(get_package("cachy", "0.1.0")) repo.add_package(get_package("cachy", "0.2.0")) - tester.execute("cachy=0.1.0") + tester.execute("cachy==0.1.0") expected = """\ @@ -69,6 +69,67 @@ def test_add_constraint(app, repo, installer): assert len(installer.installs) == 1 +def test_add_greater_constraint(app, repo, installer): + command = app.find("add") + tester = CommandTester(command) + + 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 len(installer.installs) == 1 + + +def test_add_constraint_with_extras(app, repo, installer): + command = app.find("add") + tester = CommandTester(command) + + 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") + + 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 len(installer.installs) == 2 + + def test_add_constraint_dependencies(app, repo, installer): command = app.find("add") tester = CommandTester(command) @@ -164,6 +225,45 @@ def test_add_git_constraint_with_poetry(app, repo, installer): assert len(installer.installs) == 2 +def test_add_git_constraint_with_extras(app, repo, installer): + 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")) + 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 len(installer.installs) == 4 + + 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_directory_constraint(app, repo, installer, mocker): p = mocker.patch("poetry.utils._compat.Path.cwd") p.return_value = Path(__file__) / ".." @@ -304,7 +404,7 @@ def test_add_file_constraint_sdist(app, repo, installer, mocker): } -def test_add_constraint_with_extras(app, repo, installer): +def test_add_constraint_with_extras_option(app, repo, installer): command = app.find("add") tester = CommandTester(command)