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.
- danielutils/__init__.py +50 -0
- danielutils/abstractions/__init__.py +3 -0
- danielutils/abstractions/db/__init__.py +10 -0
- danielutils/abstractions/db/database.py +206 -0
- danielutils/abstractions/db/database_definitions.py +219 -0
- danielutils/abstractions/db/database_exceptions.py +34 -0
- danielutils/abstractions/db/database_factory.py +70 -0
- danielutils/abstractions/db/database_initializer.py +261 -0
- danielutils/abstractions/db/database_models.py +213 -0
- danielutils/abstractions/db/dependencies.py +6 -0
- danielutils/abstractions/db/implementations/__init__.py +4 -0
- danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
- danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
- danielutils/abstractions/db/implementations/redis_database.py +403 -0
- danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
- danielutils/abstractions/multiprogramming/__init__.py +3 -0
- danielutils/abstractions/multiprogramming/multi_id.py +26 -0
- danielutils/abstractions/multiprogramming/worker.py +104 -0
- danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
- danielutils/abstractions/repl.py +125 -0
- danielutils/aliases.py +174 -0
- danielutils/async_/__init__.py +7 -0
- danielutils/async_/async_cmd.py +28 -0
- danielutils/async_/async_command.py +879 -0
- danielutils/async_/async_layered_command.py +125 -0
- danielutils/async_/async_retry_executor.py +86 -0
- danielutils/async_/async_worker_pool.py +110 -0
- danielutils/async_/time_strategy.py +73 -0
- danielutils/async_/utils.py +57 -0
- danielutils/better_builtins/__init__.py +3 -0
- danielutils/better_builtins/counter.py +48 -0
- danielutils/better_builtins/frange.py +266 -0
- danielutils/better_builtins/typed_builtins/__init__.py +4 -0
- danielutils/better_builtins/typed_builtins/factory.py +126 -0
- danielutils/better_builtins/typed_builtins/tdict.py +36 -0
- danielutils/better_builtins/typed_builtins/tlist.py +223 -0
- danielutils/better_builtins/typed_builtins/tset.py +67 -0
- danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
- danielutils/colors.py +203 -0
- danielutils/context_managers/__init__.py +5 -0
- danielutils/context_managers/attr_context.py +40 -0
- danielutils/context_managers/multi_context.py +34 -0
- danielutils/context_managers/optional_context.py +33 -0
- danielutils/context_managers/state_context.py +25 -0
- danielutils/context_managers/temporary_file.py +108 -0
- danielutils/convenience.py +12 -0
- danielutils/conversions/__init__.py +2 -0
- danielutils/conversions/main_conversions.py +131 -0
- danielutils/conversions/specialized_conversions/__init__.py +2 -0
- danielutils/conversions/specialized_conversions/to_hex.py +25 -0
- danielutils/conversions/specialized_conversions/to_int.py +19 -0
- danielutils/custom_types.py +18 -0
- danielutils/data_structures/__init__.py +8 -0
- danielutils/data_structures/algorithms.py +52 -0
- danielutils/data_structures/comparer.py +47 -0
- danielutils/data_structures/default_dict.py +23 -0
- danielutils/data_structures/functions.py +35 -0
- danielutils/data_structures/graph/__init__.py +4 -0
- danielutils/data_structures/graph/binary_node.py +71 -0
- danielutils/data_structures/graph/graph.py +208 -0
- danielutils/data_structures/graph/multinode.py +101 -0
- danielutils/data_structures/graph/node.py +60 -0
- danielutils/data_structures/heap/__init__.py +3 -0
- danielutils/data_structures/heap/heap.py +119 -0
- danielutils/data_structures/heap/max_heap.py +18 -0
- danielutils/data_structures/heap/min_heap.py +18 -0
- danielutils/data_structures/queue/__init__.py +3 -0
- danielutils/data_structures/queue/atomic_queue.py +15 -0
- danielutils/data_structures/queue/priority_queue.py +110 -0
- danielutils/data_structures/queue/queue.py +92 -0
- danielutils/data_structures/stack.py +102 -0
- danielutils/data_structures/trees/__init__.py +2 -0
- danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
- danielutils/data_structures/trees/binary_tree.py +63 -0
- danielutils/date.py +43 -0
- danielutils/date_time.py +23 -0
- danielutils/decorators/__init__.py +18 -0
- danielutils/decorators/atomic.py +45 -0
- danielutils/decorators/attach.py +61 -0
- danielutils/decorators/chain_decorators.py +39 -0
- danielutils/decorators/decorate_conditionally.py +32 -0
- danielutils/decorators/delay_call.py +47 -0
- danielutils/decorators/deprecate.py +46 -0
- danielutils/decorators/final.py +39 -0
- danielutils/decorators/limit_recursion.py +63 -0
- danielutils/decorators/memo.py +68 -0
- danielutils/decorators/normalize_decorator.py +55 -0
- danielutils/decorators/overload.py +204 -0
- danielutils/decorators/partially_implemented.py +32 -0
- danielutils/decorators/processify.py +74 -0
- danielutils/decorators/property.py +42 -0
- danielutils/decorators/singleton.py +59 -0
- danielutils/decorators/threadify.py +40 -0
- danielutils/decorators/timeout.py +82 -0
- danielutils/decorators/total_ordering.py +82 -0
- danielutils/decorators/validate.py +227 -0
- danielutils/exceptions.py +90 -0
- danielutils/functions/__init__.py +13 -0
- danielutils/functions/areoneof.py +47 -0
- danielutils/functions/check_foreach.py +39 -0
- danielutils/functions/factorial.py +24 -0
- danielutils/functions/flatten.py +33 -0
- danielutils/functions/foreach.py +20 -0
- danielutils/functions/isoftype.py +475 -0
- danielutils/functions/isoneof.py +71 -0
- danielutils/functions/multiloop.py +51 -0
- danielutils/functions/parallel_for.py +47 -0
- danielutils/functions/partition.py +45 -0
- danielutils/functions/powerset.py +31 -0
- danielutils/functions/subseteq.py +25 -0
- danielutils/functions/types_subseteq.py +51 -0
- danielutils/generators/__init__.py +3 -0
- danielutils/generators/conditional_generator.py +59 -0
- danielutils/generators/generator_from_stream.py +31 -0
- danielutils/generators/join_generators.py +109 -0
- danielutils/internet.py +92 -0
- danielutils/io_.py +451 -0
- danielutils/java/__init__.py +18 -0
- danielutils/java/interfaces/__init__.py +1 -0
- danielutils/java/interfaces/comparable.py +31 -0
- danielutils/java/java_interface.py +59 -0
- danielutils/logging_/__init__.py +2 -0
- danielutils/logging_/_impl/__init__.py +4 -0
- danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
- danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
- danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
- danielutils/logging_/_impl/log_level.py +13 -0
- danielutils/logging_/_impl/logger.py +109 -0
- danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
- danielutils/logging_/utils.py +199 -0
- danielutils/lombok/__init__.py +1 -0
- danielutils/lombok/builder.py +68 -0
- danielutils/math_/__init__.py +2 -0
- danielutils/math_/constants.py +103 -0
- danielutils/math_/functions.py +34 -0
- danielutils/math_/math_print.py +52 -0
- danielutils/math_/math_symbols.py +125 -0
- danielutils/metaclasses/__init__.py +5 -0
- danielutils/metaclasses/atomic_class_meta.py +36 -0
- danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
- danielutils/metaclasses/instance_cache_meta.py +66 -0
- danielutils/metaclasses/interface.py +328 -0
- danielutils/metaclasses/overload_meta.py +48 -0
- danielutils/mock_/__init__.py +1 -0
- danielutils/mock_/mock_module.py +27 -0
- danielutils/path.py +55 -0
- danielutils/print_.py +123 -0
- danielutils/progress_bar/__init__.py +3 -0
- danielutils/progress_bar/ascii_progress_bar.py +161 -0
- danielutils/progress_bar/progress_bar.py +75 -0
- danielutils/progress_bar/progress_bar_pool.py +72 -0
- danielutils/protocols/__init__.py +3 -0
- danielutils/protocols/dictable.py +38 -0
- danielutils/protocols/evaluable.py +26 -0
- danielutils/protocols/serializable.py +51 -0
- danielutils/py.typed +0 -0
- danielutils/random_.py +47 -0
- danielutils/reflection/__init__.py +6 -0
- danielutils/reflection/declarations.py +288 -0
- danielutils/reflection/file/__init__.py +1 -0
- danielutils/reflection/file/file_reflection.py +85 -0
- danielutils/reflection/function/__init__.py +1 -0
- danielutils/reflection/function/function_reflections.py +152 -0
- danielutils/reflection/info_classes/__init__.py +6 -0
- danielutils/reflection/info_classes/argument_info.py +278 -0
- danielutils/reflection/info_classes/class_info.py +204 -0
- danielutils/reflection/info_classes/decorator_info.py +54 -0
- danielutils/reflection/info_classes/file_info.py +1180 -0
- danielutils/reflection/info_classes/folder_info.py +592 -0
- danielutils/reflection/info_classes/function_info.py +551 -0
- danielutils/reflection/info_classes/import_info.py +207 -0
- danielutils/reflection/interpreter/__init__.py +9 -0
- danielutils/reflection/interpreter/callstack.py +57 -0
- danielutils/reflection/interpreter/get_traceback.py +27 -0
- danielutils/reflection/interpreter/interpreter.py +41 -0
- danielutils/reflection/interpreter/is_debugging.py +13 -0
- danielutils/reflection/interpreter/os_.py +33 -0
- danielutils/reflection/interpreter/packages.py +33 -0
- danielutils/reflection/interpreter/python_version.py +33 -0
- danielutils/reflection/interpreter/signals.py +18 -0
- danielutils/reflection/interpreter/tracer.py +175 -0
- danielutils/reflection/module/__init__.py +3 -0
- danielutils/reflection/module/lazy_module.py +104 -0
- danielutils/reflection/module/module_reflections.py +34 -0
- danielutils/reflection/module/package_reflection.py +161 -0
- danielutils/retry_executor/__init__.py +3 -0
- danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
- danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
- danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
- danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
- danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
- danielutils/retry_executor/backoff_strategy.py +28 -0
- danielutils/retry_executor/retry_executor.py +55 -0
- danielutils/snippets/__init__.py +1 -0
- danielutils/snippets/try_get.py +28 -0
- danielutils/system/__init__.py +3 -0
- danielutils/system/independent.py +197 -0
- danielutils/system/layered_command.py +130 -0
- danielutils/system/windows/__init__.py +2 -0
- danielutils/system/windows/utils/__init__.py +1 -0
- danielutils/system/windows/utils/filetime.py +19 -0
- danielutils/system/windows/win32_ctime.py +183 -0
- danielutils/system/windows/windows.py +78 -0
- danielutils/testing/__init__.py +1 -0
- danielutils/testing/unittest_/__init__.py +2 -0
- danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
- danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
- danielutils/text.py +169 -0
- danielutils/time.py +64 -0
- danielutils/tools/__init__.py +1 -0
- danielutils/tools/unittest_test_runner/__init__.py +9 -0
- danielutils/tools/unittest_test_runner/cli.py +70 -0
- danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
- danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
- danielutils/tools/unittest_test_runner/core/execution.py +244 -0
- danielutils/tools/unittest_test_runner/core/parser.py +163 -0
- danielutils/tools/unittest_test_runner/core/runner.py +561 -0
- danielutils/tools/unittest_test_runner/models.py +82 -0
- danielutils/university/__init__.py +7 -0
- danielutils/university/computability_and_complexity/__init__.py +2 -0
- danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
- danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
- danielutils/university/computability_and_complexity/languages/language.py +17 -0
- danielutils/university/computability_and_complexity/languages/sat.py +71 -0
- danielutils/university/computability_and_complexity/turing_machine.py +47 -0
- danielutils/university/databases/__init__.py +1 -0
- danielutils/university/databases/all.py +756 -0
- danielutils/university/image_proccesing/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/__init__.py +3 -0
- danielutils/university/image_proccesing/encoding/encoding.py +35 -0
- danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
- danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
- danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
- danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
- danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
- danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
- danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
- danielutils/university/image_proccesing/tansformations/hough.py +0 -0
- danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
- danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
- danielutils/university/linear_algebra/__init__.py +1 -0
- danielutils/university/linear_algebra/matrix.py +404 -0
- danielutils/university/machine_learning/__init__.py +0 -0
- danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
- danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
- danielutils/university/machine_learning/activation_functions/relu.py +11 -0
- danielutils/university/machine_learning/neuron.py +15 -0
- danielutils/university/oop/__init__.py +1 -0
- danielutils/university/oop/observer.py +31 -0
- danielutils/university/oop/strategy.py +9 -0
- danielutils/university/probability/__init__.py +7 -0
- danielutils/university/probability/conditional_variable/__init__.py +3 -0
- danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
- danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
- danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
- danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
- danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
- danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
- danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
- danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
- danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
- danielutils/university/probability/distributions.py +35 -0
- danielutils/university/probability/expressions/__init__.py +2 -0
- danielutils/university/probability/expressions/accumulation_expression.py +291 -0
- danielutils/university/probability/expressions/probability_expression.py +110 -0
- danielutils/university/probability/funcs/__init__.py +4 -0
- danielutils/university/probability/funcs/covariance.py +12 -0
- danielutils/university/probability/funcs/expected_value.py +41 -0
- danielutils/university/probability/funcs/probability_function.py +14 -0
- danielutils/university/probability/funcs/variance.py +17 -0
- danielutils/university/probability/operator.py +79 -0
- danielutils/university/probability/protocols.py +32 -0
- danielutils/university/probability/supp.py +101 -0
- danielutils/university/probability/transformation.py +44 -0
- danielutils/versioned_imports.py +31 -0
- danielutils-1.1.28.dist-info/METADATA +390 -0
- danielutils-1.1.28.dist-info/RECORD +287 -0
- danielutils-1.1.28.dist-info/WHEEL +5 -0
- danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|