Commit f205ac75 by Sébastien Eustace Committed by GitHub

Add support for url dependencies (#1260)

parent b6f45426
...@@ -115,6 +115,24 @@ my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" } ...@@ -115,6 +115,24 @@ my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" }
the `install` command. 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 ### Python restricted dependencies
You can also specify that a dependency should be installed only for specific Python versions: You can also specify that a dependency should be installed only for specific Python versions:
......
...@@ -76,7 +76,11 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -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]: for key in poetry_content[section]:
if key.lower() == name.lower(): if key.lower() == name.lower():
pair = self._parse_requirements([name])[0] 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 continue
raise ValueError("Package {} is already present".format(name)) raise ValueError("Package {} is already present".format(name))
......
...@@ -14,6 +14,8 @@ from tomlkit import inline_table ...@@ -14,6 +14,8 @@ from tomlkit import inline_table
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import OrderedDict from poetry.utils._compat import OrderedDict
from poetry.utils._compat import urlparse
from poetry.utils.helpers import temporary_directory
from .command import Command from .command import Command
from .env_command import EnvCommand from .env_command import EnvCommand
...@@ -149,6 +151,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in ...@@ -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 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 file path (<b>../my-package/my-package.whl</b>)\n"
" - A directory (<b>../my-package/</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 help_displayed = False
if self.confirm(question, True): if self.confirm(question, True):
...@@ -211,6 +214,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in ...@@ -211,6 +214,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
constraint = self._parse_requirements([package])[0] constraint = self._parse_requirements([package])[0]
if ( if (
"git" in constraint "git" in constraint
or "url" in constraint
or "path" in constraint or "path" in constraint
or "version" in constraint or "version" in constraint
): ):
...@@ -276,7 +280,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in ...@@ -276,7 +280,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
requires = self._parse_requirements(requires) requires = self._parse_requirements(requires)
result = [] result = []
for requirement in requires: 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) result.append(requirement)
continue continue
elif "version" not in requirement: elif "version" not in requirement:
...@@ -343,7 +347,10 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in ...@@ -343,7 +347,10 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
extras = [e.strip() for e in extras_m.group(1).split(",")] extras = [e.strip() for e in extras_m.group(1).split(",")]
requirement, _ = requirement.split("[") requirement, _ = requirement.split("[")
if requirement.startswith(("git+https://", "git+ssh://")): 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+") url = requirement.lstrip("git+")
rev = None rev = None
if "@" in url: if "@" in url:
...@@ -365,6 +372,17 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in ...@@ -365,6 +372,17 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
result.append(pair) result.append(pair)
continue continue
elif url_parsed.scheme in ["http", "https"]:
package = Provider.get_package_from_url(requirement)
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( elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath(
requirement requirement
).exists(): ).exists():
......
...@@ -211,6 +211,9 @@ ...@@ -211,6 +211,9 @@
"$ref": "#/definitions/path-dependency" "$ref": "#/definitions/path-dependency"
}, },
{ {
"$ref": "#/definitions/url-dependency"
},
{
"$ref": "#/definitions/multiple-constraints-dependency" "$ref": "#/definitions/multiple-constraints-dependency"
} }
] ]
...@@ -394,6 +397,42 @@ ...@@ -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": { "multiple-constraints-dependency": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
......
...@@ -20,6 +20,7 @@ from .utils.utils import is_installable_dir ...@@ -20,6 +20,7 @@ from .utils.utils import is_installable_dir
from .utils.utils import is_url from .utils.utils import is_url
from .utils.utils import path_to_url from .utils.utils import path_to_url
from .utils.utils import strip_extras from .utils.utils import strip_extras
from .url_dependency import URLDependency
from .vcs_dependency import VCSDependency from .vcs_dependency import VCSDependency
......
...@@ -171,6 +171,9 @@ class Dependency(object): ...@@ -171,6 +171,9 @@ class Dependency(object):
def is_directory(self): def is_directory(self):
return False return False
def is_url(self):
return False
def accepts(self, package): # type: (poetry.packages.Package) -> bool def accepts(self, package): # type: (poetry.packages.Package) -> bool
""" """
Determines if the given package matches this dependency. Determines if the given package matches this dependency.
......
...@@ -18,8 +18,8 @@ from .constraints import parse_constraint as parse_generic_constraint ...@@ -18,8 +18,8 @@ from .constraints import parse_constraint as parse_generic_constraint
from .dependency import Dependency from .dependency import Dependency
from .directory_dependency import DirectoryDependency from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency from .file_dependency import FileDependency
from .url_dependency import URLDependency
from .vcs_dependency import VCSDependency from .vcs_dependency import VCSDependency
from .utils.utils import convert_markers
from .utils.utils import create_nested_marker from .utils.utils import create_nested_marker
AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?)>)?$") AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?)>)?$")
...@@ -111,7 +111,7 @@ class Package(object): ...@@ -111,7 +111,7 @@ class Package(object):
@property @property
def full_pretty_version(self): 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) return "{} {}".format(self._pretty_version, self.source_url)
if self.source_type not in ["hg", "git"]: if self.source_type not in ["hg", "git"]:
...@@ -314,6 +314,8 @@ class Package(object): ...@@ -314,6 +314,8 @@ class Package(object):
base=self.root_dir, base=self.root_dir,
develop=constraint.get("develop", True), develop=constraint.get("develop", True),
) )
elif "url" in constraint:
dependency = URLDependency(name, constraint["url"], category=category)
else: else:
version = constraint["version"] 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 ...@@ -17,6 +17,7 @@ from poetry.packages import DirectoryDependency
from poetry.packages import FileDependency from poetry.packages import FileDependency
from poetry.packages import Package from poetry.packages import Package
from poetry.packages import PackageCollection from poetry.packages import PackageCollection
from poetry.packages import URLDependency
from poetry.packages import VCSDependency from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508 from poetry.packages import dependency_from_pep_508
...@@ -30,10 +31,13 @@ from poetry.repositories import Pool ...@@ -30,10 +31,13 @@ from poetry.repositories import Pool
from poetry.utils._compat import PY35 from poetry.utils._compat import PY35
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import OrderedDict from poetry.utils._compat import OrderedDict
from poetry.utils._compat import urlparse
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import safe_rmtree 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 EnvManager
from poetry.utils.env import EnvCommandError from poetry.utils.env import EnvCommandError
from poetry.utils.inspector import Inspector
from poetry.utils.setup_reader import SetupReader from poetry.utils.setup_reader import SetupReader
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
...@@ -63,6 +67,7 @@ class Provider: ...@@ -63,6 +67,7 @@ class Provider:
self._package = package self._package = package
self._pool = pool self._pool = pool
self._io = io self._io = io
self._inspector = Inspector()
self._python_constraint = package.python_constraint self._python_constraint = package.python_constraint
self._search_for = {} self._search_for = {}
self._is_debugging = self._io.is_debug() or self._io.is_very_verbose() self._is_debugging = self._io.is_debug() or self._io.is_very_verbose()
...@@ -127,6 +132,8 @@ class Provider: ...@@ -127,6 +132,8 @@ class Provider:
packages = self.search_for_file(dependency) packages = self.search_for_file(dependency)
elif dependency.is_directory(): elif dependency.is_directory():
packages = self.search_for_directory(dependency) packages = self.search_for_directory(dependency)
elif dependency.is_url():
packages = self.search_for_url(dependency)
else: else:
constraint = dependency.constraint constraint = dependency.constraint
...@@ -234,18 +241,18 @@ class Provider: ...@@ -234,18 +241,18 @@ class Provider:
@classmethod @classmethod
def get_package_from_file(cls, file_path): # type: (Path) -> Package def get_package_from_file(cls, file_path): # type: (Path) -> Package
if file_path.suffix == ".whl": info = Inspector().inspect(file_path)
meta = pkginfo.Wheel(str(file_path)) if not info["name"]:
else: raise RuntimeError(
# Assume sdist "Unable to determine the package name of {}".format(file_path)
meta = pkginfo.SDist(str(file_path)) )
package = Package(meta.name, meta.version) package = Package(info["name"], info["version"])
package.source_type = "file" package.source_type = "file"
package.source_url = file_path.as_posix() package.source_url = file_path.as_posix()
package.description = meta.summary package.description = info["summary"]
for req in meta.requires_dist: for req in info["requires_dist"]:
dep = dependency_from_pep_508(req) dep = dependency_from_pep_508(req)
for extra in dep.in_extras: for extra in dep.in_extras:
if extra not in package.extras: if extra not in package.extras:
...@@ -256,8 +263,8 @@ class Provider: ...@@ -256,8 +263,8 @@ class Provider:
if not dep.is_optional(): if not dep.is_optional():
package.requires.append(dep) package.requires.append(dep)
if meta.requires_python: if info["requires_python"]:
package.python_versions = meta.requires_python package.python_versions = info["requires_python"]
return package return package
...@@ -428,6 +435,40 @@ class Provider: ...@@ -428,6 +435,40 @@ class Provider:
return package 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( def incompatibilities_for(
self, package self, package
): # type: (DependencyPackage) -> List[Incompatibility] ): # type: (DependencyPackage) -> List[Incompatibility]
...@@ -495,6 +536,7 @@ class Provider: ...@@ -495,6 +536,7 @@ class Provider:
if not package.is_root() and package.source_type not in { if not package.is_root() and package.source_type not in {
"directory", "directory",
"file", "file",
"url",
"git", "git",
}: }:
package = DependencyPackage( package = DependencyPackage(
......
...@@ -39,6 +39,7 @@ from poetry.semver import VersionConstraint ...@@ -39,6 +39,7 @@ from poetry.semver import VersionConstraint
from poetry.semver import VersionRange from poetry.semver import VersionRange
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.utils.inspector import Inspector
from poetry.utils.patterns import wheel_file_re from poetry.utils.patterns import wheel_file_re
from poetry.version.markers import InvalidMarker from poetry.version.markers import InvalidMarker
...@@ -163,8 +164,8 @@ class LegacyRepository(PyPiRepository): ...@@ -163,8 +164,8 @@ class LegacyRepository(PyPiRepository):
self._name = name self._name = name
self._url = url.rstrip("/") self._url = url.rstrip("/")
self._auth = auth self._auth = auth
self._inspector = Inspector()
self._cache_dir = Path(CACHE_DIR) / "cache" / "repositories" / name self._cache_dir = Path(CACHE_DIR) / "cache" / "repositories" / name
self._cache = CacheManager( self._cache = CacheManager(
{ {
"default": "releases", "default": "releases",
......
import logging import logging
import os import os
import tarfile
import zipfile
import pkginfo
from bz2 import BZ2File
from collections import defaultdict from collections import defaultdict
from gzip import GzipFile
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Union from typing import Union
...@@ -40,6 +35,7 @@ from poetry.utils._compat import Path ...@@ -40,6 +35,7 @@ from poetry.utils._compat import Path
from poetry.utils._compat import to_str from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import temporary_directory from poetry.utils.helpers import temporary_directory
from poetry.utils.inspector import Inspector
from poetry.utils.patterns import wheel_file_re from poetry.utils.patterns import wheel_file_re
from poetry.utils.setup_reader import SetupReader from poetry.utils.setup_reader import SetupReader
from poetry.version.markers import InvalidMarker from poetry.version.markers import InvalidMarker
...@@ -76,6 +72,7 @@ class PyPiRepository(Repository): ...@@ -76,6 +72,7 @@ class PyPiRepository(Repository):
self._session = CacheControl( self._session = CacheControl(
session(), cache=FileCache(str(release_cache_dir / "_http")) session(), cache=FileCache(str(release_cache_dir / "_http"))
) )
self._inspector = Inspector()
super(PyPiRepository, self).__init__() super(PyPiRepository, self).__init__()
...@@ -456,30 +453,14 @@ class PyPiRepository(Repository): ...@@ -456,30 +453,14 @@ class PyPiRepository(Repository):
"Downloading wheel: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]), "Downloading wheel: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]),
level="debug", level="debug",
) )
info = {"summary": "", "requires_python": None, "requires_dist": None}
filename = os.path.basename(urlparse.urlparse(url).path.rsplit("/")[-1]) filename = os.path.basename(urlparse.urlparse(url).path.rsplit("/")[-1])
with temporary_directory() as temp_dir: with temporary_directory() as temp_dir:
filepath = os.path.join(temp_dir, filename) filepath = Path(temp_dir) / filename
self._download(url, filepath) self._download(url, str(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
return info return self._inspector.inspect_wheel(filepath)
def _get_info_from_sdist( def _get_info_from_sdist(
self, url self, url
...@@ -488,7 +469,6 @@ class PyPiRepository(Repository): ...@@ -488,7 +469,6 @@ class PyPiRepository(Repository):
"Downloading sdist: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]), "Downloading sdist: {}".format(urlparse.urlparse(url).path.rsplit("/")[-1]),
level="debug", level="debug",
) )
info = {"summary": "", "requires_python": None, "requires_dist": None}
filename = os.path.basename(urlparse.urlparse(url).path) filename = os.path.basename(urlparse.urlparse(url).path)
...@@ -496,120 +476,7 @@ class PyPiRepository(Repository): ...@@ -496,120 +476,7 @@ class PyPiRepository(Repository):
filepath = Path(temp_dir) / filename filepath = Path(temp_dir) / filename
self._download(url, str(filepath)) self._download(url, str(filepath))
try: return self._inspector.inspect_sdist(filepath)
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
def _download(self, url, dest): # type: (str, str) -> None def _download(self, url, dest): # type: (str, str) -> None
r = get(url, stream=True) 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: ...@@ -10,6 +10,8 @@ except ImportError:
import urlparse import urlparse
from poetry.config import Config 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._compat import Path
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
...@@ -43,10 +45,28 @@ def mock_clone(_, source, dest): ...@@ -43,10 +45,28 @@ def mock_clone(_, source, dest):
/ parts.path.lstrip("/").rstrip(".git") / parts.path.lstrip("/").rstrip(".git")
) )
if dest.exists():
shutil.rmtree(str(dest))
shutil.rmtree(str(dest)) shutil.rmtree(str(dest))
shutil.copytree(str(folder), 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 @pytest.fixture
def tmp_dir(): def tmp_dir():
dir_ = tempfile.mkdtemp(prefix="poetry_") dir_ = tempfile.mkdtemp(prefix="poetry_")
...@@ -75,6 +95,12 @@ def git_mock(mocker): ...@@ -75,6 +95,12 @@ def git_mock(mocker):
p.return_value = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24" 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 @pytest.fixture
def http(): def http():
httpretty.enable() httpretty.enable()
......
...@@ -446,6 +446,86 @@ Package operations: 2 installs, 0 updates, 0 removals ...@@ -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): def test_add_constraint_with_python(app, repo, installer):
command = app.find("add") command = app.find("add")
tester = CommandTester(command) tester = CommandTester(command)
......
...@@ -17,6 +17,8 @@ from poetry.poetry import Poetry as BasePoetry ...@@ -17,6 +17,8 @@ from poetry.poetry import Poetry as BasePoetry
from poetry.packages import Locker as BaseLocker from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository as BaseRepository 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._compat import Path
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import PackageNotFound
...@@ -43,6 +45,21 @@ def mock_clone(self, source, dest): ...@@ -43,6 +45,21 @@ def mock_clone(self, source, dest):
shutil.copytree(str(folder), str(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 @pytest.fixture
def installed(): def installed():
return BaseRepository() return BaseRepository()
...@@ -68,6 +85,9 @@ def setup(mocker, installer, installed, config): ...@@ -68,6 +85,9 @@ def setup(mocker, installer, installed, config):
p = mocker.patch("poetry.vcs.git.Git.rev_parse") p = mocker.patch("poetry.vcs.git.Git.rev_parse")
p.return_value = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24" 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 # Setting terminal width
environ = dict(os.environ) environ = dict(os.environ)
os.environ["COLUMNS"] = "80" 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( ...@@ -1499,3 +1499,18 @@ def test_installer_can_install_dependencies_from_forced_source(
assert len(installer.installer.installs) == 1 assert len(installer.installer.installs) == 1
assert len(installer.installer.updates) == 0 assert len(installer.installer.updates) == 0
assert len(installer.installer.removals) == 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(): ...@@ -132,3 +132,20 @@ def test_metadata_with_vcs_dependencies():
requires_dist = metadata["Requires-Dist"] requires_dist = metadata["Requires-Dist"]
assert "cleo @ git+https://github.com/sdispater/cleo.git@master" == 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