qa-testing-utils 0.0.8__tar.gz → 0.0.10__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 (30) hide show
  1. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/PKG-INFO +3 -3
  2. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/pyproject.toml +3 -3
  3. qa_testing_utils-0.0.10/src/qa_testing_utils/__init__.py +1 -0
  4. qa_testing_utils-0.0.10/src/qa_testing_utils/logger.py +177 -0
  5. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/object_utils.py +9 -0
  6. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/thread_utils.py +14 -0
  7. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/logger_tests.py +2 -2
  8. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/self_tests.py +2 -2
  9. qa_testing_utils-0.0.8/src/qa_testing_utils/__init__.py +0 -1
  10. qa_testing_utils-0.0.8/src/qa_testing_utils/logger.py +0 -155
  11. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/README.md +0 -0
  12. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/conftest_helpers.py +0 -0
  13. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/exception_utils.py +0 -0
  14. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/exceptions.py +0 -0
  15. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/file_utils.py +0 -0
  16. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/logging.ini +0 -0
  17. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/matchers.py +0 -0
  18. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/stream_utils.py +0 -0
  19. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/string_utils.py +0 -0
  20. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/src/qa_testing_utils/tuple_utils.py +0 -0
  21. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/__init__.py +0 -0
  22. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/assertion_tests.py +0 -0
  23. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/exception_utils_tests.py +0 -0
  24. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/file_utils_tests.py +0 -0
  25. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/matchers_tests.py +0 -0
  26. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/object_utils_tests.py +0 -0
  27. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/stream_utils_tests.py +0 -0
  28. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/string_utils_tests.py +0 -0
  29. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/thread_utils_tests.py +0 -0
  30. {qa_testing_utils-0.0.8 → qa_testing_utils-0.0.10}/tests/tuple_utils_tests.py +0 -0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qa-testing-utils
3
- Version: 0.0.8
3
+ Version: 0.0.10
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.3.5
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.2
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
@@ -23,15 +23,15 @@ authors = [
23
23
  readme = "README.md"
24
24
  requires-python = ">=3.13"
25
25
  dependencies = [
26
- "pytest==8.3.5",
26
+ "pytest==8.4.0",
27
27
  "PyHamcrest==2.1.0",
28
28
  "pyfunctional==1.5.0",
29
29
  "ppretty==1.3",
30
- "allure-pytest==2.14.2",
30
+ "allure-pytest==2.14.3",
31
31
  "more-itertools==10.7.0",
32
32
  "returns==0.25.0",
33
33
  ]
34
- version = "0.0.8"
34
+ version = "0.0.10"
35
35
 
36
36
  [project.license]
37
37
  text = "Apache-2.0"
@@ -0,0 +1 @@
1
+ __version__ = '0.0.10'
@@ -0,0 +1,177 @@
1
+ # SPDX-FileCopyrightText: 2025 Adrian Herscu
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from dataclasses import dataclass
6
+ import inspect
7
+ import logging
8
+ from functools import cached_property, wraps
9
+ from typing import Callable, ClassVar, ParamSpec, TypeVar, cast, final
10
+
11
+ import allure
12
+ from qa_testing_utils.object_utils import classproperty
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
+ @dataclass
20
+ class Context:
21
+ """Per-thread context for reporting and logging, allowing dynamic formatting of messages."""
22
+ _local: ClassVar[ThreadLocal['Context']]
23
+ _context_fn: Callable[[str], str]
24
+
25
+ @classproperty
26
+ def _apply(cls) -> Callable[[str], str]:
27
+ return Context._local.get()._context_fn
28
+
29
+ @staticmethod
30
+ def set(context_fn: Callable[[str], str]) -> None:
31
+ """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]:
36
+ """
37
+ Decorator to log function entry, arguments, and return value at DEBUG level.
38
+
39
+ Also adds an Allure step for reporting. Use on methods where tracing is useful
40
+ for debugging or reporting.
41
+
42
+ Example:
43
+ @Context.traced
44
+ def my_method(self, x):
45
+ ...
46
+
47
+ Args:
48
+ func (Callable[P, R]): The function to be decorated.
49
+ *args (Any): Positional arguments to be passed to the function.
50
+ **kwargs (Any): Keyword arguments to be passed to the function.
51
+
52
+ Returns:
53
+ Callable[P, R]: The result of the function call.
54
+ """
55
+ @wraps(func)
56
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
57
+ # NOTE: each time a decorated function is called this logic will be
58
+ # re-evaluated.
59
+ signature = inspect.signature(func)
60
+ parameters = list(signature.parameters.keys())
61
+
62
+ if parameters and parameters[0] == 'self' and len(args) > 0:
63
+ instance = args[0]
64
+ logger = logging.getLogger(f"{instance.__class__.__name__}")
65
+ 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}"))
72
+
73
+ with allure.step( # type: ignore
74
+ Context._apply(
75
+ f"{func.__name__} "
76
+ f"{', '.join([str(arg) for arg in args[1:]])}")):
77
+ result = func(*args, **kwargs)
78
+
79
+ if result == instance:
80
+ logger.debug(f"<<< " + Context._apply(f"{func.__name__}"))
81
+ else:
82
+ logger.debug(f"<<< " + Context._apply(f"{func.__name__} {result}"))
83
+
84
+ return result
85
+ else:
86
+ logger = logging.getLogger(func.__name__)
87
+ logger.debug(f">>> {func.__name__} {args} {kwargs}")
88
+ result = func(*args, **kwargs)
89
+ logger.debug(f"<<< {func.__name__} {result}")
90
+ return result
91
+
92
+ return wrapper
93
+
94
+
95
+ # NOTE: python does not support static initializers, so we init here.
96
+ Context._local = ThreadLocal(Context(lambda _: _)) # type: ignore
97
+
98
+ def trace[T](value: T) -> T:
99
+ """Logs at debug level using the invoking module name as the logger."""
100
+ frame = inspect.currentframe()
101
+ try:
102
+ if frame is not None:
103
+ caller_frame = frame.f_back
104
+ if caller_frame is not None:
105
+ caller_module = inspect.getmodule(caller_frame)
106
+ logger_name = caller_module.__name__ if caller_module else '__main__'
107
+ logger = logging.getLogger(logger_name)
108
+ logger.debug(f"=== {value}")
109
+ else:
110
+ logging.getLogger(__name__).debug(f"=== {value}")
111
+ else:
112
+ logging.getLogger(__name__).debug(f"=== {value}")
113
+ finally:
114
+ del frame
115
+
116
+ return value
117
+
118
+
119
+ def logger[T:type](cls: T) -> T:
120
+ """
121
+ Class decorator that injects a logger into annotated class.
122
+
123
+ Args:
124
+ cls (type): automatically provided by the runtime
125
+
126
+ Returns:
127
+ _type_: the decorated class
128
+ """
129
+ cls._logger = logging.getLogger(cls.__name__)
130
+
131
+ @property
132
+ def log(self: T) -> logging.Logger:
133
+ return cast(logging.Logger, getattr(self, '_logger', None))
134
+
135
+ cls.log = log
136
+
137
+ return cls
138
+
139
+
140
+ class LoggerMixin:
141
+ """
142
+ Mixin that provides a `log` property for convenient class-based logging.
143
+
144
+ Inherit from this mixin to get a `self.log` logger named after the class.
145
+ Useful for adding debug/info/error logging to any class without boilerplate.
146
+
147
+ Example:
148
+ class MyClass(LoggerMixin):
149
+ def do_something(self):
150
+ self.log.info("Doing something")
151
+ """
152
+ @final
153
+ @cached_property
154
+ def log(self) -> logging.Logger:
155
+ return logging.getLogger(self.__class__.__name__)
156
+
157
+ @final
158
+ def trace[T](self, value: T) -> T:
159
+ """
160
+ Logs value at DEBUG level using this logger.
161
+
162
+ Use to log something as a value, usually in a lambda expression::
163
+
164
+ then.eventually_assert_that(
165
+ lambda: self.trace(...call some API...),
166
+ greater_that(0)) \
167
+
168
+ .and_....other verifications may follow...
169
+
170
+ Args:
171
+ value (T): the value
172
+
173
+ Returns:
174
+ T: the value
175
+ """
176
+ self.log.debug(f"=== {value}")
177
+ return value
@@ -2,6 +2,7 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ from typing import Callable, Any
5
6
  import threading
6
7
  from dataclasses import asdict, fields, is_dataclass, replace
7
8
  from enum import Enum
@@ -236,3 +237,11 @@ def require_not_none[T](
236
237
  if value is None:
237
238
  raise ValueError(message)
238
239
  return value
240
+
241
+
242
+ class classproperty[T]:
243
+ def __init__(self, fget: Callable[[Any], T]) -> None:
244
+ self.fget = fget
245
+
246
+ def __get__(self, instance: Any, owner: Any) -> T:
247
+ return self.fget(owner)
@@ -3,8 +3,10 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import concurrent.futures
6
+ from threading import local
6
7
  import time
7
8
  from datetime import timedelta
9
+ from typing import cast
8
10
 
9
11
  COMMON_EXECUTOR = concurrent.futures.ThreadPoolExecutor()
10
12
 
@@ -16,3 +18,15 @@ def sleep_for(duration: timedelta):
16
18
  duration (timedelta): The amount of time to sleep.
17
19
  """
18
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)
@@ -17,7 +17,7 @@ def should_trace():
17
17
 
18
18
  @to_string()
19
19
  class Foo(LoggerMixin):
20
- @traced
20
+ @Context.traced
21
21
  def run(self, message: Message) -> Self:
22
22
  self.log.debug(f"{message}")
23
23
  return self
@@ -85,7 +85,7 @@ def should_return_value_and_log_with_logger_mixin_trace():
85
85
  def should_log_entry_and_exit_with_traced_decorator():
86
86
  calls: list[tuple[int, int]] = []
87
87
 
88
- @traced
88
+ @Context.traced
89
89
  def foo(x: int, y: int) -> int:
90
90
  calls.append((x, y))
91
91
  return x + y
@@ -10,7 +10,7 @@ import attr
10
10
  from hamcrest import assert_that, is_ # type: ignore
11
11
  import pytest
12
12
  from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_fixed
13
- from qa_testing_utils.logger import traced
13
+ from qa_testing_utils.logger import Context
14
14
  from qa_testing_utils.logger import *
15
15
  from qa_testing_utils.exceptions import *
16
16
  from qa_testing_utils.thread_utils import *
@@ -28,7 +28,7 @@ class SelfTests(LoggerMixin):
28
28
  """Test that print statement works (placeholder/self-test)."""
29
29
  print("hello")
30
30
 
31
- @traced
31
+ @Context.traced
32
32
  def should_assert_true(self):
33
33
  """Test that a traced assertion passes (decorator coverage)."""
34
34
  assert True
@@ -1 +0,0 @@
1
- __version__ = '0.0.8'
@@ -1,155 +0,0 @@
1
- # SPDX-FileCopyrightText: 2025 Adrian Herscu
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- import inspect
6
- import logging
7
- from functools import cached_property, wraps
8
- from typing import Callable, ParamSpec, TypeVar, cast, final
9
-
10
- import allure
11
- from qa_testing_utils.string_utils import EMPTY_STRING, LF
12
-
13
-
14
- def trace[T](value: T) -> T:
15
- """Logs at debug level using the invoking module name as the logger."""
16
- frame = inspect.currentframe()
17
- try:
18
- if frame is not None:
19
- caller_frame = frame.f_back
20
- if caller_frame is not None:
21
- caller_module = inspect.getmodule(caller_frame)
22
- logger_name = caller_module.__name__ if caller_module else '__main__'
23
- logger = logging.getLogger(logger_name)
24
- logger.debug(f"=== {value}")
25
- else:
26
- logging.getLogger(__name__).debug(f"=== {value}")
27
- else:
28
- logging.getLogger(__name__).debug(f"=== {value}")
29
- finally:
30
- del frame
31
-
32
- return value
33
-
34
-
35
- def logger[T:type](cls: T) -> T:
36
- """
37
- Class decorator that injects a logger into annotated class.
38
-
39
- Args:
40
- cls (type): automatically provided by the runtime
41
-
42
- Returns:
43
- _type_: the decorated class
44
- """
45
- cls._logger = logging.getLogger(cls.__name__)
46
-
47
- @property
48
- def log(self: T) -> logging.Logger:
49
- return cast(logging.Logger, getattr(self, '_logger', None))
50
-
51
- cls.log = log
52
-
53
- return cls
54
-
55
-
56
- class LoggerMixin:
57
- """
58
- Mixin that provides a `log` property for convenient class-based logging.
59
-
60
- Inherit from this mixin to get a `self.log` logger named after the class.
61
- Useful for adding debug/info/error logging to any class without boilerplate.
62
-
63
- Example:
64
- class MyClass(LoggerMixin):
65
- def do_something(self):
66
- self.log.info("Doing something")
67
- """
68
- @final
69
- @cached_property
70
- def log(self) -> logging.Logger:
71
- return logging.getLogger(self.__class__.__name__)
72
-
73
- @final
74
- def trace[T](self, value: T) -> T:
75
- """
76
- Logs value at DEBUG level using this logger.
77
-
78
- Use to log something as a value, usually in a lambda expression::
79
-
80
- then.eventually_assert_that(
81
- lambda: self.trace(...call some API...),
82
- greater_that(0)) \
83
-
84
- .and_....other verifications may follow...
85
-
86
- Args:
87
- value (T): the value
88
-
89
- Returns:
90
- T: the value
91
- """
92
- self.log.debug(f"=== {value}")
93
- 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