pathconf()   B
last analyzed

Complexity

Conditions 6

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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