Project
Toggl API Wrapper
Simple Toggl Track API wrapper with caching functionality.
Recently as I was working on the 1.0.0 release for the Toggl API Wrapper I have built. One change I wanted to make was to change SQLAlchemy into an optional dependency, as it was a fairly heavy, while only being used for cache and an alternate JSON based cached integration with standard libraries existed already. This lead me to search for a way to make all my dependent logic impartial to missing the external library I had made optional.
First of all I had to deal with all import errors, as they were popping up due to the missing dependencies. One way was to put the error raising import in the try/except clauses and ignore the error or replace it with a none value.
try:
import sqlalchemy
except ImportError:
pass
Or:
try:
import sqlalchemy
except ImportError:
sqlalchemy = None
This can be checked in logic afterwards when there is a need to use the imported functionality. A great video on this is by Anthony, where he implements this and also goes through how to test the logic as well.
Additionally wrote a decorator which could easily be attached to functionality that requires the optional dependency and which will re raise the ImportError
if the dependency is not present. This makes it easy to add other optional dependencies in the future without much hassle, as will only need to add a decorator and the module name in order to manage the tool.
import functools
import importlib.util
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def requires(module: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
def requires_dec(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
if importlib.util.find_spec(module) is None:
msg = f"'{module.title()}' is required for this functionality!"
raise ImportError(msg)
return fn(*args, **kwargs)
return wrapper
return requires_dec
Lets have a look at a few examples, so here one from the Plotly express module:
pd = optional_imports.get_module("pandas")
if pd is None:
raise ImportError("Plotly express requires pandas to be installed.")
Where the get_module function is this:
from importlib import import_module
import logging
import sys
from types import ModuleType
logger = logging.getLogger(__name__)
_not_importable = set()
def get_module(name: str, should_load: bool = True) -> ModuleType | None:
if not should_load:
return sys.modules.get(name, None)
if name not in _not_importable:
try:
return import_module(name)
except ImportError:
_not_importable.add(name)
except Exception:
_not_importable.add(name)
msg = f"Error importing optional module {name}"
logger.exception(msg)
return None
Now this kind of handling works pretty well, but it will be a bit of an overkill for packages that only require one package, while on the other hand will allow for more easily checking if heavily optional functionality is available.
Example from HTTPX __init__.py :
try:
from ._main import main
except ImportError: # pragma: no cover
def main() -> None: # type: ignore
import sys
print(
"The httpx command line client could not run because the required "
"dependencies were not installed.\nMake sure you've installed "
"everything with: pip install 'httpx[cli]'"
)
sys.exit(1)
This completely replaces the main method if optional dependencies are not present. A pretty clean setup, but not always the way to go, as more complex libraries can have issues with this. This will only work with functionality that have a single entry point like a CLI, which check all dependencies in one go.