Commit c3731a57 by Sébastien Eustace

Add support for private repositories

parent 31b9dbf9
...@@ -27,6 +27,7 @@ cleo = "^0.6" ...@@ -27,6 +27,7 @@ cleo = "^0.6"
requests = "^2.18" requests = "^2.18"
toml = "^0.9" toml = "^0.9"
cachy = "^0.1.0" cachy = "^0.1.0"
pip-tools = "^1.11"
[dev-dependencies] [dev-dependencies]
pytest = "~3.4" pytest = "~3.4"
......
...@@ -92,7 +92,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -92,7 +92,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
self.output, self.output,
self.poetry.package, self.poetry.package,
self.poetry.locker, self.poetry.locker,
self.poetry.repository self.poetry.pool
) )
installer.update(True) installer.update(True)
...@@ -135,7 +135,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -135,7 +135,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
name, name,
required_version=None required_version=None
) -> Tuple[str, str]: ) -> Tuple[str, str]:
selector = VersionSelector(self.poetry.repository) selector = VersionSelector(self.poetry.pool)
package = selector.find_best_candidate(name, required_version) package = selector.find_best_candidate(name, required_version)
if not package: if not package:
......
...@@ -27,7 +27,7 @@ exist it will look for <comment>poetry.toml</> and do the same. ...@@ -27,7 +27,7 @@ exist it will look for <comment>poetry.toml</> and do the same.
self.output, self.output,
self.poetry.package, self.poetry.package,
self.poetry.locker, self.poetry.locker,
PyPiRepository() self.poetry.pool
) )
installer.dev_mode(not self.option('no-dev')) installer.dev_mode(not self.option('no-dev'))
......
...@@ -23,7 +23,7 @@ the current directory, processes it, and locks the depdencies in the <comment>po ...@@ -23,7 +23,7 @@ the current directory, processes it, and locks the depdencies in the <comment>po
self.output, self.output,
self.poetry.package, self.poetry.package,
self.poetry.locker, self.poetry.locker,
PyPiRepository() self.poetry.pool
) )
installer.update(True) installer.update(True)
......
...@@ -83,7 +83,7 @@ list of installed packages ...@@ -83,7 +83,7 @@ list of installed packages
self.output, self.output,
self.poetry.package, self.poetry.package,
self.poetry.locker, self.poetry.locker,
self.poetry.repository self.poetry.pool
) )
installer.dry_run(self.option('dry-run')) installer.dry_run(self.option('dry-run'))
......
...@@ -22,7 +22,7 @@ class UpdateCommand(Command): ...@@ -22,7 +22,7 @@ class UpdateCommand(Command):
self.output, self.output,
self.poetry.package, self.poetry.package,
self.poetry.locker, self.poetry.locker,
self.poetry.repository self.poetry.pool
) )
if packages: if packages:
......
...@@ -8,6 +8,7 @@ from poetry.puzzle.operations import Install ...@@ -8,6 +8,7 @@ from poetry.puzzle.operations import Install
from poetry.puzzle.operations import Uninstall from poetry.puzzle.operations import Uninstall
from poetry.puzzle.operations import Update from poetry.puzzle.operations import Update
from poetry.puzzle.operations.operation import Operation from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
...@@ -21,11 +22,11 @@ class Installer: ...@@ -21,11 +22,11 @@ class Installer:
io, io,
package: Package, package: Package,
locker: Locker, locker: Locker,
repository: Repository): pool: Pool):
self._io = io self._io = io
self._package = package self._package = package
self._locker = locker self._locker = locker
self._repository = repository self._pool = pool
self._dry_run = False self._dry_run = False
self._update = False self._update = False
...@@ -130,7 +131,7 @@ class Installer: ...@@ -130,7 +131,7 @@ class Installer:
request = self._package.requires request = self._package.requires
request += self._package.dev_requires request += self._package.dev_requires
ops = solver.solve(request, self._repository, fixed=fixed) ops = solver.solve(request, self._pool, 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
......
...@@ -10,9 +10,17 @@ class PipInstaller(BaseInstaller): ...@@ -10,9 +10,17 @@ class PipInstaller(BaseInstaller):
self._io = io self._io = io
def install(self, package): def install(self, package):
self.run('install', self.requirement(package), '--no-deps') args = ['install', self.requirement(package), '--no-deps']
if package.source_type == 'legacy' and package.source_url:
args += ['--index-url', package.source_url]
self.run(*args)
def update(self, source, target): def update(self, source, target):
args = ['install', self.requirement(target), '--no-deps', '-U']
if target.source_type == 'legacy' and target.source_url:
args += ['--index-url', target.source_url]
self.run('install', self.requirement(target), '--no-deps', '-U') self.run('install', self.requirement(target), '--no-deps', '-U')
def remove(self, package): def remove(self, package):
......
...@@ -18,6 +18,7 @@ class Locker: ...@@ -18,6 +18,7 @@ class Locker:
'python_versions', 'python_versions',
'dependencies', 'dependencies',
'dev-dependencies', 'dev-dependencies',
'source',
] ]
def __init__(self, lock: Path, original: Path): def __init__(self, lock: Path, original: Path):
......
...@@ -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 import Pool
from .repositories.pypi_repository import PyPiRepository 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
...@@ -18,7 +19,14 @@ class Poetry: ...@@ -18,7 +19,14 @@ class Poetry:
self._package = package self._package = package
self._config = config self._config = config
self._locker = locker self._locker = locker
self._repository = PyPiRepository()
# Configure sources
self._pool = Pool()
for source in self._config.get('source', []):
self._pool.configure(source)
# Always put PyPI last to prefere private repositories
self._pool.add_repository(PyPiRepository())
@property @property
def package(self) -> Package: def package(self) -> Package:
...@@ -33,12 +41,8 @@ class Poetry: ...@@ -33,12 +41,8 @@ class Poetry:
return self._locker return self._locker
@property @property
def repository(self) -> PyPiRepository: def pool(self) -> Pool:
return self._repository return self._pool
@property
def installer(self):
return self._instal
@classmethod @classmethod
def create(cls, cwd) -> 'Poetry': def create(cls, cwd) -> 'Poetry':
......
...@@ -17,7 +17,7 @@ from poetry.packages import Dependency ...@@ -17,7 +17,7 @@ from poetry.packages import Dependency
from poetry.packages import Package from poetry.packages import Package
from poetry.packages import VCSDependency from poetry.packages import VCSDependency
from poetry.repositories.repository import Repository from poetry.repositories import Pool
from poetry.semver import less_than from poetry.semver import less_than
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
...@@ -31,12 +31,12 @@ class Provider(SpecificationProvider): ...@@ -31,12 +31,12 @@ class Provider(SpecificationProvider):
UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'} UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'}
def __init__(self, repository: Repository): def __init__(self, pool: Pool):
self._repository = repository self._pool = pool
@property @property
def repository(self) -> Repository: def pool(self) -> Pool:
return self._repository return self._pool
@property @property
def name_for_explicit_dependency_source(self) -> str: def name_for_explicit_dependency_source(self) -> str:
...@@ -62,7 +62,7 @@ class Provider(SpecificationProvider): ...@@ -62,7 +62,7 @@ class Provider(SpecificationProvider):
if dependency.is_vcs(): if dependency.is_vcs():
return self.search_for_vcs(dependency) return self.search_for_vcs(dependency)
packages = self._repository.find_packages( packages = self._pool.find_packages(
dependency.name, dependency.name,
dependency.constraint dependency.constraint
) )
...@@ -150,7 +150,7 @@ class Provider(SpecificationProvider): ...@@ -150,7 +150,7 @@ class Provider(SpecificationProvider):
# Information should already be set # Information should already be set
pass pass
else: else:
package = self._repository.package(package.name, package.version) package = self._pool.package(package.name, package.version)
return [ return [
r for r in package.requires r for r in package.requires
......
...@@ -20,8 +20,8 @@ class Solver: ...@@ -20,8 +20,8 @@ class Solver:
self._locked = locked self._locked = locked
self._io = io self._io = io
def solve(self, requested, repository, fixed=None) -> List[Operation]: def solve(self, requested, pool, fixed=None) -> List[Operation]:
resolver = Resolver(Provider(repository), UI(self._io)) resolver = Resolver(Provider(pool), UI(self._io))
base = None base = None
if fixed is not None: if fixed is not None:
......
from .pool import Pool
from .repository import Repository from .repository import Repository
import re
from pathlib import Path
from piptools.cache import DependencyCache
from piptools.repositories import PyPIRepository
from piptools.resolver import Resolver
from piptools.scripts.compile import get_pip_command
from pip.req import InstallRequirement
from pip.exceptions import InstallationError
from cachy import CacheManager
import poetry.packages
from poetry.locations import CACHE_DIR
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
from .repository import Repository
class LegacyRepository(Repository):
def __init__(self, name, url):
if name == 'pypi':
raise ValueError('The name [pypi] is reserved for repositories')
self._name = name
self._url = url
command = get_pip_command()
opts, _ = command.parse_args([])
self._session = command._build_session(opts)
self._repository = PyPIRepository(opts, self._session)
self._cache_dir = Path(CACHE_DIR) / 'cache' / 'repositories' / name
self._cache = CacheManager({
'default': 'releases',
'serializer': 'json',
'stores': {
'releases': {
'driver': 'file',
'path': Path(CACHE_DIR) / 'cache' / 'repositories' / name
},
'packages': {
'driver': 'dict'
},
'matches': {
'driver': 'dict'
}
}
})
super().__init__()
def find_packages(self, name, constraint=None):
packages = []
if constraint is not None and not isinstance(constraint,
BaseConstraint):
version_parser = VersionParser()
constraint = version_parser.parse_constraints(constraint)
key = name
if constraint:
key = f'{key}:{str(constraint)}'
if self._cache.store('matches').has(key):
versions = self._cache.store('matches').get(key)
else:
candidates = [str(c.version) for c in self._repository.find_all_candidates(name)]
versions = []
for version in candidates:
if version in versions:
continue
if (
not constraint
or (constraint and constraint.matches(Constraint('=', version)))
):
versions.append(version)
self._cache.store('matches').put(key, versions, 5)
for version in versions:
packages.append(self.package(name, version))
return packages
def package(self, name, version) -> 'poetry.packages.Package':
"""
Retrieve the release information.
This is a heavy task which takes time.
We have to download a package to get the dependencies.
We also need to download every file matching this release
to get the various hashes.
Note that, this will be cached so the subsequent operations
should be much faster.
"""
try:
index = self._packages.index(
poetry.packages.Package(name, version, version)
)
return self._packages[index]
except ValueError:
release_info = self.get_release_info(name, version)
package = poetry.packages.Package(name, version, version)
for dependency in release_info['requires_dist']:
m = re.match(
'^(?P<name>[^ ;]+)'
'(?: \((?P<version>.+)\))?'
'(?:;(?P<extra>(.+)))?$',
dependency
)
package.requires.append(
poetry.packages.Dependency(
m.group('name'),
m.group('version') or '*',
optional=m.group('extra') is not None
)
)
package.source_type = 'legacy'
package.source_url = self._url
# Adding hashes information
package.hashes = release_info['digests']
self._packages.append(package)
return package
def get_release_info(self, name: str, version: str) -> dict:
"""
Return the release information given a package name and a version.
The information is returned from the cache if it exists
or retrieved from the remote server.
"""
return self._cache.store('releases').remember_forever(
f'{name}:{version}',
lambda: self._get_release_info(name, version)
)
def _get_release_info(self, name: str, version: str) -> dict:
ireq = InstallRequirement.from_line(f'{name}=={version}')
resolver = Resolver(
[ireq], self._repository,
cache=DependencyCache(self._cache_dir.as_posix())
)
try:
requirements = list(resolver._iter_dependencies(ireq))
except InstallationError as e:
# setup.py egg-info error most likely
# So we assume no dependencies
requirements = []
requires = []
for dep in requirements:
constraint = str(dep.req.specifier)
require = f'{dep.name}'
if constraint:
require += f' ({constraint})'
requires.append(require)
hashes = resolver.resolve_hashes([ireq])[ireq]
hashes = [h.split(':')[1] for h in hashes]
data = {
'name': name,
'version': version,
'requires_dist': requires,
'digests': hashes
}
resolver.repository.freshen_build_caches()
return data
from typing import List
from typing import Union
import poetry.packages
from .base_repository import BaseRepository
from .legacy_repository import LegacyRepository
from .repository import Repository
class Pool(BaseRepository):
def __init__(self, repositories: Union[list, None] = None):
if repositories is None:
repositories = []
self._repositories = []
for repository in repositories:
self.add_repository(repository)
super().__init__()
@property
def repositories(self) -> List[Repository]:
return self._repositories
def add_repository(self, repository: Repository) -> 'Pool':
"""
Adds a repository to the pool.
"""
self._repositories.append(repository)
return self
def configure(self, source: dict) -> 'Pool':
"""
Configures a repository based on a source
specification and add it to the pool.
"""
if 'url' in source:
# PyPI-like repository
if 'name' not in source:
raise RuntimeError('Missing [name] in source.')
repository = LegacyRepository(source['name'], source['url'])
else:
raise RuntimeError('Unsupported source specified')
return self.add_repository(repository)
def has_package(self, package):
raise NotImplementedError()
def package(self, name, version) -> Union['poetry.packages.Package', None]:
package = poetry.packages.Package(name, version, version)
if package in self._packages:
return self._packages[self._packages.index(package)]
for repository in self._repositories:
package = repository.package(name, version)
if package:
self._packages.append(package)
return package
return None
def find_packages(self,
name,
constraint=None) -> List['poetry.packages.Package']:
for repository in self._repositories:
packages = repository.find_packages(name, constraint)
if packages:
return packages
return []
def search(self, query, mode=BaseRepository.SEARCH_FULLTEXT):
raise NotImplementedError()
...@@ -27,7 +27,7 @@ class PyPiRepository(Repository): ...@@ -27,7 +27,7 @@ class PyPiRepository(Repository):
'stores': { 'stores': {
'releases': { 'releases': {
'driver': 'file', 'driver': 'file',
'path': Path(CACHE_DIR) / 'cache' 'path': Path(CACHE_DIR) / 'cache' / 'repositories' / 'pypi'
}, },
'packages': { 'packages': {
'driver': 'dict' 'driver': 'dict'
...@@ -35,7 +35,7 @@ class PyPiRepository(Repository): ...@@ -35,7 +35,7 @@ class PyPiRepository(Repository):
} }
}) })
super(PyPiRepository, self).__init__() super().__init__()
def find_packages(self, def find_packages(self,
name: str, name: str,
......
...@@ -2,6 +2,7 @@ import re ...@@ -2,6 +2,7 @@ import re
from typing import Union from typing import Union
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.comparison import less_than
from poetry.semver.helpers import normalize_version from poetry.semver.helpers import normalize_version
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
...@@ -34,7 +35,7 @@ class VersionSelector(object): ...@@ -34,7 +35,7 @@ class VersionSelector(object):
package = candidates[0] package = candidates[0]
for candidate in candidates: for candidate in candidates:
# Select highest version of the two # Select highest version of the two
if package.version < candidate.version: if less_than(package.version, candidate.version):
package = candidate package = candidate
return package return package
......
...@@ -7,6 +7,7 @@ from poetry.installation import Installer as BaseInstaller ...@@ -7,6 +7,7 @@ from poetry.installation import Installer as BaseInstaller
from poetry.installation.noop_installer import NoopInstaller from poetry.installation.noop_installer import NoopInstaller
from poetry.io import NullIO from poetry.io import NullIO
from poetry.packages import Locker as BaseLocker from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from tests.helpers import get_package from tests.helpers import get_package
...@@ -58,6 +59,14 @@ def repo(): ...@@ -58,6 +59,14 @@ def repo():
@pytest.fixture() @pytest.fixture()
def pool(repo):
pool = Pool()
pool.add_repository(repo)
return pool
@pytest.fixture()
def locker(): def locker():
return Locker() return Locker()
...@@ -69,8 +78,8 @@ def fixture(name): ...@@ -69,8 +78,8 @@ def fixture(name):
@pytest.fixture() @pytest.fixture()
def installer(package, repo, locker): def installer(package, pool, locker):
return Installer(NullIO(), package, locker, repo) return Installer(NullIO(), package, locker, pool)
def test_run_no_dependencies(installer, locker): def test_run_no_dependencies(installer, locker):
......
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