Commit c0fcaf71 by Sébastien Eustace

Add basic support for virtualenvs autogeneration

parent 241033c1
# Change Log
## [Unreleased]
### Added
- Added support for virtualenv autogeneration (Python 3.6+ only).
## [0.5.0] - 2018-03-14
......
from pathlib import Path
from typing import Any
from .locations import CONFIG_DIR
from .utils.toml_file import TomlFile
class Config:
def __init__(self, file: TomlFile):
self._file = file
self._raw_content = file.read(raw=True)
self._content = file.read()
@property
def name(self):
return str(self._file.path)
@property
def file(self):
return self._file
@property
def raw_content(self):
return self._raw_content
@property
def content(self):
return self._content
def setting(self, setting_name: str) -> Any:
"""
Retrieve a setting value.
"""
keys = setting_name.split('.')
config = self._raw_content
for key in keys:
if key not in config:
return None
config = config[key]
return config
def add_property(self, key, value):
keys = key.split('.')
config = self._content
for i, key in enumerate(keys):
if key not in config and i < len(keys) - 1:
config[key] = {}
if i == len(keys) - 1:
config[key] = value
break
config = config[key]
self.dump()
def remove_property(self, key):
keys = key.split('.')
config = self._content
for i, key in enumerate(keys):
if key not in config:
return
if i == len(keys) - 1:
del config[key]
break
config = config[key]
self.dump()
def dump(self):
self._file.write(self._content)
@classmethod
def create(cls, file, base_dir=None) -> 'Config':
if base_dir is None:
base_dir = CONFIG_DIR
file = TomlFile(Path(base_dir) / file)
return cls(file)
......@@ -24,7 +24,6 @@ class Application(BaseApplication):
super().__init__('Poetry', Poetry.VERSION)
self._poetry = None
self._venv = Venv.create()
@property
def poetry(self) -> Poetry:
......@@ -38,10 +37,6 @@ class Application(BaseApplication):
def reset_poetry(self) -> None:
self._poetry = None
@property
def venv(self) -> Venv:
return self._venv
def get_default_commands(self) -> list:
commands = super(Application, self).get_default_commands()
......@@ -58,9 +53,3 @@ class Application(BaseApplication):
ShowCommand(),
UpdateCommand(),
]
def do_run(self, i, o) -> int:
if self._venv.is_venv() and o.is_verbose():
o.writeln(f'Using virtualenv: <comment>{self._venv.venv}</>')
return super().do_run(i, o)
......@@ -7,10 +7,10 @@ from poetry.installation import Installer
from poetry.semver.version_parser import VersionParser
from poetry.version.version_selector import VersionSelector
from .command import Command
from .venv_command import VenvCommand
class AddCommand(Command):
class AddCommand(VenvCommand):
"""
Add a new depdency to <comment>poetry.toml</>.
......@@ -66,6 +66,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
installer = Installer(
self.output,
self.venv,
self.poetry.package,
self.poetry.locker,
self.poetry.pool
......
from .command import Command
from .venv_command import VenvCommand
from poetry.masonry import Builder
class BuildCommand(Command):
class BuildCommand(VenvCommand):
"""
Builds a package, as a tarball and a wheel by default.
......
......@@ -35,6 +35,6 @@ class Command(BaseCommand):
Initialize command.
"""
self.input = i
self.output = PoetryStyle(i, o, self.get_application().venv)
self.output = PoetryStyle(i, o)
return super(BaseCommand, self).run(i, o)
import json
import re
from pathlib import Path
from poetry.locations import CONFIG_DIR
from poetry.toml import loads
from poetry.config import Config
from .command import Command
TEMPLATE = """[repositories]
TEMPLATE = """[settings]
[repositories]
"""
AUTH_TEMPLATE = """[http-basic]
......@@ -41,36 +40,24 @@ To remove a repository (repo is a short alias for repositories):
def __init__(self):
super().__init__()
self._config_file = None
self._config = {}
self._auth_config_file = None
self._auth_config = {}
self._config = Config.create('config.toml')
self._auth_config = Config.create('auth.toml')
def initialize(self, i, o):
super().initialize(i, o)
# Create config file if it does not exist
self._config_file = Path(CONFIG_DIR) / 'config.toml'
self._auth_config_file = Path(CONFIG_DIR) / 'auth.toml'
if not self._config_file.exists():
self._config_file.parent.mkdir(parents=True, exist_ok=True)
self._config_file.write_text(TEMPLATE)
if not self._auth_config_file.exists():
if not self._config.file.exists():
self._config.file.parent.mkdir(parents=True, exist_ok=True)
self._config.file.write_text(TEMPLATE)
self._auth_config_file.parent.mkdir(parents=True, exist_ok=True)
self._auth_config_file.write_text(AUTH_TEMPLATE)
with self._config_file.open() as f:
self._config = loads(f.read())
with self._auth_config_file.open() as f:
self._auth_config = loads(f.read())
if not self._auth_config.file.exists():
self._auth_config.file.parent.mkdir(parents=True, exist_ok=True)
self._auth_config.file.write_text(AUTH_TEMPLATE)
def handle(self):
if self.option('list'):
self._list_configuration(self._config)
self._list_configuration(self._config.raw_content)
return 0
......@@ -87,15 +74,16 @@ To remove a repository (repo is a short alias for repositories):
if m:
if not m.group(1):
value = {}
if 'repositories' in self._config:
value = self._config['repositories']
if self._config.setting('repositories') is not None:
value = self._config.setting('repositories')
else:
if m.group(1) not in self._config['repositories']:
repo = self._config.setting(f'repositories.{m.group(1)}')
if repo is None:
raise ValueError(
f'There is not {m.group(1)} repository defined'
f'There is no {m.group(1)} repository defined'
)
value = self._config['repositories'][m.group(1)]
value = repo
self.line(str(value))
......@@ -103,6 +91,23 @@ To remove a repository (repo is a short alias for repositories):
values = self.argument('value')
boolean_validator = lambda val: val in {'true', 'false', '1', '0'}
boolean_normalizer = lambda val: True if val in ['true', '1'] else False
unique_config_values = {
'settings.virtualenvs.create': (boolean_validator, boolean_normalizer)
}
if setting_key in unique_config_values:
if self.option('unset'):
return self._remove_single_value(setting_key)
return self._handle_single_value(
setting_key,
unique_config_values[setting_key],
values
)
# handle repositories
m = re.match('^repos?(?:itories)?(?:\.(.+))?', self.argument('key'))
if m:
......@@ -110,26 +115,18 @@ To remove a repository (repo is a short alias for repositories):
raise ValueError('You cannot remove the [repositories] section')
if self.option('unset'):
if m.group(1) not in self._config['repositories']:
raise ValueError(f'There is not {m.group(1)} repository defined')
del self._config[m.group(1)]
repo = self._config.setting(f'repositories.{m.group(1)}')
if repo is None:
raise ValueError(f'There is no {m.group(1)} repository defined')
self._config_file.write_text(self._config.dumps())
self._config.remove_property(f'repositories.{m.group(1)}')
return 0
if len(values) == 1:
url = values[0]
if m.group(1) in self._config['repositories']:
self._config['repositories'][m.group(1)]['url'] = url
else:
self._config['repositories'][m.group(1)] = {
'url': url
}
self._config_file.write_text(self._config.dumps())
self._config.add_property(f'repositories.{m.group(1)}.url', url)
return 0
......@@ -142,14 +139,12 @@ To remove a repository (repo is a short alias for repositories):
m = re.match('^(http-basic)\.(.+)', self.argument('key'))
if m:
if self.option('unset'):
if m.group(2) not in self._auth_config[m.group(1)]:
if not self._auth_config.setting(f'{m.group(1)}.{m.group(2)}'):
raise ValueError(
f'There is no {m.group(2)} {m.group(1)} defined'
)
del self._auth_config[m.group(1)][m.group(2)]
self._auth_config_file.write_text(self._auth_config.dumps())
self._auth_config.remove_property(f'{m.group(1)}.{m.group(2)}')
return 0
......@@ -165,17 +160,38 @@ To remove a repository (repo is a short alias for repositories):
username = values[0]
password = values[1]
self._auth_config[m.group(1)][m.group(2)] = {
'username': username,
'password': password
}
self._auth_config_file.write_text(self._auth_config.dumps())
self._auth_config.add_property(
f'{m.group(1)}.{m.group(2)}', {
'username': username,
'password': password
}
)
return 0
raise ValueError(f'Setting {self.argument("key")} does not exist')
def _handle_single_value(self, key, callbacks, values):
validator, normalizer = callbacks
if len(values) > 1:
raise RuntimeError('You can only pass one value.')
value = values[0]
if not validator(value):
raise RuntimeError(
f'"{value}" is an invalid value for {key}'
)
self._config.add_property(key, normalizer(value))
return 0
def _remove_single_value(self, key):
self._config.remove_property(key)
return 0
def _list_configuration(self, contents, k=None):
orig_k = k
......
from poetry.installation import Installer
from poetry.repositories.pypi_repository import PyPiRepository
from .command import Command
from .venv_command import VenvCommand
class InstallCommand(Command):
class InstallCommand(VenvCommand):
"""
Installs the project dependencies.
......@@ -27,6 +26,7 @@ exist it will look for <comment>poetry.toml</> and do the same.
def handle(self):
installer = Installer(
self.output,
self.venv,
self.poetry.package,
self.poetry.locker,
self.poetry.pool
......
from poetry.installation import Installer
from .command import Command
from .venv_command import VenvCommand
class LockCommand(Command):
class LockCommand(VenvCommand):
"""
Locks the project dependencies.
......@@ -19,6 +19,7 @@ the current directory, processes it, and locks the depdencies in the <comment>po
def handle(self):
installer = Installer(
self.output,
self.venv,
self.poetry.package,
self.poetry.locker,
self.poetry.pool
......
from poetry.installation import Installer
from .command import Command
from .venv_command import VenvCommand
class RemoveCommand(Command):
class RemoveCommand(VenvCommand):
"""
Removes a package from the project dependencies.
......@@ -54,6 +54,7 @@ list of installed packages
installer = Installer(
self.output,
self.venv,
self.poetry.package,
self.poetry.locker,
self.poetry.pool
......
from poetry.semver import statisfies
from poetry.version.version_selector import VersionSelector
from .command import Command
from .venv_command import VenvCommand
class ShowCommand(Command):
class ShowCommand(VenvCommand):
"""
Shows information about packages.
......
from poetry.installation import Installer
from poetry.repositories.pypi_repository import PyPiRepository
from .command import Command
from .venv_command import VenvCommand
class UpdateCommand(Command):
class UpdateCommand(VenvCommand):
"""
Update dependencies as according to the <comment>poetry.toml</> file.
......@@ -20,6 +19,7 @@ class UpdateCommand(Command):
installer = Installer(
self.output,
self.venv,
self.poetry.package,
self.poetry.locker,
self.poetry.pool
......
from poetry.utils.venv import Venv
from .command import Command
class VenvCommand(Command):
def __init__(self, name=None):
self._venv = None
super().__init__(name)
def initialize(self, i, o):
super().initialize(i, o)
self._venv = Venv.create(o, self.poetry.package.name)
if self._venv.is_venv() and o.is_verbose():
o.writeln(f'Using virtualenv: <comment>{self._venv.venv}</>')
@property
def venv(self):
return self._venv
......@@ -4,18 +4,12 @@ from cleo.styles import OutputStyle
class PoetryStyle(CleoStyle):
def __init__(self, i, o, venv):
self._venv = venv
def __init__(self, i, o):
super().__init__(i, o)
self.output.get_formatter().add_style('warning', 'black', 'yellow')
self.output.get_formatter().add_style('question', 'blue')
@property
def venv(self):
return self._venv
def writeln(self, messages,
type=OutputStyle.OUTPUT_NORMAL,
verbosity=OutputStyle.VERBOSITY_NORMAL):
......
......@@ -25,10 +25,12 @@ class Installer:
def __init__(self,
io,
venv,
package: Package,
locker: Locker,
pool: Pool):
self._io = io
self._venv = venv
self._package = package
self._locker = locker
self._pool = pool
......@@ -345,7 +347,7 @@ class Installer:
def _get_operations_from_lock(self,
locked_repository: Repository
) -> List[Operation]:
installed_repo = InstalledRepository.load(self._io.venv)
installed_repo = InstalledRepository.load(self._venv)
ops = []
extra_packages = [
......@@ -390,7 +392,7 @@ class Installer:
continue
parser = VersionParser()
python = '.'.join([str(i) for i in self._io.venv.version_info[:3]])
python = '.'.join([str(i) for i in self._venv.version_info[:3]])
if 'python' in package.requirements:
python_constraint = parser.parse_constraints(
package.requirements['python']
......@@ -462,4 +464,4 @@ class Installer:
return _extra_packages(extra_packages)
def _get_installer(self) -> BaseInstaller:
return PipInstaller(self._io.venv, self._io)
return PipInstaller(self._venv, self._io)
from poetry.console.styles.poetry import PoetryStyle
from poetry.utils.venv import Venv
class NullVenv(Venv):
def __init__(self, execute=False):
super().__init__()
self.executed = []
self._execute = execute
from cleo.inputs import ListInput
from cleo.outputs import NullOutput
def run(self, bin: str, *args):
self.executed.append([bin] + list(args))
if self._execute:
return super().run(bin, *args)
def _bin(self, bin):
return bin
from poetry.console.styles.poetry import PoetryStyle
class NullIO(PoetryStyle):
def __init__(self, execute=False):
self._venv = NullVenv(execute=execute)
@property
def venv(self) -> NullVenv:
return self._venv
def __init__(self):
super().__init__(ListInput([]), NullOutput())
def is_quiet(self) -> bool:
return False
......
from poetry.semver.constraints import MultiConstraint
from .builders import CompleteBuilder
from .builders import SdistBuilder
from .builders import WheelBuilder
......@@ -13,14 +11,15 @@ class Builder:
'all': CompleteBuilder
}
def __init__(self, poetry, io):
def __init__(self, poetry, venv, io):
self._poetry = poetry
self._venv = venv
self._io = io
def build(self, fmt: str):
if fmt not in self._FORMATS:
raise ValueError(f'Invalid format: {fmt}')
builder = self._FORMATS[fmt](self._poetry, self._io)
builder = self._FORMATS[fmt](self._poetry, self._venv, self._io)
return builder.build()
......@@ -24,8 +24,9 @@ class Builder:
'3.4', '3.5', '3.6', '3.7'
}
def __init__(self, poetry, io):
def __init__(self, poetry, venv, io):
self._poetry = poetry
self._venv = venv
self._io = io
self._package = poetry.package
self._path = poetry.file.parent
......
......@@ -17,7 +17,7 @@ class CompleteBuilder(Builder):
def build(self):
# We start by building the tarball
# We will use it to build the wheel
sdist_builder = SdistBuilder(self._poetry, self._io)
sdist_builder = SdistBuilder(self._poetry, self._venv, self._io)
sdist_file = sdist_builder.build()
sdist_info = SimpleNamespace(builder=sdist_builder, file=sdist_file)
......@@ -26,7 +26,7 @@ class CompleteBuilder(Builder):
dist_dir = self._path / 'dist'
with self.unpacked_tarball(sdist_file) as tmpdir:
wheel_info = WheelBuilder.make_in(
poetry.Poetry.create(tmpdir), self._io, dist_dir,
poetry.Poetry.create(tmpdir), self._venv, self._io, dist_dir,
original=self._poetry
)
......
......@@ -51,9 +51,6 @@ Author-email: {author_email}
class SdistBuilder(Builder):
def __init__(self, poetry, io):
super().__init__(poetry, io)
def build(self, target_dir: Path = None) -> Path:
self._io.writeln(' - Building <info>sdist</info>')
if target_dir is None:
......
......@@ -35,8 +35,8 @@ Root-Is-Purelib: true
class WheelBuilder(Builder):
def __init__(self, poetry, io, target_fp, original=None):
super().__init__(poetry, io)
def __init__(self, poetry, venv, io, target_fp, original=None):
super().__init__(poetry, venv, io)
self._records = []
self._original_path = self._path
......@@ -48,14 +48,14 @@ class WheelBuilder(Builder):
compression=zipfile.ZIP_DEFLATED)
@classmethod
def make_in(cls, poetry, io, directory, original=None) -> SimpleNamespace:
def make_in(cls, poetry, venv, io, directory, original=None) -> SimpleNamespace:
# We don't know the final filename until metadata is loaded, so write to
# a temporary_file, and rename it afterwards.
(fd, temp_path) = tempfile.mkstemp(suffix='.whl',
dir=str(directory))
try:
with open(fd, 'w+b') as fp:
wb = WheelBuilder(poetry, io, fp, original=original)
wb = WheelBuilder(poetry, venv, io, fp, original=original)
wb.build()
wheel_path = directory / wb.wheel_filename
......@@ -67,7 +67,7 @@ class WheelBuilder(Builder):
return SimpleNamespace(builder=wb, file=wheel_path)
@classmethod
def make(cls, poetry, io) -> SimpleNamespace:
def make(cls, poetry, venv, io) -> SimpleNamespace:
"""Build a wheel in the dist/ directory, and optionally upload it.
"""
dist_dir = poetry.file.parent / 'dist'
......@@ -76,7 +76,7 @@ class WheelBuilder(Builder):
except FileExistsError:
pass
return cls.make_in(poetry, io, dist_dir)
return cls.make_in(poetry, venv, io, dist_dir)
def build(self) -> None:
self._io.writeln(' - Building <info>wheel</info>')
......@@ -99,7 +99,7 @@ class WheelBuilder(Builder):
current_path = os.getcwd()
try:
os.chdir(str(self._path))
self._io.venv.run(
self._venv.run(
'python',
str(setup),
'build',
......
......@@ -3,8 +3,14 @@ import os
import subprocess
import sys
from pathlib import Path
from subprocess import CalledProcessError
from venv import EnvBuilder
from poetry.config import Config
from poetry.locations import CACHE_DIR
class VenvError(Exception):
......@@ -27,10 +33,46 @@ class Venv:
self._version_info = None
@classmethod
def create(cls) -> 'Venv':
def create(cls, io, name=None) -> 'Venv':
if 'VIRTUAL_ENV' not in os.environ:
# Not in a virtualenv
return cls()
# Checking if we need to create one
config = Config.create('config.toml')
create_venv = config.setting('settings.virtualenvs.create')
if create_venv is False:
io.writeln(
'<fg=black;bg=yellow>'
'Skipping virtualenv creation, '
'as specified in config file.'
'</>'
)
return cls()
venv_path = config.setting('settings.virtualenvs.path')
if venv_path is None:
venv_path = Path(CACHE_DIR) / 'virtualenvs'
else:
venv_path = Path(venv_path)
if not name:
name = Path.cwd().name
name = f'{name}-py{".".join([str(v) for v in sys.version_info[:2]])}'
venv = venv_path / name
if not venv.exists():
io.writeln(
f'Creating virtualenv <info>{name}</> in {str(venv_path)}'
)
builder = EnvBuilder(with_pip=True)
builder.create(str(venv))
else:
if io.is_very_verbose():
io.writeln(f'Virtualenv <info>{name}</> already exists.')
os.environ['VIRTUAL_ENV'] = str(venv)
# venv detection:
# stdlib venv may symlink sys.executable, so we can't use realpath.
......@@ -134,3 +176,21 @@ class Venv:
def is_venv(self) -> bool:
return self._venv is not None
class NullVenv(Venv):
def __init__(self, execute=False):
super().__init__()
self.executed = []
self._execute = execute
def run(self, bin: str, *args):
self.executed.append([bin] + list(args))
if self._execute:
return super().run(bin, *args)
def _bin(self, bin):
return bin
......@@ -11,6 +11,7 @@ from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository
from poetry.utils.venv import NullVenv
from tests.helpers import get_dependency
from tests.helpers import get_package
......@@ -108,17 +109,22 @@ def locker():
return Locker()
@pytest.fixture()
def venv():
return NullVenv()
@pytest.fixture()
def installer(package, pool, locker, venv):
return Installer(NullIO(), venv, package, locker, pool)
def fixture(name):
file = Path(__file__).parent / 'fixtures' / f'{name}.test'
return toml.loads(file.read_text())
@pytest.fixture()
def installer(package, pool, locker):
return Installer(NullIO(), package, locker, pool)
def test_run_no_dependencies(installer, locker):
installer.run()
expected = fixture('no-dependencies')
......
......@@ -8,6 +8,7 @@ from pathlib import Path
from poetry import Poetry
from poetry.io import NullIO
from poetry.masonry.builders import CompleteBuilder
from poetry.utils.venv import NullVenv
fixtures_dir = Path(__file__).parent / 'fixtures'
......@@ -29,7 +30,7 @@ def clear_samples_dist():
def test_wheel_c_extension():
module_path = fixtures_dir / 'extended'
builder = CompleteBuilder(Poetry.create(module_path), NullIO(True))
builder = CompleteBuilder(Poetry.create(module_path), NullVenv(True), NullIO())
builder.build()
sdist = fixtures_dir / 'extended' / 'dist' / 'extended-0.1.tar.gz'
......
......@@ -8,6 +8,7 @@ from pathlib import Path
from poetry import Poetry
from poetry.io import NullIO
from poetry.masonry.builders.sdist import SdistBuilder
from poetry.utils.venv import NullVenv
from tests.helpers import get_dependency
......@@ -71,7 +72,7 @@ def test_convert_dependencies():
def test_make_setup():
poetry = Poetry.create(project('complete'))
builder = SdistBuilder(poetry, NullIO())
builder = SdistBuilder(poetry, NullVenv(), NullIO())
setup = builder.build_setup()
setup_ast = ast.parse(setup)
......@@ -94,7 +95,7 @@ def test_make_setup():
def test_find_files_to_add():
poetry = Poetry.create(project('complete'))
builder = SdistBuilder(poetry, NullIO())
builder = SdistBuilder(poetry, NullVenv(), NullIO())
result = builder.find_files_to_add()
assert result == [
......@@ -111,7 +112,7 @@ def test_find_files_to_add():
def test_package():
poetry = Poetry.create(project('complete'))
builder = SdistBuilder(poetry, NullIO())
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder.build()
sdist = fixtures_dir / 'complete' / 'dist' / 'my-package-1.2.3.tar.gz'
......@@ -122,7 +123,7 @@ def test_package():
def test_prelease():
poetry = Poetry.create(project('prerelease'))
builder = SdistBuilder(poetry, NullIO())
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder.build()
sdist = fixtures_dir / 'prerelease' / 'dist' / 'prerelease-0.1b1.tar.gz'
......@@ -133,7 +134,7 @@ def test_prelease():
def test_with_c_extensions():
poetry = Poetry.create(project('extended'))
builder = SdistBuilder(poetry, NullIO())
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder.build()
sdist = fixtures_dir / 'extended' / 'dist' / 'extended-0.1.tar.gz'
......
......@@ -6,6 +6,7 @@ from pathlib import Path
from poetry import Poetry
from poetry.io import NullIO
from poetry.masonry.builders import WheelBuilder
from poetry.utils.venv import NullVenv
fixtures_dir = Path(__file__).parent / 'fixtures'
......@@ -28,7 +29,7 @@ def clear_samples_dist():
def test_wheel_module():
module_path = fixtures_dir / 'module1'
WheelBuilder.make(Poetry.create(str(module_path)), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
whl = module_path / 'dist' / 'module1-0.1-py2.py3-none-any.whl'
......@@ -37,7 +38,7 @@ def test_wheel_module():
def test_wheel_package():
module_path = fixtures_dir / 'complete'
WheelBuilder.make(Poetry.create(str(module_path)), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
whl = module_path / 'dist' / 'my_package-1.2.3-py3-none-any.whl'
......@@ -46,7 +47,7 @@ def test_wheel_package():
def test_wheel_prerelease():
module_path = fixtures_dir / 'prerelease'
WheelBuilder.make(Poetry.create(str(module_path)), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
whl = module_path / 'dist' / 'prerelease-0.1b1-py2.py3-none-any.whl'
......
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