#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import os.path
import sys
import itertools
import warnings
import functools
import posixpath
import ntpath
FS_ENCODING = sys.getfilesystemencoding()
PY_LEGACY = sys.version_info < (3, )
TRUE_VALUES = frozenset(('true', 'yes', '1', 'enable', 'enabled', True, 1))
try:
from os import scandir, walk
except ImportError:
from scandir import scandir, walk # noqa
try:
from shutil import get_terminal_size
except ImportError:
from backports.shutil_get_terminal_size import get_terminal_size # noqa
[docs]def isexec(path):
'''
Check if given path points to an executable file.
:param path: file path
:type path: str
:return: True if executable, False otherwise
:rtype: bool
'''
return os.path.isfile(path) and os.access(path, os.X_OK)
[docs]def fsdecode(path, os_name=os.name, fs_encoding=FS_ENCODING, errors=None):
'''
Decode given path.
:param path: path will be decoded if using bytes
:type path: bytes or str
:param os_name: operative system name, defaults to os.name
:type os_name: str
:param fs_encoding: current filesystem encoding, defaults to autodetected
:type fs_encoding: str
:return: decoded path
:rtype: str
'''
if not isinstance(path, bytes):
return path
if not errors:
use_strict = PY_LEGACY or os_name == 'nt'
errors = 'strict' if use_strict else 'surrogateescape'
return path.decode(fs_encoding, errors=errors)
[docs]def fsencode(path, os_name=os.name, fs_encoding=FS_ENCODING, errors=None):
'''
Encode given path.
:param path: path will be encoded if not using bytes
:type path: bytes or str
:param os_name: operative system name, defaults to os.name
:type os_name: str
:param fs_encoding: current filesystem encoding, defaults to autodetected
:type fs_encoding: str
:return: encoded path
:rtype: bytes
'''
if isinstance(path, bytes):
return path
if not errors:
use_strict = PY_LEGACY or os_name == 'nt'
errors = 'strict' if use_strict else 'surrogateescape'
return path.encode(fs_encoding, errors=errors)
[docs]def getcwd(fs_encoding=FS_ENCODING, cwd_fnc=os.getcwd):
'''
Get current work directory's absolute path.
Like os.getcwd but garanteed to return an unicode-str object.
:param fs_encoding: filesystem encoding, defaults to autodetected
:type fs_encoding: str
:param cwd_fnc: callable used to get the path, defaults to os.getcwd
:type cwd_fnc: Callable
:return: path
:rtype: str
'''
path = fsdecode(cwd_fnc(), fs_encoding=fs_encoding)
return os.path.abspath(path)
[docs]def getdebug(environ=os.environ, true_values=TRUE_VALUES):
'''
Get if app is expected to be ran in debug mode looking at environment
variables.
:param environ: environment dict-like object
:type environ: collections.abc.Mapping
:returns: True if debug contains a true-like string, False otherwise
:rtype: bool
'''
return environ.get('DEBUG', '').lower() in true_values
[docs]def deprecated(func_or_text, environ=os.environ):
'''
Decorator used to mark functions as deprecated. It will result in a
warning being emmitted hen the function is called.
Usage:
>>> @deprecated
... def fnc():
... pass
Usage (custom message):
>>> @deprecated('This is deprecated')
... def fnc():
... pass
:param func_or_text: message or callable to decorate
:type func_or_text: callable
:param environ: optional environment mapping
:type environ: collections.abc.Mapping
:returns: nested decorator or new decorated function (depending on params)
:rtype: callable
'''
def inner(func):
message = (
'Deprecated function {}.'.format(func.__name__)
if callable(func_or_text) else
func_or_text
)
@functools.wraps(func)
def new_func(*args, **kwargs):
with warnings.catch_warnings():
if getdebug(environ):
warnings.simplefilter('always', DeprecationWarning)
warnings.warn(message, category=DeprecationWarning,
stacklevel=3)
return func(*args, **kwargs)
return new_func
return inner(func_or_text) if callable(func_or_text) else inner
[docs]def usedoc(other):
'''
Decorator which copies __doc__ of given object into decorated one.
Usage:
>>> def fnc1():
... """docstring"""
... pass
>>> @usedoc(fnc1)
... def fnc2():
... pass
>>> fnc2.__doc__
'docstring'collections.abc.D
:param other: anything with a __doc__ attribute
:type other: any
:returns: decorator function
:rtype: callable
'''
def inner(fnc):
fnc.__doc__ = fnc.__doc__ or getattr(other, '__doc__')
return fnc
return inner
[docs]def pathsplit(value, sep=os.pathsep):
'''
Get enviroment PATH elements as list.
This function only cares about spliting across OSes.
:param value: path string, as given by os.environ['PATH']
:type value: str
:param sep: PATH separator, defaults to os.pathsep
:type sep: str
:yields: every path
:ytype: str
'''
for part in value.split(sep):
if part[:1] == part[-1:] == '"' or part[:1] == part[-1:] == '\'':
part = part[1:-1]
yield part
[docs]def pathparse(value, sep=os.pathsep, os_sep=os.sep):
'''
Get enviroment PATH directories as list.
This function cares about spliting, escapes and normalization of paths
across OSes.
:param value: path string, as given by os.environ['PATH']
:type value: str
:param sep: PATH separator, defaults to os.pathsep
:type sep: str
:param os_sep: OS filesystem path separator, defaults to os.sep
:type os_sep: str
:yields: every path
:ytype: str
'''
escapes = []
normpath = ntpath.normpath if os_sep == '\\' else posixpath.normpath
if '\\' not in (os_sep, sep):
escapes.extend((
('\\\\', '<ESCAPE-ESCAPE>', '\\'),
('\\"', '<ESCAPE-DQUOTE>', '"'),
('\\\'', '<ESCAPE-SQUOTE>', '\''),
('\\%s' % sep, '<ESCAPE-PATHSEP>', sep),
))
for original, escape, unescape in escapes:
value = value.replace(original, escape)
for part in pathsplit(value, sep=sep):
if part[-1:] == os_sep and part != os_sep:
part = part[:-1]
for original, escape, unescape in escapes:
part = part.replace(escape, unescape)
yield normpath(fsdecode(part))
[docs]def pathconf(path,
os_name=os.name,
isdir_fnc=os.path.isdir,
pathconf_fnc=getattr(os, 'pathconf', None),
pathconf_names=getattr(os, 'pathconf_names', ())):
'''
Get all pathconf variables for given path.
:param path: absolute fs path
:type path: str
:returns: dictionary containing pathconf keys and their values (both str)
:rtype: dict
'''
if pathconf_fnc and pathconf_names:
return {key: pathconf_fnc(path, key) for key in pathconf_names}
if os_name == 'nt':
maxpath = 246 if isdir_fnc(path) else 259 # 260 minus <END>
else:
maxpath = 255 # conservative sane default
return {
'PC_PATH_MAX': maxpath,
'PC_NAME_MAX': maxpath - len(path),
}
ENV_PATH = tuple(pathparse(os.getenv('PATH', '')))
ENV_PATHEXT = tuple(pathsplit(os.getenv('PATHEXT', '')))
[docs]def which(name,
env_path=ENV_PATH,
env_path_ext=ENV_PATHEXT,
is_executable_fnc=isexec,
path_join_fnc=os.path.join,
os_name=os.name):
'''
Get command absolute path.
:param name: name of executable command
:type name: str
:param env_path: OS environment executable paths, defaults to autodetected
:type env_path: list of str
:param is_executable_fnc: callable will be used to detect if path is
executable, defaults to `isexec`
:type is_executable_fnc: Callable
:param path_join_fnc: callable will be used to join path components
:type path_join_fnc: Callable
:param os_name: os name, defaults to os.name
:type os_name: str
:return: absolute path
:rtype: str or None
'''
for path in env_path:
for suffix in env_path_ext:
exe_file = path_join_fnc(path, name) + suffix
if is_executable_fnc(exe_file):
return exe_file
return None
[docs]def re_escape(pattern, chars=frozenset("()[]{}?*+|^$\\.-#")):
'''
Escape all special regex characters in pattern.
Logic taken from regex module.
:param pattern: regex pattern to escape
:type patterm: str
:returns: escaped pattern
:rtype: str
'''
escape = '\\{}'.format
return ''.join(
escape(c) if c in chars or c.isspace() else
'\\000' if c == '\x00' else c
for c in pattern
)
if PY_LEGACY:
FileNotFoundError = OSError # noqa
range = xrange # noqa
filter = itertools.ifilter
basestring = basestring # noqa
unicode = unicode # noqa
chr = unichr # noqa
bytes = str # noqa
else:
FileNotFoundError = FileNotFoundError
range = range
filter = filter
basestring = str
unicode = str
chr = chr
bytes = bytes