Commit 54c82eeb by Sébastien Eustace

Add the add command

parent 2c53da3f
......@@ -6,6 +6,7 @@ from poetry.poetry import Poetry
from poetry.utils.venv import Venv
from .commands import AboutCommand
from .commands import AddCommand
from .commands import InstallCommand
from .commands import LockCommand
from .commands import UpdateCommand
......@@ -28,6 +29,9 @@ class Application(BaseApplication):
return self._poetry
def reset_poetry(self) -> None:
self._poetry = None
@property
def venv(self) -> Venv:
return self._venv
......@@ -37,6 +41,7 @@ class Application(BaseApplication):
return commands + [
AboutCommand(),
AddCommand(),
InstallCommand(),
LockCommand(),
UpdateCommand(),
......
from .about import AboutCommand
from .add import AddCommand
from .install import InstallCommand
from .lock import LockCommand
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):
def poetry(self) -> Poetry:
return self.get_application().poetry
def reset_poetry(self) -> None:
self.get_application().reset_poetry()
def run(self, i, o) -> int:
"""
Initialize command.
......
from typing import List
from poetry.packages import Dependency
from poetry.packages import Locker
from poetry.packages import Package
from poetry.puzzle import Solver
......@@ -32,6 +33,8 @@ class Installer:
self._dev_mode = True
self._execute_operations = True
self._whitelist = {}
self._installer = PipInstaller(self._io.venv, self._io)
def run(self):
......@@ -86,6 +89,11 @@ class Installer:
return self
def whitelist(self, packages: dict) -> 'Installer':
self._whitelist = packages
return self
def _do_install(self, local_repo):
locked_repository = Repository()
# initialize locked repo if we are installing from lock
......@@ -94,6 +102,27 @@ class Installer:
if self._update:
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)
......@@ -101,7 +130,7 @@ class Installer:
if self.is_dev_mode():
request += self._package.dev_requires
ops = solver.solve(request, self._repository)
ops = solver.solve(request, self._repository, fixed=fixed)
else:
self._io.writeln('<info>Installing dependencies from lock file</>')
# If we are installing from lock
......@@ -112,7 +141,7 @@ class Installer:
self._io.new_line()
# 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')
if ops and (self._execute_operations or self._dry_run):
......
......@@ -27,6 +27,14 @@ class Locker:
self._content_hash = self._get_content_hash()
@property
def original(self) -> TomlFile:
return self._original
@property
def lock(self) -> TomlFile:
return self._lock
@property
def lock_data(self):
if self._lock_data is None:
self._lock_data = self._get_lock_data()
......
......@@ -2,6 +2,7 @@ from pathlib import Path
from .packages import Locker
from .packages import Package
from .repositories.pypi_repository import PyPiRepository
from .semver.helpers import normalize_version
from .utils.toml_file import TomlFile
......@@ -17,19 +18,24 @@ class Poetry:
self._package = package
self._config = config
self._locker = locker
self._repository = PyPiRepository()
@property
def package(self):
def package(self) -> Package:
return self._package
@property
def config(self):
def config(self) -> dict:
return self._config
@property
def locker(self):
def locker(self) -> Locker:
return self._locker
@property
def repository(self) -> PyPiRepository:
return self._repository
@classmethod
def create(cls, cwd) -> 'Poetry':
poetry_file = Path(cwd) / 'poetry.toml'
......
from typing import List
from poetry.mixology import Resolver
from poetry.mixology.dependency_graph import DependencyGraph
from poetry.mixology.exceptions import ResolverError
from .exceptions import SolverProblemError
......@@ -19,11 +20,17 @@ class Solver:
self._installed = installed
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))
base = None
if fixed is not None:
base = DependencyGraph()
for fixed_req in fixed:
base.add_vertex(fixed_req.name, fixed_req, True)
try:
graph = resolver.resolve(requested)
graph = resolver.resolve(requested, base=base)
except ResolverError as e:
raise SolverProblemError(e)
......
......@@ -9,6 +9,10 @@ class TomlFile:
def __init__(self, path):
self._path = Path(path)
@property
def path(self):
return self._path
def read(self) -> dict:
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