Completed
Pull Request — master (#694)
by Eric
01:25
created

get_from_context()   C

Complexity

Conditions 7

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
c 1
b 0
f 1
dl 0
loc 37
rs 5.5
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
4
"""
5
cookiecutter.config
6
-------------------
7
8
Global configuration handling
9
"""
10
11
from __future__ import unicode_literals
12
import copy
13
import logging
14
import os
15
import io
16
17
import poyo
18
19
from .exceptions import ConfigDoesNotExistException
20
from .exceptions import InvalidConfiguration
21
22
23
logger = logging.getLogger(__name__)
24
25
USER_CONFIG_PATH = os.path.expanduser('~/.cookiecutterrc')
26
27
DEFAULT_CONFIG = {
28
    'cookiecutters_dir': os.path.expanduser('~/.cookiecutters/'),
29
    'replay_dir': os.path.expanduser('~/.cookiecutter_replay/'),
30
    'default_context': {}
31
}
32
33
34
def _expand_path(path):
35
    """Expand both environment variables and user home in the given path."""
36
    path = os.path.expandvars(path)
37
    path = os.path.expanduser(path)
38
    return path
39
40
41
def get_config(config_path):
42
    """
43
    Retrieve the config from the specified path, returning it as a config dict.
44
    """
45
46
    if not os.path.exists(config_path):
47
        raise ConfigDoesNotExistException
48
49
    logger.debug('config_path is {0}'.format(config_path))
50
    with io.open(config_path, encoding='utf-8') as file_handle:
51
        try:
52
            yaml_dict = poyo.parse_string(file_handle.read())
53
        except poyo.exceptions.PoyoException as e:
54
            raise InvalidConfiguration(
55
                'Unable to parse YAML file {}. Error: {}'
56
                ''.format(config_path, e)
57
            )
58
59
    config_dict = copy.copy(DEFAULT_CONFIG)
60
    config_dict.update(yaml_dict)
61
62
    raw_replay_dir = config_dict['replay_dir']
63
    config_dict['replay_dir'] = _expand_path(raw_replay_dir)
64
65
    raw_cookies_dir = config_dict['cookiecutters_dir']
66
    config_dict['cookiecutters_dir'] = _expand_path(raw_cookies_dir)
67
68
    return config_dict
69
70
71
def get_user_config(config_file=USER_CONFIG_PATH):
72
    """Retrieve the config from a file or return the defaults if None is
73
    passed. If an environment variable `COOKIECUTTER_CONFIG` is set up, try
74
    to load its value. Otherwise fall back to a default file or config.
75
    """
76
    # Do NOT load a config. Return defaults instead.
77
    if config_file is None:
78
        return copy.copy(DEFAULT_CONFIG)
79
80
    # Load the given config file
81
    if config_file and config_file is not USER_CONFIG_PATH:
82
        return get_config(config_file)
83
84
    try:
85
        # Does the user set up a config environment variable?
86
        env_config_file = os.environ['COOKIECUTTER_CONFIG']
87
    except KeyError:
88
        # Load an optional user config if it exists
89
        # otherwise return the defaults
90
        if os.path.exists(USER_CONFIG_PATH):
91
            return get_config(USER_CONFIG_PATH)
92
        else:
93
            return copy.copy(DEFAULT_CONFIG)
94
    else:
95
        # There is a config environment variable. Try to load it.
96
        # Do not check for existence, so invalid file paths raise an error.
97
        return get_config(env_config_file)
98
99
100
def get_from_context(context, key, default=None, update=False):
101
    """
102
    Get the value referenced by a given key from a given context
103
    Keys can be defined using dot notation to retrieve values from nested
104
    dictionaries
105
    Keys can contain integers to retrieve values from lists
106
    ie.
107
    context = {
108
        'cookiecutter': {
109
            'project_name': 'Project Name'
110
        }
111
    }
112
    project_name = get_from_context(context, 'cookiecutter.project_name')
113
114
    :param context: context to search in
115
    :param key: key to look for
116
    :param default: default value that will be returned if the key is not found
117
    :param update: if True, create the key in the context and set its value
118
                   using the default argument value
119
    """
120
    result = default
121
    current_context = context
122
    key_parts = key.split('.')
123
    for subkey in key_parts:
124
        id = int(subkey) if subkey.isdigit() else subkey
125
126
        if subkey in current_context or \
127
                (isinstance(id, int) and id < len(current_context)):
128
129
            result = current_context[id]
130
            current_context = result
131
        else:
132
            result = default
133
            if update:
134
                current_context[id] = default
135
136
    return result
137
138
139
def get_from_cookiecutter_context(context, key, default=None, update=False):
140
    """
141
    Shorthand function to look directly under the cookiecutter sub context
142
    The given key is prefixed with 'cookiecutter.'
143
    ie.
144
    context = {
145
        'cookiecutter':{
146
            <----- the search for the given key takes place under the
147
                   cookiecutter parent one
148
        }
149
    }
150
151
    :param context: context to search in
152
    :param key: key to look for (will be prefixed with 'cookiecutter.')
153
    :param default: default value that will be returned if the key is not found
154
    :param update: if True, create the key in the context and set its value
155
                   using the default argument value
156
    """
157
    return get_from_context(context, 'cookiecutter.' + key, default, update)
158
159
160
def set_to_context(context, key, value):
161
    """
162
    Set a key/value pair to a given context
163
    Keys can be defined using dot notation to set values to nested dictionaries
164
    ie.
165
    context = {}
166
    set_to_context(context, 'cookiecutter.project_name', 'My project')
167
168
    # will result in
169
    # context = {
170
    #     'cookiecutter': {
171
    #         'project_name': 'My project'
172
    #     }
173
    # }
174
175
    :param context: context to set the key/value pair to
176
    :param key: key that reference the value
177
    :param value: value to set
178
    """
179
    current_context = context
180
    key_parts = key.split('.')
181
    id = key_parts.pop()
182
    for subkey in key_parts:
183
        if subkey not in current_context:
184
            current_context[subkey] = {}
185
186
        current_context = current_context[subkey]
187
188
    current_context[id] = value
189
190
191
def set_to_cookiecutter_context(context, key, value):
192
    """
193
    Shorthand function to set key/value pairs directly under the cookiecutter
194
    sub context
195
    The given key is prefixed with 'cookiecutter.'
196
    ie.
197
    context = {
198
        'cookiecutter':{
199
            <----- the key/value pair will be set under the cookiecutter key
200
        }
201
    }
202
203
    :param context: context to set the key/value pair to
204
    :param key: key that reference the value
205
                (will be prefixed with 'cookiecutter.')
206
    :param value: value to set
207
    """
208
    set_to_context(context, 'cookiecutter.' + key, value)
209