Completed
Push — issue#20 ( c33ee0...91547a )
by Felipe A.
33s
created

pathparse()   C

Complexity

Conditions 7

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
dl 0
loc 32
rs 5.5
c 0
b 0
f 0
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import os
5
import os.path
6
import sys
7
import itertools
8
9
import warnings
10
import functools
11
12
FS_ENCODING = sys.getfilesystemencoding()
13
PY_LEGACY = sys.version_info < (3, )
14
TRUE_VALUES = frozenset(('true', 'yes', '1', 'enable', 'enabled', True, 1))
15
16
try:
17
    from os import scandir, walk
18
except ImportError:
19
    from scandir import scandir, walk  # noqa
20
21
try:
22
    from shutil import get_terminal_size
23
except ImportError:
24
    from backports.shutil_get_terminal_size import get_terminal_size  # noqa
25
26
27
def isexec(path):
28
    '''
29
    Check if given path points to an executable file.
30
31
    :param path: file path
32
    :type path: str
33
    :return: True if executable, False otherwise
34
    :rtype: bool
35
    '''
36
    return os.path.isfile(path) and os.access(path, os.X_OK)
37
38
39
def fsdecode(path, os_name=os.name, fs_encoding=FS_ENCODING, errors=None):
40
    '''
41
    Decode given path.
42
43
    :param path: path will be decoded if using bytes
44
    :type path: bytes or str
45
    :param os_name: operative system name, defaults to os.name
46
    :type os_name: str
47
    :param fs_encoding: current filesystem encoding, defaults to autodetected
48
    :type fs_encoding: str
49
    :return: decoded path
50
    :rtype: str
51
    '''
52
    if not isinstance(path, bytes):
53
        return path
54
    if not errors:
55
        use_strict = PY_LEGACY or os_name == 'nt'
56
        errors = 'strict' if use_strict else 'surrogateescape'
57
    return path.decode(fs_encoding, errors=errors)
58
59
60
def fsencode(path, os_name=os.name, fs_encoding=FS_ENCODING, errors=None):
61
    '''
62
    Encode given path.
63
64
    :param path: path will be encoded if not using bytes
65
    :type path: bytes or str
66
    :param os_name: operative system name, defaults to os.name
67
    :type os_name: str
68
    :param fs_encoding: current filesystem encoding, defaults to autodetected
69
    :type fs_encoding: str
70
    :return: encoded path
71
    :rtype: bytes
72
    '''
73
    if isinstance(path, bytes):
74
        return path
75
    if not errors:
76
        use_strict = PY_LEGACY or os_name == 'nt'
77
        errors = 'strict' if use_strict else 'surrogateescape'
78
    return path.encode(fs_encoding, errors=errors)
79
80
81
def getcwd(fs_encoding=FS_ENCODING, cwd_fnc=os.getcwd):
82
    '''
83
    Get current work directory's absolute path.
84
    Like os.getcwd but garanteed to return an unicode-str object.
85
86
    :param fs_encoding: filesystem encoding, defaults to autodetected
87
    :type fs_encoding: str
88
    :param cwd_fnc: callable used to get the path, defaults to os.getcwd
89
    :type cwd_fnc: Callable
90
    :return: path
91
    :rtype: str
92
    '''
93
    path = fsdecode(cwd_fnc(), fs_encoding=fs_encoding)
94
    return os.path.abspath(path)
95
96
97
def getdebug(environ=os.environ, true_values=TRUE_VALUES):
98
    '''
99
    Get if app is expected to be ran in debug mode looking at environment
100
    variables.
101
102
    :param environ: environment dict-like object
103
    :type environ: collections.abc.Mapping
104
    :returns: True if debug contains a true-like string, False otherwise
105
    :rtype: bool
106
    '''
107
    return environ.get('DEBUG', '').lower() in true_values
108
109
110
def deprecated(func_or_text, environ=os.environ):
111
    '''
112
    Decorator used to mark functions as deprecated. It will result in a
113
    warning being emmitted hen the function is called.
114
115
    Usage:
116
117
    >>> @deprecated
118
    ... def fnc():
119
    ...     pass
120
121
    Usage (custom message):
122
123
    >>> @deprecated('This is deprecated')
124
    ... def fnc():
125
    ...     pass
126
127
    :param func_or_text: message or callable to decorate
128
    :type func_or_text: callable
129
    :param environ: optional environment mapping
130
    :type environ: collections.abc.Mapping
131
    :returns: nested decorator or new decorated function (depending on params)
132
    :rtype: callable
133
    '''
134
    def inner(func):
135
        message = (
136
            'Deprecated function {}.'.format(func.__name__)
137
            if callable(func_or_text) else
138
            func_or_text
139
            )
140
141
        @functools.wraps(func)
142
        def new_func(*args, **kwargs):
143
            with warnings.catch_warnings():
144
                if getdebug(environ):
145
                    warnings.simplefilter('always', DeprecationWarning)
146
                warnings.warn(message, category=DeprecationWarning,
147
                              stacklevel=3)
148
            return func(*args, **kwargs)
149
        return new_func
150
    return inner(func_or_text) if callable(func_or_text) else inner
151
152
153
def usedoc(other):
154
    '''
155
    Decorator which copies __doc__ of given object into decorated one.
156
157
    Usage:
158
159
    >>> def fnc1():
160
    ...     """docstring"""
161
    ...     pass
162
    >>> @usedoc(fnc1)
163
    ... def fnc2():
164
    ...     pass
165
    >>> fnc2.__doc__
166
    'docstring'collections.abc.D
167
168
    :param other: anything with a __doc__ attribute
169
    :type other: any
170
    :returns: decorator function
171
    :rtype: callable
172
    '''
173
    def inner(fnc):
174
        fnc.__doc__ = fnc.__doc__ or getattr(other, '__doc__')
175
        return fnc
176
    return inner
177
178
179
def pathsplit(value, sep=os.pathsep):
180
    '''
181
    Get enviroment PATH elements as list.
182
183
    This function only cares about spliting across OSes.
184
185
    :param value: path string, as given by os.environ['PATH']
186
    :type value: str
187
    :param sep: PATH separator, defaults to os.pathsep
188
    :type sep: str
189
    :yields: every path
190
    :ytype: str
191
    '''
192
    for part in value.split(sep):
193
        if part[:1] == part[-1:] == '"' or part[:1] == part[-1:] == '\'':
194
            part = part[1:-1]
195
        yield part
196
197
198
def pathparse(value, sep=os.pathsep, os_sep=os.sep):
199
    '''
200
    Get enviroment PATH directories as list.
201
202
    This function cares about spliting, escapes and normalization of paths
203
    across OSes.
204
205
    :param value: path string, as given by os.environ['PATH']
206
    :type value: str
207
    :param sep: PATH separator, defaults to os.pathsep
208
    :type sep: str
209
    :param os_sep: OS filesystem path separator, defaults to os.sep
210
    :type os_sep: str
211
    :yields: every path
212
    :ytype: str
213
    '''
214
    escapes = []
215
    if '\\' not in (os_sep, sep):
216
        escapes.extend((
217
            ('\\\\', '<ESCAPE-ESCAPE>', '\\'),
218
            ('\\"', '<ESCAPE-DQUOTE>', '"'),
219
            ('\\\'', '<ESCAPE-SQUOTE>', '\''),
220
            ('\\%s' % sep, '<ESCAPE-PATHSEP>', sep),
221
            ))
222
    for original, escape, unescape in escapes:
223
        value = value.replace(original, escape)
224
    for part in pathsplit(value, sep=sep):
225
        if part[-1:] == os_sep and part != os_sep:
226
            part = part[:-1]
227
        for original, escape, unescape in escapes:
228
            part = part.replace(escape, unescape)
229
        yield os.path.normpath(fsdecode(part))
230
231
232
ENV_PATH = tuple(pathparse(os.getenv('PATH', '')))
233
ENV_PATHEXT = tuple(pathsplit(os.getenv('PATHEXT', '')))
234
235
236
def which(name,
237
          env_path=ENV_PATH,
238
          env_path_ext=ENV_PATHEXT,
239
          is_executable_fnc=isexec,
240
          path_join_fnc=os.path.join,
241
          os_name=os.name):
242
    '''
243
    Get command absolute path.
244
245
    :param name: name of executable command
246
    :type name: str
247
    :param env_path: OS environment executable paths, defaults to autodetected
248
    :type env_path: list of str
249
    :param is_executable_fnc: callable will be used to detect if path is
250
                              executable, defaults to `isexec`
251
    :type is_executable_fnc: Callable
252
    :param path_join_fnc: callable will be used to join path components
253
    :type path_join_fnc: Callable
254
    :param os_name: os name, defaults to os.name
255
    :type os_name: str
256
    :return: absolute path
257
    :rtype: str or None
258
    '''
259
    for path in env_path:
260
        for suffix in env_path_ext:
261
            exe_file = path_join_fnc(path, name) + suffix
262
            if is_executable_fnc(exe_file):
263
                return exe_file
264
    return None
265
266
267
def re_escape(pattern, chars=frozenset("()[]{}?*+|^$\\.-#")):
268
    '''
269
    Escape all special regex characters in pattern.
270
    Logic taken from regex module.
271
272
    :param pattern: regex pattern to escape
273
    :type patterm: str
274
    :returns: escaped pattern
275
    :rtype: str
276
    '''
277
    escape = '\\{}'.format
278
    return ''.join(
279
        escape(c) if c in chars or c.isspace() else
280
        '\\000' if c == '\x00' else c
281
        for c in pattern
282
        )
283
284
285
if PY_LEGACY:
286
    FileNotFoundError = OSError  # noqa
287
    range = xrange  # noqa
288
    filter = itertools.ifilter
289
    basestring = basestring  # noqa
290
    unicode = unicode  # noqa
291
    chr = unichr  # noqa
292
    bytes = str  # noqa
293
else:
294
    FileNotFoundError = FileNotFoundError
295
    range = range
296
    filter = filter
297
    basestring = str
298
    unicode = str
299
    chr = chr
300
    bytes = bytes
301