Completed
Pull Request — master (#971)
by
unknown
31s
created

_find_user_config()   B

Complexity

Conditions 6

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
cc 6
c 2
b 1
f 1
dl 0
loc 44
rs 7.5384
1
# -*- coding: utf-8 -*-
2
3
"""Global configuration handling."""
4
5
from __future__ import unicode_literals
6
import copy
7
import logging
8
import os
9
from os.path import (
10
    abspath, normpath, expandvars, expanduser, isfile, isdir, join
11
)
12
import io
13
14
import poyo
15
16
from .exceptions import ConfigDoesNotExistException
17
from .exceptions import InvalidConfiguration
18
19
20
logger = logging.getLogger(__name__)
21
22
HOME_DIR = os.path.expanduser('~')
23
USER_CONFIG_FALLBACK_PATH = expanduser('~/.cookiecutterrc')
24
25
26
def _find_user_config():
27
    r"""
28
    If ``$COOKIECUTTER_CONFIG`` is set and valid, use it. If
29
    ``$COOKIECUTTER_CONFIG`` is set but not a file, raise
30
    ``ConfigDoesNotExistException``.
31
32
    If ``$COOKIECUTTER_CONFIG`` is unset but ``~/.cookiecutterrc`` exists,
33
    return ``~/.cookiecutterrc``.
34
35
    If ``$COOKIECUTTER_CONFIG`` is unset and ``~/.cookiecutterrc`` does not
36
    exist, then search the following:
37
38
        ``$XDG_CONFIG_HOME/cookiecutter/config``
39
        ``%APPDATA%\cookiecutter\config``
40
        ``~/.config/cookiecutter/config``
41
        ``~/Library/Application\ Support/cookiecutter/config``
42
43
    And the return the first matching file that exists.
44
    """
45
    # user override gets returned immediately
46
    if 'COOKIECUTTER_CONFIG' in os.environ:
47
        if isfile(expandvars('$COOKIECUTTER_CONFIG')):
48
            return expandvars('$COOKIECUTTER_CONFIG')
49
        else:
50
            # COOKIECUTTER_CONFIG is set but invalid
51
            raise ConfigDoesNotExistException
52
53
    # give priority to existing cookie cutter rc's
54
    if isfile(USER_CONFIG_FALLBACK_PATH):
55
        return USER_CONFIG_FALLBACK_PATH
56
57
    paths = [
58
        expandvars('$XDG_CONFIG_HOME'),  # *nix
59
        expandvars('$APPDATA'),          # Windows
60
        join(HOME_DIR, '.config'),       # lazy Linux (not all set XDG)
61
        join(HOME_DIR, 'Library', 'Application Support'),  # OS X
62
    ]
63
    for _path in paths:
64
        path = normpath(abspath(join(_path, 'cookiecutter', 'config')))
65
        if isfile(path):
66
            return path
67
    # if we reach this point then either the config file does not exist or we
68
    # have not properly been told where to look via env vars
69
    return None
70
71
72
def _find_user_data_dir(kind):
73
    """Attempt to locate a suitable directory for storing app data in based on
74
    the XDG spec and other common locations.
75
    """
76
    envvar = '${}'.format(kind.upper())
77
    dotdir = '.{}'.format(kind.lower())
78
79
    # only two types of data dir in the cookiecutter project at the moment
80
    assert kind.lower() in ('cookiecutters_dir', 'cookiecutter_replay')
81
82
    # user override via $COOKIECUTTERS_DIR
83
    if isdir(expandvars(envvar)):
84
        return expandvars(envvar)
85
86
    # respect existing COOKIECUTTERS_DIR
87
    fallback = join(HOME_DIR, dotdir)
88
    if isdir(fallback):
89
        return fallback
90
91
    # data dir search path
92
    paths = [
93
        expandvars('$XDG_DATA_HOME'),       # *nix
94
        expandvars('$APPDATA'),             # Windows
95
        join(HOME_DIR, '.local', 'share'),  # lazy Linux (not all set XDG)
96
        join(HOME_DIR, 'Library', 'Application Support'),  # OS X
97
    ]
98
99
    # search for an existing, appropriate location
100
    for _path in paths:
101
        if isdir(_path):
102
            return abspath(join(_path, 'cookiecutter', kind))
103
    # No appropriate location exists; use the fallback
104
    return fallback
105
106
107
BUILTIN_ABBREVIATIONS = {
108
    'gh': 'https://github.com/{0}.git',
109
    'gl': 'https://gitlab.com/{0}.git',
110
    'bb': 'https://bitbucket.org/{0}',
111
}
112
113
DEFAULT_CONFIG = {
114
    'cookiecutters_dir': _find_user_data_dir('cookiecutters_dir'),
115
    'replay_dir': _find_user_data_dir('cookiecutter_replay'),
116
    'default_context': {},
117
    'abbreviations': BUILTIN_ABBREVIATIONS,
118
}
119
120
121
def merge_configs(default, overwrite):
122
    """Recursively update a dict with the key/value pair of another.
123
124
    Dict values that are dictionaries themselves will be updated, whilst
125
    preserving existing keys.
126
    """
127
    new_config = copy.deepcopy(default)
128
129
    for k, v in overwrite.items():
130
        # Make sure to preserve existing items in
131
        # nested dicts, for example `abbreviations`
132
        if isinstance(v, dict):
133
            new_config[k] = merge_configs(default[k], v)
134
        else:
135
            new_config[k] = v
136
137
    return new_config
138
139
140
def get_config(config_path):
141
    """Retrieve the config from the specified path, returning a config dict."""
142
    if not isfile(config_path):
143
        raise ConfigDoesNotExistException
144
145
    logger.debug('config_path is {0}'.format(config_path))
146
    with io.open(config_path, encoding='utf-8') as file_handle:
147
        try:
148
            yaml_dict = poyo.parse_string(file_handle.read())
149
        except poyo.exceptions.PoyoException as e:
150
            raise InvalidConfiguration(
151
                'Unable to parse YAML file {}. Error: {}'
152
                ''.format(config_path, e)
153
            )
154
155
    config_dict = merge_configs(DEFAULT_CONFIG, yaml_dict)
156
157
    # the calls to expanduser/expandvars are only necessary if the User
158
    # overrides the default location using envvars or '~'
159
    raw_replay_dir = config_dict['replay_dir']
160
    config_dict['replay_dir'] = expanduser(expandvars(raw_replay_dir))
161
162
    raw_cookies_dir = config_dict['cookiecutters_dir']
163
    config_dict['cookiecutters_dir'] = expanduser(expandvars(raw_cookies_dir))
164
165
    return config_dict
166
167
168
def get_user_config(config_file=None, default_config=False):
169
    """Return the user config as a dict.
170
171
    If ``default_config`` is True, ignore ``config_file`` and return default
172
    values for the config parameters.
173
174
    If a path to a ``config_file`` is given explicitly, load the user config
175
    from that.
176
177
    Otherwise look up the config file path via ``_find_user_config``.  If no
178
    user config can be found by that function, return the default values.
179
    """
180
    # Do NOT load a config. Return defaults instead.
181
    if default_config:
182
        return copy.copy(DEFAULT_CONFIG)
183
184
    # Load the given config file
185
    if config_file is not None:
186
        return get_config(config_file)
187
188
    # If the user has a valid COOKIECUTTER_CONFIG set or has placed a
189
    # config file in a platform appropriate location (defaulting to HOME),
190
    # this should find it.  If COOKIECUTTER_CONFIG is set but invalid,
191
    # _find_user_config() will raise ConfigDoesNotExistException and we let
192
    # that propagate up
193
    found_config_file = _find_user_config()
194
    if found_config_file is not None:
195
        # There IS a config file. Try to load it
196
        return get_config(found_config_file)
197
    else:
198
        # Otherwise, return the defaults
199
        return copy.copy(DEFAULT_CONFIG)
200