Completed
Push — 0.5.3 ( 1367b0...e9a59e )
by Felipe A.
03:47
created

parsepath()   F

Complexity

Conditions 9

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
c 0
b 0
f 0
dl 0
loc 33
rs 3
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 = cwd_fnc()
94
    if isinstance(path, bytes):
95
        path = fsdecode(path, fs_encoding=fs_encoding)
96
    return os.path.abspath(path)
97
98
99
def getdebug(environ=os.environ, true_values=TRUE_VALUES):
100
    '''
101
    Get if app is expected to be ran in debug mode looking at environment
102
    variables.
103
104
    :param environ: environment dict-like object
105
    :type environ: collections.abc.Mapping
106
    :returns: True if debug contains a true-like string, False otherwise
107
    :rtype: bool
108
    '''
109
    return environ.get('DEBUG', '').lower() in true_values
110
111
112
def deprecated(func_or_text, environ=os.environ):
113
    '''
114
    Decorator used to mark functions as deprecated. It will result in a
115
    warning being emmitted hen the function is called.
116
117
    Usage:
118
119
    >>> @deprecated
120
    ... def fnc():
121
    ...     pass
122
123
    Usage (custom message):
124
125
    >>> @deprecated('This is deprecated')
126
    ... def fnc():
127
    ...     pass
128
129
    :param func_or_text: message or callable to decorate
130
    :type func_or_text: callable
131
    :param environ: optional environment mapping
132
    :type environ: collections.abc.Mapping
133
    :returns: nested decorator or new decorated function (depending on params)
134
    :rtype: callable
135
    '''
136
    def inner(func):
137
        message = (
138
            'Deprecated function {}.'.format(func.__name__)
139
            if callable(func_or_text) else
140
            func_or_text
141
            )
142
143
        @functools.wraps(func)
144
        def new_func(*args, **kwargs):
145
            with warnings.catch_warnings():
146
                if getdebug(environ):
147
                    warnings.simplefilter('always', DeprecationWarning)
148
                warnings.warn(message, category=DeprecationWarning,
149
                              stacklevel=3)
150
            return func(*args, **kwargs)
151
        return new_func
152
    return inner(func_or_text) if callable(func_or_text) else inner
153
154
155
def usedoc(other):
156
    '''
157
    Decorator which copies __doc__ of given object into decorated one.
158
159
    Usage:
160
161
    >>> def fnc1():
162
    ...     """docstring"""
163
    ...     pass
164
    >>> @usedoc(fnc1)
165
    ... def fnc2():
166
    ...     pass
167
    >>> fnc2.__doc__
168
    'docstring'collections.abc.D
169
170
    :param other: anything with a __doc__ attribute
171
    :type other: any
172
    :returns: decorator function
173
    :rtype: callable
174
    '''
175
    def inner(fnc):
176
        fnc.__doc__ = fnc.__doc__ or getattr(other, '__doc__')
177
        return fnc
178
    return inner
179
180
181
def parsepath(
182
  value=os.getenv('PATH', ''), sep=os.pathsep, os_sep=os.sep,
183
  fsdecode=fsdecode
184
  ):
185
    '''
186
    Get enviroment PATH directories as list.
187
188
    This function cares about spliting, escapes and normalization of paths
189
    across OSes.
190
191
    :yields: every path
192
    :ytype: str
193
    '''
194
    escapes = []
195
    normalize = os.path.normpath
196
    if '\\' not in (os_sep, sep):
197
        escapes.extend((
198
            ('\\\\', '<ESCAPE-ESCAPE>'),
199
            ('\\"', '<ESCAPE-DQUOTE>'),
200
            ('\\\'', '<ESCAPE-SQUOTE>'),
201
            ('\\%s' % sep, '<ESCAPE-PATHSEP>'),
202
            ))
203
    for original, escape in escapes:
204
        value = value.replace(original, escape)
205
    for part in value.split(sep):
206
        if part[:0] == part[-1:] == '"' or part[:0] == part[-1:] == '\'':
207
            part = part[1:-1]
208
        if part[-1:] == os_sep:
209
            part = part[:-1]
210
        for original, escape in escapes:
211
            part = part.replace(escape, original)
212
        if part:
213
            yield normalize(fsdecode(part))
214
215
216
ENV_PATH = tuple(parsepath())
217
218
219
def which(name,
220
          env_path=ENV_PATH,
221
          is_executable_fnc=isexec,
222
          path_join_fnc=os.path.join,
223
          os_name=os.name):
224
    '''
225
    Get command absolute path.
226
227
    :param name: name of executable command
228
    :type name: str
229
    :param env_path: OS environment executable paths, defaults to autodetected
230
    :type env_path: list of str
231
    :param is_executable_fnc: callable will be used to detect if path is
232
                              executable, defaults to `isexec`
233
    :type is_executable_fnc: Callable
234
    :param path_join_fnc: callable will be used to join path components
235
    :type path_join_fnc: Callable
236
    :param os_name: os name, defaults to os.name
237
    :type os_name: str
238
    :return: absolute path
239
    :rtype: str or None
240
    '''
241
    suffixes = ('', '.exe', '.bat', '.com') if os_name == 'nt' else ('',)
242
    for suffix in suffixes:
243
        for path in env_path:
244
            exe_file = path_join_fnc(path, name)
245
            if is_executable_fnc(exe_file + suffix):
246
                return exe_file
247
    return None
248
249
250
def re_escape(pattern, chars=frozenset("()[]{}?*+|^$\\.-#")):
251
    '''
252
    Escape all special regex characters in pattern.
253
    Logic taken from regex module.
254
255
    :param pattern: regex pattern to escape
256
    :type patterm: str
257
    :returns: escaped pattern
258
    :rtype: str
259
    '''
260
    escape = '\\{}'.format
261
    return ''.join(
262
        escape(c) if c in chars or c.isspace() else
263
        '\\000' if c == '\x00' else c
264
        for c in pattern
265
        )
266
267
268
if PY_LEGACY:
269
    FileNotFoundError = type('FileNotFoundError', (OSError,), {})  # noqa
270
    range = xrange  # noqa
271
    filter = itertools.ifilter
272
    basestring = basestring  # noqa
273
    unicode = unicode  # noqa
274
    chr = unichr  # noqa
275
    bytes = str  # noqa
276
else:
277
    FileNotFoundError = FileNotFoundError
278
    range = range
279
    filter = filter
280
    basestring = str
281
    unicode = str
282
    chr = chr
283
    bytes = bytes
284