Completed
Push — master ( d09250...b67a9c )
by Alexandre M.
9s
created

_build_path()   B

Complexity

Conditions 6

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
c 0
b 0
f 0
dl 0
loc 36
rs 7.5384
1
# -*- coding: utf-8 -*-
2
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3
# vi: set ft=python sts=4 ts=4 sw=4 et:
4
"""
5
Crumb manipulation utilities
6
"""
7
import os
8
from string import Formatter
9
from typing import Iterable, Tuple, Dict, Iterator
10
11
_txt_idx = 0
12
_fld_idx = 1
13
_rgx_idx = 2
14
_cnv_idx = 3
15
16
17
def _yield_items(crumb_path: str, index=None) -> Iterator[str]:
18
    """ An iterator over the items in `crumb_path` given by string.Formatter."""
19
    if index is None:
20
        return Formatter().parse(crumb_path)
21
22
    # for (literal_text, field_name, format_spec, conversion) in fmt.parse(crumb_path):
23
    # (txt, fld, fmt, conv)
24
    return (items[index] for items in Formatter().parse(crumb_path) if items[index] is not None)
25
26
27
def _enum_items(crumb_path: str) -> Iterator[Tuple[int, str]]:
28
    """ An iterator over the enumerated items, i.e., (index, items) in
29
    `crumb_path` given by string.Formatter. """
30
    yield from enumerate(Formatter().parse(crumb_path))
31
32
33
def _depth_items(crumb_path: str, index: int = None) -> Iterator[Tuple[int, str]]:
34
    """ Return a generator with  (depth, items) in `crumb_path`. Being `depth`
35
     the place in the file path each argument is."""
36
    if index is None:
37
        index = slice(_txt_idx, _cnv_idx + 1)
38
39
    depth = 0
40
    for idx, items in _enum_items(crumb_path):
41
        if items[_fld_idx]:
42
            depth += items[_txt_idx].count(os.path.sep)
43
            yield depth, items[index]
44
45
46
def _arg_names(crumb_path: str) -> Iterator[str]:
47
    """ Return an iterator over arg_name in crumb_path."""
48
    yield from _yield_items(crumb_path, _fld_idx)
49
50
51
def _depth_names(crumb_path: str) -> Iterator[Tuple[int, str]]:
52
    """ Return an iterator over (depth, arg_name)."""
53
    yield from _depth_items(crumb_path, _fld_idx)
54
55
56
def _depth_names_regexes(crumb_path: str) -> Iterator[Tuple[int, str]]:
57
    """ Return an iterator over (depth, (arg_name, arg_regex))."""
58
    yield from _depth_items(crumb_path, slice(_fld_idx, _cnv_idx))
59
60
61
def _build_path(
62
    crumb_path: str,
63
    arg_values: Dict[str, str],
64
    with_regex: bool=True,
65
    regexes: Dict[str, str]=None
66
) -> str:
67
    """ Build the crumb_path with the values in arg_values.
68
    Parameters
69
    ----------
70
    crumb_path:
71
72
    arg_values:
73
        arg_name -> arg_value
74
75
    with_regex:
76
77
    regexes:
78
        dict[arg_name] -> regex
79
        The regexes contained here will replace or be added as a regex for
80
        the corresponding arg_name.
81
82
    Returns
83
    -------
84
    built_path:
85
    """
86
    if regexes is None:
87
        regexes = {}
88
89
    path = ''
90
    for txt, fld, rgx, conv in _yield_items(crumb_path):
91
        path += txt
92
        if fld is None:
93
            continue
94
95
        if fld in arg_values:
96
            path += arg_values[fld]
97
        else:
98
            regex = regexes.get(fld, rgx) if with_regex else ''
99
            path += _format_arg(fld, regex=regex)
100
101
    return path
102
103
104
def is_valid(crumb_path: str) -> bool:
105
    """ Return True if `crumb_path` is a valid Crumb value, False otherwise. """
106
    try:
107
        list(_depth_names_regexes(crumb_path))
108
    except ValueError:
109
        return False
110
    else:
111
        return True
112
113
114
def _first_txt(crumb_path: str) -> str:
115
    """ Return the first text part without arguments in `crumb_path`. """
116
    for txt in _yield_items(crumb_path, index=_txt_idx):
117
        return txt
118
119
120
def _find_arg_depth(crumb_path: str, arg_name: str) -> Tuple[int, str, str]:
121
    """ Return the depth, name and regex of the argument with name `arg_name`.
122
    """
123
    for depth, (txt, fld, rgx, conv) in _depth_items(crumb_path):
124
        if fld == arg_name:
125
            return depth, fld, rgx
126
127
128
def _has_arg(crumb_path: str, arg_name: str) -> bool:
129
    """ Return the True if the `arg_name` is found in `crumb_path`. """
130
    for txt, fld, rgx, conv in _yield_items(crumb_path):
131
        if fld == arg_name:
132
            return True
133
    return False
134
135
136
def _check(crumb_path: str) -> str:
137
    """ Raises some Errors if there is something wrong with `crumb_path`, if
138
    not the type needed or is not valid.
139
    Parameters
140
    ----------
141
    crumb_path: str
142
143
    Raises
144
    ------
145
     - ValueError if the path of the Crumb has errors using `self.is_valid`.
146
     - TypeError if the crumb_path is not a str or a Crumb.
147
    """
148
    if not isinstance(crumb_path, str):
149
        raise TypeError("Expected `crumb_path` to be a {}, "
150
                        "got {}.".format(str, type(crumb_path)))
151
152
    if not is_valid(crumb_path):
153
        raise ValueError("The current crumb path has errors, "
154
                         "got {}.".format(crumb_path))
155
156
    return crumb_path
157
158
159
def _get_path(crumb_path: str) -> str:
160
    """ Return the path string from `crumb_path`.
161
    Parameters
162
    ----------
163
    crumb_path: str or Crumb
164
165
    Returns
166
    -------
167
    path: str
168
    """
169
    if hasattr(crumb_path, '_path'):
170
        crumb_path = crumb_path._path
171
172
    if not isinstance(crumb_path, str):
173
        raise TypeError(
174
            "Expected `crumb_path` to be a string, got {}.".format(type(crumb_path))
175
        )
176
177
    return crumb_path
178
179
180
def _is_crumb_arg(crumb_arg: str) -> bool:
181
    """ Return True if `crumb_arg` is a well formed crumb argument, i.e.,
182
    is a string that starts with `start_sym` and ends with `end_sym`.
183
    False otherwise.
184
    """
185
    if not isinstance(crumb_arg, str):
186
        return False
187
    start_sym, end_sym = ('{', '}')
188
    return crumb_arg.startswith(start_sym) and crumb_arg.endswith(end_sym)
189
190
191
def _format_arg(arg_name: str, regex: str='') -> str:
192
    """ Return the crumb argument for its string `format()` representation. """
193
    start_sym, end_sym = ('{', '}')
194
    reg_sym = ':'
195
196
    arg_fmt = start_sym + arg_name
197
    if regex:
198
        arg_fmt += reg_sym + regex
199
    arg_fmt += end_sym
200
201
    return arg_fmt
202
203
204
def has_crumbs(crumb_path: str) -> bool:
205
    """ Return True if the `crumb_path.split(os.path.sep)` has item which is a
206
    crumb argument that starts with '{' and ends with '}'."""
207
    crumb_path = _get_path(crumb_path)
208
209
    splt = crumb_path.split(os.path.sep)
210
    for i in splt:
211
        if _is_crumb_arg(i):
212
            return True
213
214
    return False
215
216
217
def _split(crumb_path: str) -> Tuple[str, str]:
218
    """ Split `crumb_path` in two parts, the first is the base folder without
219
        any crumb argument and the second is the rest of `crumb_path` beginning
220
        with the first crumb argument.
221
        If `crumb_path` starts with an argument, will return ('', crumb_path).
222
    """
223
    crumb_path = _get_path(crumb_path)
224
225
    if not has_crumbs(crumb_path):
226
        return crumb_path, ''
227
228
    if not is_valid(crumb_path):
229
        raise ValueError('Crumb path {} is not valid.'.format(crumb_path))
230
231
    start_sym = '{'
232
    if crumb_path.startswith(start_sym):
233
        base = ''
234
        rest = crumb_path
235
    else:
236
        idx = crumb_path.find(start_sym)
237
        base = crumb_path[0:idx]
238
        if base.endswith(os.path.sep):
239
            base = base[:-1]
240
241
        rest = crumb_path[idx:]
242
243
    return base, rest
244
245
246
def _touch(crumb_path: str, exist_ok: bool=True) -> str:
247
    """ Create a leaf directory and all intermediate ones
248
    using the non crumbed part of `crumb_path`.
249
    If the target directory already exists, raise an IOError
250
    if exist_ok is False. Otherwise no exception is raised.
251
    Parameters
252
    ----------
253
    crumb_path:
254
255
    exist_ok:
256
        Default = True
257
258
    Returns
259
    -------
260
    nupath:
261
        The new path created.
262
    """
263
    if has_crumbs(crumb_path):
264
        nupath = _split(crumb_path)[0]
265
    else:
266
        nupath = crumb_path
267
268
    os.makedirs(nupath, exist_ok=exist_ok)
269
    return nupath
270
271
272
def _split_exists(crumb_path: str) -> bool:
273
    """ Return True if the part without crumb arguments of `crumb_path`
274
    is an existing path or a symlink, False otherwise.
275
    """
276
    if has_crumbs(crumb_path):
277
        rpath = _split(crumb_path)[0]
278
    else:
279
        rpath = str(crumb_path)
280
281
    return os.path.exists(rpath) or os.path.islink(rpath)
282
283
284
def _check_is_subset(list1: Iterable[str], list2: Iterable[str]):
285
    """ Raise an error if `list1` is not a subset of `list2`."""
286
    if not set(list1).issubset(set(list2)):
287
        raise KeyError(
288
            'The `list1` argument should be a subset of `list2` '
289
            'got {} and {}.'.format(list1, list2)
290
        )
291