Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/util.py: 100%
96 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-08-26 14:27 -0500
« prev ^ index » next coverage.py v7.3.2, created at 2024-08-26 14:27 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-2024 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"""
27import importlib
28import logging
29import os
30import shlex
31from uuid import uuid1
34log = logging.getLogger(__name__)
37# nb. this is used as default kwarg value in some places, to
38# distinguish passing a ``None`` value, vs. *no* value at all
39UNSPECIFIED = object()
42def get_class_hierarchy(klass, topfirst=True):
43 """
44 Returns a list of all classes in the inheritance chain for the
45 given class.
47 For instance::
49 class A:
50 pass
52 class B(A):
53 pass
55 class C(B):
56 pass
58 get_class_hierarchy(C)
59 # -> [A, B, C]
61 :param klass: The reference class. The list of classes returned
62 will include this class and all its parents.
64 :param topfirst: Whether the returned list should be sorted in a
65 "top first" way, e.g. A) grandparent, B) parent, C) child.
66 This is the default but pass ``False`` to get the reverse.
67 """
68 hierarchy = []
70 def traverse(cls):
71 if cls is not object:
72 hierarchy.append(cls)
73 for parent in cls.__bases__:
74 traverse(parent)
76 traverse(klass)
77 if topfirst:
78 hierarchy.reverse()
79 return hierarchy
82def load_entry_points(group, ignore_errors=False):
83 """
84 Load a set of ``setuptools``-style entry points.
86 This is used to locate "plugins" and similar things, e.g. the set
87 of subcommands which belong to a main command.
89 :param group: The group (string name) of entry points to be
90 loaded, e.g. ``'wutta.commands'``.
92 :param ignore_errors: If false (the default), any errors will be
93 raised normally. If true, errors will be logged but not
94 raised.
96 :returns: A dictionary whose keys are the entry point names, and
97 values are the loaded entry points.
98 """
99 entry_points = {}
101 try:
102 # nb. this package was added in python 3.8
103 import importlib.metadata as importlib_metadata
104 except ImportError:
105 import importlib_metadata
107 eps = importlib_metadata.entry_points()
108 if not hasattr(eps, 'select'):
109 # python < 3.10
110 eps = eps.get(group, [])
111 else:
112 # python >= 3.10
113 eps = eps.select(group=group)
114 for entry_point in eps:
115 try:
116 ep = entry_point.load()
117 except:
118 if not ignore_errors:
119 raise
120 log.warning("failed to load entry point: %s", entry_point,
121 exc_info=True)
122 else:
123 entry_points[entry_point.name] = ep
125 return entry_points
128def load_object(spec):
129 """
130 Load an arbitrary object from a module, according to the spec.
132 The spec string should contain a dotted path to an importable module,
133 followed by a colon (``':'``), followed by the name of the object to be
134 loaded. For example:
136 .. code-block:: none
138 wuttjamaican.util:parse_bool
140 You'll notice from this example that "object" in this context refers to any
141 valid Python object, i.e. not necessarily a class instance. The name may
142 refer to a class, function, variable etc. Once the module is imported, the
143 ``getattr()`` function is used to obtain a reference to the named object;
144 therefore anything supported by that approach should work.
146 :param spec: Spec string.
148 :returns: The specified object.
149 """
150 if not spec:
151 raise ValueError("no object spec provided")
153 module_path, name = spec.split(':')
154 module = importlib.import_module(module_path)
155 return getattr(module, name)
158def make_title(text):
159 """
160 Return a human-friendly "title" for the given text.
162 This is mostly useful for converting a Python variable name (or
163 similar) to a human-friendly string, e.g.::
165 make_title('foo_bar') # => 'Foo Bar'
166 """
167 text = text.replace('_', ' ')
168 text = text.replace('-', ' ')
169 words = text.split()
170 return ' '.join([x.capitalize() for x in words])
173def make_uuid():
174 """
175 Generate a universally-unique identifier.
177 :returns: A 32-character hex string.
178 """
179 return uuid1().hex
182def parse_bool(value):
183 """
184 Derive a boolean from the given string value.
185 """
186 if value is None:
187 return None
188 if isinstance(value, bool):
189 return value
190 if str(value).lower() in ('true', 'yes', 'y', 'on', '1'):
191 return True
192 return False
195def parse_list(value):
196 """
197 Parse a configuration value, splitting by whitespace and/or commas
198 and taking quoting into account etc., yielding a list of strings.
199 """
200 if value is None:
201 return []
202 if isinstance(value, list):
203 return value
204 parser = shlex.shlex(value)
205 parser.whitespace += ','
206 parser.whitespace_split = True
207 values = list(parser)
208 for i, value in enumerate(values):
209 if value.startswith('"') and value.endswith('"'):
210 values[i] = value[1:-1]
211 elif value.startswith("'") and value.endswith("'"):
212 values[i] = value[1:-1]
213 return values
216def progress_loop(func, items, factory, message=None):
217 """
218 Convenience function to iterate over a set of items, invoking
219 logic for each, and updating a progress indicator along the way.
221 This function may also be called via the :term:`app handler`; see
222 :meth:`~wuttjamaican.app.AppHandler.progress_loop()`.
224 The ``factory`` will be called to create the progress indicator,
225 which should be an instance of
226 :class:`~wuttjamaican.progress.ProgressBase`.
228 The ``factory`` may also be ``None`` in which case there is no
229 progress, and this is really just a simple "for loop".
231 :param func: Callable to be invoked for each item in the sequence.
232 See below for more details.
234 :param items: Sequence of items over which to iterate.
236 :param factory: Callable which creates/returns a progress
237 indicator, or can be ``None`` for no progress.
239 :param message: Message to display along with the progress
240 indicator. If no message is specified, whether a default is
241 shown will be up to the progress indicator.
243 The ``func`` param should be a callable which accepts 2 positional
244 args ``(obj, i)`` - meaning for which is as follows:
246 :param obj: This will be an item within the sequence.
248 :param i: This will be the *one-based* sequence number for the
249 item.
251 See also :class:`~wuttjamaican.progress.ConsoleProgress` for a
252 usage example.
253 """
254 progress = None
255 if factory:
256 count = len(items)
257 progress = factory(message, count)
259 for i, item in enumerate(items, 1):
261 func(item, i)
263 if progress:
264 progress.update(i)
266 if progress:
267 progress.finish()
270def resource_path(path):
271 """
272 Returns the absolute file path for the given resource path.
274 A "resource path" is one which designates a python package name,
275 plus some path under that. For instance:
277 .. code-block:: none
279 wuttjamaican.email:templates
281 Assuming such a path should exist, the question is "where?"
283 So this function uses :mod:`python:importlib.resources` to locate
284 the path, possibly extracting the file(s) from a zipped package,
285 and returning the final path on disk.
287 It only does this if it detects it is needed, based on the given
288 ``path`` argument. If that is already an absolute path then it
289 will be returned as-is.
291 :param path: Either a package resource specifier as shown above,
292 or regular file path.
294 :returns: Absolute file path to the resource.
295 """
296 if not os.path.isabs(path) and ':' in path:
298 try:
299 # nb. these were added in python 3.9
300 from importlib.resources import files, as_file
301 except ImportError: # python < 3.9
302 from importlib_resources import files, as_file
304 package, filename = path.split(':')
305 ref = files(package) / filename
306 with as_file(ref) as path:
307 return str(path)
309 return path