pythonwrench 0.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonwrench/__init__.py +490 -0
- pythonwrench/__main__.py +7 -0
- pythonwrench/_core.py +192 -0
- pythonwrench/abc.py +30 -0
- pythonwrench/argparse/__init__.py +81 -0
- pythonwrench/argparse/dataclass_.py +284 -0
- pythonwrench/argparse/parsers.py +619 -0
- pythonwrench/cast.py +247 -0
- pythonwrench/checksum.py +427 -0
- pythonwrench/collections/__init__.py +104 -0
- pythonwrench/collections/collections.py +900 -0
- pythonwrench/collections/prop.py +104 -0
- pythonwrench/collections/reducers.py +330 -0
- pythonwrench/concurrent.py +73 -0
- pythonwrench/csv.py +12 -0
- pythonwrench/dataclasses.py +117 -0
- pythonwrench/datetime.py +17 -0
- pythonwrench/difflib.py +39 -0
- pythonwrench/disk_cache.py +615 -0
- pythonwrench/entrypoints/info.py +44 -0
- pythonwrench/entrypoints/safe_rmdir.py +98 -0
- pythonwrench/entrypoints/tree.py +113 -0
- pythonwrench/enum.py +55 -0
- pythonwrench/functools.py +234 -0
- pythonwrench/hashlib.py +95 -0
- pythonwrench/importlib.py +243 -0
- pythonwrench/inspect.py +69 -0
- pythonwrench/json.py +12 -0
- pythonwrench/jsonl.py +12 -0
- pythonwrench/logging.py +252 -0
- pythonwrench/math.py +107 -0
- pythonwrench/os.py +226 -0
- pythonwrench/pickle.py +12 -0
- pythonwrench/random.py +60 -0
- pythonwrench/re.py +139 -0
- pythonwrench/semver.py +406 -0
- pythonwrench/serialization/__init__.py +70 -0
- pythonwrench/serialization/_core.py +70 -0
- pythonwrench/serialization/csv.py +493 -0
- pythonwrench/serialization/json.py +178 -0
- pythonwrench/serialization/jsonl.py +215 -0
- pythonwrench/serialization/pickle.py +186 -0
- pythonwrench/time.py +34 -0
- pythonwrench/typing/__init__.py +125 -0
- pythonwrench/typing/checks.py +551 -0
- pythonwrench/typing/classes.py +251 -0
- pythonwrench/warnings.py +118 -0
- pythonwrench-0.6.4.dist-info/METADATA +242 -0
- pythonwrench-0.6.4.dist-info/RECORD +52 -0
- pythonwrench-0.6.4.dist-info/WHEEL +4 -0
- pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
- pythonwrench-0.6.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import importlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from importlib.metadata import Distribution, PackageNotFoundError
|
|
10
|
+
from importlib.util import find_spec
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
from typing import Any, Callable, Dict, Iterable, List, Union
|
|
13
|
+
|
|
14
|
+
from typing_extensions import ParamSpec, TypeVar
|
|
15
|
+
|
|
16
|
+
from pythonwrench.warnings import warn_once
|
|
17
|
+
|
|
18
|
+
P = ParamSpec("P")
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_DEFAULT_SKIPPED = (
|
|
23
|
+
"reimport_all",
|
|
24
|
+
"get_ipython",
|
|
25
|
+
"exit",
|
|
26
|
+
"quit",
|
|
27
|
+
"__name__",
|
|
28
|
+
"__doc__",
|
|
29
|
+
"__package__",
|
|
30
|
+
"__loader__",
|
|
31
|
+
"__spec__",
|
|
32
|
+
"__builtin__",
|
|
33
|
+
"__builtins__",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_available_package(package: str) -> bool:
|
|
40
|
+
"""Returns True if package is installed in the current python environment."""
|
|
41
|
+
if "-" in package:
|
|
42
|
+
msg = f"Found character '-' in package name '{package}'. (it will be replaced by '_')"
|
|
43
|
+
warn_once(msg)
|
|
44
|
+
package = package.replace("-", "_")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
return find_spec(package) is not None
|
|
48
|
+
except AttributeError:
|
|
49
|
+
# Old support for Python <= 3.6
|
|
50
|
+
return False
|
|
51
|
+
except (ImportError, ModuleNotFoundError):
|
|
52
|
+
# Python >= 3.7
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_editable_package(package: str) -> bool:
|
|
57
|
+
"""Returns True if package is installed in editable mode in the current python environment."""
|
|
58
|
+
package = package.replace("_", "-")
|
|
59
|
+
try:
|
|
60
|
+
direct_url = Distribution.from_name(package).read_text("direct_url.json")
|
|
61
|
+
except PackageNotFoundError:
|
|
62
|
+
return False
|
|
63
|
+
if direct_url is None:
|
|
64
|
+
return False
|
|
65
|
+
editable = json.loads(direct_url).get("dir_info", {}).get("editable", False)
|
|
66
|
+
return editable
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def search_submodules(
|
|
70
|
+
root: ModuleType,
|
|
71
|
+
only_editable: bool = True,
|
|
72
|
+
only_loaded: bool = False,
|
|
73
|
+
) -> List[ModuleType]:
|
|
74
|
+
"""Return the submodules already imported."""
|
|
75
|
+
|
|
76
|
+
def _impl(
|
|
77
|
+
root: ModuleType,
|
|
78
|
+
accumulator: Dict[ModuleType, None],
|
|
79
|
+
) -> Dict[ModuleType, None]:
|
|
80
|
+
"""Perform the impl operation."""
|
|
81
|
+
attrs = [getattr(root, attr_name) for attr_name in dir(root)]
|
|
82
|
+
submodules = [
|
|
83
|
+
attr
|
|
84
|
+
for attr in attrs
|
|
85
|
+
if isinstance(attr, ModuleType) and attr not in accumulator
|
|
86
|
+
]
|
|
87
|
+
submodules = {
|
|
88
|
+
submodule
|
|
89
|
+
for submodule in submodules
|
|
90
|
+
if (
|
|
91
|
+
(
|
|
92
|
+
not only_editable
|
|
93
|
+
or is_editable_package(submodule.__name__.split(".")[0])
|
|
94
|
+
)
|
|
95
|
+
and (not only_loaded or submodule.__name__ in sys.modules)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
accumulator.update(dict.fromkeys(submodules))
|
|
99
|
+
|
|
100
|
+
for submodule in submodules:
|
|
101
|
+
accumulator = _impl(submodule, accumulator)
|
|
102
|
+
return accumulator
|
|
103
|
+
|
|
104
|
+
submodules = _impl(root, {root: None})
|
|
105
|
+
submodules = list(submodules)
|
|
106
|
+
submodules = submodules[::-1]
|
|
107
|
+
return submodules
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def reload_submodules(
|
|
111
|
+
*modules: ModuleType,
|
|
112
|
+
verbose: int = 0,
|
|
113
|
+
only_editable: bool = True,
|
|
114
|
+
only_loaded: bool = False,
|
|
115
|
+
) -> List[ModuleType]:
|
|
116
|
+
"""Reload all submodule recursively."""
|
|
117
|
+
candidates: Dict[ModuleType, None] = {}
|
|
118
|
+
for module in modules:
|
|
119
|
+
submodules = search_submodules(
|
|
120
|
+
module,
|
|
121
|
+
only_editable=only_editable,
|
|
122
|
+
only_loaded=only_loaded,
|
|
123
|
+
)
|
|
124
|
+
candidates.update(dict.fromkeys(submodules))
|
|
125
|
+
|
|
126
|
+
for candidate in candidates:
|
|
127
|
+
if verbose > 0:
|
|
128
|
+
logger.info(f"Reload '{candidate}'...")
|
|
129
|
+
try:
|
|
130
|
+
importlib.reload(candidate)
|
|
131
|
+
except ModuleNotFoundError as err:
|
|
132
|
+
msg = f"ModuleNotFound: did this module '{candidate.__name__}' has been renamed after starting execution?"
|
|
133
|
+
logger.warning(msg)
|
|
134
|
+
raise err
|
|
135
|
+
|
|
136
|
+
return list(candidates)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def reload_editable_packages(*, verbose: int = 0) -> List[ModuleType]:
|
|
140
|
+
"""Reload all submodules of editable packages already imported."""
|
|
141
|
+
pkg_names = {name.split(".")[0] for name in sys.modules.keys()}
|
|
142
|
+
editable_packages = [
|
|
143
|
+
sys.modules[name] for name in pkg_names if is_editable_package(name)
|
|
144
|
+
]
|
|
145
|
+
if verbose >= 2:
|
|
146
|
+
msg = f"{len(editable_packages)}/{len(pkg_names)} editable packages found: {editable_packages}"
|
|
147
|
+
logger.debug(msg)
|
|
148
|
+
|
|
149
|
+
return reload_submodules(
|
|
150
|
+
*editable_packages,
|
|
151
|
+
verbose=verbose,
|
|
152
|
+
only_editable=True,
|
|
153
|
+
only_loaded=False,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def requires_packages(
|
|
158
|
+
arg0: Union[Iterable[str], str],
|
|
159
|
+
/,
|
|
160
|
+
*args: str,
|
|
161
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
162
|
+
"""Decorator to wrap a function and raises an error if the function is called.
|
|
163
|
+
|
|
164
|
+
Example
|
|
165
|
+
-------
|
|
166
|
+
>>> @requires_packages("pandas")
|
|
167
|
+
>>> def f(x):
|
|
168
|
+
>>> return x
|
|
169
|
+
>>> f(1) # raises ImportError if pandas is not installed
|
|
170
|
+
"""
|
|
171
|
+
if isinstance(arg0, str):
|
|
172
|
+
packages = [arg0] + list(args)
|
|
173
|
+
elif isinstance(arg0, Iterable):
|
|
174
|
+
packages = list(arg0) + list(args)
|
|
175
|
+
else:
|
|
176
|
+
raise TypeError(f"Invalid arguments types {(arg0,) + args}.")
|
|
177
|
+
|
|
178
|
+
def _wrap(fn: Callable[P, T]) -> Callable[P, T]:
|
|
179
|
+
"""Perform the wrap operation."""
|
|
180
|
+
|
|
181
|
+
@wraps(fn)
|
|
182
|
+
def _impl(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
183
|
+
"""Perform the impl operation."""
|
|
184
|
+
missing = [pkg for pkg in packages if not is_available_package(pkg)]
|
|
185
|
+
if len(missing) == 0:
|
|
186
|
+
return fn(*args, **kwargs)
|
|
187
|
+
else:
|
|
188
|
+
prefix = "\n - "
|
|
189
|
+
missing_str = prefix.join(missing)
|
|
190
|
+
msg = (
|
|
191
|
+
f"Cannot use/import objects because the following optionals dependencies are missing:"
|
|
192
|
+
f"{prefix}{missing_str}\n"
|
|
193
|
+
)
|
|
194
|
+
raise ImportError(msg)
|
|
195
|
+
|
|
196
|
+
return _impl
|
|
197
|
+
|
|
198
|
+
return _wrap
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Placeholder:
|
|
202
|
+
"""Placeholder object. All instances attributes always returns the object itself."""
|
|
203
|
+
|
|
204
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
205
|
+
"""Initialize the instance."""
|
|
206
|
+
super().__init__()
|
|
207
|
+
self.__excluded_self_attrs = [
|
|
208
|
+
"__file__",
|
|
209
|
+
"__eq__",
|
|
210
|
+
"__ne__",
|
|
211
|
+
"__str__",
|
|
212
|
+
"__repr__",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
def __getattr__(self, name: str) -> Any:
|
|
216
|
+
"""Return a dynamically imported attribute."""
|
|
217
|
+
if name in self.__excluded_self_attrs:
|
|
218
|
+
return self.__getattribute__(name)
|
|
219
|
+
else:
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def __call__(self, *args, **kwargs) -> Any:
|
|
223
|
+
"""Call the instance."""
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def __getitem__(self, *args, **kwargs) -> Any:
|
|
227
|
+
"""Return the item at the requested index or key."""
|
|
228
|
+
return self
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ModulePlaceholder(ModuleType, Placeholder):
|
|
232
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
233
|
+
"""Initialize the instance."""
|
|
234
|
+
Placeholder.__init__(self, *args, **kwargs)
|
|
235
|
+
ModuleType.__init__(self, *args, **kwargs)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def import_if_available(name: str) -> ModuleType:
|
|
239
|
+
"""Perform the import if available operation."""
|
|
240
|
+
if is_available_package(name):
|
|
241
|
+
return __import__(name)
|
|
242
|
+
else:
|
|
243
|
+
return ModulePlaceholder(name)
|
pythonwrench/inspect.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, Callable, List, TypeVar, Union, get_args
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_argnames(fn: Callable) -> List[str]:
|
|
11
|
+
"""Get arguments names of a method, function or callable object. This function does not return the 'self' argument for methods."""
|
|
12
|
+
spec = inspect.getfullargspec(fn)
|
|
13
|
+
all_args = spec.args + spec.kwonlyargs
|
|
14
|
+
if inspect.ismethod(fn) or inspect.isclass(fn):
|
|
15
|
+
return all_args[1:]
|
|
16
|
+
else:
|
|
17
|
+
return all_args
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_current_fn_name(*, default: T = "") -> Union[str, T]:
|
|
21
|
+
"""Get caller function name."""
|
|
22
|
+
try:
|
|
23
|
+
return inspect.currentframe().f_back.f_code.co_name # type: ignore
|
|
24
|
+
except AttributeError:
|
|
25
|
+
return default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_fullname(x: Any, *, inst_suffix: str = "(...)") -> str:
|
|
29
|
+
"""Returns the classname of an object with parent modules.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
obj: Object to scan.
|
|
33
|
+
inst_suffix: Suffix appended to the classname in case the object is an instance of a class.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> get_fullname([0, 1, 2])
|
|
38
|
+
... 'builtins.list(...)'
|
|
39
|
+
>>> get_fullname(1.0)
|
|
40
|
+
... 'builtins.float(...)'
|
|
41
|
+
>>> class A: def f(self): return 0
|
|
42
|
+
>>> a = A()
|
|
43
|
+
>>> get_fullname(a)
|
|
44
|
+
... '__main__.A(...)'
|
|
45
|
+
>>> get_fullname(A)
|
|
46
|
+
... '__main__.A'
|
|
47
|
+
>>> get_fullname(a.f)
|
|
48
|
+
... '__main__.A.f'
|
|
49
|
+
>>> get_fullname(A.f)
|
|
50
|
+
... '__main__.A.f'
|
|
51
|
+
"""
|
|
52
|
+
if hasattr(x, "__module__") and hasattr(x, "__qualname__"):
|
|
53
|
+
if x.__module__ is None:
|
|
54
|
+
name = f"{x.__qualname__}"
|
|
55
|
+
else:
|
|
56
|
+
name = f"{x.__module__}.{x.__qualname__}"
|
|
57
|
+
elif inspect.ismodule(x):
|
|
58
|
+
name = x.__name__
|
|
59
|
+
else:
|
|
60
|
+
cls = x.__class__
|
|
61
|
+
name = f"{cls.__module__}.{cls.__qualname__}{inst_suffix}"
|
|
62
|
+
|
|
63
|
+
cls_args = get_args(x)
|
|
64
|
+
if len(cls_args) != 0:
|
|
65
|
+
argsnames = [get_fullname(arg, inst_suffix=inst_suffix) for arg in cls_args]
|
|
66
|
+
argsnames_str = ", ".join(argsnames)
|
|
67
|
+
name = f"{name}[{argsnames_str}]"
|
|
68
|
+
|
|
69
|
+
return name
|
pythonwrench/json.py
ADDED
pythonwrench/jsonl.py
ADDED
pythonwrench/logging.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from logging import FileHandler, Formatter, Logger, StreamHandler
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
from typing import IO, List, Literal, Optional, TypeVar, Union
|
|
12
|
+
|
|
13
|
+
from typing_extensions import TypeAlias
|
|
14
|
+
|
|
15
|
+
from pythonwrench.importlib import reload_submodules
|
|
16
|
+
from pythonwrench.semver import Version
|
|
17
|
+
from pythonwrench.typing import SupportsIterLen
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T", covariant=True)
|
|
20
|
+
|
|
21
|
+
PackageOrLogger: TypeAlias = Union[
|
|
22
|
+
str,
|
|
23
|
+
ModuleType,
|
|
24
|
+
None,
|
|
25
|
+
Logger,
|
|
26
|
+
Literal["__parent_file__"],
|
|
27
|
+
]
|
|
28
|
+
PackageOrLoggerList: TypeAlias = Union[
|
|
29
|
+
PackageOrLogger,
|
|
30
|
+
SupportsIterLen[PackageOrLogger],
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_PARENT_FILE_KEY = "__parent_file__"
|
|
34
|
+
DEFAULT_FMT = "[%(asctime)s][%(name)s][%(levelname)s] - %(message)s"
|
|
35
|
+
VERBOSE_DEBUG = 2
|
|
36
|
+
VERBOSE_INFO = 1
|
|
37
|
+
VERBOSE_WARNING = 0
|
|
38
|
+
VERBOSE_ERROR = -1
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@lru_cache(maxsize=None)
|
|
44
|
+
def log_once(
|
|
45
|
+
msg: str,
|
|
46
|
+
logger: PackageOrLoggerList = _PARENT_FILE_KEY,
|
|
47
|
+
*,
|
|
48
|
+
level: int = logging.INFO,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Log message to loggers at the specified level."""
|
|
51
|
+
loggers = _get_loggers(logger)
|
|
52
|
+
for logger in loggers:
|
|
53
|
+
logger.log(level, msg)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def setup_logging_verbose(
|
|
57
|
+
package_or_logger: PackageOrLoggerList = None,
|
|
58
|
+
verbose: Optional[int] = VERBOSE_INFO,
|
|
59
|
+
*,
|
|
60
|
+
fmt: Union[str, None, Formatter] = DEFAULT_FMT,
|
|
61
|
+
stream: Union[IO[str], Literal["auto"]] = "auto",
|
|
62
|
+
set_fmt: bool = True,
|
|
63
|
+
capture_warnings: bool = True,
|
|
64
|
+
autoreload: bool = True,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Helper function to customize logging messages using verbose_level.
|
|
67
|
+
|
|
68
|
+
Note: Higher verbose values means more debug messages.
|
|
69
|
+
"""
|
|
70
|
+
if verbose is None:
|
|
71
|
+
level = None
|
|
72
|
+
else:
|
|
73
|
+
level = _verbose_to_logging_level(verbose)
|
|
74
|
+
|
|
75
|
+
return setup_logging_level(
|
|
76
|
+
package_or_logger,
|
|
77
|
+
level=level,
|
|
78
|
+
fmt=fmt,
|
|
79
|
+
stream=stream,
|
|
80
|
+
set_fmt=set_fmt,
|
|
81
|
+
capture_warnings=capture_warnings,
|
|
82
|
+
autoreload=autoreload,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def setup_logging_level(
|
|
87
|
+
package_or_logger: PackageOrLoggerList = None,
|
|
88
|
+
level: Optional[int] = logging.INFO,
|
|
89
|
+
*,
|
|
90
|
+
fmt: Union[str, None, Formatter] = DEFAULT_FMT,
|
|
91
|
+
stream: Union[IO[str], Literal["auto"]] = "auto",
|
|
92
|
+
set_fmt: bool = True,
|
|
93
|
+
capture_warnings: bool = True,
|
|
94
|
+
autoreload: bool = True,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Helper function to customize logging messages using logging.level.
|
|
97
|
+
|
|
98
|
+
Note: Lower level values means more debug messages.
|
|
99
|
+
"""
|
|
100
|
+
logging.captureWarnings(capture_warnings)
|
|
101
|
+
|
|
102
|
+
logger_lst = _get_loggers(package_or_logger)
|
|
103
|
+
if isinstance(fmt, str):
|
|
104
|
+
fmt = Formatter(fmt)
|
|
105
|
+
|
|
106
|
+
if stream == "auto":
|
|
107
|
+
if running_on_interpreter():
|
|
108
|
+
stream = sys.stdout
|
|
109
|
+
else:
|
|
110
|
+
stream = sys.stderr
|
|
111
|
+
|
|
112
|
+
for logger in logger_lst:
|
|
113
|
+
if set_fmt:
|
|
114
|
+
found = False
|
|
115
|
+
|
|
116
|
+
for handler in logger.handlers:
|
|
117
|
+
if isinstance(handler, StreamHandler) and handler.stream is stream:
|
|
118
|
+
handler.setFormatter(fmt)
|
|
119
|
+
found = True
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
if not found:
|
|
123
|
+
handler = StreamHandler(stream) # type: ignore
|
|
124
|
+
handler.setFormatter(fmt)
|
|
125
|
+
logger.addHandler(handler)
|
|
126
|
+
|
|
127
|
+
if level is not None:
|
|
128
|
+
logger.setLevel(level)
|
|
129
|
+
|
|
130
|
+
if autoreload:
|
|
131
|
+
for logger in logger_lst:
|
|
132
|
+
if logger.name not in sys.modules:
|
|
133
|
+
continue
|
|
134
|
+
reload_submodules(sys.modules[logger.name])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def running_on_interpreter() -> bool:
|
|
138
|
+
"""Return whether running on interpreter."""
|
|
139
|
+
return get_ipython_name() is None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def running_on_terminal() -> bool:
|
|
143
|
+
"""Return whether running on terminal."""
|
|
144
|
+
return get_ipython_name() == "TerminalInteractiveShell"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def running_on_notebook() -> bool:
|
|
148
|
+
"""Return whether running on notebook."""
|
|
149
|
+
return get_ipython_name() == "ZMQInteractiveShell"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_ipython_name() -> Optional[
|
|
153
|
+
Literal["TerminalInteractiveShell", "ZMQInteractiveShell"]
|
|
154
|
+
]:
|
|
155
|
+
"""Return ipython name."""
|
|
156
|
+
try:
|
|
157
|
+
return get_ipython().__class__.__name__ # type: ignore
|
|
158
|
+
except NameError:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_current_file_logger(
|
|
163
|
+
*,
|
|
164
|
+
parent_deep: int = 1,
|
|
165
|
+
default: T = logging.root,
|
|
166
|
+
) -> Union[Logger, T]:
|
|
167
|
+
"""Returns the logger of the caller file. If this cannot be found, returns the root logger."""
|
|
168
|
+
try:
|
|
169
|
+
frame = inspect.currentframe()
|
|
170
|
+
for _ in range(parent_deep):
|
|
171
|
+
frame = frame.f_back # type: ignore
|
|
172
|
+
parent_name = frame.f_globals["__name__"] # type: ignore
|
|
173
|
+
return logging.getLogger(parent_name)
|
|
174
|
+
except (AttributeError, KeyError):
|
|
175
|
+
return default
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@lru_cache(maxsize=None)
|
|
179
|
+
def get_null_logger() -> Logger:
|
|
180
|
+
"""Return null logger."""
|
|
181
|
+
logger = logging.getLogger("null_logger")
|
|
182
|
+
logger.addHandler(logging.NullHandler())
|
|
183
|
+
logger.setLevel(logging.CRITICAL + 1)
|
|
184
|
+
return logger
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _get_loggers(pkg_name_log_arg: PackageOrLoggerList) -> List[Logger]:
|
|
188
|
+
"""Perform the get loggers operation."""
|
|
189
|
+
if pkg_name_log_arg is None or isinstance(
|
|
190
|
+
pkg_name_log_arg, (str, Logger, ModuleType)
|
|
191
|
+
):
|
|
192
|
+
pkg_name_log_lst = [pkg_name_log_arg]
|
|
193
|
+
else:
|
|
194
|
+
pkg_name_log_lst = list(pkg_name_log_arg)
|
|
195
|
+
|
|
196
|
+
loggers: List[Logger] = []
|
|
197
|
+
for pkg_name_log in pkg_name_log_lst:
|
|
198
|
+
if isinstance(pkg_name_log, ModuleType):
|
|
199
|
+
logger = logging.getLogger(pkg_name_log.__name__)
|
|
200
|
+
|
|
201
|
+
elif pkg_name_log == _PARENT_FILE_KEY:
|
|
202
|
+
logger = get_current_file_logger(parent_deep=2)
|
|
203
|
+
|
|
204
|
+
elif isinstance(pkg_name_log, (type(None), str)):
|
|
205
|
+
logger = logging.getLogger(pkg_name_log)
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
logger = pkg_name_log
|
|
209
|
+
|
|
210
|
+
loggers.append(logger)
|
|
211
|
+
|
|
212
|
+
return loggers
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class MkdirFileHandler(FileHandler):
|
|
216
|
+
"""FileHandler that build intermediate directories to filename.
|
|
217
|
+
|
|
218
|
+
Used for export hydra logs to a file contained in a folder that does not exists yet at the start of the program.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
filename: Union[str, Path],
|
|
224
|
+
mode: str = "a",
|
|
225
|
+
encoding: Optional[str] = None,
|
|
226
|
+
delay: bool = True,
|
|
227
|
+
errors: Optional[str] = None,
|
|
228
|
+
*,
|
|
229
|
+
mkdir_parents: bool = True,
|
|
230
|
+
mkdir_exist_ok: bool = True,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Initialize the instance."""
|
|
233
|
+
filename = Path(filename)
|
|
234
|
+
filename.parent.mkdir(parents=mkdir_parents, exist_ok=mkdir_exist_ok)
|
|
235
|
+
|
|
236
|
+
if Version.python() < Version("3.9.0"):
|
|
237
|
+
super().__init__(filename, mode, encoding, delay)
|
|
238
|
+
else:
|
|
239
|
+
super().__init__(filename, mode, encoding, delay, errors) # type: ignore
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _verbose_to_logging_level(verbose: int) -> int:
|
|
243
|
+
"""Perform the verbose to logging level operation."""
|
|
244
|
+
if verbose <= VERBOSE_ERROR:
|
|
245
|
+
level = logging.ERROR
|
|
246
|
+
elif verbose == VERBOSE_WARNING:
|
|
247
|
+
level = logging.WARNING
|
|
248
|
+
elif verbose == VERBOSE_INFO:
|
|
249
|
+
level = logging.INFO
|
|
250
|
+
else:
|
|
251
|
+
level = logging.DEBUG
|
|
252
|
+
return level
|
pythonwrench/math.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import struct
|
|
6
|
+
from numbers import Real
|
|
7
|
+
from typing import Any, Callable, Iterable, List, Optional, Tuple, TypeVar
|
|
8
|
+
|
|
9
|
+
from pythonwrench.functools import compose, function_alias
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
T_Real = TypeVar("T_Real", bound=Real)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def clip(
|
|
16
|
+
x: T_Real,
|
|
17
|
+
xmin: Optional[T_Real] = None,
|
|
18
|
+
xmax: Optional[T_Real] = None,
|
|
19
|
+
) -> T_Real:
|
|
20
|
+
"""Perform the clip operation."""
|
|
21
|
+
if xmin is not None:
|
|
22
|
+
x = max(x, xmin)
|
|
23
|
+
if xmax is not None:
|
|
24
|
+
x = min(x, xmax)
|
|
25
|
+
return x
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@function_alias(clip)
|
|
29
|
+
def clamp(*args, **kwargs):
|
|
30
|
+
"""Perform the clamp operation."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def argmax(x: Iterable) -> int:
|
|
35
|
+
"""Perform the argmax operation."""
|
|
36
|
+
max_index, _max_value = max(enumerate(x), key=lambda t: t[1])
|
|
37
|
+
return max_index
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def argmin(x: Iterable) -> int:
|
|
41
|
+
"""Perform the argmin operation."""
|
|
42
|
+
min_index, _max_value = min(enumerate(x), key=lambda t: t[1])
|
|
43
|
+
return min_index
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def argsort(
|
|
47
|
+
x: Iterable[T],
|
|
48
|
+
*,
|
|
49
|
+
key: Optional[Callable[[T], Any]] = None,
|
|
50
|
+
reverse: bool = False,
|
|
51
|
+
) -> List[int]:
|
|
52
|
+
"""Perform the argsort operation."""
|
|
53
|
+
|
|
54
|
+
def get_second(t: Tuple[int, T]) -> T:
|
|
55
|
+
"""Return second."""
|
|
56
|
+
return t[1]
|
|
57
|
+
|
|
58
|
+
if key is None:
|
|
59
|
+
key_fn = get_second
|
|
60
|
+
else:
|
|
61
|
+
key_fn = compose(get_second, key)
|
|
62
|
+
|
|
63
|
+
sorted_x = sorted(enumerate(x), key=key_fn, reverse=reverse) # type: ignore
|
|
64
|
+
indices = [idx for idx, _ in sorted_x]
|
|
65
|
+
return indices
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def nextdown(x: float) -> float:
|
|
69
|
+
"""Perform the nextdown operation."""
|
|
70
|
+
return -_nextup(-x)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def nextafter(x: float, y: float) -> float:
|
|
74
|
+
"""Equivalent to `math.nextafter` for python <=3.8."""
|
|
75
|
+
|
|
76
|
+
# BASED on https://stackoverflow.com/questions/10420848/how-do-you-get-the-next-value-in-the-floating-point-sequence/10426033#10426033
|
|
77
|
+
# If either argument is a NaN, return that argument.
|
|
78
|
+
# This matches the implementation in decimal.Decimal
|
|
79
|
+
if math.isnan(x):
|
|
80
|
+
return x
|
|
81
|
+
if math.isnan(y):
|
|
82
|
+
return y
|
|
83
|
+
|
|
84
|
+
if y == x:
|
|
85
|
+
return y
|
|
86
|
+
elif y > x:
|
|
87
|
+
return _nextup(x)
|
|
88
|
+
else:
|
|
89
|
+
return nextdown(x)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _nextup(x: float) -> float:
|
|
93
|
+
# NaNs and positive infinity map to themselves.
|
|
94
|
+
"""Perform the nextup operation."""
|
|
95
|
+
if math.isnan(x) or (math.isinf(x) and x > 0):
|
|
96
|
+
return x
|
|
97
|
+
|
|
98
|
+
# 0.0 and -0.0 both map to the smallest +ve float.
|
|
99
|
+
if x == 0.0:
|
|
100
|
+
x = 0.0
|
|
101
|
+
|
|
102
|
+
n = struct.unpack("<q", struct.pack("<d", x))[0]
|
|
103
|
+
if n >= 0:
|
|
104
|
+
n += 1
|
|
105
|
+
else:
|
|
106
|
+
n -= 1
|
|
107
|
+
return struct.unpack("<d", struct.pack("<q", n))[0]
|