# -*- coding: utf-8 -*-
"""pysourceinfo.helper - miscellaneous utilities - details see manuals
The provided interfaces are foreseen for the bottom layer of the software stack,
thus provide basic handling only. This includes special cases of the file system
parameters. For more advanced file system path processing refer to *filesysobjects*.
"""
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
import re
from inspect import stack
from imp import PY_SOURCE, PY_COMPILED, C_EXTENSION, PKG_DIRECTORY, C_BUILTIN, PY_FROZEN
from pythonids import PYV35Plus, ISSTR
from sourceinfo import SourceInfoError, \
presolve, P_FIRST, P_LAST, P_LONGEST, P_SHORTEST, P_IGNORE0, \
OID_STR, OID_TUPLE, OID_LIST
if PYV35Plus:
# pylint: disable-msg=F0401
from importlib.machinery import SOURCE_SUFFIXES, DEBUG_BYTECODE_SUFFIXES, OPTIMIZED_BYTECODE_SUFFIXES, BYTECODE_SUFFIXES, EXTENSION_SUFFIXES # @UnresolvedImport
from importlib import util # @UnresolvedImport
# pylint: enable-msg=F0401
else:
from inspect import getmoduleinfo # @UnresolvedImport
__author__ = 'Arno-Can Uestuensoez'
__license__ = "Artistic-License-2.0 + Forced-Fairplay-Constraints"
__copyright__ = "Copyright (C) 2010-2017 Arno-Can Uestuensoez" \
" @Ingenieurbuero Arno-Can Uestuensoez"
__version__ = '0.1.21'
__uuid__ = '9de52399-7752-4633-9fdc-66c87a9200b8'
__docformat__ = "restructuredtext en"
# redundant for dependency reduction - keep in sync with pysourceinfo.objectinfo
__MT_UNKNOWN = 0
__MT_SOURCE = 1 # PY_SOURCE #1
__MT_COMPILED = 2 # PY_COMPILED #2
__MT_EXTENSION = 3 # C_EXTENSION #3
__MT_DIRECTORY = 5 # PKG_DIRECTORY #5
__MT_BUILTIN = 6 # C_BUILTIN #6
__MT_FROZEN = 7 # PY_FROZEN #7
__MT_COMPILED_OPT1 = 10 # PY_COMPILED | <opt1> # 2 | 8
__MT_COMPILED_OPT2 = 18 # PY_COMPILED | <opt2> # 2 | 16
__MT_COMPILED_DEBUG = 34 # PY_COMPILED # 2
_scname = re.compile(
r'''.*[\\\\/]([^.]+)[.]([^-]+)-([0-9]{2})[.](opt-[012])*([.].+)$''')
# removes init file from path - foreseen for package path to OID processing
CUT_INIT_PY = re.compile(r'__init__.py')
[docs]def getfilepathname_type(fpath):
"""Type for stored module as defined by *imp*.
Args:
fpath:
Module path.
Returns:
Returns the type of the file. ::
result := (
C_BUILTIN
| C_EXTENSION
| PKG_DIRECTORY
| PY_COMPILED
| PY_FROZEN
| PY_SOURCE
)
Raises:
pass-through
"""
if not PYV35Plus: # for now the more frequent call
if not os.path.exists(fpath):
return
ret = getmoduleinfo(fpath)
if ret and ret[3] in (PY_SOURCE, PY_COMPILED, C_EXTENSION, PKG_DIRECTORY, C_BUILTIN, PY_FROZEN):
return ret[3]
else:
if not os.path.exists(fpath):
return
#
# the redundnacy of type-postfixes requires now prefix-analysis by '_scname'
#
# In [1]: from importlib.machinery import SOURCE_SUFFIXES, DEBUG_BYTECODE_SUFFIXES
# ...: , OPTIMIZED_BYTECODE_SUFFIXES, BYTECODE_SUFFIXES, EXTENSION_SUFFIXES
#
# In [3]: SOURCE_SUFFIXES
# Out[3]: ['.py']
#
# In [4]: DEBUG_BYTECODE_SUFFIXES
# Out[4]: ['.pyc']
#
# In [5]: OPTIMIZED_BYTECODE_SUFFIXES
# Out[5]: ['.pyc']
#
# In [6]: BYTECODE_SUFFIXES
# Out[6]: ['.pyc']
#
# In [7]: EXTENSION_SUFFIXES
# Out[7]: ['.cpython-36m-x86_64-linux-gnu.so', '.abi3.so', '.so']
x = _scname.findall(fpath)
if x:
x = x[0] # trust...
else:
x = os.path.splitext(fpath)
if x[-1] in SOURCE_SUFFIXES:
return __MT_SOURCE
elif x[-1] in EXTENSION_SUFFIXES:
return __MT_EXTENSION
elif x[-1] in BYTECODE_SUFFIXES and not x[-2]:
return __MT_COMPILED
elif x[-1] in OPTIMIZED_BYTECODE_SUFFIXES and x[-2]:
if x[-2] == 'opt-1':
return __MT_COMPILED_OPT1
if x[-2] == 'opt-2':
return __MT_COMPILED_OPT2
raise SourceInfoError(
"Unknown opt:" + str(x[-2]) + " " + str(fpath))
elif x[-1] in DEBUG_BYTECODE_SUFFIXES:
return __MT_COMPILED_DEBUG
return __MT_UNKNOWN
def get_oid_filepathname(oid, spath=None):
"""File path name for OID.
"""
find_spec = util.find_spec(oid, spath)
if not find_spec is None:
return find_spec.origin
[docs]def getpythonpath(pname, plist=None, **kw):
"""Matches prefix from sys.path.
Foreseen to be used for canonical base references in unit tests.
This enables in particular for generic tests of file system positions
where originally absolute pathnames were required.
Args:
pname:
Path name to be searched an appropriate
pythonpath prefix for, either absolute
or relative to an item of *plist*.
plist:
List of possible python paths.
Alternative to *sys.path*.
default := sys.path
kw:
ispre:
If *True* adds a postfix path separator.
presolve:
Defines the path resolution
strategy::
presolve := (
P_FIRST
| P_LAST
| P_LONGEST
| P_SHORTEST
)
The default is stored in::
sourceinfo.presolve = P_FIRST
verify:
Verify against the filesystem, else no checks
are done.
default := True
Returns:
Returns the first matching path prefix from sys.path,
as 'normpath'.
Else::
ispre == True: returns empty str - ''
else: returns None
Raises:
SourceInfoError
pass-through
"""
_pre = kw.get('ispre')
_verify = kw.get('verify', True)
if not pname:
# shortcut for empty/None - so following check know a parameter is provided
if _verify:
raise SourceInfoError('no valid param pname = "%s"' % (str(pname)))
if _pre:
return ''
return None
if not plist:
plist = sys.path
_presolve = kw.get('presolve', presolve)
# path or object name
try:
_fp = os.path.normpath(pname)
except TypeError:
try:
_fp = os.path.normpath(pname.__file__)
except:
if _verify:
raise SourceInfoError('not valid pname = %s' % (str(pname)))
#
# now shure, 'pname' is s.th. valid
#
if os.path.isabs(_fp):
if pname in plist:
# is a search path itself
if _verify and not os.path.exists(pname):
# absolute is has to exist itself
raise SourceInfoError('does not exist: \n abs: %s' % (str(pname)))
# simply not searched
if _pre:
return _fp + os.sep
return _fp
if _verify:
_fplst = []
for x in plist:
if _fp.startswith(x) and os.path.exists(x):
_fplst.append(x)
else:
_fplst = [x for x in plist if _fp.startswith(x)]
else:
_fplst = []
for _sp in plist:
if _verify and not os.path.exists(_sp + os.sep + _fp):
continue
if _pre:
_fplst.append(_sp + os.sep)
else:
_fplst.append(_sp)
if _verify and not _fplst:
raise SourceInfoError('does not exist: %s' % (str(pname)))
if _presolve == P_FIRST:
if _verify and not os.path.exists(_fplst[0]):
raise SourceInfoError('does not exist: %s' % (str(_fplst[0])))
return _fplst[0]
elif _presolve == P_LAST:
if _verify and not os.path.exists(_fplst[-1]):
raise SourceInfoError('does not exist: %s' % (str(_fplst[-1])))
return _fplst[-1]
elif _presolve == P_LONGEST:
_fpnew = ''
for x in _fplst:
if _verify and not os.path.exists(x):
continue
if len(x.split(os.sep)) > len(_fpnew.split(os.sep)): # number of path-items
_fpnew = x
elif _presolve == P_SHORTEST:
_fpnew = ''
for x in _fplst:
if _verify and not os.path.exists(x):
continue
if not _fpnew:
_fpnew = x
elif len(x.split(os.sep)) < len(_fpnew.split(os.sep)): # number of path-items
_fpnew = x
else:
raise SourceInfoError("unknown presolve = " + str(presolve))
if _verify and not _fpnew:
raise SourceInfoError('does not exist: %s' % (str(pname)))
return _fpnew
[docs]def getpythonpath_rel(pname, plist=None, **kw):
"""Verifies an absolute or relative path name to be
reachable as a relative path to one item of the search
list *plist*.
Args:
pname:
Path name of a path searched for relative to
an item of *plist*. Absolute is cut, relative
is searched literally.
plist:
Search path alternative to *sys.path*.
*default := sys.path*
kargs:
presolve:
The type of path resolution: ::
pname is relative:
presolve := (
P_FIRST
)
pname is absolute:
presolve := (
P_FIRST
| P_LAST
| P_LONGEST
| P_SHORTEST
)
*default := pysourceinfo.presolve(P_FIRST)*
raw:
If *True* suppresses the call of 'os.path.normpath()'::
raw := (
True # keep e.g. multiple and trailing *os.sep*
| False # apply os.path.normpath()
)
default := False
normpath:
The method to be called for the normalization of the provided
input path. This is in particular required when cross-platform
path strings are provided. These are by default processed
internally with the *os.sep* of the current platform only.
The *API* is as defined by 'os.path.normpath'::
normapth := (
<custom-call>
| <default-call>
)
custom-call := (
ntpath
| posixpath
| <any-compatible-api>
)
default-call := os.path.normpath
verify:
Verify against the file system, else no checks
are done.
*default := True*
Returns:
First matched path from *sys.path* as normalized path.
Else::
if PYTHONPATH[i] == pname: returns empty str - ''
else:
if verify: raise SourceInfoError
else: returns None
Raises:
SourceInfoError
pass-through
"""
_normpath = kw.get('normpath', os.path.normpath)
_raw = kw.get('raw', False)
_verify = kw.get('verify')
if not pname:
# shortcut for empty/None - so following check know a parameter is provided
if _verify:
raise SourceInfoError('no valid param pname = "%s"' % (str(pname)))
return pname
if not plist:
plist = sys.path
if os.path.isabs(pname):
_presolve = kw.get('presolve', presolve)
else:
_presolve = P_FIRST
# path or object name
if not _raw and _normpath:
try:
_fp = _normpath(pname)
except TypeError:
try:
_fp = _normpath(pname.__file__)
except:
if _verify:
raise SourceInfoError('not valid pname = %s' % (str(pname)))
else:
_fp = pname
#
# now shure, 'pname' is s.th. valid
#
if os.path.isabs(_fp):
if pname in plist:
# is a search path itself
if _verify and not os.path.exists(pname):
# absolute is has to exist itself
raise SourceInfoError('does not exist: \n abs: %s' % (str(pname)))
if pname == _fp:
return ''
return _fp
# for now assume x=='' is not valid
_fplst = [x for x in plist if x and _fp.startswith(x)]
else:
_fplst = []
for _sp in plist:
if os.path.exists(_sp + os.sep + _fp):
_fplst.append(_sp)
else:
continue
# no plist
if not _fplst:
if pname == '':
if not _verify:
return ''
if not _verify:
return
raise SourceInfoError('does not exist: %s' % (str(pname)))
if _presolve == P_FIRST:
if _verify and not os.path.exists(_fplst[0]):
raise SourceInfoError('does not exist: %s' % (str(_fplst[0])))
if os.path.isabs(_fp):
_ret = _fp[len(_fplst[0]):]
if os.path.isabs(_ret):
return _ret[1:]
else:
return _fp
elif _presolve == P_LAST:
if _verify and not os.path.exists(_fplst[-1]):
raise SourceInfoError('does not exist: %s' % (str(_fplst[-1])))
if os.path.isabs(_fp):
_ret = _fp[len(_fplst[-1]):]
if os.path.isabs(_ret):
return _ret[1:]
else:
return _fp
elif _presolve == P_LONGEST:
_fpnew = ''
_shortest = ''
for x in _fplst:
if _verify and not os.path.exists(x):
continue
if not _shortest:
_shortest = x
for y in plist:
if x.startswith(y):
if y and len(y.split(os.sep)) < len(_shortest.split(os.sep)):
_shortest = y
if not _fpnew:
_fpnew = x
elif len(x.split(os.sep)) < len(_fpnew.split(os.sep)):
# number of path-items - search longest relative sub, neef shortest python path
_fpnew = x
if _fpnew:
if _fpnew[-1] == os.sep:
return pname[len(_fpnew):]
return pname[len(_fpnew) + 1:]
elif _presolve == P_SHORTEST:
# simple literal static sub directories only
kw['presolve'] = P_LONGEST
_longest = getpythonpath(_fp, plist, **kw)
_sp = _fp[len(_longest):]
if os.path.isabs(_sp):
return _sp[1:]
return _sp
else:
raise SourceInfoError("unknown presolve = " + str(presolve))
if _verify and not _fpnew:
raise SourceInfoError('does not exist: %s' % (str(pname)))
return _fpnew
[docs]def getpythonpath_rel_oid(pname, plist=None, **kw):
"""Calls *getpythonpath_rel*, returns the relative
path as dotted *OID* relative to the search path.
Args:
For interface parameters refer to::
sourceinfo.helper.getpythonpath_rel()
kargs:
For remaining parameters refer to
*getpythonpath_rel*.
restype:
Result type::
restype = (
OID_STR # OID as dottedt string representation
| OID_TUPLE # OID as tuple
| OID_LIST # OID as list
)
default := OID_STR
sepin:
The path separator::
default := os.sep
The function internally checks the presence of the provided path.
This is not cross-converted, thus requires native separator
characters. While the *NT* file system supports multiple separators,
the *posix* based file systems support regularly the slash '/' only.
Thus these fail when other separator characters are provided. For
advanced cross-capabilities refer to *filesysobjects*.
sepout:
The path separator::
default := '.'
Returns:
Returns the provided result by *getpythonpath_rel* in accordance to
the requested result type - see *restype*.
Raises:
SourceInfoError
pass-through
"""
_restype = kw.get('restype', OID_STR)
_sepin = kw.get('sepin', os.sep)
_sepout = kw.get('sepout', '.')
if not isinstance(pname, ISSTR):
raise SourceInfoError("Supports string input only, got:" + str(pname))
# __init__.py is not part of OID naming convention, so cut it off
_res = getpythonpath_rel(CUT_INIT_PY.sub('', pname), plist=None, **kw)
if not _res:
return
if isinstance(_res, ISSTR):
if _restype == OID_STR:
return re.sub(re.escape(_sepin), _sepout, _res)
elif _restype == OID_TUPLE:
return tuple(re.split(re.escape(_sepin), _res))
elif _restype == OID_LIST:
return list(re.split(re.escape(_sepin), _res))
raise SourceInfoError("Supports string conversion only, got:" + str(_res))
[docs]def getstack_frame(spos=1, fromtop=False):
"""List of current mem-addresses on stack frames.
Args:
spos:
Stack position.
fromtop:
Start on top, default from bottom.
Returns:
Returns the specified frame.
Raises:
passed through exceptions
"""
if fromtop:
return stack()[-1 * spos]
return stack()[spos]
[docs]def getstack_len():
"""Length of current stack.
Args:
none.
Returns:
Returns the length of current stack.
Raises:
pass-through
"""
return len(stack())
[docs]def matchpath(path, pathlist=None, pmatch=P_FIRST):
"""Match a file pathname of pathname on a pathlist.
Args:
path:
Path to be searched in *pathlist*.
pathlist:
Search path for a given *path*.
default := *sys.path*
pmatch:
Match criteria for search. ::
pmatch := (
P_FIRST, P_LAST, P_SHORTEST,
P_LONGEST, P_IGNORE0
)
default := *P_FIRST*
Returns:
Returns the matched pathname from pathlist.
Raises:
pass-through
"""
_buf = None
if pathlist == None:
pathlist = sys.path
if pmatch & P_IGNORE0:
_pl = pathlist[1:]
else:
_pl = pathlist
for sx in _pl:
p0 = os.path.normpath(sx)
if path.startswith(p0):
if pmatch & P_FIRST:
return p0
elif pmatch & P_LAST:
_buf = p0
elif pmatch & P_LONGEST:
if len(p0) > len(_buf):
_buf = p0
elif pmatch & P_SHORTEST:
if len(p0) < len(_buf) or not _buf:
_buf = p0
return _buf