Commit 54c82eeb by Sébastien Eustace

Add the add command

parent 2c53da3f
...@@ -6,6 +6,7 @@ from poetry.poetry import Poetry ...@@ -6,6 +6,7 @@ from poetry.poetry import Poetry
from poetry.utils.venv import Venv from poetry.utils.venv import Venv
from .commands import AboutCommand from .commands import AboutCommand
from .commands import AddCommand
from .commands import InstallCommand from .commands import InstallCommand
from .commands import LockCommand from .commands import LockCommand
from .commands import UpdateCommand from .commands import UpdateCommand
...@@ -28,6 +29,9 @@ class Application(BaseApplication): ...@@ -28,6 +29,9 @@ class Application(BaseApplication):
return self._poetry return self._poetry
def reset_poetry(self) -> None:
self._poetry = None
@property @property
def venv(self) -> Venv: def venv(self) -> Venv:
return self._venv return self._venv
...@@ -37,6 +41,7 @@ class Application(BaseApplication): ...@@ -37,6 +41,7 @@ class Application(BaseApplication):
return commands + [ return commands + [
AboutCommand(), AboutCommand(),
AddCommand(),
InstallCommand(), InstallCommand(),
LockCommand(), LockCommand(),
UpdateCommand(), UpdateCommand(),
......
from .about import AboutCommand from .about import AboutCommand
from .add import AddCommand
from .install import InstallCommand from .install import InstallCommand
from .lock import LockCommand from .lock import LockCommand
from .update import UpdateCommand from .update import UpdateCommand
import re
from typing import List
from typing import Tuple
from poetry.installation import Installer
from poetry.semver.version_parser import VersionParser
from poetry.version.version_selector import VersionSelector
from .command import Command
class AddCommand(Command):
"""
Add a new depdency to <comment>poetry.toml</>.
add
{ name* : Packages to add. }
{--D|dev : Add package as development dependency. }
{--optional : Add as an optional dependency. }
"""
help = """The add command adds required packages to your <comment>poetry.toml</> and installs them.
If you do not specify a version constraint, poetry will choose a suitable one based on the available package versions.
"""
def handle(self):
names = self.argument('name')
is_dev = self.option('dev')
requirements = self._determine_requirements(names)
requirements = self._format_requirements(requirements)
# validate requirements format
parser = VersionParser()
for constraint in requirements.values():
parser.parse_constraints(constraint)
# Trying to figure out where to add our dependencies
# If we find a toml library that keeps comments
# We could remove this whole section
section = '[dependencies]'
if is_dev:
section = '[dev-dependencies]'
new_content = None
with self.poetry.locker.original.path.open() as fd:
content = fd.read().split('\n')
in_section = False
index = None
for i, line in enumerate(content):
line = line.strip()
if line == section:
in_section = True
continue
if in_section and not line:
index = i
break
if index is not None:
for i, require in enumerate(requirements.items()):
name, version = require
content.insert(
index + i,
f'{name} = "{version}"'
)
new_content = '\n'.join(content)
if new_content is not None:
with self.poetry.locker.original.path.open('w') as fd:
fd.write(new_content)
else:
# We could not find where to put the dependencies
# We raise an warning
self.warning('Unable to automatically add dependencies')
self.warning('Add them manually to your poetry.toml')
return 1
# Update packages
self.reset_poetry()
installer = Installer(
self.output,
self.poetry.package,
self.poetry.locker,
self.poetry.repository
)
installer.update(True)
installer.whitelist(requirements)
installer.run()
def _determine_requirements(self, requires: List[str]) -> List[str]:
if not requires:
return []
requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires:
if 'version' not in requirement:
# determine the best version automatically
name, version = self._find_best_version_for_package(
requirement['name']
)
requirement['version'] = version
requirement['name'] = name
self.line(
f'Using version <info>{version}</> for <info>{name}</>'
)
else:
# check that the specified version/constraint exists
# before we proceed
name, _ = self._find_best_version_for_package(
requirement['name'], requirement['version']
)
requirement['name'] = name
result.append(f'{requirement["name"]} {requirement["version"]}')
return result
def _find_best_version_for_package(self,
name,
required_version=None
) -> Tuple[str, str]:
selector = VersionSelector(self.poetry.repository)
package = selector.find_best_candidate(name, required_version)
if not package:
# TODO: find similar
raise ValueError(
f'Could not find a matching version of package {name}'
)
return (
package.pretty_name,
selector.find_recommended_require_version(package)
)
def _parse_name_version_pairs(self, pairs: list) -> list:
result = []
for i in range(len(pairs)):
pair = re.sub('^([^=: ]+)[=: ](.*)$', '\\1 \\2', pairs[i].strip())
pair = pair.strip()
if ' ' in pair:
name, version = pair.split(' ', 2)
result.append({
'name': name,
'version': version
})
else:
result.append({
'name': pair
})
return result
def _format_requirements(self, requirements: List[str]) -> dict:
requires = {}
requirements = self._parse_name_version_pairs(requirements)
for requirement in requirements:
requires[requirement['name']] = requirement['version']
return requires
...@@ -11,6 +11,9 @@ class Command(BaseCommand): ...@@ -11,6 +11,9 @@ class Command(BaseCommand):
def poetry(self) -> Poetry: def poetry(self) -> Poetry:
return self.get_application().poetry return self.get_application().poetry
def reset_poetry(self) -> None:
self.get_application().reset_poetry()
def run(self, i, o) -> int: def run(self, i, o) -> int:
""" """
Initialize command. Initialize command.
......
from typing import List from typing import List
from poetry.packages import Dependency
from poetry.packages import Locker from poetry.packages import Locker
from poetry.packages import Package from poetry.packages import Package
from poetry.puzzle import Solver from poetry.puzzle import Solver
...@@ -32,6 +33,8 @@ class Installer: ...@@ -32,6 +33,8 @@ class Installer:
self._dev_mode = True self._dev_mode = True
self._execute_operations = True self._execute_operations = True
self._whitelist = {}
self._installer = PipInstaller(self._io.venv, self._io) self._installer = PipInstaller(self._io.venv, self._io)
def run(self): def run(self):
...@@ -86,6 +89,11 @@ class Installer: ...@@ -86,6 +89,11 @@ class Installer:
return self return self
def whitelist(self, packages: dict) -> 'Installer':
self._whitelist = packages
return self
def _do_install(self, local_repo): def _do_install(self, local_repo):
locked_repository = Repository() locked_repository = Repository()
# initialize locked repo if we are installing from lock # initialize locked repo if we are installing from lock
...@@ -94,6 +102,27 @@ class Installer: ...@@ -94,6 +102,27 @@ class Installer:
if self._update: if self._update:
self._io.writeln('<info>Updating dependencies</>') self._io.writeln('<info>Updating dependencies</>')
fixed = []
# If the whitelist is enabled, packages not in it are fixed
# to the version specified in the lock
if self._whitelist:
# collect packages to fixate from root requirements
candidates = []
for package in locked_repository.packages:
candidates.append(package)
# fix them to the version in lock if they are not updateable
for candidate in candidates:
to_fix = True
for require in self._whitelist.keys():
if require == candidate.name:
to_fix = False
if to_fix:
fixed.append(
Dependency(candidate.name, candidate.version)
)
solver = Solver(locked_repository, self._io) solver = Solver(locked_repository, self._io)
...@@ -101,7 +130,7 @@ class Installer: ...@@ -101,7 +130,7 @@ class Installer:
if self.is_dev_mode(): if self.is_dev_mode():
request += self._package.dev_requires request += self._package.dev_requires
ops = solver.solve(request, self._repository) ops = solver.solve(request, self._repository, fixed=fixed)
else: else:
self._io.writeln('<info>Installing dependencies from lock file</>') self._io.writeln('<info>Installing dependencies from lock file</>')
# If we are installing from lock # If we are installing from lock
...@@ -112,7 +141,7 @@ class Installer: ...@@ -112,7 +141,7 @@ class Installer:
self._io.new_line() self._io.new_line()
# Execute operations # Execute operations
if not ops and self._execute_operations: if not ops and (self._execute_operations or self._dry_run):
self._io.writeln('Nothing to install or update') self._io.writeln('Nothing to install or update')
if ops and (self._execute_operations or self._dry_run): if ops and (self._execute_operations or self._dry_run):
......
...@@ -27,6 +27,14 @@ class Locker: ...@@ -27,6 +27,14 @@ class Locker:
self._content_hash = self._get_content_hash() self._content_hash = self._get_content_hash()
@property @property
def original(self) -> TomlFile:
return self._original
@property
def lock(self) -> TomlFile:
return self._lock
@property
def lock_data(self): def lock_data(self):
if self._lock_data is None: if self._lock_data is None:
self._lock_data = self._get_lock_data() self._lock_data = self._get_lock_data()
......
...@@ -2,6 +2,7 @@ from pathlib import Path ...@@ -2,6 +2,7 @@ from pathlib import Path
from .packages import Locker from .packages import Locker
from .packages import Package from .packages import Package
from .repositories.pypi_repository import PyPiRepository
from .semver.helpers import normalize_version from .semver.helpers import normalize_version
from .utils.toml_file import TomlFile from .utils.toml_file import TomlFile
...@@ -17,19 +18,24 @@ class Poetry: ...@@ -17,19 +18,24 @@ class Poetry:
self._package = package self._package = package
self._config = config self._config = config
self._locker = locker self._locker = locker
self._repository = PyPiRepository()
@property @property
def package(self): def package(self) -> Package:
return self._package return self._package
@property @property
def config(self): def config(self) -> dict:
return self._config return self._config
@property @property
def locker(self): def locker(self) -> Locker:
return self._locker return self._locker
@property
def repository(self) -> PyPiRepository:
return self._repository
@classmethod @classmethod
def create(cls, cwd) -> 'Poetry': def create(cls, cwd) -> 'Poetry':
poetry_file = Path(cwd) / 'poetry.toml' poetry_file = Path(cwd) / 'poetry.toml'
......
from typing import List from typing import List
from poetry.mixology import Resolver from poetry.mixology import Resolver
from poetry.mixology.dependency_graph import DependencyGraph
from poetry.mixology.exceptions import ResolverError from poetry.mixology.exceptions import ResolverError
from .exceptions import SolverProblemError from .exceptions import SolverProblemError
...@@ -19,11 +20,17 @@ class Solver: ...@@ -19,11 +20,17 @@ class Solver:
self._installed = installed self._installed = installed
self._io = io self._io = io
def solve(self, requested, repository) -> List[Operation]: def solve(self, requested, repository, fixed=None) -> List[Operation]:
resolver = Resolver(Provider(repository), UI(self._io)) resolver = Resolver(Provider(repository), UI(self._io))
base = None
if fixed is not None:
base = DependencyGraph()
for fixed_req in fixed:
base.add_vertex(fixed_req.name, fixed_req, True)
try: try:
graph = resolver.resolve(requested) graph = resolver.resolve(requested, base=base)
except ResolverError as e: except ResolverError as e:
raise SolverProblemError(e) raise SolverProblemError(e)
......
...@@ -9,6 +9,10 @@ class TomlFile: ...@@ -9,6 +9,10 @@ class TomlFile:
def __init__(self, path): def __init__(self, path):
self._path = Path(path) self._path = Path(path)
@property
def path(self):
return self._path
def read(self) -> dict: def read(self) -> dict:
return loads(self._path.read_text()) return loads(self._path.read_text())
......
import re
from typing import Union
from poetry.packages import Package
from poetry.semver.helpers import normalize_version
from poetry.semver.version_parser import VersionParser
class VersionSelector(object):
def __init__(self, repository, parser=VersionParser()):
self._repository = repository
self._parser = parser
def find_best_candidate(self,
package_name: str,
target_package_version: Union[str, None] = None
) -> Union[Package, bool]:
"""
Given a package name and optional version,
returns the latest Package that matches
"""
if target_package_version:
constraint = self._parser.parse_constraints(target_package_version)
else:
constraint = None
candidates = self._repository.find_packages(package_name, constraint)
if not candidates:
return False
# Select highest version if we have many
package = candidates[0]
for candidate in candidates:
# Select highest version of the two
if package.version < candidate.version:
package = candidate
return package
def find_recommended_require_version(self, package):
version = package.version
return self._transform_version(version, package.pretty_version)
def _transform_version(self, version, pretty_version):
# attempt to transform 2.1.1 to 2.1
# this allows you to upgrade through minor versions
try:
parts = normalize_version(version).split('.')
except ValueError:
return pretty_version
# check to see if we have a semver-looking version
if len(parts) == 4 and re.match('^0\D?', parts[3]):
# remove the last parts (the patch version number and any extra)
if parts[0] == '0':
del parts[3]
else:
del parts[3]
del parts[2]
version = '.'.join(parts)
else:
return pretty_version
return f'^{version}'
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