module_utils.py

#

Module Manipulation

import builtins
import importlib._bootstrap
import importlib._bootstrap_external
import importlib.machinery
import importlib.util
import logging
import os
import sys
from typing import Any, Dict, Tuple, cast

from pydoc_fork.custom_types import TypeLike

LOGGER = logging.getLogger(__name__)
#

Errors that occurred while trying to import something to document it.

class ErrorDuringImport(Exception):
#
#
    def __init__(self, filename: str, exc_info: Tuple[Any, Any, Any]) -> None:
        self.filename = filename
        self.exc, self.value, self.tb = exc_info
#
    def __str__(self) -> str:
        exc = self.exc.__name__
        return f"problem in {self.filename} - {exc}: {self.value}"
#

Import a Python source file or compiled file given its path.

def importfile(path: str) -> TypeLike:
#
    magic = importlib.util.MAGIC_NUMBER
    with open(path, "rb") as file:
        is_bytecode = magic == file.read(len(magic))
    filename = os.path.basename(path)
    name, _ = os.path.splitext(filename)
    if is_bytecode:
        loader = importlib._bootstrap_external.SourcelessFileLoader(name, path)
    else:
        loader = importlib._bootstrap_external.SourceFileLoader(name, path)
#

XXX We probably don’t need to pass in the loader here.

    spec = importlib.util.spec_from_file_location(name, path, loader=loader)
    try:
        return cast(TypeLike, importlib._bootstrap._load(spec))
#

pylint: disable=broad-except

    except BaseException as import_error:
        raise ErrorDuringImport(path, sys.exc_info()) from import_error
#

Import a module; handle errors; return None if the module isn’t found.

def safe_import(
    path: str,
    forceload: int = 0,
    cache: Dict[str, Any] = {},  # noqa - this is mutable on purpose!
) -> Any:
#

If the module is found but an exception occurs, it’s wrapped in an ErrorDuringImport exception and reraised. Unlike import, if a package path is specified, the module at the end of the path is returned, not the package at the beginning. If the optional ‘forceload’ argument

    is 1, we reload the module from disk (unless it's a dynamic extension)."""
    LOGGER.debug(str(path))
    try:
#

If forceload is 1 and the module has been previously loaded from disk, we always have to reload the module. Checking the file’s mtime isn’t good enough (e.g. the module could contain a class that inherits from another module that has changed).

        if forceload and path in sys.modules:
            if path not in sys.builtin_module_names:
#

Remove the module from sys.modules and re-import to try and avoid problems with partially loaded modules. Also remove any submodules because they won’t appear in the newly loaded module’s namespace if they’re already in sys.modules.

                subs = [m for m in sys.modules if m.startswith(path + ".")]
                for key in [path] + subs:
#

Prevent garbage collection.

                    cache[key] = sys.modules[key]
                    del sys.modules[key]
        module = __import__(path)
#

pylint: disable=broad-except

    except BaseException as import_error:
#

Did the error occur before or after the module was found?

        (exc, value, _) = info = sys.exc_info()
        if path in sys.modules:
#

An error occurred while executing the imported module.

            raise ErrorDuringImport(sys.modules[path].__file__, info) from import_error
        if exc is SyntaxError:
#

A SyntaxError occurred before we could execute the module. MR : this isn’t null safe.

            raise ErrorDuringImport(value.filename, info) from import_error
        if issubclass(exc, ImportError) and value.name == path:
#

No such module in the path.

            return None
#

Some other error occurred during the importing process.

        raise ErrorDuringImport(path, sys.exc_info()) from import_error
    for part in path.split(".")[1:]:
        try:
            module = getattr(module, part)
        except AttributeError:
            return None
    return module
#
def ispackage(path: str) -> bool:
    """Guess whether a path refers to a package directory."""
    if os.path.isdir(path):
        for ext in (".py", ".pyc"):
            if os.path.isfile(os.path.join(path, "__init__" + ext)):
                return True
    return False
#

Locate an object by name or dotted path, importing as necessary.

def locate(path: str, forceload: int = 0) -> Any:
#
    if "-" in path:
#

Not sure about this

        path = path.replace("-","_")

    LOGGER.debug(f"locating {path}")
    parts = [part for part in path.split(".") if part]
    LOGGER.debug(str(parts))
    module, index = None, 0
    while index < len(parts):
        nextmodule = safe_import(".".join(parts[: index + 1]), forceload)
        if nextmodule:
            module, index = nextmodule, index + 1
        else:
            break
    if module:
        the_object = module
#

this errors?! LOGGER.debug(f”putative module {str(the_object)}”)

    else:
        the_object = builtins

    for part in parts[index:]:
        try:
            the_object = getattr(the_object, part)
        except AttributeError:
            LOGGER.debug(f"Don't think this is a module {the_object}")
            return None
    return the_object