Source code for nexus.utilities
import string
from numbers import Number
from pathlib import Path
# attempt to regain python 2 sorting
# code below is from https://stackoverflow.com/questions/26575183/how-can-i-get-2-x-like-sorting-behaviour-in-python-3-x
#===========================
# decorator for type to function mapping special cases
[docs]
def per_type_cmp(type_):
try:
mapping = per_type_cmp.mapping
except AttributeError:
mapping = per_type_cmp.mapping = {}
#end try
def decorator(cmpfunc):
mapping[type_] = cmpfunc
return cmpfunc
#end def decorator
return decorator
#ned def per_type_cmp
[docs]
class python2_sort_key(object):
_unhandled_types = {complex}
def __init__(self, ob):
self._ob = ob
#end def __init__
def __lt__(self, other):
_unhandled_types = self._unhandled_types
self, other = self._ob, other._ob # we don't care about the wrapper
# default_3way_compare is used only if direct comparison failed
try:
return self < other
except TypeError:
pass
#end try
# hooks to implement special casing for types, dict in Py2 has
# a dedicated __cmp__ method that is gone in Py3 for example.
for type_, special_cmp in per_type_cmp.mapping.items():
if isinstance(self, type_) and isinstance(other, type_):
return special_cmp(self, other)
#end if
#end for
# explicitly raise again for types that won't sort in Python 2 either
if type(self) in _unhandled_types:
raise TypeError('no ordering relation is defined for {}'.format(
type(self).__name__))
#end if
if type(other) in _unhandled_types:
raise TypeError('no ordering relation is defined for {}'.format(
type(other).__name__))
#end if
# default_3way_compare from Python 2 as Python code
# same type but no ordering defined, go by id
if type(self) is type(other):
return id(self) < id(other)
#end if
# None always comes first
if self is None:
return True
#end if
if other is None:
return False
#end if
# Sort by typename, but numbers are sorted before other types
self_tname = '' if isinstance(self, Number) else type(self).__name__
other_tname = '' if isinstance(other, Number) else type(other).__name__
if self_tname != other_tname:
return self_tname < other_tname
#end if
# same typename, or both numbers, but different type objects, order
# by the id of the type object
return id(type(self)) < id(type(other))
#end def __lt__
#end class python2_sort_key
[docs]
@per_type_cmp(dict)
def dict_cmp(a, b, _s=object()):
if len(a) != len(b):
return len(a) < len(b)
#end if
adiff = min((k for k in a if a[k] != b.get(k, _s)), key=python2_sort_key, default=_s)
if adiff is _s:
# All keys in a have a matching value in b, so the dicts are equal
return False
#end if
bdiff = min((k for k in b if b[k] != a.get(k, _s)), key=python2_sort_key)
if adiff != bdiff:
return python2_sort_key(adiff) < python2_sort_key(bdiff)
#end if
return python2_sort_key(a[adiff]) < python2_sort_key(b[bdiff])
#end def dict_cmp
[docs]
def sorted_py2(iterable):
return sorted(iterable,key=python2_sort_key)
#end def sorted_py2
#===========================
[docs]
def to_str(s):
'''Convert a value to a string'''
if isinstance(s,bytes):
return str(s,encoding='utf-8')
else:
return str(s)
#end if
#end def to_str
def _path_to_str(path: str | bytes | Path) -> str:
'''Simple conversion from bytes/Path types to str.'''
if isinstance(path, str):
pass
elif isinstance(path, bytes):
path = str(path, encoding='utf-8')
elif isinstance(path, Path):
path = str(path)
else:
raise TypeError(
'path must be of type "str", "bytes" or "Path". Type received: {}'
.format(path.__class__.__name__)
)
return path
#end def _path_to_str
[docs]
def is_valid_path(path: str | bytes | Path) -> bool:
'''Screen out paths with invalid characters.'''
path = _path_to_str(path)
if not hasattr(is_valid_path,'invalid_chars'):
unprintable = [chr(c) for c in range(128) if chr(c) not in string.printable]
special = r'!@#$%^&*;|?\`",()[]{}<>' + r"'"
whitespace = set(string.whitespace) - set([' '])
invalid = set(unprintable) | set(special) | whitespace
is_valid_path.invalid_chars = invalid
invalid_chars = is_valid_path.invalid_chars
is_valid = len(set(path) & invalid_chars) == 0
return is_valid
#end def is_valid_path
[docs]
def is_valid_filename(filename: str | bytes | Path) -> bool:
'''Screen out filenames with invalid characters.'''
filename = _path_to_str(filename)
is_valid = True
if len(filename) == 0:
is_valid = False
elif filename == '.':
is_valid = False
elif filename.startswith('..'):
is_valid = False
elif '/' in filename:
is_valid = False
elif not is_valid_path(filename):
is_valid = False
return is_valid
#end def is_valid_filename
[docs]
def is_relative_path(path: str | bytes | Path):
'''Determine if a path is relative to some current working directory.'''
path = _path_to_str(path)
absolute = len(path) > 0 and (path[0] == '/' or path[0] == '~')
relative = not absolute
return relative
#end def is_relative_path
[docs]
def path_string(
path: str | bytes | Path,
strict: bool = False,
relative: bool = False,
check: bool = False,
) -> str:
"""Convert a path to a string.
Parameters
----------
path : str, bytes or Path
A file path or directory path.
strict : bool, default=False
Require inputted path to be str type.
Raises ValueError otherwise.
relative : bool, default=False
Check if path is a relative path.
Raises ValueError otherwise.
check : bool, default=True
Check if a path contains only valid characters.
ValueError is raised for invalid paths.
Returns
-------
path_out : str
The path as a string.
"""
if relative and not is_relative_path(path):
raise ValueError('path must be relative')
if strict:
if not isinstance(path, str):
raise ValueError('path must strictly be str type')
path_out = path
else:
path_out = _path_to_str(path)
if relative and not is_relative_path(path_out):
raise ValueError('path_string converted relative path into non-relative path')
if check and not is_valid_path(path_out):
raise ValueError('path contains invalid characters:\n'+path_out)
return path_out
#end def path_string