Commit f205ac75 by Sébastien Eustace Committed by GitHub

Add support for url dependencies (#1260)

parent b6f45426
......@@ -113,6 +113,24 @@ my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" }
You can install path dependencies in editable/development mode.
Just pass `--develop my-package` (repeatable as much as you want) to
the `install` command.
### `url` dependencies
To depend on a library located on a remote archive,
you can use the `url` property:
```toml
[tool.poetry.dependencies]
# directory
my-package = { url = "https://example.com/my-package-0.1.0.tar.gz" }
```
with the corresponding `add` call:
```bash
poetry add https://example.com/my-package-0.1.0.tar.gz
```
### Python restricted dependencies
......
......@@ -76,7 +76,11 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
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":
if (
"git" in pair
or "url" in pair
or pair.get("version") == "latest"
):
continue
raise ValueError("Package {} is already present".format(name))
......
......@@ -14,6 +14,8 @@ from tomlkit import inline_table
from poetry.utils._compat import Path
from poetry.utils._compat import OrderedDict
from poetry.utils._compat import urlparse
from poetry.utils.helpers import temporary_directory
from .command import Command
from .env_command import EnvCommand
......@@ -149,6 +151,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
" - A git url with a revision (<b>https://github.com/sdispater/poetry.git@develop</b>)\n"
" - A file path (<b>../my-package/my-package.whl</b>)\n"
" - A directory (<b>../my-package/</b>)\n"
" - An url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n"
)
help_displayed = False
if self.confirm(question, True):
......@@ -211,6 +214,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
constraint = self._parse_requirements([package])[0]
if (
"git" in constraint
or "url" in constraint
or "path" in constraint
or "version" in constraint
):
......@@ -276,7 +280,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
requires = self._parse_requirements(requires)
result = []
for requirement in requires:
if "git" in requirement or "path" in requirement:
if "git" in requirement or "url" in requirement or "path" in requirement:
result.append(requirement)
continue
elif "version" not in requirement:
......@@ -343,28 +347,42 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
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("@")
url_parsed = urlparse.urlparse(requirement)
if url_parsed.scheme and url_parsed.netloc:
# Url
if url_parsed.scheme in ["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
pair = OrderedDict(
[("name", url.split("/")[-1].rstrip(".git")), ("git", url)]
)
if rev:
pair["rev"] = rev
if extras:
pair["extras"] = extras
if extras:
pair["extras"] = extras
package = Provider.get_package_from_vcs(
"git", url, reference=pair.get("rev")
)
pair["name"] = package.name
result.append(pair)
package = Provider.get_package_from_vcs(
"git", url, reference=pair.get("rev")
)
pair["name"] = package.name
result.append(pair)
continue
elif url_parsed.scheme in ["http", "https"]:
package = Provider.get_package_from_url(requirement)
continue
pair = OrderedDict(
[("name", package.name), ("url", package.source_url)]
)
if extras:
pair["extras"] = extras
result.append(pair)
continue
elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath(
requirement
).exists():
......
......@@ -211,6 +211,9 @@
"$ref": "#/definitions/path-dependency"
},
{
"$ref": "#/definitions/url-dependency"
},
{
"$ref": "#/definitions/multiple-constraints-dependency"
}
]
......@@ -394,6 +397,42 @@
}
}
},
"url-dependency": {
"type": "object",
"required": [
"url"
],
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"description": "The url to the file."
},
"python": {
"type": "string",
"description": "The python versions for which the dependency should be installed."
},
"platform": {
"type": "string",
"description": "The platform(s) for which the dependency should be installed."
},
"markers": {
"type": "string",
"description": "The PEP 508 compliant environment markers for which the dependency should be installed."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
}
}
},
"multiple-constraints-dependency": {
"type": "array",
"minItems": 1,
......
......@@ -20,6 +20,7 @@ from .utils.utils import is_installable_dir
from .utils.utils import is_url
from .utils.utils import path_to_url
from .utils.utils import strip_extras
from .url_dependency import URLDependency
from .vcs_dependency import VCSDependency
......
......@@ -171,6 +171,9 @@ class Dependency(object):
def is_directory(self):
return False
def is_url(self):
return False
def accepts(self, package): # type: (poetry.packages.Package) -> bool
"""
Determines if the given package matches this dependency.
......
......@@ -18,8 +18,8 @@ from .constraints import parse_constraint as parse_generic_constraint
from .dependency import Dependency
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .url_dependency import URLDependency
from .vcs_dependency import VCSDependency
from .utils.utils import convert_markers
from .utils.utils import create_nested_marker
AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?)>)?$")
......@@ -111,7 +111,7 @@ class Package(object):
@property
def full_pretty_version(self):
if self.source_type in ["file", "directory"]:
if self.source_type in ["file", "directory", "url"]:
return "{} {}".format(self._pretty_version, self.source_url)
if self.source_type not in ["hg", "git"]:
......@@ -314,6 +314,8 @@ class Package(object):
base=self.root_dir,
develop=constraint.get("develop", True),
)
elif "url" in constraint:
dependency = URLDependency(name, constraint["url"], category=category)
else:
version = constraint["version"]
......
from poetry.utils._compat import urlparse
from .dependency import Dependency
class URLDependency(Dependency):
def __init__(
self,
name,
url, # type: str
category="main", # type: str
optional=False, # type: bool
):
self._url = url
parsed = urlparse.urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError("{} does not seem like a valid url".format(url))
super(URLDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def url(self):
return self._url
@property
def base_pep_508_name(self): # type: () -> str
requirement = self.pretty_name
if self.extras:
requirement += "[{}]".format(",".join(self.extras))
requirement += " @ {}".format(self._url)
return requirement
def is_url(self): # type: () -> bool
return True
......@@ -17,6 +17,7 @@ from poetry.packages import DirectoryDependency
from poetry.packages import FileDependency
from poetry.packages import Package
from poetry.packages import PackageCollection
from poetry.packages import URLDependency
from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508
......@@ -30,10 +31,13 @@ from poetry.repositories import Pool
from poetry.utils._compat import PY35
from poetry.utils._compat import Path
from poetry.utils._compat import OrderedDict
from poetry.utils._compat import urlparse
from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import safe_rmtree
from poetry.utils.helpers import temporary_directory
from poetry.utils.env import EnvManager
from poetry.utils.env import EnvCommandError
from poetry.utils.inspector import Inspector
from poetry.utils.setup_reader import SetupReader
from poetry.utils.toml_file import TomlFile
......@@ -63,6 +67,7 @@ class Provider:
self._package = package
self._pool = pool
self._io = io
self._inspector = Inspector()
self._python_constraint = package.python_constraint
self._search_for = {}
self._is_debugging = self._io.is_debug() or self._io.is_very_verbose()
......@@ -127,6 +132,8 @@ class Provider:
packages = self.search_for_file(dependency)
elif dependency.is_directory():
packages = self.search_for_directory(dependency)
elif dependency.is_url():
packages = self.search_for_url(dependency)
else:
constraint = dependency.constraint
......@@ -234,18 +241,18 @@ class Provider:
@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))
info = Inspector().inspect(file_path)
if not info["name"]:
raise RuntimeError(
"Unable to determine the package name of {}".format(file_path)
)
package = Package(meta.name, meta.version)
package = Package(info["name"], info["version"])
package.source_type = "file"
package.source_url = file_path.as_posix()
package.description = meta.summary
for req in meta.requires_dist:
package.description = info["summary"]
for req in info["requires_dist"]:
dep = dependency_from_pep_508(req)
for extra in dep.in_extras:
if extra not in package.extras:
......@@ -256,8 +263,8 @@ class Provider:
if not dep.is_optional():
package.requires.append(dep)
if meta.requires_python:
package.python_versions = meta.requires_python
if info["requires_python"]:
package.python_versions = info["requires_python"]
return package
......@@ -428,6 +435,40 @@ class Provider:
return package
def search_for_url(self, dependency): # type: (URLDependency) -> List[Package]
package = self.get_package_from_url(dependency.url)
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, package.name
)
)
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_url(cls, url): # type: (str) -> Package
with temporary_directory() as temp_dir:
temp_dir = Path(temp_dir)
file_name = os.path.basename(urlparse.urlparse(url).path)
Inspector().download(url, temp_dir / file_name)
package = cls.get_package_from_file(temp_dir / file_name)
package.source_type = "url"
package.source_url = url
return package
def incompatibilities_for(
self, package
): # type: (DependencyPackage) -> List[Incompatibility]
......@@ -495,6 +536,7 @@ class Provider:
if not package.is_root() and package.source_type not in {
"directory",
"file",
"url",
"git",
}:
package = DependencyPackage(
......
......@@ -39,6 +39,7 @@ from poetry.semver import VersionConstraint
from poetry.semver import VersionRange
from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name
from poetry.utils.inspector import Inspector
from poetry.utils.patterns import wheel_file_re
from poetry.version.markers import InvalidMarker
......@@ -163,8 +164,8 @@ class LegacyRepository(PyPiRepository):
self._name = name
self._url = url.rstrip("/")
self._auth = auth
self._inspector = Inspector()
self._cache_dir = Path(CACHE_DIR) / "cache" / "repositories" / name
self._cache = CacheManager(
{
"default": "releases",
......
import logging
import os
import tarfile
import zipfile
import pkginfo
from bz2 import BZ2File
from collections import defaultdict
from gzip import GzipFile
from typing import Dict
from typing import List
from typing import Union
......@@ -40,6 +35,7 @@ from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import temporary_directory
from poetry.utils.inspector import Inspector
from poetry.utils.patterns import wheel_file_re
from poetry.utils.setup_reader import SetupReader
from poetry.version.markers import InvalidMarker
......@@ -76,6 +72,7 @@ class PyPiRepository(Repository):
self._session = CacheControl(
session(), cache=FileCache(str(release_cache_dir / "_http"))
)
self._inspector = Inspector()
super(PyPiRepository, self).__init__()
......@@ -456,30 +453,14 @@ class PyPiRepository(Repository):
"Downloading wheel: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]),
level="debug",
)
info = {"summary": "", "requires_python": None, "requires_dist": None}
filename = os.path.basename(urlparse.urlparse(url).path.rsplit("/")[-1])
with temporary_directory() as temp_dir:
filepath = os.path.join(temp_dir, filename)
self._download(url, filepath)
try:
meta = pkginfo.Wheel(filepath)
except ValueError:
# Unable to determine dependencies
# Assume none
return info
if meta.summary:
info["summary"] = meta.summary or ""
info["requires_python"] = meta.requires_python
if meta.requires_dist:
info["requires_dist"] = meta.requires_dist
filepath = Path(temp_dir) / filename
self._download(url, str(filepath))
return info
return self._inspector.inspect_wheel(filepath)
def _get_info_from_sdist(
self, url
......@@ -488,7 +469,6 @@ class PyPiRepository(Repository):
"Downloading sdist: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]),
level="debug",
)
info = {"summary": "", "requires_python": None, "requires_dist": None}
filename = os.path.basename(urlparse.urlparse(url).path)
......@@ -496,120 +476,7 @@ class PyPiRepository(Repository):
filepath = Path(temp_dir) / filename
self._download(url, str(filepath))
try:
meta = pkginfo.SDist(str(filepath))
if meta.summary:
info["summary"] = meta.summary
if meta.requires_python:
info["requires_python"] = meta.requires_python
if meta.requires_dist:
info["requires_dist"] = list(meta.requires_dist)
return info
except ValueError:
# Unable to determine dependencies
# We pass and go deeper
pass
# Still not dependencies found
# So, we unpack and introspect
suffix = filepath.suffix
gz = None
if suffix == ".zip":
tar = zipfile.ZipFile(str(filepath))
else:
if suffix == ".bz2":
gz = BZ2File(str(filepath))
suffixes = filepath.suffixes
if len(suffixes) > 1 and suffixes[-2] == ".tar":
suffix = ".tar.bz2"
else:
gz = GzipFile(str(filepath))
suffix = ".tar.gz"
tar = tarfile.TarFile(str(filepath), fileobj=gz)
try:
tar.extractall(os.path.join(temp_dir, "unpacked"))
finally:
if gz:
gz.close()
tar.close()
unpacked = Path(temp_dir) / "unpacked"
sdist_dir = unpacked / Path(filename).name.rstrip(suffix)
# Checking for .egg-info at root
eggs = list(sdist_dir.glob("*.egg-info"))
if eggs:
egg_info = eggs[0]
requires = egg_info / "requires.txt"
if requires.exists():
with requires.open(encoding="utf-8") as f:
info["requires_dist"] = parse_requires(f.read())
return info
# Searching for .egg-info in sub directories
eggs = list(sdist_dir.glob("**/*.egg-info"))
if eggs:
egg_info = eggs[0]
requires = egg_info / "requires.txt"
if requires.exists():
with requires.open(encoding="utf-8") as f:
info["requires_dist"] = parse_requires(f.read())
return info
# Still nothing, try reading (without executing it)
# the setup.py file.
try:
setup_info = self._inspect_sdist_with_setup(sdist_dir)
for key, value in info.items():
if value:
continue
info[key] = setup_info[key]
return info
except Exception as e:
self._log(
"An error occurred when reading setup.py or setup.cfg: {}".format(
str(e)
),
"warning",
)
return info
def _inspect_sdist_with_setup(self, sdist_dir):
info = {"requires_python": None, "requires_dist": None}
result = SetupReader.read_from_directory(sdist_dir)
requires = ""
for dep in result["install_requires"]:
requires += dep + "\n"
if result["extras_require"]:
requires += "\n"
for extra_name, deps in result["extras_require"].items():
requires += "[{}]\n".format(extra_name)
for dep in deps:
requires += dep + "\n"
requires += "\n"
info["requires_dist"] = parse_requires(requires)
info["requires_python"] = result["python_requires"]
return info
return self._inspector.inspect_sdist(filepath)
def _download(self, url, dest): # type: (str, str) -> None
r = get(url, stream=True)
......
from typing import Dict
from typing import List
from typing import Union
import logging
import os
import tarfile
import zipfile
from bz2 import BZ2File
from gzip import GzipFile
import pkginfo
from requests import get
from ._compat import Path
from .helpers import parse_requires
from .setup_reader import SetupReader
from .toml_file import TomlFile
logger = logging.getLogger(__name__)
class Inspector:
"""
A class to download and inspect remote packages.
"""
@classmethod
def download(cls, url, dest): # type: (str, Path) -> None
r = get(url, stream=True)
r.raise_for_status()
with open(str(dest), "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
def inspect(self, file_path): # type: (Path) -> Dict[str, Union[str, List[str]]]
if file_path.suffix == ".whl":
return self.inspect_wheel(file_path)
return self.inspect_sdist(file_path)
def inspect_wheel(
self, file_path
): # type: (Path) -> Dict[str, Union[str, List[str]]]
info = {
"name": "",
"version": "",
"summary": "",
"requires_python": None,
"requires_dist": None,
}
try:
meta = pkginfo.Wheel(str(file_path))
except ValueError:
# Unable to determine dependencies
# Assume none
return info
if meta.name:
info["name"] = meta.name
if meta.version:
info["version"] = meta.version
if meta.summary:
info["summary"] = meta.summary or ""
info["requires_python"] = meta.requires_python
if meta.requires_dist:
info["requires_dist"] = meta.requires_dist
return info
def inspect_sdist(
self, file_path
): # type: (Path) -> Dict[str, Union[str, List[str]]]
info = {
"name": "",
"version": "",
"summary": "",
"requires_python": None,
"requires_dist": None,
}
try:
meta = pkginfo.SDist(str(file_path))
if meta.name:
info["name"] = meta.name
if meta.version:
info["version"] = meta.version
if meta.summary:
info["summary"] = meta.summary
if meta.requires_python:
info["requires_python"] = meta.requires_python
if meta.requires_dist:
info["requires_dist"] = list(meta.requires_dist)
return info
except ValueError:
# Unable to determine dependencies
# We pass and go deeper
pass
# Still not dependencies found
# So, we unpack and introspect
suffix = file_path.suffix
gz = None
if suffix == ".zip":
tar = zipfile.ZipFile(str(file_path))
else:
if suffix == ".bz2":
gz = BZ2File(str(file_path))
suffixes = file_path.suffixes
if len(suffixes) > 1 and suffixes[-2] == ".tar":
suffix = ".tar.bz2"
else:
gz = GzipFile(str(file_path))
suffix = ".tar.gz"
tar = tarfile.TarFile(str(file_path), fileobj=gz)
try:
tar.extractall(os.path.join(str(file_path.parent), "unpacked"))
finally:
if gz:
gz.close()
tar.close()
unpacked = file_path.parent / "unpacked"
elements = list(unpacked.glob("*"))
if len(elements) == 1 and elements[0].is_dir():
sdist_dir = elements[0]
else:
sdist_dir = unpacked / file_path.name.rstrip(suffix)
pyproject = TomlFile(sdist_dir / "pyproject.toml")
if pyproject.exists():
from poetry.poetry import Poetry
pyproject_content = pyproject.read()
if "tool" in pyproject_content and "poetry" in pyproject_content["tool"]:
package = Poetry.create(sdist_dir).package
return {
"name": package.name,
"version": package.version.text,
"summary": package.description,
"requires_dist": [dep.to_pep_508() for dep in package.requires],
"requires_python": package.python_versions,
}
# Checking for .egg-info at root
eggs = list(sdist_dir.glob("*.egg-info"))
if eggs:
egg_info = eggs[0]
requires = egg_info / "requires.txt"
if requires.exists():
with requires.open(encoding="utf-8") as f:
info["requires_dist"] = parse_requires(f.read())
return info
# Searching for .egg-info in sub directories
eggs = list(sdist_dir.glob("**/*.egg-info"))
if eggs:
egg_info = eggs[0]
requires = egg_info / "requires.txt"
if requires.exists():
with requires.open(encoding="utf-8") as f:
info["requires_dist"] = parse_requires(f.read())
return info
# Still nothing, try reading (without executing it)
# the setup.py file.
try:
setup_info = self._inspect_sdist_with_setup(sdist_dir)
for key, value in info.items():
if value:
continue
info[key] = setup_info[key]
return info
except Exception as e:
logger.warning(
"An error occurred when reading setup.py or setup.cfg: {}".format(
str(e)
)
)
return info
def _inspect_sdist_with_setup(
self, sdist_dir
): # type: (Path) -> Dict[str, Union[str, List[str]]]
info = {
"name": None,
"version": None,
"summary": "",
"requires_python": None,
"requires_dist": None,
}
result = SetupReader.read_from_directory(sdist_dir)
requires = ""
for dep in result["install_requires"]:
requires += dep + "\n"
if result["extras_require"]:
requires += "\n"
for extra_name, deps in result["extras_require"].items():
requires += "[{}]\n".format(extra_name)
for dep in deps:
requires += dep + "\n"
requires += "\n"
info["name"] = result["name"]
info["version"] = result["version"]
info["requires_dist"] = parse_requires(requires)
info["requires_python"] = result["python_requires"]
return info
......@@ -10,6 +10,8 @@ except ImportError:
import urlparse
from poetry.config import Config
from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile
......@@ -43,10 +45,28 @@ def mock_clone(_, source, dest):
/ parts.path.lstrip("/").rstrip(".git")
)
if dest.exists():
shutil.rmtree(str(dest))
shutil.rmtree(str(dest))
shutil.copytree(str(folder), str(dest))
def mock_download(self, url, dest):
parts = urlparse.urlparse(url)
fixtures = Path(__file__).parent / "fixtures"
fixture = fixtures / parts.path.lstrip("/")
if dest.exists():
os.unlink(str(dest))
if PY2 and WINDOWS:
shutil.copyfile(str(fixture), str(dest))
else:
os.symlink(str(fixture), str(dest))
@pytest.fixture
def tmp_dir():
dir_ = tempfile.mkdtemp(prefix="poetry_")
......@@ -75,6 +95,12 @@ def git_mock(mocker):
p.return_value = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24"
@pytest.fixture(autouse=True)
def download_mock(mocker):
# Patch download to not download anything but to just copy from fixtures
mocker.patch("poetry.utils.inspector.Inspector.download", new=mock_download)
@pytest.fixture
def http():
httpretty.enable()
......
......@@ -446,6 +446,86 @@ Package operations: 2 installs, 0 updates, 0 removals
}
def test_add_url_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(
"https://poetry.eustace.io/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://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl)
"""
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"] == {
"url": "https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl"
}
def test_add_url_constraint_wheel_with_extras(app, repo, installer, mocker):
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(
"https://poetry.eustace.io/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://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl)
"""
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"] == {
"url": "https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl",
"extras": ["foo", "bar"],
}
def test_add_constraint_with_python(app, repo, installer):
command = app.find("add")
tester = CommandTester(command)
......
......@@ -17,6 +17,8 @@ from poetry.poetry import Poetry as BasePoetry
from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool
from poetry.repositories import Repository as BaseRepository
from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile
from poetry.repositories.exceptions import PackageNotFound
......@@ -43,6 +45,21 @@ def mock_clone(self, source, dest):
shutil.copytree(str(folder), str(dest))
def mock_download(self, url, dest):
parts = urlparse.urlparse(url)
fixtures = Path(__file__).parent.parent / "fixtures"
fixture = fixtures / parts.path.lstrip("/")
if dest.exists():
shutil.rmtree(str(dest))
if PY2 and WINDOWS:
shutil.copyfile(str(fixture), str(dest))
else:
os.symlink(str(fixture), str(dest))
@pytest.fixture
def installed():
return BaseRepository()
......@@ -68,6 +85,9 @@ def setup(mocker, installer, installed, config):
p = mocker.patch("poetry.vcs.git.Git.rev_parse")
p.return_value = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24"
# Patch download to not download anything but to just copy from fixtures
mocker.patch("poetry.utils.inspector.Inspector.download", new=mock_download)
# Setting terminal width
environ = dict(os.environ)
os.environ["COLUMNS"] = "80"
......
[[package]]
name = "demo"
version = "0.1.0"
description = ""
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.source]
type = "url"
reference = ""
url = "https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl"
[package.dependencies]
pendulum = ">=1.4.4"
[package.extras]
bar = ["tomlkit"]
foo = ["cleo"]
[[package]]
name = "pendulum"
version = "1.4.4"
description = ""
category = "main"
optional = false
python-versions = "*"
[metadata]
python-versions = "*"
content-hash = "123456789"
[metadata.hashes]
demo = []
pendulum = []
......@@ -1499,3 +1499,18 @@ def test_installer_can_install_dependencies_from_forced_source(
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://poetry.eustace.io/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
[tool.poetry]
name = "with-url-dependency"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
homepage = "https://poetry.eustace.io/"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
demo = { url = "https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl" }
......@@ -132,3 +132,20 @@ def test_metadata_with_vcs_dependencies():
requires_dist = metadata["Requires-Dist"]
assert "cleo @ git+https://github.com/sdispater/cleo.git@master" == requires_dist
def test_metadata_with_url_dependencies():
builder = Builder(
Poetry.create(Path(__file__).parent / "fixtures" / "with_url_dependency"),
NullEnv(),
NullIO(),
)
metadata = Parser().parsestr(builder.get_metadata_content())
requires_dist = metadata["Requires-Dist"]
assert (
"demo @ https://poetry.eustace.io/distributions/demo-0.1.0-py2.py3-none-any.whl"
== requires_dist
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment