imports.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import sys
  2. import warnings
  3. from typing import Any, Dict, Iterable, Optional
  4. __all__ = ["lazy_import"]
  5. def import_name(name: str, source: str, namespace: Dict[str, Any]) -> Any:
  6. """
  7. Import <name> from <source> in <namespace>.
  8. There are two cases:
  9. - <name> is an object defined in <source>
  10. - <name> is a submodule of source
  11. Neither __import__ nor importlib.import_module does exactly this.
  12. __import__ is closer to the intended behavior.
  13. """
  14. level = 0
  15. while source[level] == ".":
  16. level += 1
  17. assert level < len(source), "importing from parent isn't supported"
  18. module = __import__(source[level:], namespace, None, [name], level)
  19. return getattr(module, name)
  20. def lazy_import(
  21. namespace: Dict[str, Any],
  22. aliases: Optional[Dict[str, str]] = None,
  23. deprecated_aliases: Optional[Dict[str, str]] = None,
  24. ) -> None:
  25. """
  26. Provide lazy, module-level imports.
  27. Typical use::
  28. __getattr__, __dir__ = lazy_import(
  29. globals(),
  30. aliases={
  31. "<name>": "<source module>",
  32. ...
  33. },
  34. deprecated_aliases={
  35. ...,
  36. }
  37. )
  38. This function defines __getattr__ and __dir__ per PEP 562.
  39. On Python 3.6 and earlier, it falls back to non-lazy imports and doesn't
  40. raise deprecation warnings.
  41. """
  42. if aliases is None:
  43. aliases = {}
  44. if deprecated_aliases is None:
  45. deprecated_aliases = {}
  46. namespace_set = set(namespace)
  47. aliases_set = set(aliases)
  48. deprecated_aliases_set = set(deprecated_aliases)
  49. assert not namespace_set & aliases_set, "namespace conflict"
  50. assert not namespace_set & deprecated_aliases_set, "namespace conflict"
  51. assert not aliases_set & deprecated_aliases_set, "namespace conflict"
  52. package = namespace["__name__"]
  53. if sys.version_info[:2] >= (3, 7):
  54. def __getattr__(name: str) -> Any:
  55. assert aliases is not None # mypy cannot figure this out
  56. try:
  57. source = aliases[name]
  58. except KeyError:
  59. pass
  60. else:
  61. return import_name(name, source, namespace)
  62. assert deprecated_aliases is not None # mypy cannot figure this out
  63. try:
  64. source = deprecated_aliases[name]
  65. except KeyError:
  66. pass
  67. else:
  68. warnings.warn(
  69. f"{package}.{name} is deprecated",
  70. DeprecationWarning,
  71. stacklevel=2,
  72. )
  73. return import_name(name, source, namespace)
  74. raise AttributeError(f"module {package!r} has no attribute {name!r}")
  75. namespace["__getattr__"] = __getattr__
  76. def __dir__() -> Iterable[str]:
  77. return sorted(namespace_set | aliases_set | deprecated_aliases_set)
  78. namespace["__dir__"] = __dir__
  79. else: # pragma: no cover
  80. for name, source in aliases.items():
  81. namespace[name] = import_name(name, source, namespace)
  82. for name, source in deprecated_aliases.items():
  83. namespace[name] = import_name(name, source, namespace)