diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..324395c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +*.cache +*.log +*.log.* +*.pid +*.wpr +*.wpu +*.a +*.o +*.py[co] +*.so +*.sw[nop] +__pycache__ +*~ +.#* +[#]*# +dropin.cache +RE:\.?[^.]+ +*.e4p +.pytest_cache +environment.yml +dist +*.egg-info +.coverage +htmlcov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4b7e0bd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,78 @@ +# Info: +# ----- +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +fail_fast: True +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + #- id: no-commit-to-branch + #- args: [--branch, master] + - id: check-docstring-first + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: check-merge-conflict + - id: double-quote-string-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: check-executables-have-shebangs + - id: fix-encoding-pragma + - id: check-case-conflict + - id: name-tests-test + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.4.1 + hooks: + - id: python-use-type-annotations + - id: python-no-log-warn + - repo: https://github.com/asottile/reorder_python_imports + rev: v1.6.0 + hooks: + - id: reorder-python-imports + - repo: https://github.com/asottile/add-trailing-comma + rev: v1.4.1 + hooks: + - id: add-trailing-comma + - repo: https://github.com/pre-commit/mirrors-yapf + rev: v0.28.0 + hooks: + - id: yapf + types: [python] + always_run: true + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + - id: check-ast + - repo: https://gitlab.com/pycqa/flake8 + rev: ace069c9c3585b4c74348f94da313bbc020dfc73 + hooks: + - id: flake8 + - repo: https://github.com/codespell-project/codespell + rev: 70196e35a7baa730e26ab6a0da77b5e68d980611 + hooks: + - id: codespell + - repo: local + hooks: + - id: pytest + name: pytest + description: running pytest + entry: bash -c "$VIRTUAL_ENV/bin/pytest" + language: system + types: [python] + pass_filenames: false + - id: coverage-badge + name: coverage-badge + description: create the coverage badge + entry: bash -c "$VIRTUAL_ENV/bin/coverage-badge -f -o docs/img/coverage.svg" + language: system + types: [python] + pass_filenames: false + - id: build-package + name: build the package for the repo + description: Generate the dist packages for this repo + language: system + entry: dephell project build + files: "^pyproject.toml$" + pass_filenames: false diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c1e43e6 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.7.3 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..268f2a9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Simon Kallfass + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2d0cb2 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# conda-env-manager: cenv + +![coverage](docs/img/coverage.svg) +[![PyPI version fury.io](https://badge.fury.io/py/ansicolortags.svg)](https://pypi.python.org/pypi/cenv_tool/) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/ansicolortags.svg)](https://pypi.python.org/pypi/cenv_tool/) +[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) + +![logo](docs/img/logo.png) + +Due to the redundant dependency information inside the `meta.yaml` (required +to create the conda-package) and the `environment.yml` (as definition file +for the conda-environment during development and for production), `cenv` +(short form for `conda-env-manager`) was created to make the `meta.yaml` +the only relevant file and to create and update conda-environment from the +definition inside this `meta.yaml`. +The name of the conda-environment to create / update is defined in the section +`extra` and the variable `env_name` inside the `meta.yaml`. + +The steps run by cenv: + +* creation of a backup if the environment already exists followed by the + removal of the previous environment. +* creation of the environment as defined in the `meta.yaml`. + If any failures occurred during creation and the backup was created, the + command to reset the backup-version can be used. +* if enabled in the config file the environment.yml is exported after creation + / update of the environment. + + +The usage of cenv reduces the conda commands to use to the following: + +* `conda activate ...` to activate the environment +* `conda deactivate` to deactivate an environment +* `conda info` to show information about the currently activated environment +* `conda search ...` to search for availability of a package in the conda + channels. +* `conda remove -n ... --all` to remove an environment +* `cenv` to create / update an environment + + +## Installation + +Install `cenv` using pip: +```bash +pip3 install cenv_tool +``` + +Now run `init_cenv` to create the relevant config-files and add the +autoactivate- and autoupdate-shell-function to your `.bashrc` / `.zshrc`. + + +### autoactivate and autoupdate + +Per default these features are deactivated, even if added to your shell by +running `init_cenv`. + + +#### autoactivate-feature + +The autoactivate-feature activates the conda-environment as named +`extra`-section in the meta.yaml located at `conda-build/meta.yaml`, if the +environment exists. +To activate the autoactivate-features run: +```bash +autoactivate_toggle +``` + +#### autoupdate-feature + +The autoupdate checks if the content of the meta.yaml changed. +The current state is stored as a md5sum in `conda-build/meta.md5`. +If it changed the cenv-process is called. + +For the autoupdate-feature run: +```bash +autoupdate_toggle +``` + + +## Usage + +All steps required to create or update the projects conda environment are +run automatically running: +```bash +cenv +``` + +**ATTENTION**: +> If you use cenv, each environment should only be created, updated and +> modified using `cenv`! +> This means the commands `conda install`, `conda remove` are not used +> anymore. +> Changes of the dependencies of the environment are defined inside the +> `meta.yaml` and are applied by using `cenv`. +> +> This means: +> +> * new dependency required => add it in `meta.yaml` and run `cenv`. +> * dependency not needed anymore => remove it from `meta.yaml` and run +> `cenv`. +> * need of another version of dependency => change the version of dependency +> in `meta.yaml` and run `cenv`. + +The required information about the projects conda environment are extracted +from the meta.yaml. +This meta.yaml should be located inside the project folder at +`./conda-build/meta.yaml`. +The project-configuration is defined in the `extra` section of the `meta.yaml`. +There you can define the name of the projects conda-environment at +`env_name`. +Also you can define requirements only needed during development but not to be +included into the resulting conda package. +These requirements have to be defined in the `dev_requirements`-section. + +All other parts of the `meta.yaml` have to be defined as default. + +A meta.yaml valid for cenv should look like the following: +```yaml + {% set data = load_setup_py_data() %} + + package: + name: "example_package" + version: {{ data.get("version") }} + + source: + path: .. + + build: + build: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + preserve_egg_dir: True + script: python -m pip install --no-deps --ignore-installed . + + requirements: + build: + - python 3.6.8 + - pip + - setuptools + run: + - python 3.6.8 + - attrs >=18.2 + - jinja2 >=2.10 + - ruamel.yaml >=0.15.23 + - six >=1.12.0 + - yaml >=0.1.7 + - marshmallow >=3.0.0rc1* + + test: + imports: + - example_package + + extra: + env_name: example + dev_requirements: + - ipython >=7.2.0 +``` + +**ATTENTION**: +> In the `requirements-run-section` the minimal version of each package +> has to be defined! +> The same is required for the `dev_requirements`-section. +> Not defining a version will not create or update a conda-environment, +> because this is not the purpose of the conda-usage. +> The validity of the `meta.yaml` is checked in `cenv` using the +> `marshmallow` package. +> You can additionally add upper limits for the version like the following: +> `- package >=0.1,<0.3` + +If cenv is run the environment is created / updated from the definition inside +this `meta.yaml`. +The creation of the backup of the previous environment ensures to undo changes +if any error occurs during recreation of the environment. + + +**ATTENTION**: +> `cenv` can only update the environment if it is not activated. +> So ensure the environment to be deactivated before running `cenv`. + +Per default exporting the conda environment definition into an environment.yml +is turned off. +If you want to turn this functionality on you need to modify your +`~/.config/cenv.yml` as described in the configuration-part. + +Example for the output of the `cenv` command: + +```bash + ┣━━ Cloning existing env as backup ... + ┣━━ Removing existing env ... + ┣━━ Creating env ... + ┣━━ Removing backup ... + ┗━━ Exporting env to environment.yml ... +``` + +# Development of cenv + +To create the environment to develop cenv run the pre-commit hooks manually: +```bash +pyenv local 3.7.3 +pre-commit run --all-files +poetry shell +``` diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..48a5079 --- /dev/null +++ b/README.rst @@ -0,0 +1,243 @@ + +conda-env-manager: cenv +======================= + + +.. image:: coverage.svg + :target: coverage.svg + :alt: coverage + + +.. image:: https://badge.fury.io/py/ansicolortags.svg + :target: https://pypi.python.org/pypi/cenv_tool/ + :alt: PyPI version fury.io + + +.. image:: https://img.shields.io/pypi/pyversions/ansicolortags.svg + :target: https://pypi.python.org/pypi/cenv_tool/ + :alt: PyPI pyversions + + +.. image:: https://img.shields.io/badge/License-MIT-blue.svg + :target: https://lbesson.mit-license.org/ + :alt: MIT license + + + +.. image:: docs/img/logo.png + :target: docs/img/logo.png + :alt: logo + + +Due to the redundant dependency information inside the ``meta.yaml`` (required +to create the conda-package) and the ``environment.yml`` (as definition file +for the conda-environment during development and for production), ``cenv`` +(short form for ``conda-env-manager``\ ) was created to make the ``meta.yaml`` +the only relevant file and to create and update conda-environment from the +definition inside this ``meta.yaml``. +The name of the conda-environment to create / update is defined in the section +``extra`` and the variable ``env_name`` inside the ``meta.yaml``. + +The steps run by cenv: + + +* creation of a backup if the environment already exists followed by the + removal of the previous environment. +* creation of the environment as defined in the ``meta.yaml``. + If any failures occurred during creation and the backup was created, the + command to reset the backup-version can be used. +* if enabled in the config file the environment.yml is exported after creation + / update of the environment. + +The usage of cenv reduces the conda commands to use to the following: + + +* ``conda activate ...`` to activate the environment +* ``conda deactivate`` to deactivate an environment +* ``conda info`` to show information about the currently activated environment +* ``conda search ...`` to search for availability of a package in the conda + channels. +* ``conda remove -n ... --all`` to remove an environment +* ``cenv`` to create / update an environment + +Installation +------------ + +Install ``cenv`` using pip: + +.. code-block:: bash + + pip3 install cenv_tool + +Now run ``init_cenv`` to create the relevant config-files and add the +autoactivate- and autoupdate-shell-function to your ``.bashrc`` / ``.zshrc``. + +autoactivate and autoupdate +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Per default these features are deactivated, even if added to your shell by +running ``init_cenv``. + +autoactivate-feature +~~~~~~~~~~~~~~~~~~~~ + +The autoactivate-feature activates the conda-environment as named +``extra``\ -section in the meta.yaml located at ``conda-build/meta.yaml``\ , if the +environment exists. +To activate the autoactivate-features run: + +.. code-block:: bash + + autoactivate_toggle + +autoupdate-feature +~~~~~~~~~~~~~~~~~~ + +The autoupdate checks if the content of the meta.yaml changed. +The current state is stored as a md5sum in ``conda-build/meta.md5``. +If it changed the cenv-process is called. + +For the autoupdate-feature run: + +.. code-block:: bash + + autoupdate_toggle + +Usage +----- + +All steps required to create or update the projects conda environment are +run automatically running: + +.. code-block:: bash + + cenv + +**ATTENTION**\ : + +.. + + If you use cenv, each environment should only be created, updated and + modified using ``cenv``\ ! + This means the commands ``conda install``\ , ``conda remove`` are not used + anymore. + Changes of the dependencies of the environment are defined inside the + ``meta.yaml`` and are applied by using ``cenv``. + + This means: + + + * new dependency required => add it in ``meta.yaml`` and run ``cenv``. + * dependency not needed anymore => remove it from ``meta.yaml`` and run + ``cenv``. + * need of another version of dependency => change the version of dependency + in ``meta.yaml`` and run ``cenv``. + + +The required information about the projects conda environment are extracted +from the meta.yaml. +This meta.yaml should be located inside the project folder at +``./conda-build/meta.yaml``. +The project-configuration is defined in the ``extra`` section of the ``meta.yaml``. +There you can define the name of the projects conda-environment at +``env_name``. +Also you can define requirements only needed during development but not to be +included into the resulting conda package. +These requirements have to be defined in the ``dev_requirements``\ -section. + +All other parts of the ``meta.yaml`` have to be defined as default. + +A meta.yaml valid for cenv should look like the following: + +.. code-block:: yaml + + {% set data = load_setup_py_data() %} + + package: + name: "example_package" + version: {{ data.get("version") }} + + source: + path: .. + + build: + build: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + preserve_egg_dir: True + script: python -m pip install --no-deps --ignore-installed . + + requirements: + build: + - python 3.6.8 + - pip + - setuptools + run: + - python 3.6.8 + - attrs >=18.2 + - jinja2 >=2.10 + - ruamel.yaml >=0.15.23 + - six >=1.12.0 + - yaml >=0.1.7 + - marshmallow >=3.0.0rc1* + + test: + imports: + - example_package + + extra: + env_name: example + dev_requirements: + - ipython >=7.2.0 + +**ATTENTION**\ : + +.. + + In the ``requirements-run-section`` the minimal version of each package + has to be defined! + The same is required for the ``dev_requirements``\ -section. + Not defining a version will not create or update a conda-environment, + because this is not the purpose of the conda-usage. + The validity of the ``meta.yaml`` is checked in ``cenv`` using the + ``marshmallow`` package. + You can additionally add upper limits for the version like the following: + ``- package >=0.1,<0.3`` + + +If cenv is run the environment is created / updated from the definition inside +this ``meta.yaml``. +The creation of the backup of the previous environment ensures to undo changes +if any error occurs during recreation of the environment. + +**ATTENTION**\ : + +.. + + ``cenv`` can only update the environment if it is not activated. + So ensure the environment to be deactivated before running ``cenv``. + + +Per default exporting the conda environment definition into an environment.yml +is turned off. +If you want to turn this functionality on you need to modify your +``~/.config/cenv.yml`` as described in the configuration-part. + +Example for the output of the ``cenv`` command: + +.. code-block:: bash + + ┣━━ Cloning existing env as backup ... + ┣━━ Removing existing env ... + ┣━━ Creating env ... + ┣━━ Removing backup ... + ┗━━ Exporting env to environment.yml ... + +Development of cenv +=================== + +To create the environment to develop cenv run the pre-commit hooks manually: + +.. code-block:: bash + + pyenv local 3.7.3 + pre-commit run --all-files + poetry shell diff --git a/cenv_tool/__init__.py b/cenv_tool/__init__.py new file mode 100644 index 0000000..c8c3ccc --- /dev/null +++ b/cenv_tool/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""Conda environment creation and update from meta.yaml.""" +from pkg_resources import get_distribution + +try: + __version__ = get_distribution('cenv_tool').version +except AttributeError: + __version__ = '' diff --git a/cenv_tool/cenv.sh b/cenv_tool/cenv.sh new file mode 100644 index 0000000..ffc2558 --- /dev/null +++ b/cenv_tool/cenv.sh @@ -0,0 +1,75 @@ +source /opt/conda/etc/profile.d/conda.sh +export AUTOACTIVATE=1 +export AUTOUPDATE=1 +export meta_yaml_path=conda-build/meta.yaml +export meta_yaml_md5_path=conda-build/meta.md5 +CYAN='\033[0;94m' +GREEN='\033[1;32m' +RED='\033[1;91m' +COLOR_END='\033[0m' +AS_BOLD='\033[1;37m' + + + +function autoactivate_toggle(){ + if (( $AUTOACTIVATE == 1 )); then + export AUTOACTIVATE=0 + else + export AUTOACTIVATE=1 + fi +} + + +function autoupdate_toggle(){ + if (( $AUTOUPDATE == 1 )); then + export AUTOUPDATE=0 + else + export AUTOUPDATE=1 + fi +} + + +function get_envname_from_meta(){ + if [ -e $meta_yaml_path ]; then + while IFS='' read -r line || [[ -n "$line" ]]; do + if [[ $line == *"env_name"* ]]; then + echo $( echo "$line" | cut -d' ' -f 6-) + fi + done < $meta_yaml_path; + fi +} + + +function autoactivate_env() { + if (( $AUTOUPDATE == 1 )); then + if [ -e $PWD/$meta_yaml_md5_path ]; then + local new_md5=$(echo "$(md5sum $PWD/$meta_yaml_path)" | cut -d' ' -f1) + local current_md5="$(head -n 1 $PWD/$meta_yaml_md5_path)" + if ! [ $new_md5 = $current_md5 ]; then + conda deactivate + cenv + fi + fi + fi + + if (( $AUTOACTIVATE == 1 )); then + if [ -e $PWD/$meta_yaml_path ]; then + local env="$(get_envname_from_meta)" + local env="${env%\"}" + local env="${env#\"}" + if [[ $PATH != *$env* ]]; then + if conda activate $env 2>/dev/null && [[ $? -eq 0 ]]; then + CONDA_ENV_ROOT="$(pwd)" + PYTHONPATH=.:$PYTHONPATH + echo -e "${GREEN}"'\u2714'" activated${COLOR_END} $env" + fi + fi + elif [[ $PATH = */envs/* ]] && [[ $(pwd) != $CONDA_ENV_ROOT ]] \ + && [[ $(pwd) != $CONDA_ENV_ROOT/* ]]; then + CONDA_ENV_ROOT="" + conda deactivate + unset PYTHONPATH + echo -e "${RED}"'\u2718'" deactivated${COLOR_END} env" + fi + fi +} diff --git a/cenv_tool/cenv.yml b/cenv_tool/cenv.yml new file mode 100644 index 0000000..5339b4a --- /dev/null +++ b/cenv_tool/cenv.yml @@ -0,0 +1,3 @@ +conda_folder: /opt/conda +env_folder: /shared/conda/envs +export_environment_yml: false diff --git a/cenv_tool/init_cenv.py b/cenv_tool/init_cenv.py new file mode 100644 index 0000000..ed57115 --- /dev/null +++ b/cenv_tool/init_cenv.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +"""Install config and cenv.sh.""" +from pathlib import Path +from typing import NoReturn + +from cenv_tool.utils import message + +CONFIG_PATH = Path.home() / '.config/cenv' +AUTOENV_SCRIPT_PATH = CONFIG_PATH / 'cenv.sh' +AUTOENV_SCRIPT_SOURCE_PATH = Path(__file__).with_name('cenv.sh') +CONFIG_FILE = CONFIG_PATH / 'cenv.yml' +CONFIG_FILE_SOURCE = Path(__file__).with_name('cenv.yml') +ZSHRC = Path.home() / '.zshrc' +BASHRC = Path.home() / '.bashrc' + +RC_CONTENT = """ +# ======================================================================>>>>>>> +# AUTOMATICALLY ADDED BY CENV TO ENABLE AUTOUPDATE AND AUTOACTIVATE + +# load the cenv shell functions +source $HOME/.config/cenv/cenv.sh + +# 0 means deactivated, 1 activated +# disable autoactivate +export AUTOACTIVATE=0 +# disable autoupdate +export AUTOUPDATE=0 + +# enable the autoactivation of conda-environments +precmd() { autoactivate_env; } +# <----- +# <<<<<<<====================================================================== +""" + + +def initialize_cenv( + config_path: Path, + autoenv_script_path: Path, + autoenv_script_source_path: Path, + config_file: Path, + config_file_source: Path, + zshrc: Path, + bashrc: Path, +) -> NoReturn: + """Install user-config and cenv.sh for autoactivate and autoupdate. + + Parameters: + config_path: the path for cenv config-stuff. + autoenv_script_path: the path to install the cenv.sh script to. + autoenv_script_source_path: the path where to get the cenv.sh script + from + config_file: the path to install the user-config into. + config_file_source: the path where to get the config file from. + zshrc: the path to the users .zshrc + bashrc: the path to the users .bashrc + + """ + if not config_path.exists(): + message(text='creating cenv-config folder', color='cyan') + config_path.mkdir(parents=True) + message(text='created cenv-config folder', color='green') + else: + message(text='cenv-config folder already exists', color='green') + + if not autoenv_script_path.exists(): + message( + text=( + 'copying script for autoactivation and autoupdate to ' + 'config-folder' + ), + color='cyan', + ) + autoenv_script_path.write_text(autoenv_script_source_path.read_text()) + message( + text='copied script for autoactivateion and autoupdate', + color='green', + ) + else: + message( + text='script for autoactivation and autupdate already copied', + color='green', + ) + + if not config_file.exists(): + message( + text='copying config-file to cenv-config-folder ...', + color='cyan', + ) + config_file.write_text(config_file_source.read_text()) + message(text='copied config-file', color='green') + else: + message(text='config-file already exists', color='green') + + for shell_config in (zshrc, bashrc): + if shell_config.exists(): + if RC_CONTENT not in shell_config.read_text(): + message( + text=f'initilized cenv in {shell_config}', + color='green', + ) + with open(shell_config, 'a') as opened_shell_config: + opened_shell_config.write(RC_CONTENT) + else: + message( + text=f'cenv already initialized in {shell_config}', + color='green', + ) + + +def main(): + """Call the initialization function to install config and cenv.sh.""" + initialize_cenv( + config_path=CONFIG_PATH, + autoenv_script_path=AUTOENV_SCRIPT_PATH, + autoenv_script_source_path=AUTOENV_SCRIPT_SOURCE_PATH, + config_file=CONFIG_FILE, + config_file_source=CONFIG_FILE_SOURCE, + zshrc=ZSHRC, + bashrc=BASHRC, + ) + + +if __name__ == '__main__': + main() diff --git a/cenv_tool/project.py b/cenv_tool/project.py new file mode 100644 index 0000000..af75354 --- /dev/null +++ b/cenv_tool/project.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +"""Contain the processing for creating conda environments from the meta.yaml. + +cenv is a tool to handle conda-environment creation and update from the +dependency-definition inside the meta.yaml file. + +As default conda has two files for dependency management: +* the environment.yml +* and the meta.yaml + +In the environment.yml the environment-definition is stored. +In the meta.yaml the required information to build a conda-package are +stored. This means redundant information. + +cenv collects the dependency-information and all project-specific settings +from the meta.yaml-file. + +The collected information is used to create / update the projects conda +environment. +""" +from argparse import ArgumentParser +from argparse import Namespace +from argparse import RawTextHelpFormatter +from pathlib import Path +from typing import List +from typing import NoReturn + +import attr + +from cenv_tool import __version__ +from cenv_tool.rules import ARGPARSE_DESCRIPTION +from cenv_tool.rules import CondaCmdFormats +from cenv_tool.rules import RULES +from cenv_tool.rules import Rules +from cenv_tool.utils import CenvProcessError +from cenv_tool.utils import message +from cenv_tool.utils import read_config +from cenv_tool.utils import read_meta_yaml +from cenv_tool.utils import run_in_bash + + +@attr.s(slots=True, auto_attribs=True) +class Project: + """Contain a python-project using conda-environments. + + Containing methods to display information to current project and methods + to update the projects conda-environment from the settings defined in the + projects meta.yaml file. + """ + + rules: Rules + conda_folder: Path = attr.ib(default=None) + env_folder: Path = attr.ib(default=None) + env_name: str = attr.ib(default=None) + dependencies: dict = attr.ib(default=None) + is_env: bool = attr.ib(default=None) + export_environment_yml: bool = attr.ib(None) + cmds: CondaCmdFormats = attr.ib(default=None) + cmd_kwargs: dict = attr.ib(default=None) + is_git: bool = attr.ib(default=None) + + def __attrs_post_init__(self): + """Set the more complex attributes of the project class.""" + try: + meta_yaml, dependencies = read_meta_yaml(Path.cwd()) + settings = meta_yaml['extra'] + except FileNotFoundError: + message(text='project has no meta.yaml!', color='red') + exit(1) + + config = read_config() + self.is_git = (Path.cwd() / self.rules.git_folder).exists() + self.export_environment_yml = config['export_environment_yml'] + self.conda_folder = Path(config['conda_folder']) + self.env_folder = Path(config['env_folder']) + self.env_name = settings['env_name'] + self.dependencies = dependencies + self.is_env = self.env_name in self.collect_available_envs() + conda_bin = self.rules.conda_cmds.conda_bin(self.conda_folder) + self.cmds = self.rules.conda_cmds + self.cmd_kwargs = { + 'conda': conda_bin, + 'name': self.env_name, + 'pkgs': ' '.join([f'"{_}"' for _ in self.dependencies]), + } + + def collect_available_envs(self) -> List[str]: + """Collect the names of the conda-environments currently installed. + + Parameters: + conda_folder: the path where conda is installed. + + Returns: + List of currently installed conda-environments + + """ + return run_in_bash( + str(self.conda_folder.absolute()) + + '/bin/conda env list | awk \'{ if( !($1=="#") ) print $1 }\'', + ).split('\n') + + def write_new_md5sum(self): + """Write the new md5sum of the meta.yaml to conda-build/meta.md5.""" + message(text='write md5sum of meta.yaml', color='bold', special='row') + command = ( + 'echo "$(md5sum $PWD/conda-build/meta.yaml)" | ' + 'cut -d\' \' -f1 > $PWD/conda-build/meta.md5' + ) + run_in_bash(cmd=command) + message(text='updated', color='green', special='end', indent=2) + + def export_environment_definition(self) -> NoReturn: + """Export the projects environment definition to an environment.yml.""" + message(text='Export environment.yml ...', color='bold', special='row') + run_in_bash(cmd=self.cmds.export.format(**self.cmd_kwargs)) + message(text='Exported', color='green', special='end', indent=2) + + def remove_backup_environment(self) -> NoReturn: + """Remove backup cloned from original environment.""" + run_in_bash(cmd=self.cmds.clean.format(**self.cmd_kwargs)) + + def restore_environment_from_backup(self, cloned: bool) -> NoReturn: + """Restore the environment from the cloned backup environment. + + After restore the backup environment is removed. + + Parameters: + cloned: indicates if the environment already existed and a backup + was created. + + """ + message(text='Error during creation!', color='red', special='row') + if self.is_env and cloned: + message(text='Recreating backup', color='bold', special='row') + run_in_bash(cmd=self.cmds.restore.format(**self.cmd_kwargs)) + self.remove_backup_environment() + message(text='Recreated', color='green', special='end', indent=2) + message(text='Exit', color='red', special='end') + + def remove_previous_environment(self) -> NoReturn: + """Remove old version of project environment. + + If the old environment can't be removed, the backup made is removed. + """ + try: + message(text='Remove existing env', color='bold', special='row') + run_in_bash(cmd=self.cmds.remove.format(**self.cmd_kwargs)) + message(text='Removed', color='green', special='end', indent=2) + except CenvProcessError: + self.remove_backup_environment() + message( + text=( + 'Could not remove environment because it is ' + 'activated! Please deactivate it first.' + ), + color='red', + ) + exit(1) + + def clone_environment_as_backup(self) -> NoReturn: + """Clone the existing environment as a backup. + + If the backup already exists, the previous backup is removed, then + the new one is created by cloning the current project environment. + """ + backup_name = f'{self.env_name}_backup' + if backup_name in self.collect_available_envs(): + message(text='Clear old backup', color='bold', special='row') + self.remove_backup_environment() + message(text='Cleared', color='green', special='end', indent=2) + message(text='Create backup', color='bold', special='row') + run_in_bash(cmd=self.cmds.clone.format(**self.cmd_kwargs)) + message(text='Created', color='green', special='end', indent=2) + + def handle_existing_environment(self) -> bool: + """Check if environment already exists and create a backup of it.""" + if self.is_env: + self.clone_environment_as_backup() + self.remove_previous_environment() + return True + + return False + + def create_environment(self, cloned: bool) -> NoReturn: + """Create the environment for the project. + + Try to create the environment for the project. If the environment + already existed and a backup was made and any error occurs, the backup + environment is restored. + If everything worked correctly the backup (if made) is finally + removed. + + Parameters: + cloned: indicates if the environment already existed and a backup + was created. + + """ + message(text='Create environment', color='bold', special='row') + + try: + run_in_bash(cmd=self.cmds.create.format(**self.cmd_kwargs)) + except CenvProcessError: + self.restore_environment_from_backup(cloned=cloned) + raise + + if cloned: + message(text='Clear backup', color='bold', special='row', indent=2) + run_in_bash(cmd=self.cmds.clean.format(**self.cmd_kwargs)) + message(text='Cleared', color='green', special='end', indent=3) + + message(text='Created', color='green', special='end', indent=2) + + def update(self) -> NoReturn: + """Create / recreate the conda-environment of the current project. + + If the conda-environment already exists the user is aked for + confirmation to continue. Then the environment will be cloned as a + backup and the original environment will be removed. Now the new + conda-environment will be created. If a backup was created it is + removed afterwards. If any errors occurs during creation of the new + environment the old environment will be recreated from backup and + then the backup will be removed. If activated in the config-file, the + environment-definition of the created environment is exported to an + environment.yml file. Finally the md5sum of the meta.yaml is stored + for the autoupdate feature. + """ + if self.is_env: + message(text=f'Updating {self.env_name}', color='cyan') + else: + message(text=f'Creating {self.env_name}', color='cyan') + + cloned = self.handle_existing_environment() + + self.create_environment(cloned=cloned) + + if self.export_environment_yml: + self.export_environment_definition() + + self.write_new_md5sum() + + message(text='Done', color='green', special='end') + + +def build_arguments() -> Namespace: + """Create arguments for the cenv-tool. + + Returns: + The parsed arguments + + """ + parser = ArgumentParser( + description=ARGPARSE_DESCRIPTION, + epilog='For additional information see http://www.cenv.ouroboros.info', + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument( + '-v', + '--version', + action='store_true', + default=False, + help='Show current version of cenv and exit.', + ) + args = parser.parse_args() + return args + + +def main() -> NoReturn: + """Collect the required args, initializing and running the Project.""" + args = build_arguments() + if args.version: + print(__version__) + else: + Project(rules=RULES).update() + + +if __name__ == '__main__': + main() diff --git a/cenv_tool/rules.py b/cenv_tool/rules.py new file mode 100644 index 0000000..2f430f3 --- /dev/null +++ b/cenv_tool/rules.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""Rules-definitions required by cenv.""" +import attr + +EXAMPLE_META_YAML = """ + {% set data = load_setup_py_data() %} + + package: + name: "example_package" + version: {{ data.get("version") }} + + source: + path: .. + + build: + build: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + preserve_egg_dir: True + script: python -m pip install --no-deps --ignore-installed . + + requirements: + build: + - python 3.6.8 + - pip + - setuptools + run: + - python 3.6.8 + - attrs >=18.2 + - marshmallow >=3.0.0rc1* + + test: + imports: + - example_package + + extra: + env_name: example + dev_requirements: + - ipython >=7.2.0 + - pylint >=2.3.1 +""" + +ARGPARSE_DESCRIPTION = ( + """ + Create / update conda environments from meta.yaml definition. + Due to the redundant dependency information inside the + meta.yaml (required to create conda-package) + and the environment.yml (definition for conda environment), + cenv (short for `conda-env-manager`) was created to make the + meta.yaml the only relevant file for creation and update of + conda environments. + The name of the conda-environment to create / update is defined + in the section "extra" and the variable "env_name" inside the + meta.yaml. + Dependencies and their versions are extracted from the + "requirements-run"-section of the meta.yaml. + Dependencies required during development are defined in the + "dev_requirements"-section. + + Steps run by cenv: + * Cloning existing environment as backup if already exists + * Removing existing environment if already exists + * Creating environment from definition in meta.yaml + * Removing backup environment if everything worked as expected + * Exporting env to environment.yml (only if activated in config)' + + + IMPORTANT: + if you do not use the functionalities "autoactivate" and + "autoupdate" from cenv.sh you have to deactivate the + environment before running cenv. + + + An example for a valid meta.yaml: + """ + EXAMPLE_META_YAML +) + + +@attr.s(slots=True) +class CondaCmdFormats: + """Contain the formats for the conda commands to use inside cenv.""" + + remove = '{conda} remove -n {name} --all -y' + export = '{conda} env export -n {name} > conda-build/environment.yml' + create = '{conda} create -n {name} {pkgs} -y' + clone = '{conda} create -n {name}_backup --clone {name} -y' + restore = '{conda} create -n {name} --clone {name}_backup -y' + clean = '{conda} remove -n {name}_backup --all -y' + + def conda_bin(self, conda_folder): + """Combine the path of conda-folder with subpath of conda-bin. + + Returns: + the path to the conda-executable + + """ + return (conda_folder / 'bin/conda').absolute() + + +@attr.s(slots=True) +class Rules: + """Contain the rules required by cenv-tool.""" + + conda_cmds = CondaCmdFormats() + git_folder = '.git' + + +RULES = Rules() diff --git a/cenv_tool/schemata.py b/cenv_tool/schemata.py new file mode 100644 index 0000000..31d46a3 --- /dev/null +++ b/cenv_tool/schemata.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +"""Contain schemata required by cenv-tool.""" +from marshmallow import fields +from marshmallow import Schema +from marshmallow import validate + + +class SNPackage(Schema): + """Contain the package-section inside a meta.yaml.""" + + name = fields.String( + strict=True, + required=True, + ) + version = fields.String( + strict=True, + required=True, + ) + + +class SNSource(Schema): + """Contain the source-section inside a meta.yaml.""" + + path = fields.String( + strict=True, + required=True, + ) + + +class SNBuild(Schema): + """Contain the build-section inside a meta.yaml. + + Schema for the build-section inside a meta.yaml file, to check this section + of a given meta.yaml file in cenv, to be valid. + The build-section requires to define the build-number, if the egg-dir + should be preserved, the script to run on installation and if any + entrypoints are defined for the package. + """ + + build = fields.String( + strict=True, + required=True, + ) + preserve_egg_dir = fields.String( + strict=True, + required=True, + validate=validate.OneOf(['True', 'False']), + ) + script = fields.String( + strict=True, + required=True, + ) + entry_points = fields.List( + fields.String(strict=True, required=False), + strict=True, + required=False, + ) + + +class SNRequirements(Schema): + """Contain requirements-section inside a meta.yaml. + + Schema for the requirements-section inside a meta.yaml file, to check this + section of a given meta.yaml file in cenv, to be valid. + The underlying build- and run-sections have to be valid! + """ + + build = fields.List( + fields.String( + strict=True, + required=True, + ), + strict=True, + required=True, + ) + run = fields.List( + fields.String( + strict=True, + required=True, + validate=lambda x: '=' in x if 'python ' not in x else True, + error_messages=dict(validator_failed='Version must be specified'), + ), + strict=True, + required=True, + ) + + +class SNTest(Schema): + """Contain tests-section inside a meta.yaml. + + Schema for the test-section inside a meta.yaml file, to check this section + of a given meta.yaml file in cenv, to be valid. + """ + + imports = fields.List( + fields.String( + strict=True, + required=False, + ), + strict=True, + required=False, + ) + commands = fields.List( + fields.String( + strict=True, + required=False, + ), + strict=True, + required=False, + ) + + +class SNExtra(Schema): + """Contain the extra-section inside a meta.yaml. + + Schema for the extra-section inside a meta.yaml file, to check this + section of a given meta.yaml file in cenv, to be valid. + The extra-section has to contains the information where to find the + conda-folder, the name of the conda-environment to use for the current + project and the cenv-version used when the meta.yaml file was created. + """ + + env_name = fields.String( + strict=True, + required=True, + validate=lambda x: ' ' not in x, + ) + + dev_requirements = fields.List( + fields.String( + strict=True, + required=True, + validate=lambda x: '=' in x, + error_messages=dict(validator_failed='Version must be specified'), + ), + strict=True, + required=False, + ) + + +class SMetaYaml(Schema): + """Contain the representable of a complete meta.yaml file. + + Schema for a meta.yaml file to be used for cenv. + Ensures the meta.yaml to load contains the relevant information about + the package, source, build, requirements and extra. + The test-section is optional. + """ + + package = fields.Nested( + SNPackage, + strict=True, + required=True, + ) + source = fields.Nested( + SNSource, + strict=True, + required=True, + ) + build = fields.Nested( + SNBuild, + strict=True, + required=True, + ) + requirements = fields.Nested( + SNRequirements, + strict=True, + required=True, + ) + test = fields.Nested( + SNTest, + strict=True, + required=False, + ) + extra = fields.Nested( + SNExtra, + strict=True, + required=True, + ) diff --git a/cenv_tool/utils.py b/cenv_tool/utils.py new file mode 100644 index 0000000..e10cf7c --- /dev/null +++ b/cenv_tool/utils.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +"""Contain utils required by cenv-tool.""" +import os +from pathlib import Path +from subprocess import CalledProcessError +from subprocess import check_output +from subprocess import STDOUT +from typing import NoReturn + +import jinja2 +import six +from marshmallow import ValidationError +from ruamel.yaml import YAML + +from cenv_tool.schemata import SMetaYaml + +CYAN = '\033[1;36m' +GREEN = '\033[1;32m' +RED = '\033[1;91m' +NCOLOR = '\033[0m' +BOLD = '\033[1;37m' + + +class CenvProcessError(Exception): + """Represent a process error during cenv execution.""" + + +def message( + *, text: str, color: str, special: str = None, indent: int = 1 +) -> NoReturn: + """Print the passed text in the passed color on terminal. + + Parameters: + text: The text to print colored on terminal + + """ + color_mapping = { + 'red': RED, + 'green': GREEN, + 'cyan': CYAN, + 'bold': BOLD, + } + if indent == 1: + indent_prefix = ' ' * indent + else: + indent_prefix = ' ' + '│  ' * (indent - 1) + special_mapping = { + 'row': f'{indent_prefix}├── ', + 'end': f'{indent_prefix}└── ', + } + + if special: + prefix = special_mapping[special] + else: + prefix = '' + print(f'{prefix}{color_mapping[color]}{text}{NCOLOR}') + + +def run_in_bash(cmd: str) -> str: + """Run passed cmd inside bash using the subprocess.check_output-function. + + Parameters: + cmd: the command to execute. + + Returns: + the output of the ran command. + + """ + try: + result = check_output([cmd], shell=True, stderr=STDOUT) + except CalledProcessError: + raise CenvProcessError() + return result.strip().decode('ascii') + + +class NullUndefined(jinja2.Undefined): + """Handle jinja2-variables with undefined content inside the meta.yaml.""" + + def __unicode__(self): + """Replace uncode dunder of this class.""" + return six.text_type(self._undefined_name) + + def __getattr__(self, attribute_name: str): + """Replace getattr dunder of this class.""" + return six.text_type(f'{self}.{attribute_name}') + + def __getitem__(self, attribute_name: str): + """Replace getitem dunder of this class.""" + return f'{self}["{attribute_name}"]' + + +class StrDict(dict): + """Handle dictionaries for jinja2-variables inside the meta.yaml.""" + + def __getitem__(self, key: str, default: str = '') -> str: + """Replace getitem dunder of this class.""" + return self[key] if key in self else default + + +def read_meta_yaml(path: Path) -> dict: + """Read the meta.yaml file. + + The file is read from relative path conda-build/meta.yaml inside + the current path, validates the meta.yaml using the marshmallow-schema, + extracts the dependency-information and the project-settings and returns + these information. + + Parameters: + path: The current working directory + + Returns: + List containing the project-settings as a dict and the dependencies + also as a dict + + """ + # load the meta.yaml-content + myaml_content = (path / 'conda-build/meta.yaml').open().read() + jinja2_env = jinja2.Environment(undefined=NullUndefined) + jinja2_loaded_myaml = jinja2_env.from_string(myaml_content) + render_kwargs = { + 'os': os, + 'environ': StrDict(), + 'load_setup_py_data': StrDict, + } + rendered_myaml = jinja2_loaded_myaml.render(**render_kwargs) + loaded_myaml = YAML(typ='base').load(rendered_myaml) + + # validate the content of loaded meta.yaml + try: + dumped = SMetaYaml(strict=True).dumps(loaded_myaml).data + meta_yaml_content = SMetaYaml(strict=True).loads(dumped).data + except ValidationError as err: + message(text='meta.yaml file is not valid!', color='red') + message(text=f'ValidationError in {err.args[0]}', color='red') + raise + + # extract the dependencies defined the the requirements-run-section + dependencies = meta_yaml_content['requirements']['run'] + if meta_yaml_content['extra'].get('dev_requirements'): + dependencies.extend(meta_yaml_content['extra']['dev_requirements']) + + # combine the collected project-settings and the collected dependencies + # to one output of this function + return meta_yaml_content, dependencies + + +def read_config(): + """Read the config file for cenv from the users-home path if it exists. + + If there is no user-config-file the default one is used. + + Returns: + the content of the read config file. + + """ + user_config_path = Path.home() / '.config/cenv/cenv.yml' + default_config_path = Path(__file__).parent / 'cenv.yml' + + # Collect settings from config file .cenv.yml + main_config = YAML(typ='safe').load(default_config_path.open().read()) + + # if a user-config-file exists, read the content and update the main-config + if user_config_path.exists(): + user_config = YAML(typ='safe').load(user_config_path.open().read()) + main_config.update(user_config) + + return main_config diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..f69e513 --- /dev/null +++ b/docs/about.md @@ -0,0 +1,3 @@ +* **Author**: Simon Kallfass +* **Homepage**: https://www.ouroboros.info +* **Email**: skallfass@ouroboros.info diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..7dc99ed --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,19 @@ +`cenv` uses the path `/opt/conda` as default conda-installation-folder +and `/shared/conda/envs` as default conda-environments-folder. + +You can overwrite these settings with a `cenv.yml` at +`~/.config/cenv/cenv.yml` with the following content: + +```yaml +conda_folder: /opt/conda +env_folder: /shared/conda/envs +export_environment_yml: false +``` + +There you can define your own conda-installation-path and the +conda-environments-folder. +The functionality to export the created / updated environment into +a `environment.yml` can be activated / deactivated here, too. +Per default it is deactivated. +If this is activated, the environment.yml will be placed at +`conda-build/environment.yml`. diff --git a/docs/img/coverage.svg b/docs/img/coverage.svg new file mode 100644 index 0000000..1c7007c --- /dev/null +++ b/docs/img/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 89% + 89% + + diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000..9d4447f Binary files /dev/null and b/docs/img/logo.png differ diff --git a/docs/impressum.md b/docs/impressum.md new file mode 100644 index 0000000..ea00793 --- /dev/null +++ b/docs/impressum.md @@ -0,0 +1,86 @@ +## Legal Disclosure + +Information in accordance with section 5 TMG + +* Simon Kallfass +* Hofäckerstr. 46 +* 76139 Karlsruhe + + +## Contact + +* Telephone: +49 177 176 7126 +* E-Mail: skallfass@ouroboros.info +* Homepage: https://www.ouroboros.info + + +## Disclaimer + +Accountability for content +The contents of our pages have been created with the utmost care. However, we +cannot guarantee the contents' accuracy, completeness or topicality. +According to statutory provisions, we are furthermore responsible for our own +content on these web pages. In this context, please note that we are +accordingly not obliged to monitor merely the transmitted or saved +information of third parties, or investigate circumstances pointing to +illegal activity. Our obligations to remove or block the use of information +under generally applicable laws remain unaffected by this as per §§ 8 to 10 +of the Telemedia Act (TMG). + + +## Accountability for links + +Responsibility for the content of external links (to web pages of third +parties) lies solely with the operators of the linked pages. No violations +were evident to us at the time of linking. Should any legal infringement +become known to us, we will remove the respective link immediately. + + +## Copyright + +Our web pages and their contents are subject to German copyright law. +Unless expressly permitted by law (§ 44a et seq. of the copyright law), +every form of utilizing, reproducing or processing works subject to copyright +protection on our web pages requires the prior consent of the respective owner +of the rights. Individual reproductions of a work are allowed only for +private use, so must not serve either directly or indirectly for earnings. +Unauthorized utilization of copyrighted works is punishable +(§ 106 of the copyright law). + + +# Privacy Statement + + +## General + +Your personal data (e.g. title, name, house address, e-mail address, +phone number, bank details, credit card number) are processed by us only in +accordance with the provisions of German data privacy laws. The following +provisions describe the type, scope and purpose of collecting, processing +and utilizing personal data. This data privacy policy applies only to our +web pages. If links on our pages route you to other pages, please inquire +there about how your data are handled in such cases. + + +## Inventory data + +1. Your personal data, insofar as these are necessary for this contractual + relationship (inventory data) in terms of its establishment, organization + of content and modifications, are used exclusively for fulfilling the + contract. For goods to be delivered, for instance, your name and address + must be relayed to the supplier of the goods. +2. Without your explicit consent or a legal basis, your personal data are not + passed on to third parties outside the scope of fulfilling this contract. + After completion of the contract, your data are blocked against further + use. After expiry of deadlines as per tax-related and commercial + regulations, these data are deleted unless you have expressly consented to + their further use. + + +## Disclosure + +According to the Federal Data Protection Act, you have a right to +free-of-charge information about your stored data, and possibly entitlement +to correction, blocking or deletion of such data. Inquiries can be directed to +the following e-mail addresses: +skallfass@ouroboros.info diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c46975e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,39 @@ +# conda-env-manager: cenv + +![coverage](img/coverage.svg) +[![PyPI version fury.io](https://badge.fury.io/py/ansicolortags.svg)](https://pypi.python.org/pypi/cenv_tool/) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/ansicolortags.svg)](https://pypi.python.org/pypi/cenv_tool/) +[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) + +![logo](img/logo.png) + +Due to the redundant dependency information inside the `meta.yaml` (required +to create the conda-package) and the `environment.yml` (as definition file +for the conda-environment during development and for production), `cenv` +(short form for `conda-env-manager`) was created to make the `meta.yaml` +the only relevant file and to create and update conda-environment from the +definition inside this `meta.yaml`. +The name of the conda-environment to create / update is defined in the section +`extra` and the variable `env_name` inside the `meta.yaml` (at +`conda-build/meta.yaml`). + +The steps run by cenv: + +* creation of a backup if the environment already exists followed by the + removal of the previous environment. +* creation of the environment as defined in the `meta.yaml`. + If any failures occurred during creation and the backup was created, the + command to reset the backup-version can be used. +* if enabled in the config file the environment.yml is exported after creation + / update of the environment. + + +The usage of cenv reduces the conda commands to use to the following: + +* `conda activate ...` to activate the environment +* `conda deactivate` to deactivate an environment +* `conda info` to show information about the currently activated environment +* `conda search ...` to search for availability of a package in the conda + channels. +* `conda remove -n ... --all` to remove an environment +* `cenv` to create / update an environment diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..09f5747 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,35 @@ +To install cenv simply run: +```bash +pip3 install cenv_tool +``` + +Now run `init_cenv` to create the relevant config-files and add the +autoactivate- and autoupdate-shell-function to your `.bashrc` / `.zshrc`. + + +# autoactivate and autoupdate + +Per default these features are deactivated, even if added to your shell by +running `init_cenv`. + + +## autoactivate-feature + +The autoactivate-feature activates the conda-environment as named +`extra`-section in the meta.yaml located at `conda-build/meta.yaml`, if the +environment exists. +To activate the autoactivate-features run: +```bash +autoactivate_toggle +``` + +## autoupdate-feature + +The autoupdate checks if the content of the meta.yaml changed. +The current state is stored as a md5sum in `conda-build/meta.md5`. +If it changed the cenv-process is called. + +For the autoupdate-feature run: +```bash +autoupdate_toggle +``` diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..268f2a9 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Simon Kallfass + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..5ac1ff5 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,110 @@ +All steps required to create or update the projects conda environment are +run automatically running: +```bash +cenv +``` + +**ATTENTION**: +> If you use cenv, each environment should only be created, updated and +> modified using `cenv`! +> This means the commands `conda install`, `conda remove` are not used +> anymore. +> Changes of the dependencies of the environment are defined inside the +> `meta.yaml` and are applied by using `cenv`. +> +> This means: +> +> * new dependency required => add it in `meta.yaml` and run `cenv`. +> * dependency not needed anymore => remove it from `meta.yaml` and run +> `cenv`. +> * need of another version of dependency => change the version of dependency +> in `meta.yaml` and run `cenv`. + +The required information about the projects conda environment are extracted +from the meta.yaml. +This meta.yaml should be located inside the project folder at +`./conda-build/meta.yaml`. +The project-configuration is defined in the `extra` section of the `meta.yaml`. +There you can define the name of the projects conda-environment at +`env_name`. +Also you can define requirements only needed during development but not to be +included into the resulting conda package. +These requirements have to be defined in the `dev_requirements`-section. + +All other parts of the `meta.yaml` have to be defined as default. + +A meta.yaml valid for cenv should look like the following: +```yaml + {% set data = load_setup_py_data() %} + + package: + name: "example_package" + version: {{ data.get("version") }} + + source: + path: .. + + build: + build: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + preserve_egg_dir: True + script: python -m pip install --no-deps --ignore-installed . + + requirements: + build: + - python 3.6.8 + - pip + - setuptools + run: + - python 3.6.8 + - attrs >=18.2 + - jinja2 >=2.10 + - ruamel.yaml >=0.15.23 + - six >=1.12.0 + - yaml >=0.1.7 + - marshmallow >=3.0.0rc1* + + test: + imports: + - example_package + + extra: + env_name: example + dev_requirements: + - ipython >=7 +``` + +**ATTENTION**: +> In the `requirements-run-section` the minimal version of each package +> has to be defined! +> The same is required for the `dev_requirements`-section. +> Not defining a version will not create or update a conda-environment, +> because this is not the purpose of the conda-usage. +> The validity of the `meta.yaml` is checked in `cenv` using the +> `marshmallow` package. +> You can additionally add upper limits for the version like the following: +> `- package >=0.1,<0.3` + +If cenv is run the environment is created / updated from the definition inside +this `meta.yaml`. +The creation of the backup of the previous environment ensures to undo changes +if any error occurs during recreation of the environment. + + +**ATTENTION**: +> `cenv` can only update the environment if it is not activated. +> So ensure the environment to be deactivated before running `cenv`. + +Per default exporting the conda environment definition into an environment.yml +is turned off. +If you want to turn this functionality on you need to modify your +`~/.config/cenv.yml` as described in [configuration](configuration.md). + +Example for the output of the `cenv` command: + +```bash + ┣━━ Cloning existing env as backup ... + ┣━━ Removing existing env ... + ┣━━ Creating env ... + ┣━━ Removing backup ... + ┗━━ Exporting env to environment.yml ... +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..7049af5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,13 @@ +site_name: 'conda-env-manager: cenv' +nav: + - Home: index.md + - Installation: installation.md + - Configuration: configuration.md + - Usage: usage.md + - About: about.md + - License: license.md + - Impressum: impressum.md +theme: + name: readthedocs + hljs_languages: + - yaml diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..644af77 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,563 @@ +[[package]] +category = "dev" +description = "Disable App Nap on OS X 10.9" +marker = "sys_platform == \"darwin\"" +name = "appnope" +optional = false +python-versions = "*" +version = "0.1.0" + +[[package]] +category = "dev" +description = "An abstract syntax tree for Python with inference support." +name = "astroid" +optional = false +python-versions = ">=3.4.*" +version = "2.2.5" + +[package.dependencies] +lazy-object-proxy = "*" +six = "*" +typed-ast = ">=1.3.0" +wrapt = "*" + +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" + +[[package]] +category = "dev" +description = "Specifications for callback functions passed in to an API" +name = "backcall" +optional = false +python-versions = "*" +version = "0.1.0" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.4.1" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +version = "4.5.4" + +[[package]] +category = "dev" +description = "Generate coverage badges for Coverage.py." +name = "coverage-badge" +optional = false +python-versions = "*" +version = "1.0.1" + +[[package]] +category = "dev" +description = "Better living through Python with decorators" +name = "decorator" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.4.0" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +name = "importlib-metadata" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.19" + +[package.dependencies] +zipp = ">=0.5" + +[[package]] +category = "dev" +description = "IPython: Productive Interactive Computing" +name = "ipython" +optional = false +python-versions = ">=3.5" +version = "7.7.0" + +[package.dependencies] +appnope = "*" +backcall = "*" +colorama = "*" +decorator = "*" +jedi = ">=0.10" +pexpect = "*" +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<2.1.0" +pygments = "*" +setuptools = ">=18.5" +traitlets = ">=4.2" + +[[package]] +category = "dev" +description = "Vestigial utilities from IPython" +name = "ipython-genutils" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[[package]] +category = "dev" +description = "An autocompletion tool for Python that can be used for text editors." +name = "jedi" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.14.1" + +[package.dependencies] +parso = ">=0.5.0" + +[[package]] +category = "main" +description = "A small but fast and easy to use stand-alone template engine written in pure python." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.1" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[[package]] +category = "dev" +description = "A fast and thorough lazy object proxy." +name = "lazy-object-proxy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "Python LiveReload is an awesome tool for web developers" +name = "livereload" +optional = false +python-versions = "*" +version = "2.6.1" + +[package.dependencies] +six = "*" +tornado = "*" + +[[package]] +category = "dev" +description = "Python implementation of Markdown." +name = "markdown" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "3.1.1" + +[package.dependencies] +setuptools = ">=36" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "marshmallow" +optional = false +python-versions = "*" +version = "2.19.5" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "Project documentation with Markdown." +name = "mkdocs" +optional = false +python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.0.4" + +[package.dependencies] +Jinja2 = ">=2.7.1" +Markdown = ">=2.3.1" +PyYAML = ">=3.10" +click = ">=3.3" +livereload = ">=2.5.1" +tornado = ">=5.0" + +[[package]] +category = "dev" +description = "Generating type annotations from sampled production types" +name = "monkeytype" +optional = false +python-versions = ">=3.6" +version = "19.5.0" + +[package.dependencies] +retype = "*" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.4" +version = "7.2.0" + +[[package]] +category = "dev" +description = "A Python Parser" +name = "parso" +optional = false +python-versions = "*" +version = "0.5.1" + +[[package]] +category = "dev" +description = "Pexpect allows easy control of interactive console applications." +marker = "sys_platform != \"win32\"" +name = "pexpect" +optional = false +python-versions = "*" +version = "4.7.0" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +category = "dev" +description = "Tiny 'shelve'-like database with concurrency support" +name = "pickleshare" +optional = false +python-versions = "*" +version = "0.7.5" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.12.0" + +[package.dependencies] +importlib-metadata = ">=0.12" + +[[package]] +category = "dev" +description = "Library for building powerful interactive command lines in Python" +name = "prompt-toolkit" +optional = false +python-versions = "*" +version = "2.0.9" + +[package.dependencies] +six = ">=1.9.0" +wcwidth = "*" + +[[package]] +category = "dev" +description = "Run a subprocess in a pseudo terminal" +marker = "sys_platform != \"win32\"" +name = "ptyprocess" +optional = false +python-versions = "*" +version = "0.6.0" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "dev" +description = "Create Python API documentation in Markdown format" +name = "pydoc-markdown" +optional = false +python-versions = "*" +version = "2.0.5" + +[package.dependencies] +Markdown = ">=2.6.11" +MkDocs = ">=0.16.0" +PyYAML = ">=3.12" +six = ">=0.11.0" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.4.2" + +[[package]] +category = "dev" +description = "python code static checker" +name = "pylint" +optional = false +python-versions = ">=3.4.*" +version = "2.3.1" + +[package.dependencies] +astroid = ">=2.2.0,<3" +colorama = "*" +isort = ">=4.2.5,<5" +mccabe = ">=0.6,<0.7" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.10.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +pluggy = ">=0.7" +py = ">=1.5.0" +setuptools = "*" +six = ">=1.10.0" + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.7.1" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[[package]] +category = "dev" +description = "py.test plugin to create a 'tmpdir' containing predefined files/directories." +name = "pytest-datafiles" +optional = false +python-versions = "*" +version = "2.0" + +[package.dependencies] +py = "*" +pytest = ">=3.6" + +[[package]] +category = "dev" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.1.2" + +[[package]] +category = "dev" +description = "Re-apply types from .pyi stub files to your codebase." +name = "retype" +optional = false +python-versions = "*" +version = "17.12.0" + +[package.dependencies] +click = "*" +typed-ast = "*" + +[[package]] +category = "main" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +name = "ruamel.yaml" +optional = false +python-versions = "*" +version = "0.16.1" + +[package.dependencies] +[package.dependencies."ruamel.yaml.clib"] +python = "<3.8" +version = ">=0.1.2" + +[[package]] +category = "main" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +marker = "platform_python_implementation == \"CPython\" and python_version < \"3.8\"" +name = "ruamel.yaml.clib" +optional = false +python-versions = "*" +version = "0.1.2" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.12.0" + +[[package]] +category = "dev" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +name = "tornado" +optional = false +python-versions = ">= 3.5" +version = "6.0.3" + +[[package]] +category = "dev" +description = "Traitlets Python config system" +name = "traitlets" +optional = false +python-versions = "*" +version = "4.3.2" + +[package.dependencies] +decorator = "*" +ipython-genutils = "*" +six = "*" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "dev" +description = "Module for decorators, wrappers and monkey patching." +name = "wrapt" +optional = false +python-versions = "*" +version = "1.11.2" + +[[package]] +category = "dev" +description = "A formatter for Python code." +name = "yapf" +optional = false +python-versions = "*" +version = "0.28.0" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.5.2" + +[extras] +docs = [] + +[metadata] +content-hash = "e22c73191a4da9993606ee107a024cdfb23a4a67294465ee622ee2d2ee961feb" +python-versions = "^3.7" + +[metadata.hashes] +appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] +astroid = ["6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", "b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"] +atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] +attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] +backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] +click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] +coverage-badge = ["142fd121f3bd14956aff3c45bff6f8bc37bd74c6350626a950ebb6accb24276e", "3796de21b4e190d38beb8806956946fbdb02fe3a2a7452b460a9cff958009833"] +decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] +importlib-metadata = ["23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", "80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"] +ipython = ["1d3a1692921e932751bc1a1f7bb96dc38671eeefdc66ed33ee4cbc57e92a410e", "537cd0176ff6abd06ef3e23f2d0c4c2c8a4d9277b7451544c6cbf56d1c79a83d"] +ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] +isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"] +jedi = ["53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", "e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c"] +jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] +lazy-object-proxy = ["159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", "23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", "3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", "3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", "4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", "4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", "64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", "6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", "7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", "7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", "8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", "a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", "acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", "be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", "bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", "c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", "dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", "e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1"] +livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"] +markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"] +markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] +marshmallow = ["9cedfc5b6f568d57e8a2cf3d293fbd81b05e5ef557854008d03e25660a39ccfd", "a4d99922116a76e5abd8f997ec0519086e24814b7e1e1344bebe2a312ba50235"] +mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] +mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"] +monkeytype = ["67c8a5694fbc78b3c763eccca834bcbc0a7964969fa467f78e43a4141d650787", "adb86d4dd4760e80e9670179ab09b09b4e3765115e31f9f2840c1ef3ebc91167"] +more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] +parso = ["63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", "666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"] +pexpect = ["2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", "9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"] +pickleshare = ["87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", "9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"] +pluggy = ["0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", "b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"] +prompt-toolkit = ["11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", "2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", "977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"] +ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] +py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] +pydoc-markdown = ["f1511ad5a6ca33a52dd2010651d18e9debb57bcf36ab87eca560dd15a753ce1f"] +pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] +pylint = ["5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", "723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1"] +pytest = ["3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", "e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"] +pytest-cov = ["2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", "e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"] +pytest-datafiles = ["143329cbb1dbbb07af24f88fa4668e2f59ce233696cf12c49fd1c98d1756dbf9", "e349b6ad7bcca111f3677b7201d3ca81f93b5e09dcfae8ee2be2c3cae9f55bc7"] +pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"] +retype = ["33cfb36601bfeb355924731d8db78fa82f3f12eb37e87236e9179d81aba97740", "b64b767befbe6f5fd918603ab7d6bbff07fc4c431bae2f471e195677a0c9b327"] +"ruamel.yaml" = ["547aeab5c51c93bc750ed2a320c1559b605bde3aa569216aa75fd91d8a1c4623"] +"ruamel.yaml.clib" = ["0bbe19d3e099f8ba384e1846e6b54f245f58aeec8700edbbf9abb87afa54fd82", "2f38024592613f3a8772bbc2904be027d9abf463518ba145f2d0c8e6da27009f", "44449b3764a3f75815eea8ae5930b98e8326be64a90b0f782747318f861abfe0", "5710be9a357801c31c1eaa37b9bc92d38176d785af5b2f0c9751385c5dc9659a", "5a089acb6833ed5f412e24cbe3e665683064c1429824d2819137b5ade54435c3", "6143386ddd61599ea081c012a69a16e5bdd7b3c6c231bd039534365a48940f30", "6726aaf851f5f9e4cbdd3e1e414bc700bdd39220e8bc386415fd41c87b1b53c2", "68fbc3b5d94d145a391452f886ae5fca240cb7e3ab6bd66e1a721507cdaac28a", "75ebddf99ba9e0b48f32b5bdcf9e5a2b84c017da9e0db7bf11995fa414aa09cd", "79948a6712baa686773a43906728e20932c923f7b2a91be7347993be2d745e55", "8a2dd8e8b08d369558cade05731172c4b5e2f4c5097762c6b352bd28fd9f9dc4", "c747acdb5e8c242ab2280df6f0c239e62838af4bee647031d96b3db2f9cefc04", "cadc8eecd27414dca30366b2535cb5e3f3b47b4e2d6be7a0b13e4e52e459ff9f", "cee86ecc893a6a8ecaa7c6a9c2d06f75f614176210d78a5f155f8e78d6989509", "e59af39e895aff28ee5f55515983cab3466d1a029c91c04db29da1c0f09cf333", "eee7ecd2eee648884fae6c51ae50c814acdcc5d6340dc96c970158aebcd25ac6", "ef8d4522d231cb9b29f6cdd0edc8faac9d9715c60dc7becbd6eb82c915a98e5b", "f504d45230cc9abf2810623b924ae048b224a90adb01f97db4e766cfdda8e6eb"] +six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] +traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] +typed-ast = ["18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] +wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] +wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] +yapf = ["02ace10a00fa2e36c7ebd1df2ead91dbfbd7989686dc4ccbdc549e95d19f5780", "6f94b6a176a7c114cfa6bad86d40f259bbe0f10cf2fa7f2f4b3596fc5802a41b"] +zipp = ["4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", "8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7552553 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[tool.poetry] +name = "cenv_tool" +version = "1.0.0" +description = "conda environment creation and update from meta.yaml" +license = "MIT" +authors = ["Simon Kallfass "] +readme = "README.md" +homepage = "https://www.cenv.ouroboros.info" +keywords = ["conda", "environment", "dependencies"] + + +[tool.poetry.dependencies] +python = "^3.7" +attrs = "~19" +jinja2 = ">=2" +"ruamel.yaml" = ">=0.15" +six = ">=1.12" +marshmallow = ">=2.19,<3" + + +[tool.poetry.dev-dependencies] +coverage-badge = "~1" +ipython = ">=7" +mkdocs = "~1" +monkeytype = ">=19" +pydoc-markdown = "~2" +pylint = ">=2" +pytest = "^3.0" +pytest-cov = '~2' +pytest-datafiles = '~2' +yapf = ">=0" + + +[tool.poetry.scripts] +cenv = "cenv_tool.project:main" +init_cenv = "cenv_tool.init_cenv:main" + + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + + +[tool.poetry.extras] +docs = ["mkdocs", "pydoc-markdown"] + + +[tool.dephell.main] +# read from poetry format +from = {format = "poetry", path = "pyproject.toml"} +# drop dev-dependencies +envs = ["main"] +# and convert into setup.py +to = {format = "setuppy", path = "setup.py"} +versioning = "semver" + + +[tool.dephell.lock] +from = {format = "poetry", path = "pyproject.toml"} +to = {format = "poetrylock", path = "poetry.lock"} + + +[tool.dephell.docs] +# read dependencies from poetry format +from = {format = "poetry", path = "pyproject.toml"} +# install only `docs` extra dependencies +envs = ["docs"] +# run this command: +command = "mkdocs build" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..01fea58 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,61 @@ +[tool:pytest] +addopts = --cov=cenv_tool + --cov-report html + --cov-report term-missing:skip-covered + --cov-config=setup.cfg + + +[coverage:report] +exclude_lines = + pragma: no cover + def __repr__ + if __name__ == .__main__.: + def main + def script_options + +[pydocstyle] +convention = google + +[flake8] +max-line-length = 100 + +[yapf] +align_closing_bracket_with_visual_indent=False +allow_multiline_dictionary_keys=False +allow_multiline_lambdas=False +allow_split_before_default_or_named_assigns=True +allow_split_before_dict_value=True +arithmetic_precedence_indication=False +blank_lines_around_top_level_definition=2 +blank_line_before_class_docstring=False +blank_line_before_module_docstring=False +blank_line_before_nested_class_or_def=True +coalesce_brackets=False +column_limit=80 +continuation_align_style=SPACE +continuation_indent_width=4 +dedent_closing_brackets=True +disable_ending_comma_heuristic=False +each_dict_entry_on_separate_line=True +i18n_comment=#\..* +i18n_function_call=N_, _ +indent_blank_lines=False +indent_dictionary_value=False +indent_width=4 +join_multiple_lines=True +spaces_around_default_or_named_assign=False +spaces_around_power_operator=False +spaces_before_comment=2 +space_between_ending_comma_and_closing_bracket=False +split_all_comma_separated_values=False +split_arguments_when_comma_terminated=False +split_before_arithmetic_operator=False +split_before_bitwise_operator=False +split_before_closing_bracket=True +split_before_dict_set_generator=False +split_before_dot=False +split_before_expression_after_opening_paren=False +split_before_first_argument=False +split_before_logical_operator=False +split_before_named_assigns=True +split_complex_comprehension=True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..467effa --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# DO NOT EDIT THIS FILE! +# This file has been autogenerated by dephell <3 +# https://github.com/dephell/dephell + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +import os.path + +readme = '' +here = os.path.abspath(os.path.dirname(__file__)) +readme_path = os.path.join(here, 'README.rst') +if os.path.exists(readme_path): + with open(readme_path, 'rb') as stream: + readme = stream.read().decode('utf8') + +setup( + long_description=readme, + name='cenv_tool', + version='1.0.0', + description='conda environment creation and update from meta.yaml', + python_requires='==3.*,>=3.7.0', + project_urls={'homepage': 'https://www.cenv.ouroboros.info'}, + author='Simon Kallfass', + author_email='skallfass@ouroboros.info', + license='MIT', + keywords='conda environment dependencies', + entry_points={ + 'console_scripts': [ + 'cenv = cenv_tool.project:main', + 'init_cenv = cenv_tool.init_cenv:main' + ] + }, + packages=['cenv_tool'], + package_data={'cenv_tool': ['*.sh', '*.yml']}, + install_requires=[ + 'attrs==19.*,>=19.0.0', 'jinja2>=2', 'marshmallow<3,>=2.19', + 'ruamel.yaml>=0.15', 'six>=1.12' + ], + extras_require={ + 'dev': [ + 'coverage-badge==1.*,>=1.0.0', 'ipython>=7', 'mkdocs==1.*,>=1.0.0', + 'monkeytype>=19', 'pydoc-markdown==2.*,>=2.0.0', 'pylint>=2', + 'pytest==3.*,>=3.0.0', 'pytest-cov==2.*,>=2.0.0', + 'pytest-datafiles==2.*,>=2.0.0', 'yapf>=0' + ], + 'docs': ['mkdocs==1.*,>=1.0.0', 'pydoc-markdown==2.*,>=2.0.0'] + }, +) diff --git a/tests/.condarc b/tests/.condarc new file mode 100644 index 0000000..e839621 --- /dev/null +++ b/tests/.condarc @@ -0,0 +1,17 @@ +allow_other_channels: True +show_channel_urls: True + +conda-build: + output-folder: /opt/conda/conda-bld + +pkgs_dirs: + - /shared/conda/pkgs + +envs_dirs: + - /shared/conda/envs + +channels: + - defaults + - local + - https://repository-channel.ouroboros.info + - conda-forge diff --git a/tests/home_test/.bashrc b/tests/home_test/.bashrc new file mode 100644 index 0000000..2ef267e --- /dev/null +++ b/tests/home_test/.bashrc @@ -0,0 +1 @@ +some content diff --git a/tests/home_test/.zshrc b/tests/home_test/.zshrc new file mode 100644 index 0000000..2ef267e --- /dev/null +++ b/tests/home_test/.zshrc @@ -0,0 +1 @@ +some content diff --git a/tests/init_cenv_test.py b/tests/init_cenv_test.py new file mode 100644 index 0000000..d3072be --- /dev/null +++ b/tests/init_cenv_test.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import pytest + +from cenv_tool.init_cenv import initialize_cenv +from cenv_tool.init_cenv import RC_CONTENT + + +@pytest.mark.datafiles('tests/home_test') +def test_initialize_cenv(datafiles): + expected_result = 'some content\n' + RC_CONTENT + config_path = Path(datafiles) / '.config/cenv' + config_file = config_path / 'cenv.yml' + autoenv_script_path = config_path / 'cenv.sh' + zshrc = Path(datafiles) / '.zshrc' + bashrc = Path(datafiles) / '.bashrc' + for _ in range(2): + initialize_cenv( + config_path=config_path, + autoenv_script_path=autoenv_script_path, + autoenv_script_source_path=Path('cenv_tool/cenv.sh'), + config_file=config_file, + config_file_source=Path('cenv_tool/cenv.yml'), + zshrc=zshrc, + bashrc=bashrc, + ) + assert expected_result == zshrc.read_text() + assert expected_result == bashrc.read_text() + assert autoenv_script_path.exists() + assert config_file.exists() + assert config_path.exists() diff --git a/tests/invalid_testproject/conda-build/meta.yaml b/tests/invalid_testproject/conda-build/meta.yaml new file mode 100644 index 0000000..dc634d0 --- /dev/null +++ b/tests/invalid_testproject/conda-build/meta.yaml @@ -0,0 +1,43 @@ +{% set data = load_setup_py_data() %} + +package: + name: "testproject" + version: {{ data.get("version") }} + +source: + path: .. + +build: + build: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + preserve_egg_dir: True + script: python -m pip install --no-deps --ignore-installed . + entry_points: + - testproject = testproject.testproject:main + +requirements: + build: + - python 3.7.3 + - pip + - setuptools + run: + - python 3.7.3 + - attrs >=19 + - jinja2 >=2.10 + - ruamel.yaml >=0.15.23 + - six >=1.12.0 + - yaml >=0.1.7 + - marshmallow >=2,<3 + +test: + imports: + - testproject + - testproject.testproject + commands: + - testproject --help + - testproject -v + +extra: + dev_requirements: + - ipython >=7.2.0 + - mkdocs >=1.0.4 + - pylint >=2.2.2 diff --git a/tests/project_test.py b/tests/project_test.py new file mode 100644 index 0000000..91e372f --- /dev/null +++ b/tests/project_test.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import os +from pathlib import Path + +import pytest + +from cenv_tool.project import Project +from cenv_tool.rules import RULES + + +def test_project_collect_available_envs(): + current_path = Path.cwd() + testfolder = Path('tests/testproject') + os.chdir(str(testfolder)) + project = Project(rules=RULES) + os.chdir(str(current_path)) + available_envs = project.collect_available_envs() + print(available_envs) + + +@pytest.mark.datafiles('tests/testproject') +def test_project_update(datafiles): + created_env = Path('/shared/conda/envs/cenv_testing_project0001') + environment_yml = Path(datafiles) / 'conda-build/environment.yml' + current_folder = Path.cwd() + os.chdir(datafiles) + project = Project(rules=RULES) + project.update() + assert created_env.exists() + project = Project(rules=RULES) + project.update() + assert created_env.exists() + project = Project(rules=RULES) + project.remove_previous_environment() + project.remove_backup_environment() + project.create_environment(cloned=False) + project.export_environment_definition() + assert environment_yml.exists() + environment_yml.unlink() + project.remove_previous_environment() + project.remove_backup_environment() + os.chdir(str(current_folder)) diff --git a/tests/rules_test.py b/tests/rules_test.py new file mode 100644 index 0000000..25ab753 --- /dev/null +++ b/tests/rules_test.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Test the rules module of cenv.""" +from cenv_tool.rules import RULES + + +def test_rules(): + """Test if RULES contain attributes as expected.""" + assert RULES.git_folder + assert RULES.conda_cmds + assert RULES.conda_cmds.remove + assert RULES.conda_cmds.export + assert RULES.conda_cmds.create + assert RULES.conda_cmds.clone + assert RULES.conda_cmds.restore + assert RULES.conda_cmds.clean diff --git a/tests/testproject/conda-build/meta.yaml b/tests/testproject/conda-build/meta.yaml new file mode 100644 index 0000000..966901d --- /dev/null +++ b/tests/testproject/conda-build/meta.yaml @@ -0,0 +1,39 @@ +{% set data = load_setup_py_data() %} + +package: + name: "cenv_testing_project0001" + version: {{ data.get("version") }} + +source: + path: .. + +build: + build: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + preserve_egg_dir: True + script: python -m pip install --no-deps --ignore-installed . + entry_points: + - testproject = testproject.testproject:main + +requirements: + build: + - python 3.7.3 + - pip + - setuptools + run: + - python 3.7.3 + - attrs >=19 + - jinja2 >=2.10 + - six >=1.12.0 + - yaml >=0.1.7 + +test: + imports: + - cenv_testing_project0001 + commands: + - cenv_testing_project0001 --help + - cenv_testing_project0001 -v + +extra: + env_name: cenv_testing_project0001 + dev_requirements: + - pylint >=2.2.2 diff --git a/tests/utils_test.py b/tests/utils_test.py new file mode 100644 index 0000000..8931748 --- /dev/null +++ b/tests/utils_test.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +"""Test the utils module of cenv.""" +from pathlib import Path + +import pytest +from marshmallow import ValidationError + +from cenv_tool.utils import CenvProcessError +from cenv_tool.utils import read_meta_yaml +from cenv_tool.utils import run_in_bash + + +@pytest.mark.parametrize( + 'meta_yaml_path, expected_meta_yaml_content, expected_dependencies', + [ + ( + Path('tests/testproject'), + { + 'requirements': { + 'build': ['python 3.7.3', 'pip', 'setuptools'], + 'run': [ + 'python 3.7.3', + 'attrs >=19', + 'jinja2 >=2.10', + 'six >=1.12.0', + 'yaml >=0.1.7', + 'pylint >=2.2.2', + ], + }, + 'extra': { + 'dev_requirements': ['pylint >=2.2.2'], + 'env_name': 'cenv_testing_project0001', + }, + 'source': { + 'path': '..', + }, + 'test': { + 'commands': [ + 'cenv_testing_project0001 --help', + 'cenv_testing_project0001 -v', + ], + 'imports': ['cenv_testing_project0001'], + }, + 'build': { + 'build': '0', + 'preserve_egg_dir': 'True', + 'script': + 'python -m pip install --no-deps --ignore-installed .', + 'entry_points': + ['testproject = testproject.testproject:main'], + }, + 'package': { + 'version': 'None', + 'name': 'cenv_testing_project0001', + }, + }, + [ + 'python 3.7.3', + 'attrs >=19', + 'jinja2 >=2.10', + 'six >=1.12.0', + 'yaml >=0.1.7', + 'pylint >=2.2.2', + ], + ), + ], +) +def test_read_meta_yaml( + meta_yaml_path, + expected_meta_yaml_content, + expected_dependencies, +): + """Test if the read_meta_yaml function works as expected.""" + meta_yaml_content, dependencies = read_meta_yaml(path=meta_yaml_path) + assert meta_yaml_content == expected_meta_yaml_content + assert expected_dependencies == dependencies + + +@pytest.mark.parametrize('meta_yaml_path', [Path('tests/invalid_testproject')]) +def test_read_meta_yaml_fails(meta_yaml_path): + """Test if read_meta_yaml function fails on invalid meta.yaml.""" + with pytest.raises(ValidationError): + read_meta_yaml(path=meta_yaml_path) + + +def test_run_in_bash(): + """Test if run_in_bash works as expected.""" + cmd_result = run_in_bash(cmd='ls tests/testproject/conda-build') + assert cmd_result == 'meta.yaml' + + +def test_run_in_bash_fails(): + """Test if run_in_bash works as expected.""" + with pytest.raises(CenvProcessError): + run_in_bash(cmd='ls-a tests/testproject/conda-build')