Python 导入和提供可选功能的良好实践是什么?

发布于 2024-07-14 08:42:26 字数 1460 浏览 5 评论 0原文

我正在 github 上写一个软件。 它基本上是一个带有一些额外功能的托盘图标。 我想提供一段工作代码,而实际上不必让用户安装可选功能的本质依赖项,而且我实际上不想导入我不会使用的东西,所以我认为这样的代码将是“好的解决方案”:

---- IN LOADING FUNCTION ----
features = []

for path in sys.path:
       if os.path.exists(os.path.join(path, 'pynotify')):
              features.append('pynotify')
       if os.path.exists(os.path.join(path, 'gnomekeyring.so')):
              features.append('gnome-keyring')

#user dialog to ask for stuff
#notifications available, do you want them enabled?
dlg = ConfigDialog(features)

if not dlg.get_notifications():
    features.remove('pynotify')


service_start(features ...)

---- SOMEWHERE ELSE ------

def service_start(features, other_config):

        if 'pynotify' in features:
               import pynotify
               #use pynotify...

但是存在一些问题。 如果用户格式化他的计算机并安装最新版本的操作系统并重新部署此应用程序,则功能会突然消失而不会发出警告。 解决方案是将其显示在配置窗口上:

if 'pynotify' in features:
    #gtk checkbox
else:
    #gtk label reading "Get pynotify and enjoy notification pop ups!"

但是如果这是一台 Mac,我怎么知道我不会让用户徒劳无功地寻找他们永远无法满足的依赖项?

第二个问题是:

if os.path.exists(os.path.join(path, 'gnomekeyring.so')):

问题。 我能否确定该文件在所有 Linux 发行版中始终名为 gnomekeyring.so?

其他人如何测试这些功能? 基本的问题

try:
    import pynotify
except:
    pynotify = disabled

是代码是全局的,这些代码可能散落各处,即使用户不想要 pynotify....它还是会加载。

那么人们认为解决这个问题的最佳方法是什么?

I'm writing a piece of software over on github. It's basically a tray icon with some extra features. I want to provide a working piece of code without actually having to make the user install what are essentially dependencies for optional features and I don't actually want to import things I'm not going to use so I thought code like this would be "good solution":

---- IN LOADING FUNCTION ----
features = []

for path in sys.path:
       if os.path.exists(os.path.join(path, 'pynotify')):
              features.append('pynotify')
       if os.path.exists(os.path.join(path, 'gnomekeyring.so')):
              features.append('gnome-keyring')

#user dialog to ask for stuff
#notifications available, do you want them enabled?
dlg = ConfigDialog(features)

if not dlg.get_notifications():
    features.remove('pynotify')


service_start(features ...)

---- SOMEWHERE ELSE ------

def service_start(features, other_config):

        if 'pynotify' in features:
               import pynotify
               #use pynotify...

There are some issues however. If a user formats his machine and installs the newest version of his OS and redeploys this application, features suddenly disappear without warning. The solution is to present this on the configuration window:

if 'pynotify' in features:
    #gtk checkbox
else:
    #gtk label reading "Get pynotify and enjoy notification pop ups!"

But if this is say, a mac, how do I know I'm not sending the user on a wild goose chase looking for a dependency they can never fill?

The second problem is the:

if os.path.exists(os.path.join(path, 'gnomekeyring.so')):

issue. Can I be sure that the file is always called gnomekeyring.so across all the linux distros?

How do other people test these features? The problem with the basic

try:
    import pynotify
except:
    pynotify = disabled

is that the code is global, these might be littered around and even if the user doesn't want pynotify....it's loaded anyway.

So what do people think is the best way to solve this problem?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

所谓喜欢 2024-07-21 08:42:26

try: 方法不需要是全局的——它可以在任何范围内使用,因此模块可以在运行时“延迟加载”。 例如:

def foo():
    try:
        import external_module
    except ImportError:
        external_module = None 

    if external_module:
        external_module.some_whizzy_feature()
    else:
        print("You could be using a whizzy feature right now, if you had external_module.")

当您的脚本运行时,不会尝试加载 external_module。 第一次调用 foo() 时,external_module 会(如果可用)加载并插入到函数的本地作用域中。 对 foo() 的后续调用会将 external_module 重新插入其作用域,而无需重新加载模块。

一般来说,最好让 Python 处理导入逻辑——它已经这么做了一段时间了。 :-)

The try: method does not need to be global — it can be used in any scope and so modules can be "lazy-loaded" at runtime. For example:

def foo():
    try:
        import external_module
    except ImportError:
        external_module = None 

    if external_module:
        external_module.some_whizzy_feature()
    else:
        print("You could be using a whizzy feature right now, if you had external_module.")

When your script is run, no attempt will be made to load external_module. The first time foo() is called, external_module is (if available) loaded and inserted into the function's local scope. Subsequent calls to foo() reinsert external_module into its scope without needing to reload the module.

In general, it's best to let Python handle import logic — it's been doing it for a while. :-)

我最亲爱的 2024-07-21 08:42:26

您可能想看看 imp 模块,它基本上做了什么您在上面手动执行。 因此,您可以首先使用 find_module() 查找模块,然后通过 load_module() 加载它或简单地导入它(检查配置后)。

顺便说一句,如果使用 except: 我总是会向其中添加特定的异常(此处为 ImportError),以免意外捕获不相关的错误。

You might want to have a look at the imp module, which basically does what you do manually above. So you can first look for a module with find_module() and then load it via load_module() or by simply importing it (after checking the config).

And btw, if using except: I always would add the specific exception to it (here ImportError) to not accidentally catch unrelated errors.

苯莒 2024-07-21 08:42:26

另一种选择是使用 @contextmanagerwith。 在这种情况下,您事先并不知道需要哪些依赖项:

from contextlib import contextmanager

@contextmanager
def optional_dependencies(error: str = "ignore"):
    assert error in {"raise", "warn", "ignore"}
    try:
        yield None
    except ImportError as e:
        if error == "raise":
            raise e
        if error == "warn":
            msg = f'Missing optional dependency "{e.name}". Use pip or conda to install.'
            print(f'Warning: {msg}')

用法:

with optional_dependencies("warn"):
    import module_which_does_not_exist_1
    import module_which_does_not_exist_2
    z = 1
print(z)

输出:

Warning: Missing optional dependency "module_which_does_not_exist_1". Use pip or conda to install.


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [43], line 5
      3     import module_which_does_not_exist_2
      4     z = 1
----> 5 print(z)

NameError: name 'z' is not defined

在这里,您应该在 with 之后立即定义所有导入。 第一个未安装的模块将抛出 ImportError,该错误由 optical_dependencies 捕获。 根据您想要如何处理此错误,它将忽略它、打印警告或再次引发它。

仅当安装了所有模块时,整个代码才会运行。

Another option is to use @contextmanager and with. In this situation, you do not know beforehand which dependencies are needed:

from contextlib import contextmanager

@contextmanager
def optional_dependencies(error: str = "ignore"):
    assert error in {"raise", "warn", "ignore"}
    try:
        yield None
    except ImportError as e:
        if error == "raise":
            raise e
        if error == "warn":
            msg = f'Missing optional dependency "{e.name}". Use pip or conda to install.'
            print(f'Warning: {msg}')

Usage:

with optional_dependencies("warn"):
    import module_which_does_not_exist_1
    import module_which_does_not_exist_2
    z = 1
print(z)

Output:

Warning: Missing optional dependency "module_which_does_not_exist_1". Use pip or conda to install.


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [43], line 5
      3     import module_which_does_not_exist_2
      4     z = 1
----> 5 print(z)

NameError: name 'z' is not defined

Here, you should define all your imports immediately after with. The first module which is not installed will throw ImportError, which is caught by optional_dependencies. Depending on how you want to handle this error, it will either ignore it, print a warning, or raise it again.

The entire code will only run if all the modules are installed.

﹎☆浅夏丿初晴 2024-07-21 08:42:26

不确定这是否是一个好的做法,但我创建了一个执行可选导入(使用 importlib )和错误处理的函数:

def _optional_import(module: str, name: str = None, package: str = None):
    import importlib
    try:
        module = importlib.import_module(module)
        return module if name is None else getattr(module, name)
    except ImportError as e:
        if package is None:
            package = module
        msg = f"install the '{package}' package to make use of this feature"
        raise ValueError(msg) from e

如果可选模块不可用,用户至少会了解什么去做。 例如,

# code ...

if file.endswith('.json'):
    from json import load
elif file.endswith('.yaml'):
    # equivalent to 'from yaml import safe_load as load'
    load = _optional_import('yaml', 'safe_load', package='pyyaml')

# code using load ...

这种方法的主要缺点是您的导入必须内嵌完成,并且不能全部位于文件的顶部。 因此,使用此函数的轻微改编可能被认为是更好的做法(假设您正在导入函数等):

def _optional_import_(module: str, name: str = None, package: str = None):
    import importlib
    try:
        module = importlib.import_module(module)
        return module if name is None else getattr(module, name)
    except ImportError as e:
        if package is None:
            package = module
        msg = f"install the '{package}' package to make use of this feature"
        import_error = e

        def _failed_import(*args):
            raise ValueError(msg) from import_error

        return _failed_import

现在,您可以使用其余的导入进行导入,并且仅在以下情况下才会引发错误导入失败的函数实际被使用。 例如

from utils import _optional_import_  # let's assume we import the function
from json import load as json_load
yaml_load = _optional_import_('yaml', 'safe_load', package='pyyaml')

# unimportant code ...

with open('test.txt', 'r') as fp:
    result = yaml_load(fp)    # will raise a value error if import was not successful

PS:抱歉回复晚了!

Not sure if this is good practice, but I created a function that does the optional import (using importlib) and error handling:

def _optional_import(module: str, name: str = None, package: str = None):
    import importlib
    try:
        module = importlib.import_module(module)
        return module if name is None else getattr(module, name)
    except ImportError as e:
        if package is None:
            package = module
        msg = f"install the '{package}' package to make use of this feature"
        raise ValueError(msg) from e

If an optional module is not available, the user will at least get the idea what to do. E.g.

# code ...

if file.endswith('.json'):
    from json import load
elif file.endswith('.yaml'):
    # equivalent to 'from yaml import safe_load as load'
    load = _optional_import('yaml', 'safe_load', package='pyyaml')

# code using load ...

The main disadvantage with this approach is that your imports have to be done in-line and are not all on the top of your file. Therefore, it might be considered better practice to use a slight adaptation of this function (assuming that you are importing a function or the like):

def _optional_import_(module: str, name: str = None, package: str = None):
    import importlib
    try:
        module = importlib.import_module(module)
        return module if name is None else getattr(module, name)
    except ImportError as e:
        if package is None:
            package = module
        msg = f"install the '{package}' package to make use of this feature"
        import_error = e

        def _failed_import(*args):
            raise ValueError(msg) from import_error

        return _failed_import

Now, you can make the imports with the rest of your imports and the error will only be raised when the function that failed to import is actually used. E.g.

from utils import _optional_import_  # let's assume we import the function
from json import load as json_load
yaml_load = _optional_import_('yaml', 'safe_load', package='pyyaml')

# unimportant code ...

with open('test.txt', 'r') as fp:
    result = yaml_load(fp)    # will raise a value error if import was not successful

PS: sorry for the late answer!

心意如水 2024-07-21 08:42:26

这是一个生产级解决方案,使用 importlib 和 Pandas 的 import_Optional_dependency as suggest by @dre-hh

from typing import *
import importlib, types

def module_exists(
        *names: Union[List[str], str],
        error: str = "ignore",
        warn_every_time: bool = False,
        __INSTALLED_OPTIONAL_MODULES: Dict[str, bool] = {}
) -> Optional[Union[Tuple[types.ModuleType, ...], types.ModuleType]]:
    """
    Try to import optional dependencies.
    Ref: https://stackoverflow.com/a/73838546/4900327

    Parameters
    ----------
    names: str or list of strings.
        The module name(s) to import.
    error: str {'raise', 'warn', 'ignore'}
        What to do when a dependency is not found.
        * raise : Raise an ImportError.
        * warn: print a warning.
        * ignore: If any module is not installed, return None, otherwise,
          return the module(s).
    warn_every_time: bool
        Whether to warn every time an import is tried. Only applies when error="warn".
        Setting this to True will result in multiple warnings if you try to
        import the same library multiple times.
    Returns
    -------
    maybe_module : Optional[ModuleType, Tuple[ModuleType...]]
        The imported module(s), if all are found.
        None is returned if any module is not found and `error!="raise"`.
    """
    assert error in {"raise", "warn", "ignore"}
    if isinstance(names, (list, tuple, set)):
        names: List[str] = list(names)
    else:
        assert isinstance(names, str)
        names: List[str] = [names]
    modules = []
    for name in names:
        try:
            module = importlib.import_module(name)
            modules.append(module)
            __INSTALLED_OPTIONAL_MODULES[name] = True
        except ImportError:
            modules.append(None)

    def error_msg(missing: Union[str, List[str]]):
        if not isinstance(missing, (list, tuple)):
            missing = [missing]
        missing_str: str = ' '.join([f'"{name}"' for name in missing])
        dep_str = 'dependencies'
        if len(missing) == 1:
            dep_str = 'dependency'
        msg = f'Missing optional {dep_str} {missing_str}. Use pip or conda to install.'
        return msg

    missing_modules: List[str] = [name for name, module in zip(names, modules) if module is None]
    if len(missing_modules) > 0:
        if error == "raise":
            raise ImportError(error_msg(missing_modules))
        if error == "warn":
            for name in missing_modules:
                ## Ensures warning is printed only once
                if warn_every_time is True or name not in __INSTALLED_OPTIONAL_MODULES:
                    print(f'Warning: {error_msg(name)}')
                    __INSTALLED_OPTIONAL_MODULES[name] = False
        return None
    if len(modules) == 1:
        return modules[0]
    return tuple(modules)

用法:忽略错误(error="ignore",默认行为)

假设我们只想在需要时运行某些代码库存在:

if module_exists("pydantic", "sklearn"):
    from pydantic import BaseModel
    from sklearn.metrics import accuracy_score
    class AccuracyCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(accuracy_score(y_true, y_pred), self.num_decimals)
    print("Defined AccuracyCalculator in global context")

如果依赖项pydanticskelarn 不存在,则类AccuracyCalculator 将不会被定义,并且打印语句将不会运行。

用法: raise ImportError (error="raise")

或者,如果任何模块不存在,您可以引发错误:

if module_exists("pydantic", "sklearn", error="raise"):
    from pydantic import BaseModel
    from sklearn.metrics import accuracy_score
    class AccuracyCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(accuracy_score(y_true, y_pred), self.num_decimals)
    print("Defined AccuracyCalculator in global context")

输出:

line 60, in module_exists(error, __INSTALLED_OPTIONAL_MODULES, *names)
     58 if len(missing_modules) > 0:
     59     if error == "raise":
---> 60         raise ImportError(error_msg(missing_modules))
     61     if error == "warn":
     62         for name in missing_modules:

ImportError: Missing optional dependencies "pydantic" "sklearn". Use pip or conda to install.

用法:打印警告 (error="warn")

或者,如果模块不存在,您可以打印警告。

if module_exists("pydantic", "sklearn", error="warn"):
    from pydantic import BaseModel
    from sklearn.metrics import accuracy_score
    class AccuracyCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(accuracy_score(y_true, y_pred), self.num_decimals)
    print("Defined AccuracyCalculator in global context")
    
if module_exists("pydantic", "sklearn", error="warn"):
    from pydantic import BaseModel
    from sklearn.metrics import roc_auc_score
    class RocAucCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(roc_auc_score(y_true, y_pred), self.num_decimals)
    print("Defined RocAucCalculator in global context")

输出:

Warning: Missing optional dependency "pydantic". Use pip or conda to install.
Warning: Missing optional dependency "sklearn". Use pip or conda to install.

在这里,我们确保对于每个丢失的模块只打印一个警告,否则每次尝试导入时都会收到警告。

这对于 Python 库非常有用,您可能会多次尝试导入相同的可选依赖项,但只想看到一个警告。

您可以传递 warn_every_time=True 以在尝试导入时始终打印警告。

Here's a production-grade solution, using importlib and Pandas's import_optional_dependency as suggested by @dre-hh

from typing import *
import importlib, types

def module_exists(
        *names: Union[List[str], str],
        error: str = "ignore",
        warn_every_time: bool = False,
        __INSTALLED_OPTIONAL_MODULES: Dict[str, bool] = {}
) -> Optional[Union[Tuple[types.ModuleType, ...], types.ModuleType]]:
    """
    Try to import optional dependencies.
    Ref: https://stackoverflow.com/a/73838546/4900327

    Parameters
    ----------
    names: str or list of strings.
        The module name(s) to import.
    error: str {'raise', 'warn', 'ignore'}
        What to do when a dependency is not found.
        * raise : Raise an ImportError.
        * warn: print a warning.
        * ignore: If any module is not installed, return None, otherwise,
          return the module(s).
    warn_every_time: bool
        Whether to warn every time an import is tried. Only applies when error="warn".
        Setting this to True will result in multiple warnings if you try to
        import the same library multiple times.
    Returns
    -------
    maybe_module : Optional[ModuleType, Tuple[ModuleType...]]
        The imported module(s), if all are found.
        None is returned if any module is not found and `error!="raise"`.
    """
    assert error in {"raise", "warn", "ignore"}
    if isinstance(names, (list, tuple, set)):
        names: List[str] = list(names)
    else:
        assert isinstance(names, str)
        names: List[str] = [names]
    modules = []
    for name in names:
        try:
            module = importlib.import_module(name)
            modules.append(module)
            __INSTALLED_OPTIONAL_MODULES[name] = True
        except ImportError:
            modules.append(None)

    def error_msg(missing: Union[str, List[str]]):
        if not isinstance(missing, (list, tuple)):
            missing = [missing]
        missing_str: str = ' '.join([f'"{name}"' for name in missing])
        dep_str = 'dependencies'
        if len(missing) == 1:
            dep_str = 'dependency'
        msg = f'Missing optional {dep_str} {missing_str}. Use pip or conda to install.'
        return msg

    missing_modules: List[str] = [name for name, module in zip(names, modules) if module is None]
    if len(missing_modules) > 0:
        if error == "raise":
            raise ImportError(error_msg(missing_modules))
        if error == "warn":
            for name in missing_modules:
                ## Ensures warning is printed only once
                if warn_every_time is True or name not in __INSTALLED_OPTIONAL_MODULES:
                    print(f'Warning: {error_msg(name)}')
                    __INSTALLED_OPTIONAL_MODULES[name] = False
        return None
    if len(modules) == 1:
        return modules[0]
    return tuple(modules)

Usage: ignore errors (error="ignore", default behavior)

Suppose we want to run certain code only if the required libraries exists:

if module_exists("pydantic", "sklearn"):
    from pydantic import BaseModel
    from sklearn.metrics import accuracy_score
    class AccuracyCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(accuracy_score(y_true, y_pred), self.num_decimals)
    print("Defined AccuracyCalculator in global context")

If either dependencies pydantic or skelarn do not exist, then the class AccuracyCalculator will not be defined and the print statement will not run.

Usage: raise ImportError (error="raise")

Alternatively, you can raise a error if any module does not exist:

if module_exists("pydantic", "sklearn", error="raise"):
    from pydantic import BaseModel
    from sklearn.metrics import accuracy_score
    class AccuracyCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(accuracy_score(y_true, y_pred), self.num_decimals)
    print("Defined AccuracyCalculator in global context")

Output:

line 60, in module_exists(error, __INSTALLED_OPTIONAL_MODULES, *names)
     58 if len(missing_modules) > 0:
     59     if error == "raise":
---> 60         raise ImportError(error_msg(missing_modules))
     61     if error == "warn":
     62         for name in missing_modules:

ImportError: Missing optional dependencies "pydantic" "sklearn". Use pip or conda to install.

Usage: print a warning (error="warn")

Alternatively, you can print a warning if the module does not exist.

if module_exists("pydantic", "sklearn", error="warn"):
    from pydantic import BaseModel
    from sklearn.metrics import accuracy_score
    class AccuracyCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(accuracy_score(y_true, y_pred), self.num_decimals)
    print("Defined AccuracyCalculator in global context")
    
if module_exists("pydantic", "sklearn", error="warn"):
    from pydantic import BaseModel
    from sklearn.metrics import roc_auc_score
    class RocAucCalculator(BaseModel):
        num_decimals: int = 5
        def calculate(self, y_pred: List, y_true: List) -> float:
            return round(roc_auc_score(y_true, y_pred), self.num_decimals)
    print("Defined RocAucCalculator in global context")

Output:

Warning: Missing optional dependency "pydantic". Use pip or conda to install.
Warning: Missing optional dependency "sklearn". Use pip or conda to install.

Here, we ensure that only one warning is printed for each missing module, otherwise you would get a warning each time you try to import.

This is very useful for Python libraries where you might try to import the same optional dependencies many times, and only want to see one Warning.

You can pass warn_every_time=True to always print the warning when you try to import.

暮凉 2024-07-21 08:42:26

我真的很高兴能分享我想出的这项新技术来处理可选依赖项!

这个概念是在使用卸载的软件包而不导入时产生错误。

只需在导入之前添加一个调用即可。 您根本不需要更改任何代码。 导入时不再使用 try:。 编写测试时不再使用条件 skip 装饰器。

主要组件

  • 一个导入器,用于为缺少导入而返回一个假模块
  • 一个假模块,在使用时引发异常 一个
  • 中引发,将自动跳过测试

自定义异常,如果在一个最小代码示例

import sys
import importlib
from unittest.case import SkipTest
from _pytest.outcomes import Skipped

class MissingOptionalDependency(SkipTest, Skipped):
    def __init__(self, msg=None):
        self.msg = msg
    def __repr__(self):
        return f"MissingOptionalDependency: {self.msg}" if self.msg else f"MissingOptionalDependency"

class GeneralImporter:
    def __init__(self, *names):
        self.names = names
        sys.meta_path.insert(0, self)
    def find_spec(self, fullname, path=None, target=None):
        if fullname in self.names:
            return importlib.util.spec_from_loader(fullname, self)
    def create_module(self, spec):
        return FakeModule(name=spec.name)
    def exec_module(self, module):
        pass

class FakeModule:
    def __init__(self, name):
        self.name = name
    def __call__(self, *args, **kwargs):
        raise MissingOptionalDependency(f"Optional dependency '{self.name}' was used but it isn't installed.")

GeneralImporter("notinstalled")
import notinstalled  # No error
print(notinstalled)  # <__main__.FakeModule object at 0x0000014B7F6D9E80>
notinstalled()  # MissingOptionalDependency: Optional dependency 'notinstalled' was used but it isn't installed.

上述技术有一些缺点,我的包已修复。

它是开源的、轻量级的,并且没有依赖项!

与上面示例的一些关键区别:

  • 涵盖 100 多个 dunder 方法(全部经过测试)
  • 涵盖 15 个常见 dunder 属性查找
  • 入口函数是 generalimport,它返回一个 ImportCatcher
    • ImportCatcher 保存名称、范围和捕获的名称
      • 可以启用和禁用它
      • 该范围可防止外部包受到影响
  • 通配符支持允许导入任何包将
  • 导入器放在sys.meta_path
    • 让它捕获命名空间导入(通常发生在未安装的软件包中)

GitHub 上的 Generalimport

PyPI pyversions Generic徽章

pip install genericimport

最小示例

from generalimport import generalimport
generalimport("notinstalled")

from notinstalled import missing_func  # No error
missing_func()  # Error occurs here

GitHub 上的自述文件 更深入

I'm really excited to share this new technique I came up with to handle optional dependencies!

The concept is to produce the error when the uninstalled package is used not imported.

Just add a single call before your imports. You don't need to change any code at all. No more using try: when importing. No more using conditional skip decorators when writing tests.

Main components

  • An importer to return a fake module for missing imports
  • A fake module that raises an exception when it's used
  • A custom Exception that will skip tests automatically if raised within one

Minimal Code Example

import sys
import importlib
from unittest.case import SkipTest
from _pytest.outcomes import Skipped

class MissingOptionalDependency(SkipTest, Skipped):
    def __init__(self, msg=None):
        self.msg = msg
    def __repr__(self):
        return f"MissingOptionalDependency: {self.msg}" if self.msg else f"MissingOptionalDependency"

class GeneralImporter:
    def __init__(self, *names):
        self.names = names
        sys.meta_path.insert(0, self)
    def find_spec(self, fullname, path=None, target=None):
        if fullname in self.names:
            return importlib.util.spec_from_loader(fullname, self)
    def create_module(self, spec):
        return FakeModule(name=spec.name)
    def exec_module(self, module):
        pass

class FakeModule:
    def __init__(self, name):
        self.name = name
    def __call__(self, *args, **kwargs):
        raise MissingOptionalDependency(f"Optional dependency '{self.name}' was used but it isn't installed.")

GeneralImporter("notinstalled")
import notinstalled  # No error
print(notinstalled)  # <__main__.FakeModule object at 0x0000014B7F6D9E80>
notinstalled()  # MissingOptionalDependency: Optional dependency 'notinstalled' was used but it isn't installed.

Package

The technique above has some shortcomings that my package fixes.

It's open-source, lightweight, and has no dependencies!

Some key differences to the example above:

  • Covers more than 100 dunder methods (All tested)
  • Covers 15 common dunder attribute lookups
  • Entry function is generalimport which returns an ImportCatcher
    • ImportCatcher holds names, scope, and caught names
      • It can be enabled and disabled
      • The scope prevents external packages from being affected
  • Wildcard support to allow any package to be imported
  • Puts the importer first in sys.meta_path
    • Lets it catch namespace imports (Usually occurs with uninstalled packages)

Generalimport on GitHub

PyPI pyversionsGeneric badge

pip install generalimport

Minimal example

from generalimport import generalimport
generalimport("notinstalled")

from notinstalled import missing_func  # No error
missing_func()  # Error occurs here

The readme on GitHub goes more in-depth

一花一树开 2024-07-21 08:42:26

处理不同功能的不同依赖关系问题的一种方法是将可选功能实现为插件。 这样,用户可以控制应用程序中激活哪些功能,但不负责自己跟踪依赖项。 然后在安装每个插件时处理该任务。

One way to handle the problem of different dependencies for different features is to implement the optional features as plugins. That way the user has control over which features are activated in the app but isn't responsible for tracking down the dependencies herself. That task then gets handled at the time of each plugin's installation.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文