diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d14eb98cca..bfcd1468665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ - Added the ability to specify packages on a per-format basis. - Added support for custom urls in metadata. - Full environment markers are now supported for dependencies via the `markers` property. +- Added the ability to specify git dependencies directly in `add`, it no longer requires the `--git` option. +- Added the ability to specify path dependencies directly in `add`, it no longer requires the `--path` option. +- Added the ability to add git and path dependencies via the `init` command. ### Changed @@ -24,6 +27,9 @@ - The `debug:resolve` command has been renamed to `debug resolve`. - The `self:update` command has been renamed to `self update`. - Changed the way virtualenvs are stored (names now depend on the project's path). +- The `--git` option of the `add` command has been removed. +- The `--path` option of the `add` command has been removed. +- The `add` command will now automatically select the latest prerelease if only prereleases are available. ### Fixed diff --git a/docs/docs/cli.md b/docs/docs/cli.md index 1aa66170c87..e6310d82672 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -182,18 +182,42 @@ poetry will choose a suitable one based on the available package versions. poetry add requests pendulum ``` +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. +However, if you specify a constraint, like above, the dependency will be updated +by using the specified constraint. If you want to get the latest version of an already +present dependency you can use the special `latest` constraint: + +```bash +poetry add pendulum@latest +``` + You can also add `git` dependencies: ```bash -poetry add pendulum --git https://github.com/sdispater/pendulum.git +poetry add git+https://github.com/sdispater/pendulum.git +``` + +If you need to checkout a specific branch, tag or revision, +you can specify it when using `add`: + +```bash +poetry add git+https://github.com/sdispater/pendulum.git@develop +poetry add git+https://github.com/sdispater/pendulum.git@2.0.5 ``` or make them point to a local directory or file: ```bash -poetry add my-package --path ../my-package/ -poetry add my-package --path ../my-package/dist/my-package-0.1.0.tar.gz -poetry add my-package --path ../my-package/dist/my_package-0.1.0.whl +poetry add ./my-package/ +poetry add ../my-package/dist/my-package-0.1.0.tar.gz +poetry add ../my-package/dist/my_package-0.1.0.whl ``` Path dependencies pointing to a local directory will be installed in editable mode (i.e. setuptools "develop mode"). @@ -201,17 +225,24 @@ 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. -* `--git`: The url of the Git repository. * `--path`: The path to a dependency. -* `--extras (-E)`: Extras to activate for the dependency. * `--optional` : Add as an optional dependency. * `--dry-run` : Outputs the operations but will not execute anything (implicitly enables --verbose). diff --git a/poetry/console/commands/add.py b/poetry/console/commands/add.py index abfa27a85fa..9c44d66b2e7 100644 --- a/poetry/console/commands/add.py +++ b/poetry/console/commands/add.py @@ -13,8 +13,6 @@ class AddCommand(EnvCommand, InitCommand): arguments = [argument("name", "Packages to add.", multiple=True)] options = [ option("dev", "D", "Add package as development dependency."), - option("git", None, "The url of the Git repository.", flag=False), - option("path", None, "The path to a dependency.", flag=False), option( "extras", "E", @@ -58,17 +56,11 @@ def handle(self): packages = self.argument("name") is_dev = self.option("dev") - if (self.option("git") or self.option("path") or self.option("extras")) and len( - packages - ) > 1: + if self.option("extras") and len(packages) > 1: raise ValueError( - "You can only specify one package " - "when using the --git or --path options" + "You can only specify one package " "when using the --extras option" ) - if self.option("git") and self.option("path"): - raise RuntimeError("--git and --path cannot be used at the same time") - section = "dependencies" if is_dev: section = "dev-dependencies" @@ -83,32 +75,27 @@ def handle(self): for name in packages: for key in poetry_content[section]: if key.lower() == name.lower(): + pair = self._parse_requirements([name])[0] + if "git" in pair or pair.get("version") == "latest": + continue + raise ValueError("Package {} is already present".format(name)) - if self.option("git") or self.option("path"): - requirements = {packages[0]: ""} - else: - requirements = self._determine_requirements( - packages, allow_prereleases=self.option("allow-prereleases") - ) - requirements = self._format_requirements(requirements) + requirements = self._determine_requirements( + packages, allow_prereleases=self.option("allow-prereleases") + ) - # validate requirements format - for constraint in requirements.values(): - parse_constraint(constraint) + for _constraint in requirements: + if "version" in _constraint: + # Validate version constraint + parse_constraint(_constraint["version"]) - for name, _constraint in requirements.items(): constraint = inline_table() - constraint["version"] = _constraint - - if self.option("git"): - del constraint["version"] - - constraint["git"] = self.option("git") - elif self.option("path"): - del constraint["version"] + for name, value in _constraint.items(): + if name == "name": + continue - constraint["path"] = self.option("path") + constraint[name] = value if self.option("optional"): constraint["optional"] = True @@ -135,7 +122,7 @@ def handle(self): if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] - poetry_content[section][name] = constraint + poetry_content[section][_constraint["name"]] = constraint # Write new content self.poetry.file.write(content) @@ -152,7 +139,7 @@ def handle(self): installer.dry_run(self.option("dry-run")) installer.update(True) - installer.whitelist(requirements) + installer.whitelist([r["name"] for r in requirements]) try: status = installer.run() diff --git a/poetry/console/commands/init.py b/poetry/console/commands/init.py index ce3638fe773..46c42bf03fb 100644 --- a/poetry/console/commands/init.py +++ b/poetry/console/commands/init.py @@ -1,12 +1,19 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import os import re +from typing import Dict from typing import List from typing import Tuple +from typing import Union from cleo import option +from tomlkit import inline_table + +from poetry.utils._compat import Path +from poetry.utils._compat import OrderedDict from .command import Command from .env_command import EnvCommand @@ -133,10 +140,20 @@ def handle(self): requirements = {} - question = ( - "Would you like to define your dependencies" " (require) interactively?" + question = "Would you like to define your main dependencies interactively?" + help_message = ( + "You can specify a package in the following forms:\n" + " - A single name (requests)\n" + " - A name and a constraint (requests ^2.23.0)\n" + " - A git url (https://github.com/sdispater/poetry.git)\n" + " - A git url with a revision (https://github.com/sdispater/poetry.git@develop)\n" + " - A file path (../my-package/my-package.whl)\n" + " - A directory (../my-package/)\n" ) + help_displayed = False if self.confirm(question, True): + self.line(help_message) + help_displayed = True requirements = self._format_requirements( self._determine_requirements(self.option("dependency")) ) @@ -149,6 +166,9 @@ def handle(self): " (require-dev) interactively" ) if self.confirm(question, True): + if not help_displayed: + self.line(help_message) + dev_requirements = self._format_requirements( self._determine_requirements(self.option("dev-dependency")) ) @@ -182,13 +202,24 @@ def handle(self): def _determine_requirements( self, requires, allow_prereleases=False - ): # type: (List[str], bool) -> List[str] + ): # type: (List[str], bool) -> List[Dict[str, str]] if not requires: requires = [] - package = self.ask("Search for package:") + package = self.ask("Add a package:") while package is not None: - matches = self._get_pool().search(package) + constraint = self._parse_requirements([package])[0] + if ( + "git" in constraint + or "path" in constraint + or "version" in constraint + ): + self.line("Adding {}".format(package)) + requires.append(constraint) + package = self.ask("\nAdd a package:") + continue + + matches = self._get_pool().search(constraint["name"]) if not matches: self.line("Unable to find package") @@ -212,7 +243,7 @@ def _determine_requirements( ) # no constraint yet, determine the best version automatically - if package is not False and " " not in package: + if package is not False and "version" not in constraint: question = self.create_question( "Enter the version constraint to require " "(or leave blank to use the latest version):" @@ -220,30 +251,35 @@ def _determine_requirements( question.attempts = 3 question.validator = lambda x: (x or "").strip() or False - constraint = self.ask(question) + package_constraint = self.ask(question) - if constraint is None: - _, constraint = self._find_best_version_for_package(package) + if package_constraint is None: + _, package_constraint = self._find_best_version_for_package( + package + ) self.line( "Using version {} for {}".format( - constraint, package + package_constraint, package ) ) - package += " {}".format(constraint) + constraint["version"] = package_constraint if package is not False: - requires.append(package) + requires.append(constraint) - package = self.ask("\nSearch for a package:") + package = self.ask("\nAdd a package:") return requires - requires = self._parse_name_version_pairs(requires) + requires = self._parse_requirements(requires) result = [] for requirement in requires: - if "version" not in requirement: + if "git" in requirement or "path" in requirement: + result.append(requirement) + continue + elif "version" not in requirement: # determine the best version automatically name, version = self._find_best_version_for_package( requirement["name"], allow_prereleases=allow_prereleases @@ -265,7 +301,7 @@ def _determine_requirements( requirement["name"] = name - result.append("{} {}".format(requirement["name"], requirement["version"])) + result.append(requirement) return result @@ -285,28 +321,123 @@ def _find_best_version_for_package( "Could not find a matching version of package {}".format(name) ) - return (package.pretty_name, selector.find_recommended_require_version(package)) + return package.pretty_name, selector.find_recommended_require_version(package) + + def _parse_requirements( + self, requirements + ): # type: (List[str]) -> List[Dict[str, str]] + from poetry.puzzle.provider import Provider - def _parse_name_version_pairs(self, pairs): # type: (list) -> list result = [] - for i in range(len(pairs)): - pair = re.sub("^([^=: ]+)[=: ](.*)$", "\\1 \\2", pairs[i].strip()) + try: + cwd = self.poetry.file.parent + except RuntimeError: + 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 + if "@" in url: + url, rev = url.split("@") + + pair = OrderedDict( + [("name", url.split("/")[-1].rstrip(".git")), ("git", url)] + ) + if rev: + pair["rev"] = rev + + if extras: + pair["extras"] = extras + + package = Provider.get_package_from_vcs( + "git", url, reference=pair.get("rev") + ) + pair["name"] = package.name + result.append(pair) + + continue + 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()) + else: + package = Provider.get_package_from_directory(path) + + result.append( + OrderedDict( + [ + ("name", package.name), + ("path", path.relative_to(cwd).as_posix()), + ] + + ([("extras", extras)] if extras else []) + ) + ) + + continue + + 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 - def _format_requirements(self, requirements): # type: (List[str]) -> dict + def _format_requirements( + self, requirements + ): # type: (List[Dict[str, str]]) -> Dict[str, Union[str, Dict[str, str]]] requires = {} - requirements = self._parse_name_version_pairs(requirements) for requirement in requirements: - requires[requirement["name"]] = requirement["version"] + name = requirement.pop("name") + if "version" in requirement and len(requirement) == 1: + constraint = requirement["version"] + else: + constraint = inline_table() + constraint.trivia.trail = "\n" + constraint.update(requirement) + + requires[name] = constraint return requires diff --git a/poetry/puzzle/provider.py b/poetry/puzzle/provider.py index d13b4e86900..a08224fd486 100644 --- a/poetry/puzzle/provider.py +++ b/poetry/puzzle/provider.py @@ -9,6 +9,7 @@ from contextlib import contextmanager from tempfile import mkdtemp from typing import List +from typing import Optional from poetry.packages import Dependency from poetry.packages import DependencyPackage @@ -34,6 +35,7 @@ from poetry.utils.env import EnvManager from poetry.utils.env import EnvCommandError from poetry.utils.setup_reader import SetupReader +from poetry.utils.toml_file import TomlFile from poetry.version.markers import MarkerUnion from poetry.vcs.git import Git @@ -56,10 +58,7 @@ class Provider: UNSAFE_PACKAGES = {"setuptools", "distribute", "pip"} def __init__( - self, - package, # type: Package - pool, # type: Pool - io, + self, package, pool, io # type: Package # type: Pool ): # type: (...) -> None self._package = package self._pool = pool @@ -158,59 +157,92 @@ def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] Basically, we clone the repository in a temporary directory and get the information we need by checking out the specified reference. """ - if dependency.vcs != "git": - raise ValueError("Unsupported VCS dependency {}".format(dependency.vcs)) + package = self.get_package_from_vcs( + dependency.vcs, + dependency.source, + dependency.reference, + name=dependency.name, + ) - tmp_dir = Path(mkdtemp(prefix="pypoetry-git-{}".format(dependency.name))) + if dependency.tag or dependency.rev: + package.source_reference = dependency.reference + + for extra in dependency.extras: + if extra in package.extras: + for dep in package.extras[extra]: + dep.activate() + + package.requires += package.extras[extra] + + return [package] + + @classmethod + def get_package_from_vcs( + cls, vcs, url, reference=None, name=None + ): # type: (str, str, Optional[str], Optional[str]) -> Package + if vcs != "git": + raise ValueError("Unsupported VCS dependency {}".format(vcs)) + + tmp_dir = Path( + mkdtemp(prefix="pypoetry-git-{}".format(url.split("/")[-1].rstrip(".git"))) + ) try: git = Git() - git.clone(dependency.source, tmp_dir) - git.checkout(dependency.reference, tmp_dir) - revision = git.rev_parse(dependency.reference, tmp_dir).strip() - - if dependency.tag or dependency.rev: - revision = dependency.reference + git.clone(url, tmp_dir) + if reference is not None: + git.checkout(reference, tmp_dir) + else: + reference = "HEAD" - directory_dependency = DirectoryDependency( - dependency.name, - tmp_dir, - category=dependency.category, - optional=dependency.is_optional(), - ) - for extra in dependency.extras: - directory_dependency.extras.append(extra) + revision = git.rev_parse(reference, tmp_dir).strip() - package = self.search_for_directory(directory_dependency)[0] + package = cls.get_package_from_directory(tmp_dir, name=name) package.source_type = "git" - package.source_url = dependency.source + package.source_url = url package.source_reference = revision except Exception: raise finally: safe_rmtree(str(tmp_dir)) - return [package] + return package def search_for_file(self, dependency): # type: (FileDependency) -> List[Package] - if dependency.path.suffix == ".whl": - meta = pkginfo.Wheel(str(dependency.full_path)) - else: - # Assume sdist - meta = pkginfo.SDist(str(dependency.full_path)) + package = self.get_package_from_file(dependency.full_path) - if dependency.name != meta.name: + if dependency.name != package.name: # For now, the dependency's name must match the actual package's name raise RuntimeError( "The dependency name for {} does not match the actual package's name: {}".format( - dependency.name, meta.name + dependency.name, package.name ) ) + package.source_url = dependency.path.as_posix() + package.hashes = [dependency.hash()] + + for extra in dependency.extras: + if extra in package.extras: + for dep in package.extras[extra]: + dep.activate() + + package.requires += package.extras[extra] + + return [package] + + @classmethod + def get_package_from_file(cls, file_path): # type: (Path) -> Package + if file_path.suffix == ".whl": + meta = pkginfo.Wheel(str(file_path)) + else: + # Assume sdist + meta = pkginfo.SDist(str(file_path)) + package = Package(meta.name, meta.version) package.source_type = "file" - package.source_url = dependency.path.as_posix() + package.source_url = file_path.as_posix() package.description = meta.summary for req in meta.requires_dist: @@ -227,7 +259,16 @@ def search_for_file(self, dependency): # type: (FileDependency) -> List[Package if meta.requires_python: package.python_versions = meta.requires_python - package.hashes = [dependency.hash()] + return package + + def search_for_directory( + self, dependency + ): # type: (DirectoryDependency) -> List[Package] + package = self.get_package_from_directory( + dependency.full_path, name=dependency.name + ) + + package.source_url = dependency.path.as_posix() for extra in dependency.extras: if extra in package.extras: @@ -238,13 +279,23 @@ def search_for_file(self, dependency): # type: (FileDependency) -> List[Package return [package] - def search_for_directory( - self, dependency - ): # type: (DirectoryDependency) -> List[Package] - if dependency.supports_poetry(): + @classmethod + def get_package_from_directory( + cls, directory, name=None + ): # type: (Path, Optional[str]) -> Package + supports_poetry = False + pyproject = directory.joinpath("pyproject.toml") + if pyproject.exists(): + pyproject = TomlFile(pyproject) + pyproject_content = pyproject.read() + supports_poetry = ( + "tool" in pyproject_content and "poetry" in pyproject_content["tool"] + ) + + if supports_poetry: from poetry.poetry import Poetry - poetry = Poetry.create(dependency.full_path) + poetry = Poetry.create(directory) pkg = poetry.package package = Package(pkg.name, pkg.version) @@ -264,25 +315,25 @@ def search_for_directory( else: # Execute egg_info current_dir = os.getcwd() - os.chdir(str(dependency.full_path)) + os.chdir(str(directory)) try: - cwd = dependency.full_path + cwd = directory venv = EnvManager().get(cwd) venv.run("python", "setup.py", "egg_info") except EnvCommandError: - result = SetupReader.read_from_directory(dependency.full_path) + result = SetupReader.read_from_directory(directory) if not result["name"]: # The name could not be determined # We use the dependency name - result["name"] = dependency.name + result["name"] = name if not result["version"]: # The version could not be determined # so we raise an error since it is mandatory raise RuntimeError( "Unable to retrieve the package version for {}".format( - dependency.path + directory ) ) @@ -321,12 +372,12 @@ def search_for_directory( egg_info = next( Path(p) for p in glob.glob( - os.path.join(str(dependency.full_path), "**", "*.egg-info"), + os.path.join(str(directory), "**", "*.egg-info"), recursive=True, ) ) else: - egg_info = next(dependency.full_path.glob("**/*.egg-info")) + egg_info = next(directory.glob("**/*.egg-info")) meta = pkginfo.UnpackedSDist(str(egg_info)) package_name = meta.name @@ -345,16 +396,16 @@ def search_for_directory( finally: os.chdir(current_dir) - package = Package(package_name, package_version) - - if dependency.name != package.name: + if name and name != package_name: # For now, the dependency's name must match the actual package's name raise RuntimeError( "The dependency name for {} does not match the actual package's name: {}".format( - dependency.name, package.name + name, package_name ) ) + package = Package(package_name, package_version) + package.description = package_summary for req in reqs: @@ -373,16 +424,9 @@ def search_for_directory( package.python_versions = python_requires package.source_type = "directory" - package.source_url = dependency.path.as_posix() + package.source_url = directory.as_posix() - for extra in dependency.extras: - if extra in package.extras: - for dep in package.extras[extra]: - dep.activate() - - package.requires += package.extras[extra] - - return [package] + return package def incompatibilities_for( self, package diff --git a/poetry/version/version_selector.py b/poetry/version/version_selector.py index 3eb41095c15..7bbc23648a5 100644 --- a/poetry/version/version_selector.py +++ b/poetry/version/version_selector.py @@ -26,8 +26,9 @@ def find_best_candidate( constraint = parse_constraint("*") candidates = self._pool.find_packages( - package_name, constraint, allow_prereleases=allow_prereleases + package_name, constraint, allow_prereleases=True ) + only_prereleases = all([c.version.is_prerelease() for c in candidates]) if not candidates: return False @@ -37,7 +38,12 @@ def find_best_candidate( # Select highest version if we have many package = candidates[0] for candidate in candidates: - if candidate.is_prerelease() and not dependency.allows_prereleases(): + if ( + candidate.is_prerelease() + and not dependency.allows_prereleases() + and not allow_prereleases + and not only_prereleases + ): continue # Select highest version of the two @@ -52,24 +58,20 @@ def find_recommended_require_version(self, package): return self._transform_version(version.text, package.pretty_version) def _transform_version(self, version, pretty_version): - # attempt to transform 2.1.1 to 2.1 - # this allows you to upgrade through minor versions try: parsed = Version.parse(version) parts = [parsed.major, parsed.minor, parsed.patch] except ValueError: return pretty_version - # check to see if we have a semver-looking version - if len(parts) == 3: - # remove the last parts (the patch version number and any extra) - if parts[0] != 0: - del parts[2] + parts = parts[: parsed.precision] + # check to see if we have a semver-looking version + if len(parts) < 3: + version = pretty_version + else: version = ".".join(str(p) for p in parts) if parsed.is_prerelease(): version += "-{}".format(".".join(str(p) for p in parsed.prerelease)) - else: - return pretty_version return "^{}".format(version) diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 700d57ffc53..0ed94470446 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -1,7 +1,10 @@ import sys +import pytest from cleo.testers import CommandTester +from poetry.utils._compat import Path + from tests.helpers import get_dependency from tests.helpers import get_package @@ -39,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 = """\ @@ -66,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) @@ -106,7 +170,7 @@ def test_add_git_constraint(app, repo, installer): repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "0.6.5")) - tester.execute("demo --git https://github.com/demo/demo.git") + tester.execute("git+https://github.com/demo/demo.git") expected = """\ @@ -140,7 +204,7 @@ def test_add_git_constraint_with_poetry(app, repo, installer): repo.add_package(get_package("pendulum", "1.4.4")) - tester.execute("demo --git https://github.com/demo/pyproject-demo.git") + tester.execute("git+https://github.com/demo/pyproject-demo.git") expected = """\ @@ -161,13 +225,121 @@ def test_add_git_constraint_with_poetry(app, repo, installer): assert len(installer.installs) == 2 -def test_add_file_constraint_wheel(app, repo, installer): +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__) / ".." + + 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") + + 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 len(installer.installs) == 2 + + 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, installer, mocker): + 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") + + 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 len(installer.installs) == 2 + + +def test_add_file_constraint_wheel(app, repo, installer, mocker): + 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("demo --path ../distributions/demo-0.1.0-py2.py3-none-any.whl") + tester.execute("../distributions/demo-0.1.0-py2.py3-none-any.whl") expected = """\ @@ -195,13 +367,16 @@ def test_add_file_constraint_wheel(app, repo, installer): } -def test_add_file_constraint_sdist(app, repo, installer): +def test_add_file_constraint_sdist(app, repo, installer, mocker): + 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("demo --path ../distributions/demo-0.1.0.tar.gz") + tester.execute("../distributions/demo-0.1.0.tar.gz") expected = """\ @@ -229,7 +404,7 @@ def test_add_file_constraint_sdist(app, repo, installer): } -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) @@ -407,3 +582,72 @@ def test_add_should_not_select_prereleases(app, repo, installer): 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, installer +): + 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") + + assert "Package foo is already present" == str(e.value) + + +def test_add_chooses_prerelease_if_only_prereleases_are_available(app, repo, installer): + command = app.find("add") + tester = CommandTester(command) + + 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_preferes_stable_releases(app, repo, installer): + command = app.find("add") + tester = CommandTester(command) + + 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() diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 8eda2ef577f..db424bd8d0a 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -88,10 +88,10 @@ def test_interactive_with_dependencies(app, repo, mocker, poetry): [tool.poetry.dependencies] python = "~2.7 || ^3.6" -pendulum = "^2.0" +pendulum = "^2.0.0" [tool.poetry.dev-dependencies] -pytest = "^3.6" +pytest = "^3.6.0" """ assert expected in tester.io.fetch_output() @@ -135,3 +135,305 @@ def test_empty_license(app, mocker, poetry): ) assert expected in tester.io.fetch_output() + + +def test_interactive_with_git_dependencies(app, repo, mocker, poetry): + repo.add_package(get_package("pendulum", "2.0.0")) + repo.add_package(get_package("pytest", "3.6.0")) + + command = app.find("init") + command._pool = poetry.pool + + mocker.patch("poetry.utils._compat.Path.open") + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__).parent + + tester = CommandTester(command) + inputs = [ + "my-package", # Package name + "1.2.3", # Version + "This is a description", # Description + "n", # Author + "MIT", # License + "~2.7 || ^3.6", # Python + "", # Interactive packages + "git+https://github.com/demo/demo.git", # Search for package + "", # Stop searching for packages + "", # Interactive dev packages + "pytest", # Search for package + "0", + "", + "", + "\n", # Generate + ] + tester.execute(inputs="\n".join(inputs)) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +demo = {git = "https://github.com/demo/demo.git"} + +[tool.poetry.dev-dependencies] +pytest = "^3.6.0" +""" + + assert expected in tester.io.fetch_output() + + +def test_interactive_with_git_dependencies_with_reference(app, repo, mocker, poetry): + repo.add_package(get_package("pendulum", "2.0.0")) + repo.add_package(get_package("pytest", "3.6.0")) + + command = app.find("init") + command._pool = poetry.pool + + mocker.patch("poetry.utils._compat.Path.open") + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__).parent + + tester = CommandTester(command) + inputs = [ + "my-package", # Package name + "1.2.3", # Version + "This is a description", # Description + "n", # Author + "MIT", # License + "~2.7 || ^3.6", # Python + "", # Interactive packages + "git+https://github.com/demo/demo.git@develop", # Search for package + "", # Stop searching for packages + "", # Interactive dev packages + "pytest", # Search for package + "0", + "", + "", + "\n", # Generate + ] + tester.execute(inputs="\n".join(inputs)) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +demo = {git = "https://github.com/demo/demo.git", rev = "develop"} + +[tool.poetry.dev-dependencies] +pytest = "^3.6.0" +""" + + assert expected in tester.io.fetch_output() + + +def test_interactive_with_git_dependencies_and_other_name(app, repo, mocker, poetry): + repo.add_package(get_package("pendulum", "2.0.0")) + repo.add_package(get_package("pytest", "3.6.0")) + + command = app.find("init") + command._pool = poetry.pool + + mocker.patch("poetry.utils._compat.Path.open") + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__).parent + + tester = CommandTester(command) + inputs = [ + "my-package", # Package name + "1.2.3", # Version + "This is a description", # Description + "n", # Author + "MIT", # License + "~2.7 || ^3.6", # Python + "", # Interactive packages + "git+https://github.com/demo/pyproject-demo.git", # Search for package + "", # Stop searching for packages + "", # Interactive dev packages + "pytest", # Search for package + "0", + "", + "", + "\n", # Generate + ] + tester.execute(inputs="\n".join(inputs)) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +demo = {git = "https://github.com/demo/pyproject-demo.git"} + +[tool.poetry.dev-dependencies] +pytest = "^3.6.0" +""" + + assert expected in tester.io.fetch_output() + + +def test_interactive_with_directory_dependency(app, repo, mocker, poetry): + repo.add_package(get_package("pendulum", "2.0.0")) + repo.add_package(get_package("pytest", "3.6.0")) + + command = app.find("init") + command._pool = poetry.pool + + mocker.patch("poetry.utils._compat.Path.open") + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__).parent + + tester = CommandTester(command) + inputs = [ + "my-package", # Package name + "1.2.3", # Version + "This is a description", # Description + "n", # Author + "MIT", # License + "~2.7 || ^3.6", # Python + "", # Interactive packages + "../../fixtures/git/github.com/demo/demo", # Search for package + "", # Stop searching for packages + "", # Interactive dev packages + "pytest", # Search for package + "0", + "", + "", + "\n", # Generate + ] + tester.execute(inputs="\n".join(inputs)) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +demo = {path = "../../fixtures/git/github.com/demo/demo"} + +[tool.poetry.dev-dependencies] +pytest = "^3.6.0" +""" + + assert expected in tester.io.fetch_output() + + +def test_interactive_with_directory_dependency_and_other_name( + app, repo, mocker, poetry +): + repo.add_package(get_package("pendulum", "2.0.0")) + repo.add_package(get_package("pytest", "3.6.0")) + + command = app.find("init") + command._pool = poetry.pool + + mocker.patch("poetry.utils._compat.Path.open") + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__).parent + + tester = CommandTester(command) + inputs = [ + "my-package", # Package name + "1.2.3", # Version + "This is a description", # Description + "n", # Author + "MIT", # License + "~2.7 || ^3.6", # Python + "", # Interactive packages + "../../fixtures/git/github.com/demo/pyproject-demo", # Search for package + "", # Stop searching for packages + "", # Interactive dev packages + "pytest", # Search for package + "0", + "", + "", + "\n", # Generate + ] + tester.execute(inputs="\n".join(inputs)) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +demo = {path = "../../fixtures/git/github.com/demo/pyproject-demo"} + +[tool.poetry.dev-dependencies] +pytest = "^3.6.0" +""" + + assert expected in tester.io.fetch_output() + + +def test_interactive_with_file_dependency(app, repo, mocker, poetry): + repo.add_package(get_package("pendulum", "2.0.0")) + repo.add_package(get_package("pytest", "3.6.0")) + + command = app.find("init") + command._pool = poetry.pool + + mocker.patch("poetry.utils._compat.Path.open") + p = mocker.patch("poetry.utils._compat.Path.cwd") + p.return_value = Path(__file__).parent + + tester = CommandTester(command) + inputs = [ + "my-package", # Package name + "1.2.3", # Version + "This is a description", # Description + "n", # Author + "MIT", # License + "~2.7 || ^3.6", # Python + "", # Interactive packages + "../../fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl", # Search for package + "", # Stop searching for packages + "", # Interactive dev packages + "pytest", # Search for package + "0", + "", + "", + "\n", # Generate + ] + tester.execute(inputs="\n".join(inputs)) + + expected = """\ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "This is a description" +authors = ["Your Name "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" +demo = {path = "../../fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"} + +[tool.poetry.dev-dependencies] +pytest = "^3.6.0" +""" + + assert expected in tester.io.fetch_output()