Coverage for encodermap/_optional_imports.py: 73%
55 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-07 11:05 +0000
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-07 11:05 +0000
1# -*- coding: utf-8 -*-
2# encodermap/_optional_imports.py
4# Copyright (c) 2021, Kevin Sawade (kevin.sawade@uni-konstanz.de)
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are met:
9#
10# * Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12# * Redistributions in binary form must reproduce the above copyright
13# notice, this list of conditions and the following disclaimer in the
14# documentation and/or other materials provided with the distribution.
15# * Neither the name of the copyright holders nor the names of any
16# contributors may be used to endorse or promote products derived
17# from this software without specific prior written permission.
18#
19# This file is free software: you can redistribute it and/or modify
20# it under the terms of the GNU Lesser General Public License as
21# published by the Free Software Foundation, either version 2.1
22# of the License, or (at your option) any later version.
23#
24# This file is distributed in the hope that it will be useful,
25# but WITHOUT ANY WARRANTY; without even the implied warranty of
26# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27# GNU Lesser General Public License for more details.
28#
29# Find the GNU Lesser General Public License under <http://www.gnu.org/licenses/>.
30"""Optional imports of python packages.
32Allows you to postpone import exceptions. Basically makes the codebase of EncoderMap
33leaner, so that users don't need to install packages for features they don't require.
35Examples:
36 >>> from encodermap._optional_imports import _optional_import
37 >>> np = _optional_import('numpy')
38 >>> np.array([1, 2, 3])
39 array([1, 2, 3])
40 >>> nonexistent = _optional_import('nonexistent_package')
41 >>> try:
42 ... nonexistent.function()
43 ... except ValueError as e:
44 ... print(e)
45 Install the `nonexistent_package` package to make use of this feature.
46 >>> try:
47 ... _ = nonexistent.variable
48 ... except ValueError as e:
49 ... print(e)
50 Install the `nonexistent_package` package to make use of this feature.
51 >>> numpy_random = _optional_import('numpy', 'random.random')
52 >>> np.random.seed(1)
53 >>> np.round(numpy_random((5, 5)) * 20, 0)
54 array([[ 8., 14., 0., 6., 3.],
55 [ 2., 4., 7., 8., 11.],
56 [ 8., 14., 4., 18., 1.],
57 [13., 8., 11., 3., 4.],
58 [16., 19., 6., 14., 18.]])
60"""
62from __future__ import annotations
64from typing import Any
67def _optional_import(
68 module: str,
69 name: str = None,
70 version: str = None,
71) -> Any:
72 import importlib
74 import pkg_resources
76 available_modules = list(pkg_resources.working_set)
77 _module: str = module
78 try:
79 # try the import
80 module: Any = importlib.import_module(module)
81 if name is None:
82 return module
83 if "." in name:
84 for i in name.split("."):
85 module = getattr(module, i)
86 return module
87 return getattr(module, name)
88 except ImportError as e:
89 # import failed
90 if version is not None: 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true
91 msg = f"Install the `{_module}` package with version `{version}` to make use of this feature."
92 else:
93 msg = f"Install the `{_module}` package to make use of this feature."
94 import_error = e
95 except AttributeError as e:
96 # absolute import failed. Try relative import
97 try:
98 if name is None: 98 ↛ 99line 98 didn't jump to line 99
99 msg = (
100 f"Absolute and relative import of module {_module} "
101 f"failed with Exception {e2}. I printed a list of available "
102 f"imports for you to check."
103 )
105 try:
106 module_name = "." + name.split(".")[-2]
107 except AttributeError as ae:
108 raise ae from e
109 object_name = name.split(".")[-1]
110 path = _module + "." + ".".join(name.split(".")[:-2])
111 path = path.rstrip(".")
112 module = importlib.import_module(module_name, path)
113 return getattr(module, object_name)
114 except Exception as e2:
115 module_name = "." + name.split(".")[-2]
116 object_name = name.split(".")[-1]
117 path = _module + "." + ".".join(name.split(".")[:-2])
118 msg = (
119 f"I was given these attrs: {_module=}, {name=}. After a "
120 f"failed absolute import, I tried to mimic a relative "
121 f"import of the object {object_name=} from the module "
122 f"{module_name=}. The path of the object was determined to "
123 f"{path=}."
124 )
126 import_error = e
128 class _failed_import:
129 def __init__(self, *args, **kwargs):
130 pass
132 def __call__(self, *args, **kwargs):
133 raise ValueError(msg) from import_error
135 def __getattribute__(self, name):
136 # if class is parent class for some other class
137 if name == "__mro_entries__": 137 ↛ 138line 137 didn't jump to line 138, because the condition on line 137 was never true
138 return object.__getattribute__(self, name)
139 raise ValueError(msg) from import_error
141 def __getattr__(self, name):
142 # if class is parent class for some other class
143 if name == "__mro_entries__":
144 return object.__getattribute__(self, name)
145 raise ValueError(msg) from import_error
147 return _failed_import()