errortools 2.2.0__tar.gz → 2.3.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 (71) hide show
  1. {errortools-2.2.0/errortools.egg-info → errortools-2.3.0}/PKG-INFO +2 -1
  2. {errortools-2.2.0 → errortools-2.3.0}/_errortools/_cli.py +33 -32
  3. {errortools-2.2.0 → errortools-2.3.0}/_errortools/classes/abc.py +3 -0
  4. {errortools-2.2.0 → errortools-2.3.0}/_errortools/cli.py +42 -54
  5. {errortools-2.2.0 → errortools-2.3.0}/_errortools/decorator/deprecated.py +28 -0
  6. errortools-2.3.0/_errortools/errno.py +78 -0
  7. {errortools-2.2.0 → errortools-2.3.0}/_errortools/ignore.py +23 -19
  8. {errortools-2.2.0 → errortools-2.3.0}/_errortools/typing.py +4 -0
  9. {errortools-2.2.0 → errortools-2.3.0}/_errortools/version.py +2 -2
  10. {errortools-2.2.0 → errortools-2.3.0}/errortools/__init__.py +14 -1
  11. {errortools-2.2.0 → errortools-2.3.0/errortools.egg-info}/PKG-INFO +2 -1
  12. {errortools-2.2.0 → errortools-2.3.0}/errortools.egg-info/SOURCES.txt +2 -1
  13. {errortools-2.2.0 → errortools-2.3.0}/errortools.egg-info/entry_points.txt +1 -1
  14. errortools-2.3.0/errortools.egg-info/requires.txt +2 -0
  15. {errortools-2.2.0 → errortools-2.3.0}/setup.py +3 -3
  16. {errortools-2.2.0 → errortools-2.3.0}/tests/__init__.py +1 -1
  17. errortools-2.2.0/tests/test_cache.py → errortools-2.3.0/tests/test_decorator.py +153 -2
  18. errortools-2.3.0/tests/test_errno.py +112 -0
  19. {errortools-2.2.0 → errortools-2.3.0}/tests/test_typing.py +9 -0
  20. errortools-2.2.0/errortools.egg-info/requires.txt +0 -1
  21. errortools-2.2.0/tests/test_decorator.py +0 -85
  22. {errortools-2.2.0 → errortools-2.3.0}/AUTHORS.txt +0 -0
  23. {errortools-2.2.0 → errortools-2.3.0}/LICENSE.txt +0 -0
  24. {errortools-2.2.0 → errortools-2.3.0}/README.md +0 -0
  25. {errortools-2.2.0 → errortools-2.3.0}/_errortools/__init__.py +0 -0
  26. {errortools-2.2.0 → errortools-2.3.0}/_errortools/classes/__init__.py +0 -0
  27. {errortools-2.2.0 → errortools-2.3.0}/_errortools/classes/errorcodes.py +0 -0
  28. {errortools-2.2.0 → errortools-2.3.0}/_errortools/classes/group.py +0 -0
  29. {errortools-2.2.0 → errortools-2.3.0}/_errortools/classes/warn.py +0 -0
  30. {errortools-2.2.0 → errortools-2.3.0}/_errortools/const.py +0 -0
  31. {errortools-2.2.0 → errortools-2.3.0}/_errortools/decorator/__init__.py +0 -0
  32. {errortools-2.2.0 → errortools-2.3.0}/_errortools/decorator/cache.py +0 -0
  33. {errortools-2.2.0 → errortools-2.3.0}/_errortools/descriptor/__init__.py +0 -0
  34. {errortools-2.2.0 → errortools-2.3.0}/_errortools/descriptor/errormsg.py +0 -0
  35. {errortools-2.2.0 → errortools-2.3.0}/_errortools/descriptor/nonblankmsg.py +0 -0
  36. {errortools-2.2.0 → errortools-2.3.0}/_errortools/future.py +0 -0
  37. {errortools-2.2.0 → errortools-2.3.0}/_errortools/logging/__init__.py +0 -0
  38. {errortools-2.2.0 → errortools-2.3.0}/_errortools/logging/base.py +0 -0
  39. {errortools-2.2.0 → errortools-2.3.0}/_errortools/logging/level.py +0 -0
  40. {errortools-2.2.0 → errortools-2.3.0}/_errortools/logging/logger.py +0 -0
  41. {errortools-2.2.0 → errortools-2.3.0}/_errortools/logging/record.py +0 -0
  42. {errortools-2.2.0 → errortools-2.3.0}/_errortools/logging/sink.py +0 -0
  43. {errortools-2.2.0 → errortools-2.3.0}/_errortools/metadata.py +0 -0
  44. {errortools-2.2.0 → errortools-2.3.0}/_errortools/methods/__init__.py +0 -0
  45. {errortools-2.2.0 → errortools-2.3.0}/_errortools/methods/errorattr.py +0 -0
  46. {errortools-2.2.0 → errortools-2.3.0}/_errortools/methods/errordelattr.py +0 -0
  47. {errortools-2.2.0 → errortools-2.3.0}/_errortools/methods/errorhasattr.py +0 -0
  48. {errortools-2.2.0 → errortools-2.3.0}/_errortools/methods/errorsetattr.py +0 -0
  49. {errortools-2.2.0 → errortools-2.3.0}/_errortools/partial.py +0 -0
  50. {errortools-2.2.0 → errortools-2.3.0}/_errortools/py.typed +0 -0
  51. {errortools-2.2.0 → errortools-2.3.0}/_errortools/raises.py +0 -0
  52. {errortools-2.2.0 → errortools-2.3.0}/_errortools/wrappers/__init__.py +0 -0
  53. {errortools-2.2.0 → errortools-2.3.0}/_errortools/wrappers/cache.py +0 -0
  54. {errortools-2.2.0 → errortools-2.3.0}/_errortools/wrappers/ignore.py +0 -0
  55. {errortools-2.2.0 → errortools-2.3.0}/errortools/__main__.py +0 -0
  56. {errortools-2.2.0 → errortools-2.3.0}/errortools.egg-info/dependency_links.txt +0 -0
  57. {errortools-2.2.0 → errortools-2.3.0}/errortools.egg-info/top_level.txt +0 -0
  58. {errortools-2.2.0 → errortools-2.3.0}/setup.cfg +0 -0
  59. {errortools-2.2.0 → errortools-2.3.0}/tests/conftest.py +0 -0
  60. {errortools-2.2.0 → errortools-2.3.0}/tests/run_tests.py +0 -0
  61. {errortools-2.2.0 → errortools-2.3.0}/tests/test_abc.py +0 -0
  62. {errortools-2.2.0 → errortools-2.3.0}/tests/test_const.py +0 -0
  63. {errortools-2.2.0 → errortools-2.3.0}/tests/test_descriptor.py +0 -0
  64. {errortools-2.2.0 → errortools-2.3.0}/tests/test_errorcodes.py +0 -0
  65. {errortools-2.2.0 → errortools-2.3.0}/tests/test_groups.py +0 -0
  66. {errortools-2.2.0 → errortools-2.3.0}/tests/test_ignore.py +0 -0
  67. {errortools-2.2.0 → errortools-2.3.0}/tests/test_logging.py +0 -0
  68. {errortools-2.2.0 → errortools-2.3.0}/tests/test_mixins.py +0 -0
  69. {errortools-2.2.0 → errortools-2.3.0}/tests/test_partials.py +0 -0
  70. {errortools-2.2.0 → errortools-2.3.0}/tests/test_raises.py +0 -0
  71. {errortools-2.2.0 → errortools-2.3.0}/tests/test_warnings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errortools
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
5
5
  Home-page: https://github.com/more-abc/errortools
6
6
  Author: Evan Yang
@@ -20,6 +20,7 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE.txt
21
21
  License-File: AUTHORS.txt
22
22
  Requires-Dist: namebyauthor==1.0.0
23
+ Requires-Dist: typing_extensions>=4.8.0
23
24
  Dynamic: author
24
25
  Dynamic: author-email
25
26
  Dynamic: classifier
@@ -1,32 +1,33 @@
1
- import sys
2
-
3
- from _errortools.metadata import (
4
- __author__,
5
- __author_email__,
6
- __copyright__,
7
- __description__,
8
- __license__,
9
- __url__,
10
- )
11
- from _errortools.version import __version__
12
-
13
-
14
- def _cmd_log(message: str, level: str, output: str) -> None:
15
- """Emit a single log message via the errortools logger."""
16
- from .logging import BaseLogger
17
- from .logging.level import get_level
18
-
19
- stream = sys.stdout if output == "stdout" else sys.stderr
20
- log = BaseLogger(name="errortools-cli")
21
- log.add(stream, level="TRACE", colorize=None)
22
- log.log(get_level(level), message)
23
-
24
-
25
- def _print_info() -> None:
26
- """Print a summary of all package metadata."""
27
- print(f"errortools v{__version__}")
28
- print(f" {__description__}")
29
- print(f" Author: {__author__} <{__author_email__}>")
30
- print(f" License: {__license__}")
31
- print(f" URL: {__url__}")
32
- print(f" Copyright: {__copyright__}")
1
+ import sys
2
+
3
+ from _errortools.metadata import (
4
+ __author__,
5
+ __author_email__,
6
+ __copyright__,
7
+ __description__,
8
+ __license__,
9
+ __url__,
10
+ )
11
+ from _errortools.version import __version__
12
+
13
+
14
+ def _cmd_log(message: str, level: str, output: str) -> None:
15
+ """Emit a single log message via the errortools logger."""
16
+ from .logging import BaseLogger
17
+ from .logging.level import get_level
18
+
19
+ stream = sys.stdout if output == "stdout" else sys.stderr
20
+ log = BaseLogger(name="errortools-cli")
21
+ log.set_level("TRACE")
22
+ log.add(stream, level=level, colorize=None)
23
+ log.log(get_level(level), message)
24
+
25
+
26
+ def _print_info() -> None:
27
+ """Print a summary of all package metadata."""
28
+ print(f"errortools v{__version__}")
29
+ print(f" {__description__}")
30
+ print(f" Author: {__author__} <{__author_email__}>")
31
+ print(f" License: {__license__}")
32
+ print(f" URL: {__url__}")
33
+ print(f" Copyright: {__copyright__}")
@@ -5,6 +5,7 @@ import shutil
5
5
  import csv
6
6
  import configparser
7
7
 
8
+ from typing_extensions import disjoint_base # I use 3.14.3
8
9
  from ..methods import (
9
10
  ErrorAttrMixin,
10
11
  ErrorAttrCheckMixin,
@@ -30,6 +31,7 @@ def _check_methods(C: type[Any], *methods: str) -> Union[bool, Literal[NotImplem
30
31
  return True
31
32
 
32
33
 
34
+ @disjoint_base
33
35
  class ErrorAttrable(ABC):
34
36
  """
35
37
  Abstract Base Class (ABC) for classes supporting custom attribute error handling.
@@ -175,6 +177,7 @@ ErrorAttrable.register(ErrorSetAttrMixin)
175
177
  # ----------------------------------------------------------------------
176
178
 
177
179
 
180
+ @disjoint_base
178
181
  class ErrorCodeable(ABC):
179
182
  """Abstract Base Class for exceptions that carry a machine-readable error code.
180
183
 
@@ -14,107 +14,95 @@ from .metadata import (
14
14
  __url__,
15
15
  )
16
16
  from .version import __version__
17
- from tests.run_tests import run_tests
18
17
 
19
18
 
20
19
  def parse_args(args: list[str] | None = None) -> argparse.Namespace:
21
20
  """Parse command line arguments."""
21
+ is_logger = "logger" in sys.argv[0]
22
+
23
+ if is_logger:
24
+ parser = argparse.ArgumentParser(
25
+ prog="logger",
26
+ description="Logger CLI - Emit log messages from command line",
27
+ )
28
+ parser.add_argument("message", help="Log message")
29
+ parser.add_argument(
30
+ "--level",
31
+ "-l",
32
+ default="info",
33
+ choices=[
34
+ "trace",
35
+ "debug",
36
+ "info",
37
+ "success",
38
+ "warning",
39
+ "error",
40
+ "critical",
41
+ ],
42
+ )
43
+ parser.add_argument(
44
+ "--output", "-o", choices=["stderr", "stdout"], default="stderr"
45
+ )
46
+ return parser.parse_args(args)
47
+
48
+ prog = "errortools"
49
+ desc = __description__
50
+
22
51
  if sys.version_info >= (3, 14):
23
- parser = argparse.ArgumentParser(description=__description__, color=True)
52
+ parser = argparse.ArgumentParser(description=desc, prog=prog, color=True)
24
53
  else:
25
- parser = argparse.ArgumentParser(description=__description__)
54
+ parser = argparse.ArgumentParser(description=desc, prog=prog)
26
55
 
27
56
  parser.add_argument(
28
57
  "-v", "--version", action="store_true", help="Show version and exit"
29
58
  )
30
-
31
59
  parser.add_argument(
32
60
  "-c", "--copyrights", action="store_true", help="Show copyright information"
33
61
  )
34
-
35
62
  parser.add_argument("-a", "--author", action="store_true", help="Show author name")
36
-
37
63
  parser.add_argument("-e", "--email", action="store_true", help="Show author email")
38
-
39
64
  parser.add_argument(
40
65
  "-l", "--license", action="store_true", help="Show license type"
41
66
  )
42
-
43
67
  parser.add_argument("-u", "--url", action="store_true", help="Show project URL")
44
-
45
68
  parser.add_argument(
46
69
  "-i", "--info", action="store_true", help="Show all package information"
47
70
  )
48
-
49
71
  parser.add_argument(
50
- "--run-tests",
51
- action="store_true",
52
- help="Run tests for errortools module. (Using pytest)",
53
- )
54
-
55
- subparsers = parser.add_subparsers(dest="subcommand")
56
-
57
- log_parser = subparsers.add_parser(
58
- "log",
59
- help="Emit a log message from the command line",
60
- )
61
- log_parser.add_argument(
62
- "message",
63
- help="The message to log",
64
- )
65
- log_parser.add_argument(
66
- "--level",
67
- "-l",
68
- default="info",
69
- choices=["trace", "debug", "info", "success", "warning", "error", "critical"],
70
- help="Log level (default: info)",
71
- )
72
- log_parser.add_argument(
73
- "--output",
74
- "-o",
75
- choices=["stderr", "stdout"],
76
- default="stderr",
77
- help="Output stream (default: stderr)",
72
+ "--run-tests", action="store_true", help="Run tests using pytest"
78
73
  )
79
74
 
80
75
  return parser.parse_args(args)
81
76
 
82
77
 
83
- def log_main() -> None:
84
- """Logging main CLI entry point."""
78
+ def main() -> None:
85
79
  args = parse_args(sys.argv[1:])
86
80
 
87
- if args.subcommand == "log":
81
+ if "logger" in sys.argv[0]:
88
82
  _cmd_log(args.message, args.level, args.output)
89
-
90
-
91
- def main() -> None:
92
- """Main CLI entry point."""
93
- args = parse_args(sys.argv[1:])
83
+ return
94
84
 
95
85
  if args.version:
96
86
  print(f"v{__version__}")
97
-
98
87
  elif args.copyrights:
99
88
  print(__copyright__)
100
-
101
89
  elif args.author:
102
90
  print(f"Author: {__author__}")
103
-
104
91
  elif args.email:
105
92
  print(f"Email: {__author_email__}")
106
-
107
93
  elif args.license:
108
94
  print(f"License: {__license__}")
109
-
110
95
  elif args.url:
111
96
  print(f"URL: {__url__}")
112
-
113
97
  elif args.run_tests:
114
- run_tests()
98
+ from tests.run_tests import run_tests
115
99
 
100
+ run_tests()
116
101
  elif args.info:
117
102
  _print_info()
118
-
119
103
  else:
120
104
  parse_args(["--help"])
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -31,3 +31,31 @@ def deprecated(
31
31
  return wrapper
32
32
 
33
33
  return decorator
34
+
35
+
36
+ def experimental(
37
+ reason: str = "This function is experimental and may change in future versions.",
38
+ ) -> Callable:
39
+ """Decorator that marks a function as experimental.
40
+
41
+ Emits a UserWarning when the decorated function is called.
42
+
43
+ Args:
44
+ reason: Optional message explaining the experimental status and any caveats.
45
+
46
+ Example:
47
+ >>> @experimental(reason="API may change without notice.")
48
+ ... def new_feature():
49
+ ... pass
50
+ """
51
+
52
+ def decorator(func: Callable) -> Callable:
53
+ @wraps(func)
54
+ def wrapper(*args, **kwargs):
55
+ msg = f"{func.__name__} is experimental. {reason}"
56
+ warnings.warn(msg, UserWarning, stacklevel=2)
57
+ return func(*args, **kwargs)
58
+
59
+ return wrapper
60
+
61
+ return decorator
@@ -0,0 +1,78 @@
1
+ import errno
2
+
3
+
4
+ def get_errno_name(code: int) -> str | None:
5
+ """Get the symbolic name for an errno code.
6
+
7
+ Args:
8
+ code: The errno code (e.g., 2 for ENOENT)
9
+
10
+ Returns:
11
+ The errno name (e.g., "ENOENT") or None if not found
12
+ """
13
+ for name in dir(errno):
14
+ if name.isupper() and getattr(errno, name) == code:
15
+ return name
16
+ return None
17
+
18
+
19
+ def get_errno_message(code: int) -> str:
20
+ """Get the error message for an errno code.
21
+
22
+ Args:
23
+ code: The errno code
24
+
25
+ Returns:
26
+ The error message string
27
+ """
28
+ try:
29
+ return errno.errorcode.get(code, f"Unknown error {code}")
30
+ except (AttributeError, KeyError):
31
+ return f"Unknown error {code}"
32
+
33
+
34
+ def get_all_errno_codes() -> dict[str, int]:
35
+ """Get a mapping of all errno names to their numeric codes.
36
+
37
+ Returns:
38
+ Dictionary mapping errno names to their numeric values
39
+ """
40
+ codes = {}
41
+ for name in dir(errno):
42
+ if name.isupper():
43
+ try:
44
+ value = getattr(errno, name)
45
+ if isinstance(value, int):
46
+ codes[name] = value
47
+ except (AttributeError, TypeError):
48
+ pass
49
+ return codes
50
+
51
+
52
+ def is_valid_errno(code: int) -> bool:
53
+ """Check if a code is a valid errno constant.
54
+
55
+ Args:
56
+ code: The code to check
57
+
58
+ Returns:
59
+ True if the code is a valid errno, False otherwise
60
+ """
61
+ return get_errno_name(code) is not None
62
+
63
+
64
+ def strerror(code: int) -> str:
65
+ """Get human-readable error message for errno code.
66
+
67
+ Args:
68
+ code: The errno code
69
+
70
+ Returns:
71
+ Error message string
72
+ """
73
+ import os
74
+
75
+ try:
76
+ return os.strerror(code)
77
+ except (ValueError, OSError):
78
+ return f"Unknown error {code}"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from collections.abc import Iterator, Callable
6
6
  from contextlib import contextmanager
7
7
  from functools import wraps
8
+ from typing import Any, TypeVar
8
9
  import asyncio
9
10
  import inspect
10
11
  import time
@@ -21,6 +22,9 @@ __all__ = [
21
22
  "retry",
22
23
  ]
23
24
 
25
+ Func = TypeVar("Func", bound=Callable[..., Any])
26
+ ExceptionType = type[BaseException]
27
+ ExceptionTypes = tuple[ExceptionType, ...]
24
28
 
25
29
  # A Context Manager? Maybe it is...
26
30
 
@@ -107,7 +111,7 @@ class fast_ignore:
107
111
 
108
112
  __slots__ = ("_excs",)
109
113
 
110
- def __init__(self, *excs: type[BaseException]) -> None:
114
+ def __init__(self, *excs: ExceptionType) -> None:
111
115
  for exc in excs:
112
116
  if not isinstance(exc, type) or not issubclass(exc, BaseException):
113
117
  raise TypeError(f"Expected Exception subclass, got {exc!r}")
@@ -116,14 +120,14 @@ class fast_ignore:
116
120
  def __enter__(self) -> None:
117
121
  return
118
122
 
119
- def __exit__(self, typ: type[BaseException] | None, _, __) -> bool:
123
+ def __exit__(self, typ: ExceptionType | None, _, __) -> bool:
120
124
  if typ is None:
121
125
  return False
122
126
  return typ in self._excs
123
127
 
124
128
 
125
129
  @contextmanager
126
- def ignore_subclass(base: type[BaseException]) -> Iterator[None]:
130
+ def ignore_subclass(base: ExceptionType) -> Iterator[None]:
127
131
  """Context manager that suppresses any exception whose type is a subclass of *base*.
128
132
 
129
133
  Args:
@@ -227,13 +231,13 @@ class retry:
227
231
  >>> unstable()
228
232
  Traceback (most recent call last):
229
233
  ...
230
- ValueError: oops
234
+ ValueError: oops
231
235
  """
232
236
 
233
237
  def __init__(
234
238
  self,
235
239
  times: int,
236
- on: type[Exception] | tuple[type[Exception], ...] = Exception,
240
+ on: ExceptionType | ExceptionTypes = Exception,
237
241
  delay: float = 0,
238
242
  ) -> None:
239
243
  if times < 0:
@@ -241,23 +245,19 @@ class retry:
241
245
 
242
246
  exc_types = on if isinstance(on, tuple) else (on,)
243
247
  for t in exc_types:
244
- if not isinstance(t, type) or not issubclass(t, Exception):
248
+ if not isinstance(t, type) or not issubclass(t, BaseException):
245
249
  raise TypeError(f"Expected Exception subclass, got {t!r}")
246
250
 
247
251
  self._times = times
248
252
  self._on = exc_types
249
253
  self._delay = delay
250
254
 
251
- # ------------------------------------------------------------------
252
- # Decorator protocol
253
- # ------------------------------------------------------------------
254
-
255
- def __call__(self, func: Callable) -> Callable:
255
+ def __call__(self, func: Func) -> Func:
256
256
  if inspect.iscoroutinefunction(func):
257
257
 
258
258
  @wraps(func)
259
- async def async_wrapper(*args, **kwargs):
260
- last_exc: Exception | None = None
259
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
260
+ last_exc: BaseException | None = None
261
261
  for attempt in range(self._times + 1):
262
262
  try:
263
263
  return await func(*args, **kwargs)
@@ -265,13 +265,15 @@ class retry:
265
265
  last_exc = exc
266
266
  if attempt < self._times and self._delay:
267
267
  await asyncio.sleep(self._delay)
268
- raise last_exc
268
+ if last_exc is not None:
269
+ raise last_exc
270
+ raise RuntimeError("No exception to raise")
269
271
 
270
- return async_wrapper
272
+ return async_wrapper # type: ignore
271
273
 
272
274
  @wraps(func)
273
- def wrapper(*args, **kwargs):
274
- last_exc: Exception | None = None
275
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
276
+ last_exc: BaseException | None = None
275
277
  for attempt in range(self._times + 1):
276
278
  try:
277
279
  return func(*args, **kwargs)
@@ -279,6 +281,8 @@ class retry:
279
281
  last_exc = exc
280
282
  if attempt < self._times and self._delay:
281
283
  time.sleep(self._delay)
282
- raise last_exc
284
+ if last_exc is not None:
285
+ raise last_exc
286
+ raise RuntimeError("No exception to raise")
283
287
 
284
- return wrapper
288
+ return wrapper # type: ignore
@@ -24,6 +24,7 @@ __all__ = [
24
24
  "LookupError_",
25
25
  "RuntimeError_",
26
26
  "ExceptionType",
27
+ "WarningType",
27
28
  "TracebackType",
28
29
  "FrameType",
29
30
  ]
@@ -74,6 +75,9 @@ RuntimeError_: TypeAlias = Union[RuntimeFailure, TimeoutFailure]
74
75
  ExceptionType: TypeAlias = type[Exception]
75
76
  """Type alias for an exception *class* (not an instance)."""
76
77
 
78
+ WarningType: TypeAlias = type[Warning]
79
+ """Type alias for an warning *class* like `ExceptionType` (not an instance)."""
80
+
77
81
  # ---------------------------------------------------------------------------
78
82
  # Types from ``types`` module
79
83
  # ---------------------------------------------------------------------------
@@ -1,5 +1,5 @@
1
- __version__: str = "2.2.0"
2
- __version_tuple__: tuple[int, int, int] = (2, 2, 0)
1
+ __version__: str = "2.3.0"
2
+ __version_tuple__: tuple[int, int, int] = (2, 3, 0)
3
3
  __commit_id__: str | None = None
4
4
 
5
5
  version = __version__
@@ -11,9 +11,15 @@ from _errortools.ignore import (
11
11
  timeout,
12
12
  retry,
13
13
  )
14
+ from _errortools.errno import (
15
+ get_errno_message,
16
+ get_errno_name,
17
+ get_all_errno_codes,
18
+ is_valid_errno,
19
+ )
14
20
  from _errortools.classes.group import BaseGroup, GroupErrors
15
21
  from _errortools.decorator.cache import error_cache
16
- from _errortools.decorator.deprecated import deprecated
22
+ from _errortools.decorator.deprecated import deprecated, experimental
17
23
  from _errortools.classes.errorcodes import (
18
24
  PureBaseException,
19
25
  ContextException,
@@ -56,6 +62,7 @@ from _errortools.typing import (
56
62
  InputError,
57
63
  AccessError,
58
64
  ExceptionType,
65
+ WarningType,
59
66
  TracebackType,
60
67
  FrameType,
61
68
  )
@@ -98,7 +105,12 @@ __all__ = [
98
105
  "ignore_warns",
99
106
  "timeout",
100
107
  "retry",
108
+ "get_errno_message",
109
+ "get_errno_name",
110
+ "get_all_errno_codes",
111
+ "is_valid_errno",
101
112
  "deprecated",
113
+ "experimental",
102
114
  "error_cache",
103
115
  "TracebackType",
104
116
  "FrameType",
@@ -141,6 +153,7 @@ __all__ = [
141
153
  "LookupError_",
142
154
  "RuntimeError_",
143
155
  "ExceptionType",
156
+ "WarningType",
144
157
  # metadata
145
158
  "__version__",
146
159
  "__version_tuple__",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errortools
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: errortools - a toolset for working with Python exceptions and warnings and logging.
5
5
  Home-page: https://github.com/more-abc/errortools
6
6
  Author: Evan Yang
@@ -20,6 +20,7 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE.txt
21
21
  License-File: AUTHORS.txt
22
22
  Requires-Dist: namebyauthor==1.0.0
23
+ Requires-Dist: typing_extensions>=4.8.0
23
24
  Dynamic: author
24
25
  Dynamic: author-email
25
26
  Dynamic: classifier
@@ -6,6 +6,7 @@ _errortools/__init__.py
6
6
  _errortools/_cli.py
7
7
  _errortools/cli.py
8
8
  _errortools/const.py
9
+ _errortools/errno.py
9
10
  _errortools/future.py
10
11
  _errortools/ignore.py
11
12
  _errortools/metadata.py
@@ -51,10 +52,10 @@ tests/__init__.py
51
52
  tests/conftest.py
52
53
  tests/run_tests.py
53
54
  tests/test_abc.py
54
- tests/test_cache.py
55
55
  tests/test_const.py
56
56
  tests/test_decorator.py
57
57
  tests/test_descriptor.py
58
+ tests/test_errno.py
58
59
  tests/test_errorcodes.py
59
60
  tests/test_groups.py
60
61
  tests/test_ignore.py
@@ -1,3 +1,3 @@
1
1
  [console_scripts]
2
- logger = _errortools.cli:log_main
2
+ logger = _errortools.cli:main
3
3
  python -m errortools = _errortools.cli:main
@@ -0,0 +1,2 @@
1
+ namebyauthor==1.0.0
2
+ typing_extensions>=4.8.0
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="errortools",
5
- version="2.2.0",
5
+ version="2.3.0",
6
6
  description="errortools - a toolset for working with Python exceptions and warnings and logging.",
7
7
  long_description=open("README.md", encoding="utf-8").read(),
8
8
  long_description_content_type="text/markdown",
@@ -25,11 +25,11 @@ setup(
25
25
  package_data={"errortools": ["py.typed"]},
26
26
  include_package_data=True,
27
27
  python_requires=">=3.10",
28
- install_requires=["namebyauthor==1.0.0"],
28
+ install_requires=["namebyauthor==1.0.0", "typing_extensions>=4.8.0"],
29
29
  entry_points={
30
30
  "console_scripts": [
31
31
  "python -m errortools = _errortools.cli:main",
32
- "logger = _errortools.cli:log_main",
32
+ "logger = _errortools.cli:main",
33
33
  ]
34
34
  },
35
35
  )
@@ -1,6 +1,6 @@
1
1
  """Tests for `errortools` module. Using pytest."""
2
2
 
3
- __version__ = "1.5"
3
+ __version__ = "1.9"
4
4
 
5
5
  try:
6
6
  import pytest
@@ -1,14 +1,165 @@
1
- """Tests for _errortools/cachederror_cache decorator and ErrorCacheWrapper."""
1
+ """Tests for _errortools/decoratordecorators."""
2
+
3
+ import warnings
2
4
 
3
5
  import pytest
4
6
 
5
7
  from _errortools.decorator.cache import error_cache
8
+ from _errortools.decorator.deprecated import deprecated, experimental
6
9
  from . import HAS_PYTEST
7
10
 
8
11
  if not HAS_PYTEST:
9
- print("pytest is required to run these tests, skip run test_cache.py")
12
+ print("pytest is required to run these tests, skip run test_decorator.py")
10
13
  exit(0)
11
14
 
15
+ # =============================================================================
16
+ # deprecated decorator
17
+ # =============================================================================
18
+
19
+
20
+ class TestDeprecatedDecorator:
21
+ def test_bare_decorator_with_version(self):
22
+ @deprecated(version="2.0")
23
+ def f(x):
24
+ return x
25
+
26
+ with warnings.catch_warnings():
27
+ warnings.simplefilter("ignore")
28
+ assert f(3) == 3
29
+
30
+ def test_decorator_with_reason(self):
31
+ @deprecated(version="2.0", reason="Use new_func instead")
32
+ def f(x):
33
+ return x * 2
34
+
35
+ with warnings.catch_warnings():
36
+ warnings.simplefilter("ignore")
37
+ assert f(5) == 10
38
+
39
+ def test_wrapper_has_correct_name(self):
40
+ @deprecated(version="2.0")
41
+ def my_func(x):
42
+ return x
43
+
44
+ assert my_func.__name__ == "my_func"
45
+
46
+ def test_wrapped_attribute(self):
47
+ def inner(x):
48
+ return x
49
+
50
+ wrapped = deprecated(version="2.0")(inner)
51
+ assert wrapped.__wrapped__ is inner
52
+
53
+
54
+ class TestDeprecatedWarning:
55
+ def test_emits_deprecation_warning_on_call(self):
56
+ @deprecated(version="2.0")
57
+ def f():
58
+ pass
59
+
60
+ with pytest.warns(DeprecationWarning) as record:
61
+ f()
62
+
63
+ assert len(record) == 1
64
+ warn = record[0]
65
+ assert "deprecated since version 2.0" in str(warn.message)
66
+
67
+ def test_warning_contains_reason(self):
68
+ @deprecated(version="2.0", reason="Please upgrade API")
69
+ def f():
70
+ pass
71
+
72
+ with pytest.warns(DeprecationWarning) as record:
73
+ f()
74
+
75
+ assert "Please upgrade API" in str(record[0].message)
76
+
77
+ def test_warning_stacklevel_correct(self):
78
+ @deprecated(version="2.0")
79
+ def f():
80
+ pass
81
+
82
+ with pytest.warns(DeprecationWarning) as record:
83
+ f()
84
+
85
+ # Ensure warning points to caller, not wrapper
86
+ assert record[0].lineno is not None
87
+
88
+
89
+ # =============================================================================
90
+ # experimental decorator
91
+ # =============================================================================
92
+
93
+
94
+ class TestExperimentalDecorator:
95
+ def test_bare_decorator(self):
96
+ @experimental()
97
+ def f(x):
98
+ return x
99
+
100
+ with warnings.catch_warnings():
101
+ warnings.simplefilter("ignore")
102
+ assert f(3) == 3
103
+
104
+ def test_decorator_with_reason(self):
105
+ @experimental(reason="API may change without notice")
106
+ def f(x):
107
+ return x * 2
108
+
109
+ with warnings.catch_warnings():
110
+ warnings.simplefilter("ignore")
111
+ assert f(5) == 10
112
+
113
+ def test_wrapper_has_correct_name(self):
114
+ @experimental()
115
+ def my_func(x):
116
+ return x
117
+
118
+ assert my_func.__name__ == "my_func"
119
+
120
+ def test_wrapped_attribute(self):
121
+ def inner(x):
122
+ return x
123
+
124
+ wrapped = experimental()(inner)
125
+ assert wrapped.__wrapped__ is inner
126
+
127
+
128
+ class TestExperimentalWarning:
129
+ def test_emits_user_warning_on_call(self):
130
+ @experimental()
131
+ def f():
132
+ pass
133
+
134
+ with pytest.warns(UserWarning) as record:
135
+ f()
136
+
137
+ assert len(record) == 1
138
+ warn = record[0]
139
+ assert "experimental" in str(warn.message)
140
+
141
+ def test_warning_contains_reason(self):
142
+ @experimental(reason="Subject to change")
143
+ def f():
144
+ pass
145
+
146
+ with pytest.warns(UserWarning) as record:
147
+ f()
148
+
149
+ assert "Subject to change" in str(record[0].message)
150
+
151
+ def test_warning_stacklevel_correct(self):
152
+ @experimental()
153
+ def f():
154
+ pass
155
+
156
+ with pytest.warns(UserWarning) as record:
157
+ f()
158
+
159
+ # Ensure warning points to caller, not wrapper
160
+ assert record[0].lineno is not None
161
+
162
+
12
163
  # =============================================================================
13
164
  # error_cache — basic decoration patterns
14
165
  # =============================================================================
@@ -0,0 +1,112 @@
1
+ """Tests for _errortools/errno.py — errno tools."""
2
+
3
+ import errno
4
+
5
+ from _errortools.errno import (
6
+ get_errno_name,
7
+ get_errno_message,
8
+ get_all_errno_codes,
9
+ is_valid_errno,
10
+ strerror,
11
+ )
12
+ from . import HAS_PYTEST
13
+
14
+ if not HAS_PYTEST:
15
+ print("pytest is required to run these tests, skip run test_errno.py")
16
+ exit(0)
17
+
18
+
19
+ class TestGetErrnoName:
20
+ def test_valid_errno_enoent(self):
21
+ assert get_errno_name(errno.ENOENT) == "ENOENT"
22
+
23
+ def test_valid_errno_eacces(self):
24
+ assert get_errno_name(errno.EACCES) == "EACCES"
25
+
26
+ def test_invalid_errno_code(self):
27
+ assert get_errno_name(9999) is None
28
+
29
+ def test_errno_zero(self):
30
+ assert get_errno_name(0) is None
31
+
32
+
33
+ class TestGetErrnoMessage:
34
+ def test_valid_errno_message(self):
35
+ message = get_errno_message(errno.ENOENT)
36
+ assert isinstance(message, str)
37
+ assert len(message) > 0
38
+
39
+ def test_invalid_errno_message(self):
40
+ message = get_errno_message(9999)
41
+ assert "Unknown error" in message or len(message) > 0
42
+
43
+ def test_errno_two_message(self):
44
+ message = get_errno_message(2)
45
+ assert isinstance(message, str)
46
+
47
+
48
+ class TestGetAllErrnocodes:
49
+ def test_returns_dict(self):
50
+ codes = get_all_errno_codes()
51
+ assert isinstance(codes, dict)
52
+
53
+ def test_contains_enoent(self):
54
+ codes = get_all_errno_codes()
55
+ assert "ENOENT" in codes
56
+ assert codes["ENOENT"] == errno.ENOENT
57
+
58
+ def test_contains_eacces(self):
59
+ codes = get_all_errno_codes()
60
+ assert "EACCES" in codes
61
+ assert codes["EACCES"] == errno.EACCES
62
+
63
+ def test_all_values_are_integers(self):
64
+ codes = get_all_errno_codes()
65
+ for _, code in codes.items():
66
+ assert isinstance(code, int)
67
+
68
+ def test_all_keys_are_uppercase(self):
69
+ codes = get_all_errno_codes()
70
+ for name in codes.keys():
71
+ assert name.isupper()
72
+
73
+
74
+ class TestIsValidErrno:
75
+ def test_valid_errno_enoent(self):
76
+ assert is_valid_errno(errno.ENOENT) is True
77
+
78
+ def test_valid_errno_eacces(self):
79
+ assert is_valid_errno(errno.EACCES) is True
80
+
81
+ def test_invalid_errno(self):
82
+ assert is_valid_errno(9999) is False
83
+
84
+ def test_errno_zero(self):
85
+ assert is_valid_errno(0) is False
86
+
87
+
88
+ class TestStrerror:
89
+ def test_enoent_message(self):
90
+ message = strerror(errno.ENOENT)
91
+ assert isinstance(message, str)
92
+ assert len(message) > 0
93
+
94
+ def test_eacces_message(self):
95
+ message = strerror(errno.EACCES)
96
+ assert isinstance(message, str)
97
+ assert len(message) > 0
98
+
99
+ def test_invalid_errno_fallback(self):
100
+ message = strerror(9999)
101
+ assert isinstance(message, str)
102
+ assert "Unknown error" in message or len(message) > 0
103
+
104
+ def test_consistency_with_os_strerror(self):
105
+ import os
106
+
107
+ code = errno.ENOENT
108
+ try:
109
+ expected = os.strerror(code)
110
+ assert strerror(code) == expected
111
+ except (ValueError, OSError):
112
+ pass
@@ -13,6 +13,7 @@ from _errortools.typing import (
13
13
  LookupError_,
14
14
  RuntimeError_,
15
15
  ExceptionType,
16
+ WarningType,
16
17
  TracebackType,
17
18
  FrameType,
18
19
  )
@@ -89,6 +90,13 @@ class TestExceptionTypeAlias:
89
90
  assert get_args(ExceptionType) == (Exception,)
90
91
 
91
92
 
93
+ class TestWarningTypeAlias:
94
+
95
+ def test_warning_type(self):
96
+ assert get_origin(WarningType) is type
97
+ assert get_args(WarningType) == (Warning,)
98
+
99
+
92
100
  class TestTracebackAndFrameTypes:
93
101
 
94
102
  def test_traceback_type_matches_real_traceback(self):
@@ -136,6 +144,7 @@ class TestModuleExports:
136
144
  "LookupError_",
137
145
  "RuntimeError_",
138
146
  "ExceptionType",
147
+ "WarningType",
139
148
  "TracebackType",
140
149
  "FrameType",
141
150
  }
@@ -1 +0,0 @@
1
- namebyauthor==1.0.0
@@ -1,85 +0,0 @@
1
- """Tests for _errortools/decorator — decorators."""
2
-
3
- import warnings
4
-
5
- import pytest
6
-
7
- from _errortools.decorator.deprecated import deprecated
8
- from . import HAS_PYTEST
9
-
10
- if not HAS_PYTEST:
11
- print("pytest is required to run these tests, skip run test_decorator.py")
12
- exit(0)
13
-
14
- # =============================================================================
15
- # deprecated decorator
16
- # =============================================================================
17
-
18
-
19
- class TestDeprecatedDecorator:
20
- def test_bare_decorator_with_version(self):
21
- @deprecated(version="2.0")
22
- def f(x):
23
- return x
24
-
25
- with warnings.catch_warnings():
26
- warnings.simplefilter("ignore")
27
- assert f(3) == 3
28
-
29
- def test_decorator_with_reason(self):
30
- @deprecated(version="2.0", reason="Use new_func instead")
31
- def f(x):
32
- return x * 2
33
-
34
- with warnings.catch_warnings():
35
- warnings.simplefilter("ignore")
36
- assert f(5) == 10
37
-
38
- def test_wrapper_has_correct_name(self):
39
- @deprecated(version="2.0")
40
- def my_func(x):
41
- return x
42
-
43
- assert my_func.__name__ == "my_func"
44
-
45
- def test_wrapped_attribute(self):
46
- def inner(x):
47
- return x
48
-
49
- wrapped = deprecated(version="2.0")(inner)
50
- assert wrapped.__wrapped__ is inner
51
-
52
-
53
- class TestDeprecatedWarning:
54
- def test_emits_deprecation_warning_on_call(self):
55
- @deprecated(version="2.0")
56
- def f():
57
- pass
58
-
59
- with pytest.warns(DeprecationWarning) as record:
60
- f()
61
-
62
- assert len(record) == 1
63
- warn = record[0]
64
- assert "deprecated since version 2.0" in str(warn.message)
65
-
66
- def test_warning_contains_reason(self):
67
- @deprecated(version="2.0", reason="Please upgrade API")
68
- def f():
69
- pass
70
-
71
- with pytest.warns(DeprecationWarning) as record:
72
- f()
73
-
74
- assert "Please upgrade API" in str(record[0].message)
75
-
76
- def test_warning_stacklevel_correct(self):
77
- @deprecated(version="2.0")
78
- def f():
79
- pass
80
-
81
- with pytest.warns(DeprecationWarning) as record:
82
- f()
83
-
84
- # Ensure warning points to caller, not wrapper
85
- assert record[0].lineno is not None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes