Commit c7825820 by Sébastien Eustace

Add support for directory dependencies

parent d8b15dd9
...@@ -29,3 +29,4 @@ MANIFEST.in ...@@ -29,3 +29,4 @@ MANIFEST.in
/setup.py /setup.py
/docs/site/* /docs/site/*
pyproject.lock pyproject.lock
/tests/fixtures/simple_project/setup.py
...@@ -6,8 +6,9 @@ ...@@ -6,8 +6,9 @@
- Added the `cache:clear` command. - Added the `cache:clear` command.
- Added support for `git` dependencies in the `add` 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 extras in the `add` command.
- Added support for directory dependencies.
- Added support for `src/` layout for packages. - Added support for `src/` layout for packages.
- Added automatic detection of `.venv` virtualenvs. - Added automatic detection of `.venv` virtualenvs.
......
...@@ -68,7 +68,7 @@ class PipInstaller(BaseInstaller): ...@@ -68,7 +68,7 @@ class PipInstaller(BaseInstaller):
return req return req
if package.source_type == 'file': if package.source_type in ['file', 'directory']:
return os.path.realpath(package.source_reference) return os.path.realpath(package.source_reference)
if package.source_type == 'git': if package.source_type == 'git':
......
...@@ -4,6 +4,7 @@ import re ...@@ -4,6 +4,7 @@ import re
from poetry.version.requirements import Requirement from poetry.version.requirements import Requirement
from .dependency import Dependency from .dependency import Dependency
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency from .file_dependency import FileDependency
from .locker import Locker from .locker import Locker
from .package import Package from .package import Package
......
...@@ -109,6 +109,9 @@ class Dependency(object): ...@@ -109,6 +109,9 @@ class Dependency(object):
def is_file(self): def is_file(self):
return False return False
def is_directory(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.
......
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 ...@@ -15,6 +15,7 @@ from poetry.version import parse as parse_version
from .constraints.generic_constraint import GenericConstraint from .constraints.generic_constraint import GenericConstraint
from .dependency import Dependency from .dependency import Dependency
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency from .file_dependency import FileDependency
from .vcs_dependency import VCSDependency from .vcs_dependency import VCSDependency
...@@ -273,20 +274,37 @@ class Package(object): ...@@ -273,20 +274,37 @@ class Package(object):
rev=constraint.get('rev', None), rev=constraint.get('rev', None),
optional=optional, optional=optional,
) )
if python_versions:
dependency.python_versions = python_versions
if platform:
dependency.platform = platform
elif 'file' in constraint: elif 'file' in constraint:
file_path = Path(constraint['file']) 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: elif 'path' in constraint:
path = Path(constraint['path']) 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: else:
version = constraint['version'] version = constraint['version']
...@@ -297,11 +315,11 @@ class Package(object): ...@@ -297,11 +315,11 @@ class Package(object):
allows_prereleases=allows_prereleases allows_prereleases=allows_prereleases
) )
if python_versions: if python_versions:
dependency.python_versions = python_versions dependency.python_versions = python_versions
if platform: if platform:
dependency.platform = platform dependency.platform = platform
if 'extras' in constraint: if 'extras' in constraint:
for extra in constraint['extras']: for extra in constraint['extras']:
......
...@@ -14,6 +14,7 @@ from poetry.mixology.contracts import SpecificationProvider ...@@ -14,6 +14,7 @@ from poetry.mixology.contracts import SpecificationProvider
from poetry.mixology.contracts import UI from poetry.mixology.contracts import UI
from poetry.packages import Dependency from poetry.packages import Dependency
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 VCSDependency from poetry.packages import VCSDependency
...@@ -84,6 +85,8 @@ class Provider(SpecificationProvider, UI): ...@@ -84,6 +85,8 @@ class Provider(SpecificationProvider, UI):
packages = self.search_for_vcs(dependency) packages = self.search_for_vcs(dependency)
elif dependency.is_file(): elif dependency.is_file():
packages = self.search_for_file(dependency) packages = self.search_for_file(dependency)
elif dependency.is_directory():
packages = self.search_for_directory(dependency)
else: else:
constraint = dependency.constraint constraint = dependency.constraint
...@@ -237,9 +240,20 @@ class Provider(SpecificationProvider, UI): ...@@ -237,9 +240,20 @@ class Provider(SpecificationProvider, UI):
return [package] 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 def dependencies_for(self, package
): # type: (Package) -> Union[List[Dependency], Dependencies] ): # 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 # Information should already be set
return [ return [
r for r in package.requires 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 } ...@@ -33,6 +33,12 @@ orator = { version = "^0.9", optional = true }
# File dependency # File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } 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] [tool.poetry.extras]
db = [ "orator" ] db = [ "orator" ]
......
[tool.poetry] [tool.poetry]
name = "my-package" name = "simple-project"
version = "1.2.3" version = "1.2.3"
description = "Some description." description = "Some description."
authors = [ 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): ...@@ -549,3 +549,26 @@ def test_run_installs_with_local_file(installer, locker, repo, package):
assert locker.written_data == expected assert locker.written_data == expected
assert len(installer.installer.installs) == 2 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(): ...@@ -63,6 +63,24 @@ def test_poetry():
assert demo.name == 'demo' assert demo.name == 'demo'
assert demo.pretty_constraint == '0.1.0' 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 assert 'db' in package.extras
classifiers = package.classifiers 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