Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a try-fallback import that puts an object in place of a failed import. #16

Merged
merged 6 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading