pyfiles.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. from __future__ import annotations
  2. import atexit
  3. from contextlib import ExitStack
  4. import importlib
  5. import importlib.machinery
  6. import importlib.util
  7. import os
  8. import pathlib
  9. import re
  10. import tempfile
  11. from types import ModuleType
  12. from typing import Any
  13. from typing import Optional
  14. from typing import Union
  15. from mako import exceptions
  16. from mako.template import Template
  17. from . import compat
  18. from .exc import CommandError
  19. def template_to_file(
  20. template_file: Union[str, os.PathLike[str]],
  21. dest: Union[str, os.PathLike[str]],
  22. output_encoding: str,
  23. *,
  24. append_with_newlines: bool = False,
  25. **kw: Any,
  26. ) -> None:
  27. template = Template(filename=_preserving_path_as_str(template_file))
  28. try:
  29. output = template.render_unicode(**kw).encode(output_encoding)
  30. except:
  31. with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as ntf:
  32. ntf.write(
  33. exceptions.text_error_template()
  34. .render_unicode()
  35. .encode(output_encoding)
  36. )
  37. fname = ntf.name
  38. raise CommandError(
  39. "Template rendering failed; see %s for a "
  40. "template-oriented traceback." % fname
  41. )
  42. else:
  43. with open(dest, "ab" if append_with_newlines else "wb") as f:
  44. if append_with_newlines:
  45. f.write("\n\n".encode(output_encoding))
  46. f.write(output)
  47. def coerce_resource_to_filename(fname_or_resource: str) -> pathlib.Path:
  48. """Interpret a filename as either a filesystem location or as a package
  49. resource.
  50. Names that are non absolute paths and contain a colon
  51. are interpreted as resources and coerced to a file location.
  52. """
  53. # TODO: there seem to be zero tests for the package resource codepath
  54. if not os.path.isabs(fname_or_resource) and ":" in fname_or_resource:
  55. tokens = fname_or_resource.split(":")
  56. # from https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-filename # noqa E501
  57. file_manager = ExitStack()
  58. atexit.register(file_manager.close)
  59. ref = compat.importlib_resources.files(tokens[0])
  60. for tok in tokens[1:]:
  61. ref = ref / tok
  62. fname_or_resource = file_manager.enter_context( # type: ignore[assignment] # noqa: E501
  63. compat.importlib_resources.as_file(ref)
  64. )
  65. return pathlib.Path(fname_or_resource)
  66. def pyc_file_from_path(
  67. path: Union[str, os.PathLike[str]],
  68. ) -> Optional[pathlib.Path]:
  69. """Given a python source path, locate the .pyc."""
  70. pathpath = pathlib.Path(path)
  71. candidate = pathlib.Path(
  72. importlib.util.cache_from_source(pathpath.as_posix())
  73. )
  74. if candidate.exists():
  75. return candidate
  76. # even for pep3147, fall back to the old way of finding .pyc files,
  77. # to support sourceless operation
  78. ext = pathpath.suffix
  79. for ext in importlib.machinery.BYTECODE_SUFFIXES:
  80. if pathpath.with_suffix(ext).exists():
  81. return pathpath.with_suffix(ext)
  82. else:
  83. return None
  84. def load_python_file(
  85. dir_: Union[str, os.PathLike[str]], filename: Union[str, os.PathLike[str]]
  86. ) -> ModuleType:
  87. """Load a file from the given path as a Python module."""
  88. dir_ = pathlib.Path(dir_)
  89. filename_as_path = pathlib.Path(filename)
  90. filename = filename_as_path.name
  91. module_id = re.sub(r"\W", "_", filename)
  92. path = dir_ / filename
  93. ext = path.suffix
  94. if ext == ".py":
  95. if path.exists():
  96. module = load_module_py(module_id, path)
  97. else:
  98. pyc_path = pyc_file_from_path(path)
  99. if pyc_path is None:
  100. raise ImportError("Can't find Python file %s" % path)
  101. else:
  102. module = load_module_py(module_id, pyc_path)
  103. elif ext in (".pyc", ".pyo"):
  104. module = load_module_py(module_id, path)
  105. else:
  106. assert False
  107. return module
  108. def load_module_py(
  109. module_id: str, path: Union[str, os.PathLike[str]]
  110. ) -> ModuleType:
  111. spec = importlib.util.spec_from_file_location(module_id, path)
  112. assert spec
  113. module = importlib.util.module_from_spec(spec)
  114. spec.loader.exec_module(module) # type: ignore
  115. return module
  116. def _preserving_path_as_str(path: Union[str, os.PathLike[str]]) -> str:
  117. """receive str/pathlike and return a string.
  118. Does not convert an incoming string path to a Path first, to help with
  119. unit tests that are doing string path round trips without OS-specific
  120. processing if not necessary.
  121. """
  122. if isinstance(path, str):
  123. return path
  124. elif isinstance(path, pathlib.PurePath):
  125. return str(path)
  126. else:
  127. return str(pathlib.Path(path))