winipedia-utils 0.1.0__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.
Files changed (64) hide show
  1. winipedia_utils/__init__.py +1 -0
  2. winipedia_utils/concurrent/__init__.py +1 -0
  3. winipedia_utils/concurrent/concurrent.py +242 -0
  4. winipedia_utils/concurrent/multiprocessing.py +115 -0
  5. winipedia_utils/concurrent/multithreading.py +93 -0
  6. winipedia_utils/consts.py +22 -0
  7. winipedia_utils/data/__init__.py +1 -0
  8. winipedia_utils/data/dataframe.py +7 -0
  9. winipedia_utils/django/__init__.py +27 -0
  10. winipedia_utils/django/bulk.py +536 -0
  11. winipedia_utils/django/command.py +334 -0
  12. winipedia_utils/django/database.py +304 -0
  13. winipedia_utils/git/__init__.py +1 -0
  14. winipedia_utils/git/gitignore.py +80 -0
  15. winipedia_utils/git/pre_commit/__init__.py +1 -0
  16. winipedia_utils/git/pre_commit/config.py +60 -0
  17. winipedia_utils/git/pre_commit/hooks.py +109 -0
  18. winipedia_utils/git/pre_commit/run_hooks.py +49 -0
  19. winipedia_utils/iterating/__init__.py +1 -0
  20. winipedia_utils/iterating/iterate.py +29 -0
  21. winipedia_utils/logging/__init__.py +1 -0
  22. winipedia_utils/logging/ansi.py +6 -0
  23. winipedia_utils/logging/config.py +64 -0
  24. winipedia_utils/logging/logger.py +26 -0
  25. winipedia_utils/modules/__init__.py +1 -0
  26. winipedia_utils/modules/class_.py +76 -0
  27. winipedia_utils/modules/function.py +86 -0
  28. winipedia_utils/modules/module.py +361 -0
  29. winipedia_utils/modules/package.py +350 -0
  30. winipedia_utils/oop/__init__.py +1 -0
  31. winipedia_utils/oop/mixins/__init__.py +1 -0
  32. winipedia_utils/oop/mixins/meta.py +315 -0
  33. winipedia_utils/oop/mixins/mixin.py +28 -0
  34. winipedia_utils/os/__init__.py +1 -0
  35. winipedia_utils/os/os.py +61 -0
  36. winipedia_utils/projects/__init__.py +1 -0
  37. winipedia_utils/projects/poetry/__init__.py +1 -0
  38. winipedia_utils/projects/poetry/config.py +91 -0
  39. winipedia_utils/projects/poetry/poetry.py +30 -0
  40. winipedia_utils/setup.py +36 -0
  41. winipedia_utils/testing/__init__.py +1 -0
  42. winipedia_utils/testing/assertions.py +23 -0
  43. winipedia_utils/testing/convention.py +177 -0
  44. winipedia_utils/testing/create_tests.py +286 -0
  45. winipedia_utils/testing/fixtures.py +28 -0
  46. winipedia_utils/testing/tests/__init__.py +1 -0
  47. winipedia_utils/testing/tests/base/__init__.py +1 -0
  48. winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -0
  49. winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -0
  50. winipedia_utils/testing/tests/base/fixtures/scopes/__init__.py +1 -0
  51. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -0
  52. winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -0
  53. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +31 -0
  54. winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -0
  55. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +224 -0
  56. winipedia_utils/testing/tests/base/utils/__init__.py +1 -0
  57. winipedia_utils/testing/tests/base/utils/utils.py +82 -0
  58. winipedia_utils/testing/tests/conftest.py +26 -0
  59. winipedia_utils/text/__init__.py +1 -0
  60. winipedia_utils/text/string.py +126 -0
  61. winipedia_utils-0.1.0.dist-info/LICENSE +21 -0
  62. winipedia_utils-0.1.0.dist-info/METADATA +350 -0
  63. winipedia_utils-0.1.0.dist-info/RECORD +64 -0
  64. winipedia_utils-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.oop."""
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.oop.mixins."""
@@ -0,0 +1,315 @@
1
+ """Metaclass utilities for class behavior modification and enforcement.
2
+
3
+ This module provides metaclasses that can be used to
4
+ modify class behavior at creation time.
5
+ These metaclasses can be used individually or combined to create classes
6
+ with enhanced capabilities and stricter implementation requirements.
7
+
8
+ """
9
+
10
+ import time
11
+ from abc import ABCMeta, abstractmethod
12
+ from collections.abc import Callable
13
+ from functools import wraps
14
+ from typing import Any, final
15
+
16
+ from winipedia_utils.logging.logger import get_logger
17
+ from winipedia_utils.modules.class_ import get_all_methods_from_cls
18
+ from winipedia_utils.modules.function import is_func
19
+ from winipedia_utils.text.string import value_to_truncated_string
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class LoggingMeta(type):
25
+ """Metaclass that automatically adds logging to class methods.
26
+
27
+ Wraps non-magic methods with a logging decorator that tracks method calls,
28
+ arguments, execution time, and return values. Includes rate limiting to
29
+ prevent log flooding.
30
+ """
31
+
32
+ def __new__(
33
+ mcs: type["LoggingMeta"],
34
+ name: str,
35
+ bases: tuple[type, ...],
36
+ dct: dict[str, Any],
37
+ ) -> "LoggingMeta":
38
+ """Create a new class with logging-wrapped methods.
39
+
40
+ Args:
41
+ mcs: The metaclass instance
42
+ name: The name of the class being created
43
+ bases: The base classes of the class being created
44
+ dct: The attribute dictionary of the class being created
45
+
46
+ Returns:
47
+ A new class with logging functionality added to its methods
48
+
49
+ """
50
+ # Wrap all callables of the class with a logging wrapper
51
+
52
+ for attr_name, attr_value in dct.items():
53
+ if mcs.is_loggable_method(attr_value):
54
+ dct[attr_name] = mcs.wrap_with_logging(
55
+ func=attr_value, class_name=name, call_times={}
56
+ )
57
+
58
+ return super().__new__(mcs, name, bases, dct)
59
+
60
+ @staticmethod
61
+ def is_loggable_method(method: Callable[..., Any]) -> bool:
62
+ """Determine if a method should have logging applied.
63
+
64
+ Args:
65
+ method: The method to check
66
+
67
+ Returns:
68
+ True if the method should be wrapped with logging, False otherwise
69
+
70
+ """
71
+ return (
72
+ is_func(method) # must be a method-like attribute
73
+ and not method.__name__.startswith("__") # must not be a magic method
74
+ )
75
+
76
+ @staticmethod
77
+ def wrap_with_logging(
78
+ func: Callable[..., Any],
79
+ class_name: str,
80
+ call_times: dict[str, float],
81
+ ) -> Callable[..., Any]:
82
+ """Wrap a function with logging functionality.
83
+
84
+ Creates a wrapper that logs method calls, arguments, execution time,
85
+ and return values. Includes rate limiting to prevent excessive logging.
86
+
87
+ Args:
88
+ func: The function to wrap with logging
89
+ class_name: The name of the class containing the function
90
+ call_times: Dictionary to track when methods were last called
91
+
92
+ Returns:
93
+ A wrapped function with logging capabilities
94
+
95
+ """
96
+ time_time = time.time # Cache the time.time function for performance
97
+
98
+ @wraps(func)
99
+ def wrapper(self: object, *args: object, **kwargs: object) -> object:
100
+ # call_times as a dictionary to store the call times of the function
101
+ # we only log if the time since the last call is greater than the threshold
102
+ # this is to avoid spamming the logs
103
+
104
+ func_name = func.__name__
105
+
106
+ threshold = 1
107
+
108
+ last_call_time = call_times.get(func_name, 0)
109
+
110
+ current_time = time_time()
111
+
112
+ do_logging = (current_time - last_call_time) > threshold
113
+
114
+ max_log_length = 20
115
+
116
+ if do_logging:
117
+ args_str = value_to_truncated_string(
118
+ value=args, max_length=max_log_length
119
+ )
120
+
121
+ kwargs_str = value_to_truncated_string(
122
+ value=kwargs, max_length=max_log_length
123
+ )
124
+
125
+ logger.info(
126
+ "%s - Calling %s with %s and %s",
127
+ class_name,
128
+ func_name,
129
+ args_str,
130
+ kwargs_str,
131
+ )
132
+
133
+ # Execute the function and return the result
134
+
135
+ result = func(self, *args, **kwargs)
136
+
137
+ if do_logging:
138
+ duration = time_time() - current_time
139
+
140
+ result_str = value_to_truncated_string(
141
+ value=result, max_length=max_log_length
142
+ )
143
+
144
+ logger.info(
145
+ "%s - %s finished with %s seconds -> returning %s",
146
+ class_name,
147
+ func_name,
148
+ duration,
149
+ result_str,
150
+ )
151
+
152
+ # save the call time for the next call
153
+
154
+ call_times[func_name] = current_time
155
+
156
+ return result
157
+
158
+ return wrapper
159
+
160
+
161
+ class ImplementationMeta(type):
162
+ """Metaclass that enforces implementation.
163
+
164
+ Ensures that concrete subclasses properly implement all required attributes
165
+ and that their types match the expected types from type annotations.
166
+ Additionally enforces that methods must be decorated with either @final or
167
+ @abstractmethod to make design intentions explicit.
168
+ """
169
+
170
+ def __init__(
171
+ cls: "ImplementationMeta",
172
+ name: str,
173
+ bases: tuple[type, ...],
174
+ dct: dict[str, Any],
175
+ ) -> None:
176
+ """Initialize a class with implementation checking.
177
+
178
+ Verifies that concrete classes (non-abstract) properly implement
179
+ all required attributes with the correct types. Also checks that
180
+ methods are properly decorated with @final or @abstractmethod.
181
+
182
+ Args:
183
+ cls: The class being initialized
184
+ name: The name of the class
185
+ bases: The base classes
186
+ dct: The attribute dictionary
187
+
188
+ Raises:
189
+ NotImplementedError: If the class doesn't define __abstract__
190
+ ValueError: If a required attribute is not implemented
191
+ TypeError: If an implemented attribute has the wrong type
192
+ TypeError: If a method is neither final nor abstract
193
+
194
+ """
195
+ super().__init__(name, bases, dct)
196
+
197
+ # Check method decorators regardless of abstract status
198
+
199
+ cls.check_method_decorators()
200
+
201
+ if cls.is_abstract_cls():
202
+ return
203
+
204
+ cls.check_attrs_implemented()
205
+
206
+ def is_abstract_cls(cls) -> bool:
207
+ """Check if the class is abstract.
208
+
209
+ Determines abstractness based on if any methods have @abstractmethod.
210
+
211
+ Returns:
212
+ True if the class is abstract, False otherwise
213
+
214
+ """
215
+ return any(cls.is_abstract_method(method) for method in cls.__dict__.values())
216
+
217
+ def check_method_decorators(cls) -> None:
218
+ """Check that all methods are properly decorated with @final or @abstractmethod.
219
+
220
+ Verifies that all methods in the class are explicitly marked
221
+ as either final or abstract to enforce design intentions.
222
+
223
+ Raises:
224
+ TypeError: If a method is neither final nor abstract
225
+
226
+ """
227
+ # Get all methods defined in this class (not inherited)
228
+
229
+ for func in get_all_methods_from_cls(cls, exclude_parent_methods=True):
230
+ # Check if the method is marked as final or abstract
231
+
232
+ if not cls.is_final_method(func) and not cls.is_abstract_method(func):
233
+ msg = (
234
+ f"Method {cls.__name__}.{func.__name__} must be decorated with "
235
+ f"@{final.__name__} or @{abstractmethod.__name__} "
236
+ f"to make design intentions explicit."
237
+ )
238
+
239
+ raise TypeError(msg)
240
+
241
+ @staticmethod
242
+ def is_final_method(method: Callable[..., Any]) -> bool:
243
+ """Check if a method is marked as final.
244
+
245
+ Args:
246
+ method: The method to check
247
+
248
+ Returns:
249
+ True if the method is marked with @final, False otherwise
250
+
251
+ """
252
+ return getattr(method, "__final__", False)
253
+
254
+ @staticmethod
255
+ def is_abstract_method(method: Callable[..., Any]) -> bool:
256
+ """Check if a method is an abstract method.
257
+
258
+ Args:
259
+ method: The method to check
260
+
261
+ Returns:
262
+ True if the method is marked with @abstractmethod, False otherwise
263
+
264
+ """
265
+ return getattr(method, "__isabstractmethod__", False)
266
+
267
+ def check_attrs_implemented(cls) -> None:
268
+ """Check that all required attributes are implemented.
269
+
270
+ Verifies that all attributes marked as NotImplemented in parent classes
271
+ are properly implemented in this class, and that their types match
272
+ the expected types from type annotations.
273
+
274
+ Raises:
275
+ ValueError: If a required attribute is not implemented
276
+
277
+ """
278
+ for attr in cls.attrs_to_implement():
279
+ value = getattr(cls, attr, NotImplemented)
280
+
281
+ if value is NotImplemented:
282
+ msg = f"{attr=} must be implemented."
283
+
284
+ raise ValueError(msg)
285
+
286
+ def attrs_to_implement(cls) -> list[str]:
287
+ """Find all attributes marked as NotImplemented in parent classes.
288
+
289
+ Searches the class hierarchy for attributes that are set to NotImplemented,
290
+ which indicates they must be implemented by concrete subclasses.
291
+
292
+ Returns:
293
+ List of attribute names that must be implemented
294
+
295
+ """
296
+ attrs = {
297
+ attr
298
+ for base_class in cls.__mro__
299
+ for attr in dir(base_class)
300
+ if getattr(base_class, attr, None) is NotImplemented
301
+ }
302
+
303
+ return list(attrs)
304
+
305
+
306
+ class ABCImplementationLoggingMeta(ImplementationMeta, LoggingMeta, ABCMeta):
307
+ """Combined metaclass that merges implementation, logging, and ABC functionality.
308
+
309
+ This metaclass combines the features of:
310
+ - ImplementationMeta: Enforces implementation of required attributes
311
+ - LoggingMeta: Adds automatic logging to methods
312
+ - ABCMeta: Provides abstract base class functionality
313
+
314
+ Use this metaclass when you need all three behaviors in a single class.
315
+ """
@@ -0,0 +1,28 @@
1
+ """Mixin utilities for class composition and behavior extension.
2
+
3
+ This module provides metaclasses and mixins that facilitate class composition
4
+ through the mixin pattern. It includes utilities for:
5
+ - Automatic method logging with performance tracking
6
+ - Abstract class implementation enforcement with type checking
7
+ - Combined metaclasses that merge multiple behaviors
8
+
9
+ These utilities help create robust class hierarchies with proper implementation
10
+ enforcement and built-in logging capabilities.
11
+ """
12
+
13
+ from winipedia_utils.logging.logger import get_logger
14
+ from winipedia_utils.oop.mixins.meta import ABCImplementationLoggingMeta
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class ABCImplementationLoggingMixin(metaclass=ABCImplementationLoggingMeta):
20
+ """mixin class that provides implementation, logging, and ABC functionality.
21
+
22
+ This mixin can be used as a base class for other mixins that need:
23
+ - Abstract method declaration (from ABC)
24
+ - Implementation enforcement (from ImplementationMeta)
25
+ - Automatic method logging (from LoggingMeta)
26
+
27
+ Subclasses must set __abstract__ = False when they provide concrete implementations.
28
+ """
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.os."""
@@ -0,0 +1,61 @@
1
+ """OS utilities for finding commands and paths.
2
+
3
+ This module provides utility functions for working with the operating system,
4
+ including finding the path to commands and managing environment variables.
5
+ These utilities help with system-level operations and configuration.
6
+ """
7
+
8
+ import shutil
9
+ import subprocess # nosec: B404
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ def which_with_raise(cmd: str) -> str:
15
+ """Give the path to the given command.
16
+
17
+ Args:
18
+ cmd: The command to find
19
+
20
+ Returns:
21
+ The path to the command
22
+
23
+ Raises:
24
+ FileNotFoundError: If the command is not found
25
+
26
+ """
27
+ path = shutil.which(cmd)
28
+ if path is None:
29
+ msg = f"Command {cmd} not found"
30
+ raise FileNotFoundError(msg)
31
+ return path
32
+
33
+
34
+ def run_subprocess(
35
+ args: list[str | Path],
36
+ *,
37
+ input_: str | bytes | None = None,
38
+ capture_output: bool = True,
39
+ timeout: int | None = None,
40
+ check: bool = True,
41
+ **kwargs: Any,
42
+ ) -> subprocess.CompletedProcess[Any]:
43
+ """Run a subprocess.
44
+
45
+ Args:
46
+ args: The arguments to pass to the subprocess
47
+ input_: The input to pass to the subprocess
48
+ capture_output: Whether to capture the output of the subprocess
49
+ timeout: The timeout for the subprocess
50
+ check: to raise an exception if the subprocess returns a non-zero exit code
51
+ kwargs: Any other arguments to pass to subprocess.run()
52
+
53
+ """
54
+ return subprocess.run( # noqa: S603 # nosec: B603
55
+ args,
56
+ check=check,
57
+ input=input_,
58
+ capture_output=capture_output,
59
+ timeout=timeout,
60
+ **kwargs,
61
+ )
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.projects."""
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.projects.poetry."""
@@ -0,0 +1,91 @@
1
+ """Config utilities for poetry and pyproject.toml."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import tomlkit
7
+ from tomlkit.toml_document import TOMLDocument
8
+
9
+ from winipedia_utils.projects.poetry.poetry import logger
10
+
11
+
12
+ def laod_pyproject_toml() -> TOMLDocument:
13
+ """Load the pyproject.toml file."""
14
+ return tomlkit.parse(Path("pyproject.toml").read_text())
15
+
16
+
17
+ def dump_pyproject_toml(toml: TOMLDocument) -> None:
18
+ """Dump the pyproject.toml file."""
19
+ with Path("pyproject.toml").open("w") as f:
20
+ tomlkit.dump(toml, f)
21
+
22
+
23
+ def get_poetry_package_name() -> str:
24
+ """Get the name of the project from pyproject.toml."""
25
+ toml = laod_pyproject_toml()
26
+ project_dict = toml.get("project", {})
27
+ project_name = str(project_dict.get("name", ""))
28
+ return project_name.replace("-", "_")
29
+
30
+
31
+ def _get_pyproject_toml_tool_configs() -> dict[str, Any]:
32
+ """Get the tool configurations for pyproject.toml."""
33
+ return {
34
+ "ruff": {
35
+ "exclude": [".*"],
36
+ "lint": {
37
+ "select": ["ALL"],
38
+ "ignore": ["D203", "D213", "COM812", "ANN401"],
39
+ "fixable": ["ALL"],
40
+ "pydocstyle": {
41
+ "convention": "google",
42
+ },
43
+ },
44
+ },
45
+ "mypy": {
46
+ "strict": True,
47
+ "warn_unreachable": True,
48
+ "show_error_codes": True,
49
+ "files": ".",
50
+ },
51
+ "pytest": {
52
+ "ini_options": {
53
+ "testpaths": ["tests"],
54
+ }
55
+ },
56
+ "bandit": {},
57
+ }
58
+
59
+
60
+ def _tool_config_is_correct(tool: str, config: dict[str, Any]) -> bool:
61
+ """Check if the tool configuration in pyproject.toml is correct."""
62
+ toml = laod_pyproject_toml()
63
+ actual_tools = toml.get("tool", {})
64
+
65
+ return bool(actual_tools.get(tool) == config)
66
+
67
+
68
+ def _pyproject_tool_configs_are_correct() -> bool:
69
+ """Check if the tool configurations in pyproject.toml are correct."""
70
+ expected_tool_dict = _get_pyproject_toml_tool_configs()
71
+ for tool, config in expected_tool_dict.items():
72
+ if not _tool_config_is_correct(tool, config):
73
+ return False
74
+
75
+ return True
76
+
77
+
78
+ def _add_tool_configurations_to_pyproject_toml() -> None:
79
+ """Add tool.* configurations to pyproject.toml."""
80
+ expected_tool_dict = _get_pyproject_toml_tool_configs()
81
+ toml = laod_pyproject_toml()
82
+ actual_tool_dict = toml.get("tool", {})
83
+ # update the toml dct and dump it but only update the tools specified not all tools
84
+ for tool, config in expected_tool_dict.items():
85
+ # if tool section already exists skip it
86
+ if not _tool_config_is_correct(tool, config):
87
+ logger.info("Adding tool.%s configuration to pyproject.toml", tool)
88
+ # updates inplace of toml_dict["tool"][tool]
89
+ actual_tool_dict[tool] = config
90
+
91
+ dump_pyproject_toml(toml)
@@ -0,0 +1,30 @@
1
+ """Project utilities for introspection and manipulation.
2
+
3
+ This module provides utility functions for working with Python projects
4
+ """
5
+
6
+ import sys
7
+
8
+ from winipedia_utils.consts import _DEV_DEPENDENCIES
9
+ from winipedia_utils.logging.logger import get_logger
10
+ from winipedia_utils.os.os import run_subprocess, which_with_raise
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ POETRY_PATH = which_with_raise("poetry")
15
+
16
+ POETRY_RUN_ARGS = [POETRY_PATH, "run"]
17
+
18
+ POETRY_ADD_ARGS = [POETRY_PATH, "add"]
19
+
20
+ POETRY_ADD_DEV_ARGS = [*POETRY_ADD_ARGS, "--group", "dev"]
21
+
22
+ POETRY_RUN_PYTHON_ARGS = [*POETRY_RUN_ARGS, sys.executable]
23
+
24
+ POETRY_RUN_RUFF_ARGS = [*POETRY_RUN_ARGS, "ruff"]
25
+
26
+
27
+ def _install_dev_dependencies() -> None:
28
+ """Install winipedia_utils dev dependencies as dev dependencies."""
29
+ logger.info("Adding dev dependencies: %s", _DEV_DEPENDENCIES)
30
+ run_subprocess([*POETRY_ADD_DEV_ARGS, *_DEV_DEPENDENCIES], check=True)
@@ -0,0 +1,36 @@
1
+ """A script that can be called after you installed the package.
2
+
3
+ This script calls create tests, creates the pre-commit config, and
4
+ creates the pyproject.toml file and some other things to set up a project.
5
+ This package assumes you are using poetry and pre-commit.
6
+ This script is intended to be called once at the beginning of a project.
7
+ """
8
+
9
+ from winipedia_utils.git.pre_commit.config import _add_package_hook_to_pre_commit_config
10
+ from winipedia_utils.git.pre_commit.run_hooks import _run_all_hooks
11
+ from winipedia_utils.logging.logger import get_logger
12
+ from winipedia_utils.projects.poetry.config import (
13
+ _add_tool_configurations_to_pyproject_toml,
14
+ )
15
+ from winipedia_utils.projects.poetry.poetry import (
16
+ _install_dev_dependencies,
17
+ )
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ def _setup() -> None:
23
+ """Set up the project."""
24
+ # install winipedia_utils dev dependencies as dev
25
+ _install_dev_dependencies()
26
+ # create pre-commit config
27
+ _add_package_hook_to_pre_commit_config()
28
+ # add tool.* configurations to pyproject.toml
29
+ _add_tool_configurations_to_pyproject_toml()
30
+ # run pre-commit once, create tests is included here
31
+ _run_all_hooks()
32
+ logger.info("Setup complete!")
33
+
34
+
35
+ if __name__ == "__main__":
36
+ _setup()
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.testing."""
@@ -0,0 +1,23 @@
1
+ """Testing assertion utilities for enhanced test validation.
2
+
3
+ This module provides custom assertion functions that extend Python's built-in
4
+ assert statement with additional features like improved error messages and
5
+ specialized validation logic for common testing scenarios.
6
+ """
7
+
8
+
9
+ def assert_with_msg(expr: bool, msg: str) -> None: # noqa: FBT001
10
+ """Assert that an expression is true with a custom error message.
11
+
12
+ A thin wrapper around Python's built-in assert statement that makes it
13
+ easier to provide meaningful error messages when assertions fail.
14
+
15
+ Args:
16
+ expr: The expression to evaluate for truthiness
17
+ msg: The error message to display if the assertion fails
18
+
19
+ Raises:
20
+ AssertionError: If the expression evaluates to False
21
+
22
+ """
23
+ assert expr, msg # noqa: S101 # nosec: B101