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

1# -*- coding: utf-8 -*- 

2# encodermap/_optional_imports.py 

3 

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. 

31 

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. 

34 

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.]]) 

59 

60""" 

61 

62from __future__ import annotations 

63 

64from typing import Any 

65 

66 

67def _optional_import( 

68 module: str, 

69 name: str = None, 

70 version: str = None, 

71) -> Any: 

72 import importlib 

73 

74 import pkg_resources 

75 

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 ) 

104 

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 ) 

125 

126 import_error = e 

127 

128 class _failed_import: 

129 def __init__(self, *args, **kwargs): 

130 pass 

131 

132 def __call__(self, *args, **kwargs): 

133 raise ValueError(msg) from import_error 

134 

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 

140 

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 

146 

147 return _failed_import()