Source code for mkinit.util.util_import

"""
This file was autogenerated based on code in :py:mod:`ubelt` via
dev/maintain/port_ubelt_code.py in the mkinit repo.
"""

import os
import sys
from os.path import (
    abspath,
    basename,
    dirname,
    exists,
    expanduser,
    isdir,
    isfile,
    join,
    realpath,
    relpath,
    split,
    splitext,
)

IS_PY_GE_308 = (sys.version_info[0] >= 3) and (sys.version_info[1] >= 8)


[docs] def _parse_static_node_value(node): """ Extract a constant value from a node if possible """ import ast import numbers from collections import OrderedDict if ( isinstance(node, ast.Constant) and isinstance(node.value, numbers.Number) if IS_PY_GE_308 else isinstance(node, ast.Constant) ): value = node.value if IS_PY_GE_308 else node.n elif ( isinstance(node, ast.Constant) and isinstance(node.value, str) if IS_PY_GE_308 else isinstance(node, ast.Constant) ): value = node.value if IS_PY_GE_308 else node.s elif isinstance(node, ast.List): value = list(map(_parse_static_node_value, node.elts)) elif isinstance(node, ast.Tuple): value = tuple(map(_parse_static_node_value, node.elts)) elif isinstance(node, (ast.Dict)): keys = map(_parse_static_node_value, node.keys) values = map(_parse_static_node_value, node.values) value = OrderedDict(zip(keys, values)) # value = dict(zip(keys, values)) elif isinstance(node, (ast.Constant)): value = node.value else: raise TypeError( 'Cannot parse a static value from non-static node ' 'of type: {!r}'.format(type(node)) ) return value
[docs] def _extension_module_tags(): """ Returns valid tags an extension module might have Returns: List[str] """ import sysconfig tags = [] # handle PEP 3149 -- ABI version tagged .so files # ABI = application binary interface tags.append(sysconfig.get_config_var('SOABI')) tags.append('abi3') # not sure why this one is valid but it is tags = [t for t in tags if t] return tags
[docs] def _static_parse(varname, fpath): """ Statically parse the a constant variable from a python file Args: varname (str): variable name to extract fpath (str | PathLike): path to python file to parse Returns: Any: the static value Example: >>> # xdoctest: +SKIP("ubelt dependency") >>> dpath = ub.Path.appdir('tests/import/staticparse').ensuredir() >>> fpath = (dpath / 'foo.py') >>> fpath.write_text('a = {1: 2}') >>> assert _static_parse('a', fpath) == {1: 2} >>> fpath.write_text('a = 2') >>> assert _static_parse('a', fpath) == 2 >>> fpath.write_text('a = "3"') >>> assert _static_parse('a', fpath) == "3" >>> fpath.write_text('a = ["3", 5, 6]') >>> assert _static_parse('a', fpath) == ["3", 5, 6] >>> fpath.write_text('a = ("3", 5, 6)') >>> assert _static_parse('a', fpath) == ("3", 5, 6) >>> fpath.write_text('b = 10' + chr(10) + 'a = None') >>> assert _static_parse('a', fpath) is None >>> import pytest >>> with pytest.raises(TypeError): >>> fpath.write_text('a = list(range(10))') >>> assert _static_parse('a', fpath) is None >>> with pytest.raises(AttributeError): >>> fpath.write_text('a = list(range(10))') >>> assert _static_parse('c', fpath) is None >>> if sys.version_info[0:2] >= (3, 6): >>> # Test with type annotations >>> fpath.write_text('b: int = 10') >>> assert _static_parse('b', fpath) == 10 """ import ast if not exists(fpath): raise ValueError('fpath={!r} does not exist'.format(fpath)) with open(fpath, 'r') as file_: sourcecode = file_.read() pt = ast.parse(sourcecode) class StaticVisitor(ast.NodeVisitor): def visit_Assign(self, node): for target in node.targets: target_id = getattr(target, 'id', None) if target_id == varname: self.static_value = _parse_static_node_value(node.value) def visit_AnnAssign(self, node): """Handle annotated assignments like `VAR: Type = value`""" if getattr(node.target, 'id', None) == varname: if node.value is not None: self.static_value = _parse_static_node_value(node.value) visitor = StaticVisitor() visitor.visit(pt) try: value = visitor.static_value except AttributeError: value = 'Unknown {}'.format(varname) raise AttributeError(value) return value
[docs] def _platform_pylib_exts(): # nocover """ Returns .so, .pyd, or .dylib depending on linux, win or mac. On python3 return the previous with and without abi (e.g. .cpython-35m-x86_64-linux-gnu) flags. On python2 returns with and without multiarch. Returns: tuple """ import sysconfig valid_exts = [] # return with and without API flags # handle PEP 3149 -- ABI version tagged .so files base_ext = '.' + sysconfig.get_config_var('EXT_SUFFIX').split('.')[-1] for tag in _extension_module_tags(): valid_exts.append('.' + tag + base_ext) valid_exts.append(base_ext) return tuple(valid_exts)
[docs] def _syspath_modname_to_modpath(modname, sys_path=None, exclude=None): """ syspath version of modname_to_modpath Args: modname (str): name of module to find sys_path (None | List[str | PathLike]): The paths to search for the module. If unspecified, defaults to ``sys.path``. exclude (List[str | PathLike] | None): If specified prevents these directories from being searched. Defaults to None. Returns: str: path to the module. Note: This is much slower than the pkgutil mechanisms. There seems to be a change to the editable install mechanism: https://github.com/pypa/setuptools/issues/3548 Trying to find more docs about it. TODO: add a test where we make an editable install, regular install, standalone install, and check that we always find the right path. Example: >>> print(_syspath_modname_to_modpath('xdoctest.static_analysis')) ...static_analysis.py >>> print(_syspath_modname_to_modpath('xdoctest')) ...xdoctest >>> # xdoctest: +REQUIRES(CPython) >>> print(_syspath_modname_to_modpath('_ctypes')) ..._ctypes... >>> assert _syspath_modname_to_modpath('xdoctest', sys_path=[]) is None >>> assert _syspath_modname_to_modpath('xdoctest.static_analysis', sys_path=[]) is None >>> assert _syspath_modname_to_modpath('_ctypes', sys_path=[]) is None >>> assert _syspath_modname_to_modpath('this', sys_path=[]) is None Example: >>> # test what happens when the module is not visible in the path >>> modname = 'xdoctest.static_analysis' >>> modpath = _syspath_modname_to_modpath(modname) >>> exclude = [split_modpath(modpath)[0]] >>> found = _syspath_modname_to_modpath(modname, exclude=exclude) >>> if found is not None: >>> # Note: the basic form of this test may fail if there are >>> # multiple versions of the package installed. Try and fix that. >>> other = split_modpath(found)[0] >>> assert other not in exclude >>> exclude.append(other) >>> found = _syspath_modname_to_modpath(modname, exclude=exclude) >>> if found is not None: >>> raise AssertionError( >>> 'should not have found {}.'.format(found) + >>> ' because we excluded: {}.'.format(exclude) + >>> ' cwd={} '.format(os.getcwd()) + >>> ' sys.path={} '.format(sys.path) >>> ) """ import glob def _isvalid(modpath, base): # every directory up to the module, should have an init subdir = dirname(modpath) while subdir and subdir != base: if not exists(join(subdir, '__init__.py')): return False subdir = dirname(subdir) return True _fname_we = modname.replace('.', os.path.sep) candidate_fnames = [ _fname_we + '.py', # _fname_we + '.pyc', # _fname_we + '.pyo', ] # Add extension library suffixes candidate_fnames += [_fname_we + ext for ext in _platform_pylib_exts()] if sys_path is None: sys_path = sys.path # the empty string in sys.path indicates cwd. Change this to a '.' candidate_dpaths = ['.' if p == '' else p for p in sys_path] if exclude: def normalize(p): if sys.platform.startswith('win32'): # nocover return realpath(p).lower() else: return realpath(p) # Keep only the paths not in exclude real_exclude = {normalize(p) for p in exclude} candidate_dpaths = [ p for p in candidate_dpaths if normalize(p) not in real_exclude ] def check_dpath(dpath): # Check for directory-based modules (has presidence over files) modpath = join(dpath, _fname_we) if exists(modpath): if isfile(join(modpath, '__init__.py')): if _isvalid(modpath, dpath): return modpath # If that fails, check for file-based modules for fname in candidate_fnames: modpath = join(dpath, fname) if isfile(modpath): if _isvalid(modpath, dpath): return modpath _pkg_name = _fname_we.split(os.path.sep)[0] _pkg_name_hypen = _pkg_name.replace('_', '-') _egglink_fname1 = _pkg_name + '.egg-link' _egglink_fname2 = _pkg_name_hypen + '.egg-link' # FIXME! suffixed modules will clobber break! # Currently mitigating this by looping over all possible matches, # but it would be nice to ensure we are not matching suffixes. # however, we should probably match and handle different versions. _editable_fname_pth_pat = '__editable__.' + _pkg_name + '-*.pth' # NOTE: the __editable__ finders are named after the package, but the # module could have a different name, so we cannot use the package name # (which in this case is really the module name) in the pattern, and we # have to check all of the finders. # _editable_fname_finder_py_pat = '__editable___' + _pkg_name + '_*finder.py' _editable_fname_finder_py_pat = '__editable___*_*finder.py' found_modpath = None for dpath in candidate_dpaths: modpath = check_dpath(dpath) if modpath: found_modpath = modpath break # Attempt to handle PEP660 import hooks. # We should look for a finder path first, because a pth might # not contain a real path, but code to load the finder. # Which one is used is defined in setuptools/editable_wheel.py # It will depend on an "Editable Strategy". # Basically a finder will be used for "complex" structures and # basic pth will be used for "simple" structures (which means has a # src/modname folder). new_editable_finder_paths = sorted( glob.glob(join(dpath, _editable_fname_finder_py_pat)) ) if new_editable_finder_paths: # nocover # This makes some assumptions, which may not hold in general # We may need to fallback entirely on pkgutil, which would # ultimately be good. Hopefully the new standards mean it does not # break with pytest anymore? Nope, pytest still doesn't work right # with it. for finder_fpath in new_editable_finder_paths: try: mapping = _static_parse('MAPPING', finder_fpath) except AttributeError: ... else: try: target = dirname(mapping[_pkg_name]) except KeyError: ... else: if ( not exclude or normalize(target) not in real_exclude ): # pragma: nobranch modpath = check_dpath(target) if modpath: # pragma: nobranch found_modpath = modpath break if found_modpath is not None: break # If a finder does not exist, then the __editable__ pth file might hold # the path itself. Check for that. new_editable_pth_paths = sorted( glob.glob(join(dpath, _editable_fname_pth_pat)) ) if new_editable_pth_paths: # nocover # Disable coverage because the test that covers this is too slow. # It can be made faster, re-enable when that lands. import pathlib for editable_pth in new_editable_pth_paths: editable_pth = pathlib.Path(editable_pth) target = editable_pth.read_text().strip().split('\n')[-1] if not exclude or normalize(target) not in real_exclude: modpath = check_dpath(target) if modpath: # pragma: nobranch found_modpath = modpath break if found_modpath is not None: break # If file path checks fails, check for egg-link based modules # (Python usually puts egg links into sys.path, but if the user is # providing the path then it is important to check them explicitly) linkpath1 = join(dpath, _egglink_fname1) linkpath2 = join(dpath, _egglink_fname2) linkpath = None if isfile(linkpath1): # nocover linkpath = linkpath1 elif isfile(linkpath2): # nocover linkpath = linkpath2 if linkpath is not None: # nocover # We exclude this from coverage because its difficult to write a # unit test where we can enforce that there is a module installed # in development mode. # Note: the new test_editable_modules.py test can do this, but # this old method may no longer be supported. # TODO: ensure this is the correct way to parse egg-link files # https://setuptools.readthedocs.io/en/latest/formats.html#egg-links # The docs state there should only be one line, but I see two. with open(linkpath, 'r') as file: target = file.readline().strip() if not exclude or normalize(target) not in real_exclude: modpath = check_dpath(target) if modpath: found_modpath = modpath break return found_modpath
[docs] def modname_to_modpath(modname, hide_init=True, hide_main=False, sys_path=None): """ Finds the path to a python module from its name. Determines the path to a python module without directly import it Converts the name of a module (__name__) to the path (__file__) where it is located without importing the module. Returns None if the module does not exist. Args: modname (str): The name of a module in ``sys_path``. hide_init (bool): if False, __init__.py will be returned for packages. Defaults to True. hide_main (bool): if False, and ``hide_init`` is True, __main__.py will be returned for packages, if it exists. Defaults to False. sys_path (None | List[str | PathLike]): The paths to search for the module. If unspecified, defaults to ``sys.path``. Returns: str | None: modpath - path to the module, or None if it doesn't exist Example: >>> modname = 'xdoctest.__main__' >>> modpath = modname_to_modpath(modname, hide_main=False) >>> assert modpath.endswith('__main__.py') >>> modname = 'xdoctest' >>> modpath = modname_to_modpath(modname, hide_init=False) >>> assert modpath.endswith('__init__.py') >>> # xdoctest: +REQUIRES(CPython) >>> modpath = basename(modname_to_modpath('_ctypes')) >>> assert 'ctypes' in modpath """ if hide_main or sys_path: modpath = _syspath_modname_to_modpath(modname, sys_path) else: # import xdev # with xdev.embed_on_exception_context: # try: # modpath = _importlib_modname_to_modpath(modname) # except Exception: # modpath = _syspath_modname_to_modpath(modname, sys_path) # modpath = _pkgutil_modname_to_modpath(modname, sys_path) modpath = _syspath_modname_to_modpath(modname, sys_path) if modpath is None: return None modpath = normalize_modpath( modpath, hide_init=hide_init, hide_main=hide_main ) return modpath
[docs] def normalize_modpath(modpath, hide_init=True, hide_main=False): """ Normalizes __init__ and __main__ paths. Args: modpath (str | PathLike): path to a module hide_init (bool): if True, always return package modules as __init__.py files otherwise always return the dpath. Defaults to True. hide_main (bool): if True, always strip away main files otherwise ignore __main__.py. Defaults to False. Returns: str | PathLike: a normalized path to the module Note: Adds __init__ if reasonable, but only removes __main__ by default Example: >>> from xdoctest import static_analysis as module >>> modpath = module.__file__ >>> assert normalize_modpath(modpath) == modpath.replace('.pyc', '.py') >>> dpath = dirname(modpath) >>> res0 = normalize_modpath(dpath, hide_init=0, hide_main=0) >>> res1 = normalize_modpath(dpath, hide_init=0, hide_main=1) >>> res2 = normalize_modpath(dpath, hide_init=1, hide_main=0) >>> res3 = normalize_modpath(dpath, hide_init=1, hide_main=1) >>> assert res0.endswith('__init__.py') >>> assert res1.endswith('__init__.py') >>> assert not res2.endswith('.py') >>> assert not res3.endswith('.py') """ if hide_init: if basename(modpath) == '__init__.py': modpath = dirname(modpath) hide_main = True else: # add in init, if reasonable modpath_with_init = join(modpath, '__init__.py') if exists(modpath_with_init): modpath = modpath_with_init if hide_main: # We can remove main, but dont add it if basename(modpath) == '__main__.py': # corner case where main might just be a module name not in a pkg parallel_init = join(dirname(modpath), '__init__.py') if exists(parallel_init): modpath = dirname(modpath) return modpath
[docs] def modpath_to_modname( modpath, hide_init=True, hide_main=False, check=True, relativeto=None ): """ Determines importable name from file path Converts the path to a module (__file__) to the importable python name (__name__) without importing the module. The filename is converted to a module name, and parent directories are recursively included until a directory without an __init__.py file is encountered. Args: modpath (str): Module filepath hide_init (bool): Removes the __init__ suffix. Defaults to True. hide_main (bool): Removes the __main__ suffix. Defaults to False. check (bool): If False, does not raise an error if modpath is a dir and does not contain an __init__ file. Defaults to True. relativeto (str | None): If specified, all checks are ignored and this is considered the path to the root module. Defaults to None. TODO: - [ ] Does this need modification to support PEP 420? https://www.python.org/dev/peps/pep-0420/ Returns: str: modname Raises: ValueError: if check is True and the path does not exist Example: >>> from xdoctest import static_analysis >>> modpath = static_analysis.__file__.replace('.pyc', '.py') >>> modpath = modpath.replace('.pyc', '.py') >>> modname = modpath_to_modname(modpath) >>> assert modname == 'xdoctest.static_analysis' Example: >>> import xdoctest >>> assert modpath_to_modname(xdoctest.__file__.replace('.pyc', '.py')) == 'xdoctest' >>> assert modpath_to_modname(dirname(xdoctest.__file__.replace('.pyc', '.py'))) == 'xdoctest' Example: >>> # xdoctest: +REQUIRES(CPython) >>> modpath = modname_to_modpath('_ctypes') >>> modname = modpath_to_modname(modpath) >>> assert modname == '_ctypes' Example: >>> modpath = '/foo/libfoobar.linux-x86_64-3.6.so' >>> modname = modpath_to_modname(modpath, check=False) >>> assert modname == 'libfoobar' """ if check and relativeto is None: if not exists(modpath): raise ValueError('modpath={} does not exist'.format(modpath)) modpath_ = abspath(expanduser(modpath)) modpath_ = normalize_modpath( modpath_, hide_init=hide_init, hide_main=hide_main ) if relativeto: dpath = dirname(abspath(expanduser(relativeto))) rel_modpath = relpath(modpath_, dpath) else: dpath, rel_modpath = split_modpath(modpath_, check=check) modname = splitext(rel_modpath)[0] if '.' in modname: modname, abi_tag = modname.split('.', 1) modname = modname.replace('/', '.') modname = modname.replace('\\', '.') return modname
[docs] def split_modpath(modpath, check=True): """ Splits the modpath into the dir that must be in PYTHONPATH for the module to be imported and the modulepath relative to this directory. Args: modpath (str): module filepath check (bool): if False, does not raise an error if modpath is a directory and does not contain an ``__init__.py`` file. Returns: Tuple[str, str]: (directory, rel_modpath) Raises: ValueError: if modpath does not exist or is not a package Example: >>> from xdoctest import static_analysis >>> modpath = static_analysis.__file__.replace('.pyc', '.py') >>> modpath = abspath(modpath) >>> dpath, rel_modpath = split_modpath(modpath) >>> recon = join(dpath, rel_modpath) >>> assert recon == modpath >>> assert rel_modpath == join('xdoctest', 'static_analysis.py') """ modpath_ = abspath(expanduser(modpath)) if check: if not exists(modpath_): if not exists(modpath): raise ValueError('modpath={} does not exist'.format(modpath)) raise ValueError('modpath={} is not a module'.format(modpath)) if isdir(modpath_) and not exists(join(modpath, '__init__.py')): # dirs without inits are not modules raise ValueError('modpath={} is not a module'.format(modpath)) full_dpath, fname_ext = split(modpath_) _relmod_parts = [fname_ext] # Recurse down directories until we are out of the package dpath = full_dpath while exists(join(dpath, '__init__.py')): dpath, dname = split(dpath) _relmod_parts.append(dname) relmod_parts = _relmod_parts[::-1] rel_modpath = os.path.sep.join(relmod_parts) return dpath, rel_modpath