1from __future__ import division, absolute_import, print_function
2
3import os
4import sys
5import argparse
6import optparse
7import platform
8import pkgutil
9
10
11PY3 = sys.version_info[0] == 3
12STRING = str if PY3 else unicode  # noqa: F821
13BASESTRING = str if PY3 else basestring  # noqa: F821
14NUMERIC_TYPES = (int, float) if PY3 else (int, float, long)  # noqa: F821
15
16
17UNIX_DIR_FALLBACK = '~/.config'
18WINDOWS_DIR_VAR = 'APPDATA'
19WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming'
20MAC_DIR = '~/Library/Application Support'
21
22
23def iter_first(sequence):
24    """Get the first element from an iterable or raise a ValueError if
25    the iterator generates no values.
26    """
27    it = iter(sequence)
28    try:
29        return next(it)
30    except StopIteration:
31        raise ValueError()
32
33
34def namespace_to_dict(obj):
35    """If obj is argparse.Namespace or optparse.Values we'll return
36      a dict representation of it, else return the original object.
37
38    Redefine this method if using other parsers.
39
40    :param obj: *
41    :return:
42    :rtype: dict or *
43    """
44    if isinstance(obj, (argparse.Namespace, optparse.Values)):
45        return vars(obj)
46    return obj
47
48
49def build_dict(obj, sep='', keep_none=False):
50    """Recursively builds a dictionary from an argparse.Namespace,
51    optparse.Values, or dict object.
52
53    Additionally, if `sep` is a non-empty string, the keys will be split
54    by `sep` and expanded into a nested dict. Keys with a `None` value
55    are dropped by default to avoid unsetting options but can be kept
56    by setting `keep_none` to `True`.
57
58    :param obj: Namespace, Values, or dict to iterate over. Other
59        values will simply be returned.
60    :type obj: argparse.Namespace or optparse.Values or dict or *
61    :param sep: Separator to use for splitting properties/keys of `obj`
62        for expansion into nested dictionaries.
63    :type sep: str
64    :param keep_none: Whether to keep keys whose value is `None`.
65    :type keep_none: bool
66    :return: A new dictionary or the value passed if obj was not a
67        dict, Namespace, or Values.
68    :rtype: dict or *
69    """
70    # We expect our root object to be a dict, but it may come in as
71    # a namespace
72    obj = namespace_to_dict(obj)
73    # We only deal with dictionaries
74    if not isinstance(obj, dict):
75        return obj
76
77    # Get keys iterator
78    keys = obj.keys() if PY3 else obj.iterkeys()
79    if sep:
80        # Splitting keys by `sep` needs sorted keys to prevent parents
81        # from clobbering children
82        keys = sorted(list(keys))
83
84    output = {}
85    for key in keys:
86        value = obj[key]
87        if value is None and not keep_none:  # Avoid unset options.
88            continue
89
90        save_to = output
91        result = build_dict(value, sep, keep_none)
92        if sep:
93            # Split keys by `sep` as this signifies nesting
94            split = key.split(sep)
95            if len(split) > 1:
96                # The last index will be the key we assign result to
97                key = split.pop()
98                # Build the dict tree if needed and change where
99                # we're saving to
100                for child_key in split:
101                    if child_key in save_to and \
102                            isinstance(save_to[child_key], dict):
103                        save_to = save_to[child_key]
104                    else:
105                        # Clobber or create
106                        save_to[child_key] = {}
107                        save_to = save_to[child_key]
108
109        # Save
110        if key in save_to:
111            save_to[key].update(result)
112        else:
113            save_to[key] = result
114    return output
115
116
117# Config file paths, including platform-specific paths and in-package
118# defaults.
119
120def find_package_path(name):
121    """Returns the path to the package containing the named module or
122    None if the path could not be identified (e.g., if
123    ``name == "__main__"``).
124    """
125    # Based on get_root_path from Flask by Armin Ronacher.
126    loader = pkgutil.get_loader(name)
127    if loader is None or name == '__main__':
128        return None
129
130    if hasattr(loader, 'get_filename'):
131        filepath = loader.get_filename(name)
132    else:
133        # Fall back to importing the specified module.
134        __import__(name)
135        filepath = sys.modules[name].__file__
136
137    return os.path.dirname(os.path.abspath(filepath))
138
139
140def xdg_config_dirs():
141    """Returns a list of paths taken from the XDG_CONFIG_DIRS
142    and XDG_CONFIG_HOME environment varibables if they exist
143    """
144    paths = []
145    if 'XDG_CONFIG_HOME' in os.environ:
146        paths.append(os.environ['XDG_CONFIG_HOME'])
147    if 'XDG_CONFIG_DIRS' in os.environ:
148        paths.extend(os.environ['XDG_CONFIG_DIRS'].split(':'))
149    else:
150        paths.append('/etc/xdg')
151    paths.append('/etc')
152    return paths
153
154
155def config_dirs():
156    """Return a platform-specific list of candidates for user
157    configuration directories on the system.
158
159    The candidates are in order of priority, from highest to lowest. The
160    last element is the "fallback" location to be used when no
161    higher-priority config file exists.
162    """
163    paths = []
164
165    if platform.system() == 'Darwin':
166        paths.append(UNIX_DIR_FALLBACK)
167        paths.append(MAC_DIR)
168        paths.extend(xdg_config_dirs())
169
170    elif platform.system() == 'Windows':
171        paths.append(WINDOWS_DIR_FALLBACK)
172        if WINDOWS_DIR_VAR in os.environ:
173            paths.append(os.environ[WINDOWS_DIR_VAR])
174
175    else:
176        # Assume Unix.
177        paths.append(UNIX_DIR_FALLBACK)
178        paths.extend(xdg_config_dirs())
179
180    # Expand and deduplicate paths.
181    out = []
182    for path in paths:
183        path = os.path.abspath(os.path.expanduser(path))
184        if path not in out:
185            out.append(path)
186    return out
187