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.
Files changed (52) hide show
  1. pythonwrench/__init__.py +490 -0
  2. pythonwrench/__main__.py +7 -0
  3. pythonwrench/_core.py +192 -0
  4. pythonwrench/abc.py +30 -0
  5. pythonwrench/argparse/__init__.py +81 -0
  6. pythonwrench/argparse/dataclass_.py +284 -0
  7. pythonwrench/argparse/parsers.py +619 -0
  8. pythonwrench/cast.py +247 -0
  9. pythonwrench/checksum.py +427 -0
  10. pythonwrench/collections/__init__.py +104 -0
  11. pythonwrench/collections/collections.py +900 -0
  12. pythonwrench/collections/prop.py +104 -0
  13. pythonwrench/collections/reducers.py +330 -0
  14. pythonwrench/concurrent.py +73 -0
  15. pythonwrench/csv.py +12 -0
  16. pythonwrench/dataclasses.py +117 -0
  17. pythonwrench/datetime.py +17 -0
  18. pythonwrench/difflib.py +39 -0
  19. pythonwrench/disk_cache.py +615 -0
  20. pythonwrench/entrypoints/info.py +44 -0
  21. pythonwrench/entrypoints/safe_rmdir.py +98 -0
  22. pythonwrench/entrypoints/tree.py +113 -0
  23. pythonwrench/enum.py +55 -0
  24. pythonwrench/functools.py +234 -0
  25. pythonwrench/hashlib.py +95 -0
  26. pythonwrench/importlib.py +243 -0
  27. pythonwrench/inspect.py +69 -0
  28. pythonwrench/json.py +12 -0
  29. pythonwrench/jsonl.py +12 -0
  30. pythonwrench/logging.py +252 -0
  31. pythonwrench/math.py +107 -0
  32. pythonwrench/os.py +226 -0
  33. pythonwrench/pickle.py +12 -0
  34. pythonwrench/random.py +60 -0
  35. pythonwrench/re.py +139 -0
  36. pythonwrench/semver.py +406 -0
  37. pythonwrench/serialization/__init__.py +70 -0
  38. pythonwrench/serialization/_core.py +70 -0
  39. pythonwrench/serialization/csv.py +493 -0
  40. pythonwrench/serialization/json.py +178 -0
  41. pythonwrench/serialization/jsonl.py +215 -0
  42. pythonwrench/serialization/pickle.py +186 -0
  43. pythonwrench/time.py +34 -0
  44. pythonwrench/typing/__init__.py +125 -0
  45. pythonwrench/typing/checks.py +551 -0
  46. pythonwrench/typing/classes.py +251 -0
  47. pythonwrench/warnings.py +118 -0
  48. pythonwrench-0.6.4.dist-info/METADATA +242 -0
  49. pythonwrench-0.6.4.dist-info/RECORD +52 -0
  50. pythonwrench-0.6.4.dist-info/WHEEL +4 -0
  51. pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
  52. 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)
@@ -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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # for backward compatibility
5
+ from pythonwrench.serialization.json import ( # noqa: F401
6
+ dump_json,
7
+ dumps_json,
8
+ load_json,
9
+ loads_json,
10
+ read_json,
11
+ save_json,
12
+ )
pythonwrench/jsonl.py ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # for backward compatibility
5
+ from pythonwrench.serialization.jsonl import ( # noqa: F401
6
+ dump_jsonl,
7
+ dumps_jsonl,
8
+ load_jsonl,
9
+ loads_jsonl,
10
+ read_jsonl,
11
+ save_jsonl,
12
+ )
@@ -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]