"""
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 _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 _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