Commit c7825820 by Sébastien Eustace

Add support for directory dependencies

parent d8b15dd9
......@@ -29,3 +29,4 @@ MANIFEST.in
/setup.py
/docs/site/*
pyproject.lock
/tests/fixtures/simple_project/setup.py
......@@ -6,8 +6,9 @@
- Added the `cache:clear` command.
- Added support for `git` dependencies in the `add` command.
- Added support for `file` dependencies in the `add` command.
- Added support for `path` dependencies in the `add` command.
- Added support for extras in the `add` command.
- Added support for directory dependencies.
- Added support for `src/` layout for packages.
- Added automatic detection of `.venv` virtualenvs.
......
......@@ -68,7 +68,7 @@ class PipInstaller(BaseInstaller):
return req
if package.source_type == 'file':
if package.source_type in ['file', 'directory']:
return os.path.realpath(package.source_reference)
if package.source_type == 'git':
......
......@@ -4,6 +4,7 @@ import re
from poetry.version.requirements import Requirement
from .dependency import Dependency
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .locker import Locker
from .package import Package
......
......@@ -109,6 +109,9 @@ class Dependency(object):
def is_file(self):
return False
def is_directory(self):
return False
def accepts(self, package): # type: (poetry.packages.Package) -> bool
"""
Determines if the given package matches this dependency.
......
import os
import pkginfo
from pkginfo.distribution import HEADER_ATTRS
from pkginfo.distribution import HEADER_ATTRS_2_0
from poetry.io import NullIO
from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires
from poetry.utils.toml_file import TomlFile
from poetry.utils.venv import NullVenv
from poetry.utils.venv import Venv
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 DirectoryDependency(Dependency):
def __init__(self,
path, # type: Path
category='main', # type: str
optional=False, # type: bool
base=None, # type: Path
develop=False # type: bool
):
from . import dependency_from_pep_508
self._path = path
self._base = base
self._full_path = path
self._develop = develop
if self._base and not self._path.is_absolute():
self._full_path = self._base / self._path
if not self._full_path.exists():
raise ValueError('Directory {} does not exist'.format(self._path))
if self._full_path.is_file():
raise ValueError(
'{} is a file, expected a directory'.format(self._path)
)
# Checking content to dertermine actions
setup = self._full_path / 'setup.py'
pyproject = TomlFile(self._full_path / 'pyproject.toml')
has_poetry = False
if pyproject.exists():
pyproject_content = pyproject.read(True)
has_poetry = (
'tool' in pyproject_content
and 'poetry' in pyproject_content['tool']
)
if not setup.exists() and not has_poetry:
raise ValueError(
'Directory {} does not seem to be a Python package'.format(
self._full_path
)
)
if has_poetry:
from poetry.masonry.builders import SdistBuilder
from poetry.poetry import Poetry
poetry = Poetry.create(self._full_path)
builder = SdistBuilder(poetry, NullVenv(), NullIO())
with setup.open('w') as f:
f.write(to_str(builder.build_setup()))
self._package = poetry.package
else:
from poetry.packages import Package
# Execute egg_info
current_dir = os.getcwd()
os.chdir(str(self._full_path))
try:
cwd = base
venv = Venv.create(NullIO(), cwd=cwd)
venv.run(
'python', 'setup.py', 'egg_info'
)
finally:
os.chdir(current_dir)
egg_info = list(self._full_path.glob('*.egg-info'))[0]
meta = pkginfo.UnpackedSDist(str(egg_info))
if meta.requires_dist:
reqs = list(meta.requires_dist)
else:
reqs = []
requires = egg_info / 'requires.txt'
if requires.exists():
with requires.open() as f:
reqs = parse_requires(f.read())
package = Package(meta.name, meta.version)
package.description = meta.summary
for req in reqs:
package.requires.append(dependency_from_pep_508(req))
if meta.requires_python:
package.python_versions = meta.requires_python
if meta.platforms:
platforms = [
p
for p in meta.platforms
if p.lower() != 'unknown'
]
if platforms:
package.platform = ' || '.join(platforms)
self._package = package
self._package.source_type = 'directory'
self._package.source_reference = str(self._path)
super(DirectoryDependency, self).__init__(
self._package.name,
self._package.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 package(self):
return self._package
@property
def develop(self):
return self._develop
def is_directory(self):
return True
......@@ -15,6 +15,7 @@ from poetry.version import parse as parse_version
from .constraints.generic_constraint import GenericConstraint
from .dependency import Dependency
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .vcs_dependency import VCSDependency
......@@ -273,20 +274,37 @@ class Package(object):
rev=constraint.get('rev', None),
optional=optional,
)
if python_versions:
dependency.python_versions = python_versions
if platform:
dependency.platform = platform
elif 'file' in constraint:
file_path = Path(constraint['file'])
dependency = FileDependency(file_path, base=self.cwd)
dependency = FileDependency(
file_path,
category=category,
base=self.cwd
)
elif 'path' in constraint:
path = Path(constraint['path'])
dependency = FileDependency(path, base=self.cwd)
if self.cwd:
is_file = (self.cwd / path).is_file()
else:
is_file = path.is_file()
if is_file:
dependency = FileDependency(
path,
category=category,
optional=optional,
base=self.cwd
)
else:
dependency = DirectoryDependency(
path,
category=category,
optional=optional,
base=self.cwd,
develop=constraint.get('develop', False)
)
else:
version = constraint['version']
......
......@@ -14,6 +14,7 @@ from poetry.mixology.contracts import SpecificationProvider
from poetry.mixology.contracts import UI
from poetry.packages import Dependency
from poetry.packages import DirectoryDependency
from poetry.packages import FileDependency
from poetry.packages import Package
from poetry.packages import VCSDependency
......@@ -84,6 +85,8 @@ class Provider(SpecificationProvider, UI):
packages = self.search_for_vcs(dependency)
elif dependency.is_file():
packages = self.search_for_file(dependency)
elif dependency.is_directory():
packages = self.search_for_directory(dependency)
else:
constraint = dependency.constraint
......@@ -237,9 +240,20 @@ class Provider(SpecificationProvider, UI):
return [package]
def search_for_directory(self, dependency
): # type: (DirectoryDependency) -> List[Package]
package = dependency.package
if dependency.extras:
for extra in dependency.extras:
if extra in package.extras:
for dep in package.extras[extra]:
dep.activate()
return [package]
def dependencies_for(self, package
): # type: (Package) -> Union[List[Dependency], Dependencies]
if package.source_type in ['git', 'file']:
if package.source_type in ['git', 'file', 'directory']:
# Information should already be set
return [
r for r in package.requires
......
# -*- coding: utf-8 -*-
from setuptools import setup
kwargs = dict(
name='my-package',
license='MIT',
version='0.1.2',
description='Demo project.',
author='Sébastien Eustace',
author_email='sebastien@eustace.io',
url='https://github.com/demo/demo',
packages=[
'my_package',
],
install_requires=[
'pendulum>=1.4.4',
'cachy[msgpack]>=0.2.0,<0.3.0',
],
)
setup(**kwargs)
......@@ -33,6 +33,12 @@ orator = { version = "^0.9", optional = true }
# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
# Dir dependency with setup.py
my-package = { path = "../project_with_setup/" }
# Dir dependency with pyproject.toml
simple-project = { path = "../simple_project/" }
[tool.poetry.extras]
db = [ "orator" ]
......
[tool.poetry]
name = "my-package"
name = "simple-project"
version = "1.2.3"
description = "Some description."
authors = [
......
[[package]]
name = "cachy"
version = "0.2.0"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "my-package"
version = "0.1.2"
description = "Demo project."
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.source]
type = "directory"
reference = "tests/fixtures/project_with_setup"
url = ""
[package.dependencies]
cachy = "< 0.3.0.0, >= 0.2.0.0"
pendulum = ">= 1.4.4.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]
cachy = []
my-package = []
pendulum = []
......@@ -549,3 +549,26 @@ def test_run_installs_with_local_file(installer, locker, repo, package):
assert locker.written_data == expected
assert len(installer.installer.installs) == 2
def test_run_installs_with_local_directory(installer, locker, repo, package):
file_path = Path(
'tests/fixtures/project_with_setup/'
)
package.add_dependency(
'demo',
{
'path': str(file_path)
}
)
repo.add_package(get_package('pendulum', '1.4.4'))
repo.add_package(get_package('cachy', '0.2.0'))
installer.run()
expected = fixture('with-directory-dependency')
assert locker.written_data == expected
assert len(installer.installer.installs) == 3
......@@ -63,6 +63,24 @@ def test_poetry():
assert demo.name == 'demo'
assert demo.pretty_constraint == '0.1.0'
demo = dependencies['my-package']
assert not demo.is_file()
assert demo.is_directory()
assert not demo.is_vcs()
assert demo.name == 'my-package'
assert demo.pretty_constraint == '0.1.2'
assert demo.package.requires[0].name == 'pendulum'
assert demo.package.requires[1].name == 'cachy'
assert demo.package.requires[1].extras == ['msgpack']
simple_project = dependencies['simple-project']
assert not simple_project.is_file()
assert simple_project.is_directory()
assert not simple_project.is_vcs()
assert simple_project.name == 'simple-project'
assert simple_project.pretty_constraint == '1.2.3'
assert simple_project.package.requires == []
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