Commit 7a14aa59 by Sébastien Eustace

Add support for git dependencies in the add command

parent 9cf87a28
......@@ -5,6 +5,7 @@
### Added
- Added the `cache:clear` command.
- Added support for `git` dependencies in the `add` command.
### Changed
......
......@@ -12,10 +12,11 @@ class AddCommand(VenvCommand):
add
{ name* : Packages to add. }
{--D|dev : Add package as development dependency. }
{--optional : Add as an optional dependency. }
{ --D|dev : Add package as development dependency. }
{ --git= : The url of the Git repository. }
{ --optional : Add as an optional dependency. }
{ --allow-prereleases : Accept prereleases. }
{--dry-run : Outputs the operations but will not execute anything
{ --dry-run : Outputs the operations but will not execute anything
(implicitly enables --verbose). }
"""
......@@ -35,6 +36,11 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
packages = self.argument('name')
is_dev = self.option('dev')
if self.option('git') and len(packages) > 1:
raise ValueError(
'You can only specify one package when using the --git option'
)
section = 'dependencies'
if is_dev:
section = 'dev-dependencies'
......@@ -50,6 +56,11 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
'Package {} is already present'.format(name)
)
if self.option('git'):
requirements = {
packages[0]: ''
}
else:
requirements = self._determine_requirements(
packages,
allow_prereleases=self.option('allow-prereleases')
......@@ -62,19 +73,24 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
parser.parse_constraints(constraint)
for name, constraint in requirements.items():
if self.option('optional') or self.option('allow-prereleases'):
constraint = {
'version': constraint
}
if self.option('git'):
del constraint['version']
constraint['git'] = self.option('git')
if self.option('optional'):
constraint = {
'optional': True
}
constraint['optional'] = True
if self.option('allow-prereleases'):
constraint['allows-prereleases'] = True
if len(constraint) == 1 and 'version' in constraint:
constraint = constraint['version']
poetry_content[section][name] = constraint
# Write new content
......
import os
import pkginfo
import shutil
from functools import cmp_to_key
......@@ -22,6 +23,7 @@ from poetry.repositories import Pool
from poetry.semver import less_than
from poetry.utils._compat import Path
from poetry.utils.helpers import parse_requires
from poetry.utils.toml_file import TomlFile
from poetry.utils.venv import Venv
......@@ -175,15 +177,27 @@ class Provider(SpecificationProvider, UI):
try:
venv = Venv.create(self._io)
output = venv.run(
'python', 'setup.py',
'--name', '--version'
venv.run(
'python', 'setup.py', 'egg_info'
)
output = output.split('\n')
name = output[-3]
version = output[-2]
package = Package(name, version, version)
# Figure out a way to get requirements
egg_info = list(tmp_dir.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)
for req in reqs:
package.requires.append(dependency_from_pep_508(req))
except Exception:
raise
finally:
......
......@@ -34,6 +34,7 @@ from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
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.version.markers import InvalidMarker
......@@ -424,7 +425,7 @@ class PyPiRepository(Repository):
requires = egg_info / 'requires.txt'
if requires.exists():
with requires.open() as f:
return self._parse_requires(f.read())
return parse_requires(f.read())
return
......@@ -442,46 +443,5 @@ class PyPiRepository(Repository):
if chunk:
f.write(chunk)
def _parse_requires(self, requires): # type: (str) -> Union[list, None]
lines = requires.split('\n')
requires_dist = []
in_section = False
current_marker = None
for line in lines:
line = line.strip()
if not line:
if in_section:
in_section = False
continue
if line.startswith('['):
# extras or conditional dependencies
marker = line.lstrip('[').rstrip(']')
if ':' not in marker:
extra, marker = marker, None
else:
extra, marker = marker.split(':')
if extra:
if marker:
marker = '{} and extra == "{}"'.format(marker, extra)
else:
marker = 'extra == "{}"'.format(extra)
if marker:
current_marker = marker
continue
if current_marker:
line = '{}; {}'.format(line, current_marker)
requires_dist.append(line)
if requires_dist:
return requires_dist
def _log(self, msg, level='info'):
getattr(logger, level)('{}: {}'.format(self._name, msg))
......@@ -3,6 +3,7 @@ import shutil
import tempfile
from contextlib import contextmanager
from typing import Union
_canonicalize_regex = re.compile('[-_.]+')
......@@ -28,3 +29,45 @@ def temporary_directory(*args, **kwargs):
yield name
shutil.rmtree(name)
def parse_requires(requires): # type: (str) -> Union[list, None]
lines = requires.split('\n')
requires_dist = []
in_section = False
current_marker = None
for line in lines:
line = line.strip()
if not line:
if in_section:
in_section = False
continue
if line.startswith('['):
# extras or conditional dependencies
marker = line.lstrip('[').rstrip(']')
if ':' not in marker:
extra, marker = marker, None
else:
extra, marker = marker.split(':')
if extra:
if marker:
marker = '{} and extra == "{}"'.format(marker, extra)
else:
marker = 'extra == "{}"'.format(extra)
if marker:
current_marker = marker
continue
if current_marker:
line = '{}; {}'.format(line, current_marker)
requires_dist.append(line)
if requires_dist:
return requires_dist
......@@ -35,6 +35,11 @@ Writing lock file
assert len(installer.installs) == 1
content = app.poetry.file.read(raw=True)['tool']['poetry']
assert 'cachy' in content['dependencies']
assert content['dependencies']['cachy'] == '^0.2.0'
def test_add_constraint(app, repo, installer):
command = app.find('add')
......@@ -104,3 +109,71 @@ Writing lock file
assert len(installer.installs) == 2
def test_add_git_constraint(app, repo, installer):
command = app.find('add')
tester = CommandTester(command)
repo.add_package(get_package('pendulum', '1.4.4'))
tester.execute([
('command', command.get_name()),
('name', ['demo']),
('--git', 'https://github.com/demo/demo.git')
])
expected = """\
Updating dependencies
Resolving dependencies..
Package operations: 2 installs, 0 updates, 0 removals
Writing lock file
- Installing pendulum (1.4.4)
- Installing demo (0.1.2 9cf87a2)
"""
assert tester.get_display() == expected
assert len(installer.installs) == 2
content = app.poetry.file.read(raw=True)['tool']['poetry']
assert 'demo' in content['dependencies']
assert content['dependencies']['demo'] == {
'git': 'https://github.com/demo/demo.git'
}
def test_add_git_constraint_with_poetry(app, repo, installer):
command = app.find('add')
tester = CommandTester(command)
repo.add_package(get_package('pendulum', '1.4.4'))
tester.execute([
('command', command.get_name()),
('name', ['demo']),
('--git', 'https://github.com/demo/pyproject-demo.git')
])
expected = """\
Updating dependencies
Resolving dependencies
Package operations: 2 installs, 0 updates, 0 removals
Writing lock file
- Installing pendulum (1.4.4)
- Installing demo (0.1.2 9cf87a2)
"""
assert tester.get_display() == expected
assert len(installer.installs) == 2
import pytest
import shutil
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
from poetry.config import Config as BaseConfig
from poetry.console import Application as BaseApplication
......@@ -17,6 +23,20 @@ def installer():
return NoopInstaller()
def mock_clone(self, source, dest):
# Checking source to determine which folder we need to copy
parts = urlparse.urlparse(source)
folder = (
Path(__file__).parent.parent
/ 'fixtures' / 'git'
/ parts.netloc / parts.path.lstrip('/').rstrip('.git')
)
shutil.rmtree(dest)
shutil.copytree(folder, dest)
@pytest.fixture(autouse=True)
def setup(mocker, installer):
# Set Installer's installer
......@@ -26,6 +46,12 @@ def setup(mocker, installer):
p = mocker.patch('poetry.installation.installer.Installer._get_installed')
p.return_value = Repository()
# Patch git module to not actually clone projects
mocker.patch('poetry.vcs.git.Git.clone', new=mock_clone)
mocker.patch('poetry.vcs.git.Git.checkout', new=lambda *_: None)
p = mocker.patch('poetry.vcs.git.Git.rev_parse')
p.return_value = '9cf87a285a2d3fbb0b9fa621997b3acc3631ed24'
class Application(BaseApplication):
......@@ -55,6 +81,9 @@ class Locker(BaseLocker):
self._lock_data = None
self._content_hash = self._get_content_hash()
def _write_lock_data(self, data):
self._lock_data = None
class Poetry(BasePoetry):
......
[tool.poetry]
name = "demo"
version = "0.1.2"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
pendulum = '^1.4'
[tool.poetry.dev-dependencies]
......@@ -64,52 +64,3 @@ def test_package_drops_malformed_dependencies():
dependency_names = [d.name for d in package.requires]
assert 'setuptools' not in dependency_names
def test_parse_requires():
requires = """\
jsonschema>=2.6.0.0,<3.0.0.0
lockfile>=0.12.0.0,<0.13.0.0
pip-tools>=1.11.0.0,<2.0.0.0
pkginfo>=1.4.0.0,<2.0.0.0
pyrsistent>=0.14.2.0,<0.15.0.0
toml>=0.9.0.0,<0.10.0.0
cleo>=0.6.0.0,<0.7.0.0
cachy>=0.1.1.0,<0.2.0.0
cachecontrol>=0.12.4.0,<0.13.0.0
requests>=2.18.0.0,<3.0.0.0
msgpack-python>=0.5.0.0,<0.6.0.0
pyparsing>=2.2.0.0,<3.0.0.0
requests-toolbelt>=0.8.0.0,<0.9.0.0
[:(python_version >= "2.7.0.0" and python_version < "2.8.0.0") or (python_version >= "3.4.0.0" and python_version < "3.5.0.0")]
typing>=3.6.0.0,<4.0.0.0
[:python_version >= "2.7.0.0" and python_version < "2.8.0.0"]
virtualenv>=15.2.0.0,<16.0.0.0
pathlib2>=2.3.0.0,<3.0.0.0
[:python_version >= "3.4.0.0" and python_version < "3.6.0.0"]
zipfile36>=0.1.0.0,<0.2.0.0
"""
result = MockRepository()._parse_requires(requires)
expected = [
'jsonschema>=2.6.0.0,<3.0.0.0',
'lockfile>=0.12.0.0,<0.13.0.0',
'pip-tools>=1.11.0.0,<2.0.0.0',
'pkginfo>=1.4.0.0,<2.0.0.0',
'pyrsistent>=0.14.2.0,<0.15.0.0',
'toml>=0.9.0.0,<0.10.0.0',
'cleo>=0.6.0.0,<0.7.0.0',
'cachy>=0.1.1.0,<0.2.0.0',
'cachecontrol>=0.12.4.0,<0.13.0.0',
'requests>=2.18.0.0,<3.0.0.0',
'msgpack-python>=0.5.0.0,<0.6.0.0',
'pyparsing>=2.2.0.0,<3.0.0.0',
'requests-toolbelt>=0.8.0.0,<0.9.0.0',
'typing>=3.6.0.0,<4.0.0.0; (python_version >= "2.7.0.0" and python_version < "2.8.0.0") or (python_version >= "3.4.0.0" and python_version < "3.5.0.0")',
'virtualenv>=15.2.0.0,<16.0.0.0; python_version >= "2.7.0.0" and python_version < "2.8.0.0"',
'pathlib2>=2.3.0.0,<3.0.0.0; python_version >= "2.7.0.0" and python_version < "2.8.0.0"',
'zipfile36>=0.1.0.0,<0.2.0.0; python_version >= "3.4.0.0" and python_version < "3.6.0.0"'
]
assert result == expected
from poetry.utils.helpers import parse_requires
def test_parse_requires():
requires = """\
jsonschema>=2.6.0.0,<3.0.0.0
lockfile>=0.12.0.0,<0.13.0.0
pip-tools>=1.11.0.0,<2.0.0.0
pkginfo>=1.4.0.0,<2.0.0.0
pyrsistent>=0.14.2.0,<0.15.0.0
toml>=0.9.0.0,<0.10.0.0
cleo>=0.6.0.0,<0.7.0.0
cachy>=0.1.1.0,<0.2.0.0
cachecontrol>=0.12.4.0,<0.13.0.0
requests>=2.18.0.0,<3.0.0.0
msgpack-python>=0.5.0.0,<0.6.0.0
pyparsing>=2.2.0.0,<3.0.0.0
requests-toolbelt>=0.8.0.0,<0.9.0.0
[:(python_version >= "2.7.0.0" and python_version < "2.8.0.0") or (python_version >= "3.4.0.0" and python_version < "3.5.0.0")]
typing>=3.6.0.0,<4.0.0.0
[:python_version >= "2.7.0.0" and python_version < "2.8.0.0"]
virtualenv>=15.2.0.0,<16.0.0.0
pathlib2>=2.3.0.0,<3.0.0.0
[:python_version >= "3.4.0.0" and python_version < "3.6.0.0"]
zipfile36>=0.1.0.0,<0.2.0.0
"""
result = parse_requires(requires)
expected = [
'jsonschema>=2.6.0.0,<3.0.0.0',
'lockfile>=0.12.0.0,<0.13.0.0',
'pip-tools>=1.11.0.0,<2.0.0.0',
'pkginfo>=1.4.0.0,<2.0.0.0',
'pyrsistent>=0.14.2.0,<0.15.0.0',
'toml>=0.9.0.0,<0.10.0.0',
'cleo>=0.6.0.0,<0.7.0.0',
'cachy>=0.1.1.0,<0.2.0.0',
'cachecontrol>=0.12.4.0,<0.13.0.0',
'requests>=2.18.0.0,<3.0.0.0',
'msgpack-python>=0.5.0.0,<0.6.0.0',
'pyparsing>=2.2.0.0,<3.0.0.0',
'requests-toolbelt>=0.8.0.0,<0.9.0.0',
'typing>=3.6.0.0,<4.0.0.0; (python_version >= "2.7.0.0" and python_version < "2.8.0.0") or (python_version >= "3.4.0.0" and python_version < "3.5.0.0")',
'virtualenv>=15.2.0.0,<16.0.0.0; python_version >= "2.7.0.0" and python_version < "2.8.0.0"',
'pathlib2>=2.3.0.0,<3.0.0.0; python_version >= "2.7.0.0" and python_version < "2.8.0.0"',
'zipfile36>=0.1.0.0,<0.2.0.0; python_version >= "3.4.0.0" and python_version < "3.6.0.0"'
]
assert result == expected
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