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,1180 @@
1
+ import ast
2
+ import tokenize
3
+ import io
4
+ import importlib.util
5
+ import inspect
6
+ import json
7
+ import logging
8
+ from collections import Counter, defaultdict
9
+ from pathlib import Path
10
+ from typing import List, Set, Dict, Any, Optional, Tuple
11
+ from enum import Enum
12
+ from dataclasses import dataclass
13
+
14
+ from .import_info import ImportInfo, ImportType
15
+ from .class_info import ClassInfo
16
+ from .function_info import FunctionInfo, FunctionStats
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class CodeQualityLevel(Enum):
22
+ """Enumeration of code quality levels."""
23
+ EXCELLENT = "excellent"
24
+ GOOD = "good"
25
+ FAIR = "fair"
26
+ POOR = "poor"
27
+ UNKNOWN = "unknown"
28
+
29
+
30
+ @dataclass
31
+ class FileComplexityStats:
32
+ """Statistics about file complexity and structure."""
33
+ total_cyclomatic_complexity: int
34
+ average_function_complexity: float
35
+ max_function_complexity: int
36
+ min_function_complexity: int
37
+ total_nesting_depth: int
38
+ average_nesting_depth: float
39
+ max_nesting_depth: int
40
+ has_nested_functions: bool
41
+ has_nested_classes: bool
42
+ inheritance_depth: int
43
+ max_inheritance_depth: int
44
+
45
+ def to_dict(self) -> Dict[str, Any]:
46
+ """Convert to dictionary for JSON serialization."""
47
+ return {
48
+ "total_cyclomatic_complexity": self.total_cyclomatic_complexity,
49
+ "average_function_complexity": round(self.average_function_complexity, 2),
50
+ "max_function_complexity": self.max_function_complexity,
51
+ "min_function_complexity": self.min_function_complexity,
52
+ "total_nesting_depth": self.total_nesting_depth,
53
+ "average_nesting_depth": round(self.average_nesting_depth, 2),
54
+ "max_nesting_depth": self.max_nesting_depth,
55
+ "has_nested_functions": self.has_nested_functions,
56
+ "has_nested_classes": self.has_nested_classes,
57
+ "inheritance_depth": self.inheritance_depth,
58
+ "max_inheritance_depth": self.max_inheritance_depth
59
+ }
60
+
61
+
62
+ @dataclass
63
+ class FileTypeStats:
64
+ """Statistics about file typing."""
65
+ fully_typed_functions: int
66
+ partially_typed_functions: int
67
+ untyped_functions: int
68
+ total_functions: int
69
+ typing_coverage: float # 0.0 to 1.0
70
+ has_generic_types: bool
71
+ has_union_types: bool
72
+ has_optional_types: bool
73
+ type_hint_usage: Dict[str, int] # Common type hints and their frequency
74
+
75
+ def to_dict(self) -> Dict[str, Any]:
76
+ """Convert to dictionary for JSON serialization."""
77
+ return {
78
+ "fully_typed_functions": self.fully_typed_functions,
79
+ "partially_typed_functions": self.partially_typed_functions,
80
+ "untyped_functions": self.untyped_functions,
81
+ "total_functions": self.total_functions,
82
+ "typing_coverage": round(self.typing_coverage, 2),
83
+ "has_generic_types": self.has_generic_types,
84
+ "has_union_types": self.has_union_types,
85
+ "has_optional_types": self.has_optional_types,
86
+ "type_hint_usage": self.type_hint_usage
87
+ }
88
+
89
+
90
+ @dataclass
91
+ class FileCodeStats:
92
+ """Statistics about file code structure."""
93
+ total_lines: int
94
+ code_lines: int
95
+ comment_lines: int
96
+ empty_lines: int
97
+ docstring_lines: int
98
+ average_line_length: float
99
+ max_line_length: int
100
+ min_line_length: int
101
+ average_function_length: float
102
+ max_function_length: int
103
+ min_function_length: int
104
+ functions_with_docstrings: int
105
+ docstring_coverage: float # 0.0 to 1.0
106
+ comment_ratio: float
107
+ blank_line_ratio: float
108
+
109
+ def to_dict(self) -> Dict[str, Any]:
110
+ """Convert to dictionary for JSON serialization."""
111
+ return {
112
+ "total_lines": self.total_lines,
113
+ "code_lines": self.code_lines,
114
+ "comment_lines": self.comment_lines,
115
+ "empty_lines": self.empty_lines,
116
+ "docstring_lines": self.docstring_lines,
117
+ "average_line_length": round(self.average_line_length, 2),
118
+ "max_line_length": self.max_line_length,
119
+ "min_line_length": self.min_line_length,
120
+ "average_function_length": round(self.average_function_length, 2),
121
+ "max_function_length": self.max_function_length,
122
+ "min_function_length": self.min_function_length,
123
+ "functions_with_docstrings": self.functions_with_docstrings,
124
+ "docstring_coverage": round(self.docstring_coverage, 2),
125
+ "comment_ratio": round(self.comment_ratio, 2),
126
+ "blank_line_ratio": round(self.blank_line_ratio, 2)
127
+ }
128
+
129
+
130
+ @dataclass
131
+ class FileStructureStats:
132
+ """Statistics about file structure."""
133
+ total_classes: int
134
+ total_functions: int
135
+ total_imports: int
136
+ used_imports: int
137
+ unused_imports: int
138
+ import_efficiency: float # 0.0 to 1.0
139
+ async_functions: int
140
+ static_methods: int
141
+ class_methods: int
142
+ instance_methods: int
143
+ properties: int
144
+ abstract_methods: int
145
+ decorators_used: List[str]
146
+ decorator_frequency: Dict[str, int]
147
+
148
+ def to_dict(self) -> Dict[str, Any]:
149
+ """Convert to dictionary for JSON serialization."""
150
+ return {
151
+ "total_classes": self.total_classes,
152
+ "total_functions": self.total_functions,
153
+ "total_imports": self.total_imports,
154
+ "used_imports": self.used_imports,
155
+ "unused_imports": self.unused_imports,
156
+ "import_efficiency": round(self.import_efficiency, 2),
157
+ "async_functions": self.async_functions,
158
+ "static_methods": self.static_methods,
159
+ "class_methods": self.class_methods,
160
+ "instance_methods": self.instance_methods,
161
+ "properties": self.properties,
162
+ "abstract_methods": self.abstract_methods,
163
+ "decorators_used": self.decorators_used,
164
+ "decorator_frequency": self.decorator_frequency
165
+ }
166
+
167
+
168
+ @dataclass
169
+ class FileStats:
170
+ """Comprehensive file statistics."""
171
+ complexity: FileComplexityStats
172
+ typing: FileTypeStats
173
+ code: FileCodeStats
174
+ structure: FileStructureStats
175
+ overall_score: float # 0.0 to 1.0
176
+ quality_assessment: str # "excellent", "good", "fair", "poor"
177
+ function_stats_summary: Dict[str, Any] # Aggregated function statistics
178
+
179
+ def to_dict(self) -> Dict[str, Any]:
180
+ """Convert to dictionary for JSON serialization."""
181
+ return {
182
+ "file_path": "", # Will be set by FileInfo
183
+ "overall_score": round(self.overall_score, 2),
184
+ "quality_assessment": self.quality_assessment,
185
+ "complexity": self.complexity.to_dict(),
186
+ "typing": self.typing.to_dict(),
187
+ "code": self.code.to_dict(),
188
+ "structure": self.structure.to_dict(),
189
+ "function_stats_summary": self.function_stats_summary
190
+ }
191
+
192
+
193
+ class FileInfo:
194
+ """
195
+ A comprehensive class for static and dynamic analysis of Python source files.
196
+
197
+ Analysis includes:
198
+ • Tokenization: token count, frequency distribution, and analysis
199
+ • Code metrics: line counts, complexity, size analysis
200
+ • Structure analysis: classes, functions, imports, and their relationships
201
+ • Import analysis: detailed import categorization and dependency tracking
202
+ • Code quality: style analysis, documentation coverage, type hints
203
+ • Dynamic analysis: runtime inspection of loaded modules
204
+ • Export capabilities: JSON, summary reports, and data structures
205
+ """
206
+
207
+ def __init__(self, file_path: str) -> None:
208
+ """
209
+ Initialize FileInfo with a file path.
210
+
211
+ :param file_path: Path to the Python source file to analyze
212
+ """
213
+ self.file_path = str(Path(file_path).resolve())
214
+ self._content: str = self._read_file()
215
+ self._tree: ast.AST = self._parse_content()
216
+ self._tokens: List[tokenize.TokenInfo] = None
217
+ self._lines: List[str] = None
218
+ self._imports: List[ImportInfo] = None
219
+ self._import_stats: Dict[str, Any] = None
220
+ self._code_metrics: Dict[str, Any] = None
221
+ self._structure_info: Dict[str, Any] = None
222
+
223
+ def _read_file(self) -> str:
224
+ """Reads and returns the file content as UTF-8 text."""
225
+ try:
226
+ with open(self.file_path, "r", encoding="utf-8") as f:
227
+ return f.read()
228
+ except Exception as e:
229
+ raise Exception(f"Error reading {self.file_path}: {e}")
230
+
231
+ def _parse_content(self) -> ast.AST:
232
+ """Parses the file content into an AST."""
233
+ try:
234
+ return ast.parse(self._content, filename=self.file_path)
235
+ except Exception as e:
236
+ raise Exception(f"Error parsing {self.file_path}: {e}")
237
+
238
+ def _tokenize(self) -> None:
239
+ """Tokenizes the file content using tokenize.tokenize()."""
240
+ self._tokens = []
241
+ stream = io.BytesIO(self._content.encode("utf-8"))
242
+ try:
243
+ for tok in tokenize.tokenize(stream.readline):
244
+ if tok.type == tokenize.ENCODING:
245
+ continue
246
+ self._tokens.append(tok)
247
+ except tokenize.TokenError as te:
248
+ print(f"Tokenize error: {te}")
249
+
250
+ def _parse_lines(self) -> None:
251
+ """Parse the file content into lines for analysis."""
252
+ self._lines = self._content.splitlines()
253
+ # Handle empty file case
254
+ if not self._lines and not self._content.strip():
255
+ self._lines = []
256
+
257
+ def _analyze_imports(self) -> None:
258
+ """Analyze imports and create comprehensive import statistics."""
259
+ if self._imports is not None:
260
+ return
261
+
262
+ self._imports = []
263
+ for node in ast.walk(self._tree):
264
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
265
+ self._imports.extend(ImportInfo.from_ast(node))
266
+
267
+ # Calculate import statistics
268
+ self._import_stats = {
269
+ 'total_imports': len(self._imports),
270
+ 'by_type': defaultdict(int),
271
+ 'by_scope': {'absolute': 0, 'relative': 0},
272
+ 'modules': set(),
273
+ 'unused_count': 0,
274
+ 'used_count': 0
275
+ }
276
+
277
+ for imp in self._imports:
278
+ self._import_stats['by_type'][imp.import_type.value] += 1
279
+ self._import_stats['by_scope']['absolute' if imp.is_absolute else 'relative'] += 1
280
+ self._import_stats['modules'].add(imp.module_name)
281
+
282
+ # Count used vs unused imports
283
+ usage = self.import_usage
284
+ self._import_stats['used_count'] = len(usage['used'])
285
+ self._import_stats['unused_count'] = len(usage['unused'])
286
+ self._import_stats['modules'] = list(self._import_stats['modules'])
287
+
288
+ def _calculate_code_metrics(self) -> None:
289
+ """Calculate comprehensive code quality and complexity metrics."""
290
+ if self._code_metrics is not None:
291
+ return
292
+
293
+ # Line metrics
294
+ total_lines = len(self.lines)
295
+ code_lines = len([line for line in self.lines if line.strip()
296
+ and not line.strip().startswith('#')])
297
+ comment_lines = len(self.comments)
298
+ blank_lines = max(0, total_lines - code_lines - comment_lines)
299
+
300
+ # Complexity metrics
301
+ function_count = len(self.function_names)
302
+ class_count = len(self.class_names)
303
+ import_count = len(self.imports)
304
+
305
+ # Calculate cyclomatic complexity (simplified)
306
+ complexity = 1 # Base complexity
307
+ for node in ast.walk(self._tree):
308
+ if isinstance(node, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.ExceptHandler)):
309
+ complexity += 1
310
+ elif isinstance(node, ast.BoolOp):
311
+ complexity += len(node.values) - 1
312
+
313
+ # Code quality indicators
314
+ has_docstrings = any(
315
+ '"""' in line or "'''" in line for line in self.lines)
316
+ has_type_hints = any(
317
+ ':' in line and '->' in line for line in self.lines)
318
+
319
+ # Determine quality level
320
+ quality_score = 0
321
+ if has_docstrings:
322
+ quality_score += 2
323
+ if has_type_hints:
324
+ quality_score += 2
325
+ if blank_lines > 0:
326
+ quality_score += 1
327
+ if complexity < 10:
328
+ quality_score += 2
329
+ elif complexity < 20:
330
+ quality_score += 1
331
+
332
+ if quality_score >= 6:
333
+ quality_level = CodeQualityLevel.EXCELLENT
334
+ elif quality_score >= 4:
335
+ quality_level = CodeQualityLevel.GOOD
336
+ elif quality_score >= 2:
337
+ quality_level = CodeQualityLevel.FAIR
338
+ elif quality_score >= 0:
339
+ quality_level = CodeQualityLevel.POOR
340
+ else:
341
+ quality_level = CodeQualityLevel.UNKNOWN
342
+
343
+ self._code_metrics = {
344
+ 'lines': {
345
+ 'total': total_lines,
346
+ 'code': code_lines,
347
+ 'comment': comment_lines,
348
+ 'blank': blank_lines,
349
+ 'comment_ratio': comment_lines / total_lines if total_lines > 0 else 0
350
+ },
351
+ 'complexity': {
352
+ 'cyclomatic': complexity,
353
+ 'function_count': function_count,
354
+ 'class_count': class_count,
355
+ 'import_count': import_count,
356
+ 'average_function_complexity': complexity / function_count if function_count > 0 else 0
357
+ },
358
+ 'quality': {
359
+ 'level': quality_level,
360
+ 'score': quality_score,
361
+ 'has_docstrings': has_docstrings,
362
+ 'has_type_hints': has_type_hints,
363
+ 'import_efficiency': len(self.imports) - len(self.import_usage['unused']) if self.imports else 0
364
+ }
365
+ }
366
+
367
+ def _analyze_structure(self) -> None:
368
+ """Analyze the structural relationships in the code."""
369
+ if self._structure_info is not None:
370
+ return
371
+
372
+ # Analyze nested structures by tracking parent-child relationships
373
+ nested_functions = []
374
+ nested_classes = []
375
+
376
+ # Build a simple parent tracking system
377
+ parent_map = {}
378
+ current_parent = None
379
+
380
+ for node in ast.walk(self._tree):
381
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
382
+ # Set parent for this node
383
+ parent_map[node] = current_parent
384
+
385
+ # Check if this is nested
386
+ if current_parent is not None:
387
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
388
+ nested_functions.append(node.name)
389
+ elif isinstance(node, ast.ClassDef):
390
+ nested_classes.append(node.name)
391
+
392
+ # Set this as current parent for children
393
+ current_parent = node
394
+ elif isinstance(node, (ast.If, ast.For, ast.While, ast.Try, ast.With)):
395
+ # These don't change the nesting level for functions/classes
396
+ pass
397
+ elif isinstance(node, ast.Module):
398
+ # Reset to module level
399
+ current_parent = None
400
+
401
+ # Analyze decorators
402
+ decorators = []
403
+ for node in ast.walk(self._tree):
404
+ if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.decorator_list:
405
+ for decorator in node.decorator_list:
406
+ if isinstance(decorator, ast.Name):
407
+ decorators.append(decorator.id)
408
+ elif isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name):
409
+ decorators.append(decorator.func.id)
410
+
411
+ # Analyze async usage
412
+ async_functions = [name for name in self.function_names if name in
413
+ [node.name for node in ast.walk(self._tree)
414
+ if isinstance(node, ast.AsyncFunctionDef)]]
415
+
416
+ self._structure_info = {
417
+ 'nested': {
418
+ 'functions': nested_functions,
419
+ 'classes': nested_classes
420
+ },
421
+ 'decorators': list(set(decorators)),
422
+ 'async': {
423
+ 'functions': async_functions,
424
+ 'count': len(async_functions)
425
+ },
426
+ 'inheritance': self._analyze_inheritance()
427
+ }
428
+
429
+ def _analyze_inheritance(self) -> Dict[str, Any]:
430
+ """Analyze class inheritance relationships."""
431
+ inheritance_info = {}
432
+
433
+ for node in ast.walk(self._tree):
434
+ if isinstance(node, ast.ClassDef):
435
+ bases = []
436
+ for base in node.bases:
437
+ if isinstance(base, ast.Name):
438
+ bases.append(base.id)
439
+ elif isinstance(base, ast.Attribute):
440
+ # Handle cases like 'module.Class'
441
+ parts = []
442
+ current = base
443
+ while isinstance(current, ast.Attribute):
444
+ parts.append(current.attr)
445
+ current = current.value
446
+ if isinstance(current, ast.Name):
447
+ parts.append(current.id)
448
+ parts.reverse()
449
+ bases.append('.'.join(parts))
450
+
451
+ inheritance_info[node.name] = {
452
+ 'bases': bases,
453
+ 'base_count': len(bases),
454
+ 'is_mixin': len(bases) > 1,
455
+ 'is_leaf': True # Will be updated below
456
+ }
457
+
458
+ # Mark classes that are used as bases
459
+ for class_name, info in inheritance_info.items():
460
+ for base_name in info['bases']:
461
+ if base_name in inheritance_info:
462
+ inheritance_info[base_name]['is_leaf'] = False
463
+
464
+ return inheritance_info
465
+
466
+ def _calculate_file_complexity_stats(self) -> FileComplexityStats:
467
+ """Calculate file complexity statistics."""
468
+ function_complexities = []
469
+ function_nesting_depths = []
470
+ has_nested_funcs = False
471
+ has_nested_classes = False
472
+ inheritance_depths = []
473
+
474
+ # Get function statistics if available
475
+ try:
476
+ dynamic_funcs = self.get_dynamic_function_info()
477
+ for func_info in dynamic_funcs:
478
+ func_stats = func_info.stats
479
+ function_complexities.append(
480
+ func_stats.complexity.cyclomatic_complexity)
481
+ function_nesting_depths.append(
482
+ func_stats.complexity.nesting_depth)
483
+ if func_stats.complexity.has_nested_functions:
484
+ has_nested_funcs = True
485
+ if func_stats.complexity.has_nested_classes:
486
+ has_nested_classes = True
487
+ except:
488
+ # Fallback to basic analysis
489
+ function_complexities = [1] # Default complexity
490
+ function_nesting_depths = [0]
491
+
492
+ # Calculate inheritance depths
493
+ inheritance_info = self._analyze_inheritance()
494
+ for class_info in inheritance_info.values():
495
+ inheritance_depths.append(class_info['base_count'])
496
+
497
+ total_cyclomatic = sum(
498
+ function_complexities) if function_complexities else 1
499
+ avg_function_complexity = sum(
500
+ function_complexities) / len(function_complexities) if function_complexities else 1
501
+ max_function_complexity = max(
502
+ function_complexities) if function_complexities else 1
503
+ min_function_complexity = min(
504
+ function_complexities) if function_complexities else 1
505
+
506
+ total_nesting = sum(
507
+ function_nesting_depths) if function_nesting_depths else 0
508
+ avg_nesting = sum(function_nesting_depths) / \
509
+ len(function_nesting_depths) if function_nesting_depths else 0
510
+ max_nesting = max(
511
+ function_nesting_depths) if function_nesting_depths else 0
512
+
513
+ inheritance_depth = sum(
514
+ inheritance_depths) if inheritance_depths else 0
515
+ max_inheritance = max(inheritance_depths) if inheritance_depths else 0
516
+
517
+ return FileComplexityStats(
518
+ total_cyclomatic_complexity=total_cyclomatic,
519
+ average_function_complexity=avg_function_complexity,
520
+ max_function_complexity=max_function_complexity,
521
+ min_function_complexity=min_function_complexity,
522
+ total_nesting_depth=total_nesting,
523
+ average_nesting_depth=avg_nesting,
524
+ max_nesting_depth=max_nesting,
525
+ has_nested_functions=has_nested_funcs,
526
+ has_nested_classes=has_nested_classes,
527
+ inheritance_depth=inheritance_depth,
528
+ max_inheritance_depth=max_inheritance
529
+ )
530
+
531
+ def _calculate_file_type_stats(self) -> FileTypeStats:
532
+ """Calculate file typing statistics."""
533
+ fully_typed = 0
534
+ partially_typed = 0
535
+ untyped = 0
536
+ total_funcs = 0
537
+ has_generics = False
538
+ has_unions = False
539
+ has_optionals = False
540
+ type_hint_usage: Dict[str, int] = defaultdict(int)
541
+
542
+ try:
543
+ dynamic_funcs = self.get_dynamic_function_info()
544
+ for func_info in dynamic_funcs:
545
+ total_funcs += 1
546
+ func_stats = func_info.stats
547
+
548
+ if func_stats.typing.is_fully_typed:
549
+ fully_typed += 1
550
+ elif func_stats.typing.has_argument_types or func_stats.typing.has_return_type:
551
+ partially_typed += 1
552
+ else:
553
+ untyped += 1
554
+
555
+ if func_stats.typing.has_generic_types:
556
+ has_generics = True
557
+ if func_stats.typing.has_union_types:
558
+ has_unions = True
559
+ if func_stats.typing.has_optional_types:
560
+ has_optionals = True
561
+
562
+ # Count type hint usage
563
+ for arg in func_info.arguments:
564
+ if arg.type:
565
+ type_hint_usage[str(arg.type)] += 1
566
+ if func_info.return_type != "None":
567
+ type_hint_usage[str(func_info.return_type)] += 1
568
+ except:
569
+ total_funcs = len(self.function_names)
570
+ fully_typed = 0
571
+ partially_typed = 0
572
+ untyped = total_funcs
573
+
574
+ typing_coverage = (fully_typed + partially_typed * 0.5) / \
575
+ total_funcs if total_funcs > 0 else 0
576
+
577
+ return FileTypeStats(
578
+ fully_typed_functions=fully_typed,
579
+ partially_typed_functions=partially_typed,
580
+ untyped_functions=untyped,
581
+ total_functions=total_funcs,
582
+ typing_coverage=typing_coverage,
583
+ has_generic_types=has_generics,
584
+ has_union_types=has_unions,
585
+ has_optional_types=has_optionals,
586
+ type_hint_usage=dict(type_hint_usage)
587
+ )
588
+
589
+ def _calculate_file_code_stats(self) -> FileCodeStats:
590
+ """Calculate file code structure statistics."""
591
+ metrics = self.code_metrics
592
+ lines = metrics['lines']
593
+
594
+ # Function length statistics
595
+ function_lengths = []
596
+ functions_with_docs = 0
597
+ total_funcs = len(self.function_names)
598
+
599
+ try:
600
+ dynamic_funcs = self.get_dynamic_function_info()
601
+ for func_info in dynamic_funcs:
602
+ func_stats = func_info.stats
603
+ function_lengths.append(func_stats.code.total_lines)
604
+ if func_stats.code.has_docstring:
605
+ functions_with_docs += 1
606
+ except:
607
+ function_lengths = [10] # Default length
608
+ functions_with_docs = 0
609
+
610
+ avg_function_length = sum(function_lengths) / \
611
+ len(function_lengths) if function_lengths else 0
612
+ max_function_length = max(function_lengths) if function_lengths else 0
613
+ min_function_length = min(function_lengths) if function_lengths else 0
614
+
615
+ docstring_coverage = functions_with_docs / total_funcs if total_funcs > 0 else 0
616
+ comment_ratio = lines['comment'] / \
617
+ lines['total'] if lines['total'] > 0 else 0
618
+ blank_line_ratio = lines['blank'] / \
619
+ lines['total'] if lines['total'] > 0 else 0
620
+
621
+ return FileCodeStats(
622
+ total_lines=lines['total'],
623
+ code_lines=lines['code'],
624
+ comment_lines=lines['comment'],
625
+ empty_lines=lines['blank'],
626
+ docstring_lines=lines['comment'], # Approximate
627
+ average_line_length=sum(
628
+ len(line) for line in self.lines) / len(self.lines) if self.lines else 0,
629
+ max_line_length=max(len(line)
630
+ for line in self.lines) if self.lines else 0,
631
+ min_line_length=min(len(line)
632
+ for line in self.lines) if self.lines else 0,
633
+ average_function_length=avg_function_length,
634
+ max_function_length=max_function_length,
635
+ min_function_length=min_function_length,
636
+ functions_with_docstrings=functions_with_docs,
637
+ docstring_coverage=docstring_coverage,
638
+ comment_ratio=comment_ratio,
639
+ blank_line_ratio=blank_line_ratio
640
+ )
641
+
642
+ def _calculate_file_structure_stats(self) -> FileStructureStats:
643
+ """Calculate file structure statistics."""
644
+ total_classes = len(self.class_names)
645
+ total_functions = len(self.function_names)
646
+ total_imports = len(self.imports)
647
+ used_imports = len(self.get_used_imports())
648
+ unused_imports = len(self.get_unused_imports())
649
+ import_efficiency = used_imports / total_imports if total_imports > 0 else 1.0
650
+
651
+ # Function type counts
652
+ async_functions = 0
653
+ static_methods = 0
654
+ class_methods = 0
655
+ instance_methods = 0
656
+ properties = 0
657
+ abstract_methods = 0
658
+
659
+ try:
660
+ dynamic_funcs = self.get_dynamic_function_info()
661
+ for func_info in dynamic_funcs:
662
+ if func_info.is_async:
663
+ async_functions += 1
664
+ if func_info.is_static_method:
665
+ static_methods += 1
666
+ elif func_info.is_class_method:
667
+ class_methods += 1
668
+ elif func_info.is_instance_method:
669
+ instance_methods += 1
670
+ if func_info.is_property:
671
+ properties += 1
672
+ if func_info.is_abstract:
673
+ abstract_methods += 1
674
+ except:
675
+ pass
676
+
677
+ # Decorator analysis
678
+ decorators = self.structure_info['decorators']
679
+ decorator_frequency = Counter(decorators)
680
+
681
+ return FileStructureStats(
682
+ total_classes=total_classes,
683
+ total_functions=total_functions,
684
+ total_imports=total_imports,
685
+ used_imports=used_imports,
686
+ unused_imports=unused_imports,
687
+ import_efficiency=import_efficiency,
688
+ async_functions=async_functions,
689
+ static_methods=static_methods,
690
+ class_methods=class_methods,
691
+ instance_methods=instance_methods,
692
+ properties=properties,
693
+ abstract_methods=abstract_methods,
694
+ decorators_used=decorators,
695
+ decorator_frequency=dict(decorator_frequency)
696
+ )
697
+
698
+ def _calculate_overall_score(self, complexity: FileComplexityStats,
699
+ typing: FileTypeStats,
700
+ code: FileCodeStats,
701
+ structure: FileStructureStats) -> Tuple[float, str]:
702
+ """Calculate overall file quality score."""
703
+ score = 0.0
704
+
705
+ # Complexity score (25% weight)
706
+ if complexity.average_function_complexity <= 5:
707
+ score += 0.25
708
+ elif complexity.average_function_complexity <= 10:
709
+ score += 0.15
710
+ elif complexity.average_function_complexity <= 20:
711
+ score += 0.05
712
+
713
+ if complexity.max_nesting_depth <= 3:
714
+ score += 0.1
715
+ elif complexity.max_nesting_depth <= 5:
716
+ score += 0.05
717
+
718
+ # Typing score (25% weight)
719
+ score += typing.typing_coverage * 0.25
720
+
721
+ # Code structure score (25% weight)
722
+ if code.docstring_coverage >= 0.8:
723
+ score += 0.1
724
+ elif code.docstring_coverage >= 0.5:
725
+ score += 0.05
726
+
727
+ if code.average_function_length <= 50:
728
+ score += 0.1
729
+ elif code.average_function_length <= 100:
730
+ score += 0.05
731
+
732
+ if code.comment_ratio >= 0.1 and code.comment_ratio <= 0.3:
733
+ score += 0.05
734
+
735
+ # Structure score (25% weight)
736
+ if structure.import_efficiency >= 0.9:
737
+ score += 0.15
738
+ elif structure.import_efficiency >= 0.7:
739
+ score += 0.1
740
+ elif structure.import_efficiency >= 0.5:
741
+ score += 0.05
742
+
743
+ if structure.total_classes <= 10:
744
+ score += 0.05
745
+ elif structure.total_classes <= 20:
746
+ score += 0.025
747
+
748
+ if structure.total_functions <= 50:
749
+ score += 0.05
750
+ elif structure.total_functions <= 100:
751
+ score += 0.025
752
+
753
+ # Determine quality level
754
+ if score >= 0.8:
755
+ quality = "excellent"
756
+ elif score >= 0.6:
757
+ quality = "good"
758
+ elif score >= 0.4:
759
+ quality = "fair"
760
+ else:
761
+ quality = "poor"
762
+
763
+ return score, quality
764
+
765
+ def _get_function_stats_summary(self) -> Dict[str, Any]:
766
+ """Get aggregated function statistics summary."""
767
+ try:
768
+ dynamic_funcs = self.get_dynamic_function_info()
769
+ if not dynamic_funcs:
770
+ return {}
771
+
772
+ # Aggregate function statistics
773
+ all_scores = []
774
+ all_complexities = []
775
+ all_lengths = []
776
+ typing_scores = []
777
+ quality_distribution: Counter[str] = Counter()
778
+
779
+ for func_info in dynamic_funcs:
780
+ func_stats = func_info.stats
781
+ all_scores.append(func_stats.overall_score)
782
+ all_complexities.append(
783
+ func_info.stats.complexity.cyclomatic_complexity)
784
+ all_lengths.append(func_info.stats.code.total_lines)
785
+ typing_scores.append(func_info.stats.typing.typing_score)
786
+ quality_distribution[func_info.stats.quality_assessment] += 1
787
+
788
+ return {
789
+ 'total_functions': len(dynamic_funcs),
790
+ 'average_function_score': sum(all_scores) / len(all_scores) if all_scores else 0,
791
+ 'average_complexity': sum(all_complexities) / len(all_complexities) if all_complexities else 0,
792
+ 'average_length': sum(all_lengths) / len(all_lengths) if all_lengths else 0,
793
+ 'average_typing_score': sum(typing_scores) / len(typing_scores) if typing_scores else 0,
794
+ 'quality_distribution': dict(quality_distribution),
795
+ 'best_function_score': max(all_scores) if all_scores else 0,
796
+ 'worst_function_score': min(all_scores) if all_scores else 0
797
+ }
798
+ except:
799
+ return {}
800
+
801
+ @property
802
+ def stats(self) -> FileStats:
803
+ """Returns comprehensive file statistics."""
804
+ # TODO: FileInfo.stats property is not yet ready for production use
805
+ # This functionality is experimental and may not provide accurate results
806
+ # The file complexity analysis, typing analysis, code structure analysis,
807
+ # and dynamic analysis methods need further testing and refinement
808
+ # before being considered stable
809
+ logger.warning(
810
+ "FileInfo.stats property is experimental and not yet ready for production use")
811
+
812
+ complexity = self._calculate_file_complexity_stats()
813
+ typing = self._calculate_file_type_stats()
814
+ code = self._calculate_file_code_stats()
815
+ structure = self._calculate_file_structure_stats()
816
+ overall_score, quality = self._calculate_overall_score(
817
+ complexity, typing, code, structure)
818
+ function_summary = self._get_function_stats_summary()
819
+
820
+ return FileStats(
821
+ complexity=complexity,
822
+ typing=typing,
823
+ code=code,
824
+ structure=structure,
825
+ overall_score=overall_score,
826
+ quality_assessment=quality,
827
+ function_stats_summary=function_summary
828
+ )
829
+
830
+ # ==================== PROPERTIES ====================
831
+
832
+ @property
833
+ def tokens(self) -> List[tokenize.TokenInfo]:
834
+ """Returns the list of tokens from the file."""
835
+ if self._tokens is None:
836
+ self._tokenize()
837
+ return self._tokens
838
+
839
+ @property
840
+ def lines(self) -> List[str]:
841
+ """Returns the list of lines from the file."""
842
+ if self._lines is None:
843
+ self._parse_lines()
844
+ return self._lines
845
+
846
+ @property
847
+ def token_count(self) -> int:
848
+ """Total number of tokens in the file."""
849
+ return len(self.tokens)
850
+
851
+ @property
852
+ def token_distribution(self) -> Dict[str, int]:
853
+ """A dictionary mapping token strings to their frequency."""
854
+ return dict(Counter(tok.string for tok in self.tokens))
855
+
856
+ @property
857
+ def comments(self) -> List[str]:
858
+ """A list of all comment strings in the file."""
859
+ return [tok.string for tok in self.tokens if tok.type == tokenize.COMMENT]
860
+
861
+ @property
862
+ def class_names(self) -> List[str]:
863
+ """Names of all classes defined in the file."""
864
+ return [node.name for node in ast.walk(self._tree) if isinstance(node, ast.ClassDef)]
865
+
866
+ @property
867
+ def function_names(self) -> List[str]:
868
+ """Names of all functions (including async) defined in the file."""
869
+ return [
870
+ node.name
871
+ for node in ast.walk(self._tree)
872
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
873
+ ]
874
+
875
+ @property
876
+ def imports(self) -> List[ImportInfo]:
877
+ """Returns a list of ImportInfo objects representing all import statements."""
878
+ if self._imports is None:
879
+ self._analyze_imports()
880
+ return self._imports
881
+
882
+ @property
883
+ def import_statistics(self) -> Dict[str, Any]:
884
+ """Returns comprehensive import statistics."""
885
+ if self._import_stats is None:
886
+ self._analyze_imports()
887
+ return self._import_stats
888
+
889
+ @property
890
+ def used_names(self) -> Set[str]:
891
+ """A set of all names (identifiers) used in the file."""
892
+ names = set()
893
+
894
+ # Add names from AST nodes
895
+ for node in ast.walk(self._tree):
896
+ if isinstance(node, ast.Name):
897
+ names.add(node.id)
898
+ elif isinstance(node, ast.Attribute):
899
+ # Handle attribute access like 'module.function'
900
+ if isinstance(node.value, ast.Name):
901
+ names.add(node.value.id)
902
+ names.add(node.attr)
903
+
904
+ # Add imported names that are used
905
+ for imp in self.imports:
906
+ if imp.effective_name:
907
+ names.add(imp.effective_name)
908
+ if imp.module_name:
909
+ # Add module name parts
910
+ for part in imp.module_name.split('.'):
911
+ if part:
912
+ names.add(part)
913
+
914
+ return names
915
+
916
+ @property
917
+ def import_usage(self) -> Dict[str, List[ImportInfo]]:
918
+ """Returns used and unused imports."""
919
+ used, unused = [], []
920
+ for imp in self.imports:
921
+ if imp.alias == "*" or imp.alias is None or imp.effective_name in self.used_names:
922
+ used.append(imp)
923
+ else:
924
+ unused.append(imp)
925
+ return {"used": used, "unused": unused}
926
+
927
+ @property
928
+ def code_metrics(self) -> Dict[str, Any]:
929
+ """Returns comprehensive code quality metrics."""
930
+ if self._code_metrics is None:
931
+ self._calculate_code_metrics()
932
+ return self._code_metrics
933
+
934
+ @property
935
+ def structure_info(self) -> Dict[str, Any]:
936
+ """Returns structural analysis information."""
937
+ if self._structure_info is None:
938
+ self._analyze_structure()
939
+ return self._structure_info
940
+
941
+ @property
942
+ def file_size(self) -> Dict[str, int]:
943
+ """Returns file size metrics in various units."""
944
+ content_bytes = len(self._content.encode('utf-8'))
945
+ return {
946
+ 'bytes': content_bytes,
947
+ 'kilobytes': content_bytes // 1024,
948
+ 'characters': len(self._content),
949
+ 'lines': len(self.lines),
950
+ 'tokens': self.token_count
951
+ }
952
+
953
+ # ==================== DYNAMIC ANALYSIS ====================
954
+
955
+ def _load_module(self) -> Any:
956
+ """Dynamically loads the file as a module and returns the module object."""
957
+ try:
958
+ module_name = "_temp_module_" + str(abs(hash(self.file_path)))
959
+ spec = importlib.util.spec_from_file_location(
960
+ module_name, self.file_path)
961
+ module = importlib.util.module_from_spec(spec)
962
+ spec.loader.exec_module(module)
963
+ return module
964
+ except Exception as e:
965
+ print(f"Error loading module from {self.file_path}: {e}")
966
+ return None
967
+
968
+ def get_dynamic_class_info(self) -> List[ClassInfo]:
969
+ """Returns detailed ClassInfo objects for all classes."""
970
+ # TODO: get_dynamic_class_info method is not yet ready for production use
971
+ # This functionality is experimental and may not provide accurate results
972
+ # Dynamic module loading and class analysis need further testing and refinement
973
+ logger.warning(
974
+ "FileInfo.get_dynamic_class_info method is experimental and not yet ready for production use")
975
+
976
+ if ClassInfo is None:
977
+ print("ClassInfo unavailable; skipping dynamic class analysis.")
978
+ return []
979
+
980
+ module = self._load_module()
981
+ if module is None:
982
+ return []
983
+
984
+ infos = []
985
+ for name, obj in inspect.getmembers(module, inspect.isclass):
986
+ if inspect.getmodule(obj) == module:
987
+ try:
988
+ infos.append(ClassInfo(obj))
989
+ except Exception as e:
990
+ print(f"Error processing class {name}: {e}")
991
+ return infos
992
+
993
+ def get_dynamic_function_info(self) -> List[FunctionInfo]:
994
+ """Returns detailed FunctionInfo objects for all functions."""
995
+ # TODO: get_dynamic_function_info method is not yet ready for production use
996
+ # This functionality is experimental and may not provide accurate results
997
+ # Dynamic module loading and function analysis need further testing and refinement
998
+ logger.warning(
999
+ "FileInfo.get_dynamic_function_info method is experimental and not yet ready for production use")
1000
+
1001
+ if FunctionInfo is None:
1002
+ print("FunctionInfo unavailable; skipping dynamic function analysis.")
1003
+ return []
1004
+
1005
+ module = self._load_module()
1006
+ if module is None:
1007
+ return []
1008
+
1009
+ infos = []
1010
+ for name, obj in inspect.getmembers(module, inspect.isfunction):
1011
+ if inspect.getmodule(obj) == module:
1012
+ try:
1013
+ infos.append(FunctionInfo(obj, module))
1014
+ except Exception as e:
1015
+ print(f"Error processing function {name}: {e}")
1016
+ return infos
1017
+
1018
+ # ==================== ANALYSIS METHODS ====================
1019
+
1020
+ def get_imports_by_type(self, import_type: ImportType) -> List[ImportInfo]:
1021
+ """Returns imports filtered by ImportType."""
1022
+ return [imp for imp in self.imports if imp.import_type == import_type]
1023
+
1024
+ def get_imports_by_scope(self, is_absolute: bool) -> List[ImportInfo]:
1025
+ """Returns imports filtered by scope (absolute or relative)."""
1026
+ return [imp for imp in self.imports if imp.is_absolute == is_absolute]
1027
+
1028
+ def get_unused_imports(self) -> List[ImportInfo]:
1029
+ """Returns list of unused imports."""
1030
+ return self.import_usage['unused']
1031
+
1032
+ def get_used_imports(self) -> List[ImportInfo]:
1033
+ """Returns list of used imports."""
1034
+ return self.import_usage['used']
1035
+
1036
+ def get_import_efficiency_score(self) -> float:
1037
+ """Returns import efficiency as a percentage."""
1038
+ if not self.imports:
1039
+ return 100.0
1040
+ return (len(self.get_used_imports()) / len(self.imports)) * 100
1041
+
1042
+ def get_complexity_assessment(self) -> str:
1043
+ """Returns a human-readable complexity assessment."""
1044
+ complexity = self.code_metrics['complexity']['cyclomatic']
1045
+ if complexity <= 5:
1046
+ return "Very Low"
1047
+ elif complexity <= 10:
1048
+ return "Low"
1049
+ elif complexity <= 20:
1050
+ return "Medium"
1051
+ elif complexity <= 50:
1052
+ return "High"
1053
+ else:
1054
+ return "Very High"
1055
+
1056
+ # ==================== EXPORT METHODS ====================
1057
+
1058
+ def to_dict(self) -> Dict[str, Any]:
1059
+ """Returns a comprehensive dictionary representation of the analysis."""
1060
+ return {
1061
+ 'file_info': {
1062
+ 'path': self.file_path,
1063
+ 'size': self.file_size
1064
+ },
1065
+ 'code_metrics': self.code_metrics,
1066
+ 'imports': {
1067
+ 'statistics': self.import_statistics,
1068
+ 'list': [imp.to_dict() for imp in self.imports],
1069
+ 'usage': {
1070
+ 'used': [imp.to_dict() for imp in self.get_used_imports()],
1071
+ 'unused': [imp.to_dict() for imp in self.get_unused_imports()]
1072
+ }
1073
+ },
1074
+ 'structure': {
1075
+ 'classes': self.class_names,
1076
+ 'functions': self.function_names,
1077
+ 'nested': self.structure_info['nested'],
1078
+ 'inheritance': self.structure_info['inheritance'],
1079
+ 'decorators': self.structure_info['decorators'],
1080
+ 'async': self.structure_info['async']
1081
+ },
1082
+ 'tokens': {
1083
+ 'count': self.token_count,
1084
+ 'distribution': self.token_distribution,
1085
+ 'comments': self.comments
1086
+ }
1087
+ }
1088
+
1089
+ def get_stats_dict(self) -> Dict[str, Any]:
1090
+ """Returns the stats in a JSON-serializable format with file path included."""
1091
+ stats = self.stats
1092
+ stats_dict = stats.to_dict()
1093
+ stats_dict["file_path"] = str(self.file_path)
1094
+ return stats_dict
1095
+
1096
+ def to_json(self, indent: int = 2) -> str:
1097
+ """Returns a JSON string representation of the analysis."""
1098
+ return json.dumps(self.to_dict(), indent=indent, default=str)
1099
+
1100
+ def export_summary(self, output_file: Optional[str] = None) -> str:
1101
+ """Exports a human-readable summary report."""
1102
+ summary = []
1103
+ summary.append(f"File Analysis Summary: {self.file_path}")
1104
+ summary.append("=" * 60)
1105
+
1106
+ # Basic metrics
1107
+ summary.append(f"\n📊 CODE METRICS:")
1108
+ summary.append(
1109
+ f" • Total Lines: {self.code_metrics['lines']['total']}")
1110
+ summary.append(f" • Code Lines: {self.code_metrics['lines']['code']}")
1111
+ summary.append(
1112
+ f" • Comment Lines: {self.code_metrics['lines']['comment']}")
1113
+ summary.append(
1114
+ f" • Blank Lines: {self.code_metrics['lines']['blank']}")
1115
+ summary.append(
1116
+ f" • Cyclomatic Complexity: {self.code_metrics['complexity']['cyclomatic']}")
1117
+ summary.append(
1118
+ f" • Quality Level: {self.code_metrics['quality']['level'].value.title()}")
1119
+
1120
+ # Structure
1121
+ summary.append(f"\n🏗️ STRUCTURE:")
1122
+ summary.append(f" • Classes: {len(self.class_names)}")
1123
+ summary.append(f" • Functions: {len(self.function_names)}")
1124
+ summary.append(
1125
+ f" • Async Functions: {self.structure_info['async']['count']}")
1126
+ summary.append(
1127
+ f" • Nested Functions: {len(self.structure_info['nested']['functions'])}")
1128
+ summary.append(
1129
+ f" • Nested Classes: {len(self.structure_info['nested']['classes'])}")
1130
+
1131
+ # Imports
1132
+ summary.append(f"\n📦 IMPORTS:")
1133
+ summary.append(f" • Total: {self.import_statistics['total_imports']}")
1134
+ summary.append(f" • Used: {self.import_statistics['used_count']}")
1135
+ summary.append(f" • Unused: {self.import_statistics['unused_count']}")
1136
+ summary.append(
1137
+ f" • Efficiency: {self.get_import_efficiency_score():.1f}%")
1138
+ summary.append(
1139
+ f" • Absolute: {self.import_statistics['by_scope']['absolute']}")
1140
+ summary.append(
1141
+ f" • Relative: {self.import_statistics['by_scope']['relative']}")
1142
+
1143
+ # Import types
1144
+ for import_type, count in self.import_statistics['by_type'].items():
1145
+ summary.append(
1146
+ f" - {import_type.replace('_', ' ').title()}: {count}")
1147
+
1148
+ summary_text = '\n'.join(summary)
1149
+
1150
+ if output_file:
1151
+ try:
1152
+ with open(output_file, 'w', encoding='utf-8') as f:
1153
+ f.write(summary_text)
1154
+ print(f"Summary exported to: {output_file}")
1155
+ except Exception as e:
1156
+ print(f"Error writing summary to {output_file}: {e}")
1157
+
1158
+ return summary_text
1159
+
1160
+ # ==================== STRING REPRESENTATIONS ====================
1161
+
1162
+ def __str__(self) -> str:
1163
+ return (f"FileInfo(path='{self.file_path}', "
1164
+ f"lines={self.code_metrics['lines']['total']}, "
1165
+ f"complexity={self.code_metrics['complexity']['cyclomatic']}, "
1166
+ f"quality={self.code_metrics['quality']['level'].value})")
1167
+
1168
+ def __repr__(self) -> str:
1169
+ return f"FileInfo('{self.file_path}')"
1170
+
1171
+
1172
+ __all__ = [
1173
+ "FileInfo",
1174
+ "CodeQualityLevel",
1175
+ "FileStats",
1176
+ "FileComplexityStats",
1177
+ "FileTypeStats",
1178
+ "FileCodeStats",
1179
+ "FileStructureStats"
1180
+ ]