Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/util.py: 100%

62 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-19 20:49 -0600

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

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24WuttJamaican - utilities 

25""" 

26 

27import importlib 

28import logging 

29import shlex 

30 

31 

32log = logging.getLogger(__name__) 

33 

34 

35# nb. this is used as default kwarg value in some places, to 

36# distinguish passing a ``None`` value, vs. *no* value at all 

37UNSPECIFIED = object() 

38 

39 

40def load_entry_points(group, ignore_errors=False): 

41 """ 

42 Load a set of ``setuptools``-style entry points. 

43 

44 This is used to locate "plugins" and similar things, e.g. the set 

45 of subcommands which belong to a main command. 

46 

47 :param group: The group (string name) of entry points to be 

48 loaded, e.g. ``'wutta.commands'``. 

49 

50 :param ignore_errors: If false (the default), any errors will be 

51 raised normally. If true, errors will be logged but not 

52 raised. 

53 

54 :returns: A dictionary whose keys are the entry point names, and 

55 values are the loaded entry points. 

56 """ 

57 entry_points = {} 

58 

59 try: 

60 # nb. this package was added in python 3.8 

61 import importlib.metadata 

62 

63 except ImportError: 

64 # older setup, must use pkg_resources 

65 # TODO: remove this section once we require python 3.8 

66 from pkg_resources import iter_entry_points 

67 

68 for entry_point in iter_entry_points(group): 

69 try: 

70 ep = entry_point.load() 

71 except: 

72 if not ignore_errors: 

73 raise 

74 log.warning("failed to load entry point: %s", entry_point, 

75 exc_info=True) 

76 else: 

77 entry_points[entry_point.name] = ep 

78 

79 else: 

80 # newer setup (python >= 3.8); can use importlib, but the 

81 # details may vary 

82 eps = importlib.metadata.entry_points() 

83 if not hasattr(eps, 'select'): 

84 # python < 3.10 

85 eps = eps.get(group, []) 

86 else: 

87 # python >= 3.10 

88 eps = eps.select(group=group) 

89 for entry_point in eps: 

90 try: 

91 ep = entry_point.load() 

92 except: 

93 if not ignore_errors: 

94 raise 

95 log.warning("failed to load entry point: %s", entry_point, 

96 exc_info=True) 

97 else: 

98 entry_points[entry_point.name] = ep 

99 

100 return entry_points 

101 

102 

103def load_object(spec): 

104 """ 

105 Load an arbitrary object from a module, according to the spec. 

106 

107 The spec string should contain a dotted path to an importable module, 

108 followed by a colon (``':'``), followed by the name of the object to be 

109 loaded. For example: 

110 

111 .. code-block:: none 

112 

113 wuttjamaican.util:parse_bool 

114 

115 You'll notice from this example that "object" in this context refers to any 

116 valid Python object, i.e. not necessarily a class instance. The name may 

117 refer to a class, function, variable etc. Once the module is imported, the 

118 ``getattr()`` function is used to obtain a reference to the named object; 

119 therefore anything supported by that approach should work. 

120 

121 :param spec: Spec string. 

122 

123 :returns: The specified object. 

124 """ 

125 if not spec: 

126 raise ValueError("no object spec provided") 

127 

128 module_path, name = spec.split(':') 

129 module = importlib.import_module(module_path) 

130 return getattr(module, name) 

131 

132 

133def parse_bool(value): 

134 """ 

135 Derive a boolean from the given string value. 

136 """ 

137 if value is None: 

138 return None 

139 if isinstance(value, bool): 

140 return value 

141 if str(value).lower() in ('true', 'yes', 'y', 'on', '1'): 

142 return True 

143 return False 

144 

145 

146def parse_list(value): 

147 """ 

148 Parse a configuration value, splitting by whitespace and/or commas 

149 and taking quoting into account etc., yielding a list of strings. 

150 """ 

151 if value is None: 

152 return [] 

153 if isinstance(value, list): 

154 return value 

155 parser = shlex.shlex(value) 

156 parser.whitespace += ',' 

157 parser.whitespace_split = True 

158 values = list(parser) 

159 for i, value in enumerate(values): 

160 if value.startswith('"') and value.endswith('"'): 

161 values[i] = value[1:-1] 

162 elif value.startswith("'") and value.endswith("'"): 

163 values[i] = value[1:-1] 

164 return values