├── tests ├── __init__.py ├── specs │ ├── __init__.py │ ├── test_notebook.py │ ├── test_requirements.py │ ├── test_yaml_file.py │ ├── test_base.py │ └── test_binstar.py ├── installers │ ├── __init__.py │ └── test_pip.py ├── support │ ├── requirements.txt │ ├── simple.yml │ ├── foo │ │ ├── environment.yml │ │ └── bar │ │ │ ├── readme │ │ │ └── baz │ │ │ └── readme │ ├── example │ │ └── environment.yml │ ├── example-yaml │ │ └── environment.yaml │ ├── saved-env │ │ └── environment.yml │ ├── with-pip.yml │ ├── notebook.ipynb │ └── notebook_with_env.ipynb ├── utils │ ├── __init__.py │ ├── test_uploader.py │ └── test_notebooks.py ├── cli.py └── test_env.py ├── conda_env ├── __init__.py ├── cli │ ├── __init__.py │ ├── main_list.py │ ├── main_remove.py │ ├── main.py │ ├── main_attach.py │ ├── main_export.py │ ├── main_upload.py │ ├── main_create.py │ └── main_update.py ├── utils │ ├── __init__.py │ ├── notebooks.py │ └── uploader.py ├── installers │ ├── __init__.py │ ├── base.py │ ├── pip.py │ └── conda.py ├── compat.py ├── specs │ ├── __init__.py │ ├── yaml_file.py │ ├── notebook.py │ ├── requirements.py │ └── binstar.py ├── yaml.py ├── exceptions.py ├── pip_util.py └── env.py ├── MANIFEST.in ├── conda.recipe ├── bld.bat ├── build.sh └── meta.yaml ├── bin └── conda-env ├── .gitignore ├── runtests.sh ├── PULL_REQUEST_TEMPLATE.md ├── .binstar.yml ├── .travis.yml ├── setup.py ├── LICENSE.txt ├── CHANGELOG.md ├── README.rst └── CONTRIBUTING.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conda_env/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/specs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conda_env/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conda_env/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/installers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conda_env/installers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py README.rst 2 | -------------------------------------------------------------------------------- /tests/support/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.10.1 2 | -------------------------------------------------------------------------------- /tests/support/simple.yml: -------------------------------------------------------------------------------- 1 | name: nlp 2 | dependencies: 3 | - nltk 4 | -------------------------------------------------------------------------------- /conda.recipe/bld.bat: -------------------------------------------------------------------------------- 1 | "%PYTHON%" setup.py install 2 | if errorlevel 1 exit 1 3 | -------------------------------------------------------------------------------- /tests/support/foo/environment.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | dependencies: 3 | - numpy 4 | -------------------------------------------------------------------------------- /tests/support/example/environment.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | dependencies: 3 | - numpy 4 | -------------------------------------------------------------------------------- /tests/support/example-yaml/environment.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | dependencies: 3 | - numpy 4 | -------------------------------------------------------------------------------- /tests/support/saved-env/environment.yml: -------------------------------------------------------------------------------- 1 | name: to-save 2 | dependencies: 3 | - python 4 | -------------------------------------------------------------------------------- /tests/support/with-pip.yml: -------------------------------------------------------------------------------- 1 | name: pip 2 | dependencies: 3 | - pip 4 | - pip: 5 | - foo 6 | - baz 7 | -------------------------------------------------------------------------------- /bin/conda-env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | from conda_env.cli.main import main 4 | 5 | sys.exit(main()) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.egg-info 4 | build/ 5 | dist/ 6 | docs/build 7 | MANIFEST 8 | __pycache__ 9 | .idea/ 10 | .cache/ -------------------------------------------------------------------------------- /tests/support/notebook.ipynb: -------------------------------------------------------------------------------- 1 | {"worksheets": [{"cells": []}], "nbformat_minor": 0, "metadata": {"signature": "", "name": ""}, "nbformat": 3} -------------------------------------------------------------------------------- /tests/support/foo/bar/readme: -------------------------------------------------------------------------------- 1 | This is here for testing purposes to verify that load_from_default() 2 | can scan parent directories properly. 3 | -------------------------------------------------------------------------------- /tests/support/foo/bar/baz/readme: -------------------------------------------------------------------------------- 1 | This is here for testing purposes to verify that load_from_default() 2 | can scan parent directories properly. 3 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def support_file(filename): 5 | return os.path.join(os.path.dirname(__file__), '../support', filename) 6 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Install nose, nose-progressive, and watchdog 3 | watchmedo shell-command -R -p "*.py" \ 4 | -c "nosetests --with-progressive" 5 | -------------------------------------------------------------------------------- /conda.recipe/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove the symlinked versions of activate/deactivate 4 | rm $PREFIX/bin/activate 5 | rm $PREFIX/bin/deactivate 6 | 7 | $PYTHON setup.py install 8 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This repository is now deprecated. The conda-env code now is versioned with and rides alongside conda proper. Please file pull requests at https://github.com/conda/conda/pulls. 2 | -------------------------------------------------------------------------------- /tests/support/notebook_with_env.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [], 3 | "metadata": { 4 | "environment": { 5 | "dependencies": [ 6 | "ipython-notebook=3.2.0=py27_0" 7 | ], 8 | "name": "stats" 9 | } 10 | }, 11 | "nbformat": 4, 12 | "nbformat_minor": 0 13 | } 14 | -------------------------------------------------------------------------------- /conda_env/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | PY3 = sys.version_info[0] == 3 4 | 5 | 6 | def u(some_str): 7 | if PY3: 8 | return some_str 9 | else: 10 | return unicode(some_str) 11 | 12 | 13 | def b(some_str, encoding="utf-8"): 14 | try: 15 | return bytes(some_str, encoding=encoding) 16 | except TypeError: 17 | return some_str 18 | -------------------------------------------------------------------------------- /conda_env/installers/base.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | ENTRY_POINT = 'conda_env.installers' 3 | 4 | 5 | class InvalidInstaller(Exception): 6 | def __init__(self, name): 7 | msg = 'Unable to load installer for {}'.format(name) 8 | super(InvalidInstaller, self).__init__(msg) 9 | 10 | 11 | def get_installer(name): 12 | try: 13 | return importlib.import_module(ENTRY_POINT + '.' + name) 14 | except ImportError: 15 | raise InvalidInstaller(name) 16 | -------------------------------------------------------------------------------- /conda_env/installers/pip.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import subprocess 3 | 4 | from conda.cli import common 5 | from conda_env.pip_util import pip_args 6 | 7 | 8 | def install(prefix, specs, args, env, prune=False): 9 | pip_cmd = pip_args(prefix) + ['install', ] + specs 10 | process = subprocess.Popen(pip_cmd, universal_newlines=True) 11 | process.communicate() 12 | 13 | if process.returncode != 0: 14 | common.exception_and_exit(ValueError("pip returned an error.")) 15 | -------------------------------------------------------------------------------- /.binstar.yml: -------------------------------------------------------------------------------- 1 | package: conda-env 2 | user: conda 3 | 4 | platform: 5 | - linux-32 6 | - linux-64 7 | - win-32 8 | - win-64 9 | - osx-64 10 | 11 | engine: 12 | - python=2 13 | - python=3.3 14 | - python=3.4 15 | # env: 16 | # - CONDA_PY=27 17 | # - CONDA_PY=33 18 | # - CONDA_PY=34 19 | 20 | # Can't test on binstar because it needs to be installed in root 21 | script: 22 | - conda build --no-test ./conda.recipe/ 23 | 24 | install: 25 | - conda install --name root jinja2 26 | - conda update --name root conda-build --yes 27 | 28 | build_targets: conda 29 | -------------------------------------------------------------------------------- /conda_env/specs/__init__.py: -------------------------------------------------------------------------------- 1 | from .binstar import BinstarSpec 2 | from .yaml_file import YamlFileSpec 3 | from .notebook import NotebookSpec 4 | from .requirements import RequirementsSpec 5 | from ..exceptions import SpecNotFound 6 | 7 | all_specs = [ 8 | BinstarSpec, 9 | NotebookSpec, 10 | YamlFileSpec, 11 | RequirementsSpec 12 | ] 13 | 14 | 15 | def detect(**kwargs): 16 | specs = [] 17 | for SpecClass in all_specs: 18 | spec = SpecClass(**kwargs) 19 | specs.append(spec) 20 | if spec.can_handle(): 21 | return spec 22 | 23 | raise SpecNotFound(build_message(specs)) 24 | 25 | 26 | def build_message(specs): 27 | return "\n".join([s.msg for s in specs if s.msg is not None]) 28 | -------------------------------------------------------------------------------- /tests/specs/test_notebook.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from conda_env import env 3 | from conda_env.specs.notebook import NotebookSpec 4 | from ..utils import support_file 5 | 6 | 7 | class TestNotebookSpec(unittest.TestCase): 8 | def test_no_notebook_file(self): 9 | spec = NotebookSpec(support_file('simple.yml')) 10 | self.assertEqual(spec.can_handle(), False) 11 | 12 | def test_notebook_no_env(self): 13 | spec = NotebookSpec(support_file('notebook.ipynb')) 14 | self.assertEqual(spec.can_handle(), False) 15 | 16 | def test_notebook_with_env(self): 17 | spec = NotebookSpec(support_file('notebook_with_env.ipynb')) 18 | self.assertTrue(spec.can_handle()) 19 | self.assertIsInstance(spec.environment, env.Environment) 20 | -------------------------------------------------------------------------------- /conda_env/yaml.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper around yaml to ensure that everything is ordered correctly. 3 | 4 | This is based on the answer at http://stackoverflow.com/a/16782282 5 | """ 6 | from __future__ import absolute_import, print_function 7 | from collections import OrderedDict 8 | import yaml 9 | 10 | 11 | def represent_ordereddict(dumper, data): 12 | value = [] 13 | 14 | for item_key, item_value in data.items(): 15 | node_key = dumper.represent_data(item_key) 16 | node_value = dumper.represent_data(item_value) 17 | 18 | value.append((node_key, node_value)) 19 | 20 | return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', value) 21 | 22 | yaml.add_representer(OrderedDict, represent_ordereddict) 23 | 24 | dump = yaml.dump 25 | load = yaml.load 26 | dict = OrderedDict 27 | -------------------------------------------------------------------------------- /conda.recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: conda-env 3 | version: 2.5.0alpha 4 | 5 | build: 6 | number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} 7 | {% if environ.get('GIT_DESCRIBE_NUMBER', '0') == '0' %}string: py{{ environ.get('PY_VER').replace('.', '') }}_0 8 | {% else %}string: py{{ environ.get('PY_VER').replace('.', '') }}_{{ environ.get('GIT_BUILD_STR', 'GIT_STUB') }}{% endif %} 9 | preserve_egg_dir: yes 10 | 11 | source: 12 | git_url: ../ 13 | 14 | requirements: 15 | build: 16 | - python 17 | run: 18 | - python 19 | 20 | test: 21 | commands: 22 | - conda env 23 | - conda env -h 24 | - conda env list -h 25 | - conda env create -h 26 | - conda env export -h 27 | - conda env remove -h 28 | 29 | about: 30 | home: https://github.com/conda/conda-env/ 31 | -------------------------------------------------------------------------------- /conda_env/cli/main_list.py: -------------------------------------------------------------------------------- 1 | from argparse import RawDescriptionHelpFormatter 2 | 3 | from conda.cli import common 4 | 5 | description = """ 6 | List the Conda environments 7 | """ 8 | 9 | example = """ 10 | examples: 11 | conda env list 12 | conda env list --json 13 | """ 14 | 15 | 16 | def configure_parser(sub_parsers): 17 | l = sub_parsers.add_parser( 18 | 'list', 19 | formatter_class=RawDescriptionHelpFormatter, 20 | description=description, 21 | help=description, 22 | epilog=example, 23 | ) 24 | 25 | common.add_parser_json(l) 26 | 27 | l.set_defaults(func=execute) 28 | 29 | 30 | def execute(args, parser): 31 | info_dict = {'envs': []} 32 | common.handle_envs_list(info_dict['envs'], not args.json) 33 | 34 | if args.json: 35 | common.stdout_json(info_dict) 36 | -------------------------------------------------------------------------------- /conda_env/specs/yaml_file.py: -------------------------------------------------------------------------------- 1 | from .. import env 2 | from ..exceptions import EnvironmentFileNotFound 3 | 4 | 5 | class YamlFileSpec(object): 6 | _environment = None 7 | 8 | def __init__(self, filename=None, **kwargs): 9 | self.filename = filename 10 | self.msg = None 11 | 12 | def can_handle(self): 13 | try: 14 | self._environment = env.from_file(self.filename) 15 | return True 16 | except EnvironmentFileNotFound as e: 17 | self.msg = str(e) 18 | return False 19 | except TypeError: 20 | self.msg = "{} is not a valid yaml file.".format(self.filename) 21 | return False 22 | 23 | @property 24 | def environment(self): 25 | if not self._environment: 26 | self.can_handle() 27 | return self._environment 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.4' 5 | install: 6 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh 7 | -O miniconda.sh; else wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh 8 | -O miniconda.sh; fi 9 | - bash miniconda.sh -b -p $HOME/miniconda 10 | - export PATH="$HOME/miniconda/bin:$PATH" 11 | - hash -r 12 | - conda config --set always_yes yes 13 | - conda --version 14 | - conda install anaconda-client nbformat mock nose 15 | - if [[ "$PYCOSAT" ]]; then conda install pycosat=$PYCOSAT; fi 16 | - python setup.py install 17 | script: 18 | - nosetests 19 | - conda env --help 20 | sudo: false 21 | notifications: 22 | email: false 23 | flowdock: 24 | secure: QH1eDdrLEHrRQnBNNqITB1XoEiz0Dey0LzL1VZl06KJINRoOvDiM4uZU6pC7odU7r+Bwxv0YTqY6tZZDQSDwzJpLqyxX7KJwNwIgU13sD7NLGdh0Ll8o8fZOS0sIu6xsFKh7X0PPTTGV0FCYUrzft0drM8jjPCRV4Ayn5wlGc/g= 25 | -------------------------------------------------------------------------------- /tests/specs/test_requirements.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .. import utils 4 | 5 | from conda_env import env 6 | from conda_env.specs.requirements import RequirementsSpec 7 | 8 | 9 | class TestRequiremets(unittest.TestCase): 10 | def test_no_environment_file(self): 11 | spec = RequirementsSpec(name=None, filename='not-a-file') 12 | self.assertEqual(spec.can_handle(), False) 13 | 14 | def test_no_name(self): 15 | spec = RequirementsSpec(filename=utils.support_file('requirements.txt')) 16 | self.assertEqual(spec.can_handle(), False) 17 | 18 | def test_req_file_and_name(self): 19 | spec = RequirementsSpec(filename=utils.support_file('requirements.txt'), name='env') 20 | self.assertTrue(spec.can_handle()) 21 | 22 | def test_environment(self): 23 | spec = RequirementsSpec(filename=utils.support_file('requirements.txt'), name='env') 24 | self.assertIsInstance(spec.environment, env.Environment) 25 | self.assertEqual( 26 | spec.environment.dependencies['conda'][0], 27 | 'flask ==0.10.1' 28 | ) 29 | -------------------------------------------------------------------------------- /conda_env/specs/notebook.py: -------------------------------------------------------------------------------- 1 | try: 2 | import nbformat 3 | except ImportError: 4 | nbformat = None 5 | from ..env import Environment 6 | from .binstar import BinstarSpec 7 | 8 | 9 | class NotebookSpec(object): 10 | msg = None 11 | 12 | def __init__(self, name=None, **kwargs): 13 | self.name = name 14 | self.nb = {} 15 | 16 | def can_handle(self): 17 | try: 18 | self.nb = nbformat.reader.reads(open(self.name).read()) 19 | return 'environment' in self.nb['metadata'] 20 | except AttributeError: 21 | self.msg = "Please install nbformat:\n\tconda install nbformat" 22 | except IOError: 23 | self.msg = "{} does not exist or can't be accessed".format(self.name) 24 | except (nbformat.reader.NotJSONError, KeyError): 25 | self.msg = "{} does not looks like a notebook file".format(self.name) 26 | except: 27 | return False 28 | return False 29 | 30 | @property 31 | def environment(self): 32 | if 'remote' in self.nb['metadata']['environment']: 33 | spec = BinstarSpec('darth/deathstar') 34 | return spec.environment 35 | else: 36 | return Environment(**self.nb['metadata']['environment']) 37 | -------------------------------------------------------------------------------- /tests/installers/test_pip.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | try: 3 | from unittest import mock 4 | except ImportError: 5 | import mock 6 | 7 | from conda_env.installers import pip 8 | 9 | 10 | class PipInstallerTest(unittest.TestCase): 11 | def test_straight_install(self): 12 | with mock.patch.object(pip.subprocess, 'Popen') as popen: 13 | popen.return_value.returncode = 0 14 | with mock.patch.object(pip, 'pip_args') as pip_args: 15 | pip_args.return_value = ['pip'] 16 | 17 | pip.install('/some/prefix', ['foo'], '', '') 18 | 19 | popen.assert_called_with(['pip', 'install', 'foo'], 20 | universal_newlines=True) 21 | self.assertEqual(1, popen.return_value.communicate.call_count) 22 | 23 | def test_stops_on_exception(self): 24 | with mock.patch.object(pip.subprocess, 'Popen') as popen: 25 | popen.return_value.returncode = 22 26 | with mock.patch.object(pip, 'pip_args') as pip_args: 27 | # make sure that installed doesn't bail early 28 | pip_args.return_value = ['pip'] 29 | 30 | self.assertRaises(SystemExit, pip.install, 31 | '/some/prefix', ['foo'], '', '') 32 | -------------------------------------------------------------------------------- /tests/specs/test_yaml_file.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | try: 4 | from unittest import mock 5 | except ImportError: 6 | import mock 7 | 8 | from conda_env import env 9 | from conda_env.specs.yaml_file import YamlFileSpec 10 | 11 | 12 | class TestYAMLFile(unittest.TestCase): 13 | def test_no_environment_file(self): 14 | spec = YamlFileSpec(name=None, filename='not-a-file') 15 | self.assertEqual(spec.can_handle(), False) 16 | 17 | def test_environment_file_exist(self): 18 | with mock.patch.object(env, 'from_file', return_value={}): 19 | spec = YamlFileSpec(name=None, filename='environment.yaml') 20 | self.assertTrue(spec.can_handle()) 21 | 22 | def test_get_environment(self): 23 | r = random.randint(100, 200) 24 | with mock.patch.object(env, 'from_file', return_value=r): 25 | spec = YamlFileSpec(name=None, filename='environment.yaml') 26 | self.assertEqual(spec.environment, r) 27 | 28 | def test_filename(self): 29 | filename = "filename_{}".format(random.randint(100, 200)) 30 | with mock.patch.object(env, 'from_file') as from_file: 31 | spec = YamlFileSpec(filename=filename) 32 | spec.environment 33 | from_file.assert_called_with(filename) 34 | -------------------------------------------------------------------------------- /conda_env/installers/conda.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from conda.cli import common 4 | from conda import plan 5 | 6 | 7 | def install(prefix, specs, args, env, prune=False): 8 | # TODO: do we need this? 9 | common.check_specs(prefix, specs, json=args.json) 10 | 11 | # TODO: support all various ways this happens 12 | # Including 'nodefaults' in the channels list disables the defaults 13 | index = common.get_index_trap(channel_urls=[chan for chan in env.channels 14 | if chan != 'nodefaults'], 15 | prepend='nodefaults' not in env.channels) 16 | actions = plan.install_actions(prefix, index, specs, prune=prune) 17 | 18 | with common.json_progress_bars(json=args.json and not args.quiet): 19 | try: 20 | plan.execute_actions(actions, index, verbose=not args.quiet) 21 | except RuntimeError as e: 22 | if len(e.args) > 0 and "LOCKERROR" in e.args[0]: 23 | error_type = "AlreadyLocked" 24 | else: 25 | error_type = "RuntimeError" 26 | common.exception_and_exit(e, error_type=error_type, json=args.json) 27 | except SystemExit as e: 28 | common.exception_and_exit(e, json=args.json) 29 | -------------------------------------------------------------------------------- /conda_env/specs/requirements.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .. import env 4 | 5 | 6 | class RequirementsSpec(object): 7 | ''' 8 | Reads depedencies from a requirements.txt file 9 | and returns an Environment object from it. 10 | ''' 11 | msg = None 12 | 13 | def __init__(self, filename=None, name=None, **kwargs): 14 | self.filename = filename 15 | self.name = name 16 | self.msg = None 17 | 18 | def _valid_file(self): 19 | if os.path.exists(self.filename): 20 | return True 21 | else: 22 | self.msg = "There is no requirements.txt" 23 | return False 24 | 25 | def _valid_name(self): 26 | if self.name is None: 27 | self.msg = "Environment with requierements.txt file needs a name" 28 | return False 29 | else: 30 | return True 31 | 32 | def can_handle(self): 33 | return self._valid_file() and self._valid_name() 34 | 35 | @property 36 | def environment(self): 37 | dependencies = [] 38 | with open(self.filename) as reqfile: 39 | for line in reqfile: 40 | dependencies.append(line) 41 | return env.Environment( 42 | name=self.name, 43 | dependencies=dependencies 44 | ) 45 | -------------------------------------------------------------------------------- /tests/utils/test_uploader.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | try: 3 | from unittest import mock 4 | except ImportError: 5 | import mock 6 | from binstar_client import errors 7 | from conda_env.utils.uploader import Uploader 8 | 9 | 10 | class UploaderTestCase(unittest.TestCase): 11 | def test_unauthorized(self): 12 | uploader = Uploader('package', 'filename') 13 | with mock.patch.object(uploader.binstar, 'user') as get_user_mock: 14 | get_user_mock.side_effect = errors.Unauthorized 15 | self.assertEqual(uploader.authorized(), False) 16 | 17 | def test_authorized(self): 18 | uploader = Uploader('package', 'filename') 19 | with mock.patch.object(uploader.binstar, 'user') as get_user_mock: 20 | get_user_mock.return_value = {} 21 | self.assertEqual(uploader.authorized(), True) 22 | 23 | def test_package_already_exist(self): 24 | uploader = Uploader('package', 'filename') 25 | with mock.patch.object(uploader.binstar, 'user') as user_mock: 26 | user_mock.return_value = {'login': 'user'} 27 | with mock.patch.object(uploader.binstar, 'distribution') as distribution_mock: 28 | distribution_mock.return_value = True 29 | self.assertEqual(uploader.ensure_distribution(), False) 30 | -------------------------------------------------------------------------------- /conda_env/cli/main_remove.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | from argparse import RawDescriptionHelpFormatter, Namespace 3 | 4 | from conda.cli import common 5 | 6 | _help = "Remove an environment" 7 | _description = _help + """ 8 | 9 | Removes a provided environment. You must deactivate the existing 10 | environment before you can remove it. 11 | """.lstrip() 12 | 13 | _example = """ 14 | 15 | Examples: 16 | 17 | conda env remove --name FOO 18 | conda env remove -n FOO 19 | """ 20 | 21 | 22 | def configure_parser(sub_parsers): 23 | p = sub_parsers.add_parser( 24 | 'remove', 25 | formatter_class=RawDescriptionHelpFormatter, 26 | description=_description, 27 | help=_help, 28 | epilog=_example, 29 | ) 30 | 31 | common.add_parser_prefix(p) 32 | common.add_parser_json(p) 33 | common.add_parser_quiet(p) 34 | common.add_parser_yes(p) 35 | 36 | p.set_defaults(func=execute) 37 | 38 | 39 | def execute(args, parser): 40 | import conda.cli.main_remove 41 | args = vars(args) 42 | args.update({ 43 | 'all': True, 'channel': None, 'features': None, 44 | 'override_channels': None, 'use_local': None, 'use_cache': None, 45 | 'offline': None, 'force': None, 'pinned': None}) 46 | conda.cli.main_remove.execute(Namespace(**args), parser) 47 | -------------------------------------------------------------------------------- /tests/utils/test_notebooks.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from conda_env.utils.notebooks import Notebook 4 | from ..utils import support_file 5 | 6 | notebook = { 7 | "metadata": { 8 | "name": "", 9 | "signature": "" 10 | }, 11 | "nbformat": 3, 12 | "nbformat_minor": 0, 13 | "worksheets": [ 14 | { 15 | "cells": [] 16 | } 17 | ] 18 | } 19 | 20 | 21 | class NotebookTestCase(unittest.TestCase): 22 | def test_notebook_not_exist(self): 23 | nb = Notebook('no-exist.ipynb') 24 | self.assertEqual(nb.inject('content'), False) 25 | self.assertEqual(nb.msg, "no-exist.ipynb may not exist or you don't have adequate permissions") 26 | 27 | def test_environment_already_exist(self): 28 | nb = Notebook(support_file('notebook-with-env.ipynb')) 29 | self.assertEqual(nb.inject('content'), False) 30 | 31 | def test_inject_env(self): 32 | nb = Notebook(support_file('notebook.ipynb')) 33 | self.assertTrue(nb.inject('content')) 34 | 35 | with open(support_file('notebook.ipynb'), 'w') as fb: 36 | fb.write(json.dumps(notebook)) 37 | 38 | def test_inject(self): 39 | nb = Notebook(support_file('notebook.ipynb')) 40 | self.assertTrue(nb.inject('user/environment')) 41 | 42 | with open(support_file('notebook.ipynb'), 'w') as fb: 43 | fb.write(json.dumps(notebook)) 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | if 'develop' in sys.argv: 5 | from setuptools import setup 6 | using_setuptools = True 7 | else: 8 | from distutils.core import setup 9 | using_setuptools = False 10 | 11 | if sys.version_info[:2] < (2, 7): 12 | sys.exit("conda is only meant for Python 2.7, with experimental support " 13 | "for python 3. current version: %d.%d" % sys.version_info[:2]) 14 | 15 | setup( 16 | name="conda-env", 17 | version="2.5.0alpha", 18 | author="Continuum Analytics, Inc.", 19 | author_email="support@continuum.io", 20 | url="https://github.com/conda/conda-env", 21 | license="BSD", 22 | classifiers=[ 23 | "Development Status :: 4 - Beta", 24 | "Intended Audience :: Developers", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python :: 2", 27 | "Programming Language :: Python :: 2.7", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.3", 30 | "Programming Language :: Python :: 3.4", 31 | ], 32 | description="tools for interacting with conda environments", 33 | long_description=open('README.rst').read(), 34 | packages=[ 35 | 'conda_env', 36 | 'conda_env.cli', 37 | 'conda_env.installers', 38 | 'conda_env.specs', 39 | 'conda_env.utils', 40 | ], 41 | scripts=[ 42 | 'bin/conda-env', 43 | ], 44 | package_data={}, 45 | ) 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Except where noted below, conda-env is released under the following terms: 2 | 3 | (c) 2014 Continuum Analytics, Inc. / http://continuum.io 4 | All Rights Reserved 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of Continuum Analytics, Inc. nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL CONTINUUM ANALYTICS BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /conda_env/utils/notebooks.py: -------------------------------------------------------------------------------- 1 | from os.path import basename 2 | import conda.config as config 3 | from ..exceptions import EnvironmentAlreadyInNotebook, NBFormatNotInstalled 4 | try: 5 | import nbformat 6 | except ImportError: 7 | nbformat = None 8 | 9 | 10 | class Notebook(object): 11 | """Inject environment into a notebook""" 12 | def __init__(self, notebook): 13 | self.msg = "" 14 | self.notebook = notebook 15 | if nbformat is None: 16 | raise NBFormatNotInstalled 17 | 18 | def inject(self, content, force=False): 19 | try: 20 | return self.store_in_file(content, force) 21 | except IOError: 22 | self.msg = "{} may not exist or you don't have adequate permissions".\ 23 | format(self.notebook) 24 | except EnvironmentAlreadyInNotebook: 25 | self.msg = "There is already an environment in {}. Consider '--force'".\ 26 | format(self.notebook) 27 | return False 28 | 29 | def store_in_file(self, content, force=False): 30 | nb = nbformat.reader.reads(open(self.notebook).read()) 31 | if force or 'environment' not in nb['metadata']: 32 | nb['metadata']['environment'] = content 33 | nbformat.write(nb, self.notebook) 34 | return True 35 | else: 36 | raise EnvironmentAlreadyInNotebook(self.notebook) 37 | 38 | 39 | def current_env(): 40 | """Retrieves dictionary with current environment's name and prefix""" 41 | if config.default_prefix == config.root_dir: 42 | name = config.root_env_name 43 | else: 44 | name = basename(config.default_prefix) 45 | return name 46 | -------------------------------------------------------------------------------- /conda_env/cli/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import os 3 | import sys 4 | 5 | try: 6 | from conda.cli.main import args_func 7 | except ImportError as e: 8 | if 'CONDA_DEFAULT_ENV' in os.environ: 9 | sys.stderr.write(""" 10 | There was an error importing conda. 11 | 12 | It appears this was caused by installing conda-env into a conda 13 | environment. Like conda, conda-env needs to be installed into your 14 | root conda/Anaconda environment. 15 | 16 | Please deactivate your current environment, then re-install conda-env 17 | using this command: 18 | 19 | conda install -c conda conda-env 20 | 21 | If you are seeing this error and have not installed conda-env into an 22 | environment, please open a bug report at: 23 | https://github.com/conda/conda-env 24 | 25 | """.lstrip()) 26 | sys.exit(-1) 27 | else: 28 | raise e 29 | 30 | from conda.cli.conda_argparse import ArgumentParser 31 | 32 | from . import main_attach 33 | from . import main_create 34 | from . import main_export 35 | from . import main_list 36 | from . import main_remove 37 | from . import main_upload 38 | from . import main_update 39 | 40 | 41 | # TODO: This belongs in a helper library somewhere 42 | # Note: This only works with `conda-env` as a sub-command. If this gets 43 | # merged into conda-env, this needs to be adjusted. 44 | def show_help_on_empty_command(): 45 | if len(sys.argv) == 1: # sys.argv == ['/path/to/bin/conda-env'] 46 | sys.argv.append('--help') 47 | 48 | 49 | def create_parser(): 50 | p = ArgumentParser() 51 | sub_parsers = p.add_subparsers() 52 | 53 | main_attach.configure_parser(sub_parsers) 54 | main_create.configure_parser(sub_parsers) 55 | main_export.configure_parser(sub_parsers) 56 | main_list.configure_parser(sub_parsers) 57 | main_remove.configure_parser(sub_parsers) 58 | main_upload.configure_parser(sub_parsers) 59 | main_update.configure_parser(sub_parsers) 60 | 61 | show_help_on_empty_command() 62 | return p 63 | 64 | 65 | def main(): 66 | parser = create_parser() 67 | args = parser.parse_args() 68 | return args_func(args, parser) 69 | 70 | 71 | if __name__ == '__main__': 72 | sys.exit(main()) 73 | -------------------------------------------------------------------------------- /conda_env/exceptions.py: -------------------------------------------------------------------------------- 1 | class CondaEnvException(Exception): 2 | pass 3 | 4 | 5 | class CondaEnvRuntimeError(RuntimeError, CondaEnvException): 6 | pass 7 | 8 | 9 | class EnvironmentFileNotFound(CondaEnvException): 10 | def __init__(self, filename, *args, **kwargs): 11 | msg = '{} file not found'.format(filename) 12 | self.filename = filename 13 | super(EnvironmentFileNotFound, self).__init__(msg, *args, **kwargs) 14 | 15 | 16 | class NoBinstar(CondaEnvRuntimeError): 17 | def __init__(self): 18 | msg = 'The anaconda-client cli must be installed to perform this action' 19 | super(NoBinstar, self).__init__(msg) 20 | 21 | 22 | class AlreadyExist(CondaEnvRuntimeError): 23 | def __init__(self): 24 | msg = 'The environment path already exists' 25 | super(AlreadyExist, self).__init__(msg) 26 | 27 | 28 | class EnvironmentAlreadyInNotebook(CondaEnvRuntimeError): 29 | def __init__(self, notebook, *args, **kwargs): 30 | msg = "The notebook {} already has an environment" 31 | super(EnvironmentAlreadyInNotebook, self).__init__(msg, *args, **kwargs) 32 | 33 | 34 | class EnvironmentFileDoesNotExist(CondaEnvRuntimeError): 35 | def __init__(self, handle, *args, **kwargs): 36 | self.handle = handle 37 | msg = "{} does not have an environment definition".format(handle) 38 | super(EnvironmentFileDoesNotExist, self).__init__(msg, *args, **kwargs) 39 | 40 | 41 | class EnvironmentFileNotDownloaded(CondaEnvRuntimeError): 42 | def __init__(self, username, packagename, *args, **kwargs): 43 | msg = '{}/{} file not downloaded'.format(username, packagename) 44 | self.username = username 45 | self.packagename = packagename 46 | super(EnvironmentFileNotDownloaded, self).__init__(msg, *args, **kwargs) 47 | 48 | 49 | class SpecNotFound(CondaEnvRuntimeError): 50 | def __init__(self, msg, *args, **kwargs): 51 | super(SpecNotFound, self).__init__(msg, *args, **kwargs) 52 | 53 | 54 | class InvalidLoader(Exception): 55 | def __init__(self, name): 56 | msg = 'Unable to load installer for {}'.format(name) 57 | super(InvalidLoader, self).__init__(msg) 58 | 59 | 60 | class NBFormatNotInstalled(CondaEnvRuntimeError): 61 | def __init__(self): 62 | msg = """nbformat is not installed. Install it with: 63 | conda install nbformat 64 | """ 65 | super(NBFormatNotInstalled, self).__init__(msg) 66 | -------------------------------------------------------------------------------- /tests/specs/test_base.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import random 3 | import types 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from conda_env import specs 11 | from conda_env.exceptions import SpecNotFound 12 | 13 | 14 | true_func = mock.Mock(return_value=True) 15 | false_func = mock.Mock(return_value=False) 16 | 17 | 18 | @contextmanager 19 | def patched_specs(*new_specs): 20 | with mock.patch.object(specs, "all_specs") as all_specs: 21 | all_specs.__iter__.return_value = new_specs 22 | yield all_specs 23 | 24 | 25 | def generate_two_specs(): 26 | spec1 = mock.Mock(can_handle=false_func) 27 | spec1.return_value = spec1 28 | spec2 = mock.Mock(can_handle=true_func) 29 | spec2.return_value = spec2 30 | return spec1, spec2 31 | 32 | 33 | class DetectTestCase(unittest.TestCase): 34 | def test_has_detect_function(self): 35 | self.assertTrue(hasattr(specs, "detect")) 36 | self.assertIsInstance(specs.detect, types.FunctionType) 37 | 38 | def test_dispatches_to_registered_specs(self): 39 | spec1, spec2 = generate_two_specs() 40 | with patched_specs(spec1, spec2) as all_specs: 41 | actual = specs.detect(name="foo") 42 | self.assertEqual(actual, spec2) 43 | 44 | def test_passes_kwargs_to_all_specs(self): 45 | random_kwargs = { 46 | "foo": random.randint(100, 200), 47 | "bar%d" % random.randint(100, 200): True 48 | } 49 | 50 | spec1, spec2 = generate_two_specs() 51 | with patched_specs(spec1, spec2): 52 | specs.detect(**random_kwargs) 53 | spec1.assert_called_with(**random_kwargs) 54 | spec2.assert_called_with(**random_kwargs) 55 | 56 | def test_raises_exception_if_no_detection(self): 57 | spec1 = generate_two_specs()[0] 58 | spec1.msg = 'msg' 59 | with patched_specs(spec1) as all_specs: 60 | with self.assertRaises(SpecNotFound): 61 | specs.detect(name="foo") 62 | 63 | def test_has_build_msg_function(self): 64 | self.assertTrue(hasattr(specs, 'build_message')) 65 | self.assertIsInstance(specs.build_message, types.FunctionType) 66 | 67 | def test_build_msg(self): 68 | spec3 = mock.Mock(msg='error 3') 69 | spec4 = mock.Mock(msg='error 4') 70 | spec5 = mock.Mock(msg=None) 71 | self.assertEqual(specs.build_message([spec3, spec4, spec5]), 'error 3\nerror 4') 72 | -------------------------------------------------------------------------------- /conda_env/cli/main_attach.py: -------------------------------------------------------------------------------- 1 | from argparse import RawDescriptionHelpFormatter 2 | from ..utils.notebooks import Notebook 3 | from conda.cli import common 4 | from ..env import from_environment 5 | 6 | 7 | description = """ 8 | Embeds information describing your conda environment 9 | into the notebook metadata 10 | """ 11 | 12 | example = """ 13 | examples: 14 | conda env attach -n root notebook.ipynb 15 | conda env attach -r user/environment notebook.ipynb 16 | """ 17 | 18 | 19 | def configure_parser(sub_parsers): 20 | p = sub_parsers.add_parser( 21 | 'attach', 22 | formatter_class=RawDescriptionHelpFormatter, 23 | description=description, 24 | help=description, 25 | epilog=example, 26 | ) 27 | group = p.add_mutually_exclusive_group(required=True) 28 | group.add_argument( 29 | '-n', '--name', 30 | action='store', 31 | help='local environment definition', 32 | default=None 33 | ) 34 | group.add_argument( 35 | '-r', '--remote', 36 | action='store', 37 | help='remote environment definition', 38 | default=None 39 | ) 40 | p.add_argument( 41 | '-p', "--prefix", 42 | action="store", 43 | help="Full path to environment prefix", 44 | metavar='PATH', 45 | default=None 46 | ) 47 | p.add_argument( 48 | '--force', 49 | action='store_true', 50 | default=False, 51 | help='Replace existing environment definition' 52 | ) 53 | p.add_argument( 54 | '--no-builds', 55 | default=False, 56 | action='store_true', 57 | required=False, 58 | help='Remove build specification from dependencies' 59 | ) 60 | p.add_argument( 61 | 'notebook', 62 | help='notebook file', 63 | action='store', 64 | default=None 65 | ) 66 | common.add_parser_json(p) 67 | p.set_defaults(func=execute) 68 | 69 | 70 | def execute(args, parser): 71 | 72 | if args.prefix is None: 73 | prefix = common.get_prefix(args) 74 | else: 75 | prefix = args.prefix 76 | 77 | if args.name is not None: 78 | content = from_environment(args.name, prefix, no_builds=args.no_builds).to_dict() 79 | else: 80 | content = {'remote': args.remote} 81 | 82 | print("Environment {} will be attach into {}".format(args.name, args.notebook)) 83 | nb = Notebook(args.notebook) 84 | if nb.inject(content, args.force): 85 | print("Done.") 86 | else: 87 | print("The environment couldn't be attached due:") 88 | print(nb.msg) 89 | -------------------------------------------------------------------------------- /tests/cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | import subprocess 5 | 6 | environment_1 = ''' 7 | name: env-1 8 | dependencies: 9 | - ojota 10 | channels: 11 | - malev 12 | ''' 13 | 14 | environment_2 = ''' 15 | name: env-1 16 | dependencies: 17 | - ojota 18 | - flask 19 | channels: 20 | - malev 21 | ''' 22 | 23 | 24 | def run(command): 25 | process = subprocess.Popen( 26 | command, 27 | shell=True, 28 | stdout=subprocess.PIPE, 29 | stderr=subprocess.STDOUT, 30 | universal_newlines=True 31 | ) 32 | stdout, stderr = process.communicate() 33 | status = process.returncode 34 | return (stdout, stderr, status) 35 | 36 | 37 | def create_env(content, filename='environment.yml'): 38 | with open(filename, 'w') as fenv: 39 | fenv.write(content) 40 | 41 | 42 | def remove_env_file(filename='environment.yml'): 43 | os.remove(filename) 44 | 45 | 46 | class IntegrationTest(unittest.TestCase): 47 | def assertStatusOk(self, status): 48 | self.assertEqual(status, 0) 49 | 50 | def assertStatusNotOk(self, status): 51 | self.assertNotEqual(0, status) 52 | 53 | def tearDown(self): 54 | run('conda env remove -n env-1 -y') 55 | run('conda env remove -n env-2 -y') 56 | run('rm environment.yml') 57 | 58 | def test_conda_env_create_no_file(self): 59 | ''' 60 | Test `conda env create` without an environment.yml file 61 | Should fail 62 | ''' 63 | o, e, s = run('conda env create') 64 | self.assertStatusNotOk(s) 65 | 66 | def test_create_valid_env(self): 67 | ''' 68 | Creates an environment.yml file and 69 | creates and environment with it 70 | ''' 71 | create_env(environment_1) 72 | 73 | o, e, s = run('conda env create') 74 | self.assertStatusOk(s) 75 | 76 | o, e, s = run('conda info --json') 77 | parsed = json.loads(o) 78 | self.assertNotEqual( 79 | len([env for env in parsed['envs'] if env.endswith('env-1')]), 80 | 0 81 | ) 82 | 83 | o, e, s = run('conda env remove -y -n env-1') 84 | self.assertStatusOk(s) 85 | 86 | def test_update(self): 87 | create_env(environment_1) 88 | o, e, s = run('conda env create') 89 | create_env(environment_2) 90 | o, e, s = run('conda env update -n env-1') 91 | o, e, s = run('conda list flask -n env-1 --json') 92 | parsed = json.loads(o) 93 | self.assertNotEqual(len(parsed), 0) 94 | 95 | def test_name(self): 96 | # smoke test for gh-254 97 | create_env(environment_1) 98 | o, e, s = run('conda env create -n new-env create') 99 | self.assertStatusOk(s) 100 | 101 | 102 | if __name__ == '__main__': 103 | unittest.main() 104 | -------------------------------------------------------------------------------- /conda_env/cli/main_export.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import os 4 | import textwrap 5 | from argparse import RawDescriptionHelpFormatter 6 | 7 | from conda import config 8 | from conda.cli import common 9 | 10 | from ..env import from_environment 11 | 12 | description = """ 13 | Export a given environment 14 | """ 15 | 16 | example = """ 17 | examples: 18 | conda env export 19 | conda env export --file SOME_FILE 20 | """ 21 | 22 | 23 | def configure_parser(sub_parsers): 24 | p = sub_parsers.add_parser( 25 | 'export', 26 | formatter_class=RawDescriptionHelpFormatter, 27 | description=description, 28 | help=description, 29 | epilog=example, 30 | ) 31 | 32 | p.add_argument( 33 | '-c', '--channel', 34 | action='append', 35 | help='Additional channel to include in the export' 36 | ) 37 | 38 | p.add_argument( 39 | "--override-channels", 40 | action="store_true", 41 | help="Do not include .condarc channels", 42 | ) 43 | 44 | p.add_argument( 45 | '-n', '--name', 46 | action='store', 47 | help='name of environment (in %s)' % os.pathsep.join(config.envs_dirs), 48 | default=None, 49 | ) 50 | 51 | p.add_argument( 52 | '-f', '--file', 53 | default=None, 54 | required=False 55 | ) 56 | 57 | p.add_argument( 58 | '--no-builds', 59 | default=False, 60 | action='store_true', 61 | required=False, 62 | help='Remove build specification from dependencies' 63 | ) 64 | 65 | p.set_defaults(func=execute) 66 | 67 | 68 | # TODO Make this aware of channels that were used to install packages 69 | def execute(args, parser): 70 | if not args.name: 71 | # Note, this is a hack fofr get_prefix that assumes argparse results 72 | # TODO Refactor common.get_prefix 73 | name = os.environ.get('CONDA_DEFAULT_ENV', False) 74 | if not name: 75 | msg = "Unable to determine environment\n\n" 76 | msg += textwrap.dedent(""" 77 | Please re-run this command with one of the following options: 78 | 79 | * Provide an environment name via --name or -n 80 | * Re-run this command inside an activated conda environment.""").lstrip() 81 | # TODO Add json support 82 | common.error_and_exit(msg, json=False) 83 | args.name = name 84 | else: 85 | name = args.name 86 | prefix = common.get_prefix(args) 87 | env = from_environment(name, prefix, no_builds=args.no_builds) 88 | 89 | if args.override_channels: 90 | env.remove_channels() 91 | 92 | if args.channel is not None: 93 | env.add_channels(args.channel) 94 | 95 | if args.file is None: 96 | print(env.to_yaml()) 97 | else: 98 | fp = open(args.file, 'wb') 99 | env.to_yaml(stream=fp) 100 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | If you need more details about the changes made visit the 4 | [releases](https://github.com/conda/conda-env/releases) page 5 | on Github. Every release commit has all the information about 6 | the changes in the source code. 7 | 8 | #### v2.5.2 (07/13/16) 9 | 10 | - Fix #257 environment.yml parsing errors. (@jseabold, #261) 11 | - Fix #254 Override file's name using --name command line argument. (@jseabold, #262) 12 | 13 | 14 | #### v2.5.1 (06/16/16) 15 | 16 | - Remove selectors. (@kalefranz, #253) 17 | - Add integration tests. (@malev, #206) 18 | - Fix conda env remove by proxying to conda remove --all. (@mcg1969, #251) 19 | - Fix unexpected prune parameter in pip installer. (@kdeldycke, #246) 20 | 21 | #### v2.5.0 (06/13/16) 22 | 23 | - Add a mechanism to let an environment disable the default channels (@mwiebe, #229) 24 | - Fix conda env create / (@oyse, #228) 25 | - Move activate scripts to conda main repo, (@msarahan, #234) 26 | - Add conda.pip module from conda (@ilanschnell, #235) 27 | - Implement --prune options for "conda env update" (@nicoddemus, #195) 28 | - Preprocessing selectors, (@Korijn, #213) 29 | 30 | #### v2.4.5 (12/08/15) 31 | 32 | - Store quiet arg as True (default to False) (@faph, #201) 33 | - Initial support for requirements.txt as env spec (@malev, #203) 34 | - Fix PROMPT reverting to $P$G default (@tadeu, #208) 35 | - Fix activation behavior on Win (keep root Scripts dir on PATH); improve behavior with paths containing spaces (@msarahan, #212) 36 | 37 | #### v2.4.4 (10/26/15) 38 | 39 | - Change environment's versions when uploading. (@ijstokes, #191) 40 | - Support specifying environment by path, as well as by name. (@remram44, #60) 41 | - activate.bat properly searches CONDA_ENVS_PATH for named environments. (@mikecroucher, #164) 42 | - Add Library\\bin to path when activating environments. (@malev, #152) 43 | 44 | #### v2.4.3 (10/18/15) 45 | 46 | - Better windows compatibility 47 | - Typos in documentation 48 | 49 | #### v2.4.2 (08/17/15) 50 | 51 | - Support Jupyter 52 | 53 | #### v2.4.1 (08/12/15) 54 | 55 | - Fix `create` bug 56 | 57 | #### v2.4.0 (08/11/15) 58 | 59 | - `update` works with remote definitions 60 | - `CONDA_ENV_PATH` fixed 61 | - Windows prompt fixed 62 | - Update `pip` dependencies 63 | - `Library/bin` add to path in win 64 | - Better authorization message 65 | - Remove `--force` flag from upload 66 | - New version created every time you run upload 67 | - Using `conda_argparse2` now 68 | - Fix `activate` script in ZSH 69 | - `--no-builds` flag in attach 70 | - Create environment from notebooks 71 | 72 | #### v2.3.0 (07/09/15) 73 | 74 | - `--force` flag on `create` 75 | - `--no-builds` flag on `export` 76 | - `conda env attach` feature 77 | 78 | #### v2.2.3 (06/18/15) 79 | 80 | - Reimplement `-n` flag 81 | 82 | #### v2.2.2 (06/16/15) 83 | 84 | - Allow `--force` flag on upload 85 | 86 | #### v2.2.1 (6/8/15) 87 | 88 | - Fix py3 issue with exceptions 89 | 90 | ### v2.2.0 (6/15/15) 91 | 92 | - Create environment from remote definitions 93 | - Upload environment definitions to anaconda.org 94 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This repository is deprecated. All code herein now rides alongside conda proper. https://github.com/conda/conda** 2 | 3 | 4 | ========= 5 | conda-env 6 | ========= 7 | 8 | .. image:: https://travis-ci.org/conda/conda-env.svg 9 | :target: https://travis-ci.org/conda/conda-env 10 | 11 | Provides the `conda env` interface to Conda environments. 12 | 13 | Installing 14 | ---------- 15 | 16 | To install `conda env` with conda, run the following command in your root environment: 17 | 18 | .. code-block:: bash 19 | 20 | $ conda install -c conda conda-env 21 | 22 | 23 | Usage 24 | ----- 25 | All of the usage is documented via the ``--help`` flag. 26 | 27 | .. code-block:: bash 28 | 29 | $ conda env --help 30 | usage: conda-env [-h] {create,export,list,remove} ... 31 | 32 | positional arguments: 33 | {attach,create,export,list,remove,upload,update} 34 | attach Embeds information describing your conda environment 35 | into the notebook metadata 36 | create Create an environment based on an environment file 37 | export Export a given environment 38 | list List the Conda environments 39 | remove Remove an environment 40 | upload Upload an environment to anaconda.org 41 | update Updates the current environment based on environment 42 | file 43 | 44 | optional arguments: 45 | -h, --help show this help message and exit 46 | 47 | 48 | ``environment.yml`` 49 | ------------------- 50 | conda-env allows creating environments using the ``environment.yml`` 51 | specification file. This allows you to specify a name, channels to use when 52 | creating the environment, and the dependencies. For example, to create an 53 | environment named ``stats`` with numpy and pandas create an ``environment.yml`` 54 | file with this as the contents: 55 | 56 | .. code-block:: yaml 57 | 58 | name: stats 59 | dependencies: 60 | - numpy 61 | - pandas 62 | 63 | Then run this from the command line: 64 | 65 | .. code-block:: bash 66 | 67 | $ conda env create 68 | Fetching package metadata: ... 69 | Solving package specifications: .Linking packages ... 70 | [ COMPLETE ] |#################################################| 100% 71 | # 72 | # To activate this environment, use: 73 | # $ source activate numpy 74 | # 75 | # To deactivate this environment, use: 76 | # $ source deactivate 77 | # 78 | 79 | Your output might vary a little bit depending on whether you have the packages 80 | in your local package cache. 81 | 82 | You can explicitly provide an environment spec file using ``-f`` or ``--file`` 83 | and the name of the file you would like to use. 84 | 85 | The default channels can be excluded by adding ``nodefaults`` to the list of 86 | channels. This is equivalent to passing the ``--override-channels`` option 87 | to most ``conda`` commands, and is like ``defaults`` in the ``.condarc`` 88 | channel configuration but with the reverse logic. 89 | 90 | Environment file example 91 | ----------------------- 92 | 93 | .. code-block:: yaml 94 | 95 | name: stats 96 | channels: 97 | - javascript 98 | dependencies: 99 | - python=3.4 # or 2.7 if you are feeling nostalgic 100 | - bokeh=0.9.2 101 | - numpy=1.9.* 102 | - nodejs=0.10.* 103 | - flask 104 | - pip: 105 | - Flask-Testing 106 | 107 | **Recommendation:** Always create your `environment.yml` file by hand. 108 | -------------------------------------------------------------------------------- /conda_env/cli/main_upload.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from argparse import RawDescriptionHelpFormatter 3 | from conda.cli import common 4 | from .. import exceptions 5 | from ..env import from_file 6 | from ..utils.uploader import is_installed, Uploader 7 | 8 | 9 | description = """ 10 | Upload an environment to anaconda.org 11 | """ 12 | 13 | example = """ 14 | examples: 15 | conda env upload 16 | conda env upload project 17 | conda env upload --file=/path/to/environment.yml 18 | conda env upload --file=/path/to/environment.yml project 19 | """ 20 | 21 | 22 | def configure_parser(sub_parsers): 23 | p = sub_parsers.add_parser( 24 | 'upload', 25 | formatter_class=RawDescriptionHelpFormatter, 26 | description=description, 27 | help=description, 28 | epilog=example, 29 | ) 30 | p.add_argument( 31 | '-n', '--name', 32 | action='store', 33 | help='environment definition [Deprecated]', 34 | default=None, 35 | dest='old_name' 36 | ) 37 | p.add_argument( 38 | '-f', '--file', 39 | action='store', 40 | help='environment definition file (default: environment.yml)', 41 | default='environment.yml', 42 | ) 43 | p.add_argument( 44 | '--summary', 45 | help='Short summary of the environment', 46 | default='Environment file' 47 | ) 48 | p.add_argument( 49 | '-q', '--quiet', 50 | default=False, 51 | action='store_true' 52 | ) 53 | p.add_argument( 54 | 'name', 55 | help='environment definition', 56 | action='store', 57 | default=None, 58 | nargs='?' 59 | ) 60 | common.add_parser_json(p) 61 | p.set_defaults(func=execute) 62 | 63 | 64 | def execute(args, parser): 65 | 66 | if not is_installed(): 67 | raise exceptions.NoBinstar() 68 | 69 | try: 70 | env = from_file(args.file) 71 | except exceptions.EnvironmentFileNotFound as e: 72 | msg = 'Unable to locate environment file: %s\n\n' % e.filename 73 | msg += "\n".join(textwrap.wrap(textwrap.dedent(""" 74 | Please verify that the above file is present and that you have 75 | permission read the file's contents. Note, you can specify the 76 | file to use by explictly adding --file=/path/to/file when calling 77 | conda env create.""").lstrip())) 78 | raise exceptions.CondaEnvRuntimeError(msg) 79 | 80 | if args.old_name: 81 | print("`--name` is deprecated. Use:\n" 82 | " conda env upload {}".format(args.old_name)) 83 | 84 | try: 85 | summary = args.summary or env.summary 86 | except AttributeError: 87 | summary = None 88 | 89 | try: 90 | name = args.name or args.old_name or env.name 91 | except AttributeError: 92 | msg = """An environment name is required.\n 93 | You can specify on the command line as in: 94 | \tconda env upload name 95 | or you can add a name property to your {} file.""".lstrip().format(args.file) 96 | raise exceptions.CondaEnvRuntimeError(msg) 97 | 98 | uploader = Uploader(name, args.file, summary=summary, env_data=dict(env.to_dict())) 99 | 100 | if uploader.authorized(): 101 | info = uploader.upload() 102 | print("Your environment file has been uploaded to {}".format(info.get('url', 'anaconda.org'))) 103 | else: 104 | msg = "\n".join(["You are not authorized to upload a package into Anaconda.org", 105 | "Verify that you are logged in anaconda.org with:", 106 | " anaconda login\n"]) 107 | raise exceptions.CondaEnvRuntimeError(msg) 108 | 109 | print("Done.") 110 | -------------------------------------------------------------------------------- /tests/specs/test_binstar.py: -------------------------------------------------------------------------------- 1 | import types 2 | import unittest 3 | try: 4 | from io import StringIO 5 | except ImportError: 6 | from StringIO import StringIO 7 | from mock import patch, MagicMock 8 | from binstar_client import errors 9 | 10 | from conda_env.specs import binstar 11 | from conda_env.specs.binstar import BinstarSpec 12 | from conda_env.env import Environment 13 | 14 | 15 | class TestBinstarSpec(unittest.TestCase): 16 | def test_has_can_handle_method(self): 17 | spec = BinstarSpec() 18 | self.assertTrue(hasattr(spec, 'can_handle')) 19 | self.assertIsInstance(spec.can_handle, types.MethodType) 20 | 21 | def test_name_not_present(self): 22 | spec = BinstarSpec(filename='filename') 23 | self.assertEqual(spec.can_handle(), False) 24 | self.assertEqual(spec.msg, "Can't process without a name") 25 | 26 | def test_invalid_name(self): 27 | spec = BinstarSpec(name='invalid-name') 28 | self.assertEqual(spec.can_handle(), False) 29 | self.assertEqual(spec.msg, "Invalid name, try the format: user/package") 30 | 31 | def test_package_not_exist(self): 32 | with patch('conda_env.specs.binstar.get_binstar') as get_binstar_mock: 33 | package = MagicMock(side_effect=errors.NotFound('msg')) 34 | binstar = MagicMock(package=package) 35 | get_binstar_mock.return_value = binstar 36 | spec = BinstarSpec(name='darth/no-exist') 37 | self.assertEqual(spec.package, None) 38 | self.assertEqual(spec.can_handle(), False) 39 | 40 | def test_package_without_environment_file(self): 41 | with patch('conda_env.specs.binstar.get_binstar') as get_binstar_mock: 42 | package = MagicMock(return_value={'files': []}) 43 | binstar = MagicMock(package=package) 44 | get_binstar_mock.return_value = binstar 45 | spec = BinstarSpec('darth/no-env-file') 46 | 47 | self.assertEqual(spec.can_handle(), False) 48 | 49 | def test_download_environment(self): 50 | fake_package = { 51 | 'files': [{'type': 'env', 'version': '1', 'basename': 'environment.yml'}] 52 | } 53 | fake_req = MagicMock(text=u"name: env") 54 | with patch('conda_env.specs.binstar.get_binstar') as get_binstar_mock: 55 | package = MagicMock(return_value=fake_package) 56 | downloader = MagicMock(return_value=fake_req) 57 | binstar = MagicMock(package=package, download=downloader) 58 | get_binstar_mock.return_value = binstar 59 | 60 | spec = BinstarSpec(name='darth/env-file') 61 | self.assertIsInstance(spec.environment, Environment) 62 | 63 | def test_environment_version_sorting(self): 64 | fake_package = { 65 | 'files': [ 66 | {'type': 'env', 'version': '0.1.1', 'basename': 'environment.yml'}, 67 | {'type': 'env', 'version': '0.1a.2', 'basename': 'environment.yml'}, 68 | {'type': 'env', 'version': '0.2.0', 'basename': 'environment.yml'}, 69 | ] 70 | } 71 | fake_req = MagicMock(text=u"name: env") 72 | with patch('conda_env.specs.binstar.get_binstar') as get_binstar_mock: 73 | package = MagicMock(return_value=fake_package) 74 | downloader = MagicMock(return_value=fake_req) 75 | binstar = MagicMock(package=package, download=downloader) 76 | get_binstar_mock.return_value = binstar 77 | 78 | spec = BinstarSpec(name='darth/env-file') 79 | spec.environment 80 | downloader.assert_called_with('darth', 'env-file', '0.2.0', 'environment.yml') 81 | 82 | def test_binstar_not_installed(self): 83 | spec = BinstarSpec(name='user/package') 84 | spec.binstar = None 85 | self.assertFalse(spec.can_handle()) 86 | self.assertEqual(spec.msg, 'Please install binstar') 87 | 88 | 89 | if __name__ == '__main__': 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /conda_env/cli/main_create.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from argparse import RawDescriptionHelpFormatter 3 | import os 4 | import sys 5 | import textwrap 6 | 7 | from conda.cli import common 8 | from conda.cli import install as cli_install 9 | from conda.install import rm_rf 10 | from conda.misc import touch_nonadmin 11 | from conda.plan import is_root_prefix 12 | 13 | from ..installers.base import get_installer, InvalidInstaller 14 | from .. import exceptions 15 | from .. import specs 16 | 17 | description = """ 18 | Create an environment based on an environment file 19 | """ 20 | 21 | example = """ 22 | examples: 23 | conda env create 24 | conda env create -n name 25 | conda env create vader/deathstar 26 | conda env create -f=/path/to/environment.yml 27 | conda env create -f=/path/to/requirements.txt -n deathstar 28 | conda env create -f=/path/to/requirements.txt -p /home/user/software/deathstar 29 | """ 30 | 31 | def configure_parser(sub_parsers): 32 | p = sub_parsers.add_parser( 33 | 'create', 34 | formatter_class=RawDescriptionHelpFormatter, 35 | description=description, 36 | help=description, 37 | epilog=example, 38 | ) 39 | p.add_argument( 40 | '-f', '--file', 41 | action='store', 42 | help='environment definition file (default: environment.yml)', 43 | default='environment.yml', 44 | ) 45 | 46 | # Add name and prefix args 47 | common.add_parser_prefix(p) 48 | 49 | p.add_argument( 50 | '-q', '--quiet', 51 | action='store_true', 52 | default=False, 53 | ) 54 | p.add_argument( 55 | 'remote_definition', 56 | help='remote environment definition / IPython notebook', 57 | action='store', 58 | default=None, 59 | nargs='?' 60 | ) 61 | p.add_argument( 62 | '--force', 63 | help='force creation of environment (removing a previously existing environment of the same name).', 64 | action='store_true', 65 | default=False, 66 | ) 67 | common.add_parser_json(p) 68 | p.set_defaults(func=execute) 69 | 70 | 71 | def execute(args, parser): 72 | name = args.remote_definition or args.name 73 | 74 | try: 75 | spec = specs.detect(name=name, filename=args.file, 76 | directory=os.getcwd()) 77 | env = spec.environment 78 | 79 | # FIXME conda code currently requires args to have a name or prefix 80 | # don't overwrite name if it's given. gh-254 81 | if args.prefix is None and args.name is None: 82 | args.name = env.name 83 | 84 | except exceptions.SpecNotFound as e: 85 | common.error_and_exit(str(e), json=args.json) 86 | 87 | prefix = common.get_prefix(args, search=False) 88 | 89 | if args.force and not is_root_prefix(prefix) and os.path.exists(prefix): 90 | rm_rf(prefix) 91 | cli_install.check_prefix(prefix, json=args.json) 92 | 93 | # TODO, add capability 94 | # common.ensure_override_channels_requires_channel(args) 95 | # channel_urls = args.channel or () 96 | 97 | for installer_type, pkg_specs in env.dependencies.items(): 98 | try: 99 | installer = get_installer(installer_type) 100 | installer.install(prefix, pkg_specs, args, env) 101 | except InvalidInstaller: 102 | sys.stderr.write(textwrap.dedent(""" 103 | Unable to install package for {0}. 104 | 105 | Please double check and ensure you dependencies file has 106 | the correct spelling. You might also try installing the 107 | conda-env-{0} package to see if provides the required 108 | installer. 109 | """).lstrip().format(installer_type) 110 | ) 111 | return -1 112 | 113 | touch_nonadmin(prefix) 114 | if not args.json: 115 | cli_install.print_activate(args.name if args.name else prefix) 116 | -------------------------------------------------------------------------------- /conda_env/utils/uploader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from collections import namedtuple 4 | from .. import exceptions 5 | try: 6 | from binstar_client.utils import get_binstar 7 | from binstar_client import errors 8 | except ImportError: 9 | get_binstar = None 10 | 11 | 12 | ENVIRONMENT_TYPE = 'env' 13 | 14 | # TODO: refactor binstar so that individual arguments are passed in instead of an arg object 15 | binstar_args = namedtuple('binstar_args', ['site', 'token']) 16 | 17 | 18 | def is_installed(): 19 | """ 20 | is Binstar-cli installed? 21 | :return: True/False 22 | """ 23 | return get_binstar is not None 24 | 25 | 26 | class Uploader(object): 27 | """ 28 | Upload environments to Binstar 29 | * Check if user is logged (offer to log in if it's not) 30 | * Check if package exist (create if not) 31 | * Check if distribution exist (overwrite if force=True) 32 | * Upload environment.yml 33 | """ 34 | 35 | _user = None 36 | _username = None 37 | _binstar = None 38 | 39 | def __init__(self, packagename, env_file, **kwargs): 40 | self.packagename = packagename 41 | self.file = env_file 42 | self.summary = kwargs.get('summary') 43 | self.env_data = kwargs.get('env_data') 44 | self.basename = os.path.basename(env_file) 45 | 46 | @property 47 | def version(self): 48 | return time.strftime('%Y.%m.%d.%H%M') 49 | 50 | @property 51 | def user(self): 52 | if self._user is None: 53 | self._user = self.binstar.user() 54 | return self._user 55 | 56 | @property 57 | def binstar(self): 58 | if self._binstar is None: 59 | self._binstar = get_binstar() 60 | return self._binstar 61 | 62 | @property 63 | def username(self): 64 | if self._username is None: 65 | self._username = self.user['login'] 66 | return self._username 67 | 68 | def authorized(self): 69 | try: 70 | return self.user is not None 71 | except errors.Unauthorized: 72 | return False 73 | 74 | def upload(self): 75 | """ 76 | Prepares and uploads env file 77 | :return: True/False 78 | """ 79 | print("Uploading environment %s to anaconda-server (%s)... " % 80 | (self.packagename, self.binstar.domain)) 81 | if self.is_ready(): 82 | with open(self.file, mode='rb') as envfile: 83 | return self.binstar.upload(self.username, self.packagename, 84 | self.version, self.basename, envfile, 85 | distribution_type=ENVIRONMENT_TYPE, attrs=self.env_data) 86 | else: 87 | raise exceptions.AlreadyExist() 88 | 89 | def is_ready(self): 90 | """ 91 | Ensures package namespace and distribution 92 | :return: True or False 93 | """ 94 | return self.ensure_package_namespace() and self.ensure_distribution() 95 | 96 | def ensure_package_namespace(self): 97 | """ 98 | Ensure that a package namespace exists. This is required to upload a file. 99 | """ 100 | try: 101 | self.binstar.package(self.username, self.packagename) 102 | except errors.NotFound: 103 | self.binstar.add_package(self.username, self.packagename, self.summary) 104 | 105 | # TODO: this should be removed as a hard requirement of binstar 106 | try: 107 | self.binstar.release(self.username, self.packagename, self.version) 108 | except errors.NotFound: 109 | self.binstar.add_release(self.username, self.packagename, self.version, {}, '', '') 110 | 111 | return True 112 | 113 | def ensure_distribution(self): 114 | """ 115 | Ensure that a package distribution does not exist. 116 | """ 117 | try: 118 | self.binstar.distribution(self.username, self.packagename, self.version, self.basename) 119 | except errors.NotFound: 120 | return True 121 | else: 122 | return False 123 | -------------------------------------------------------------------------------- /conda_env/pip_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions related to core conda functionality that relates to pip 3 | 4 | NOTE: This modules used to in conda, as conda/pip.py 5 | """ 6 | from __future__ import absolute_import, print_function 7 | from os.path import isfile, join 8 | import re 9 | import subprocess 10 | import sys 11 | 12 | 13 | def pip_args(prefix): 14 | """ 15 | return the arguments required to invoke pip (in prefix), or None if pip 16 | is not installed 17 | """ 18 | if sys.platform == 'win32': 19 | pip_path = join(prefix, 'Scripts', 'pip-script.py') 20 | py_path = join(prefix, 'python.exe') 21 | else: 22 | pip_path = join(prefix, 'bin', 'pip') 23 | py_path = join(prefix, 'bin', 'python') 24 | if isfile(pip_path) and isfile(py_path): 25 | ret = [py_path, pip_path] 26 | 27 | # Check the version of pip 28 | # --disable-pip-version-check was introduced in pip 6.0 29 | # If older than that, they should probably get the warning anyway. 30 | pip_version = subprocess.check_output(ret + ['-V']).decode('utf-8').split()[1] 31 | major_ver = pip_version.split('.')[0] 32 | if int(major_ver) >= 6: 33 | ret.append('--disable-pip-version-check') 34 | return ret 35 | else: 36 | return None 37 | 38 | 39 | class PipPackage(dict): 40 | def __str__(self): 41 | if 'path' in self: 42 | return '%s (%s)-%s-' % ( 43 | self['name'], 44 | self['path'], 45 | self['version'] 46 | ) 47 | return '%s-%s-' % (self['name'], self['version']) 48 | 49 | 50 | def installed(prefix, output=True): 51 | args = pip_args(prefix) 52 | if args is None: 53 | return 54 | args.append('list') 55 | try: 56 | pipinst = subprocess.check_output( 57 | args, universal_newlines=True 58 | ).splitlines() 59 | except Exception: 60 | # Any error should just be ignored 61 | if output: 62 | print("# Warning: subprocess call to pip failed") 63 | return 64 | 65 | # For every package in pipinst that is not already represented 66 | # in installed append a fake name to installed with 'pip' 67 | # as the build string 68 | pat = re.compile('([\w.-]+)\s+\((.+)\)') 69 | for line in pipinst: 70 | line = line.strip() 71 | if not line: 72 | continue 73 | m = pat.match(line) 74 | if m is None: 75 | if output: 76 | print('Could not extract name and version from: %r' % line) 77 | continue 78 | name, version = m.groups() 79 | name = name.lower() 80 | kwargs = { 81 | 'name': name, 82 | 'version': version, 83 | } 84 | if ', ' in version: 85 | # Packages installed with setup.py develop will include a path in 86 | # the version. They should be included here, even if they are 87 | # installed with conda, as they are preferred over the conda 88 | # version. We still include the conda version, though, because it 89 | # is still installed. 90 | 91 | version, path = version.split(', ') 92 | # We do this because the code below uses rsplit('-', 2) 93 | version = version.replace('-', ' ') 94 | kwargs.update({ 95 | 'path': path, 96 | 'version': version, 97 | }) 98 | yield PipPackage(**kwargs) 99 | 100 | 101 | def add_pip_installed(prefix, installed_pkgs, json=None, output=True): 102 | # Defer to json for backwards compatibility 103 | if type(json) is bool: 104 | output = not json 105 | 106 | # TODO Refactor so installed is a real list of objects/dicts 107 | # instead of strings allowing for direct comparison 108 | conda_names = {d.rsplit('-', 2)[0] for d in installed_pkgs} 109 | 110 | for pip_pkg in installed(prefix, output=output): 111 | if pip_pkg['name'] in conda_names and not 'path' in pip_pkg: 112 | continue 113 | installed_pkgs.add(str(pip_pkg)) 114 | -------------------------------------------------------------------------------- /conda_env/specs/binstar.py: -------------------------------------------------------------------------------- 1 | import re 2 | from conda.resolve import normalized_version 3 | from .. import env 4 | from ..exceptions import EnvironmentFileNotDownloaded, CondaEnvException 5 | try: 6 | from binstar_client import errors 7 | from binstar_client.utils import get_binstar 8 | except ImportError: 9 | get_binstar = None 10 | 11 | ENVIRONMENT_TYPE = 'env' 12 | # TODO: isolate binstar related code into conda_env.utils.binstar 13 | 14 | 15 | class BinstarSpec(object): 16 | """ 17 | spec = BinstarSpec('darth/deathstar') 18 | spec.can_handle() # => True / False 19 | spec.environment # => YAML string 20 | spec.msg # => Error messages 21 | :raises: EnvironmentFileDoesNotExist, EnvironmentFileNotDownloaded 22 | """ 23 | 24 | _environment = None 25 | _username = None 26 | _packagename = None 27 | _package = None 28 | _file_data = None 29 | msg = None 30 | 31 | def __init__(self, name=None, **kwargs): 32 | self.name = name 33 | self.quiet = False 34 | if get_binstar is not None: 35 | self.binstar = get_binstar() 36 | else: 37 | self.binstar = None 38 | 39 | def can_handle(self): 40 | """ 41 | Validates loader can process environment definition. 42 | :return: True or False 43 | """ 44 | # TODO: log information about trying to find the package in binstar.org 45 | if self.valid_name(): 46 | if self.binstar is None: 47 | self.msg = "Please install binstar" 48 | return False 49 | return self.package is not None and self.valid_package() 50 | return False 51 | 52 | def valid_name(self): 53 | """ 54 | Validates name 55 | :return: True or False 56 | """ 57 | if re.match("^(.+)/(.+)$", str(self.name)) is not None: 58 | return True 59 | elif self.name is None: 60 | self.msg = "Can't process without a name" 61 | else: 62 | self.msg = "Invalid name, try the format: user/package" 63 | return False 64 | 65 | def valid_package(self): 66 | """ 67 | Returns True if package has an environment file 68 | :return: True or False 69 | """ 70 | return len(self.file_data) > 0 71 | 72 | @property 73 | def file_data(self): 74 | if self._file_data is None: 75 | self._file_data = [data for data in self.package['files'] if data['type'] == ENVIRONMENT_TYPE] 76 | return self._file_data 77 | 78 | @property 79 | def environment(self): 80 | """ 81 | :raises: EnvironmentFileNotDownloaded 82 | """ 83 | if self._environment is None: 84 | versions = [{ 85 | 'normalized': normalized_version(d['version']), 86 | 'original': d['version']} for d in self.file_data] 87 | latest_version = max(versions, key=lambda x: x['normalized'])['original'] 88 | file_data = [data for data in self.package['files'] if data['version'] == latest_version] 89 | req = self.binstar.download(self.username, self.packagename, latest_version, file_data[0]['basename']) 90 | if req is None: 91 | raise EnvironmentFileNotDownloaded(self.username, self.packagename) 92 | self._environment = req.text 93 | return env.from_yaml(self._environment) 94 | 95 | @property 96 | def package(self): 97 | if self._package is None: 98 | try: 99 | self._package = self.binstar.package(self.username, self.packagename) 100 | except errors.NotFound: 101 | self.msg = "{} was not found on anaconda.org.\n"\ 102 | "You may need to be logged in. Try running:\n"\ 103 | " anaconda login".format(self.name) 104 | return self._package 105 | 106 | @property 107 | def username(self): 108 | if self._username is None: 109 | self._username = self.parse()[0] 110 | return self._username 111 | 112 | @property 113 | def packagename(self): 114 | if self._packagename is None: 115 | self._packagename = self.parse()[1] 116 | return self._packagename 117 | 118 | def parse(self): 119 | """Parse environment definition handle""" 120 | return self.name.split('/', 1) 121 | -------------------------------------------------------------------------------- /conda_env/cli/main_update.py: -------------------------------------------------------------------------------- 1 | from argparse import RawDescriptionHelpFormatter 2 | import os 3 | import textwrap 4 | import sys 5 | 6 | from conda import config 7 | from conda.cli import common 8 | from conda.cli import install as cli_install 9 | from conda.misc import touch_nonadmin 10 | 11 | from ..installers.base import get_installer, InvalidInstaller 12 | from .. import specs as install_specs 13 | from .. import exceptions 14 | 15 | description = """ 16 | Update the current environment based on environment file 17 | """ 18 | 19 | example = """ 20 | examples: 21 | conda env update 22 | conda env update -n=foo 23 | conda env update -f=/path/to/environment.yml 24 | conda env update --name=foo --file=environment.yml 25 | conda env update vader/deathstar 26 | """ 27 | 28 | 29 | def configure_parser(sub_parsers): 30 | p = sub_parsers.add_parser( 31 | 'update', 32 | formatter_class=RawDescriptionHelpFormatter, 33 | description=description, 34 | help=description, 35 | epilog=example, 36 | ) 37 | p.add_argument( 38 | '-n', '--name', 39 | action='store', 40 | help='name of environment (in %s)' % os.pathsep.join(config.envs_dirs), 41 | default=None, 42 | ) 43 | p.add_argument( 44 | '-f', '--file', 45 | action='store', 46 | help='environment definition (default: environment.yml)', 47 | default='environment.yml', 48 | ) 49 | p.add_argument( 50 | '--prune', 51 | action='store_true', 52 | default=False, 53 | help='remove installed packages not defined in environment.yml', 54 | ) 55 | p.add_argument( 56 | '-q', '--quiet', 57 | action='store_true', 58 | default=False, 59 | ) 60 | p.add_argument( 61 | 'remote_definition', 62 | help='remote environment definition / IPython notebook', 63 | action='store', 64 | default=None, 65 | nargs='?' 66 | ) 67 | common.add_parser_json(p) 68 | p.set_defaults(func=execute) 69 | 70 | 71 | def execute(args, parser): 72 | name = args.remote_definition or args.name 73 | 74 | try: 75 | spec = install_specs.detect(name=name, filename=args.file, 76 | directory=os.getcwd()) 77 | env = spec.environment 78 | except exceptions.SpecNotFound as e: 79 | common.error_and_exit(str(e), json=args.json) 80 | 81 | if not args.name: 82 | if not env.name: 83 | # Note, this is a hack fofr get_prefix that assumes argparse results 84 | # TODO Refactor common.get_prefix 85 | name = os.environ.get('CONDA_DEFAULT_ENV', False) 86 | if not name: 87 | msg = "Unable to determine environment\n\n" 88 | msg += textwrap.dedent(""" 89 | Please re-run this command with one of the following options: 90 | 91 | * Provide an environment name via --name or -n 92 | * Re-run this command inside an activated conda environment.""").lstrip() 93 | # TODO Add json support 94 | common.error_and_exit(msg, json=False) 95 | 96 | # Note: stubbing out the args object as all of the 97 | # conda.cli.common code thinks that name will always 98 | # be specified. 99 | args.name = env.name 100 | 101 | prefix = common.get_prefix(args, search=False) 102 | # CAN'T Check with this function since it assumes we will create prefix. 103 | # cli_install.check_prefix(prefix, json=args.json) 104 | 105 | # TODO, add capability 106 | # common.ensure_override_channels_requires_channel(args) 107 | # channel_urls = args.channel or () 108 | 109 | for installer_type, specs in env.dependencies.items(): 110 | try: 111 | installer = get_installer(installer_type) 112 | installer.install(prefix, specs, args, env, prune=args.prune) 113 | except InvalidInstaller: 114 | sys.stderr.write(textwrap.dedent(""" 115 | Unable to install package for {0}. 116 | 117 | Please double check and ensure you dependencies file has 118 | the correct spelling. You might also try installing the 119 | conda-env-{0} package to see if provides the required 120 | installer. 121 | """).lstrip().format(installer_type) 122 | ) 123 | return -1 124 | 125 | touch_nonadmin(prefix) 126 | if not args.json: 127 | cli_install.print_activate(args.name if args.name else prefix) 128 | -------------------------------------------------------------------------------- /conda_env/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | from collections import OrderedDict 3 | from copy import copy 4 | import os 5 | import re 6 | import sys 7 | 8 | # TODO This should never have to import from conda.cli 9 | from itertools import chain 10 | 11 | from conda.cli import common 12 | from conda import install 13 | from conda import config 14 | 15 | from . import compat 16 | from . import exceptions 17 | from . import yaml 18 | from conda_env.pip_util import add_pip_installed 19 | 20 | 21 | def load_from_directory(directory): 22 | """Load and return an ``Environment`` from a given ``directory``""" 23 | files = ['environment.yml', 'environment.yaml'] 24 | while True: 25 | for f in files: 26 | try: 27 | return from_file(os.path.join(directory, f)) 28 | except exceptions.EnvironmentFileNotFound: 29 | pass 30 | old_directory = directory 31 | directory = os.path.dirname(directory) 32 | if directory == old_directory: 33 | break 34 | raise exceptions.EnvironmentFileNotFound(files[0]) 35 | 36 | 37 | # TODO This should lean more on conda instead of divining it from the outside 38 | # TODO tests!!! 39 | def from_environment(name, prefix, no_builds=False): 40 | installed = install.linked(prefix) 41 | conda_pkgs = copy(installed) 42 | # json=True hides the output, data is added to installed 43 | add_pip_installed(prefix, installed, json=True) 44 | 45 | pip_pkgs = sorted(installed - conda_pkgs) 46 | 47 | if no_builds: 48 | dependencies = ['='.join(a.rsplit('-', 2)[0:2]) for a in sorted(conda_pkgs)] 49 | else: 50 | dependencies = ['='.join(a.rsplit('-', 2)) for a in sorted(conda_pkgs)] 51 | if len(pip_pkgs) > 0: 52 | dependencies.append({'pip': ['=='.join(a.rsplit('-', 2)[:2]) for a in pip_pkgs]}) 53 | 54 | # conda uses ruamel_yaml which returns a ruamel_yaml.comments.CommentedSeq 55 | # this doesn't dump correctly using pyyaml 56 | channels = list(config.get_rc_urls()) 57 | 58 | return Environment(name=name, dependencies=dependencies, channels=channels, prefix=prefix) 59 | 60 | 61 | def from_yaml(yamlstr, **kwargs): 62 | """Load and return a ``Environment`` from a given ``yaml string``""" 63 | data = yaml.load(yamlstr) 64 | if kwargs is not None: 65 | for key, value in kwargs.items(): 66 | data[key] = value 67 | return Environment(**data) 68 | 69 | 70 | def from_file(filename): 71 | if not os.path.exists(filename): 72 | raise exceptions.EnvironmentFileNotFound(filename) 73 | with open(filename, 'r') as fp: 74 | yamlstr = fp.read() 75 | return from_yaml(yamlstr, filename=filename) 76 | 77 | 78 | # TODO test explicitly 79 | class Dependencies(OrderedDict): 80 | def __init__(self, raw, *args, **kwargs): 81 | super(Dependencies, self).__init__(*args, **kwargs) 82 | self.raw = raw 83 | self.parse() 84 | 85 | def parse(self): 86 | if not self.raw: 87 | return 88 | 89 | self.update({'conda': []}) 90 | 91 | for line in self.raw: 92 | if isinstance(line, dict): 93 | self.update(line) 94 | else: 95 | self['conda'].append(common.arg2spec(line)) 96 | 97 | # TODO only append when it's not already present 98 | def add(self, package_name): 99 | self.raw.append(package_name) 100 | self.parse() 101 | 102 | 103 | def unique(seq, key=None): 104 | """ Return only unique elements of a sequence 105 | >>> tuple(unique((1, 2, 3))) 106 | (1, 2, 3) 107 | >>> tuple(unique((1, 2, 1, 3))) 108 | (1, 2, 3) 109 | Uniqueness can be defined by key keyword 110 | >>> tuple(unique(['cat', 'mouse', 'dog', 'hen'], key=len)) 111 | ('cat', 'mouse') 112 | """ 113 | seen = set() 114 | seen_add = seen.add 115 | if key is None: 116 | for item in seq: 117 | if item not in seen: 118 | seen_add(item) 119 | yield item 120 | else: # calculate key 121 | for item in seq: 122 | val = key(item) 123 | if val not in seen: 124 | seen_add(val) 125 | yield item 126 | 127 | 128 | class Environment(object): 129 | def __init__(self, name=None, filename=None, channels=None, 130 | dependencies=None, prefix=None): 131 | self.name = name 132 | self.filename = filename 133 | self.prefix = prefix 134 | self.dependencies = Dependencies(dependencies) 135 | 136 | if channels is None: 137 | channels = [] 138 | self.channels = channels 139 | 140 | def add_channels(self, channels): 141 | self.channels = list(unique(chain.from_iterable((channels, self.channels)))) 142 | 143 | def remove_channels(self): 144 | self.channels = [] 145 | 146 | def to_dict(self): 147 | d = yaml.dict([('name', self.name)]) 148 | if self.channels: 149 | d['channels'] = self.channels 150 | if self.dependencies: 151 | d['dependencies'] = self.dependencies.raw 152 | if self.prefix: 153 | d['prefix'] = self.prefix 154 | return d 155 | 156 | def to_yaml(self, stream=None): 157 | d = self.to_dict() 158 | out = compat.u(yaml.dump(d, default_flow_style=False)) 159 | if stream is None: 160 | return out 161 | stream.write(compat.b(out, encoding="utf-8")) 162 | 163 | def save(self): 164 | with open(self.filename, "wb") as fp: 165 | self.to_yaml(stream=fp) 166 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to conda-env 2 | 3 | So you wanna contribute?! Awesome! :+1: 4 | 5 | There are a couple of ways you can contribute to the development. Not all of 6 | which require you do any coding. 7 | 8 | 9 | ## Reporting Issues 10 | 11 | We try are best to make sure there aren't issues with all of code we release, 12 | but occasionally bugs (:bug:) make it through and are released. Here's a 13 | checklist of things to do: 14 | 15 | * [ ] Make sure you're on the latest version of `conda-env`. You can do that 16 | by running `conda update conda-env`. 17 | * [ ] If you're adventurous, you can try running the latest development version 18 | by running `conda update --channel conda/c/dev conda-env` (you can also 19 | use `-c` as the short parameter). 20 | 21 | If updating `conda-env` doesn't work for you, the next thing to d 22 | [open a report][]. Here are a few things to make sure you include to help us 23 | out the most: 24 | 25 | * [ ] Explain what you expect to happen and what actually happened. 26 | * [ ] Add the steps you used to reproduce the error. 27 | * [ ] Include the command you ran and *all* of the output. The error message is 28 | useful, but often information that's displayed before that can point us in 29 | the right direction. 30 | * [ ] Include all of the output from `conda info --all` (or `conda info -a`). 31 | * [ ] Include the output from `conda list conda` (note: you need to include 32 | `--name root` if you have an environment activated). 33 | 34 | The last three outputs can be posted in a [gist][] and included in the issue as 35 | a link. 36 | 37 | 38 | ## Run development versions 39 | 40 | We automatically build new versions of `conda-env` using [binstar build][]. 41 | Every commit that hits our [develop branch][] is automatically built and added 42 | to our [conda/c/dev][] channel. You can add that channels to your conda config 43 | and you will always be on the latest versions of conda-env. 44 | 45 | > **Warning**: This is meant for people who are interested in actively helping 46 | > further development of conda-env. You should only use it if you're ok with 47 | > conda-env occasionally being broken while we work out issues before a release. 48 | 49 | ```bash 50 | conda config --add channels conda/c/dev 51 | ``` 52 | 53 | ## Contribute Code 54 | 55 | The easiest thing to do is contribute a feature or fix a bug that you've found. 56 | If you want to contribute but don't know where to start, check out our 57 | (hopefully) small list of [open bugs][]. If you want to take a stab at adding 58 | something, check out the list of [open enhancement requests][]. 59 | 60 | Once you know what you're going to add, there's a few things you can do to help 61 | us figure out how to process your code: 62 | 63 | * [Create a fork](https://github.com/conda/conda-env/fork) 64 | * Create a branch with your change off of `develop`. Follow these naming 65 | conventions for naming your branches: 66 | * `feature/{{ feature name }}` <-- new features 67 | * `fix/{{ issue number or general bug description }}` <-- fixes to the code 68 | * `refactor/{{ refactor name / description }}` <-- general cleanup / 69 | refactoring 70 | * Commit your changes to your branch. Changes with extensive unit tests are 71 | given priority because they simplify the process of verifying the code. 72 | * Open a pull request against the `develop` branch 73 | * :tada: profit! 74 | 75 | There's a lot happening in conda-land. We might not always get right back to 76 | you and your PR, but we'll try to do it as quickly as possible. If you haven't 77 | heard anything after a few days to a week, please comment again to make sure 78 | notices are sent back out. 79 | 80 | 81 | ## Coding Guidelines 82 | 83 | The rest of this document is aimed at people developing and releasing conda-env. 84 | 85 | 86 | ### Coding Style 87 | 88 | * Please run all of your code through [flake8][] (yes, it's more strict than 89 | straight [pep8][], but it helps ensure a consistent visual style for our 90 | code). 91 | * Please include tests for all new code and bugfixes. The tests are run using 92 | [pytest][] so you can use things like [parameterized tests][] and such. 93 | * Please [mark any tests][] that are slow with the `@pytest.mark.slow` 94 | decorator so they can be skipped when needed. 95 | 96 | 97 | ### Releasing conda-env 98 | 99 | conda-env follows [SemVer][] with one specific change: pre-release versions are 100 | denoted without using a hyphen. For example, a development version of conda-env 101 | might be `2.1alpha.0`. Anything released with the `alpha` must be treated as 102 | not-final code. 103 | 104 | * The `develop` branch should always have a version number that is +1 minor 105 | version ahead of what is in `master`. For example, if the latest code in 106 | `master` is `v2.0.2`, the `develop` branch should be `v2.1alpha.0`. 107 | 108 | 109 | #### Merging Feature Releases to `master` 110 | 111 | If you're ready to release, open a PR to ensure that `develop` has everything 112 | that it needs for this release, including any **updates to change logs** and such. 113 | Do not merge `conda-env` directly via GitHub's interface. Instead, follow 114 | these steps from your working tree (note: this assumes you have 115 | github.com/conda/conda-env.git setup as the remote `conda`): 116 | 117 | * `git fetch conda-env` 118 | * `git checkout master` 119 | * `git merge --no-ff --no-commit conda-env/develop` 120 | * Modify the `setup.py` and `conda.recipe/meta.yaml` to remove the `alpha` from 121 | the minor version. 122 | * `git commit` the changes. Ensure that the subject line includes the version 123 | number at the end of the message. You may also wish to include a descriptive 124 | sentence explaining the main feature(s) of the release. 125 | * `git tag vX.Y.Z` 126 | * `git push conda-env master --tags` 127 | * After binstar build has successfully built the new version, make sure that all 128 | builds are added to the `main` channel of conda-env. 129 | 130 | > Author's Note: It would be great to automate this entirely into a tool that 131 | > would build a release for you! 132 | 133 | 134 | #### Handling Bugfix Releases 135 | 136 | You should create a new branch called `vX.Y.Z-prep` from the tag and increment 137 | the bugfix version number (`Z` in this example) by one and add `alpha`. For 138 | example, if the latest release is `v2.0.2`, you would create a branch called 139 | `v2.0.3alpha` and the first commit should be the changes to `setup.py` and 140 | `conda.recipe/meta.yaml`. 141 | 142 | Please open a pull request for this fix, but do not merge via GitHub. Instead, 143 | follow the outline above to handle the bugfix release. Once merged, make sure 144 | to merge `master` into `develop` and push that branch as well so the bugfix is 145 | included in future versions. 146 | 147 | 148 | [binstar build]: http://docs.anaconda.org/build_cli.html 149 | [conda/c/dev]: https://conda.anaconda.org/conda/c/dev 150 | [develop branch]: https://github.com/conda/conda-env/tree/develop 151 | [flake8]: http://flake8.readthedocs.org/ 152 | [gist]: https://gist.github.com/ 153 | [mark any tests]: http://pytest.org/latest/example/markers.html 154 | [open a report]: https://github.com/conda/conda-env/issues/new 155 | [open bugs]: https://github.com/conda/conda-env/issues?q=is%3Aopen+is%3Aissue+label%3Abug 156 | [open enhancement requests]: https://github.com/conda/conda-env/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement 157 | [parameterized tests]: http://pytest.org/latest/parametrize.html#parametrize 158 | [pep8]: https://www.python.org/dev/peps/pep-0008/ 159 | [pytest]: http://pytest.org/latest/ 160 | [SemVer]: http://semver.org/ 161 | -------------------------------------------------------------------------------- /tests/test_env.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import os 3 | import sys 4 | import random 5 | import textwrap 6 | import unittest 7 | import yaml 8 | 9 | try: 10 | from io import StringIO 11 | except ImportError: 12 | from StringIO import StringIO 13 | 14 | from conda_env import env 15 | from conda_env import exceptions 16 | 17 | from . import utils 18 | 19 | 20 | class FakeStream(object): 21 | def __init__(self): 22 | self.output = '' 23 | 24 | def write(self, chunk): 25 | self.output += chunk.decode('utf-8') 26 | 27 | 28 | def get_simple_environment(): 29 | return env.from_file(utils.support_file('simple.yml')) 30 | 31 | 32 | class from_file_TestCase(unittest.TestCase): 33 | def test_returns_Environment(self): 34 | e = get_simple_environment() 35 | self.assertIsInstance(e, env.Environment) 36 | 37 | def test_retains_full_filename(self): 38 | e = get_simple_environment() 39 | self.assertEqual(utils.support_file('simple.yml'), e.filename) 40 | 41 | def test_with_pip(self): 42 | e = env.from_file(utils.support_file('with-pip.yml')) 43 | self.assert_('pip' in e.dependencies) 44 | self.assert_('foo' in e.dependencies['pip']) 45 | self.assert_('baz' in e.dependencies['pip']) 46 | 47 | 48 | class EnvironmentTestCase(unittest.TestCase): 49 | def test_has_empty_filename_by_default(self): 50 | e = env.Environment() 51 | self.assertEqual(e.filename, None) 52 | 53 | def test_has_filename_if_provided(self): 54 | r = random.randint(100, 200) 55 | random_filename = '/path/to/random/environment-{}.yml'.format(r) 56 | e = env.Environment(filename=random_filename) 57 | self.assertEqual(e.filename, random_filename) 58 | 59 | def test_has_empty_name_by_default(self): 60 | e = env.Environment() 61 | self.assertEqual(e.name, None) 62 | 63 | def test_has_name_if_provided(self): 64 | random_name = 'random-{}'.format(random.randint(100, 200)) 65 | e = env.Environment(name=random_name) 66 | self.assertEqual(e.name, random_name) 67 | 68 | def test_dependencies_are_empty_by_default(self): 69 | e = env.Environment() 70 | self.assertEqual(0, len(e.dependencies)) 71 | 72 | def test_parses_dependencies_from_raw_file(self): 73 | e = get_simple_environment() 74 | expected = OrderedDict([('conda', ['nltk'])]) 75 | self.assertEqual(e.dependencies, expected) 76 | 77 | def test_builds_spec_from_line_raw_dependency(self): 78 | # TODO Refactor this inside conda to not be a raw string 79 | e = env.Environment(dependencies=['nltk=3.0.0=np18py27']) 80 | expected = OrderedDict([('conda', ['nltk 3.0.0 np18py27'])]) 81 | self.assertEqual(e.dependencies, expected) 82 | 83 | def test_args_are_wildcarded(self): 84 | e = env.Environment(dependencies=['python=2.7']) 85 | expected = OrderedDict([('conda', ['python 2.7*'])]) 86 | self.assertEqual(e.dependencies, expected) 87 | 88 | def test_other_tips_of_dependencies_are_supported(self): 89 | e = env.Environment( 90 | dependencies=['nltk', {'pip': ['foo', 'bar']}] 91 | ) 92 | expected = OrderedDict([ 93 | ('conda', ['nltk']), 94 | ('pip', ['foo', 'bar']) 95 | ]) 96 | self.assertEqual(e.dependencies, expected) 97 | 98 | def test_channels_default_to_empty_list(self): 99 | e = env.Environment() 100 | self.assertIsInstance(e.channels, list) 101 | self.assertEqual(e.channels, []) 102 | 103 | def test_add_channels(self): 104 | e = env.Environment() 105 | e.add_channels(['dup', 'dup', 'unique']) 106 | self.assertEqual(e.channels, ['dup', 'unique']) 107 | 108 | def test_remove_channels(self): 109 | e = env.Environment(channels=['channel']) 110 | e.remove_channels() 111 | self.assertEqual(e.channels, []) 112 | 113 | def test_channels_are_provided_by_kwarg(self): 114 | random_channels = (random.randint(100, 200), random) 115 | e = env.Environment(channels=random_channels) 116 | self.assertEqual(e.channels, random_channels) 117 | 118 | def test_to_dict_returns_dictionary_of_data(self): 119 | random_name = 'random{}'.format(random.randint(100, 200)) 120 | e = env.Environment( 121 | name=random_name, 122 | channels=['javascript'], 123 | dependencies=['nodejs'] 124 | ) 125 | 126 | expected = { 127 | 'name': random_name, 128 | 'channels': ['javascript'], 129 | 'dependencies': ['nodejs'] 130 | } 131 | self.assertEqual(e.to_dict(), expected) 132 | 133 | def test_to_dict_returns_just_name_if_only_thing_present(self): 134 | e = env.Environment(name='simple') 135 | expected = {'name': 'simple'} 136 | self.assertEqual(e.to_dict(), expected) 137 | 138 | def test_to_yaml_returns_yaml_parseable_string(self): 139 | random_name = 'random{}'.format(random.randint(100, 200)) 140 | e = env.Environment( 141 | name=random_name, 142 | channels=['javascript'], 143 | dependencies=['nodejs'] 144 | ) 145 | 146 | expected = { 147 | 'name': random_name, 148 | 'channels': ['javascript'], 149 | 'dependencies': ['nodejs'] 150 | } 151 | 152 | actual = yaml.load(StringIO(e.to_yaml())) 153 | self.assertEqual(expected, actual) 154 | 155 | def test_to_yaml_returns_proper_yaml(self): 156 | random_name = 'random{}'.format(random.randint(100, 200)) 157 | e = env.Environment( 158 | name=random_name, 159 | channels=['javascript'], 160 | dependencies=['nodejs'] 161 | ) 162 | 163 | expected = '\n'.join([ 164 | "name: %s" % random_name, 165 | "channels:", 166 | "- javascript", 167 | "dependencies:", 168 | "- nodejs", 169 | "" 170 | ]) 171 | 172 | actual = e.to_yaml() 173 | self.assertEqual(expected, actual) 174 | 175 | def test_to_yaml_takes_stream(self): 176 | random_name = 'random{}'.format(random.randint(100, 200)) 177 | e = env.Environment( 178 | name=random_name, 179 | channels=['javascript'], 180 | dependencies=['nodejs'] 181 | ) 182 | 183 | s = FakeStream() 184 | e.to_yaml(stream=s) 185 | 186 | expected = "\n".join([ 187 | 'name: %s' % random_name, 188 | 'channels:', 189 | '- javascript', 190 | 'dependencies:', 191 | '- nodejs', 192 | '', 193 | ]) 194 | self.assertEqual(expected, s.output) 195 | 196 | def test_can_add_dependencies_to_environment(self): 197 | e = get_simple_environment() 198 | e.dependencies.add('bar') 199 | 200 | s = FakeStream() 201 | e.to_yaml(stream=s) 202 | 203 | expected = "\n".join([ 204 | 'name: nlp', 205 | 'dependencies:', 206 | '- nltk', 207 | '- bar', 208 | '' 209 | ]) 210 | self.assertEqual(expected, s.output) 211 | 212 | def test_dependencies_update_after_adding(self): 213 | e = get_simple_environment() 214 | self.assert_('bar' not in e.dependencies['conda']) 215 | e.dependencies.add('bar') 216 | self.assert_('bar' in e.dependencies['conda']) 217 | 218 | 219 | class DirectoryTestCase(unittest.TestCase): 220 | directory = utils.support_file('example') 221 | 222 | def setUp(self): 223 | self.original_working_dir = os.getcwd() 224 | self.env = env.load_from_directory(self.directory) 225 | 226 | def tearDown(self): 227 | os.chdir(self.original_working_dir) 228 | 229 | def test_returns_env_object(self): 230 | self.assertIsInstance(self.env, env.Environment) 231 | 232 | def test_has_expected_name(self): 233 | self.assertEqual('test', self.env.name) 234 | 235 | def test_has_dependencies(self): 236 | self.assertEqual(1, len(self.env.dependencies['conda'])) 237 | self.assert_('numpy' in self.env.dependencies['conda']) 238 | 239 | 240 | class load_from_directory_example_TestCase(DirectoryTestCase): 241 | directory = utils.support_file('example') 242 | 243 | 244 | class load_from_directory_example_yaml_TestCase(DirectoryTestCase): 245 | directory = utils.support_file('example-yaml') 246 | 247 | 248 | class load_from_directory_recursive_TestCase(DirectoryTestCase): 249 | directory = utils.support_file('foo/bar') 250 | 251 | 252 | class load_from_directory_recursive_two_TestCase(DirectoryTestCase): 253 | directory = utils.support_file('foo/bar/baz') 254 | 255 | 256 | class load_from_directory_trailing_slash_TestCase(DirectoryTestCase): 257 | directory = utils.support_file('foo/bar/baz/') 258 | 259 | 260 | class load_from_directory_TestCase(unittest.TestCase): 261 | def test_raises_when_unable_to_find(self): 262 | with self.assertRaises(exceptions.EnvironmentFileNotFound): 263 | env.load_from_directory('/path/to/unknown/env-spec') 264 | 265 | def test_raised_exception_has_environment_yml_as_file(self): 266 | with self.assertRaises(exceptions.EnvironmentFileNotFound) as e: 267 | env.load_from_directory('/path/to/unknown/env-spec') 268 | self.assertEqual(e.exception.filename, 'environment.yml') 269 | 270 | 271 | class LoadEnvFromFileAndSaveTestCase(unittest.TestCase): 272 | env_path = utils.support_file(os.path.join('saved-env', 'environment.yml')) 273 | 274 | def setUp(self): 275 | with open(self.env_path, "rb") as fp: 276 | self.original_file_contents = fp.read() 277 | self.env = env.load_from_directory(self.env_path) 278 | 279 | def tearDown(self): 280 | with open(self.env_path, "wb") as fp: 281 | fp.write(self.original_file_contents) 282 | 283 | def test_expected_default_conditions(self): 284 | self.assertEqual(1, len(self.env.dependencies['conda'])) 285 | 286 | def test(self): 287 | self.env.dependencies.add('numpy') 288 | self.env.save() 289 | 290 | e = env.load_from_directory(self.env_path) 291 | self.assertEqual(2, len(e.dependencies['conda'])) 292 | self.assert_('numpy' in e.dependencies['conda']) 293 | 294 | 295 | class EnvironmentSaveTestCase(unittest.TestCase): 296 | env_file = utils.support_file('saved.yml') 297 | 298 | def tearDown(self): 299 | if os.path.exists(self.env_file): 300 | os.unlink(self.env_file) 301 | 302 | def test_creates_file_on_save(self): 303 | self.assertFalse(os.path.exists(self.env_file), msg='sanity check') 304 | 305 | e = env.Environment(filename=self.env_file, name='simple') 306 | e.save() 307 | 308 | self.assertTrue(os.path.exists(self.env_file)) 309 | 310 | def _test_saves_yaml_representation_of_file(self): 311 | e = env.Environment(filename=self.env_file, name='simple') 312 | e.save() 313 | 314 | with open(self.env_file, "rb") as fp: 315 | actual = fp.read() 316 | 317 | self.assert_(len(actual) > 0, msg='sanity check') 318 | self.assertEqual(e.to_yaml(), actual) 319 | --------------------------------------------------------------------------------