From 875a94ced84d94e278bf5b206e142e36b7ec7225 Mon Sep 17 00:00:00 2001 From: Lou Marvin Caraig Date: Wed, 24 Jun 2020 09:48:33 +0200 Subject: [PATCH] Cleanup legacy code Signed-off-by: Lou Marvin Caraig --- .flake8 | 1 - .gitignore | 15 ++ .isort.cfg | 4 +- .travis.yml | 24 ++-- pydockenv/__init__.py | 7 - pydockenv/cli.py | 119 ---------------- pydockenv/client.py | 13 -- pydockenv/commands/__init__.py | 0 pydockenv/commands/dependency.py | 48 ------- pydockenv/commands/environment.py | 188 -------------------------- pydockenv/commands/io.py | 130 ------------------ pydockenv/definitions.py | 7 - pydockenv/executor.py | 130 ------------------ requirements-dev.txt | 2 - requirements.txt | 3 - setup.py | 2 +- tests/__init__.py | 0 tests/base.py | 77 ----------- tests/commander.py | 97 ------------- tests/integration/__init__.py | 0 tests/integration/test_dependency.py | 36 ----- tests/integration/test_environment.py | 184 ------------------------- tests/integration/test_others.py | 41 ------ tests/integration/test_port_mapper.py | 76 ----------- tox.ini | 10 -- 25 files changed, 27 insertions(+), 1187 deletions(-) delete mode 100644 pydockenv/__init__.py delete mode 100644 pydockenv/cli.py delete mode 100644 pydockenv/client.py delete mode 100644 pydockenv/commands/__init__.py delete mode 100644 pydockenv/commands/dependency.py delete mode 100644 pydockenv/commands/environment.py delete mode 100644 pydockenv/commands/io.py delete mode 100644 pydockenv/definitions.py delete mode 100644 pydockenv/executor.py delete mode 100644 requirements.txt delete mode 100644 tests/__init__.py delete mode 100644 tests/base.py delete mode 100644 tests/commander.py delete mode 100644 tests/integration/__init__.py delete mode 100644 tests/integration/test_dependency.py delete mode 100644 tests/integration/test_environment.py delete mode 100644 tests/integration/test_others.py delete mode 100644 tests/integration/test_port_mapper.py delete mode 100644 tox.ini diff --git a/.flake8 b/.flake8 index 0f2b2ec..aec1659 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,2 @@ [flake8] max-complexity = 10 -exclude = setup.py,.tox diff --git a/.gitignore b/.gitignore index 32e127f..5fe9151 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,18 @@ +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.isort.cfg b/.isort.cfg index 0a422bc..57b611e 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -5,7 +5,5 @@ order_by_type=True lines_after_imports=2 indent=' ' atomic=True -known_docker=docker -known_first_party=pydockenv,tests -sections=STDLIB,THIRDPARTY,DOCKER,FIRSTPARTY,LOCALFOLDER +sections=STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section=THIRDPARTY diff --git a/.travis.yml b/.travis.yml index 5e252a2..0eac105 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,14 @@ dist: xenial -language: python -python: - - "3.6" - - "3.7" - - "3.8" - services: - docker -before_install: - - pip install . - # For some reason if this is not done before, the image won't be pulled. - # This is not required when running locally. - - docker pull alpine/socat:latest - -script: - - python -m unittest discover +matrix: + include: + - language: go + script: echo "TO DO" + - language: python + before_script: + - pip install -r requirements-dev.txt + script: + - flake8 + - isort -rc -c -vb diff --git a/pydockenv/__init__.py b/pydockenv/__init__.py deleted file mode 100644 index aaa9220..0000000 --- a/pydockenv/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -__title__ = 'pydockenv' -__version__ = '0.5.0' -__author__ = 'Lou Marvin Caraig' -__author_email__ = 'loumarvincaraig@gmail.com' -__description__ = 'Python Virtualenv Powered by Docker' -__project_url__ = 'https://github.com/se7entyse7en/pydockenv' -__copyright__ = 'Copyright 2019 Lou Marvin Caraig' diff --git a/pydockenv/cli.py b/pydockenv/cli.py deleted file mode 100644 index d5f31eb..0000000 --- a/pydockenv/cli.py +++ /dev/null @@ -1,119 +0,0 @@ -import click - -from pydockenv.commands import dependency -from pydockenv.commands import environment -from pydockenv.commands import io -from pydockenv.executor import Executor - - -@click.group() -def cli(): - pass - - -@cli.command() -@click.argument('project_dir') -@click.option('--file', 'file_', help='Toml file') -@click.option('--name', help='Name of the environment') -@click.option('--version', help='Python version') -def create(project_dir, file_, name, version): - environment.create(project_dir, file_, name, version) - - -@cli.command() -def status(): - environment.status() - - -@cli.command() -@click.argument('name') -def activate(name): - environment.activate(name) - - -@cli.command() -def deactivate(): - environment.deactivate() - - -@cli.command() -@click.argument('name') -def remove(name): - environment.remove(name) - - -@cli.command() -def list_environments(): - environment.list_environments() - - -@cli.command() -@click.argument('package', required=False) -@click.option('-f', '--file', 'requirements_file', - help='File to containing the requirements to install') -def install(package, requirements_file): - dependency.install(package, requirements_file) - - -@cli.command() -@click.argument('package') -@click.option('-y', '--yes', is_flag=True) -def uninstall(package, yes): - dependency.uninstall(package, yes) - - -@cli.command() -def list_packages(): - dependency.list_packages() - - -@cli.command() -@click.argument('name') -@click.argument('project_dir') -@click.argument('input_file') -def load(name, project_dir, input_file): - io.load(name, project_dir, input_file) - - -@cli.command() -@click.option('--output', help='Name of the output file') -def save(name, output): - io.save(name, output) - - -@cli.command() -@click.option('--output', help='Name of the output file') -def export(output): - io.export(output) - - -@cli.command() -@click.argument('args', nargs=-1) -def shell(args): - click.echo('Running...') - try: - Executor.execute('python', *args) - finally: - click.echo('Exited!') - - -@cli.command() -@click.argument('cmd') -@click.argument('args', nargs=-1) -@click.option('-d', '--detach', is_flag=True) -@click.option('-e', '--env-var', multiple=True, - help='Environment variable to set') -@click.option('-p', '--port', multiple=True, - help='Port to reach') -def run(cmd, args, detach, env_var, port): - click.echo('Running...') - env_vars = dict(e.split('=') for e in env_var) - try: - Executor.execute(cmd, *args, detach=detach, - env_vars=env_vars, ports=list(port)) - finally: - click.echo('Exited!') - - -if __name__ == '__main__': - cli() diff --git a/pydockenv/client.py b/pydockenv/client.py deleted file mode 100644 index b32e7ff..0000000 --- a/pydockenv/client.py +++ /dev/null @@ -1,13 +0,0 @@ -import docker - - -class Client: - - _instance = None - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = docker.from_env() - - return cls._instance diff --git a/pydockenv/commands/__init__.py b/pydockenv/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pydockenv/commands/dependency.py b/pydockenv/commands/dependency.py deleted file mode 100644 index 522610c..0000000 --- a/pydockenv/commands/dependency.py +++ /dev/null @@ -1,48 +0,0 @@ -import click - -from pydockenv.executor import Executor - - -def _build_install_args(packages, requirements_file): - if not isinstance(packages, list): - packages = [packages] - - args = ['pip', 'install'] - if requirements_file: - args.extend(['-r', requirements_file]) - else: - args.extend(packages) - - return args - - -def install(packages, requirements_file): - return Executor.execute( - *_build_install_args(packages, requirements_file)) - - -def install_for_container(container, packages, requirements_file): - return Executor.execute_for_container( - container, *_build_install_args(packages, requirements_file), - bypass_check=True) - - -def uninstall(package, yes): - click.echo('Running...') - args = ['pip', 'uninstall'] - if yes: - args.append('-y') - - args.append(package) - try: - Executor.execute(*args) - finally: - click.echo('Exited!') - - -def list_packages(): - click.echo('Running...') - try: - Executor.execute('pip', 'freeze') - finally: - click.echo('Exited!') diff --git a/pydockenv/commands/environment.py b/pydockenv/commands/environment.py deleted file mode 100644 index d157128..0000000 --- a/pydockenv/commands/environment.py +++ /dev/null @@ -1,188 +0,0 @@ -import json -import os -from dataclasses import dataclass -from dataclasses import field -from typing import Dict - -import click -import toml - -import docker -from docker.types import Mount - -from pydockenv import definitions -from pydockenv.client import Client - - -def get_current_env(): - return os.environ.get('PYDOCKENV') - - -@dataclass(frozen=True) -class EnvironmentConfig: - name: str - python: str = 'latest' - dependencies: Dict[str, str] = field(default_factory=dict) - container_args: Dict[str, str] = field(default_factory=dict) - aliases: Dict[str, Dict[str, str]] = field(default_factory=dict) - - @classmethod - def from_file(cls, file_: str) -> 'EnvironmentConfig': - config = toml.load(file_)['tool']['pydockenv'] - return EnvironmentConfig(**config) - - -def create(project_dir, file_, name, version): - if file_: - config = EnvironmentConfig.from_file(file_) - else: - config = EnvironmentConfig(name, python=version or 'latest') - - create_from_config(project_dir, config) - - -def create_from_config(project_dir: str, config: EnvironmentConfig): - click.echo(f'Creating environment {config.name} with python version ' - f'{config.python}...') - image_name = f'python:{config.python}' - - client = Client.get_instance() - try: - image = client.images.get(image_name) - except docker.errors.ImageNotFound: - click.echo(f'Image {image_name} not found, pulling...') - image = client.images.pull('python', tag=config.python) - - create_network(config.name) - create_env(image, project_dir, config) - - click.echo(f'Environment {config.name} with python version ' - f'{config.python} created!') - - -def status(): - current_env = get_current_env() - if not current_env: - click.echo('No active environment') - else: - click.echo(f'Active environment: {current_env}') - - -def activate(name): - click.echo('Activating environment...') - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + name) - except docker.errors.NotFound: - click.echo(f'Environment {name} not found, exiting...') - else: - container.start() - click.echo('Environment activated!') - - -def deactivate(): - click.echo('Deactivating current environment...') - current_env = get_current_env() - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.ImageNotFound: - click.echo(f'Environment {current_env} not found, exiting...') - else: - container.stop() - click.echo('Environment deactivated!') - - -def remove(name): - click.echo(f'Removing environment {name}...') - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + name) - except docker.errors.NotFound: - click.echo(f'Environment {name} not found, exiting...') - raise - - kwargs = { - 'force': True, - } - container.remove(**kwargs) - delete_network(name) - click.echo(f'Environment {name} removed!') - - -def list_environments(): - click.echo(f'Listing environments...') - kwargs = { - 'all': True, - } - containers = Client.get_instance().containers.list(kwargs) - - current_env = get_current_env() - envs = [] - for c in containers: - if not c.name.startswith(definitions.CONTAINERS_PREFIX): - continue - - env_name = c.name[len(definitions.CONTAINERS_PREFIX):] - prefix = '* ' if env_name == current_env else ' ' - envs.append(f'{prefix}{env_name}') - - click.echo('\n'.join(envs)) - click.echo(f'Environments listed!') - - -def create_network(env_name): - network_name = definitions.CONTAINERS_PREFIX + env_name + '_network' - Client.get_instance().networks.create(network_name, check_duplicate=True) - - -def delete_network(env_name): - network_name = definitions.CONTAINERS_PREFIX + env_name + '_network' - try: - network = Client.get_instance().networks.get(network_name) - except docker.errors.ImageNotFound: - click.echo(f'Network {network_name} not found, exiting...') - raise - - for c in network.containers: - network.disconnect(c) - - network.remove() - - -def create_env(image, project_dir, config): - workdir = os.path.abspath(project_dir) - mounts = [ - Mount('/usr/src', workdir, type='bind') - ] - kwargs = { - 'command': '/bin/sh', - 'stdin_open': True, - 'labels': { - 'workdir': workdir, - 'env_name': config.name, - 'aliases': json.dumps(config.aliases), - }, - 'name': definitions.CONTAINERS_PREFIX + config.name, - 'mounts': mounts, - 'network': definitions.CONTAINERS_PREFIX + config.name + '_network', - } - - filtered_container_args = {k: v for k, v in config.container_args.items() - if k not in kwargs} - kwargs.update(filtered_container_args) - - container = Client.get_instance().containers.create(image, **kwargs) - - if config.dependencies: - # TODO: Remove this from here just to avoid circular imports - from pydockenv.commands import dependency - - container.start() - - click.echo(f'Installing {len(config.dependencies)} dependencies...') - packages = [f'{dep}{v}' for dep, v in config.dependencies.items()] - click.echo(f'Installing {packages}...') - dependency.install_for_container(container, packages, None) - - container.stop() diff --git a/pydockenv/commands/io.py b/pydockenv/commands/io.py deleted file mode 100644 index f9e3db6..0000000 --- a/pydockenv/commands/io.py +++ /dev/null @@ -1,130 +0,0 @@ -import subprocess - -import click -import toml - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from pydockenv.commands.environment import EnvironmentConfig -from pydockenv.commands.environment import create_env -from pydockenv.commands.environment import create_network -from pydockenv.commands.environment import get_current_env -from pydockenv.executor import Executor - - -def load(name, project_dir, input_file): - click.echo(f'Loading environment {name} from {input_file}...') - with open(input_file, 'rb') as fin: - image = Client.get_instance().images.load(fin)[0] - - create_network(name) - config = EnvironmentConfig(name) - create_env(image, project_dir, config) - - click.echo(f'Environment {name} loaded from {input_file}!') - - -def save(name, output): - current_env = get_current_env() - - click.echo(f'Saving environment {current_env}...') - - image_name = _commit(name, current_env) - _export(image_name, output) - - click.echo(f'Removing image {image_name}...') - Client.get_instance().images.remove(image_name) - click.echo(f'Image {image_name} removed') - - -def export(output): - client = Client.get_instance() - current_env = get_current_env() - try: - container = client.containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.NotFound: - click.echo(f'Container {current_env} not found, exiting...') - raise - - click.echo(f'Exporting environment {current_env}...') - - out = Executor.execute_for_container( - container, 'pip', 'freeze', subprocess_kwargs={ - 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE - }) - - deps = { - k: f'=={v}' for k, v in ( - r.split('==') for r in out.stdout.decode('utf8').splitlines() - ) - } - - # TODO: this is a hacky way to get the python version. One way to achieve - # this could be to add a label to the initial image. But this requires - # rebuilding the image with the new label as it's not possible to add a - # label to an already built image. - out = Executor.execute_for_container( - container, 'python', '--version', subprocess_kwargs={ - 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE - }) - - python_version = out.stdout.strip().decode('utf8').split(' ')[1] - - toml_doc = { - 'tool': { - 'pydockenv': { - 'name': current_env, - 'python': python_version, - 'dependencies': deps - } - } - } - - if not output: - click.echo(toml.dumps(toml_doc)) - else: - with open(output, 'w') as fout: - toml.dump(toml_doc, fout) - - click.echo(f'Environment {current_env} exported!') - - -def _commit(name, current_env): - click.echo(f'Saving environment {current_env} as image...') - try: - container = Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.ImageNotFound: - click.echo(f'Container {current_env} not found, exiting...') - raise - - if not name: - repository = f'{definitions.CONTAINERS_PREFIX + current_env}' - tag = 'latest' - else: - repository, tag = name.split(':') - - container.commit(repository=repository, tag=tag) - - image_name = f'{repository}:{tag}' - click.echo(f'Environment {current_env} saved as image {image_name}!') - return image_name - - -def _export(image_name, output): - click.echo(f'Saving image {image_name} to {output}...') - - try: - image = Client.get_instance().images.get(image_name) - except docker.errors.ImageNotFound: - raise - - output = output or f'{image_name}.tar.gz' - with open(output, 'wb') as fout: - for chunk in image.save(named=True): - fout.write(chunk) - - click.echo(f'Image {image_name} saved to {output}!') diff --git a/pydockenv/definitions.py b/pydockenv/definitions.py deleted file mode 100644 index 5a71c1b..0000000 --- a/pydockenv/definitions.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -import pydockenv - - -ROOT_DIR = os.path.dirname(os.path.dirname(pydockenv.__file__)) -CONTAINERS_PREFIX = 'pydockenv_' diff --git a/pydockenv/executor.py b/pydockenv/executor.py deleted file mode 100644 index b81fe35..0000000 --- a/pydockenv/executor.py +++ /dev/null @@ -1,130 +0,0 @@ -import json -import os -import subprocess -from contextlib import contextmanager -from itertools import chain - -import click - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from pydockenv.commands.environment import get_current_env - - -class Executor: - - @classmethod - def execute_for_container(cls, container, *args, **kwargs): - env_name = container.labels['env_name'] - host_base_wd = container.labels['workdir'] - current_wd = os.getcwd() - if ( - not current_wd.startswith(host_base_wd) and - not kwargs.get('bypass_check') - ): - raise RuntimeError( - f'Cannot run commands outside of {host_base_wd}') - - relative_wd = current_wd[len(host_base_wd):] - guest_wd = f'/usr/src{relative_wd}' - - detach = kwargs.get('detach') - env_vars = cls._build_env_vars(kwargs.get('env_vars')) - with cls._with_mapped_ports(container, kwargs.get('ports'), detach): - # This cannot be done with docker python sdk - cmd = ['docker', 'exec', '-w', guest_wd] - if detach: - cmd.append('-d') - else: - cmd.extend(['-i', '-t']) - - cmd = ( - cmd + env_vars + - [(definitions.CONTAINERS_PREFIX + env_name)] + - list(args) - ) - - return subprocess.run(cmd, **kwargs.get('subprocess_kwargs', {})) - - @classmethod - def execute(cls, *args, **kwargs): - client = Client.get_instance() - current_env = get_current_env() - try: - container = client.containers.get( - definitions.CONTAINERS_PREFIX + current_env) - except docker.errors.NotFound: - click.echo(f'Container {current_env} not found, exiting...') - raise - - if len(args) == 1: - aliases = container.labels.get('aliases') - if aliases: - alias = json.loads(aliases).get(args[0]) - if alias is not None: - kwargs['ports'] = alias.get('ports', []) - args = alias['cmd'].split(' ') - - return cls.execute_for_container(container, *args, **kwargs) - - @classmethod - def _build_env_vars(cls, env_vars): - if env_vars: - return list(chain.from_iterable([ - ['-e', f'{k}={v}']for k, v in env_vars.items() - ])) - - return [] - - @classmethod - @contextmanager - def _with_mapped_ports(cls, container, ports, detach): - if ports: - port_mappers_containers_names = cls._run_port_mapper( - container, ports) - else: - port_mappers_containers_names = [] - - yield - - if detach: - return - - for container_name in port_mappers_containers_names: - container = Client.get_instance().containers.get(container_name) - container.stop() - - @classmethod - def _run_port_mapper(cls, container, ports): - network_name = f'{container.name}_network' - guest_ip = container.attrs['NetworkSettings']['Networks'][ - network_name]['IPAddress'] - containers_names = [] - for port in ports: - # TODO: Use a single container for all port mappings instead of - # spinning a container for each port - name = f'{container.name}_port_mapper_{port}' - client = Client.get_instance() - - try: - container = client.containers.get(name) - except docker.errors.NotFound: - cmd = f'TCP-LISTEN:1234,fork TCP-CONNECT:{guest_ip}:{port}' - kwargs = { - 'command': cmd, - 'ports': {'1234': f'{port}/tcp'}, - 'name': name, - 'detach': True, - 'auto_remove': True, - 'network': network_name, - } - - client.containers.run('alpine/socat', **kwargs) - else: - container.start() - - containers_names.append(name) - - return containers_names diff --git a/requirements-dev.txt b/requirements-dev.txt index 338fa50..7312493 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,5 +2,3 @@ bumpversion==0.5.3 isort==4.3.16 flake8==3.7.7 twine==1.13.0 -tox==3.9.0 -requests==2.21.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 33ffdf5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Click==7.0 -docker==3.7.0 -toml==0.10.0 diff --git a/setup.py b/setup.py index 14bfc93..6a17ce8 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def get_tag(self): long_description=long_description, long_description_content_type='text/markdown', url=about['project_url'], - packages=setuptools.find_packages(exclude=['tests']), + packages=setuptools.find_packages(), scripts=scripts, package_data={ '': ['LICENSE'], diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/base.py b/tests/base.py deleted file mode 100644 index de24243..0000000 --- a/tests/base.py +++ /dev/null @@ -1,77 +0,0 @@ -import os -import shutil -import unittest -from pathlib import Path - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from pydockenv.commands.environment import delete_network -from tests.commander import Commander - - -class BaseIntegrationTest(unittest.TestCase): - - ENV_SUFFIX = '__test-{index}' - - @classmethod - def setUpClass(cls): - cls._client = docker.from_env() - cls._low_level_client = docker.APIClient() - - @classmethod - def tearDownClass(cls): - cls._client.close() - cls._low_level_client.close() - - def setUp(self): - self._cwd = os.getcwd() - self._test_dir = Path(definitions.ROOT_DIR, '.test-dir') - self._projs_dir = Path(str(self._test_dir), 'projs') - - self._commander = Commander() - - self._env_index = 1 - os.makedirs(str(self._projs_dir)) - - def tearDown(self): - os.chdir(self._cwd) - try: - for i in range(1, self._env_index): - env_name = self._create_env_name(i) - try: - Client.get_instance().containers.get( - definitions.CONTAINERS_PREFIX + env_name).remove( - force=True) - delete_network(env_name) - except docker.errors.NotFound: - pass - - self._remove_port_mappers(env_name) - finally: - shutil.rmtree(self._test_dir.name) - - def assertCommandOk(self, command_out): - self.assertEqual(command_out.returncode, 0, - msg=command_out.stderr.decode('utf8')) - - def _remove_port_mappers(self, env_name): - prefix = definitions.CONTAINERS_PREFIX + env_name + '_port_mapper_' - for c in Client.get_instance().containers.list(all=True): - if c.name.startswith(prefix): - c.remove(force=True) - - def _env_name(self): - env_name = self._create_env_name(self._env_index) - self._env_index += 1 - return env_name - - def _create_env_name(self, index): - suffix = self.ENV_SUFFIX.format(index=index) - return f'env{suffix}' - - def _create_project_dir(self, proj_name): - proj_dir = Path(str(self._projs_dir), proj_name) - os.makedirs(str(proj_dir)) - return proj_dir diff --git a/tests/commander.py b/tests/commander.py deleted file mode 100644 index 5d8f478..0000000 --- a/tests/commander.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import subprocess -from contextlib import contextmanager -from pathlib import Path - -from pydockenv import definitions - - -BIN_PATH = str(Path(definitions.ROOT_DIR, 'bin', 'pydockenv')) - - -class Commander: - - _instance = None - - def __init__(self, env=None): - self._bin_path = BIN_PATH - self._env = env or {} - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = Commander() - - return cls._instance - - def add_env_var(self, k, v): - self._env[k] = v - - def run(self, cmd, env=None): - args = cmd.split(' ') - - env = self._prepare_env(env) - - return subprocess.run( - [self._bin_path, *args], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=env - ) - - @contextmanager - def active_env(self, env_name): - env_diff = self.activate_env(env_name) - env = os.environ.copy() - env.update({k: v[1] for k, v in env_diff.items()}) - - try: - yield env - finally: - self.deactivate_env(env=env) - - def activate_env(self, env_name, env=None): - return self.source(f'activate {env_name}', env=env) - - def deactivate_env(self, env=None): - return self.source('deactivate', env=env) - - def source(self, cmd, env=None): - env = self._prepare_env(env) - - proc = subprocess.Popen('env', stdout=subprocess.PIPE, shell=True, - env=env) - initial_env = self._get_env(proc.stdout) - proc.communicate() - - command = f"bash -c 'PYDOCKENV_DEBUG=1 source {self._bin_path} {cmd}'" - proc = subprocess.Popen(command, stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, shell=True, env=env) - post_env = self._get_env(proc.stderr) - proc.communicate() - - env_diff = {} - for k in set().union(initial_env.keys(), post_env.keys()): - initial_value, post_value = initial_env.get(k), post_env.get(k) - if initial_value != post_value: - env_diff[k] = (initial_value, post_value) - - return env_diff - - def _get_env(self, stdout): - env = {} - for line in stdout: - (key, _, value) = line.decode('utf8').strip().partition("=") - env[key] = value - - return env - - def _prepare_env(self, env): - env = {**self._env, **(env or {})} - env['PYTHONPATH'] = definitions.ROOT_DIR - if env: - env = {k: v for k, v in {**os.environ, **env}.items() - if v is not None} - else: - env = None - - return env diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_dependency.py b/tests/integration/test_dependency.py deleted file mode 100644 index 05b7738..0000000 --- a/tests/integration/test_dependency.py +++ /dev/null @@ -1,36 +0,0 @@ -import os - -from tests.base import BaseIntegrationTest - - -class TestIntegrationDependencyCommands(BaseIntegrationTest): - - def test_deps_handling(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - proj_dir = self._create_project_dir(proj_name) - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - with self._commander.active_env(env_name) as env: - os.chdir(proj_dir) - - out = self._commander.run('list-packages', env=env) - self.assertCommandOk(out) - self.assertNotIn(f'pydockenv', out.stdout.decode('utf8')) - - out = self._commander.run('install pydockenv', env=env) - self.assertCommandOk(out) - - out = self._commander.run('list-packages', env=env) - self.assertCommandOk(out) - self.assertIn(f'pydockenv', out.stdout.decode('utf8')) - - out = self._commander.run('uninstall -y pydockenv ', env=env) - self.assertCommandOk(out) - - out = self._commander.run('list-packages', env=env) - self.assertCommandOk(out) - self.assertNotIn(f'pydockenv', out.stdout.decode('utf8')) diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py deleted file mode 100644 index 8f47e40..0000000 --- a/tests/integration/test_environment.py +++ /dev/null @@ -1,184 +0,0 @@ -from pathlib import Path - -import docker - -from pydockenv import definitions -from tests.base import BaseIntegrationTest - - -class TestIntegrationEnvironmentCommands(BaseIntegrationTest): - - def test_create(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - cont_name = definitions.CONTAINERS_PREFIX + env_name - - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get(cont_name) - - proj_dir = self._create_project_dir(proj_name) - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - expected = (f'Environment {env_name} with python version ' - f'{py_version} created!') - self.assertIn(expected, out.stdout.decode('utf8')) - - r = self._client.containers.get(cont_name) - self.assertEqual(r.status, 'created') - - r = self._low_level_client.inspect_container(cont_name) - self.assertEqual(len(r['Mounts']), 1) - - expected = { - 'Destination': '/usr/src', - 'Mode': '', - 'Propagation': 'rprivate', - 'RW': True, - 'Source': str(proj_dir.absolute()), - 'Type': 'bind' - } - actual = r['Mounts'][0] - self.assertEqual(expected, actual) - - expected = {f'{cont_name}_network'} - actual = set(r['NetworkSettings']['Networks'].keys()) - self.assertEqual(expected, actual) - - expected = { - 'env_name': env_name, - 'workdir': str(Path(self._projs_dir, proj_name)), - 'aliases': '{}', - } - actual = r['Config']['Labels'] - self.assertEqual(expected, actual) - - def test_remove(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - cont_name = definitions.CONTAINERS_PREFIX + env_name - - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get(cont_name) - - proj_dir = self._create_project_dir(proj_name) - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - r = self._client.containers.get(cont_name) - self.assertEqual(r.status, 'created') - - out = self._commander.run(f'remove {env_name}') - self.assertCommandOk(out) - - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get(cont_name) - - with self.assertRaises(docker.errors.NotFound): - self._client.networks.get(f'{cont_name}_network') - - def test_activate(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - proj_dir = self._create_project_dir(proj_name) - - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - env_diff = self._commander.activate_env(f'{env_name}') - - self.assertTrue({'PYDOCKENV', 'PYDOCKENV_DEBUG', 'PS1', 'SHLVL'} <= - set(env_diff.keys())) - self.assertEqual(env_diff['PYDOCKENV'][1], env_name) - self.assertEqual(env_diff['PYDOCKENV_DEBUG'][1], '1') - self.assertEqual(env_diff['PS1'][1], f'({env_name})') - self.assertEqual(int(env_diff['SHLVL'][1]), - int(env_diff['SHLVL'][0] or 0) + 1) - - def test_deactivate(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - proj_dir = self._create_project_dir(proj_name) - - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - with self._commander.active_env(env_name) as env: - env_diff_post_deactivate = self._commander.deactivate_env(env=env) - - self.assertEqual({'PYDOCKENV', 'PS1', 'SHLVL'}, - env_diff_post_deactivate.keys()) - self.assertEqual(env_diff_post_deactivate['PYDOCKENV'][1], '') - self.assertEqual(env_diff_post_deactivate['PS1'][1], '') - self.assertEqual(int(env_diff_post_deactivate['SHLVL'][1]), - int(env_diff_post_deactivate['SHLVL'][0]) + 1) - - def test_list_environments(self): - out = self._commander.run('list-environments') - self.assertCommandOk(out) - - stdout_lines = out.stdout.decode('utf8').split('\n') - initial_envs = set([s.strip() for s in stdout_lines if s][1:-1]) - - data = [ - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-1', - 'v': '3.7', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-2', - 'v': '3.6', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-3', - 'v': '2.7', - }, - ] - - for d in data: - with self.assertRaises(docker.errors.NotFound): - self._client.containers.get( - definitions.CONTAINERS_PREFIX + d['env_name']) - - proj_dir = self._create_project_dir(d['proj_name']) - out = self._commander.run( - f"create --name={d['env_name']} --version={d['v']} " - f"{str(proj_dir)}" - ) - self.assertCommandOk(out) - - out = self._commander.run('list-environments') - self.assertCommandOk(out) - - stdout_lines = out.stdout.decode('utf8').split('\n') - envs = set([s.strip() for s in stdout_lines if s][1:-1]) - - self.assertEqual(envs - initial_envs, {d['env_name'] for d in data}) - - def test_status(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - proj_dir = self._create_project_dir(proj_name) - - out = self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - self.assertCommandOk(out) - - out = self._commander.run('status') - self.assertCommandOk(out) - self.assertEqual(out.stdout.decode('utf8').strip(), - 'No active environment') - - with self._commander.active_env(env_name) as env: - out = self._commander.run('status', env=env) - self.assertCommandOk(out) - self.assertEqual(out.stdout.decode('utf8').strip(), - f'Active environment: {env_name}') diff --git a/tests/integration/test_others.py b/tests/integration/test_others.py deleted file mode 100644 index b40709a..0000000 --- a/tests/integration/test_others.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -from pydockenv import definitions -from tests.base import BaseIntegrationTest - - -class TestIntegrationOtherCommands(BaseIntegrationTest): - - def test_run(self): - data = [ - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-1', - 'v': '3.7', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-2', - 'v': '3.6', - }, - { - 'env_name': self._env_name(), - 'proj_name': 'test-proj-3', - 'v': '2.7', - }, - ] - - for d in data: - proj_dir = self._create_project_dir(d['proj_name']) - out = self._commander.run( - f"create --name={d['env_name']} --version={d['v']} " - f"{str(proj_dir)}" - ) - self.assertCommandOk(out) - with self._commander.active_env(d['env_name']) as env: - os.chdir(proj_dir) - out = self._commander.run('run -- python --version', env=env) - self.assertCommandOk(out) - self.assertIn(f"Python {d['v']}", out.stdout.decode('utf8')) - - os.chdir(definitions.ROOT_DIR) diff --git a/tests/integration/test_port_mapper.py b/tests/integration/test_port_mapper.py deleted file mode 100644 index 8de65f4..0000000 --- a/tests/integration/test_port_mapper.py +++ /dev/null @@ -1,76 +0,0 @@ -import os - -import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry - -import docker - -from pydockenv import definitions -from pydockenv.client import Client -from tests.base import BaseIntegrationTest - - -class TestIntegrationPortMapperCommands(BaseIntegrationTest): - - def assertPortMapperExists(self, env_name, port): - port_mapper_container_name = ( - definitions.CONTAINERS_PREFIX + env_name + f'_port_mapper_{port}' - ) - - try: - Client.get_instance().containers.get( - port_mapper_container_name) - except docker.errors.NotFound: - self.fail( - f'Cannot find port mapper for environment {env_name} with' - f'port {port}' - ) - - def test_port_mapping_single_port(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - proj_dir = self._create_project_dir(proj_name) - self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - with self._commander.active_env(env_name) as env: - os.chdir(proj_dir) - - port = 8000 - out = self._commander.run( - f'run -d -p {port} -- python -m http.server {port}', env=env) - self.assertCommandOk(out) - self.assertPortMapperExists(env_name, port) - - s = requests.Session() - s.mount('http://', HTTPAdapter( - max_retries=Retry(connect=3, backoff_factor=1))) - r = s.get(f'http://localhost:{port}') - - self.assertEqual(r.status_code, 200, msg=r.content) - - def test_port_mapping_multi_ports(self): - env_name = self._env_name() - proj_name, py_version = 'test-proj', '3.7' - - proj_dir = self._create_project_dir(proj_name) - self._commander.run( - f'create --name={env_name} --version={py_version} {str(proj_dir)}') - with self._commander.active_env(env_name) as env: - os.chdir(proj_dir) - - for port in range(8000, 8003): - out = self._commander.run( - f'run -d -p {port} -- python -m http.server {port}', - env=env - ) - self.assertCommandOk(out) - self.assertPortMapperExists(env_name, port) - - s = requests.Session() - s.mount('http://', HTTPAdapter( - max_retries=Retry(connect=3, backoff_factor=1))) - r = s.get(f'http://localhost:{port}') - - self.assertEqual(r.status_code, 200, msg=r.content) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 778f9d7..0000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py36,py37 -skipsdist = True -skip_missing_interpreters=true - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = python -m unittest discover \ No newline at end of file