Skip to content

Commit

Permalink
Merge pull request #16 from DavidCEllis/try_else_import
Browse files Browse the repository at this point in the history
Add a try-fallback import that puts an object in place of a failed import.
  • Loading branch information
DavidCEllis authored Jun 12, 2024
2 parents ec77bab + 66527a8 commit 8d0e073
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.idea
.mypy_cache
.pytest_cache
env
env*/
references
.coverage
scratch/
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,28 +179,40 @@ 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
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.
Expand Down
62 changes: 61 additions & 1 deletion src/ducktools/lazyimporter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
import abc
import sys

__version__ = "v0.4.0"
__version__ = "v0.5.0"
__all__ = [
"LazyImporter",
"ModuleImport",
"FromImport",
"MultiFromImport",
"TryExceptImport",
"TryExceptFromImport",
"TryFallbackImport",
"ImportBase",
"get_importer_state",
"get_module_funcs",
Expand Down Expand Up @@ -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
Expand Down
29 changes: 25 additions & 4 deletions src/ducktools/lazyimporter/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ __all__: list[str] = [
"MultiFromImport",
"TryExceptImport",
"TryExceptFromImport",
"TryFallbackImport",
"ImportBase",
"get_importer_state",
"get_module_funcs",
Expand Down Expand Up @@ -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 = ...
Expand All @@ -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):
Expand All @@ -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]: ...
Expand All @@ -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):
Expand All @@ -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: ...
Expand Down
31 changes: 30 additions & 1 deletion tests/test_basic_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
MultiFromImport,
TryExceptImport,
TryExceptFromImport,
TryFallbackImport,
)


Expand Down Expand Up @@ -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")])

Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 8d0e073

Please sign in to comment.