narwhals icon indicating copy to clipboard operation
narwhals copied to clipboard

Make `import_optional_dependency` util

Open MarcoGorelli opened this issue 11 months ago • 5 comments

We should make such a util, which can take an optional argument specifying the minimum version. Then we can raise unified and consistent error messages

MarcoGorelli avatar Feb 04 '25 23:02 MarcoGorelli

Sounds interesting @MarcoGorelli - could you explain a bit more on this?

mikeweltevrede avatar Mar 29 '25 16:03 mikeweltevrede

hey @mikeweltevrede ! good to hear from you again!

we have some places where we import optional dependencies inline, e.g.

https://github.com/narwhals-dev/narwhals/blob/5f1f19f542e13dd3efc217f56291b16db88b88d6/narwhals/dataframe.py#L503-L507

We should probably have a helper function which does this, and a custom narwhals error for missing optional dependencies

pandas has something similar (import_optional_dependency in their source code) which we could probably use (with attribution, of course)

MarcoGorelli avatar Mar 29 '25 20:03 MarcoGorelli

pandas has something similar (import_optional_dependency in their source code) which we could probably use (with attribution, of course)

Here's the polars one - which is pretty clean and well-documented

https://github.com/pola-rs/polars/blob/0ca485a88a217eab2d55efbd981dfd68709e63be/py-polars/polars/dependencies.py#L225-L287

dangotbanned avatar Mar 29 '25 21:03 dangotbanned

AFAIK, if we want the imported modules to be understood by a type checker - we'll need 1 function per-import.

Example

_module_not_found

from __future__ import annotations

from importlib import import_module
from importlib.util import find_spec

from types import ModuleType
from typing import Any
from typing import Literal

def _module_not_found(module_name: str, **kwds: Any) -> ModuleNotFoundError:
    some_logic_transforming_kwds = repr(kwds)
    msg = f"{module_name!r} not found.\n{some_logic_transforming_kwds!r}"
    return ModuleNotFoundError(msg)

importlib-only

By default, we know nothing about the the contents of the module.

def import_optional_1(module_name: Literal["polars", "pandas"], **kwds: Any):
    if find_spec(module_name):
        return import_module(module_name)
    raise _module_not_found(module_name=module_name, **kwds)

Image

Single function

Better than the first option. But we can't capture @overload(s)

def import_optional_2(module_name: Literal["polars", "pandas"], **kwds: Any):
    if module_name == "polars" and find_spec("polars"):
        import polars as pl

        return pl
    elif module_name == "pandas" and find_spec("pandas"):
        import pandas as pd

        return pd

    raise _module_not_found(module_name=module_name, **kwds)

Image

Multiple functions

So far, this is the only route I've found that would preserve typing

def import_polars(**kwds: Any):
    if find_spec("polars"):
        import polars as pl

        return pl

    raise _module_not_found(module_name="polars", **kwds)


def import_pandas(**kwds: Any):
    if find_spec("pandas"):
        import pandas as pd

        return pd

    raise _module_not_found(module_name="pandas", **kwds)

Image

[!IMPORTANT] The warning above is something we don't want to lose

dangotbanned avatar Apr 05 '25 09:04 dangotbanned

Other examples (in case we need more :) )

  • https://pypi.org/project/lazy-imports/
  • optuna try_import

EdAbati avatar Apr 05 '25 10:04 EdAbati