qa-testing-utils 0.0.9__py3-none-any.whl → 0.0.11__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.
- qa_testing_utils/__init__.py +88 -1
- qa_testing_utils/_version.py +1 -0
- qa_testing_utils/conftest_helpers.py +23 -3
- qa_testing_utils/exception_utils.py +2 -3
- qa_testing_utils/file_utils.py +2 -2
- qa_testing_utils/logger.py +113 -69
- qa_testing_utils/matchers.py +37 -9
- qa_testing_utils/object_utils.py +37 -9
- qa_testing_utils/stream_utils.py +0 -1
- qa_testing_utils/string_utils.py +7 -7
- qa_testing_utils/thread_utils.py +43 -1
- qa_testing_utils/tuple_utils.py +19 -4
- {qa_testing_utils-0.0.9.dist-info → qa_testing_utils-0.0.11.dist-info}/METADATA +3 -3
- qa_testing_utils-0.0.11.dist-info/RECORD +18 -0
- qa_testing_utils-0.0.9.dist-info/RECORD +0 -17
- {qa_testing_utils-0.0.9.dist-info → qa_testing_utils-0.0.11.dist-info}/WHEEL +0 -0
- {qa_testing_utils-0.0.9.dist-info → qa_testing_utils-0.0.11.dist-info}/entry_points.txt +0 -0
qa_testing_utils/__init__.py
CHANGED
@@ -1 +1,88 @@
|
|
1
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
qa_testing_utils/file_utils.py
CHANGED
@@ -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
|
qa_testing_utils/logger.py
CHANGED
@@ -4,11 +4,109 @@
|
|
4
4
|
|
5
5
|
import inspect
|
6
6
|
import logging
|
7
|
+
from dataclasses import dataclass
|
7
8
|
from functools import cached_property, wraps
|
8
|
-
from typing import Callable, ParamSpec, TypeVar, cast, final
|
9
|
+
from typing import Callable, ClassVar, Final, ParamSpec, TypeVar, cast, final
|
9
10
|
|
10
11
|
import allure
|
12
|
+
from qa_testing_utils.object_utils import classproperty
|
11
13
|
from qa_testing_utils.string_utils import EMPTY_STRING, LF
|
14
|
+
from qa_testing_utils.thread_utils import ThreadLocal
|
15
|
+
|
16
|
+
_P = ParamSpec('_P')
|
17
|
+
_R = TypeVar('_R')
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
@final
|
22
|
+
class Context:
|
23
|
+
"""Per-thread context for reporting and logging, allowing dynamic formatting of messages."""
|
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
|
36
|
+
|
37
|
+
@classproperty
|
38
|
+
def _format(cls) -> Callable[[str], str]:
|
39
|
+
return cls._THREAD_LOCAL.get()._formatter
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def set(cls, context_fn: Callable[[str], str]) -> None:
|
43
|
+
"""Sets per-thread context function to be used for formatting report and log messages."""
|
44
|
+
cls._THREAD_LOCAL.set(Context(context_fn))
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def traced(cls, func: Callable[_P, _R]) -> Callable[_P, _R]:
|
48
|
+
"""
|
49
|
+
Decorator to log function entry, arguments, and return value at DEBUG level.
|
50
|
+
|
51
|
+
Also adds an Allure step for reporting. Use on methods where tracing is useful
|
52
|
+
for debugging or reporting.
|
53
|
+
|
54
|
+
Example:
|
55
|
+
@Context.traced
|
56
|
+
def my_method(self, x):
|
57
|
+
...
|
58
|
+
|
59
|
+
Args:
|
60
|
+
func (Callable[P, R]): The function to be decorated.
|
61
|
+
*args (Any): Positional arguments to be passed to the function.
|
62
|
+
**kwargs (Any): Keyword arguments to be passed to the function.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Callable[P, R]: The result of the function call.
|
66
|
+
"""
|
67
|
+
@wraps(func)
|
68
|
+
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
69
|
+
# NOTE: each time a decorated function is called this logic will be
|
70
|
+
# re-evaluated.
|
71
|
+
signature = inspect.signature(func)
|
72
|
+
parameters = list(signature.parameters.keys())
|
73
|
+
|
74
|
+
if parameters and parameters[0] == 'self' and len(args) > 0:
|
75
|
+
instance = args[0]
|
76
|
+
logger = logging.getLogger(f"{instance.__class__.__name__}")
|
77
|
+
logger.debug(f">>> "
|
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}"))
|
84
|
+
|
85
|
+
with allure.step( # type: ignore
|
86
|
+
cls._format(
|
87
|
+
f"{func.__name__} "
|
88
|
+
f"{', '.join([str(arg) for arg in args[1:]])}")):
|
89
|
+
result = func(*args, **kwargs)
|
90
|
+
|
91
|
+
if result == instance:
|
92
|
+
logger.debug(f"<<< " + cls._format(f"{func.__name__}"))
|
93
|
+
else:
|
94
|
+
logger.debug(
|
95
|
+
f"<<< " + cls._format(f"{func.__name__} {result}"))
|
96
|
+
|
97
|
+
return result
|
98
|
+
else:
|
99
|
+
logger = logging.getLogger(func.__name__)
|
100
|
+
logger.debug(f">>> {func.__name__} {args} {kwargs}")
|
101
|
+
result = func(*args, **kwargs)
|
102
|
+
logger.debug(f"<<< {func.__name__} {result}")
|
103
|
+
return result
|
104
|
+
|
105
|
+
return wrapper
|
106
|
+
|
107
|
+
|
108
|
+
# NOTE: python does not support static initializers, so we init here.
|
109
|
+
Context._THREAD_LOCAL = ThreadLocal(Context.default()) # type: ignore
|
12
110
|
|
13
111
|
|
14
112
|
def trace[T](value: T) -> T:
|
@@ -34,13 +132,16 @@ def trace[T](value: T) -> T:
|
|
34
132
|
|
35
133
|
def logger[T:type](cls: T) -> T:
|
36
134
|
"""
|
37
|
-
Class decorator that injects a logger into
|
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.
|
38
139
|
|
39
140
|
Args:
|
40
|
-
cls (type):
|
141
|
+
cls (type): The class to decorate.
|
41
142
|
|
42
143
|
Returns:
|
43
|
-
|
144
|
+
type: The decorated class with a `log` property.
|
44
145
|
"""
|
45
146
|
cls._logger = logging.getLogger(cls.__name__)
|
46
147
|
|
@@ -68,6 +169,12 @@ class LoggerMixin:
|
|
68
169
|
@final
|
69
170
|
@cached_property
|
70
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
|
+
"""
|
71
178
|
return logging.getLogger(self.__class__.__name__)
|
72
179
|
|
73
180
|
@final
|
@@ -80,76 +187,13 @@ class LoggerMixin:
|
|
80
187
|
then.eventually_assert_that(
|
81
188
|
lambda: self.trace(...call some API...),
|
82
189
|
greater_that(0)) \
|
83
|
-
|
84
190
|
.and_....other verifications may follow...
|
85
191
|
|
86
192
|
Args:
|
87
|
-
value (T):
|
193
|
+
value (T): The value to log.
|
88
194
|
|
89
195
|
Returns:
|
90
|
-
T:
|
196
|
+
T: The value (unchanged).
|
91
197
|
"""
|
92
198
|
self.log.debug(f"=== {value}")
|
93
199
|
return value
|
94
|
-
|
95
|
-
|
96
|
-
P = ParamSpec('P')
|
97
|
-
R = TypeVar('R')
|
98
|
-
|
99
|
-
|
100
|
-
def traced(func: Callable[P, R]) -> Callable[P, R]:
|
101
|
-
"""
|
102
|
-
Decorator to log function entry, arguments, and return value at DEBUG level.
|
103
|
-
|
104
|
-
Also adds an Allure step for reporting. Use on methods where tracing is useful
|
105
|
-
for debugging or reporting.
|
106
|
-
|
107
|
-
Example:
|
108
|
-
@traced
|
109
|
-
def my_method(self, x):
|
110
|
-
...
|
111
|
-
|
112
|
-
Args:
|
113
|
-
func (Callable[P, R]): The function to be decorated.
|
114
|
-
*args (Any): Positional arguments to be passed to the function.
|
115
|
-
**kwargs (Any): Keyword arguments to be passed to the function.
|
116
|
-
|
117
|
-
Returns:
|
118
|
-
Callable[P, R]: The result of the function call.
|
119
|
-
"""
|
120
|
-
@wraps(func)
|
121
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
122
|
-
# NOTE: each time a decorated function is called this logic will be
|
123
|
-
# re-evaluated.
|
124
|
-
signature = inspect.signature(func)
|
125
|
-
parameters = list(signature.parameters.keys())
|
126
|
-
|
127
|
-
if parameters and parameters[0] == 'self' and len(args) > 0:
|
128
|
-
instance = args[0]
|
129
|
-
logger = logging.getLogger(f"{instance.__class__.__name__}")
|
130
|
-
logger.debug(
|
131
|
-
f">>> {func.__name__} "
|
132
|
-
f"{", ".join([str(arg) for arg in args[1:]])} "
|
133
|
-
f"{LF.join(
|
134
|
-
f"{key}={str(value)}"
|
135
|
-
for key, value in kwargs.items()) if kwargs else EMPTY_STRING}")
|
136
|
-
|
137
|
-
with allure.step( # type: ignore
|
138
|
-
f"{func.__name__} "
|
139
|
-
f"{', '.join([str(arg) for arg in args[1:]])}"):
|
140
|
-
result = func(*args, **kwargs)
|
141
|
-
|
142
|
-
if result == instance:
|
143
|
-
logger.debug(f"<<< {func.__name__}")
|
144
|
-
else:
|
145
|
-
logger.debug(f"<<< {func.__name__} {result}")
|
146
|
-
|
147
|
-
return result
|
148
|
-
else:
|
149
|
-
logger = logging.getLogger(func.__name__)
|
150
|
-
logger.debug(f">>> {func.__name__} {args} {kwargs}")
|
151
|
-
result = func(*args, **kwargs)
|
152
|
-
logger.debug(f"<<< {func.__name__} {result}")
|
153
|
-
return result
|
154
|
-
|
155
|
-
return wrapper
|
qa_testing_utils/matchers.py
CHANGED
@@ -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 (
|
7
|
-
|
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
|
49
|
+
def tracing[T](matcher: Matcher[T]) -> TracingMatcher[T]:
|
35
50
|
"""
|
36
|
-
Wraps a matcher with
|
51
|
+
Wraps a matcher with TracingMatcher to enable debug logging.
|
37
52
|
|
38
53
|
Usage:
|
39
|
-
assert_that(actual,
|
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
|
-
|
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](
|
qa_testing_utils/object_utils.py
CHANGED
@@ -5,8 +5,16 @@
|
|
5
5
|
import threading
|
6
6
|
from dataclasses import asdict, fields, is_dataclass, replace
|
7
7
|
from enum import Enum
|
8
|
-
from typing import (
|
9
|
-
|
8
|
+
from typing import (
|
9
|
+
Any,
|
10
|
+
Callable,
|
11
|
+
ClassVar,
|
12
|
+
Dict,
|
13
|
+
Optional,
|
14
|
+
Protocol,
|
15
|
+
final,
|
16
|
+
runtime_checkable,
|
17
|
+
)
|
10
18
|
|
11
19
|
|
12
20
|
@runtime_checkable
|
@@ -158,6 +166,7 @@ class ToDictMixin:
|
|
158
166
|
return flat_dict
|
159
167
|
|
160
168
|
|
169
|
+
@final
|
161
170
|
class SingletonMeta(type):
|
162
171
|
"""
|
163
172
|
Thread-safe singleton metaclass.
|
@@ -200,16 +209,16 @@ class InvalidValueException(ValueError):
|
|
200
209
|
|
201
210
|
def valid[T:Valid](value: T) -> T:
|
202
211
|
"""
|
203
|
-
Validates specified object, assuming
|
212
|
+
Validates the specified object, assuming it supports the Valid protocol.
|
204
213
|
|
205
214
|
Args:
|
206
|
-
value (T:Valid):
|
215
|
+
value (T:Valid): The object to validate.
|
207
216
|
|
208
217
|
Raises:
|
209
|
-
InvalidValueException:
|
218
|
+
InvalidValueException: If the object is invalid (is_valid() returns False).
|
210
219
|
|
211
220
|
Returns:
|
212
|
-
T:Valid:
|
221
|
+
T:Valid: The validated object if valid.
|
213
222
|
"""
|
214
223
|
if value.is_valid():
|
215
224
|
return value
|
@@ -227,12 +236,31 @@ def require_not_none[T](
|
|
227
236
|
value (Optional[T]): The value to check for None.
|
228
237
|
message (str, optional): The error message to use if value is None. Defaults to "Value must not be None".
|
229
238
|
|
230
|
-
Returns:
|
231
|
-
T: The value, guaranteed to be not None.
|
232
|
-
|
233
239
|
Raises:
|
234
240
|
ValueError: If value is None.
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
T: The value, guaranteed to be not None.
|
235
244
|
"""
|
236
245
|
if value is None:
|
237
246
|
raise ValueError(message)
|
238
247
|
return value
|
248
|
+
|
249
|
+
|
250
|
+
@final
|
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
|
+
|
262
|
+
def __init__(self, fget: Callable[[Any], T]) -> None:
|
263
|
+
self.fget = fget
|
264
|
+
|
265
|
+
def __get__(self, instance: Any, owner: Any) -> T:
|
266
|
+
return self.fget(owner)
|
qa_testing_utils/stream_utils.py
CHANGED
qa_testing_utils/string_utils.py
CHANGED
@@ -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 = ' ',
|
qa_testing_utils/thread_utils.py
CHANGED
@@ -5,14 +5,56 @@
|
|
5
5
|
import concurrent.futures
|
6
6
|
import time
|
7
7
|
from datetime import timedelta
|
8
|
+
from threading import local
|
9
|
+
from typing import Final, Optional, cast
|
8
10
|
|
9
|
-
COMMON_EXECUTOR = concurrent.futures.ThreadPoolExecutor()
|
11
|
+
COMMON_EXECUTOR: Final[concurrent.futures.ThreadPoolExecutor] = concurrent.futures.ThreadPoolExecutor()
|
12
|
+
"""
|
13
|
+
A shared thread pool executor for concurrent tasks across the application.
|
14
|
+
"""
|
10
15
|
|
11
16
|
|
12
17
|
def sleep_for(duration: timedelta):
|
13
18
|
"""
|
14
19
|
Sleep for the specified duration.
|
20
|
+
|
15
21
|
Args:
|
16
22
|
duration (timedelta): The amount of time to sleep.
|
17
23
|
"""
|
18
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)
|
qa_testing_utils/tuple_utils.py
CHANGED
@@ -2,19 +2,34 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
|
5
|
-
from dataclasses import is_dataclass, replace
|
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
|
-
|
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
|
-
|
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)]
|
@@ -1,15 +1,15 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qa-testing-utils
|
3
|
-
Version: 0.0.
|
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
|
7
7
|
Requires-Python: >=3.13
|
8
|
-
Requires-Dist: pytest==8.
|
8
|
+
Requires-Dist: pytest==8.4.0
|
9
9
|
Requires-Dist: PyHamcrest==2.1.0
|
10
10
|
Requires-Dist: pyfunctional==1.5.0
|
11
11
|
Requires-Dist: ppretty==1.3
|
12
|
-
Requires-Dist: allure-pytest==2.14.
|
12
|
+
Requires-Dist: allure-pytest==2.14.3
|
13
13
|
Requires-Dist: more-itertools==10.7.0
|
14
14
|
Requires-Dist: returns==0.25.0
|
15
15
|
Description-Content-Type: text/markdown
|
@@ -0,0 +1,18 @@
|
|
1
|
+
qa_testing_utils-0.0.11.dist-info/METADATA,sha256=TtxoTs7IaCSVV2hynTF-MQcvIM1vI9DCSmjBECTTTIs,480
|
2
|
+
qa_testing_utils-0.0.11.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
3
|
+
qa_testing_utils-0.0.11.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
4
|
+
qa_testing_utils/__init__.py,sha256=1iHT5E3nkIIl4RwihbpM4K-RGPKlsIY4wPkR0_UTeCk,2436
|
5
|
+
qa_testing_utils/_version.py,sha256=BkvGdgIByn5SRGserbTFWSVZfsbl4zCa8iLXMuX-QKg,22
|
6
|
+
qa_testing_utils/conftest_helpers.py,sha256=aOtDrTUp18d0Z40FJE0UT4MFvmn_R5ywf8kuWhlK4As,2133
|
7
|
+
qa_testing_utils/exception_utils.py,sha256=RHrYDDo5TeZi9LXk8XEl_Eg6s91c8tTua9m2qEuNwwo,1316
|
8
|
+
qa_testing_utils/exceptions.py,sha256=_s7es20G9-ET2HeLqU0yhuDAXpnQQs_ecjBmztz94Pk,441
|
9
|
+
qa_testing_utils/file_utils.py,sha256=9aPDY03Uw_lMxZ9Zuhovv-ePbVQhqjiEnY4C-8-vqSg,6486
|
10
|
+
qa_testing_utils/logger.py,sha256=DwaR31GvA16zJGxF2yQuyNeR-3jUg0M3kb5uoa57b9k,6547
|
11
|
+
qa_testing_utils/logging.ini,sha256=ZcCKCnUiRl3IVB0ZK9fe79PsAXEMag7QwRpxmO4yBHE,834
|
12
|
+
qa_testing_utils/matchers.py,sha256=H8SEs-JCqP_ZoPZdJDddMOxYZ9IshWB_zedLM1wVkWI,13030
|
13
|
+
qa_testing_utils/object_utils.py,sha256=yj_mng4sFZEhzX1pJbFXd3xAbA_tinCAfGvP4Tv4qGU,7898
|
14
|
+
qa_testing_utils/stream_utils.py,sha256=1dxDL89ZJE-_UeQSX4p3xLnHqUvZHxP-SvG2roWtUjo,1535
|
15
|
+
qa_testing_utils/string_utils.py,sha256=0ojTg65To8ibNDpqdGFUoAW7veZUDs2qSayuwWvpuFA,2714
|
16
|
+
qa_testing_utils/thread_utils.py,sha256=koOi8aFBQlXDjj_eV_sNM9c5brPJtjQKRP2kY_M5rac,1577
|
17
|
+
qa_testing_utils/tuple_utils.py,sha256=0BbvlzeQFjlSuNvBTi92ybCj2Qxk3RO7l06n5BJ3TVI,2416
|
18
|
+
qa_testing_utils-0.0.11.dist-info/RECORD,,
|
@@ -1,17 +0,0 @@
|
|
1
|
-
qa_testing_utils-0.0.9.dist-info/METADATA,sha256=FEqqeR5_qM1chBwuVr-RtgDyGg3TptUXtK5OLWpqFpU,479
|
2
|
-
qa_testing_utils-0.0.9.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
3
|
-
qa_testing_utils-0.0.9.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
4
|
-
qa_testing_utils/__init__.py,sha256=lZoBoCrPmDHiztV57wRru9CSJWCzBJ4qzAHBIoqWxZQ,21
|
5
|
-
qa_testing_utils/conftest_helpers.py,sha256=Hcpbc1CFNANFfVDyu9Elf5TB5tX4zSHzHSrRhlsHGsM,1408
|
6
|
-
qa_testing_utils/exception_utils.py,sha256=iPa-EE1gvKLxzEB3KzNMWGHaS7xEH_B3Yxd8KWiMROA,1340
|
7
|
-
qa_testing_utils/exceptions.py,sha256=_s7es20G9-ET2HeLqU0yhuDAXpnQQs_ecjBmztz94Pk,441
|
8
|
-
qa_testing_utils/file_utils.py,sha256=a6VPIbSZQVuOEnrprzlj-YTtfMskNzuClnhECRN2CPw,6444
|
9
|
-
qa_testing_utils/logger.py,sha256=840RhoO5AHewqEBNa6A2WbWb8DF-AFrasMPDSPH6JNM,4625
|
10
|
-
qa_testing_utils/logging.ini,sha256=ZcCKCnUiRl3IVB0ZK9fe79PsAXEMag7QwRpxmO4yBHE,834
|
11
|
-
qa_testing_utils/matchers.py,sha256=WOPqtCPt5tFdn6JpyDDqPfPSHQvRMpv0uXCv6-2IqNE,12558
|
12
|
-
qa_testing_utils/object_utils.py,sha256=CNbRVB3RAmjeGDRQe9cDsSF_iuZY6vENvZj2pwB_mS0,7393
|
13
|
-
qa_testing_utils/stream_utils.py,sha256=zYa2UDfrWsXwRP6nrG87uGkkUZPBLATHXBgu7GUk3Aw,1536
|
14
|
-
qa_testing_utils/string_utils.py,sha256=tU1VfmzcS_Zqj4hqzdr6kWdM5dpdlRhiBNiwJAbBDVg,2617
|
15
|
-
qa_testing_utils/thread_utils.py,sha256=3Ecyg-bnkcPyT_r9xVWS79uv1BTgXxeIJJZhT3tXorM,416
|
16
|
-
qa_testing_utils/tuple_utils.py,sha256=pIcJntr-PNvaOIP0Pv4sBwO7oIbTVFmGwr9Ic5nJDA0,1851
|
17
|
-
qa_testing_utils-0.0.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|