errortools 3.3.0__tar.gz → 3.4.0__tar.gz

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 (82) hide show
  1. {errortools-3.3.0/errortools.egg-info → errortools-3.4.0}/PKG-INFO +1 -1
  2. {errortools-3.3.0 → errortools-3.4.0}/_errortools/classes/abc.py +1 -17
  3. {errortools-3.3.0 → errortools-3.4.0}/_errortools/logging/logger.py +1 -1
  4. errortools-3.4.0/_errortools/plugins.py +134 -0
  5. {errortools-3.3.0 → errortools-3.4.0}/_errortools/version.py +1 -1
  6. {errortools-3.3.0 → errortools-3.4.0}/errortools/__init__.py +16 -8
  7. {errortools-3.3.0 → errortools-3.4.0/errortools.egg-info}/PKG-INFO +1 -1
  8. {errortools-3.3.0 → errortools-3.4.0}/errortools.egg-info/SOURCES.txt +0 -8
  9. {errortools-3.3.0 → errortools-3.4.0}/pyproject.toml +1 -1
  10. {errortools-3.3.0 → errortools-3.4.0}/testing/__init__.py +1 -1
  11. {errortools-3.3.0 → errortools-3.4.0}/testing/test_plugins.py +200 -105
  12. errortools-3.3.0/_errortools/plugins.py +0 -83
  13. errortools-3.3.0/docs/conf.py +0 -60
  14. errortools-3.3.0/testing/test_abc.py +0 -297
  15. errortools-3.3.0/testing/test_errorcodes.py +0 -395
  16. errortools-3.3.0/testing/test_future.py +0 -296
  17. errortools-3.3.0/testing/test_logging.py +0 -673
  18. errortools-3.3.0/testing/test_partials.py +0 -228
  19. errortools-3.3.0/testing/test_protocols.py +0 -260
  20. errortools-3.3.0/testing/test_warnings.py +0 -151
  21. {errortools-3.3.0 → errortools-3.4.0}/AUTHORS.txt +0 -0
  22. {errortools-3.3.0 → errortools-3.4.0}/LICENSE.txt +0 -0
  23. {errortools-3.3.0 → errortools-3.4.0}/README.md +0 -0
  24. {errortools-3.3.0 → errortools-3.4.0}/_errortools/__init__.py +0 -0
  25. {errortools-3.3.0 → errortools-3.4.0}/_errortools/__main__.py +0 -0
  26. {errortools-3.3.0 → errortools-3.4.0}/_errortools/_cli.py +0 -0
  27. {errortools-3.3.0 → errortools-3.4.0}/_errortools/_speedup.c +0 -0
  28. {errortools-3.3.0 → errortools-3.4.0}/_errortools/classes/__init__.py +0 -0
  29. {errortools-3.3.0 → errortools-3.4.0}/_errortools/classes/errorcodes.py +0 -0
  30. {errortools-3.3.0 → errortools-3.4.0}/_errortools/classes/group.py +0 -0
  31. {errortools-3.3.0 → errortools-3.4.0}/_errortools/classes/warn.py +0 -0
  32. {errortools-3.3.0 → errortools-3.4.0}/_errortools/cli.py +0 -0
  33. {errortools-3.3.0 → errortools-3.4.0}/_errortools/decorator/__init__.py +0 -0
  34. {errortools-3.3.0 → errortools-3.4.0}/_errortools/decorator/cache.py +0 -0
  35. {errortools-3.3.0 → errortools-3.4.0}/_errortools/decorator/deprecated.py +0 -0
  36. {errortools-3.3.0 → errortools-3.4.0}/_errortools/decorator/handlers.py +0 -0
  37. {errortools-3.3.0 → errortools-3.4.0}/_errortools/decorator/retry.py +0 -0
  38. {errortools-3.3.0 → errortools-3.4.0}/_errortools/decorator/timeout.py +0 -0
  39. {errortools-3.3.0 → errortools-3.4.0}/_errortools/descriptor/__init__.py +0 -0
  40. {errortools-3.3.0 → errortools-3.4.0}/_errortools/descriptor/base.py +0 -0
  41. {errortools-3.3.0 → errortools-3.4.0}/_errortools/descriptor/errormsg.py +0 -0
  42. {errortools-3.3.0 → errortools-3.4.0}/_errortools/descriptor/nonblankmsg.py +0 -0
  43. {errortools-3.3.0 → errortools-3.4.0}/_errortools/errno.py +0 -0
  44. {errortools-3.3.0 → errortools-3.4.0}/_errortools/future.py +0 -0
  45. {errortools-3.3.0 → errortools-3.4.0}/_errortools/ignore.py +0 -0
  46. {errortools-3.3.0 → errortools-3.4.0}/_errortools/logging/__init__.py +0 -0
  47. {errortools-3.3.0 → errortools-3.4.0}/_errortools/logging/base.py +0 -0
  48. {errortools-3.3.0 → errortools-3.4.0}/_errortools/logging/level.py +0 -0
  49. {errortools-3.3.0 → errortools-3.4.0}/_errortools/logging/record.py +0 -0
  50. {errortools-3.3.0 → errortools-3.4.0}/_errortools/logging/sink.py +0 -0
  51. {errortools-3.3.0 → errortools-3.4.0}/_errortools/metadata.py +0 -0
  52. {errortools-3.3.0 → errortools-3.4.0}/_errortools/partial.py +0 -0
  53. {errortools-3.3.0 → errortools-3.4.0}/_errortools/py.typed +0 -0
  54. {errortools-3.3.0 → errortools-3.4.0}/_errortools/raises.py +0 -0
  55. {errortools-3.3.0 → errortools-3.4.0}/_errortools/typing.py +0 -0
  56. {errortools-3.3.0 → errortools-3.4.0}/_errortools/wrappers/__init__.py +0 -0
  57. {errortools-3.3.0 → errortools-3.4.0}/_errortools/wrappers/cache.py +0 -0
  58. {errortools-3.3.0 → errortools-3.4.0}/_errortools/wrappers/ignore.py +0 -0
  59. {errortools-3.3.0 → errortools-3.4.0}/errortools/__main__.py +0 -0
  60. {errortools-3.3.0 → errortools-3.4.0}/errortools/future.py +0 -0
  61. {errortools-3.3.0 → errortools-3.4.0}/errortools/logging.py +0 -0
  62. {errortools-3.3.0 → errortools-3.4.0}/errortools/partial.py +0 -0
  63. {errortools-3.3.0 → errortools-3.4.0}/errortools.egg-info/dependency_links.txt +0 -0
  64. {errortools-3.3.0 → errortools-3.4.0}/errortools.egg-info/entry_points.txt +0 -0
  65. {errortools-3.3.0 → errortools-3.4.0}/errortools.egg-info/requires.txt +0 -0
  66. {errortools-3.3.0 → errortools-3.4.0}/errortools.egg-info/top_level.txt +0 -0
  67. {errortools-3.3.0 → errortools-3.4.0}/setup.cfg +0 -0
  68. {errortools-3.3.0 → errortools-3.4.0}/testing/__main__.py +0 -0
  69. {errortools-3.3.0 → errortools-3.4.0}/testing/benchmark/__init__.py +0 -0
  70. {errortools-3.3.0 → errortools-3.4.0}/testing/benchmark/test_future_perf.py +0 -0
  71. {errortools-3.3.0 → errortools-3.4.0}/testing/conftest.py +0 -0
  72. {errortools-3.3.0 → errortools-3.4.0}/testing/run_tests.py +0 -0
  73. {errortools-3.3.0 → errortools-3.4.0}/testing/test_decorator.py +0 -0
  74. {errortools-3.3.0 → errortools-3.4.0}/testing/test_descriptor.py +0 -0
  75. {errortools-3.3.0 → errortools-3.4.0}/testing/test_errno.py +0 -0
  76. {errortools-3.3.0 → errortools-3.4.0}/testing/test_groups.py +0 -0
  77. {errortools-3.3.0 → errortools-3.4.0}/testing/test_ignore.py +0 -0
  78. {errortools-3.3.0 → errortools-3.4.0}/testing/test_raises.py +0 -0
  79. {errortools-3.3.0 → errortools-3.4.0}/testing/test_testing/__init__.py +0 -0
  80. {errortools-3.3.0 → errortools-3.4.0}/testing/test_testing/test_testing.py +0 -0
  81. {errortools-3.3.0 → errortools-3.4.0}/testing/test_typing.py +0 -0
  82. {errortools-3.3.0 → errortools-3.4.0}/testing/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errortools
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
5
5
  Author-email: Evan Yang <quantbit@126.com>
6
6
  License: Copyright (c) 2026 authors see AUTHORS.txt
@@ -1,5 +1,6 @@
1
1
  from typing import Any, Literal, Union
2
2
  from abc import ABC, abstractmethod
3
+ from _collections_abc import _check_methods # type: ignore[attr-defined]
3
4
  import copy
4
5
  import shutil
5
6
  import csv
@@ -12,23 +13,6 @@ else:
12
13
  from typing_extensions import disjoint_base
13
14
 
14
15
 
15
- def _check_methods(C: type[Any], *methods: str) -> Union[bool, Literal[NotImplemented]]: # type: ignore
16
- """Check methods in `C`. If has, return `True`, else `NotImplemented`."""
17
- # from `_collections_abc.py`.
18
- # Copyright 2007 Google, Inc. All Rights Reserved.
19
- # Licensed to PSF under a Contributor Agreement.
20
- mro: tuple[type[Any], ...] = C.__mro__ # Added type hints for mro var
21
- for method in methods:
22
- for B in mro:
23
- if method in B.__dict__:
24
- if B.__dict__[method] is None:
25
- return NotImplemented
26
- break
27
- else:
28
- return NotImplemented
29
- return True
30
-
31
-
32
16
  # ----------------------------------------------------------------------
33
17
  # ErrorCodeable
34
18
  # ----------------------------------------------------------------------
@@ -10,4 +10,4 @@ from .level import Level
10
10
  # Create the default global logger.
11
11
  # It ships with a single stderr sink at DEBUG level (mirrors loguru's default).
12
12
  logger: BaseLogger = BaseLogger(name="errortools")
13
- logger.add(sys.stderr, level=Level.DEBUG, colorize=None)
13
+ logger.add(sys.stderr, level=Level.TRACE, colorize=None)
@@ -0,0 +1,134 @@
1
+ """Ultra-lightweight plugin system for errortools."""
2
+
3
+ from typing import Callable, Any
4
+
5
+ _REGISTRY: dict[str, Callable[..., Any]] = {}
6
+ _UNSET = object()
7
+
8
+ __all__ = [
9
+ "register",
10
+ "get",
11
+ "has",
12
+ "list_all",
13
+ "run",
14
+ "remove",
15
+ "clear",
16
+ "Registry",
17
+ ]
18
+
19
+
20
+ def register(name: str) -> Callable:
21
+ """Register plugin (decorator).
22
+
23
+ .. versionadded:: 3.2
24
+ """
25
+
26
+ def decorator(func: Callable) -> Callable:
27
+ _REGISTRY[name] = func
28
+ return func
29
+
30
+ return decorator
31
+
32
+
33
+ def get(name: str, default: Any = _UNSET) -> Any:
34
+ """Get registered plugin.
35
+
36
+ Args:
37
+ name: Plugin identifier.
38
+ default: Value returned when the plugin is missing.
39
+ If not provided, a `ValueError` is raised instead.
40
+
41
+ Raises:
42
+ ValueError: If the plugin does not exist and no *default* was supplied.
43
+
44
+ .. versionadded:: 3.2
45
+ """
46
+ try:
47
+ return _REGISTRY[name]
48
+ except KeyError:
49
+ if default is not _UNSET:
50
+ return default
51
+ raise ValueError(f"Plugin {name!r} is not registered")
52
+
53
+
54
+ def has(name: str) -> bool:
55
+ """Check whether a plugin is registered.
56
+
57
+ Args:
58
+ name: Plugin identifier.
59
+
60
+ Returns:
61
+ ``True`` if a plugin with the given *name* is registered,
62
+ otherwise ``False``.
63
+
64
+ .. versionadded:: 3.3
65
+ """
66
+ return name in _REGISTRY
67
+
68
+
69
+ def remove(name: str) -> None:
70
+ """Remove a plugin.
71
+
72
+ This is a no-op if the plugin does not exist.
73
+
74
+ .. versionadded:: 3.2
75
+ """
76
+ _REGISTRY.pop(name, None)
77
+
78
+
79
+ def clear() -> None:
80
+ """Remove all plugins from the registry.
81
+
82
+ .. versionadded:: 3.3
83
+ """
84
+ _REGISTRY.clear()
85
+
86
+
87
+ def list_all() -> list[str]:
88
+ """List all plugin names.
89
+
90
+ .. versionadded:: 3.2
91
+ """
92
+ return list(_REGISTRY.keys())
93
+
94
+
95
+ def run(name: str, *args, **kwargs) -> Any:
96
+ """Run plugin.
97
+
98
+ Raises:
99
+ ValueError: If the plugin does not exist.
100
+
101
+ .. versionadded:: 3.2
102
+ """
103
+ return get(name)(*args, **kwargs)
104
+
105
+
106
+ class Registry:
107
+ """Static class providing an alternative API for the plugin registry.
108
+
109
+ .. versionadded:: 3.2
110
+ """
111
+
112
+ @staticmethod
113
+ def register(name: str, func: Callable) -> None:
114
+ _REGISTRY[name] = func
115
+
116
+ @staticmethod
117
+ def list_all() -> list[str]:
118
+ return list_all()
119
+
120
+ @staticmethod
121
+ def get(name: str) -> Any:
122
+ return get(name)
123
+
124
+ @staticmethod
125
+ def has(name: str) -> bool:
126
+ return has(name)
127
+
128
+ @staticmethod
129
+ def remove(name: str) -> None:
130
+ remove(name)
131
+
132
+ @staticmethod
133
+ def clear() -> None:
134
+ clear()
@@ -8,7 +8,7 @@ def _get_version_tuple(version: str) -> tuple[int, int, int]:
8
8
  return (major, minor, patch)
9
9
 
10
10
 
11
- __version__: str = "3.3.0"
11
+ __version__: str = "3.4.0"
12
12
  __version_tuple__: tuple[int, int, int] = _get_version_tuple(__version__)
13
13
  __commit_id__: str | None = None
14
14
 
@@ -2,6 +2,7 @@
2
2
  errortools - a toolset for working with Python exceptions and warnings and logging.
3
3
  """
4
4
 
5
+ import sys
5
6
  from typing import Any
6
7
 
7
8
  from _errortools.raises import raises, assert_raises, raises_all, reraise
@@ -18,7 +19,7 @@ from _errortools.errno import (
18
19
  get_all_errno_codes,
19
20
  is_valid_errno,
20
21
  )
21
- from _errortools.classes.group import BaseGroup, GroupErrors
22
+ from _errortools.classes.group import BaseGroup, GroupErrors # noqa: F401
22
23
  from _errortools.decorator.cache import error_cache
23
24
  from _errortools.decorator.deprecated import deprecated, experimental
24
25
  from _errortools.decorator.handlers import suppress, convert
@@ -47,7 +48,7 @@ from _errortools.classes.abc import (
47
48
  Raiseable,
48
49
  Error,
49
50
  )
50
- from _errortools.classes.protocol import (
51
+ from _errortools.classes.protocol import ( # noqa: F401
51
52
  ExceptionLike,
52
53
  ExceptionGroupLike,
53
54
  BaseExceptionGroupLike,
@@ -73,7 +74,7 @@ from _errortools.typing import (
73
74
  TracebackType,
74
75
  FrameType,
75
76
  )
76
- from _errortools.plugins import run, register, list_all, get, remove, Registry
77
+ from _errortools.plugins import run, register, list_all, get, has, remove, clear, Registry
77
78
  from _errortools.descriptor.errormsg import ErrorMsg
78
79
  from _errortools.descriptor.nonblankmsg import NonBlankErrorMsg
79
80
  from _errortools.version import (
@@ -149,6 +150,13 @@ class PluginNamespace:
149
150
 
150
151
  plugins = PluginNamespace()
151
152
 
153
+ _PYTHON_3_11_CAN_USE: list[str] = [
154
+ "GroupErrors",
155
+ "BaseGroup",
156
+ "BaseExceptionGroupLike",
157
+ "ExceptionGroupLike",
158
+ "GroupErrorsLike",
159
+ ]
152
160
  __all__ = [
153
161
  # functions
154
162
  "raises",
@@ -173,8 +181,6 @@ __all__ = [
173
181
  "TracebackType",
174
182
  "FrameType",
175
183
  # classes
176
- "GroupErrors",
177
- "BaseGroup",
178
184
  "BaseErrorCodes",
179
185
  "InvalidInputError",
180
186
  "NotFoundError",
@@ -197,8 +203,6 @@ __all__ = [
197
203
  "ContextException",
198
204
  "Error",
199
205
  "ExceptionLike",
200
- "ExceptionGroupLike",
201
- "BaseExceptionGroupLike",
202
206
  "BlockingIOErrorLike",
203
207
  "NameErrorLike",
204
208
  "StopIterationLike",
@@ -209,7 +213,6 @@ __all__ = [
209
213
  "UnicodeEncodeErrorLike",
210
214
  "UnicodeTranslateErrorLike",
211
215
  "AttributeErrorLike",
212
- "GroupErrorsLike",
213
216
  "ErrortoolsDeprecationWarning",
214
217
  # for type hints
215
218
  "PureBaseExceptionType",
@@ -225,9 +228,11 @@ __all__ = [
225
228
  # plugins
226
229
  "register",
227
230
  "get",
231
+ "has",
228
232
  "list_all",
229
233
  "run",
230
234
  "remove",
235
+ "clear",
231
236
  "Registry",
232
237
  # metadata
233
238
  "__version__",
@@ -254,3 +259,6 @@ __all__ = [
254
259
  ]
255
260
 
256
261
  __all__.append("plugins")
262
+
263
+ if sys.version_info >= (3, 11):
264
+ __all__.append(_PYTHON_3_11_CAN_USE)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errortools
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
5
5
  Author-email: Evan Yang <quantbit@126.com>
6
6
  License: Copyright (c) 2026 authors see AUTHORS.txt
@@ -41,7 +41,6 @@ _errortools/logging/sink.py
41
41
  _errortools/wrappers/__init__.py
42
42
  _errortools/wrappers/cache.py
43
43
  _errortools/wrappers/ignore.py
44
- docs/conf.py
45
44
  errortools/__init__.py
46
45
  errortools/__main__.py
47
46
  errortools/future.py
@@ -57,22 +56,15 @@ testing/__init__.py
57
56
  testing/__main__.py
58
57
  testing/conftest.py
59
58
  testing/run_tests.py
60
- testing/test_abc.py
61
59
  testing/test_decorator.py
62
60
  testing/test_descriptor.py
63
61
  testing/test_errno.py
64
- testing/test_errorcodes.py
65
- testing/test_future.py
66
62
  testing/test_groups.py
67
63
  testing/test_ignore.py
68
- testing/test_logging.py
69
- testing/test_partials.py
70
64
  testing/test_plugins.py
71
- testing/test_protocols.py
72
65
  testing/test_raises.py
73
66
  testing/test_typing.py
74
67
  testing/test_version.py
75
- testing/test_warnings.py
76
68
  testing/benchmark/__init__.py
77
69
  testing/benchmark/test_future_perf.py
78
70
  testing/test_testing/__init__.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "errortools"
8
- version = "3.3.0"
8
+ version = "3.4.0"
9
9
  authors = [
10
10
  { name = "Evan Yang", email = "quantbit@126.com" }
11
11
  ]
@@ -10,7 +10,7 @@ __all__ = [
10
10
  "HAS_PYTEST",
11
11
  "NO_ONE_CHANGE_VERSION",
12
12
  ]
13
- __version__ = "1.2.5"
13
+ __version__ = "1.3.5"
14
14
  __version_tuple__ = _get_version_tuple(__version__)
15
15
 
16
16
  try:
@@ -1,105 +1,200 @@
1
- """Tests for _errortools/plugins — ultra-lightweight plugin system."""
2
-
3
- import pytest
4
-
5
- from _errortools.plugins import (
6
- register,
7
- get,
8
- run,
9
- list_all,
10
- remove,
11
- Registry,
12
- )
13
-
14
- # =============================================================================
15
- # register & run
16
- # =============================================================================
17
-
18
-
19
- class TestPluginRegisterAndRun:
20
- def test_register_decorator(self):
21
- @register("test_func")
22
- def f():
23
- return "ok"
24
-
25
- assert run("test_func") == "ok"
26
-
27
- def test_run_raises_when_not_registered(self):
28
- with pytest.raises(ValueError, match="Plugin 'nonexistent' is not registered"):
29
- run("nonexistent")
30
-
31
- def test_plugin_with_args_kwargs(self):
32
- @register("add")
33
- def add(a, b):
34
- return a + b
35
-
36
- assert run("add", 2, 3) == 5
37
- assert run("add", a=10, b=20) == 30
38
-
39
- def test_get_returns_correct_function(self):
40
- @register("myfunc")
41
- def f():
42
- return "test"
43
-
44
- assert get("myfunc") is f
45
-
46
-
47
- # =============================================================================
48
- # list_all
49
- # =============================================================================
50
-
51
-
52
- class TestPluginList:
53
- def test_list_all_includes_registered_plugins(self):
54
- @register("plugin1")
55
- def f1():
56
- pass
57
-
58
- @register("plugin2")
59
- def f2():
60
- pass
61
-
62
- plugins = list_all()
63
- assert "plugin1" in plugins
64
- assert "plugin2" in plugins
65
-
66
-
67
- # =============================================================================
68
- # remove
69
- # =============================================================================
70
-
71
-
72
- class TestPluginRemove:
73
- def test_remove_existing_plugin(self):
74
- @register("toremove")
75
- def f():
76
- pass
77
-
78
- remove("toremove")
79
- with pytest.raises(ValueError):
80
- get("toremove")
81
-
82
- def test_remove_nonexistent_is_safe(self):
83
- # Should not raise
84
- remove("never_existed")
85
-
86
-
87
- # =============================================================================
88
- # Registry class (static)
89
- # =============================================================================
90
-
91
-
92
- class TestRegistryClass:
93
- def test_static_register_and_get(self):
94
- def my_static_func():
95
- return "static"
96
-
97
- Registry.register("static_plugin", my_static_func)
98
- assert Registry.get("static_plugin") is my_static_func
99
-
100
- def test_static_list_all(self):
101
- @register("listme")
102
- def f():
103
- pass
104
-
105
- assert "listme" in Registry.list_all()
1
+ """Tests for _errortools/plugins — ultra-lightweight plugin system."""
2
+
3
+ import pytest
4
+
5
+ from _errortools.plugins import (
6
+ register,
7
+ get,
8
+ has,
9
+ run,
10
+ list_all,
11
+ remove,
12
+ clear,
13
+ Registry,
14
+ )
15
+
16
+ # =============================================================================
17
+ # register & run
18
+ # =============================================================================
19
+
20
+
21
+ class TestPluginRegisterAndRun:
22
+ def test_register_decorator(self):
23
+ @register("test_func")
24
+ def f():
25
+ return "ok"
26
+
27
+ assert run("test_func") == "ok"
28
+
29
+ def test_run_raises_when_not_registered(self):
30
+ with pytest.raises(ValueError, match="Plugin 'nonexistent' is not registered"):
31
+ run("nonexistent")
32
+
33
+ def test_plugin_with_args_kwargs(self):
34
+ @register("add")
35
+ def add(a, b):
36
+ return a + b
37
+
38
+ assert run("add", 2, 3) == 5
39
+ assert run("add", a=10, b=20) == 30
40
+
41
+ def test_get_returns_correct_function(self):
42
+ @register("myfunc")
43
+ def f():
44
+ return "test"
45
+
46
+ assert get("myfunc") is f
47
+
48
+ def test_get_with_default(self):
49
+ assert get("missing", default=42) == 42
50
+
51
+ def test_get_with_none_default(self):
52
+ assert get("missing", default=None) is None
53
+
54
+ def test_register_overwrites(self):
55
+ @register("overwrite_me")
56
+ def first():
57
+ return "first"
58
+
59
+ @register("overwrite_me")
60
+ def second():
61
+ return "second"
62
+
63
+ assert run("overwrite_me") == "second"
64
+
65
+ def test_run_forwards_all_args(self):
66
+ @register("variadic")
67
+ def variadic(*args, **kwargs):
68
+ return (args, kwargs)
69
+
70
+ assert run("variadic", 1, 2, x=3) == ((1, 2), {"x": 3})
71
+
72
+
73
+ # =============================================================================
74
+ # has
75
+ # =============================================================================
76
+
77
+
78
+ class TestPluginHas:
79
+ def test_has_true(self):
80
+ @register("exists")
81
+ def f():
82
+ pass
83
+
84
+ assert has("exists") is True
85
+
86
+ def test_has_false(self):
87
+ assert has("never_registered") is False
88
+
89
+
90
+ # =============================================================================
91
+ # list_all
92
+ # =============================================================================
93
+
94
+
95
+ class TestPluginList:
96
+ def test_list_all_includes_registered_plugins(self):
97
+ @register("plugin1")
98
+ def f1():
99
+ pass
100
+
101
+ @register("plugin2")
102
+ def f2():
103
+ pass
104
+
105
+ plugins = list_all()
106
+ assert "plugin1" in plugins
107
+ assert "plugin2" in plugins
108
+
109
+
110
+ # =============================================================================
111
+ # remove
112
+ # =============================================================================
113
+
114
+
115
+ class TestPluginRemove:
116
+ def test_remove_existing_plugin(self):
117
+ @register("toremove")
118
+ def f():
119
+ pass
120
+
121
+ remove("toremove")
122
+ with pytest.raises(ValueError):
123
+ get("toremove")
124
+
125
+ def test_remove_nonexistent_is_safe(self):
126
+ # Should not raise
127
+ remove("never_existed")
128
+
129
+ def test_remove_returns_none(self):
130
+ @register("for_remove")
131
+ def f():
132
+ pass
133
+
134
+ assert remove("for_remove") is None
135
+
136
+
137
+ # =============================================================================
138
+ # clear
139
+ # =============================================================================
140
+
141
+
142
+ class TestPluginClear:
143
+ def test_clear_removes_all(self):
144
+ @register("a")
145
+ def a():
146
+ pass
147
+
148
+ @register("b")
149
+ def b():
150
+ pass
151
+
152
+ clear()
153
+ assert list_all() == []
154
+ assert has("a") is False
155
+ assert has("b") is False
156
+
157
+
158
+ # =============================================================================
159
+ # Registry class (static)
160
+ # =============================================================================
161
+
162
+
163
+ class TestRegistryClass:
164
+ def test_static_register_and_get(self):
165
+ def my_static_func():
166
+ return "static"
167
+
168
+ Registry.register("static_plugin", my_static_func)
169
+ assert Registry.get("static_plugin") is my_static_func
170
+
171
+ def test_static_list_all(self):
172
+ @register("listme")
173
+ def f():
174
+ pass
175
+
176
+ assert "listme" in Registry.list_all()
177
+
178
+ def test_registry_get_raises_when_missing(self):
179
+ with pytest.raises(ValueError, match="Plugin 'missing' is not registered"):
180
+ Registry.get("missing")
181
+
182
+ def test_registry_register_overwrites(self):
183
+ def first():
184
+ return "first"
185
+
186
+ def second():
187
+ return "second"
188
+
189
+ Registry.register("overwrite_registry", first)
190
+ assert Registry.get("overwrite_registry") is first
191
+ Registry.register("overwrite_registry", second)
192
+ assert Registry.get("overwrite_registry") is second
193
+
194
+ def test_registry_remove(self):
195
+ @register("registry_remove_me")
196
+ def f():
197
+ pass
198
+
199
+ Registry.remove("registry_remove_me")
200
+ assert has("registry_remove_me") is False