Skip to content
This repository has been archived by the owner on Jan 30, 2019. It is now read-only.

jinja2 + envvars and aliases #88

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,84 @@ in your local package cache.

You can explicitly provide an environment spec file using ``-f`` or ``--file``
and the name of the file you would like to use.


``environment.yml`` jinja2 rendering
------------------------------------

If you have ``jinja2`` available in the environment, ``environment.yml`` files will be
rendered with it before processing.

.. code-block:: yaml

name: pytest
dependencies:
{% for i in ['xunit', 'coverage','mock'] %}
- pytest-{{ i }}
{% endfor %}

In this example, the previous file with ``jinja2`` syntax is equivalent to:

.. code-block:: yaml

name: pytest
dependencies:
- pytest-xunit
- pytest-coverage
- pytest-mock

Available variables
^^^^^^^^^^^^^^^^^^^

When using ``jinja2``, on top of the usual template capabilities, you have access to the
following variables:

- ``root``: The directory containing ``environment.yml``
- ``os``: Python's ``os`` module.


``environment.yml`` examples
----------------------------

Name and dependencies
^^^^^^^^^^^^^^^^^^^^^

.. code-block:: yaml

name: stats
dependencies:
- numpy
- pandas

Name and version specific dependencies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: yaml

name: stats
dependencies:
- numpy==1.8
- pandas==0.16.1


Environment/aliases
^^^^^^^^^^^^^^^^^^^

.. code-block:: yaml

name: oracle
dependencies:
- oracle_instantclient

# List type environment variables will be joined with os.pathsep (':' in unix, ';' in windows).
# These values will be inserted in front of any existing value in the current environment.
# e.g.:
# current PATH: "/usr/local/bin:/usr/bin"
# new PATH: "{{ root }}/bin:/usr/local/bin:/usr/bin"
environment:
- ORACLE_HOME: /usr/local/oracle_instantclient
- PATH:
- {{ root }}/bin

aliases:
run_db: bash {{ root }}/bin/run_db.sh
7 changes: 4 additions & 3 deletions bin/activate
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ get_dirname() {
}

run_scripts() {
_PREFIX="$(echo $(echo $PATH | awk -F ':' '{print $1}')/..)"
_CONDA_D="${_PREFIX}/etc/conda/$1.d"
_CONDA_D="${CONDA_ENV_PATH}/etc/conda/$1.d"
if [[ -d $_CONDA_D ]]; then
for f in $(find $_CONDA_D -name "*.sh"); do source $f; done
fi
Expand Down Expand Up @@ -67,7 +66,9 @@ if (( $? == 0 )); then
export CONDA_DEFAULT_ENV="$@"
fi

export CONDA_ENV_PATH=$(get_dirname $_THIS_DIR)
# Get first path and convert it to absolute
ENV_BIN_DIR="$(echo $(echo $PATH | awk -F ':' '{print $1}'))"
export CONDA_ENV_PATH="$(get_dirname "$ENV_BIN_DIR")"

if (( $("$_THIS_DIR/conda" ..changeps1) )); then
CONDA_OLD_PS1="$PS1"
Expand Down
3 changes: 1 addition & 2 deletions bin/deactivate
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ get_dirname() {
}

run_scripts() {
_PREFIX="$(echo $(echo $PATH | awk -F ':' '{print $1}')/..)"
_CONDA_D="${_PREFIX}/etc/conda/$1.d"
_CONDA_D="${CONDA_ENV_PATH}/etc/conda/$1.d"
if [[ -d $_CONDA_D ]]; then
for f in $(find $_CONDA_D -name "*.sh"); do source $f; done
fi
Expand Down
46 changes: 45 additions & 1 deletion conda_env/cli/main_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ def configure_parser(sub_parsers):


def execute(args, parser):

name = None
if args.old_name:
print("--name is deprecated. Use the following command instead:\n"
Expand Down Expand Up @@ -103,6 +102,51 @@ def execute(args, parser):
)
return -1

write_activate_deactivate(env, prefix)

touch_nonadmin(prefix)
if not args.json:
cli_install.print_activate(args.name if args.name else prefix)


def write_activate_deactivate(env, prefix):
'''Write activate/deactivate environment variable/aliases scripts'''
if not env.environment and not env.aliases:
return

# Create directories
conda_dir = os.path.join(prefix, 'etc', 'conda')
activate_dir = os.path.join(conda_dir, 'activate.d')
deactivate_dir = os.path.join(conda_dir, 'deactivate.d')
for directory in [conda_dir, activate_dir, deactivate_dir]:
if not os.path.exists(directory):
os.makedirs(directory)

# Copy print_env.py
import shutil
shutil.copyfile(
os.path.join(os.path.dirname(__file__), '..', 'print_env.py'),
os.path.join(conda_dir, 'print_env.py'),
)

# Create activate and deactivate scripts
if sys.platform == 'win32':
ext = '.bat'
source = 'call'
rm = 'del'
else:
ext = '.sh'
source = 'source'
rm = 'rm'

with open(os.path.join(activate_dir, '_activate' + ext), 'w') as activate:
activate.write('python "%s" activate "%s" "%s" > _tmp_activate%s\n' % \
(os.path.join(conda_dir, 'print_env.py'), repr(env.environment), repr(env.aliases), ext))
activate.write(source + ' _tmp_activate%s\n' % ext)
activate.write(rm + ' _tmp_activate%s\n' % ext)

with open(os.path.join(deactivate_dir, '_deactivate' + ext), 'w') as deactivate:
deactivate.write('python "%s" deactivate "%s" "%s" > _tmp_deactivate%s\n' % \
(os.path.join(conda_dir, 'print_env.py'), repr(env.environment), repr(env.aliases), ext))
deactivate.write(source + ' _tmp_deactivate%s\n' % ext)
deactivate.write(rm + ' _tmp_deactivate%s\n' % ext)
43 changes: 37 additions & 6 deletions conda_env/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,37 @@ def from_environment(name, prefix):
return Environment(name=name, dependencies=dependencies)


# TODO: This is duplicated from conda_build. Could yaml parsing from both libraries
# be merged instead of duplicated? This could include jinja2 and "# [unix]" flags.
def render_jinja(content, **kwargs):
try:
import jinja2
except ImportError:
return content

# Add {{ root }} to render dict
if 'filename' in kwargs:
kwargs['root'] = os.path.dirname(os.path.abspath(kwargs['filename']))

# Add {{ os }} to render dict
kwargs['os'] = os

return jinja2.Template(content).render(**kwargs)


def from_yaml(yamlstr, **kwargs):
"""Load and return a ``Environment`` from a given ``yaml string``"""
data = yaml.load(yamlstr)
yamlstr = render_jinja(yamlstr, **kwargs)

try:
data = yaml.load(yamlstr)
except yaml.parser.ParserError:
try:
import jinja2
except ImportError:
raise exceptions.UnableToParseMissingJinja2()
raise

if kwargs is not None:
for key, value in kwargs.items():
data[key] = value
Expand Down Expand Up @@ -89,21 +117,24 @@ def add(self, package_name):

class Environment(object):
def __init__(self, name=None, filename=None, channels=None,
dependencies=None):
dependencies=None, environment=None, aliases=None):
self.name = name
self.filename = filename
self.dependencies = Dependencies(dependencies)

if channels is None:
channels = []
self.channels = channels
self.channels = channels or []
self.environment = environment or {}
self.aliases = aliases or {}

def to_dict(self):
d = yaml.dict([('name', self.name)])
if self.channels:
d['channels'] = self.channels
if self.dependencies:
d['dependencies'] = self.dependencies.raw
if self.environment:
d['environment'] = self.environment
if self.aliases:
d['aliases'] = self.aliases
return d

def to_yaml(self, stream=None):
Expand Down
9 changes: 9 additions & 0 deletions conda_env/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ class InvalidLoader(Exception):
def __init__(self, name):
msg = 'Unable to load installer for {}'.format(name)
super(InvalidLoader, self).__init__(msg)


# TODO: This is copied from conda_build. Could yaml parsing from both libraries
# be merged instead of duplicated? This could include jinja2 and "# [unix]" flags.
class UnableToParseMissingJinja2(CondaEnvRuntimeError):
def __init__(self, *args, **kwargs):
msg = 'It appears you are missing jinja2. Please install that ' + \
'package, then attempt to create the environment.'
super(UnableToParseMissingJinja2, self).__init__(msg, *args, **kwargs)
114 changes: 114 additions & 0 deletions conda_env/print_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python
'''
Supports:
* bash
* cmd.exe (windows default shell)
* tcc.exe (windows https://jpsoft.com/take-command-windows-scripting.html)
'''
from __future__ import print_function
import os


def print_env(action, environment=[], aliases={}):
# Determine shell
shell = os.environ.get('SHELL', os.environ.get('COMSPEC'))
if shell is None:
raise RuntimeError('Could not determine shell from environment variables {SHELL, COMSPEC}')
shell = os.path.basename(shell).lower()
pathsep = os.pathsep

# Join duplicated environment variables
env = {}
for env_dict in environment:
for k, v in env_dict.items():
if isinstance(v, list):
env.setdefault(k, [])
env[k] += v
else:
env[k] = v

# Create environment configuration functions
if shell == 'bash':
def Export(name, value):
if isinstance(value, list):
value = pathsep.join(value)
return 'export %(name)s=%(value)s%(pathsep)s$%(name)s\n' % locals()
else:
return 'export %(name)s=%(value)s\n' % locals()
def Unset(name):
return 'unset %(name)s\n' % locals()
def Alias(name, value):
return 'alias %(name)s="%(value)s"\n' % locals()
def Unalias(name):
return '[ `alias | grep %(name)s= | wc -l` != 0 ] && unalias %(name)s\n' % locals()


elif shell == 'cmd.exe':
def Export(name, value):
if isinstance(value, list):
value = pathsep.join(value)
return 'set %(name)s=%(value)s%(pathsep)s%%%(name)s%%\n' % locals()
else:
return 'set %(name)s=%(value)s\n' % locals()
def Unset(name):
return 'set %(name)s=\n' % locals()
def Alias(name, value):
return 'doskey %(name)s=%(value)s\n' % locals()
def Unalias(name):
return 'doskey %(name)s=\n' % locals()

elif shell == 'tcc.exe':
def Export(name, value):
if isinstance(value, list):
value = pathsep.join(value)
return 'set %(name)s=%(value)s%(pathsep)s%%%(name)s%%\n' % locals()
else:
return 'set %(name)s=%(value)s\n' % locals()
def Unset(name):
return 'set %(name)s=\n' % locals()
def Alias(name, value):
return 'alias %(name)s %(value)s\n' % locals()
def Unalias(name):
return 'unalias %(name)s\n' % locals()

# Activate/Deactivate
if action == 'activate':
s = ''
for k, v in sorted(env.items()):
s += Export(k, v)
for k, v in sorted(aliases.items()):
s += Alias(k, v)
return s

elif action == 'deactivate':
s = ''
for k, v in sorted(env.items()):
if k not in os.environ:
continue

if isinstance(v, list):
current_value = os.environ[k].split(os.pathsep)
current_value = [c for c in current_value if c != '']
for path in v:
if path in current_value: current_value.remove(path)
if len(current_value) == 0:
s += Unset(k)
else:
s += Export(k, os.pathsep.join(current_value))
else:
s += Unset(k)

for alias in sorted(aliases):
s += Unalias(alias)
return s


if __name__ == '__main__':
import sys

action = sys.argv[1]
environment = eval(sys.argv[2])
aliases = eval(sys.argv[3])

s = print_env(action, environment, aliases)
print(s)
1 change: 1 addition & 0 deletions conda_env/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ def represent_ordereddict(dumper, data):

dump = yaml.dump
load = yaml.load
parser = yaml.parser
dict = OrderedDict
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
'conda_env',
'conda_env.cli',
'conda_env.installers',
'conda_env.specs',
'conda_env.utils',
],
scripts=[
'bin/conda-env',
Expand Down
9 changes: 9 additions & 0 deletions tests/support/with-jinja.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: with_jinja

dependencies:
{% for i in ['xunit', 'coverage','mock'] %}
- pytest-{{ i }}
{% endfor %}

environment:
PYTHON_DIR: {{ os.path.join(root, 'python') }}
Loading