Skip to content

Commit 09043ae

Browse files
Update
1 parent 570114b commit 09043ae

File tree

4 files changed

+229
-24
lines changed

4 files changed

+229
-24
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
Программа предоставляет функциональность для визуализации данных в виде гистограмм и круговых диаграмм, отображая количество различных импортируемых библиотек и их использование в проекте. Это позволяет разработчикам увидеть, какие библиотеки они активно используют, а также улучшить управление зависимостями.
1919

2020

21+
## Принцип работы
22+
<img src="https://github.com/user-attachments/assets/2a8bd464-d3ed-41d0-88bc-ce51682ba76e" style="width: 50%;" />
23+
24+
2125

2226
## Функциональность
2327

main.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
# main.py
2+
13
import os
24
import re
35
import threading
6+
from threading import Event
47
from collections import Counter
58
import matplotlib.pyplot as plt
69
from tkinter import Tk, filedialog, Label, Button, Text, Scrollbar, Frame, Menu
@@ -10,36 +13,25 @@
1013
import queue
1114
from concurrent.futures import ThreadPoolExecutor, as_completed
1215
import ast
16+
from stats_window import open_stats_window, analyze_project_structure, parse_python_files
17+
18+
from utils import read_gitignore, is_ignored
19+
1320
init(autoreset=True)
21+
stop_event = Event()
1422

1523
# Очередь для передачи данных между потоками
1624
imports_count = {}
1725
task_queue = queue.Queue()
1826

1927

28+
29+
2030
# =========================
2131
# Функции для анализа файлов
2232
# =========================
2333

24-
def read_gitignore(directory):
25-
gitignore_path = os.path.join(directory, '.gitignore')
26-
ignored_paths = set()
2734

28-
if os.path.exists(gitignore_path):
29-
with open(gitignore_path, 'r') as f:
30-
for line in f:
31-
line = line.strip()
32-
if line and not line.startswith('#'):
33-
ignored_paths.add(line)
34-
return ignored_paths
35-
36-
37-
def is_ignored(file_path, ignored_paths):
38-
relative_path = os.path.relpath(file_path)
39-
for ignored_path in ignored_paths:
40-
if relative_path.startswith(ignored_path):
41-
return True
42-
return False
4335

4436
def get_gitignore_excluded_dirs(gitignore_path='.gitignore'):
4537
excluded_dirs = []
@@ -93,7 +85,7 @@ def find_imports_in_file(file_path):
9385

9486

9587

96-
def scan_directory_for_imports_parallel(directory, progress_label, output_text):
88+
def scan_directory_for_imports_parallel(directory, progress_label, output_text,task_queue, stop_event):
9789
global imports_count, total_imports
9890

9991
ignored_paths = read_gitignore(directory)
@@ -120,6 +112,8 @@ def scan_directory_for_imports_parallel(directory, progress_label, output_text):
120112
imports_list = []
121113

122114
def process_file(file_path):
115+
if stop_event.is_set():
116+
return []
123117
return find_imports_in_file(file_path)
124118

125119
with ThreadPoolExecutor(max_workers=20) as executor:
@@ -139,6 +133,11 @@ def process_file(file_path):
139133

140134
task_queue.put(('stats', imports_count, total_imports))
141135

136+
progress_label.config(text="Анализ структуры проекта...")
137+
progress_label.update()
138+
# После анализа импортов
139+
analyze_project_structure(directory, task_queue)
140+
142141

143142

144143

@@ -151,7 +150,7 @@ def browse_directory():
151150
if directory:
152151
excluded_dirs = get_gitignore_excluded_dirs() # Получаем исключенные директории из .gitignore
153152
threading.Thread(target=scan_directory_for_imports_parallel,
154-
args=(directory, progress_label, output_text), # Передаем только 3 аргумента
153+
args=(directory, progress_label, output_text, task_queue, stop_event), # Передаем только 3 аргумента
155154
daemon=True).start()
156155

157156

@@ -274,27 +273,46 @@ def on_copy(event=None):
274273
def update_gui():
275274
try:
276275
message = task_queue.get_nowait()
276+
277277
if isinstance(message, str):
278278
progress_label.config(text=message)
279+
279280
elif isinstance(message, tuple) and message[0] == 'stats':
280281
imports_count, total_imports = message[1], message[2]
281282
print_import_statistics(imports_count, total_imports, output_text)
282283
plot_import_statistics(imports_count, total_imports)
284+
285+
elif isinstance(message, tuple) and message[0] == 'project_stats':
286+
structure = message[1]
287+
output_text.insert("insert", "\n--- Статистика проекта ---\n")
288+
output_text.insert("insert", f"Всего файлов: {structure['total_files']}\n")
289+
output_text.insert("insert", f"Всего директорий: {structure['total_dirs']}\n")
290+
output_text.insert("insert", f"Python файлов: {structure['py_files']} (моего кода)\n")
291+
output_text.insert("insert", f"Python файлов в виртуальных/служебных папках: {structure['py_files_venv']}\n")
292+
output_text.insert("insert", f"Прочих файлов: {structure['other_files']}\n")
293+
output_text.insert("insert", f"\nСписок директорий:\n")
294+
for folder in structure['folders']:
295+
output_text.insert("insert", f" - {folder}\n")
296+
283297
except queue.Empty:
284298
pass
285-
window.after(100, update_gui) # Обновляем GUI каждую сотую долю секунды
286299

300+
window.after(100, update_gui)
287301

288302
# =========================
289303
# GUI
290304
# =========================
291305

306+
def on_closing():
307+
stop_event.set() # Сигнал остановки для потоков
308+
window.destroy() # Закрытие окна
292309

293310

294311

295312
window = Tk()
296313
window.title("Статистика импортов в проектах")
297314
window.geometry("1200x800")
315+
window.protocol("WM_DELETE_WINDOW", on_closing)
298316

299317
frame = Frame(window)
300318
frame.pack(pady=20)
@@ -333,6 +351,16 @@ def update_gui():
333351
btn_others = Button(lib_frame, text="Показать прочие библиотеки", command=lambda: show_others(imports_count, total_imports, output_text))
334352
btn_others.pack(side="left", padx=10)
335353

354+
# Получаем данные о проектах для анализа
355+
default_project_path = "E:/Code/PYTHON/projects" # <-- путь к проектам
356+
project_data = list(parse_python_files(default_project_path).values())
357+
358+
359+
# Новая кнопка: временной анализ проектов
360+
btn_stats_by_date = Button(lib_frame, text="Анализ проектов по дате", command=lambda: open_stats_window(window, project_data) )
361+
btn_stats_by_date.pack(side="left", padx=10)
362+
363+
336364
# Добавляем контекстное меню для копирования
337365
context_menu = Menu(window, tearoff=0)
338366
context_menu.add_command(label="Копировать", command=on_copy)
@@ -349,7 +377,10 @@ def show_context_menu(event):
349377
progress_label.pack(pady=10)
350378

351379
# Запуск функции обновления GUI
352-
window.after(100, update_gui)
353-
354-
window.mainloop()
380+
def periodic_check():
381+
update_gui()
382+
window.after(100, periodic_check) # каждые 100 мс проверяем очередь
355383

384+
# Запуск Tkinter
385+
periodic_check()
386+
window.mainloop()

stats_window.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# stats_window.py
2+
3+
import tkinter as tk
4+
import pandas as pd
5+
import matplotlib.pyplot as plt
6+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
7+
import os
8+
import ast
9+
import datetime
10+
11+
from utils import read_gitignore, is_ignored
12+
13+
def analyze_project_structure(directory, task_queue):
14+
ignored_paths = read_gitignore(directory)
15+
16+
structure = {
17+
'total_files': 0,
18+
'total_dirs': 0,
19+
'py_files': 0,
20+
'py_files_venv': 0,
21+
'other_files': 0,
22+
'folders': []
23+
}
24+
25+
venv_like = ('venv', '.venv', 'env', '.env', '__pycache__', '.git', '.idea', '.vscode', '.mypy_cache')
26+
27+
for root, dirs, files in os.walk(directory):
28+
# исключаем сразу ненужные папки
29+
dirs[:] = [d for d in dirs if d not in venv_like and not is_ignored(os.path.join(root, d), ignored_paths)]
30+
31+
structure['total_dirs'] += len(dirs)
32+
structure['total_files'] += len(files)
33+
34+
for file in files:
35+
file_path = os.path.join(root, file)
36+
if file.endswith('.py'):
37+
if any(p in file_path for p in venv_like) or is_ignored(file_path, ignored_paths):
38+
structure['py_files_venv'] += 1
39+
else:
40+
structure['py_files'] += 1
41+
else:
42+
structure['other_files'] += 1
43+
44+
relative_root = os.path.relpath(root, directory)
45+
structure['folders'].append(relative_root)
46+
47+
task_queue.put(('project_stats', structure))
48+
49+
50+
51+
"""
52+
def parse_python_files(projects_dir):
53+
project_stats = {}
54+
55+
for root, dirs, files in os.walk(projects_dir):
56+
project_name = os.path.relpath(root, projects_dir).split(os.sep)[0]
57+
58+
if project_name not in project_stats:
59+
project_stats[project_name] = {
60+
"py_count": 0,
61+
"libs": set(),
62+
"created": None,
63+
"dirs": set()
64+
}
65+
66+
for file in files:
67+
if file.endswith(".py"):
68+
file_path = os.path.join(root, file)
69+
70+
# Обновляем счётчик Python файлов
71+
project_stats[project_name]["py_count"] += 1
72+
73+
# Обновляем дату создания проекта
74+
creation_time = os.path.getctime(file_path)
75+
creation_date = datetime.datetime.fromtimestamp(creation_time)
76+
77+
current_created = project_stats[project_name]["created"]
78+
if current_created is None or creation_date < current_created:
79+
project_stats[project_name]["created"] = creation_date
80+
81+
# Сбор директорий
82+
rel_dir = os.path.relpath(root, os.path.join(projects_dir, project_name))
83+
if rel_dir != ".":
84+
project_stats[project_name]["dirs"].add(rel_dir)
85+
86+
# Парсим файл для библиотек
87+
try:
88+
with open(file_path, "r", encoding="utf-8") as f:
89+
node = ast.parse(f.read(), filename=file_path)
90+
91+
for sub_node in ast.walk(node):
92+
if isinstance(sub_node, ast.Import):
93+
for alias in sub_node.names:
94+
project_stats[project_name]["libs"].add(alias.name.split('.')[0])
95+
elif isinstance(sub_node, ast.ImportFrom):
96+
if sub_node.module:
97+
project_stats[project_name]["libs"].add(sub_node.module.split('.')[0])
98+
except Exception:
99+
continue
100+
101+
# Приведение к нужному формату
102+
for proj in project_stats:
103+
project_stats[proj]["libs"] = sorted(project_stats[proj]["libs"])
104+
project_stats[proj]["dirs"] = sorted(project_stats[proj]["dirs"])
105+
if project_stats[proj]["created"]:
106+
project_stats[proj]["created"] = project_stats[proj]["created"].strftime("%Y-%m-%d %H:%M:%S")
107+
108+
return project_stats
109+
"""
110+
111+
112+
def open_stats_window(root, project_data: list[dict]):
113+
stats_win = tk.Toplevel(root)
114+
stats_win.title("Статистика проектов")
115+
stats_win.geometry("800x600")
116+
117+
df = pd.DataFrame(project_data)
118+
if df.empty or 'date' not in df.columns:
119+
tk.Label(stats_win, text="Недостаточно данных для отображения статистики.").pack(pady=20)
120+
return
121+
122+
df['date'] = pd.to_datetime(df['date'])
123+
124+
stats_canvas = tk.Frame(stats_win)
125+
stats_canvas.pack(fill="both", expand=True)
126+
127+
# === График по дате создания ===
128+
fig, ax = plt.subplots(figsize=(6, 4))
129+
df_by_month = df.groupby(df['date'].dt.to_period('M')).size().sort_index()
130+
df_by_month.plot(kind='bar', ax=ax, title='Проекты по месяцам', rot=45)
131+
fig.tight_layout()
132+
133+
canvas = FigureCanvasTkAgg(fig, master=stats_canvas)
134+
canvas.draw()
135+
canvas.get_tk_widget().pack(pady=10)
136+
137+
# === Анализ технологий ===
138+
if 'stack' in df.columns:
139+
all_stacks = sum(df['stack'].tolist(), [])
140+
stack_series = pd.Series(all_stacks).value_counts()
141+
stack_text = "\n".join(f"{lang}: {count}" for lang, count in stack_series.items())
142+
else:
143+
stack_text = "Нет данных по стеку технологий."
144+
145+
tk.Label(stats_canvas, text="Анализ технологий:\n" + stack_text, justify="left", font=("Arial", 12)).pack(pady=5)
146+
147+
# === Кнопка закрытия ===
148+
tk.Button(stats_canvas, text="Закрыть", command=stats_win.destroy).pack(pady=10)

utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
3+
4+
def read_gitignore(directory):
5+
gitignore_path = os.path.join(directory, '.gitignore')
6+
ignored_paths = set()
7+
8+
if os.path.exists(gitignore_path):
9+
with open(gitignore_path, 'r') as f:
10+
for line in f:
11+
line = line.strip()
12+
if line and not line.startswith('#'):
13+
ignored_paths.add(line)
14+
return ignored_paths
15+
16+
17+
def is_ignored(file_path, ignored_paths):
18+
relative_path = os.path.relpath(file_path)
19+
for ignored_path in ignored_paths:
20+
if relative_path.startswith(ignored_path):
21+
return True
22+
return False

0 commit comments

Comments
 (0)