Files
2025-12-10 22:47:38 +01:00

154 lines
4.6 KiB
Python

from __future__ import annotations
import atexit
from contextlib import ExitStack
import importlib
import importlib.machinery
import importlib.util
import os
import pathlib
import re
import tempfile
from types import ModuleType
from typing import Any
from typing import Optional
from typing import Union
from mako import exceptions
from mako.template import Template
from . import compat
from .exc import CommandError
def template_to_file(
template_file: Union[str, os.PathLike[str]],
dest: Union[str, os.PathLike[str]],
output_encoding: str,
*,
append_with_newlines: bool = False,
**kw: Any,
) -> None:
template = Template(filename=_preserving_path_as_str(template_file))
try:
output = template.render_unicode(**kw).encode(output_encoding)
except:
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as ntf:
ntf.write(
exceptions.text_error_template()
.render_unicode()
.encode(output_encoding)
)
fname = ntf.name
raise CommandError(
"Template rendering failed; see %s for a "
"template-oriented traceback." % fname
)
else:
with open(dest, "ab" if append_with_newlines else "wb") as f:
if append_with_newlines:
f.write("\n\n".encode(output_encoding))
f.write(output)
def coerce_resource_to_filename(fname_or_resource: str) -> pathlib.Path:
"""Interpret a filename as either a filesystem location or as a package
resource.
Names that are non absolute paths and contain a colon
are interpreted as resources and coerced to a file location.
"""
# TODO: there seem to be zero tests for the package resource codepath
if not os.path.isabs(fname_or_resource) and ":" in fname_or_resource:
tokens = fname_or_resource.split(":")
# from https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-filename # noqa E501
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = compat.importlib_resources.files(tokens[0])
for tok in tokens[1:]:
ref = ref / tok
fname_or_resource = file_manager.enter_context( # type: ignore[assignment] # noqa: E501
compat.importlib_resources.as_file(ref)
)
return pathlib.Path(fname_or_resource)
def pyc_file_from_path(
path: Union[str, os.PathLike[str]],
) -> Optional[pathlib.Path]:
"""Given a python source path, locate the .pyc."""
pathpath = pathlib.Path(path)
candidate = pathlib.Path(
importlib.util.cache_from_source(pathpath.as_posix())
)
if candidate.exists():
return candidate
# even for pep3147, fall back to the old way of finding .pyc files,
# to support sourceless operation
ext = pathpath.suffix
for ext in importlib.machinery.BYTECODE_SUFFIXES:
if pathpath.with_suffix(ext).exists():
return pathpath.with_suffix(ext)
else:
return None
def load_python_file(
dir_: Union[str, os.PathLike[str]], filename: Union[str, os.PathLike[str]]
) -> ModuleType:
"""Load a file from the given path as a Python module."""
dir_ = pathlib.Path(dir_)
filename_as_path = pathlib.Path(filename)
filename = filename_as_path.name
module_id = re.sub(r"\W", "_", filename)
path = dir_ / filename
ext = path.suffix
if ext == ".py":
if path.exists():
module = load_module_py(module_id, path)
else:
pyc_path = pyc_file_from_path(path)
if pyc_path is None:
raise ImportError("Can't find Python file %s" % path)
else:
module = load_module_py(module_id, pyc_path)
elif ext in (".pyc", ".pyo"):
module = load_module_py(module_id, path)
else:
assert False
return module
def load_module_py(
module_id: str, path: Union[str, os.PathLike[str]]
) -> ModuleType:
spec = importlib.util.spec_from_file_location(module_id, path)
assert spec
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
return module
def _preserving_path_as_str(path: Union[str, os.PathLike[str]]) -> str:
"""receive str/pathlike and return a string.
Does not convert an incoming string path to a Path first, to help with
unit tests that are doing string path round trips without OS-specific
processing if not necessary.
"""
if isinstance(path, str):
return path
elif isinstance(path, pathlib.PurePath):
return str(path)
else:
return str(pathlib.Path(path))