diff --git a/.gitignore b/.gitignore index aa10f90..4c27e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea .mypy_cache .pytest_cache -env +env*/ references .coverage scratch/ diff --git a/README.md b/README.md index 9ad0c85..efba1ef 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ from collections import namedtuple, defaultdict as dd when provided to a LazyImporter. -### TryExceptImport ### +### TryExceptImport, TryExceptFromImport and TryFallbackImport ### `TryExceptImport` is used for compatibility where a module may not be available and so a fallback module providing the same functionality should be used. For @@ -187,20 +187,32 @@ example when a newer version of python has a stdlib module that has replaced a third party module that was used previously. ```python -from ducktools.lazyimporter import TryExceptImport +from ducktools.lazyimporter import TryExceptImport, TryExceptFromImport, TryFallbackImport modules = [ TryExceptImport("tomllib", "tomli", "tomllib"), + TryExceptFromImport("tomllib", "loads", "tomli", "loads", "loads"), + TryFallbackImport("tomli", None), ] ``` -is equivalent to +is roughly equivalent to ```python try: import tomllib as tomllib except ImportError: import tomli as tomllib + +try: + from tomllib import loads as loads +except ImportError: + from tomli import loads as loads + +try: + import tomli +except ImportError: + tomli = None ``` when provided to a LazyImporter. diff --git a/src/ducktools/lazyimporter/__init__.py b/src/ducktools/lazyimporter/__init__.py index 224bf4e..e48b470 100644 --- a/src/ducktools/lazyimporter/__init__.py +++ b/src/ducktools/lazyimporter/__init__.py @@ -26,7 +26,7 @@ import abc import sys -__version__ = "v0.4.0" +__version__ = "v0.5.0" __all__ = [ "LazyImporter", "ModuleImport", @@ -34,6 +34,7 @@ "MultiFromImport", "TryExceptImport", "TryExceptFromImport", + "TryFallbackImport", "ImportBase", "get_importer_state", "get_module_funcs", @@ -496,6 +497,65 @@ def do_import(self, globs=None): return {self.asname: attrib} +class TryFallbackImport(ImportBase): + def __init__(self, module_name, fallback, asname=None): + self.module_name = module_name + self.fallback = fallback + + if asname is None: + if self.import_level > 0: + raise ValueError( + f"Relative import {self.module_name!r} requires an assigned name." + ) + elif self.submodule_names: + raise ValueError( + f"Submodule import {self.module_name!r} requires an assigned name." + ) + self.asname = module_name + else: + self.asname = asname + + if not self.asname.isidentifier(): + raise ValueError(f"{self.asname!r} is not a valid Python identifier.") + + def __repr__(self): + return ( + f"{self.__class__.__name__}(" + f"module_name={self.module_name!r}, " + f"fallback={self.fallback!r}, " + f"asname={self.asname!r}" + f")" + ) + + def __eq__(self, other): + if self.__class__ is other.__class__: + return ( + self.module_name, + self.fallback, + self.asname, + ) == ( + other.module_name, + other.fallback, + other.asname, + ) + return NotImplemented + + def do_import(self, globs=None): + try: + mod = __import__( + self.module_name_noprefix, + globals=globs, + level=self.import_level, + ) + except ImportError: + mod = self.fallback + else: + for submod in self.submodule_names: + mod = getattr(mod, submod) + + return {self.asname: mod} + + class _ImporterGrouper: def __init__(self): self._name = None diff --git a/src/ducktools/lazyimporter/__init__.pyi b/src/ducktools/lazyimporter/__init__.pyi index 0d1ab66..0e6a043 100644 --- a/src/ducktools/lazyimporter/__init__.pyi +++ b/src/ducktools/lazyimporter/__init__.pyi @@ -15,6 +15,7 @@ __all__: list[str] = [ "MultiFromImport", "TryExceptImport", "TryExceptFromImport", + "TryFallbackImport", "ImportBase", "get_importer_state", "get_module_funcs", @@ -42,6 +43,7 @@ class ModuleImport(ImportBase): asname: str def __init__(self, module_name: str, asname: str | None = ...) -> None: ... + def __repr__(self) -> str: ... def __eq__(self, other) -> bool: ... def do_import( self, globs: dict[str, Any] | None = ... @@ -55,7 +57,8 @@ class FromImport(ImportBase): def __init__( self, module_name: str, attrib_name: str, asname: str | None = ... ) -> None: ... - def __eq__(self, other): ... + def __repr__(self) -> str: ... + def __eq__(self, other) -> bool: ... def do_import(self, globs: dict[str, Any] | None = ...) -> dict[str, Any]: ... class MultiFromImport(ImportBase): @@ -65,7 +68,8 @@ class MultiFromImport(ImportBase): def __init__( self, module_name: str, attrib_names: list[str | tuple[str, str]] ) -> None: ... - def __eq__(self, other): ... + def __repr__(self) -> str: ... + def __eq__(self, other) -> bool: ... @property def asnames(self): ... def do_import(self, globs: dict[str, Any] | None = ...) -> dict[str, Any]: ... @@ -87,7 +91,8 @@ class TryExceptImport(_TryExceptImportMixin, ImportBase): asname: str def __init__(self, module_name: str, except_module: str, asname: str) -> None: ... - def __eq__(self, other): ... + def __repr__(self) -> str: ... + def __eq__(self, other) -> bool: ... def do_import(self, globs: dict[str, Any] | None = ...): ... class TryExceptFromImport(_TryExceptImportMixin, ImportBase): @@ -104,9 +109,25 @@ class TryExceptFromImport(_TryExceptImportMixin, ImportBase): except_attribute: str, asname: str, ) -> None: ... - def __eq__(self, other): ... + def __repr__(self) -> str: ... + def __eq__(self, other) -> bool: ... def do_import(self, globs: dict[str, Any] | None = ...): ... +class TryFallbackImport(ImportBase): + module_name: str + fallback: Any + asname: str + + def __init__( + self, + module_name: str, + fallback: Any, + asname: str | None = None, + ) -> None: ... + def __repr__(self) -> str: ... + def __eq__(self, other) -> bool: ... + def do_import(self, globs: dict[str, Any] | None = ...): ... + class _ImporterGrouper: def __init__(self) -> None: ... def __set_name__(self, owner, name) -> None: ... diff --git a/tests/test_basic_imports.py b/tests/test_basic_imports.py index 924d4c6..b8c14b3 100644 --- a/tests/test_basic_imports.py +++ b/tests/test_basic_imports.py @@ -9,6 +9,7 @@ MultiFromImport, TryExceptImport, TryExceptFromImport, + TryFallbackImport, ) @@ -59,7 +60,6 @@ def test_from_import(self): assert example_2.item is laz.i - def test_imports_submod_asname(self): laz_sub = LazyImporter([ModuleImport("ex_mod.ex_submod", asname="ex_submod")]) @@ -143,6 +143,35 @@ def test_try_except_from_import(self): assert laz.name == "ex_submod" + def test_try_fallback_import(self): + # noinspection PyUnresolvedReferences + import ex_mod + test_obj = object() + + laz = LazyImporter( + [TryFallbackImport("ex_mod", test_obj)] + ) + + assert laz.ex_mod is ex_mod + + laz = LazyImporter( + [TryFallbackImport("ex_mod", test_obj, "module_name")] + ) + + assert laz.module_name is ex_mod + + laz = LazyImporter( + [TryFallbackImport("module_does_not_exist", test_obj)] + ) + + assert laz.module_does_not_exist is test_obj + + laz = LazyImporter( + [TryFallbackImport("module_does_not_exist", test_obj, "module_name")] + ) + + assert laz.module_name is test_obj + class TestRelativeImports: def test_relative_import(self):