Skip to content

Commit 1a50cbc

Browse files
author
PyCompiler ARK++
committed
Merge origin/main: résolution des conflits
2 parents fbd9116 + 8bb95cf commit 1a50cbc

44 files changed

Lines changed: 5677 additions & 351 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ __pycache__/
44
*$py.class
55
experimental_file/
66
*.bak
7+
building_script_pyinstaller.txt
78
# C extensions
89
*.so
910
*.swp
1011
.swp
12+
building_script_pyinstaller.txt
1113
# Distribution / packaging
1214
.Python
1315
build/
@@ -29,7 +31,7 @@ share/python-wheels/
2931
.installed.cfg
3032
*.egg
3133
MANIFEST
32-
34+
ui/6ebd46c8a6039f10a8aa16a9b56a23e6.jpg
3335
# PyInstaller
3436
# Usually these files are written by a python script from a template
3537
# before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -168,6 +170,23 @@ artifacts/
168170
*.deb
169171
*.rpm
170172

173+
# Package copy artifacts (generic safe defaults)
174+
# common destinations produced by copy/package tasks
175+
packages/
176+
copied_packages/
177+
package_copy/
178+
copied_libs/
179+
releases/
180+
out/
181+
# archives and wheels produced by copy/build
182+
*.whl
183+
*.tar.gz
184+
*.zip
185+
# prevent accidental vendor or site-packages copies
186+
vendor/
187+
third_party/
188+
site-packages-copy/
189+
171190
# Configuration files (keep templates)
172191
config.json
173192
config.yaml

Core/MainWindow.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ def __init__(self):
163163
super().__init__()
164164
global _latest_gui_instance
165165
_latest_gui_instance = self
166-
self.setWindowTitle("PyCompiler ARK++")
167-
self.setGeometry(100, 100, 1280, 720)
166+
167+
# Set window properties before UI initialization
168+
self.setWindowTitle("PyCompiler ARK - IDE")
169+
self.setGeometry(100, 100, 1400, 840)
168170
self.setAcceptDrops(True)
169171

170172
self.workspace_dir = None

Core/Venv_Manager/Manager.py

Lines changed: 267 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import shutil
88
import sys
9+
import re
910

1011
from PySide6.QtCore import QProcess, QTimer
1112
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox
@@ -20,6 +21,9 @@ class VenvManager:
2021
Responsibilities:
2122
- Manual venv selection (updates parent UI label and internal path)
2223
- Create venv if missing
24+
- Discover multiple venvs and select the best suited for the workspace
25+
- Ensure/generate project requirements automatically if missing
26+
- Provide interpreter and pip command to other components (central authority)
2327
- Check/install tools in an existing venv
2428
- Install project requirements.txt
2529
- Report/terminate active background tasks related to venv operations
@@ -120,29 +124,283 @@ def __init__(self, parent_widget):
120124
},
121125
}
122126

123-
# ---------- Public helpers for engines ----------
127+
# Centralized active workspace/venv state
128+
self.workspace_dir: str | None = None
129+
self.active_venv: dict | None = None # {'path': str, 'python': str, 'score': float, 'distance': int}
130+
self._project_requirements_path: str | None = None
131+
132+
# ---------- Public helpers for engines and analyzers ----------
133+
def set_selected_workspace(self, workspace_dir: str):
134+
"""Set the active workspace, ensure requirements, discover/select venv, and cache interpreter/pip."""
135+
try:
136+
self.workspace_dir = os.path.abspath(workspace_dir) if workspace_dir else None
137+
if not self.workspace_dir or not os.path.isdir(self.workspace_dir):
138+
self._safe_log("❌ Workspace invalide pour VenvManager.")
139+
return
140+
# Ensure requirements file exists
141+
self._project_requirements_path = self.ensure_project_requirements(self.workspace_dir)
142+
# Discover venvs
143+
venvs = self.discover_venvs(self.workspace_dir)
144+
# Select best venv (may be None)
145+
reqs = self.get_project_requirements(self.workspace_dir)
146+
best = self.select_best_venv(self.workspace_dir, venvs, reqs)
147+
self.active_venv = best
148+
if best and best.get('python'):
149+
self._safe_log(f"ℹ️ Venv sélectionné: {best.get('path')} (score={best.get('score', 0):.2f}, distance={best.get('distance', 0)})")
150+
else:
151+
self._safe_log("ℹ️ Aucun venv approprié trouvé. Les opérations pip utiliseront l'interpréteur courant.")
152+
except Exception as e:
153+
self._safe_log(f"⚠️ set_selected_workspace: {e}")
154+
124155
def resolve_project_venv(self) -> str | None:
125-
"""Resolve the venv root to use based on manual selection or workspace.
126-
Prefers an existing .venv over venv; if none exists, returns the default path (.venv).
127-
"""
156+
"""Resolve the venv root to use based on manual selection or discovered best venv."""
128157
try:
129158
manual = getattr(self.parent, "venv_path_manuel", None)
130159
if manual:
131-
base = os.path.abspath(manual)
132-
return base
133-
if getattr(self.parent, "workspace_dir", None):
134-
base = os.path.abspath(self.parent.workspace_dir)
135-
existing, default_path = self._detect_venv_in(base)
160+
return os.path.abspath(manual)
161+
if self.active_venv and self.active_venv.get('path'):
162+
return self.active_venv['path']
163+
if self.workspace_dir:
164+
existing, default_path = self._detect_venv_in(self.workspace_dir)
136165
return existing or default_path
137166
except Exception:
138167
return None
139168
return None
140169

170+
def get_active_interpreter(self) -> str | None:
171+
"""Return the python interpreter path of the selected venv, if any."""
172+
try:
173+
root = self.resolve_project_venv()
174+
if root:
175+
return self.python_path(root)
176+
except Exception:
177+
pass
178+
return None
179+
180+
def get_pip_command(self) -> tuple[str, list[str]]:
181+
"""Return (program, prefix_args) for invoking pip as python -m pip in the active venv (or current python)."""
182+
prog = self.get_active_interpreter() or sys.executable
183+
return prog, ["-m", "pip"]
184+
141185
def pip_path(self, venv_root: str) -> str:
142186
return os.path.join(
143187
venv_root, "Scripts" if platform.system() == "Windows" else "bin", "pip"
144188
)
145189

190+
# ---------- Discovery/select best venv ----------
191+
def discover_venvs(self, workspace_dir: str, max_depth: int = 3) -> list[dict]:
192+
"""Scan workspace for venv-like folders and return list of dicts with path, python, distance."""
193+
results: list[dict] = []
194+
try:
195+
ws = os.path.abspath(workspace_dir)
196+
candidates = {"venv", ".venv", "env", ".env", "ev", ".ev"}
197+
for root, dirs, files in os.walk(ws):
198+
# compute depth relative to ws
199+
rel = os.path.relpath(root, ws)
200+
depth = 0 if rel == '.' else len(rel.split(os.sep))
201+
if depth > max_depth:
202+
# prune deeper levels
203+
dirs[:] = []
204+
continue
205+
# if this folder itself is a venv root candidate name
206+
base = os.path.basename(root)
207+
if base in candidates:
208+
py = self.python_path(root)
209+
if os.path.isfile(py) and os.path.isfile(os.path.join(root, 'pyvenv.cfg')):
210+
results.append({
211+
'path': root,
212+
'python': py,
213+
'distance': depth,
214+
})
215+
# do not dive into venv internals further
216+
dirs[:] = []
217+
continue
218+
# Also prune typical venv internals for performance
219+
if base in (".git", "__pycache__", "node_modules"):
220+
dirs[:] = []
221+
continue
222+
except Exception as e:
223+
self._safe_log(f"⚠️ discover_venvs: {e}")
224+
# Manual venv takes precedence if set
225+
manual = getattr(self.parent, "venv_path_manuel", None)
226+
if manual:
227+
mpath = os.path.abspath(manual)
228+
py = self.python_path(mpath)
229+
if os.path.isfile(py):
230+
return [{'path': mpath, 'python': py, 'distance': 0}] + [v for v in results if v['path'] != mpath]
231+
return results
232+
233+
def get_project_requirements(self, workspace_dir: str) -> set[str]:
234+
"""Return a normalized set of requirement package names for the project.
235+
Looks for requirements.txt/constraints.txt/pyproject.toml; if none and a cached path exists, use it."""
236+
reqs: set[str] = set()
237+
try:
238+
ws = os.path.abspath(workspace_dir)
239+
# requirements.txt
240+
req_path = os.path.join(ws, "requirements.txt")
241+
if os.path.isfile(req_path):
242+
with open(req_path, encoding="utf-8", errors="ignore") as f:
243+
for line in f:
244+
line = line.strip()
245+
if not line or line.startswith('#'):
246+
continue
247+
# strip version specifiers
248+
pkg = re.split(r"[<>=!~]", line, 1)[0].strip()
249+
if pkg:
250+
reqs.add(pkg)
251+
return reqs
252+
# constraints.txt
253+
c_path = os.path.join(ws, "constraints.txt")
254+
if os.path.isfile(c_path):
255+
with open(c_path, encoding="utf-8", errors="ignore") as f:
256+
for line in f:
257+
line = line.strip()
258+
if not line or line.startswith('#'):
259+
continue
260+
pkg = re.split(r"[<>=!~]", line, 1)[0].strip()
261+
if pkg:
262+
reqs.add(pkg)
263+
return reqs
264+
# pyproject.toml (basic read without tomli dependency)
265+
pp = os.path.join(ws, "pyproject.toml")
266+
if os.path.isfile(pp):
267+
try:
268+
import tomllib # py311+
269+
except Exception:
270+
tomllib = None
271+
if tomllib:
272+
try:
273+
with open(pp, 'rb') as f:
274+
data = tomllib.load(f)
275+
deps = data.get('project', {}).get('dependencies', []) or []
276+
for d in deps:
277+
pkg = re.split(r"[<>=!~]", d, 1)[0].strip()
278+
if pkg:
279+
reqs.add(pkg)
280+
return reqs
281+
except Exception:
282+
pass
283+
except Exception:
284+
pass
285+
return reqs
286+
287+
def ensure_project_requirements(self, workspace_dir: str) -> str | None:
288+
"""Ensure the project has a requirements.txt; if missing, generate it via dependency analysis."""
289+
try:
290+
ws = os.path.abspath(workspace_dir)
291+
req_path = os.path.join(ws, "requirements.txt")
292+
if os.path.isfile(req_path):
293+
return req_path
294+
# Try to generate: basic import scan from Python files in workspace
295+
pkgs = self._analyze_workspace_imports(ws)
296+
if pkgs:
297+
with open(req_path, 'w', encoding='utf-8') as f:
298+
for p in sorted(pkgs):
299+
f.write(p + "\n")
300+
self._safe_log(f"📝 requirements.txt généré ({len(pkgs)} packages).")
301+
return req_path
302+
return None
303+
except Exception as e:
304+
self._safe_log(f"⚠️ ensure_project_requirements: {e}")
305+
return None
306+
307+
def select_best_venv(self, workspace_dir: str, venvs: list[dict], requirements: set[str]) -> dict | None:
308+
"""Select the best venv based on distance to root and how many deps are already satisfied."""
309+
if not venvs:
310+
return None
311+
try:
312+
# Precompute installed pkgs once per venv using 'pip list --format=json'
313+
enriched = []
314+
for v in venvs:
315+
installed = self._pip_list_names(v['python'])
316+
if installed is None:
317+
score = 0.0
318+
else:
319+
have = sum(1 for r in requirements if r.lower() in installed)
320+
total = len(requirements) or 1
321+
score = have / total
322+
v2 = dict(v)
323+
v2['score'] = score
324+
enriched.append(v2)
325+
# Choose: minimal distance, then maximal score
326+
enriched.sort(key=lambda x: (x.get('distance', 0), -x.get('score', 0.0)))
327+
return enriched[0]
328+
except Exception as e:
329+
self._safe_log(f"⚠️ select_best_venv: {e}")
330+
return venvs[0]
331+
332+
# ---------- Internal helpers ----------
333+
def _pip_list_names(self, python_exe: str) -> set[str] | None:
334+
try:
335+
import subprocess, json
336+
cp = subprocess.run([
337+
python_exe, '-m', 'pip', 'list', '--format', 'json'
338+
], capture_output=True, text=True)
339+
if cp.returncode != 0:
340+
return None
341+
data = json.loads(cp.stdout.strip() or '[]')
342+
names = { (item.get('name') or '').lower() for item in data if item.get('name') }
343+
return names
344+
except Exception:
345+
return None
346+
347+
def _analyze_workspace_imports(self, workspace_dir: str) -> set[str]:
348+
"""Very lightweight import analyzer to extract top-level packages from .py files, excluding stdlib and venv dirs."""
349+
pkgs: set[str] = set()
350+
try:
351+
import ast, sysconfig, pkgutil, re as _re
352+
ws = os.path.abspath(workspace_dir)
353+
# Exclude common venv dirs
354+
exclude_dirs = {".venv", "venv", "env", ".env", "ev", ".ev", "__pycache__", ".git"}
355+
for root, dirs, files in os.walk(ws):
356+
base = os.path.basename(root)
357+
if base in exclude_dirs:
358+
dirs[:] = []
359+
continue
360+
# prune venv internals
361+
for d in list(dirs):
362+
if d in exclude_dirs:
363+
dirs.remove(d)
364+
for fn in files:
365+
if not fn.endswith('.py'):
366+
continue
367+
fpath = os.path.join(root, fn)
368+
try:
369+
with open(fpath, encoding='utf-8') as f:
370+
src = f.read()
371+
tree = ast.parse(src, filename=fpath)
372+
for node in ast.walk(tree):
373+
if isinstance(node, ast.Import):
374+
for alias in node.names:
375+
pkgs.add(alias.name.split('.') [0])
376+
elif isinstance(node, ast.ImportFrom):
377+
if node.module:
378+
pkgs.add(node.module.split('.') [0])
379+
# dynamic imports
380+
dyn = _re.findall(r"__import__\(['\"]([\w\.]+)['\"]\)", src)
381+
pkgs.update([m.split('.')[0] for m in dyn])
382+
mod = _re.findall(r"importlib\.import_module\(['\"]([\w\.]+)['\"]\)", src)
383+
pkgs.update([m.split('.')[0] for m in mod])
384+
except Exception:
385+
continue
386+
# remove stdlib and common internals
387+
std = set(sys.builtin_module_names)
388+
try:
389+
import sysconfig as _sc
390+
stdlib_path = _sc.get_path('stdlib') or ''
391+
stdlib_path = os.path.realpath(stdlib_path)
392+
# best effort: also add stdlib discovered modules
393+
for _, name, _ in pkgutil.iter_modules([stdlib_path]):
394+
std.add(name)
395+
except Exception:
396+
pass
397+
# Heuristic removal of common internals
398+
core_names = {"Core", "ENGINES", "Plugins", "engine_sdk", "Plugins_SDK"}
399+
out = {m for m in pkgs if m not in std and m not in core_names}
400+
return out
401+
except Exception:
402+
return set()
403+
146404
def python_path(self, venv_root: str) -> str:
147405
base = os.path.join(
148406
venv_root, "Scripts" if platform.system() == "Windows" else "bin"

0 commit comments

Comments
 (0)