danielutils 1.1.28__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 (287) hide show
  1. danielutils/__init__.py +50 -0
  2. danielutils/abstractions/__init__.py +3 -0
  3. danielutils/abstractions/db/__init__.py +10 -0
  4. danielutils/abstractions/db/database.py +206 -0
  5. danielutils/abstractions/db/database_definitions.py +219 -0
  6. danielutils/abstractions/db/database_exceptions.py +34 -0
  7. danielutils/abstractions/db/database_factory.py +70 -0
  8. danielutils/abstractions/db/database_initializer.py +261 -0
  9. danielutils/abstractions/db/database_models.py +213 -0
  10. danielutils/abstractions/db/dependencies.py +6 -0
  11. danielutils/abstractions/db/implementations/__init__.py +4 -0
  12. danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
  13. danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
  14. danielutils/abstractions/db/implementations/redis_database.py +403 -0
  15. danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
  16. danielutils/abstractions/multiprogramming/__init__.py +3 -0
  17. danielutils/abstractions/multiprogramming/multi_id.py +26 -0
  18. danielutils/abstractions/multiprogramming/worker.py +104 -0
  19. danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
  20. danielutils/abstractions/repl.py +125 -0
  21. danielutils/aliases.py +174 -0
  22. danielutils/async_/__init__.py +7 -0
  23. danielutils/async_/async_cmd.py +28 -0
  24. danielutils/async_/async_command.py +879 -0
  25. danielutils/async_/async_layered_command.py +125 -0
  26. danielutils/async_/async_retry_executor.py +86 -0
  27. danielutils/async_/async_worker_pool.py +110 -0
  28. danielutils/async_/time_strategy.py +73 -0
  29. danielutils/async_/utils.py +57 -0
  30. danielutils/better_builtins/__init__.py +3 -0
  31. danielutils/better_builtins/counter.py +48 -0
  32. danielutils/better_builtins/frange.py +266 -0
  33. danielutils/better_builtins/typed_builtins/__init__.py +4 -0
  34. danielutils/better_builtins/typed_builtins/factory.py +126 -0
  35. danielutils/better_builtins/typed_builtins/tdict.py +36 -0
  36. danielutils/better_builtins/typed_builtins/tlist.py +223 -0
  37. danielutils/better_builtins/typed_builtins/tset.py +67 -0
  38. danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
  39. danielutils/colors.py +203 -0
  40. danielutils/context_managers/__init__.py +5 -0
  41. danielutils/context_managers/attr_context.py +40 -0
  42. danielutils/context_managers/multi_context.py +34 -0
  43. danielutils/context_managers/optional_context.py +33 -0
  44. danielutils/context_managers/state_context.py +25 -0
  45. danielutils/context_managers/temporary_file.py +108 -0
  46. danielutils/convenience.py +12 -0
  47. danielutils/conversions/__init__.py +2 -0
  48. danielutils/conversions/main_conversions.py +131 -0
  49. danielutils/conversions/specialized_conversions/__init__.py +2 -0
  50. danielutils/conversions/specialized_conversions/to_hex.py +25 -0
  51. danielutils/conversions/specialized_conversions/to_int.py +19 -0
  52. danielutils/custom_types.py +18 -0
  53. danielutils/data_structures/__init__.py +8 -0
  54. danielutils/data_structures/algorithms.py +52 -0
  55. danielutils/data_structures/comparer.py +47 -0
  56. danielutils/data_structures/default_dict.py +23 -0
  57. danielutils/data_structures/functions.py +35 -0
  58. danielutils/data_structures/graph/__init__.py +4 -0
  59. danielutils/data_structures/graph/binary_node.py +71 -0
  60. danielutils/data_structures/graph/graph.py +208 -0
  61. danielutils/data_structures/graph/multinode.py +101 -0
  62. danielutils/data_structures/graph/node.py +60 -0
  63. danielutils/data_structures/heap/__init__.py +3 -0
  64. danielutils/data_structures/heap/heap.py +119 -0
  65. danielutils/data_structures/heap/max_heap.py +18 -0
  66. danielutils/data_structures/heap/min_heap.py +18 -0
  67. danielutils/data_structures/queue/__init__.py +3 -0
  68. danielutils/data_structures/queue/atomic_queue.py +15 -0
  69. danielutils/data_structures/queue/priority_queue.py +110 -0
  70. danielutils/data_structures/queue/queue.py +92 -0
  71. danielutils/data_structures/stack.py +102 -0
  72. danielutils/data_structures/trees/__init__.py +2 -0
  73. danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
  74. danielutils/data_structures/trees/binary_tree.py +63 -0
  75. danielutils/date.py +43 -0
  76. danielutils/date_time.py +23 -0
  77. danielutils/decorators/__init__.py +18 -0
  78. danielutils/decorators/atomic.py +45 -0
  79. danielutils/decorators/attach.py +61 -0
  80. danielutils/decorators/chain_decorators.py +39 -0
  81. danielutils/decorators/decorate_conditionally.py +32 -0
  82. danielutils/decorators/delay_call.py +47 -0
  83. danielutils/decorators/deprecate.py +46 -0
  84. danielutils/decorators/final.py +39 -0
  85. danielutils/decorators/limit_recursion.py +63 -0
  86. danielutils/decorators/memo.py +68 -0
  87. danielutils/decorators/normalize_decorator.py +55 -0
  88. danielutils/decorators/overload.py +204 -0
  89. danielutils/decorators/partially_implemented.py +32 -0
  90. danielutils/decorators/processify.py +74 -0
  91. danielutils/decorators/property.py +42 -0
  92. danielutils/decorators/singleton.py +59 -0
  93. danielutils/decorators/threadify.py +40 -0
  94. danielutils/decorators/timeout.py +82 -0
  95. danielutils/decorators/total_ordering.py +82 -0
  96. danielutils/decorators/validate.py +227 -0
  97. danielutils/exceptions.py +90 -0
  98. danielutils/functions/__init__.py +13 -0
  99. danielutils/functions/areoneof.py +47 -0
  100. danielutils/functions/check_foreach.py +39 -0
  101. danielutils/functions/factorial.py +24 -0
  102. danielutils/functions/flatten.py +33 -0
  103. danielutils/functions/foreach.py +20 -0
  104. danielutils/functions/isoftype.py +475 -0
  105. danielutils/functions/isoneof.py +71 -0
  106. danielutils/functions/multiloop.py +51 -0
  107. danielutils/functions/parallel_for.py +47 -0
  108. danielutils/functions/partition.py +45 -0
  109. danielutils/functions/powerset.py +31 -0
  110. danielutils/functions/subseteq.py +25 -0
  111. danielutils/functions/types_subseteq.py +51 -0
  112. danielutils/generators/__init__.py +3 -0
  113. danielutils/generators/conditional_generator.py +59 -0
  114. danielutils/generators/generator_from_stream.py +31 -0
  115. danielutils/generators/join_generators.py +109 -0
  116. danielutils/internet.py +92 -0
  117. danielutils/io_.py +451 -0
  118. danielutils/java/__init__.py +18 -0
  119. danielutils/java/interfaces/__init__.py +1 -0
  120. danielutils/java/interfaces/comparable.py +31 -0
  121. danielutils/java/java_interface.py +59 -0
  122. danielutils/logging_/__init__.py +2 -0
  123. danielutils/logging_/_impl/__init__.py +4 -0
  124. danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
  125. danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
  126. danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
  127. danielutils/logging_/_impl/log_level.py +13 -0
  128. danielutils/logging_/_impl/logger.py +109 -0
  129. danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
  130. danielutils/logging_/utils.py +199 -0
  131. danielutils/lombok/__init__.py +1 -0
  132. danielutils/lombok/builder.py +68 -0
  133. danielutils/math_/__init__.py +2 -0
  134. danielutils/math_/constants.py +103 -0
  135. danielutils/math_/functions.py +34 -0
  136. danielutils/math_/math_print.py +52 -0
  137. danielutils/math_/math_symbols.py +125 -0
  138. danielutils/metaclasses/__init__.py +5 -0
  139. danielutils/metaclasses/atomic_class_meta.py +36 -0
  140. danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
  141. danielutils/metaclasses/instance_cache_meta.py +66 -0
  142. danielutils/metaclasses/interface.py +328 -0
  143. danielutils/metaclasses/overload_meta.py +48 -0
  144. danielutils/mock_/__init__.py +1 -0
  145. danielutils/mock_/mock_module.py +27 -0
  146. danielutils/path.py +55 -0
  147. danielutils/print_.py +123 -0
  148. danielutils/progress_bar/__init__.py +3 -0
  149. danielutils/progress_bar/ascii_progress_bar.py +161 -0
  150. danielutils/progress_bar/progress_bar.py +75 -0
  151. danielutils/progress_bar/progress_bar_pool.py +72 -0
  152. danielutils/protocols/__init__.py +3 -0
  153. danielutils/protocols/dictable.py +38 -0
  154. danielutils/protocols/evaluable.py +26 -0
  155. danielutils/protocols/serializable.py +51 -0
  156. danielutils/py.typed +0 -0
  157. danielutils/random_.py +47 -0
  158. danielutils/reflection/__init__.py +6 -0
  159. danielutils/reflection/declarations.py +288 -0
  160. danielutils/reflection/file/__init__.py +1 -0
  161. danielutils/reflection/file/file_reflection.py +85 -0
  162. danielutils/reflection/function/__init__.py +1 -0
  163. danielutils/reflection/function/function_reflections.py +152 -0
  164. danielutils/reflection/info_classes/__init__.py +6 -0
  165. danielutils/reflection/info_classes/argument_info.py +278 -0
  166. danielutils/reflection/info_classes/class_info.py +204 -0
  167. danielutils/reflection/info_classes/decorator_info.py +54 -0
  168. danielutils/reflection/info_classes/file_info.py +1180 -0
  169. danielutils/reflection/info_classes/folder_info.py +592 -0
  170. danielutils/reflection/info_classes/function_info.py +551 -0
  171. danielutils/reflection/info_classes/import_info.py +207 -0
  172. danielutils/reflection/interpreter/__init__.py +9 -0
  173. danielutils/reflection/interpreter/callstack.py +57 -0
  174. danielutils/reflection/interpreter/get_traceback.py +27 -0
  175. danielutils/reflection/interpreter/interpreter.py +41 -0
  176. danielutils/reflection/interpreter/is_debugging.py +13 -0
  177. danielutils/reflection/interpreter/os_.py +33 -0
  178. danielutils/reflection/interpreter/packages.py +33 -0
  179. danielutils/reflection/interpreter/python_version.py +33 -0
  180. danielutils/reflection/interpreter/signals.py +18 -0
  181. danielutils/reflection/interpreter/tracer.py +175 -0
  182. danielutils/reflection/module/__init__.py +3 -0
  183. danielutils/reflection/module/lazy_module.py +104 -0
  184. danielutils/reflection/module/module_reflections.py +34 -0
  185. danielutils/reflection/module/package_reflection.py +161 -0
  186. danielutils/retry_executor/__init__.py +3 -0
  187. danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
  188. danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
  189. danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
  190. danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
  191. danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
  192. danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
  193. danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
  194. danielutils/retry_executor/backoff_strategy.py +28 -0
  195. danielutils/retry_executor/retry_executor.py +55 -0
  196. danielutils/snippets/__init__.py +1 -0
  197. danielutils/snippets/try_get.py +28 -0
  198. danielutils/system/__init__.py +3 -0
  199. danielutils/system/independent.py +197 -0
  200. danielutils/system/layered_command.py +130 -0
  201. danielutils/system/windows/__init__.py +2 -0
  202. danielutils/system/windows/utils/__init__.py +1 -0
  203. danielutils/system/windows/utils/filetime.py +19 -0
  204. danielutils/system/windows/win32_ctime.py +183 -0
  205. danielutils/system/windows/windows.py +78 -0
  206. danielutils/testing/__init__.py +1 -0
  207. danielutils/testing/unittest_/__init__.py +2 -0
  208. danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
  209. danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
  210. danielutils/text.py +169 -0
  211. danielutils/time.py +64 -0
  212. danielutils/tools/__init__.py +1 -0
  213. danielutils/tools/unittest_test_runner/__init__.py +9 -0
  214. danielutils/tools/unittest_test_runner/cli.py +70 -0
  215. danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
  216. danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
  217. danielutils/tools/unittest_test_runner/core/execution.py +244 -0
  218. danielutils/tools/unittest_test_runner/core/parser.py +163 -0
  219. danielutils/tools/unittest_test_runner/core/runner.py +561 -0
  220. danielutils/tools/unittest_test_runner/models.py +82 -0
  221. danielutils/university/__init__.py +7 -0
  222. danielutils/university/computability_and_complexity/__init__.py +2 -0
  223. danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
  224. danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
  225. danielutils/university/computability_and_complexity/languages/language.py +17 -0
  226. danielutils/university/computability_and_complexity/languages/sat.py +71 -0
  227. danielutils/university/computability_and_complexity/turing_machine.py +47 -0
  228. danielutils/university/databases/__init__.py +1 -0
  229. danielutils/university/databases/all.py +756 -0
  230. danielutils/university/image_proccesing/__init__.py +1 -0
  231. danielutils/university/image_proccesing/encoding/__init__.py +3 -0
  232. danielutils/university/image_proccesing/encoding/encoding.py +35 -0
  233. danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
  234. danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
  235. danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
  236. danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
  237. danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
  238. danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
  239. danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
  240. danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
  241. danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
  242. danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
  243. danielutils/university/image_proccesing/tansformations/hough.py +0 -0
  244. danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
  245. danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
  246. danielutils/university/linear_algebra/__init__.py +1 -0
  247. danielutils/university/linear_algebra/matrix.py +404 -0
  248. danielutils/university/machine_learning/__init__.py +0 -0
  249. danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
  250. danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
  251. danielutils/university/machine_learning/activation_functions/relu.py +11 -0
  252. danielutils/university/machine_learning/neuron.py +15 -0
  253. danielutils/university/oop/__init__.py +1 -0
  254. danielutils/university/oop/observer.py +31 -0
  255. danielutils/university/oop/strategy.py +9 -0
  256. danielutils/university/probability/__init__.py +7 -0
  257. danielutils/university/probability/conditional_variable/__init__.py +3 -0
  258. danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
  259. danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
  260. danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
  261. danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
  262. danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
  263. danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
  264. danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
  265. danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
  266. danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
  267. danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
  268. danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
  269. danielutils/university/probability/distributions.py +35 -0
  270. danielutils/university/probability/expressions/__init__.py +2 -0
  271. danielutils/university/probability/expressions/accumulation_expression.py +291 -0
  272. danielutils/university/probability/expressions/probability_expression.py +110 -0
  273. danielutils/university/probability/funcs/__init__.py +4 -0
  274. danielutils/university/probability/funcs/covariance.py +12 -0
  275. danielutils/university/probability/funcs/expected_value.py +41 -0
  276. danielutils/university/probability/funcs/probability_function.py +14 -0
  277. danielutils/university/probability/funcs/variance.py +17 -0
  278. danielutils/university/probability/operator.py +79 -0
  279. danielutils/university/probability/protocols.py +32 -0
  280. danielutils/university/probability/supp.py +101 -0
  281. danielutils/university/probability/transformation.py +44 -0
  282. danielutils/versioned_imports.py +31 -0
  283. danielutils-1.1.28.dist-info/METADATA +390 -0
  284. danielutils-1.1.28.dist-info/RECORD +287 -0
  285. danielutils-1.1.28.dist-info/WHEEL +5 -0
  286. danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
  287. danielutils-1.1.28.dist-info/top_level.txt +1 -0
@@ -0,0 +1,322 @@
1
+ """
2
+ Test discovery and structure analysis functionality.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import List, Dict, Optional, Tuple, Literal
10
+ from ..models import UnittestDiscovery
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ VerboseLevel = Literal["module", "file", "class", "function"]
15
+
16
+
17
+ class UnittestDiscoveryService:
18
+ """Handles discovery of test structure and target parsing."""
19
+
20
+ def __init__(self, python_path: str, verbose: VerboseLevel = "class"):
21
+ logger.debug("Initializing UnittestDiscoveryService with python_path=%s, verbose=%s", python_path, verbose)
22
+ self._python_path = python_path
23
+ self._verbose = verbose
24
+
25
+ def discover_test_modules(self, tests_dir: str = "tests", target: Optional[str] = None) -> List[str]:
26
+ """Discover all test modules based on target or default tests directory."""
27
+ logger.info("Discovering test modules with tests_dir=%s, target=%s", tests_dir, target)
28
+
29
+ if target:
30
+ # If specific target provided, look for tests relative to that target
31
+ logger.debug("Target specified, looking for modules matching: %s", target)
32
+ if "." in target:
33
+ # Target is a module path, look for tests in that module's directory
34
+ target_parts = target.split(".")
35
+ if target_parts[0] == "test_runner":
36
+ # Look in test_runner/tests directory
37
+ tests_path = Path("test_runner/tests")
38
+ else:
39
+ # Look in the main tests directory
40
+ tests_path = Path("tests")
41
+ else:
42
+ # Single module name, look in default tests directory
43
+ tests_path = Path("tests")
44
+
45
+ if tests_path.exists():
46
+ found_modules = []
47
+ logger.debug("Searching for test files in: %s", tests_path)
48
+ for py_file in tests_path.rglob("test_*.py"):
49
+ # Convert path to module format
50
+ relative_path = py_file.relative_to(tests_path.parent)
51
+ module_path = str(relative_path).replace(os.sep, ".").replace(".py", "")
52
+ # Check if this module matches our target
53
+ if module_path.startswith(target):
54
+ found_modules.append(module_path)
55
+ logger.debug("Found matching module: %s", module_path)
56
+
57
+ # If we're looking for a specific module.class target, find all test modules in that module
58
+ if "." in target and not found_modules:
59
+ # Split target like "test_runner.tests" into "test_runner" and "tests"
60
+ target_parts = target.split(".")
61
+ if len(target_parts) >= 2:
62
+ module_prefix = ".".join(target_parts[:-1]) # "test_runner" for "test_runner.tests"
63
+ logger.debug("No exact matches, trying module prefix: %s", module_prefix)
64
+ for py_file in tests_path.rglob("test_*.py"):
65
+ relative_path = py_file.relative_to(tests_path.parent)
66
+ module_path = str(relative_path).replace(os.sep, ".").replace(".py", "")
67
+ if module_path.startswith(module_prefix):
68
+ found_modules.append(module_path)
69
+ logger.debug("Found module with prefix match: %s", module_path)
70
+
71
+ if found_modules:
72
+ logger.info("Discovered %d test modules for target '%s'", len(found_modules), target)
73
+ if self._verbose in ["module", "file", "class", "function"]:
74
+ print(f" 📁 Discovered {len(found_modules)} test module(s) for target '{target}':")
75
+ for module in sorted(found_modules):
76
+ print(f" 📄 {module}")
77
+ return found_modules
78
+
79
+ # If no tests found relative to target, return the target itself
80
+ logger.warning("No test modules found for target '%s', using target as module", target)
81
+ if self._verbose in ["module", "file", "class", "function"]:
82
+ print(f" 📁 No test modules found for target '{target}', using target as module")
83
+ return [target]
84
+
85
+ # No target provided, look in the default tests directory
86
+ test_modules = []
87
+ tests_path = Path(tests_dir)
88
+
89
+ if not tests_path.exists():
90
+ logger.error("Tests directory not found: %s", tests_dir)
91
+ print(f"Tests directory not found: {tests_dir}")
92
+ return []
93
+
94
+ logger.debug("Searching for test files in default directory: %s", tests_path)
95
+ for py_file in tests_path.rglob("test_*.py"):
96
+ # Convert path to module format
97
+ relative_path = py_file.relative_to(tests_path.parent)
98
+ module_path = str(relative_path).replace(os.sep, ".").replace(".py", "")
99
+ test_modules.append(module_path)
100
+ logger.debug("Found test module: %s", module_path)
101
+
102
+ logger.info("Discovered %d test modules in '%s'", len(test_modules), tests_dir)
103
+ if self._verbose in ["module", "file", "class", "function"]:
104
+ print(f" 📁 Discovered {len(test_modules)} test module(s) in '{tests_dir}':")
105
+ for module in sorted(test_modules):
106
+ print(f" 📄 {module}")
107
+
108
+ return sorted(test_modules)
109
+
110
+ def discover_test_structure(self, test_modules: List[str]) -> UnittestDiscovery:
111
+ """Discover the complete test structure for progress tracking."""
112
+ logger.info("Discovering test structure for %d modules", len(test_modules))
113
+ if self._verbose in ["module", "file", "class", "function"]:
114
+ print(f" 🔍 Discovering test structure for {len(test_modules)} module(s)...")
115
+
116
+ modules = []
117
+ classes = {} # module -> list of classes
118
+ functions = {} # class -> list of functions
119
+ total_functions = 0
120
+ total_classes = 0
121
+
122
+ for i, module_path in enumerate(test_modules):
123
+ modules.append(module_path)
124
+
125
+ logger.debug("Analyzing module %d/%d: %s", i+1, len(test_modules), module_path)
126
+ if self._verbose in ["class", "function"]:
127
+ print(f" 📦 Analyzing module {i+1}/{len(test_modules)}: {module_path}")
128
+
129
+ # Discover classes and functions in this module
130
+ try:
131
+ # Run unittest discovery to get test structure
132
+ cmd = [self._python_path, "-m", "unittest", "discover", "-s", "tests", "-p", f"{module_path.split('.')[-1]}.py", "--dry-run"]
133
+ logger.debug("Running discovery command: %s", " ".join(cmd))
134
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
135
+
136
+ if result.returncode == 0:
137
+ logger.debug("Discovery command successful for module: %s", module_path)
138
+ # Parse the discovery output to find classes and functions
139
+ module_classes = []
140
+ for line in result.stdout.split('\n'):
141
+ if 'test_' in line and '(' in line and ')' in line:
142
+ # Extract class name from line like "test_something (class_name)"
143
+ parts = line.split()
144
+ for part in parts:
145
+ if part.startswith('(') and part.endswith(')'):
146
+ class_name = part[1:-1]
147
+ if class_name not in module_classes:
148
+ module_classes.append(class_name)
149
+
150
+ # Count functions for this class
151
+ class_functions = []
152
+ for func_line in result.stdout.split('\n'):
153
+ if f'({class_name})' in func_line and 'test_' in func_line:
154
+ func_parts = func_line.split()
155
+ for func_part in func_parts:
156
+ if func_part.startswith('test_'):
157
+ class_functions.append(func_part)
158
+ break
159
+
160
+ functions[f"{module_path}.{class_name}"] = class_functions
161
+ total_functions += len(class_functions)
162
+ logger.debug("Found class %s with %d functions", class_name, len(class_functions))
163
+
164
+ classes[module_path] = module_classes
165
+ total_classes += len(module_classes)
166
+
167
+ if self._verbose == "function":
168
+ if module_classes:
169
+ print(f" 🏛️ Found {len(module_classes)} test class(es): {', '.join(module_classes)}")
170
+ for class_name in module_classes:
171
+ class_funcs = functions.get(f"{module_path}.{class_name}", [])
172
+ if class_funcs:
173
+ print(f" 🧪 {class_name}: {len(class_funcs)} function(s) - {', '.join(class_funcs[:3])}{'...' if len(class_funcs) > 3 else ''}")
174
+ else:
175
+ print(f" ⚠️ No test classes found")
176
+ else:
177
+ logger.warning("Discovery command failed for module %s (returncode: %d)", module_path, result.returncode)
178
+ # Fallback: assume one class per module
179
+ classes[module_path] = ["UnknownClass"]
180
+ functions[f"{module_path}.UnknownClass"] = []
181
+
182
+ except Exception as e:
183
+ logger.error("Error discovering structure for module %s: %s", module_path, e)
184
+ if self._verbose in ["class", "function"]:
185
+ print(f" ❌ Error discovering structure for {module_path}: {e}")
186
+ # Fallback: assume one class per module
187
+ classes[module_path] = ["UnknownClass"]
188
+ functions[f"{module_path}.UnknownClass"] = []
189
+
190
+ logger.info("Test structure discovery complete: %d modules, %d classes, %d functions",
191
+ len(modules), total_classes, total_functions)
192
+ if self._verbose in ["module", "file", "class", "function"]:
193
+ print(f" ✅ Discovery complete: {len(modules)} modules, {total_classes} classes, {total_functions} functions")
194
+
195
+ discovery = UnittestDiscovery(
196
+ modules=modules,
197
+ classes=classes,
198
+ functions=functions,
199
+ total_functions=total_functions,
200
+ total_classes=total_classes,
201
+ total_modules=len(modules)
202
+ )
203
+
204
+ if self._verbose == "function":
205
+ print(f"DEBUG: Discovered {len(modules)} modules, {total_classes} classes, {total_functions} functions")
206
+
207
+ return discovery
208
+
209
+ def parse_target(self, target: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
210
+ """Parse unittest dot-notation target into module, class, function components."""
211
+ logger.debug("Parsing target: %s", target)
212
+ if not target:
213
+ return None, None, None
214
+
215
+ parts = target.split('.')
216
+
217
+ # Handle different cases:
218
+ # 1. "module" -> module=module, class=None, function=None
219
+ # 2. "module.class" -> module=module, class=class, function=None
220
+ # 3. "module.class.function" -> module=module, class=class, function=function
221
+ # 4. "module.class.function.too.much" -> module=module.class.function, class=too, function=much
222
+
223
+ if len(parts) == 1:
224
+ # Just module name
225
+ module = parts[0]
226
+ class_name = None
227
+ function_name = None
228
+ elif len(parts) == 2:
229
+ # Could be module.class or module.submodule
230
+ # Check if second part starts with uppercase (class) or lowercase (submodule)
231
+ if parts[1][0].isupper():
232
+ # Second part starts with uppercase, it's a class
233
+ module = parts[0]
234
+ class_name = parts[1]
235
+ function_name = None
236
+ else:
237
+ # Second part starts with lowercase, it's a submodule
238
+ module = '.'.join(parts)
239
+ class_name = None
240
+ function_name = None
241
+ elif len(parts) == 3:
242
+ # Module.submodule.class - join first two as module, third is class
243
+ module = '.'.join(parts[:2])
244
+ class_name = parts[2]
245
+ function_name = None
246
+ else:
247
+ # More than 3 parts - for invalid formats like "invalid.target.too.many.parts"
248
+ # Join first 2 parts as module, take 3rd as class, last part as function
249
+ module = '.'.join(parts[:2])
250
+ class_name = parts[2]
251
+ function_name = parts[-1]
252
+
253
+ logger.debug("Parsed target components - module: %s, class: %s, function: %s", module, class_name, function_name)
254
+ return module, class_name, function_name
255
+
256
+ def create_focused_discovery(self, target_module: str, target_class: Optional[str],
257
+ target_function: Optional[str], full_discovery: UnittestDiscovery) -> UnittestDiscovery:
258
+ """Create a focused test discovery based on target specification."""
259
+ logger.debug("Creating focused discovery for target: %s.%s.%s", target_module, target_class or '', target_function or '')
260
+ if self._verbose == "function":
261
+ print(f"DEBUG: Creating focused discovery for target: {target_module}.{target_class or ''}.{target_function or ''}")
262
+
263
+ # Find the target module
264
+ target_modules = [m for m in full_discovery.modules if m == target_module or m.endswith(f".{target_module}")]
265
+ if not target_modules:
266
+ logger.error("Target module '%s' not found in discovered modules", target_module)
267
+ raise ValueError(f"Target module '{target_module}' not found in discovered modules")
268
+
269
+ module = target_modules[0]
270
+ modules = [module]
271
+ classes = {}
272
+ functions = {}
273
+
274
+ # Get classes for this module
275
+ module_classes = full_discovery.classes.get(module, [])
276
+ logger.debug("Found %d classes in target module %s", len(module_classes), module)
277
+
278
+ if target_class:
279
+ # Filter to specific class
280
+ if target_class in module_classes:
281
+ logger.debug("Filtering to specific class: %s", target_class)
282
+ classes[module] = [target_class]
283
+ class_key = f"{module}.{target_class}"
284
+ class_functions = full_discovery.functions.get(class_key, [])
285
+
286
+ if target_function:
287
+ # Filter to specific function
288
+ if target_function in class_functions:
289
+ logger.debug("Filtering to specific function: %s", target_function)
290
+ functions[class_key] = [target_function]
291
+ else:
292
+ logger.warning("Target function '%s' not found in class '%s'", target_function, target_class)
293
+ functions[class_key] = []
294
+ else:
295
+ functions[class_key] = class_functions
296
+ logger.debug("Including all %d functions in class %s", len(class_functions), target_class)
297
+ else:
298
+ logger.warning("Target class '%s' not found in module '%s'", target_class, module)
299
+ classes[module] = []
300
+ functions[f"{module}.{target_class}"] = []
301
+ else:
302
+ # Include all classes in module
303
+ logger.debug("Including all %d classes in module %s", len(module_classes), module)
304
+ classes[module] = module_classes
305
+ for class_name in module_classes:
306
+ class_key = f"{module}.{class_name}"
307
+ functions[class_key] = full_discovery.functions.get(class_key, [])
308
+
309
+ total_functions = sum(len(funcs) for funcs in functions.values())
310
+ total_classes = sum(len(cls) for cls in classes.values())
311
+
312
+ logger.info("Created focused discovery: %d modules, %d classes, %d functions",
313
+ len(modules), total_classes, total_functions)
314
+
315
+ return UnittestDiscovery(
316
+ modules=modules,
317
+ classes=classes,
318
+ functions=functions,
319
+ total_functions=total_functions,
320
+ total_classes=total_classes,
321
+ total_modules=len(modules)
322
+ )
@@ -0,0 +1,244 @@
1
+ """
2
+ Test execution hierarchy functionality.
3
+ Handles the core execution context with _run_test_function as the base.
4
+ """
5
+
6
+ import logging
7
+ import time
8
+ import subprocess
9
+ from typing import List, Optional, Literal
10
+ from ..models import UnittestFunctionState, UnittestResult, ModuleState
11
+ from .parser import UnittestOutputParser
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ VerboseLevel = Literal["module", "file", "class", "function"]
16
+
17
+
18
+ class UnittestExecutor:
19
+ """Handles hierarchical test execution with _run_test_function as the core."""
20
+
21
+ def __init__(self, python_path: str, verbose: VerboseLevel = "class",
22
+ test_discovery: Optional[object] = None):
23
+ logger.debug("Initializing UnittestExecutor with python_path=%s, verbose=%s", python_path, verbose)
24
+ self._python_path = python_path
25
+ self._verbose = verbose
26
+ self._test_discovery = test_discovery
27
+ self._parser = UnittestOutputParser(verbose)
28
+
29
+ def run_test_function(self, module_path: str, test_class: str, test_function: str,
30
+ function_index: int = 0, total_functions: int = 0) -> UnittestFunctionState:
31
+ """Run a single test function and return its state. This is the core execution context."""
32
+ logger.debug("Running test function: %s.%s.%s", module_path, test_class, test_function)
33
+ indent = " " # 8 spaces for function level
34
+
35
+ if self._verbose == "function":
36
+ progress = f"[{function_index + 1}/{total_functions}]" if total_functions > 0 else ""
37
+ print(f"{indent}{progress} Running test function: {test_class}.{test_function}")
38
+
39
+ try:
40
+ # Run specific test function: python -m unittest module.class.function
41
+ cmd = [self._python_path, "-m", "unittest", f"{module_path}.{test_class}.{test_function}", "-v"]
42
+ logger.debug("Executing command: %s", " ".join(cmd))
43
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
44
+
45
+ # Parse the output
46
+ output_lines = result.stdout.split('\n')
47
+ stderr_lines = result.stderr.split('\n')
48
+
49
+ # If stdout is empty but stderr has test output, use stderr
50
+ if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
51
+ logger.debug("Using stderr output for test function: %s", test_function)
52
+ output_lines = stderr_lines
53
+ stderr_lines = []
54
+
55
+ # Parse test function result
56
+ test_functions, _, _ = self._parser.parse_test_output(output_lines, stderr_lines)
57
+
58
+ if test_functions:
59
+ test_state = test_functions[0]
60
+ logger.debug("Test function %s completed with status: %s (runtime: %.3fs)",
61
+ test_function, test_state.status, test_state.runtime)
62
+ if self._verbose == "function":
63
+ status_symbol = "✓" if test_state.status == "passed" else "✗" if test_state.status == "failed" else "⚠" if test_state.status == "error" else "⏭" if test_state.status == "skipped" else "?"
64
+ print(f"{indent} {status_symbol} {test_function} ({test_state.status}) - {test_state.runtime:.3f}s")
65
+ return test_state
66
+ else:
67
+ # Create a failed state if parsing failed
68
+ logger.warning("Failed to parse test output for function: %s", test_function)
69
+ error_state = UnittestFunctionState(
70
+ function_name=test_function,
71
+ status="error",
72
+ runtime=0.0,
73
+ timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
74
+ error_message="Failed to parse test output",
75
+ warning_count=0,
76
+ should_skip=False,
77
+ skip_reason=""
78
+ )
79
+ if self._verbose == "function":
80
+ print(f"{indent} ⚠ {test_function} (error) - Failed to parse output")
81
+ return error_state
82
+
83
+ except Exception as e:
84
+ logger.error("Error running test function %s: %s", test_function, e)
85
+ error_state = UnittestFunctionState(
86
+ function_name=test_function,
87
+ status="error",
88
+ runtime=0.0,
89
+ timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
90
+ error_message=f"Error running test: {e}",
91
+ warning_count=0,
92
+ should_skip=False,
93
+ skip_reason=""
94
+ )
95
+ if self._verbose == "function":
96
+ print(f"{indent} ⚠ {test_function} (error) - {e}")
97
+ return error_state
98
+
99
+ def run_test_class(self, module_path: str, test_class: str, class_index: int = 0, total_classes: int = 0) -> List[UnittestFunctionState]:
100
+ """Run all test functions in a test class and return their states."""
101
+ logger.debug("Running test class: %s.%s", module_path, test_class)
102
+ indent = " " # 6 spaces for class level
103
+
104
+ if self._verbose in ["class", "function"]:
105
+ progress = f"[{class_index + 1}/{total_classes}]" if total_classes > 0 else ""
106
+ print(f"{indent}{progress} Running test class: {test_class}")
107
+
108
+ # Get functions for this class from discovery
109
+ class_key = f"{module_path}.{test_class}"
110
+ class_functions = self._test_discovery.functions.get(class_key, []) if self._test_discovery else []
111
+ logger.debug("Found %d functions in class %s", len(class_functions), test_class)
112
+
113
+ if not class_functions:
114
+ # Fallback: run the class as a whole
115
+ logger.debug("No discovery info for class %s, running as whole", test_class)
116
+ try:
117
+ cmd = [self._python_path, "-m", "unittest", f"{module_path}.{test_class}", "-v"]
118
+ logger.debug("Executing class command: %s", " ".join(cmd))
119
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
120
+
121
+ output_lines = result.stdout.split('\n')
122
+ stderr_lines = result.stderr.split('\n')
123
+
124
+ if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
125
+ logger.debug("Using stderr output for class: %s", test_class)
126
+ output_lines = stderr_lines
127
+ stderr_lines = []
128
+
129
+ test_functions, _, _ = self._parser.parse_test_output(output_lines, stderr_lines)
130
+ logger.debug("Class %s completed with %d test functions", test_class, len(test_functions))
131
+
132
+ if self._verbose == "function" and test_functions:
133
+ for test_func in test_functions:
134
+ status_symbol = "✓" if test_func.status == "passed" else "✗" if test_func.status == "failed" else "⚠" if test_func.status == "error" else "⏭" if test_func.status == "skipped" else "?"
135
+ print(f" {status_symbol} {test_func.function_name} ({test_func.status}) - {test_func.runtime:.3f}s")
136
+
137
+ return test_functions
138
+
139
+ except Exception as e:
140
+ logger.error("Failed to run test class %s: %s", test_class, e)
141
+ if self._verbose in ["class", "function"]:
142
+ print(f" ERROR: Failed to run test class {test_class}: {e}")
143
+ return []
144
+ else:
145
+ # Run each function individually using run_test_function
146
+ logger.debug("Running %d individual functions in class %s", len(class_functions), test_class)
147
+ test_functions = []
148
+ for i, function_name in enumerate(class_functions):
149
+ test_state = self.run_test_function(module_path, test_class, function_name, i, len(class_functions))
150
+ test_functions.append(test_state)
151
+
152
+ # Show class summary
153
+ passed = sum(1 for t in test_functions if t.status == "passed")
154
+ failed = sum(1 for t in test_functions if t.status == "failed")
155
+ errors = sum(1 for t in test_functions if t.status == "error")
156
+ skipped = sum(1 for t in test_functions if t.status == "skipped")
157
+ logger.debug("Class %s summary: %d passed, %d failed, %d errors, %d skipped",
158
+ test_class, passed, failed, errors, skipped)
159
+
160
+ if self._verbose in ["class", "function"]:
161
+ print(f"{indent} {test_class}: {passed} passed, {failed} failed, {errors} errors, {skipped} skipped")
162
+
163
+ return test_functions
164
+
165
+ def run_test_file(self, module_path: str, file_index: int = 0, total_files: int = 0) -> List[UnittestFunctionState]:
166
+ """Run all test classes in a test file and return all test function states."""
167
+ logger.debug("Running test file: %s", module_path)
168
+ indent = " " # 4 spaces for file level
169
+
170
+ if self._verbose in ["file", "class", "function"]:
171
+ progress = f"[{file_index + 1}/{total_files}]" if total_files > 0 else ""
172
+ print(f"{indent}{progress} Running test file: {module_path}")
173
+
174
+ # Get classes for this module from discovery
175
+ module_classes = self._test_discovery.classes.get(module_path, []) if self._test_discovery else []
176
+ logger.debug("Found %d classes in module %s", len(module_classes), module_path)
177
+
178
+ if not module_classes:
179
+ # Fallback: run the module as a whole
180
+ logger.debug("No discovery info for module %s, running as whole", module_path)
181
+ try:
182
+ cmd = [self._python_path, "-m", "unittest", module_path, "-v"]
183
+ logger.debug("Executing module command: %s", " ".join(cmd))
184
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
185
+
186
+ output_lines = result.stdout.split('\n')
187
+ stderr_lines = result.stderr.split('\n')
188
+
189
+ if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
190
+ logger.debug("Using stderr output for module: %s", module_path)
191
+ output_lines = stderr_lines
192
+ stderr_lines = []
193
+
194
+ test_functions, _, _ = self._parser.parse_test_output(output_lines, stderr_lines)
195
+ logger.debug("Module %s completed with %d test functions", module_path, len(test_functions))
196
+
197
+ if self._verbose in ["class", "function"] and test_functions:
198
+ # Group by test class
199
+ classes = {}
200
+ for test_func in test_functions:
201
+ class_name = "UnknownClass" # We'll improve this later
202
+ if class_name not in classes:
203
+ classes[class_name] = []
204
+ classes[class_name].append(test_func)
205
+
206
+ for class_name, class_tests in classes.items():
207
+ if self._verbose == "class":
208
+ passed = sum(1 for t in class_tests if t.status == "passed")
209
+ failed = sum(1 for t in class_tests if t.status == "failed")
210
+ errors = sum(1 for t in class_tests if t.status == "error")
211
+ print(f" {class_name}: {passed} passed, {failed} failed, {errors} errors")
212
+ elif self._verbose == "function":
213
+ print(f" {class_name}:")
214
+ for test_func in class_tests:
215
+ status_symbol = "✓" if test_func.status == "passed" else "✗" if test_func.status == "failed" else "⚠" if test_func.status == "error" else "⏭" if test_func.status == "skipped" else "?"
216
+ print(f" {status_symbol} {test_func.function_name} ({test_func.status}) - {test_func.runtime:.3f}s")
217
+
218
+ return test_functions
219
+
220
+ except Exception as e:
221
+ logger.error("Failed to run test file %s: %s", module_path, e)
222
+ if self._verbose in ["file", "class", "function"]:
223
+ print(f" ERROR: Failed to run test file {module_path}: {e}")
224
+ return []
225
+ else:
226
+ # Run each class individually using run_test_class
227
+ logger.debug("Running %d individual classes in module %s", len(module_classes), module_path)
228
+ all_test_functions = []
229
+ for i, class_name in enumerate(module_classes):
230
+ class_functions = self.run_test_class(module_path, class_name, i, len(module_classes))
231
+ all_test_functions.extend(class_functions)
232
+
233
+ # Show file summary
234
+ passed = sum(1 for t in all_test_functions if t.status == "passed")
235
+ failed = sum(1 for t in all_test_functions if t.status == "failed")
236
+ errors = sum(1 for t in all_test_functions if t.status == "error")
237
+ skipped = sum(1 for t in all_test_functions if t.status == "skipped")
238
+ logger.debug("Module %s summary: %d passed, %d failed, %d errors, %d skipped",
239
+ module_path, passed, failed, errors, skipped)
240
+
241
+ if self._verbose in ["file", "class", "function"]:
242
+ print(f"{indent} {module_path}: {passed} passed, {failed} failed, {errors} errors, {skipped} skipped")
243
+
244
+ return all_test_functions