qa-testing-utils 0.0.10__tar.gz → 0.0.11__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 (31) hide show
  1. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/PKG-INFO +1 -1
  2. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/pyproject.toml +2 -2
  3. qa_testing_utils-0.0.11/src/qa_testing_utils/__init__.py +88 -0
  4. qa_testing_utils-0.0.11/src/qa_testing_utils/_version.py +1 -0
  5. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/conftest_helpers.py +23 -3
  6. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/exception_utils.py +2 -3
  7. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/file_utils.py +2 -2
  8. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/logger.py +53 -31
  9. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/matchers.py +37 -9
  10. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/object_utils.py +29 -10
  11. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/stream_utils.py +0 -1
  12. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/string_utils.py +7 -7
  13. qa_testing_utils-0.0.11/src/qa_testing_utils/thread_utils.py +60 -0
  14. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/tuple_utils.py +19 -4
  15. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/assertion_tests.py +7 -1
  16. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/file_utils_tests.py +3 -1
  17. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/logger_tests.py +1 -1
  18. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/matchers_tests.py +3 -2
  19. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/self_tests.py +11 -5
  20. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/thread_utils_tests.py +2 -1
  21. qa_testing_utils-0.0.10/src/qa_testing_utils/__init__.py +0 -1
  22. qa_testing_utils-0.0.10/src/qa_testing_utils/thread_utils.py +0 -32
  23. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/README.md +0 -0
  24. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/exceptions.py +0 -0
  25. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/src/qa_testing_utils/logging.ini +0 -0
  26. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/__init__.py +0 -0
  27. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/exception_utils_tests.py +0 -0
  28. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/object_utils_tests.py +0 -0
  29. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/stream_utils_tests.py +0 -0
  30. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/string_utils_tests.py +1 -1
  31. {qa_testing_utils-0.0.10 → qa_testing_utils-0.0.11}/tests/tuple_utils_tests.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qa-testing-utils
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: QA testing utilities
5
5
  Author-Email: Adrian Herscu <adrian.herscu@gmail.com>
6
6
  License: Apache-2.0
@@ -3,7 +3,7 @@ distribution = true
3
3
 
4
4
  [tool.pdm.version]
5
5
  source = "scm"
6
- write_to = "qa_testing_utils/__init__.py"
6
+ write_to = "qa_testing_utils/_version.py"
7
7
  write_template = "__version__ = '{}'"
8
8
  fallback_version = "0.0.0"
9
9
 
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "more-itertools==10.7.0",
32
32
  "returns==0.25.0",
33
33
  ]
34
- version = "0.0.10"
34
+ version = "0.0.11"
35
35
 
36
36
  [project.license]
37
37
  text = "Apache-2.0"
@@ -0,0 +1,88 @@
1
+ # mkinit: start preserve
2
+ from ._version import __version__ # isort: skip
3
+ # mkinit: end preserve
4
+
5
+ from qa_testing_utils.conftest_helpers import (
6
+ configure,
7
+ get_test_body,
8
+ makereport,
9
+ )
10
+ from qa_testing_utils.exception_utils import (
11
+ safely,
12
+ swallow,
13
+ )
14
+ from qa_testing_utils.exceptions import (
15
+ TestException,
16
+ )
17
+ from qa_testing_utils.file_utils import (
18
+ IterableReader,
19
+ crc32_of,
20
+ decompress_xz_stream,
21
+ extract_files_from_tar,
22
+ read_lines,
23
+ stream_file,
24
+ write_csv,
25
+ )
26
+ from qa_testing_utils.logger import (
27
+ Context,
28
+ LoggerMixin,
29
+ logger,
30
+ trace,
31
+ )
32
+ from qa_testing_utils.matchers import (
33
+ ContainsStringIgnoringCase,
34
+ IsIteratorYielding,
35
+ IsIteratorYieldingAll,
36
+ IsStreamContainingEvery,
37
+ IsWithinDates,
38
+ TracingMatcher,
39
+ adapted_iterator,
40
+ adapted_object,
41
+ adapted_sequence,
42
+ contains_string_ignoring_case,
43
+ match_as,
44
+ tracing,
45
+ within_dates,
46
+ yields_every,
47
+ yields_item,
48
+ yields_items,
49
+ )
50
+ from qa_testing_utils.object_utils import (
51
+ ImmutableMixin,
52
+ InvalidValueException,
53
+ SingletonBase,
54
+ SingletonMeta,
55
+ ToDictMixin,
56
+ Valid,
57
+ WithMixin,
58
+ classproperty,
59
+ require_not_none,
60
+ valid,
61
+ )
62
+ from qa_testing_utils.stream_utils import (
63
+ process_next,
64
+ )
65
+ from qa_testing_utils.string_utils import (
66
+ to_string,
67
+ )
68
+ from qa_testing_utils.thread_utils import (
69
+ ThreadLocal,
70
+ sleep_for,
71
+ )
72
+ from qa_testing_utils.tuple_utils import (
73
+ FromTupleMixin,
74
+ )
75
+
76
+ __all__ = ['ContainsStringIgnoringCase', 'Context', 'FromTupleMixin',
77
+ 'ImmutableMixin', 'InvalidValueException', 'IsIteratorYielding',
78
+ 'IsIteratorYieldingAll', 'IsStreamContainingEvery', 'IsWithinDates',
79
+ 'IterableReader', 'LoggerMixin', 'SingletonBase', 'SingletonMeta',
80
+ 'TestException', 'ThreadLocal', 'ToDictMixin', 'TracingMatcher',
81
+ 'Valid', 'WithMixin', 'adapted_iterator', 'adapted_object',
82
+ 'adapted_sequence', 'classproperty', 'configure',
83
+ 'contains_string_ignoring_case', 'crc32_of', 'decompress_xz_stream',
84
+ 'extract_files_from_tar', 'get_test_body', 'logger', 'makereport',
85
+ 'match_as', 'process_next', 'read_lines', 'require_not_none',
86
+ 'safely', 'sleep_for', 'stream_file', 'swallow', 'to_string',
87
+ 'trace', 'tracing', 'valid', 'within_dates', 'write_csv',
88
+ 'yields_every', 'yields_item', 'yields_items']
@@ -0,0 +1 @@
1
+ __version__ = '0.0.11'
@@ -4,9 +4,8 @@
4
4
 
5
5
  import inspect
6
6
  import logging.config
7
- from pathlib import Path
8
7
  import sys
9
- from typing import Callable, Optional
8
+ from pathlib import Path
10
9
 
11
10
  import pytest
12
11
 
@@ -15,6 +14,10 @@ def configure(config: pytest.Config,
15
14
  path: Path = Path(__file__).parent / "logging.ini") -> None:
16
15
  """
17
16
  Configures logging for pytest using a specified INI file, or defaults to internal logging.ini.
17
+
18
+ Args:
19
+ config (pytest.Config): The pytest configuration object.
20
+ path (Path, optional): Path to the logging configuration file. Defaults to 'logging.ini' in the current directory.
18
21
  """
19
22
  caller_module = inspect.getmodule(inspect.stack()[1][0])
20
23
  module_name = caller_module.__name__ if caller_module else "unknown"
@@ -28,6 +31,15 @@ def configure(config: pytest.Config,
28
31
 
29
32
  def makereport(
30
33
  item: pytest.Item, call: pytest.CallInfo[None]) -> pytest.TestReport:
34
+ """
35
+ Creates a pytest test report and appends the test body source code to the report sections.
36
+
37
+ Args:
38
+ item (pytest.Item): The pytest test item.
39
+ call (pytest.CallInfo[None]): The call information for the test.
40
+ Returns:
41
+ pytest.TestReport: The generated test report with the test body included.
42
+ """
31
43
  report = pytest.TestReport.from_item_and_call(item, call)
32
44
 
33
45
  if call.when == "call":
@@ -37,7 +49,15 @@ def makereport(
37
49
 
38
50
 
39
51
  def get_test_body(item: pytest.Item) -> str:
40
- function: Optional[Callable[..., None]] = getattr(item, 'function', None)
52
+ """
53
+ Retrieves the source code of the test function for the given pytest item.
54
+
55
+ Args:
56
+ item (pytest.Item): The pytest test item.
57
+ Returns:
58
+ str: The source code of the test function, or an error message if unavailable.
59
+ """
60
+ function = getattr(item, 'function', None)
41
61
  if function is None:
42
62
  return "No function found for this test item."
43
63
 
@@ -6,8 +6,8 @@ import functools
6
6
  import logging
7
7
  from typing import Any, Callable
8
8
 
9
- from returns.maybe import Maybe, Nothing, Some
10
9
  from qa_testing_utils.stream_utils import Supplier
10
+ from returns.maybe import Maybe, Nothing, Some
11
11
 
12
12
 
13
13
  def safely[T](supplier: Supplier[T]) -> Maybe[T]:
@@ -24,8 +24,7 @@ def safely[T](supplier: Supplier[T]) -> Maybe[T]:
24
24
  Maybe[T]: The result wrapped in Maybe, or Nothing if an exception occurs.
25
25
  """
26
26
  try:
27
- result = supplier()
28
- return Some(result)
27
+ return Some(supplier())
29
28
  except Exception as e:
30
29
  logging.exception(f"Exception occurred: {e}")
31
30
  return Nothing
@@ -13,9 +13,9 @@ from zlib import crc32
13
13
  from more_itertools import peekable
14
14
  from qa_testing_utils.logger import *
15
15
  from qa_testing_utils.object_utils import *
16
- from qa_testing_utils.string_utils import *
16
+ from qa_testing_utils.string_utils import DOT, EMPTY_BYTES, SPACE, UTF_8
17
17
 
18
- LAUNCHING_DIR = Path.cwd()
18
+ LAUNCHING_DIR: Final[Path] = Path.cwd()
19
19
 
20
20
 
21
21
  @final
@@ -2,37 +2,49 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from dataclasses import dataclass
6
5
  import inspect
7
6
  import logging
7
+ from dataclasses import dataclass
8
8
  from functools import cached_property, wraps
9
- from typing import Callable, ClassVar, ParamSpec, TypeVar, cast, final
9
+ from typing import Callable, ClassVar, Final, ParamSpec, TypeVar, cast, final
10
10
 
11
11
  import allure
12
12
  from qa_testing_utils.object_utils import classproperty
13
13
  from qa_testing_utils.string_utils import EMPTY_STRING, LF
14
14
  from qa_testing_utils.thread_utils import ThreadLocal
15
15
 
16
- P = ParamSpec('P')
17
- R = TypeVar('R')
16
+ _P = ParamSpec('_P')
17
+ _R = TypeVar('_R')
18
+
18
19
 
19
20
  @dataclass
21
+ @final
20
22
  class Context:
21
23
  """Per-thread context for reporting and logging, allowing dynamic formatting of messages."""
22
- _local: ClassVar[ThreadLocal['Context']]
23
- _context_fn: Callable[[str], str]
24
+ _THREAD_LOCAL: ClassVar[ThreadLocal['Context']]
25
+ _formatter: Final[Callable[[str], str]]
26
+
27
+ @classmethod
28
+ def default(cls) -> "Context":
29
+ """
30
+ Returns a default Context instance with a no-op formatter.
31
+
32
+ Returns:
33
+ Context: A Context instance with the identity formatter.
34
+ """
35
+ return cls(lambda _: _) # no formatter
24
36
 
25
37
  @classproperty
26
- def _apply(cls) -> Callable[[str], str]:
27
- return Context._local.get()._context_fn
38
+ def _format(cls) -> Callable[[str], str]:
39
+ return cls._THREAD_LOCAL.get()._formatter
28
40
 
29
- @staticmethod
30
- def set(context_fn: Callable[[str], str]) -> None:
41
+ @classmethod
42
+ def set(cls, context_fn: Callable[[str], str]) -> None:
31
43
  """Sets per-thread context function to be used for formatting report and log messages."""
32
- return Context._local.set(Context(context_fn))
33
-
34
- @staticmethod
35
- def traced(func: Callable[P, R]) -> Callable[P, R]:
44
+ cls._THREAD_LOCAL.set(Context(context_fn))
45
+
46
+ @classmethod
47
+ def traced(cls, func: Callable[_P, _R]) -> Callable[_P, _R]:
36
48
  """
37
49
  Decorator to log function entry, arguments, and return value at DEBUG level.
38
50
 
@@ -53,7 +65,7 @@ class Context:
53
65
  Callable[P, R]: The result of the function call.
54
66
  """
55
67
  @wraps(func)
56
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
68
+ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
57
69
  # NOTE: each time a decorated function is called this logic will be
58
70
  # re-evaluated.
59
71
  signature = inspect.signature(func)
@@ -63,23 +75,24 @@ class Context:
63
75
  instance = args[0]
64
76
  logger = logging.getLogger(f"{instance.__class__.__name__}")
65
77
  logger.debug(f">>> "
66
- + Context._apply(
67
- f"{func.__name__} "
68
- f"{", ".join([str(arg) for arg in args[1:]])} "
69
- f"{LF.join(
70
- f"{key}={str(value)}"
71
- for key, value in kwargs.items()) if kwargs else EMPTY_STRING}"))
78
+ + cls._format(
79
+ f"{func.__name__} "
80
+ f"{", ".join([str(arg) for arg in args[1:]])} "
81
+ f"{LF.join(
82
+ f"{key}={str(value)}"
83
+ for key, value in kwargs.items()) if kwargs else EMPTY_STRING}"))
72
84
 
73
85
  with allure.step( # type: ignore
74
- Context._apply(
86
+ cls._format(
75
87
  f"{func.__name__} "
76
88
  f"{', '.join([str(arg) for arg in args[1:]])}")):
77
89
  result = func(*args, **kwargs)
78
90
 
79
91
  if result == instance:
80
- logger.debug(f"<<< " + Context._apply(f"{func.__name__}"))
92
+ logger.debug(f"<<< " + cls._format(f"{func.__name__}"))
81
93
  else:
82
- logger.debug(f"<<< " + Context._apply(f"{func.__name__} {result}"))
94
+ logger.debug(
95
+ f"<<< " + cls._format(f"{func.__name__} {result}"))
83
96
 
84
97
  return result
85
98
  else:
@@ -93,7 +106,8 @@ class Context:
93
106
 
94
107
 
95
108
  # NOTE: python does not support static initializers, so we init here.
96
- Context._local = ThreadLocal(Context(lambda _: _)) # type: ignore
109
+ Context._THREAD_LOCAL = ThreadLocal(Context.default()) # type: ignore
110
+
97
111
 
98
112
  def trace[T](value: T) -> T:
99
113
  """Logs at debug level using the invoking module name as the logger."""
@@ -118,13 +132,16 @@ def trace[T](value: T) -> T:
118
132
 
119
133
  def logger[T:type](cls: T) -> T:
120
134
  """
121
- Class decorator that injects a logger into annotated class.
135
+ Class decorator that injects a logger into the decorated class.
136
+
137
+ Adds a `log` property to the class, providing a logger named after the class.
138
+ Useful for adding logging to any class without boilerplate.
122
139
 
123
140
  Args:
124
- cls (type): automatically provided by the runtime
141
+ cls (type): The class to decorate.
125
142
 
126
143
  Returns:
127
- _type_: the decorated class
144
+ type: The decorated class with a `log` property.
128
145
  """
129
146
  cls._logger = logging.getLogger(cls.__name__)
130
147
 
@@ -152,6 +169,12 @@ class LoggerMixin:
152
169
  @final
153
170
  @cached_property
154
171
  def log(self) -> logging.Logger:
172
+ """
173
+ Returns a logger named after the class.
174
+
175
+ Returns:
176
+ logging.Logger: The logger instance for this class.
177
+ """
155
178
  return logging.getLogger(self.__class__.__name__)
156
179
 
157
180
  @final
@@ -164,14 +187,13 @@ class LoggerMixin:
164
187
  then.eventually_assert_that(
165
188
  lambda: self.trace(...call some API...),
166
189
  greater_that(0)) \
167
-
168
190
  .and_....other verifications may follow...
169
191
 
170
192
  Args:
171
- value (T): the value
193
+ value (T): The value to log.
172
194
 
173
195
  Returns:
174
- T: the value
196
+ T: The value (unchanged).
175
197
  """
176
198
  self.log.debug(f"=== {value}")
177
199
  return value
@@ -3,20 +3,35 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  from datetime import date, datetime
6
- from typing import (Any, Callable, Iterable, Iterator, List, Optional, Sequence,
7
- Union, cast, final, override)
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Iterable,
10
+ Iterator,
11
+ List,
12
+ Optional,
13
+ Sequence,
14
+ Union,
15
+ cast,
16
+ final,
17
+ override,
18
+ )
8
19
 
9
20
  from hamcrest.core.base_matcher import BaseMatcher
10
21
  from hamcrest.core.description import Description
11
22
  from hamcrest.core.helpers.wrap_matcher import wrap_matcher
12
23
  from hamcrest.core.matcher import Matcher
13
-
14
24
  from qa_testing_utils.logger import LoggerMixin
15
25
 
16
26
 
17
27
  class TracingMatcher[T](BaseMatcher[T], LoggerMixin):
18
28
  """
19
29
  A matcher wrapper that adds debug logging around another matcher.
30
+
31
+ Logs the result of each match attempt using the class logger.
32
+
33
+ Args:
34
+ matcher (Matcher[T]): The matcher to wrap and trace.
20
35
  """
21
36
 
22
37
  def __init__(self, matcher: Matcher[T]) -> None:
@@ -31,17 +46,30 @@ class TracingMatcher[T](BaseMatcher[T], LoggerMixin):
31
46
  self._matcher.describe_to(description)
32
47
 
33
48
 
34
- def traced[T](matcher: Matcher[T]) -> TracingMatcher[T]:
49
+ def tracing[T](matcher: Matcher[T]) -> TracingMatcher[T]:
35
50
  """
36
- Wraps a matcher with TraceMatcher to enable debug logging.
51
+ Wraps a matcher with TracingMatcher to enable debug logging.
37
52
 
38
53
  Usage:
39
- assert_that(actual, trace(contains_string("hello")))
54
+ assert_that(actual, traced(contains_string("hello")))
55
+
56
+ Args:
57
+ matcher (Matcher[T]): The matcher to wrap.
58
+ Returns:
59
+ TracingMatcher[T]: The wrapped matcher with tracing enabled.
40
60
  """
41
61
  return TracingMatcher(matcher)
42
62
 
43
63
 
64
+ @final
44
65
  class ContainsStringIgnoringCase(BaseMatcher[str]):
66
+ """
67
+ Matcher that checks if a string contains a given substring, ignoring case.
68
+
69
+ Args:
70
+ substring (str): The substring to search for (case-insensitive).
71
+ """
72
+
45
73
  def __init__(self, substring: str) -> None:
46
74
  self.substring: str = substring.lower()
47
75
 
@@ -147,9 +175,10 @@ class IsIteratorYieldingAll[T](BaseMatcher[Iterator[T]]):
147
175
  description.append_description_of(matcher)
148
176
 
149
177
 
150
- DateOrDateTime = Union[date, datetime]
178
+ type DateOrDateTime = Union[date, datetime]
151
179
 
152
180
 
181
+ @final
153
182
  class IsWithinDates(BaseMatcher[DateOrDateTime]):
154
183
  def __init__(
155
184
  self, start_date: Optional[DateOrDateTime],
@@ -255,8 +284,7 @@ def yields_items[T](matches: Iterable[Union[Matcher[T],
255
284
  This matcher will iterate through the evaluated iterator and check if it yields
256
285
  at least one instance of each specified matcher or value.
257
286
  """
258
- wrapped_matchers = [wrap_matcher(match) for match in matches]
259
- return IsIteratorYieldingAll(wrapped_matchers)
287
+ return IsIteratorYieldingAll([wrap_matcher(match) for match in matches])
260
288
 
261
289
 
262
290
  def adapted_object[T, R](
@@ -2,12 +2,19 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from typing import Callable, Any
6
5
  import threading
7
6
  from dataclasses import asdict, fields, is_dataclass, replace
8
7
  from enum import Enum
9
- from typing import (Any, Dict, Optional, Protocol,
10
- final, runtime_checkable, ClassVar)
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ ClassVar,
12
+ Dict,
13
+ Optional,
14
+ Protocol,
15
+ final,
16
+ runtime_checkable,
17
+ )
11
18
 
12
19
 
13
20
  @runtime_checkable
@@ -159,6 +166,7 @@ class ToDictMixin:
159
166
  return flat_dict
160
167
 
161
168
 
169
+ @final
162
170
  class SingletonMeta(type):
163
171
  """
164
172
  Thread-safe singleton metaclass.
@@ -201,16 +209,16 @@ class InvalidValueException(ValueError):
201
209
 
202
210
  def valid[T:Valid](value: T) -> T:
203
211
  """
204
- Validates specified object, assuming that it supports the Valid protocol.
212
+ Validates the specified object, assuming it supports the Valid protocol.
205
213
 
206
214
  Args:
207
- value (T:Valid): the object
215
+ value (T:Valid): The object to validate.
208
216
 
209
217
  Raises:
210
- InvalidValueException: if the object is invalid
218
+ InvalidValueException: If the object is invalid (is_valid() returns False).
211
219
 
212
220
  Returns:
213
- T:Valid: the validated object
221
+ T:Valid: The validated object if valid.
214
222
  """
215
223
  if value.is_valid():
216
224
  return value
@@ -228,18 +236,29 @@ def require_not_none[T](
228
236
  value (Optional[T]): The value to check for None.
229
237
  message (str, optional): The error message to use if value is None. Defaults to "Value must not be None".
230
238
 
231
- Returns:
232
- T: The value, guaranteed to be not None.
233
-
234
239
  Raises:
235
240
  ValueError: If value is None.
241
+
242
+ Returns:
243
+ T: The value, guaranteed to be not None.
236
244
  """
237
245
  if value is None:
238
246
  raise ValueError(message)
239
247
  return value
240
248
 
241
249
 
250
+ @final
242
251
  class classproperty[T]:
252
+ """
253
+ Descriptor for defining class-level properties (like @property but for classes).
254
+
255
+ Example:
256
+ class MyClass:
257
+ @classproperty
258
+ def foo(cls):
259
+ return ...
260
+ """
261
+
243
262
  def __init__(self, fget: Callable[[Any], T]) -> None:
244
263
  self.fget = fget
245
264
 
@@ -4,7 +4,6 @@
4
4
 
5
5
  from typing import Callable, Iterator
6
6
 
7
-
8
7
  """
9
8
  A generic callable type alias representing a supplier of values of type T.
10
9
 
@@ -2,16 +2,16 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from typing import Callable, Type
5
+ from typing import Callable, Final, Literal, Type
6
6
 
7
7
  from ppretty import ppretty # type: ignore
8
8
 
9
- EMPTY_STRING = ""
10
- SPACE = " "
11
- DOT = "."
12
- LF = "\n"
13
- UTF_8 = "utf-8"
14
- EMPTY_BYTES = b''
9
+ EMPTY_STRING: Final[str] = ""
10
+ SPACE: Final[str] = " "
11
+ DOT: Final[str] = "."
12
+ LF: Final[str] = "\n"
13
+ UTF_8: Final[str] = "utf-8"
14
+ EMPTY_BYTES: Final[Literal[b'']] = b''
15
15
 
16
16
 
17
17
  def to_string[T](indent: str = ' ',
@@ -0,0 +1,60 @@
1
+ # SPDX-FileCopyrightText: 2025 Adrian Herscu
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import concurrent.futures
6
+ import time
7
+ from datetime import timedelta
8
+ from threading import local
9
+ from typing import Final, Optional, cast
10
+
11
+ COMMON_EXECUTOR: Final[concurrent.futures.ThreadPoolExecutor] = concurrent.futures.ThreadPoolExecutor()
12
+ """
13
+ A shared thread pool executor for concurrent tasks across the application.
14
+ """
15
+
16
+
17
+ def sleep_for(duration: timedelta):
18
+ """
19
+ Sleep for the specified duration.
20
+
21
+ Args:
22
+ duration (timedelta): The amount of time to sleep.
23
+ """
24
+ time.sleep(duration.total_seconds())
25
+
26
+
27
+ class ThreadLocal[T]:
28
+ """
29
+ Thread-local storage for a value, with a default initializer.
30
+
31
+ Provides per-thread storage for a value of type T, initialized with a default.
32
+ """
33
+
34
+ def __init__(self, default: Optional[T] = None):
35
+ """
36
+ Initializes the thread-local storage with a default value.
37
+
38
+ Args:
39
+ default (Optional[T]): The default value for each thread, None if not specified.
40
+ """
41
+ self._local = local()
42
+ self._local.value = default
43
+
44
+ def set(self, value: T) -> None:
45
+ """
46
+ Sets the thread-local value for the current thread.
47
+
48
+ Args:
49
+ value (T): The value to set for the current thread.
50
+ """
51
+ self._local.value = value
52
+
53
+ def get(self) -> T:
54
+ """
55
+ Gets the thread-local value for the current thread.
56
+
57
+ Returns:
58
+ T: The value for the current thread.
59
+ """
60
+ return cast(T, self._local.value)
@@ -2,19 +2,34 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from dataclasses import is_dataclass, replace, fields
5
+ from dataclasses import fields, is_dataclass, replace
6
6
  from typing import Any, Self, Tuple, Type
7
7
 
8
8
 
9
9
  class FromTupleMixin:
10
10
  """
11
- Class decorator adding a `from_tuple` method allowing instantiation from
12
- a tuple matching the order of decorated class fields.
11
+ Mixin that adds a `from_tuple` class method for instantiating objects from a tuple.
13
12
 
14
- Works with frozen dataclasses too.
13
+ Allows creating an instance of a class (dataclass or regular class) by passing a tuple
14
+ whose values match the order of the class fields. Works with frozen dataclasses as well.
15
+
16
+ Example:
17
+ @dataclass(frozen=True)
18
+ class Point(FromTupleMixin):
19
+ x: int
20
+ y: int
21
+ p = Point.from_tuple((1, 2))
15
22
  """
16
23
  @classmethod
17
24
  def from_tuple(cls: Type[Self], data: Tuple[Any, ...]) -> Self:
25
+ """
26
+ Instantiates the class from a tuple of values, matching the order of class fields.
27
+
28
+ Args:
29
+ data (Tuple[Any, ...]): Tuple of values corresponding to the class fields.
30
+ Returns:
31
+ Self: An instance of the class with fields set from the tuple.
32
+ """
18
33
  if is_dataclass(cls):
19
34
  # Retrieve all fields, including inherited ones
20
35
  cls_fields = [f.name for f in fields(cls)]
@@ -7,7 +7,13 @@ from random import randint
7
7
  from typing import Callable
8
8
 
9
9
  import pytest
10
- from tenacity import Retrying, before_sleep_log, retry_if_exception_type, stop_after_attempt, wait_exponential
10
+ from tenacity import (
11
+ Retrying,
12
+ before_sleep_log,
13
+ retry_if_exception_type,
14
+ stop_after_attempt,
15
+ wait_exponential,
16
+ )
11
17
 
12
18
 
13
19
  def unstable_function():
@@ -1,5 +1,6 @@
1
1
  import csv
2
2
  from pathlib import Path
3
+
3
4
  from qa_testing_utils.file_utils import *
4
5
 
5
6
 
@@ -70,8 +71,9 @@ def should_decompress_xz_stream():
70
71
 
71
72
 
72
73
  def should_extract_files_from_tar():
73
- import tarfile
74
74
  import io
75
+ import tarfile
76
+
75
77
  # Create a tar archive in memory
76
78
  file_content = b"testdata"
77
79
  tar_bytes = io.BytesIO()
@@ -5,8 +5,8 @@
5
5
  from functools import wraps
6
6
  from typing import Callable, ParamSpec, Self, TypeVar
7
7
 
8
+ from qa_pytest_rabbitmq.queue_handler import to_string
8
9
  from qa_testing_utils.logger import *
9
- from qa_testing_utils.string_utils import *
10
10
 
11
11
 
12
12
  def should_trace():
@@ -8,11 +8,12 @@ from typing import Callable, Union
8
8
  import attr
9
9
  import pytest
10
10
  from functional import seq
11
- from hamcrest import all_of, assert_that, has_item, has_property, is_ # type: ignore -- seq
12
- from qa_testing_utils.string_utils import to_string
11
+ from hamcrest import is_ # type: ignore
12
+ from hamcrest import all_of, assert_that, has_item, has_property
13
13
  from qa_testing_utils.matchers import *
14
14
  from qa_testing_utils.object_utils import *
15
15
  from qa_testing_utils.string_utils import *
16
+ from qa_testing_utils.string_utils import to_string
16
17
 
17
18
 
18
19
  @to_string()
@@ -2,18 +2,24 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from datetime import timedelta
6
5
  import random
6
+ from datetime import timedelta
7
7
  from typing import Any, final
8
8
 
9
9
  import attr
10
- from hamcrest import assert_that, is_ # type: ignore
11
10
  import pytest
12
- from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_fixed
13
- from qa_testing_utils.logger import Context
14
- from qa_testing_utils.logger import *
11
+ from hamcrest import assert_that, is_ # type: ignore
15
12
  from qa_testing_utils.exceptions import *
13
+ from qa_testing_utils.logger import *
14
+ from qa_testing_utils.logger import Context
16
15
  from qa_testing_utils.thread_utils import *
16
+ from tenacity import (
17
+ before_sleep_log,
18
+ retry,
19
+ retry_if_exception_type,
20
+ stop_after_attempt,
21
+ wait_fixed,
22
+ )
17
23
 
18
24
 
19
25
  @final
@@ -2,9 +2,10 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ import time
5
6
  from datetime import timedelta
7
+
6
8
  from qa_testing_utils.thread_utils import sleep_for
7
- import time
8
9
 
9
10
 
10
11
  def should_sleep_for_specified_duration():
@@ -1 +0,0 @@
1
- __version__ = '0.0.10'
@@ -1,32 +0,0 @@
1
- # SPDX-FileCopyrightText: 2025 Adrian Herscu
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- import concurrent.futures
6
- from threading import local
7
- import time
8
- from datetime import timedelta
9
- from typing import cast
10
-
11
- COMMON_EXECUTOR = concurrent.futures.ThreadPoolExecutor()
12
-
13
-
14
- def sleep_for(duration: timedelta):
15
- """
16
- Sleep for the specified duration.
17
- Args:
18
- duration (timedelta): The amount of time to sleep.
19
- """
20
- time.sleep(duration.total_seconds())
21
-
22
-
23
- class ThreadLocal[T]:
24
- def __init__(self, default: T):
25
- self._local = local()
26
- self._local.value = default
27
-
28
- def set(self, value: T) -> None:
29
- self._local.value = value
30
-
31
- def get(self) -> T:
32
- return cast(T, self._local.value)
@@ -2,8 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from enum import Enum
6
5
  import logging
6
+ from enum import Enum
7
7
  from typing import List
8
8
 
9
9
  from qa_testing_utils.string_utils import *