Commit 6344e7a8 by Sébastien Eustace

Add support for local files as dependencies

parent c2bb760a
......@@ -7,6 +7,7 @@
- Added support for Python 2.7.
- Added a fallback mechanism (opt-in) for missing dependencies.
- Added `search` command.
- Added support for local files as dependencies.
### Changes
......
......@@ -59,7 +59,7 @@ class PipInstaller(BaseInstaller):
return self._venv.run('pip', *args, **kwargs)
def requirement(self, package, formatted=False):
if formatted and not package.source_type == 'git':
if formatted and not package.source_type:
req = '{}=={}'.format(package.name, package.version)
for h in package.hashes:
req += ' --hash sha256:{}'.format(h)
......@@ -68,6 +68,9 @@ class PipInstaller(BaseInstaller):
return req
if package.source_type == 'file':
return os.path.realpath(package.source_reference)
if package.source_type == 'git':
return 'git+{}@{}#egg={}'.format(
package.source_url,
......
......@@ -75,6 +75,9 @@
},
{
"$ref": "#/definitions/git-dependency"
},
{
"$ref": "#/definitions/file-dependency"
}
]
}
......@@ -95,6 +98,9 @@
},
{
"$ref": "#/definitions/git-dependency"
},
{
"$ref": "#/definitions/file-dependency"
}
]
}
......@@ -157,7 +163,7 @@
},
"python": {
"type": "string",
"description": "The python versions for c=which the dependency should be installed."
"description": "The python versions for which the dependency should be installed."
},
"allows_prereleases": {
"type": "boolean",
......@@ -200,7 +206,37 @@
},
"python": {
"type": "string",
"description": "The python versions for c=which the dependency should be installed."
"description": "The python versions for which the dependency should be installed."
},
"allows_prereleases": {
"type": "boolean",
"description": "Whether the dependency allows prereleases or not."
},
"optional": {
"type": "boolean",
"description": "Whether the dependency is optional or not."
},
"extras": {
"type": "array",
"description": "The required extras for this dependency.",
"items": {
"type": "string"
}
}
}
},
"file-dependency": {
"type": "object",
"required": ["file"],
"additionalProperties": false,
"properties": {
"file": {
"type": "string",
"description": "The path to the file."
},
"python": {
"type": "string",
"description": "The python versions for which the dependency should be installed."
},
"allows_prereleases": {
"type": "boolean",
......
......@@ -43,7 +43,7 @@ setup(**setup_kwargs)
PKG_INFO = """\
Metadata-Version: 1.1
Metadata-Version: 2.1
Name: {name}
Version: {version}
Summary: {summary}
......@@ -96,14 +96,7 @@ class SdistBuilder(Builder):
tar_info.size = len(setup)
tar.addfile(tar_info, BytesIO(setup))
pkg_info = encode(PKG_INFO.format(
name=self._meta.name,
version=self._meta.version,
summary=self._meta.summary,
home_page=self._meta.home_page,
author=to_str(self._meta.author),
author_email=to_str(self._meta.author_email),
))
pkg_info = self.build_pkg_info()
tar_info = tarfile.TarInfo(pjoin(tar_dir, 'PKG-INFO'))
tar_info.size = len(pkg_info)
......@@ -172,6 +165,35 @@ class SdistBuilder(Builder):
after='\n'.join(after)
))
def build_pkg_info(self):
pkg_info = PKG_INFO.format(
name=self._meta.name,
version=self._meta.version,
summary=self._meta.summary,
home_page=self._meta.home_page,
author=to_str(self._meta.author),
author_email=to_str(self._meta.author_email),
)
if self._meta.keywords:
pkg_info += "Keywords: {}\n".format(self._meta.keywords)
if self._meta.requires_python:
pkg_info += 'Requires-Python: {}\n'.format(
self._meta.requires_python
)
for classifier in self._meta.classifiers:
pkg_info += 'Classifier: {}\n'.format(classifier)
for extra in sorted(self._meta.provides_extra):
pkg_info += 'Provides-Extra: {}\n'.format(extra)
for dep in sorted(self._meta.requires_dist):
pkg_info += 'Requires-Dist: {}\n'.format(dep)
return encode(pkg_info)
@classmethod
def find_packages(cls, path):
"""
......
......@@ -4,6 +4,7 @@ import re
from poetry.version.requirements import Requirement
from .dependency import Dependency
from .file_dependency import FileDependency
from .locker import Locker
from .package import Package
from .utils.link import Link
......
......@@ -106,6 +106,9 @@ class Dependency(object):
def is_vcs(self):
return False
def is_file(self):
return False
def accepts(self, package): # type: (poetry.packages.Package) -> bool
"""
Determines if the given package matches this dependency.
......@@ -209,4 +212,4 @@ class Dependency(object):
)
def __repr__(self):
return '<Dependency {}>'.format(str(self))
return '<{} {}>'.format(self.__class__.__name__, str(self))
import hashlib
import io
import pkginfo
from pkginfo.distribution import HEADER_ATTRS
from pkginfo.distribution import HEADER_ATTRS_2_0
from poetry.utils._compat import Path
from .dependency import Dependency
# Patching pkginfo to support Metadata version 2.1 (PEP 566)
HEADER_ATTRS.update(
{
'2.1': HEADER_ATTRS_2_0 + (
('Provides-Extra', 'provides_extra', True),
)
}
)
class FileDependency(Dependency):
def __init__(self,
path, # type: Path
category='main', # type: str
optional=False, # type: bool
base=None # type: Path
):
self._path = path
self._base = base
self._full_path = path
if self._base and not self._path.is_absolute():
self._full_path = self._base / self._path
if not self._full_path.exists():
raise ValueError('File {} does not exist'.format(self._path))
if self._full_path.is_dir():
raise ValueError(
'{} is a directory, expected a file'.format(self._path)
)
if self._path.suffix == '.whl':
self._meta = pkginfo.Wheel(str(self._full_path))
else:
# Assume sdist
self._meta = pkginfo.SDist(str(self._full_path))
super(FileDependency, self).__init__(
self._meta.name,
self._meta.version,
category=category,
optional=optional,
allows_prereleases=True
)
@property
def path(self):
return self._path
@property
def full_path(self):
return self._full_path.resolve()
@property
def metadata(self):
return self._meta
def is_file(self):
return True
def hash(self):
h = hashlib.sha256()
with self._path.open('rb') as fp:
for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b''):
h.update(content)
return h.hexdigest()
......@@ -10,10 +10,12 @@ from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser
from poetry.spdx import license_by_id
from poetry.spdx import License
from poetry.utils._compat import Path
from poetry.version import parse as parse_version
from .constraints.generic_constraint import GenericConstraint
from .dependency import Dependency
from .file_dependency import FileDependency
from .vcs_dependency import VCSDependency
AUTHOR_REGEX = re.compile('(?u)^(?P<name>[- .,\w\d\'’"()]+) <(?P<email>.+?)>$')
......@@ -104,6 +106,8 @@ class Package(object):
self._platform = '*'
self._platform_constraint = EmptyConstraint()
self.cwd = None
@property
def name(self):
return self._name
......@@ -253,12 +257,13 @@ class Package(object):
constraint = '*'
if isinstance(constraint, dict):
optional = constraint.get('optional', False)
python_versions = constraint.get('python')
platform = constraint.get('platform')
allows_prereleases = constraint.get('allows_prereleases', False)
if 'git' in constraint:
# VCS dependency
optional = constraint.get('optional', False)
python_versions = constraint.get('python')
platform = constraint.get('platform')
dependency = VCSDependency(
name,
'git', constraint['git'],
......@@ -273,12 +278,12 @@ class Package(object):
if platform:
dependency.platform = platform
elif 'file' in constraint:
file_path = Path(constraint['file'])
dependency = FileDependency(file_path, base=self.cwd)
else:
version = constraint['version']
optional = constraint.get('optional', False)
allows_prereleases = constraint.get('allows_prereleases', False)
python_versions = constraint.get('python')
platform = constraint.get('platform')
dependency = Dependency(
name, version,
......
......@@ -99,6 +99,7 @@ class Poetry:
name = local_config['name']
version = local_config['version']
package = Package(name, version, version)
package.cwd = Path(cwd)
for author in local_config['authors']:
package.authors.append(author)
......
......@@ -11,8 +11,10 @@ from poetry.mixology.conflict import Conflict
from poetry.mixology.contracts import SpecificationProvider
from poetry.packages import Dependency
from poetry.packages import FileDependency
from poetry.packages import Package
from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508
from poetry.repositories import Pool
......@@ -71,6 +73,8 @@ class Provider(SpecificationProvider):
if dependency.is_vcs():
packages = self.search_for_vcs(dependency)
elif dependency.is_file():
packages = self.search_for_file(dependency)
else:
packages = self._pool.find_packages(
dependency.name,
......@@ -175,8 +179,28 @@ class Provider(SpecificationProvider):
return [package]
def search_for_file(self, dependency
): # type: (FileDependency) -> List[Package]
package = Package(dependency.name, dependency.pretty_constraint)
package.source_type = 'file'
package.source_reference = str(dependency.path)
package.description = dependency.metadata.summary
for req in dependency.metadata.requires_dist:
package.requires.append(dependency_from_pep_508(req))
if dependency.metadata.requires_python:
package.python_versions = dependency.metadata.requires_python
if dependency.metadata.platforms:
package.platform = ' || '.join(dependency.metadata.platforms)
package.hashes = [dependency.hash()]
return [package]
def dependencies_for(self, package): # type: (Package) -> List[Dependency]
if package.source_type == 'git':
if package.source_type in ['git', 'file']:
# Information should already be set
pass
else:
......
......@@ -7,7 +7,7 @@ class WilcardConstraint(Constraint):
def __init__(self, constraint): # type: (str) -> None
m = re.match(
'^(!=|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
'^(!= ?|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
constraint
)
if not m:
......@@ -16,7 +16,7 @@ class WilcardConstraint(Constraint):
if not m.group(1):
operator = '=='
else:
operator = m.group(1)
operator = m.group(1).strip()
super(WilcardConstraint, self).__init__(
operator,
......
......@@ -185,7 +185,7 @@ class VersionParser:
# A partial version range is treated as an X-Range,
# so the special character is in fact optional.
m = re.match(
'^(!=|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
'^(!= ?|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
constraint
)
if m:
......
......@@ -30,6 +30,9 @@ pathlib2 = { version = "^2.2", python = "~2.7" }
orator = { version = "^0.9", optional = true }
# File dependency
demo = { file = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
[tool.poetry.extras]
db = [ "orator" ]
......
......@@ -2,6 +2,10 @@ from poetry.packages import Dependency
from poetry.packages import Package
from poetry.semver.helpers import normalize_version
from poetry.utils._compat import Path
FIXTURE_PATH = Path(__file__).parent / 'fixtures'
def get_package(name, version):
......@@ -20,3 +24,10 @@ def get_dependency(name,
optional=optional,
allows_prereleases=allows_prereleases
)
def fixture(path=None):
if path:
return FIXTURE_PATH / path
else:
return FIXTURE_PATH
[[package]]
name = "demo"
version = "0.1.0"
description = "Description"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
platform = "*"
[package.source]
type = "file"
reference = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
url = ""
[package.dependencies]
pendulum = ">= 1.4.0.0, < 2.0.0.0"
[[package]]
name = "pendulum"
version = "1.4.4"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
demo = ["5eb592cb6e9c42379ad576b1226ef37cb727365d40222fa9e49ef44e83cf8d1e"]
pendulum = []
......@@ -16,6 +16,7 @@ from poetry.utils._compat import Path
from poetry.utils._compat import PY2
from poetry.utils.venv import NullVenv
from tests.helpers import fixture as root_fixture
from tests.helpers import get_dependency
from tests.helpers import get_package
from tests.repositories.test_pypi_repository import MockRepository
......@@ -529,3 +530,25 @@ def test_installer_with_pypi_repository(package, locker, installed):
expected = fixture('with-pypi-repository')
assert locker.written_data == expected
def test_run_installs_with_local_file(installer, locker, repo, package):
file_path = Path(
'tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl'
)
package.add_dependency(
'demo',
{
'file': str(file_path)
}
)
repo.add_package(get_package('pendulum', '1.4.4'))
installer.run()
expected = fixture('with-file-dependency')
assert locker.written_data == expected
assert len(installer.installer.installs) == 2
import pytest
from poetry.packages import FileDependency
from poetry.utils._compat import Path
DIST_PATH = Path(__file__).parent.parent / 'fixtures' / 'distributions'
def test_file_dependency_wheel():
dependency = FileDependency(DIST_PATH / 'demo-0.1.0-py2.py3-none-any.whl')
assert dependency.is_file()
assert dependency.name == 'demo'
assert dependency.pretty_constraint == '0.1.0'
assert dependency.python_versions == '*'
assert dependency.platform == '*'
meta = dependency.metadata
assert meta.requires_dist == [
'pendulum (>=1.4.0.0,<2.0.0.0)'
]
def test_file_dependency_sdist():
dependency = FileDependency(DIST_PATH / 'demo-0.1.0.tar.gz')
assert dependency.is_file()
assert dependency.name == 'demo'
assert dependency.pretty_constraint == '0.1.0'
assert dependency.python_versions == '*'
assert dependency.platform == '*'
meta = dependency.metadata
assert meta.requires_dist == [
'pendulum (>=1.4.0.0,<2.0.0.0)'
]
def test_file_dependency_wrong_path():
with pytest.raises(ValueError):
FileDependency(DIST_PATH / 'demo-0.2.0.tar.gz')
def test_file_dependency_dir():
with pytest.raises(ValueError):
FileDependency(DIST_PATH)
......@@ -57,6 +57,12 @@ def test_poetry():
assert pathlib2.python_versions == '~2.7'
assert not pathlib2.is_optional()
demo = dependencies['demo']
assert demo.is_file()
assert not demo.is_vcs()
assert demo.name == 'demo'
assert demo.pretty_constraint == '0.1.0'
assert 'db' in package.extras
classifiers = package.classifiers
......
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