"""
defines:
- deprecated(old_name, new_name, deprecated_version, levels=None)
- print_bad_path(path)
- object_attributes(obj, mode='public', keys_to_skip=None)
- object_methods(obj, mode='public', keys_to_skip=None)
"""
# -*- coding: utf-8 -*-
from types import MethodType, FunctionType
import os
import io
import sys
#import getpass
import inspect
import warnings
from pathlib import PurePath
#from abc import abstractmethod
from typing import Optional, Any
import pyNastran
PathLike = str | PurePath
[docs]
def ipython_info() -> Optional[str]:
"""determines if iPython/Jupyter notebook is running"""
try:
return get_ipython()
except NameError:
return None
[docs]
def is_file_obj(filename: PathLike) -> bool:
"""does this object behave like a file object?"""
return (
(hasattr(filename, 'read') and hasattr(filename, 'write'))
or isinstance(filename, (io.IOBase, io.StringIO))
)
#def b(string: str) -> bytes:
#"""reimplementation of six.b(...) to work in Python 2"""
#return string.encode('latin-1')
#def merge_dicts(dict_list, strict: bool=True):
#"""merges two or more dictionaries"""
#assert isinstance(dict_list, list), type(dict_list)
#dict_out = {}
#for adict in dict_list:
#assert isinstance(adict, dict), adict
#for key, value in adict.items():
#if key not in dict_out:
#dict_out[key] = value
#elif strict:
#raise RuntimeError('key=%r exists in multiple dictionaries' % key)
#else:
#print('key=%r is dropped?' % key)
#return dict_out
[docs]
def remove_files(filenames: list[PathLike]) -> None:
"""remvoes a series of files; quietly continues if the file can't be removed"""
for filename in filenames:
try:
os.remove(filename)
except OSError:
pass
[docs]
def is_binary_file(filename: PathLike) -> bool:
"""
Return true if the given filename is binary.
Parameters
----------
filename : str
the filename to test
Returns
-------
binary_flag : bool
True if filename is a binary file (contains null byte)
and False otherwise.
:raises: IOError if the file cannot be opened.
Based on the idea (.. seealso:: http://bytes.com/topic/python/answers/21222-determine-file-type-binary-text)
that file is binary if it contains null.
.. warning:: this may not work for unicode."""
assert isinstance(filename, (str, PurePath)), f'{filename!r} is not a valid filename'
check_path(filename)
with io.open(filename, mode='rb') as fil:
for chunk in iter(lambda: fil.read(1024), bytes()):
if b'\0' in chunk: # found null byte
return True
return False
[docs]
def check_path(filename: PathLike, name: str='file') -> None:
"""checks that the file exists"""
try:
exists = os.path.exists(filename)
except TypeError:
msg = f'cannot find {name}={str(filename)!r}\n'
raise TypeError(msg)
if not exists:
msg = f'cannot find {name}={str(filename)!r}\n{print_bad_path(filename)}'
raise FileNotFoundError(msg)
[docs]
def print_bad_path(path: PathLike) -> str:
"""
Prints information about the existence (access possibility) of the parts
of the given path. Useful for debugging when the path to a given file
is wrong.
Parameters
----------
path : str
path to check
Returns
-------
msg : str
string with information whether access to parts of the path
is possible
"""
#raw_path = path
if isinstance(path, PurePath):
path = str(path)
path0 = path
#if len(path) > 255:
#path = os.path.abspath(_filename(path))
#npath = os.path.dirname(path)
#res = [path]
#while path != npath:
#path, npath = npath, os.path.dirname(npath)
#res.append(path)
#msg = {True: 'passed', False: 'failed'}
#return '\n'.join(['%s: %s' % (msg[os.path.exists(i)], i[4:]) for i in res])
path = os.path.abspath(path)
npath = os.path.dirname(path)
res = [path]
while path != npath:
path, npath = npath, os.path.dirname(npath)
res.append(path)
msg = {True: 'passed', False: 'failed'}
if not os.path.exists(path0):
return 'Missing File:\n' + '\n'.join(['%s: %s' % (msg[os.path.exists(i)], i) for i in res])
return '\n'.join(['%s: %s' % (msg[os.path.exists(i)], i) for i in res])
#def _filename(filename: str) -> str:
#"""
#Prepends some magic data to a filename in order to have long filenames.
#.. warning:: This might be Windows specific.
#"""
#if len(filename) > 255:
#return '\\\\?\\' + filename
#return filename
def __object_attr(obj: Any,
mode: str,
keys_to_skip: list[str],
attr_type: str,
filter_properties: bool=False) -> list[str]:
"""list object attributes of a given type"""
#print('keys_to_skip=%s' % keys_to_skip)
keys_to_skip = [] if keys_to_skip is None else keys_to_skip
test = {
'public': lambda k: (not k.startswith('_') and k not in keys_to_skip),
'private': lambda k: (k.startswith('_') and not k.startswith('__')
and k not in keys_to_skip),
'both': lambda k: (not k.startswith('__') and k not in keys_to_skip),
'all': lambda k: (k not in keys_to_skip),
}
if mode not in test: # pragma: no cover
raise ValueError(f'Wrong mode={mode!r}! Accepted modes: public, private, both, all.')
check = test[mode]
out = []
obj_type = type(obj)
for key in dir(obj):
#if isinstance(key, abstractmethod): # doesn't work...
#print(key, ' abstractmethod')
#if isinstance(key, FunctionType):
#print(key, ' FunctionType')
#if isinstance(key, MethodType):
#print(key, ' MethodType')
if key in keys_to_skip or not check(key):
continue
try:
value = getattr(obj, key)
save_value = attr_type(value)
if not save_value:
continue
if filter_properties:
if not isinstance(getattr(obj_type, key, None), property):
out.append(key)
else:
out.append(key)
except Exception:
pass
out.sort()
return out
#return sorted([k for k in dir(obj) if (check(k) and
# attr_type(getattr(obj, k)))])
[docs]
def object_methods(obj: Any, mode: str='public',
keys_to_skip: Optional[list[str]]=None) -> list[str]:
"""
List the names of methods of a class as strings. Returns public methods
as default.
Parameters
----------
obj : instance
the object for checking
mode : str
defines what kind of methods will be listed
* "public" - names that do not begin with underscore
* "private" - names that begin with single underscore
* "both" - private and public
* "all" - all methods that are defined for the object
keys_to_skip : list[str]; default=None -> []
names to not consider to avoid deprecation warnings
Returns
-------
method : list[str]
sorted list of the names of methods of a given type
or None if the mode is wrong
"""
return __object_attr(obj, mode, keys_to_skip, lambda x: isinstance(x, MethodType))
[docs]
def simplify_object_keys(keys_to_skip: Optional[list[str]]) -> list[str]:
if keys_to_skip is None:
keys_to_skip = []
elif isinstance(keys_to_skip, str):
keys_to_skip = [keys_to_skip]
return keys_to_skip
[docs]
def object_stats(obj: Any,
mode: str='public',
keys_to_skip: Optional[list[str]]=None,
filter_properties: bool=False) -> str:
"""Prints an easy to read summary of the object"""
msg = '%s:\n' % obj.__class__.__name__
attrs = object_attributes(
obj, mode=mode, keys_to_skip=keys_to_skip,
filter_properties=filter_properties)
for name in sorted(attrs):
#if short and '_ref' in name:
#continue
value = getattr(obj, name)
msg += ' %-6s : %r\n' % (name, value)
return msg
[docs]
def object_attributes(obj: Any, mode: str='public',
keys_to_skip: Optional[list[str]]=None,
filter_properties: bool=False) -> list[str]:
"""
List the names of attributes of a class as strings. Returns public
attributes as default.
Parameters
----------
obj : instance
the object for checking
mode : str
defines what kind of attributes will be listed
* 'public' - names that do not begin with underscore
* 'private' - names that begin with single underscore
* 'both' - private and public
* 'all' - all attributes that are defined for the object
keys_to_skip : list[str]; default=None -> []
names to not consider to avoid deprecation warnings
filter_properties: bool: default=False
filters the @property objects
Returns
-------
attribute_names : list[str]
sorted list of the names of attributes of a given type or None
if the mode is wrong
"""
#if hasattr(obj, '__properties__'):
#keys_to_skip += obj.__properties__()
return __object_attr(
obj, mode, keys_to_skip,
lambda x: not isinstance(x, (MethodType, FunctionType)),
filter_properties=filter_properties,
)
#def remove_files(*filenames):
#"""delete a list of files"""
#failed_list = []
#for filename in filenames:
#try:
#os.remove(filename)
#except OSError: # OSError is the general version of WindowsError
#failed_list.append(filename)
#return failed_list
[docs]
def int_version(name: str, version: str) -> list[int]:
"""splits the version into a tuple of integers"""
sversion = version.split('-')[0].split('+')[0].split('.post')[0].split('a')[0].split('b')[0].split('rc')[0]
#numpy
#scipy
#matplotlib
#qtpy
#vtk
#cpylog
#pyNastran
# '1.20.0rc1'
# '1.4.0+dev.8913610a0'
# matplotlib 3.1rc1
# matplotlib 3.5.5b1
# matplotlib 3.9.1.post1
try:
return [int(val) for val in sversion.split('.')]
except ValueError:
raise SyntaxError('cannot determine version for %s %s' % (name, sversion))
def deprecated(old_name: str, new_name: str, deprecated_version: str,
levels: Optional[list[int]]=None) -> None:
"""
Throws a deprecation message and crashes if past a specific version.
Parameters
----------
old_name : str
the old function name
new_name : str
the new function name
deprecated_version : float
the version the method was first deprecated in
levels : list[int]
the deprecation levels to show
[1, 2, 3] shows 3 levels up from this function (good for classes)
None : ???
TODO: turn this into a decorator?
"""
assert isinstance(deprecated_version, str), type(deprecated_version)
assert isinstance(levels, list), type(levels)
assert old_name != new_name, "'%s' and '%s' are the same..." % (old_name, new_name)
version = pyNastran.__version__.split('_')[0]
dep_ver_tuple = tuple([int(i) for i in deprecated_version.split('.')])
ver_tuple = tuple([int(i) for i in version.split('.')[:2]])
#new_line = ''
msg = "'%s' was deprecated in v%s (current=%s)" % (
old_name, deprecated_version, version)
if new_name:
msg += "; replace it with '%s'\n" % new_name
for level in levels:
# jump to get out of the inspection code
frame = sys._getframe(4 + level)
line_no = frame.f_lineno
code = frame.f_code
try:
#filename = os.path.basename(frame.f_globals['__file__'])
filename = os.path.basename(inspect.getfile(code))
except Exception: # pragma: no cover
print(code)
raise
source_lines, line_no0 = inspect.getsourcelines(code)
delta_nlines = line_no - line_no0
try:
line = source_lines[delta_nlines]
except IndexError:
break
msg += ' %-25s:%-4s %s\n' % (filename, str(line_no) + ';', line.strip())
#user_name = getpass.getuser()
if ver_tuple > dep_ver_tuple: # or 'id' in msg:
# fail
raise NotImplementedError(msg)
#elif user_name not in ['travis']:
warnings.warn(msg, DeprecationWarning)