Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clone without virtualenv #462

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/source/cloning.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,37 @@ You can configure cloning in your configuration TOML in the
`cosmic-ray.cloning` section. At a minimum, you must have a
`cosmic-ray.cloning.method` entry in your config.

Possible entry into `[cosmic-ray.cloning]` are:

- `workspace_type`
- `method`
- `command`

workspace_type
==============

The `workspace_type` entry is the name of workspace method creation:
Possible names are:

- `cloned`: Workspace is built by cloning your sources with the `copy` method
(see bellow). Useful if your sources can be run in-place.
- `cloned_with_virtualenv`: Workspace is built by cloning your sources like
previous method and create an local virtualenv environment locally.
Useful to deploy your sources.

::

[cosmic-ray.cloning]
workspace_type = 'cloned'

This entry is optional. For compatibility reason, the default is
`cloned_with_virtualenv`.

After This workspace deployent, `command` will be executed (see bellow).

copy
====

The "copy" cloning method simple copies an entire filesystem directory tree. You can use configure it like
this::

Expand Down
111 changes: 68 additions & 43 deletions src/cosmic_ray/cloning.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Support for making clones of projects for test isolation.
"""

import contextlib
import abc
import logging
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
from typing import Type

import virtualenv

import git
Expand All @@ -17,69 +18,68 @@
log = logging.getLogger(__name__)


@contextlib.contextmanager
def cloned_workspace(clone_config, chdir=True):
"""Create a cloned workspace and yield it.
class WorkspaceMeta(type):
_registred = {}

This creates a workspace for a with-block and cleans it up on exit. By
default, this will also change to the workspace's `clone_dir` for the
duration of the with-block.
def __new__(cls, *args, **kwargs):
new_class = super(WorkspaceMeta, cls).__new__(cls, *args, **kwargs) # type: Type[Workspace]
cls._registred[new_class.name] = new_class
return new_class

Args:
clone_config: The execution engine configuration to use for the workspace.
chdir: Whether to change to the workspace's `clone_dir` before entering the with-block.
@classmethod
def get_workspace_class(cls, name):
return cls._registred[name]

Yields: The `CloneWorkspace` instance created for the context.
"""
workspace = ClonedWorkspace(clone_config)
original_dir = os.getcwd()
if chdir:
os.chdir(workspace.clone_dir)

try:
yield workspace
finally:
os.chdir(original_dir)
workspace.cleanup()
class Workspace(metaclass=WorkspaceMeta):
name = None

@classmethod
def get_workspace(cls, name, clone_config) -> 'Workspace':
return type(cls).get_workspace_class(name)(clone_config)

class ClonedWorkspace:
"""Clone a project and install it into a temporary virtual environment.
@abc.abstractmethod
def cleanup(self):
pass

Note that this actually *activates* the virtual environment, so don't construct one
of these unless you want that to happen in your process.
@property
@abc.abstractmethod
def clone_dir(self):
pass


class ClonedWorkspace(Workspace):
"""Clone a project into a temporary directory.
"""
name = 'cloned'

def __init__(self, clone_config):
self._tempdir = tempfile.TemporaryDirectory()
log.info('New project clone in %s', self._tempdir.name)
self._prepare_directory(clone_config)
self._load_environment()
self._run_commands(clone_config.get('commands', ()))

def _prepare_directory(self, clone_config):
log.info('New project clone in %s', self._tempdir.name)
self._clone_dir = str(Path(self._tempdir.name) / 'repo')

if clone_config['method'] == 'git':
method = clone_config['method']
if method == 'git':
_clone_with_git(
clone_config.get('repo-uri', '.'),
self._clone_dir)
elif clone_config['method'] == 'copy':

elif method == 'copy':
_clone_with_copy(
os.getcwd(),
self._clone_dir)

# pylint: disable=fixme
# TODO: We should allow user to specify which version of Python to use.
# How? The EnvBuilder could be passed a path to a python interpreter
# which is used in the call to pip. This path would need to come from
# the config.
else:
raise Exception("Clone method '%s' unknown" % method)

# Install into venv
self._venv_path = Path(self._tempdir.name) / 'venv'
log.info('Creating virtual environment in %s', self._venv_path)
virtualenv.create_environment(str(self._venv_path))

_activate(self._venv_path)
_install_sitecustomize(self._venv_path)

self._run_commands(clone_config.get('commands', ()))
def _load_environment(self):
os.environ['PYTHONPATH'] = \
'%s:%s' % (self.clone_dir, os.environ.get('PYTHONPATH', ''))

@property
def clone_dir(self):
Expand Down Expand Up @@ -113,6 +113,31 @@ def _run_commands(self, commands):
command, exc.output)


class ClonedWorkspaceWithVirtualenv(ClonedWorkspace):
"""Clone a project and install it into a temporary virtual environment.

Note that this actually *activates* the virtual environment, so don't construct one
of these unless you want that to happen in your process.
"""
name = 'cloned_with_virtualenv'

def _prepare_directory(self, clone_config):
# pylint: disable=fixme
# TODO: We should allow user to specify which version of Python to use.
# How? The EnvBuilder could be passed a path to a python interpreter
# which is used in the call to pip. This path would need to come from
# the config.

# Install into venv
self._venv_path = Path(self._tempdir.name) / 'venv'
log.info('Creating virtual environment in %s', self._venv_path)
virtualenv.create_environment(str(self._venv_path))

def _load_environment(self):
_activate(self._venv_path)
_install_sitecustomize(self._venv_path)


def _clone_with_git(repo_uri, dest_path):
"""Create a clone by cloning a git repository.

Expand Down
1 change: 1 addition & 0 deletions src/cosmic_ray/commands/new_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def new_config():
config['execution-engine']['name'] = menu.show(header="Execution engine", returns="desc")

config["cloning"] = ConfigDict()
config['cloning']['workspace_type'] = 'cloned_with_virtualenv'
config['cloning']['method'] = 'copy'
config['cloning']['commands'] = []

Expand Down
5 changes: 5 additions & 0 deletions src/cosmic_ray/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ def badge_format(self):
def badge_thresholds(self):
return self.badge['thresholds']

@property
def cloning_config_workspace_type(self):
"The 'workspace' section of the cloning section"
Copy link

@mmphego mmphego Aug 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"The 'workspace' section of the cloning section"
"""The 'workspace' section of the cloning section."""

return self.cloning_config.get('workspace_type', 'cloned_with_virtualenv')


@contextmanager
def _config_stream(filename):
Expand Down
8 changes: 5 additions & 3 deletions src/cosmic_ray/execution/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
import multiprocessing.util
import os

from cosmic_ray.cloning import ClonedWorkspace
from cosmic_ray.cloning import ClonedWorkspaceWithVirtualenv, Workspace
from cosmic_ray.config import ConfigDict
from cosmic_ray.execution.execution_engine import ExecutionEngine
from cosmic_ray.worker import worker

Expand All @@ -62,7 +63,7 @@ def excursion(dirname):
os.chdir(orig)


def _initialize_worker(config):
def _initialize_worker(config: ConfigDict):
# pylint: disable=global-statement
global _workspace
global _config
Expand All @@ -72,7 +73,8 @@ def _initialize_worker(config):
_config = config

log.info('Initialize local-git worker in PID %s', os.getpid())
_workspace = ClonedWorkspace(config.cloning_config)
_workspace = Workspace.get_workspace(config.cloning_config_workspace_type,
config.cloning_config)

# Register a finalizer
multiprocessing.util.Finalize(_workspace, _workspace.cleanup, exitpriority=16)
Expand Down