Compare commits
4 Commits
v.0.0.0.20
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a675da959 | ||
|
|
c67280df44 | ||
|
|
ea1d31b82e | ||
|
|
0901887a3e |
@ -53,9 +53,82 @@ The interface is divided into functional zones to keep the workflow organized.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Step-by-Step Guide
|
## 4. Configuration and Settings
|
||||||
|
|
||||||
### 4.1 First Run & Profile Configuration
|
### 4.1 Configuration Files Location
|
||||||
|
|
||||||
|
PyUCC stores all configuration in the application directory (where the executable or source code is located):
|
||||||
|
|
||||||
|
- **`profiles.json`** - Stores all your analysis profiles (project paths, filters, ignore patterns)
|
||||||
|
- **`settings.json`** - Stores application settings (baseline directory, retention policy, duplicates parameters)
|
||||||
|
|
||||||
|
**Location:**
|
||||||
|
- If running from source: in the repository root folder (e.g., `C:\src\PyUcc\`)
|
||||||
|
- If running from compiled exe: in the same folder as the executable
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Fully portable application - copy the entire folder to move your setup
|
||||||
|
- Easy to backup - just backup the application folder
|
||||||
|
- No hidden files in user's home directory
|
||||||
|
|
||||||
|
### 4.2 Application Settings (Settings.json)
|
||||||
|
|
||||||
|
You can configure PyUCC's behavior through the **⚙️ Settings** menu in the top bar.
|
||||||
|
|
||||||
|
**Available Settings:**
|
||||||
|
|
||||||
|
1. **Baseline Directory**
|
||||||
|
- **What it is:** Where PyUCC stores baseline snapshots.
|
||||||
|
- **Default:** `baseline/` subfolder in the application directory.
|
||||||
|
- **Recommendation:** Use the default, or set a custom path if you want to store baselines on a network drive or separate disk.
|
||||||
|
- **Example:** `D:\ProjectBaselines\` or `\\server\share\baselines\`
|
||||||
|
|
||||||
|
2. **Max Baselines to Keep**
|
||||||
|
- **What it is:** Maximum number of baseline snapshots to retain per profile.
|
||||||
|
- **Default:** 5
|
||||||
|
- **Behavior:** When exceeded, PyUCC automatically deletes the oldest baselines.
|
||||||
|
- **Recommendation:**
|
||||||
|
- 3-5 for small projects
|
||||||
|
- 10+ for critical projects requiring long history
|
||||||
|
- 20+ if disk space is not a concern
|
||||||
|
|
||||||
|
3. **Zip Baselines**
|
||||||
|
- **What it is:** Whether to compress baseline snapshots as `.zip` files.
|
||||||
|
- **Default:** `false` (disabled)
|
||||||
|
- **Advantages when enabled:**
|
||||||
|
- Saves disk space (50-80% reduction for source code)
|
||||||
|
- Faster to transfer/backup
|
||||||
|
- **Disadvantages:**
|
||||||
|
- Slightly slower to create/compare (compression overhead)
|
||||||
|
- Cannot browse snapshot files directly
|
||||||
|
- **Recommendation:** Enable for large projects (>10,000 files) or when disk space is limited.
|
||||||
|
|
||||||
|
4. **Duplicates Settings** (stored automatically)
|
||||||
|
- Threshold, k-gram size, winnowing window
|
||||||
|
- These are saved when you use the Duplicates feature
|
||||||
|
- See Section 9 for detailed explanation
|
||||||
|
|
||||||
|
**How to Configure:**
|
||||||
|
|
||||||
|
1. Click **⚙️ Settings** in the top bar.
|
||||||
|
2. Set your preferred baseline directory (or leave default).
|
||||||
|
3. Set max baselines to keep.
|
||||||
|
4. Check "Zip baselines" if desired.
|
||||||
|
5. Click **Save**.
|
||||||
|
|
||||||
|
**First-Time Setup Recommendation:**
|
||||||
|
|
||||||
|
At first run, PyUCC will use sensible defaults:
|
||||||
|
- Baselines stored in `baseline/` subfolder
|
||||||
|
- Keep last 5 baselines
|
||||||
|
- No compression
|
||||||
|
|
||||||
|
**You should configure Settings if:**
|
||||||
|
- You want baselines on a different drive (e.g., network storage, external disk)
|
||||||
|
- You need to keep more baseline history
|
||||||
|
- You're running out of disk space and want compression
|
||||||
|
|
||||||
|
### 4.3 First Run & Profile Configuration
|
||||||
The first thing to do upon opening PyUCC is to define *what* to analyze.
|
The first thing to do upon opening PyUCC is to define *what* to analyze.
|
||||||
|
|
||||||
1. Click on **⚙️ Manage...** in the top bar.
|
1. Click on **⚙️ Manage...** in the top bar.
|
||||||
@ -66,7 +139,7 @@ The first thing to do upon opening PyUCC is to define *what* to analyze.
|
|||||||
6. In the **Ignore patterns** box, you can keep the defaults (which already exclude `.git`, `__pycache__`, etc.).
|
6. In the **Ignore patterns** box, you can keep the defaults (which already exclude `.git`, `__pycache__`, etc.).
|
||||||
7. Click **💾 Save**.
|
7. Click **💾 Save**.
|
||||||
|
|
||||||
### 4.2 Simple Analysis (Scan, Countings, Metrics)
|
### 4.4 Simple Analysis (Scan, Countings, Metrics)
|
||||||
If you only want to analyze the current state without comparisons:
|
If you only want to analyze the current state without comparisons:
|
||||||
|
|
||||||
* **🔍 Scan:** Simply verifies which files are found based on the profile filters. Useful to check if you are including the right files.
|
* **🔍 Scan:** Simply verifies which files are found based on the profile filters. Useful to check if you are including the right files.
|
||||||
@ -75,7 +148,7 @@ If you only want to analyze the current state without comparisons:
|
|||||||
|
|
||||||
> **Tip:** You can double-click on a file in the results table to open it in the built-in **File Viewer**, which provides syntax highlighting and a colored minimap (blue=code, green=comments).
|
> **Tip:** You can double-click on a file in the results table to open it in the built-in **File Viewer**, which provides syntax highlighting and a colored minimap (blue=code, green=comments).
|
||||||
|
|
||||||
### 4.3 The "Differing" Workflow (Comparison)
|
### 4.5 The "Differing" Workflow (Comparison)
|
||||||
This is PyUCC's most powerful feature.
|
This is PyUCC's most powerful feature.
|
||||||
|
|
||||||
**Step A: Create the First Baseline**
|
**Step A: Create the First Baseline**
|
||||||
|
|||||||
@ -55,7 +55,67 @@ L'interfaccia è divisa in zone funzionali per mantenere il flusso di lavoro ord
|
|||||||
|
|
||||||
## 4. Guida Passo-Passo
|
## 4. Guida Passo-Passo
|
||||||
|
|
||||||
### 4.1 Primo Avvio e Configurazione Profilo
|
### 4.1 File di Configurazione e Posizione
|
||||||
|
PyUCC salva tutte le sue configurazioni in file JSON nella stessa cartella dell'eseguibile, rendendo il software completamente portatile.
|
||||||
|
|
||||||
|
**File di configurazione:**
|
||||||
|
- **`profiles.json`**: Contiene tutti i profili di analisi salvati (percorsi, filtri, impostazioni).
|
||||||
|
- **`settings.json`**: Contiene le impostazioni globali dell'applicazione (cartella baseline, duplicati, ecc.).
|
||||||
|
|
||||||
|
**Posizione:**
|
||||||
|
- **Esecuzione da eseguibile**: I file si trovano nella stessa cartella di `pyucc.exe`
|
||||||
|
- **Esecuzione da sorgente**: I file si trovano nella cartella radice del progetto (accanto a `README.md`)
|
||||||
|
|
||||||
|
Questi file vengono creati automaticamente al primo avvio dell'applicazione. Se non esistono, PyUCC utilizza valori predefiniti.
|
||||||
|
|
||||||
|
### 4.2 Impostazioni dell'Applicazione (settings.json)
|
||||||
|
Per configurare correttamente PyUCC al primo avvio, è importante comprendere le impostazioni disponibili:
|
||||||
|
|
||||||
|
**`baseline_dir` (Cartella Baseline)**
|
||||||
|
- **Cosa fa**: Specifica dove vengono salvate le baseline (snapshot del progetto per i confronti).
|
||||||
|
- **Predefinito**: `./baseline` (nella cartella dell'eseguibile)
|
||||||
|
- **Come configurare**:
|
||||||
|
- Vai su **⚙️ Settings** → **Baseline Directory**
|
||||||
|
- Scegli una cartella dedicata (es. `C:\MyProjects\baselines`)
|
||||||
|
- **Quando configurare**: Al primo avvio, soprattutto se hai più progetti da analizzare
|
||||||
|
- **Consiglio**: Usa una cartella separata per ogni progetto o una cartella centrale con sottocartelle
|
||||||
|
|
||||||
|
**`max_keep` (Massime Baseline da Mantenere)**
|
||||||
|
- **Cosa fa**: Numero massimo di baseline da conservare per ogni profilo.
|
||||||
|
- **Predefinito**: 10
|
||||||
|
- **Come configurare**: Vai su **⚙️ Settings** → **Max Baseline To Keep**
|
||||||
|
- **Quando configurare**: Se lavori su progetti grandi o hai poco spazio disco
|
||||||
|
- **Consiglio**: 5-10 per progetti piccoli, 20-30 per progetti grandi con molte release
|
||||||
|
|
||||||
|
**`zip_baselines` (Comprimi Baseline)**
|
||||||
|
- **Cosa fa**: Comprime automaticamente le baseline per risparmiare spazio.
|
||||||
|
- **Predefinito**: `true`
|
||||||
|
- **Come configurare**: Vai su **⚙️ Settings** → checkbox **Zip Baselines**
|
||||||
|
- **Quando configurare**: Lascia abilitato a meno che non abbia problemi di performance
|
||||||
|
- **Consiglio**: Tienilo attivo, riduce l'uso del disco del 70-90%
|
||||||
|
|
||||||
|
**Impostazioni Duplicati (`duplicates_settings`)**
|
||||||
|
- **`threshold`** (Soglia di Similarità Fuzzy): 0.0-1.0, default 0.85
|
||||||
|
- Valori più alti = meno falsi positivi, ma potrebbero perdere duplicati reali
|
||||||
|
- Valori più bassi = più duplicati trovati, ma più falsi positivi
|
||||||
|
- **`k`** (K-gram Size): 3-10, default 5
|
||||||
|
- K-gram più grandi = meno sensibile a piccole modifiche
|
||||||
|
- K-gram più piccoli = più sensibile a piccole modifiche
|
||||||
|
- **`window`** (Finestra Winnowing): 3-20, default 4
|
||||||
|
- Finestre più grandi = meno hash da confrontare (più veloce ma meno preciso)
|
||||||
|
- Finestre più piccole = più hash da confrontare (più lento ma più preciso)
|
||||||
|
|
||||||
|
**Come configurare i duplicati:**
|
||||||
|
1. Vai su **⚙️ Settings** → **Duplicates Detection**
|
||||||
|
2. Regola i parametri in base alle tue esigenze
|
||||||
|
3. Fai test con diversi valori sul tuo codice
|
||||||
|
|
||||||
|
**Raccomandazioni per il primo avvio:**
|
||||||
|
1. **Configura `baseline_dir`** prima di creare la prima baseline
|
||||||
|
2. Lascia gli altri valori predefiniti e regolali solo se necessario
|
||||||
|
3. Le impostazioni si applicano a tutti i profili
|
||||||
|
|
||||||
|
### 4.3 Configurazione Profilo
|
||||||
Non appena apri PyUCC, la prima cosa da fare è dire al programma *cosa* analizzare.
|
Non appena apri PyUCC, la prima cosa da fare è dire al programma *cosa* analizzare.
|
||||||
|
|
||||||
1. Clicca su **⚙️ Manage...** in alto.
|
1. Clicca su **⚙️ Manage...** in alto.
|
||||||
@ -66,7 +126,7 @@ Non appena apri PyUCC, la prima cosa da fare è dire al programma *cosa* analizz
|
|||||||
6. Nella casella **Ignore patterns**, puoi lasciare i default (che escludono già `.git`, `__pycache__`, ecc.).
|
6. Nella casella **Ignore patterns**, puoi lasciare i default (che escludono già `.git`, `__pycache__`, ecc.).
|
||||||
7. Clicca **💾 Save**.
|
7. Clicca **💾 Save**.
|
||||||
|
|
||||||
### 4.2 Analisi Semplice (Scan, Countings, Metrics)
|
### 4.4 Analisi Semplice (Scan, Countings, Metrics)
|
||||||
Se vuoi solo analizzare lo stato attuale senza confronti:
|
Se vuoi solo analizzare lo stato attuale senza confronti:
|
||||||
|
|
||||||
* **🔍 Scan:** Verifica semplicemente quali file vengono trovati in base ai filtri del profilo. Utile per controllare se stai includendo i file giusti.
|
* **🔍 Scan:** Verifica semplicemente quali file vengono trovati in base ai filtri del profilo. Utile per controllare se stai includendo i file giusti.
|
||||||
@ -75,7 +135,7 @@ Se vuoi solo analizzare lo stato attuale senza confronti:
|
|||||||
|
|
||||||
> **Tip:** Puoi fare doppio click su un file nella tabella dei risultati per aprirlo nel **File Viewer** integrato, che evidenzia la sintassi e mostra una minimappa colorata (blu=codice, verde=commenti).
|
> **Tip:** Puoi fare doppio click su un file nella tabella dei risultati per aprirlo nel **File Viewer** integrato, che evidenzia la sintassi e mostra una minimappa colorata (blu=codice, verde=commenti).
|
||||||
|
|
||||||
### 4.3 Il Flusso di Lavoro "Differing" (Confronto)
|
### 4.5 Il Flusso di Lavoro "Differing" (Confronto)
|
||||||
Questa è la funzione più potente di PyUCC.
|
Questa è la funzione più potente di PyUCC.
|
||||||
|
|
||||||
**Passo A: Creare la prima Baseline**
|
**Passo A: Creare la prima Baseline**
|
||||||
|
|||||||
BIN
doc/PyUCC_SUM_20251212.pdf
Normal file
BIN
doc/PyUCC_SUM_20251212.pdf
Normal file
Binary file not shown.
BIN
doc/PyUCC_SUM_20251212_ENG.pdf
Normal file
BIN
doc/PyUCC_SUM_20251212_ENG.pdf
Normal file
Binary file not shown.
BIN
doc/PyUCC_SUM_20251212_ITA.pdf
Normal file
BIN
doc/PyUCC_SUM_20251212_ITA.pdf
Normal file
Binary file not shown.
BIN
doc/_SUM_20251212.pdf
Normal file
BIN
doc/_SUM_20251212.pdf
Normal file
Binary file not shown.
BIN
doc/_SUM___20251212.docx
Normal file
BIN
doc/_SUM___20251212.docx
Normal file
Binary file not shown.
BIN
doc/_SUM___20251212.pdf
Normal file
BIN
doc/_SUM___20251212.pdf
Normal file
Binary file not shown.
7
hooks/hook-pygount.py
Normal file
7
hooks/hook-pygount.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
# Collect all package data, binaries and hidden imports from pygount
|
||||||
|
datas, binaries, hiddenimports = collect_all('pygount')
|
||||||
|
|
||||||
|
# Export to PyInstaller
|
||||||
|
__all__ = ['datas', 'binaries', 'hiddenimports']
|
||||||
@ -143,8 +143,8 @@
|
|||||||
{
|
{
|
||||||
"name": "DSP",
|
"name": "DSP",
|
||||||
"paths": [
|
"paths": [
|
||||||
"C:\\__temp\\Metrics\\attuale\\REP\\Projects\\DSP",
|
"C:\\src\\____GitProjects\\SXXXXXXX_PyUcc\\__UCC\\Metrics\\_25_11\\REP\\Projects\\DSP",
|
||||||
"C:\\__temp\\Metrics\\attuale\\REP\\Projects\\DspAlgorithms"
|
"C:\\src\\____GitProjects\\SXXXXXXX_PyUcc\\__UCC\\Metrics\\_25_11\\REP\\Projects\\DspAlgorithms"
|
||||||
],
|
],
|
||||||
"languages": [
|
"languages": [
|
||||||
"C",
|
"C",
|
||||||
|
|||||||
23
pyucc.spec.bak
Normal file
23
pyucc.spec.bak
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
# Ensure pygount package contents are explicitly collected (datas, binaries, hiddenimports)
|
||||||
|
_pygount_datas, _pygount_binaries, _pygount_hiddenimports = collect_all('pygount')
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
a = Analysis(pathex=['pyucc', '.'], binaries=[], datas=[('PyUcc.ico', '.'), ('external\\python-tkinter-logger\\tkinter_logger.py', '.'), ('external\\python-resource-monitor\\resource_monitor.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger'), ('external\\_setup_paths.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger'), ('external\\_setup_paths.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger'), ('external\\_setup_paths.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger')], hiddenimports=['tkinter_logger', 'resource_monitor', 'pyucc.core.differ', 'pyucc.gui.gui', 'pyucc.config.settings', 'logging', 'logging.handlers', 'logging.config', 'ConfigParser', 'Cython', 'HTMLParser', 'IPython', 'OpenSSL', 'PIL', 'Queue', 'StringIO', 'android', 'annotationlib', 'argcomplete', 'attr', 'autocommand', 'backports', 'cgi', 'chardet', 'colorama', 'contextlib2', 'cryptography', 'ctags', 'distutils', 'dl', 'docutils', 'dummy_thread', 'dummy_threading', 'exceptiongroup', 'fcntl', 'filelock', 'future_builtins', 'git', 'gitdb', 'gitdb_speedups', 'google', 'grp', 'htmlentitydefs', 'httplib', 'importlib_metadata', 'importlib_resources', 'inflect', 'ini2toml', 'iniconfig', 'ipywidgets', 'jaraco', 'java', 'jinja2', 'jnius', 'keyring', 'linkify_it', 'lizard', 'lizard_ext', 'lizard_languages', 'markdown_it', 'mdurl', 'mock', 'mod', 'mod2', 'more_itertools', 'nspkg', 'ntlm', 'numpy', 'packaging', 'path', 'pathspec', 'pexpect', 'pip', 'pkg1', 'pkg_resources', 'platformdirs', 'pluggy', 'psutil', 'pwd', 'py', 'pygments', 'pygount', 'pytest', 'pyucc', 'pywintypes', 'readline', 'redis', 'resource', 'rich', 'setuptools', 'sha', 'smmap', 'socks', 'sphinx', 'target_simulator', 'thread', 'tomli', 'tomli_w', 'trove_classifiers', 'twisted', 'typeguard', 'typeshed', 'typing_extensions', 'urllib2', 'urllib3_secure_extra', 'urlparse', 'wheel', 'win32api', 'win32con', 'win32process', 'wmi', 'xmlrpclib', 'xx', 'zipp', 'zope', 'traitlets', 'gdb', 'matplotlib', 'PyInstaller'], hookspath=['hooks'], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, scripts=['pyucc\\__main__.py'])
|
||||||
|
try:
|
||||||
|
a.hiddenimports += _pygount_hiddenimports
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='PyUcc', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, icon='PyUcc.ico', exclude_binaries=True)
|
||||||
|
coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='PyUcc')
|
||||||
|
block_cipher = None
|
||||||
|
a = Analysis(pathex=['pyucc', '.'], binaries=[], datas=[('PyUcc.ico', '.'), ('external\\python-tkinter-logger\\tkinter_logger.py', '.'), ('external\\python-resource-monitor\\resource_monitor.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger'), ('external\\_setup_paths.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger'), ('external\\_setup_paths.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger'), ('external\\_setup_paths.py', '.'), ('external\\python-resource-monitor', 'python-resource-monitor'), ('external\\python-tkinter-logger', 'python-tkinter-logger')], hiddenimports=['tkinter_logger', 'resource_monitor', 'pyucc.core.differ', 'pyucc.gui.gui', 'pyucc.config.settings', 'logging', 'logging.handlers', 'logging.config', 'ConfigParser', 'Cython', 'HTMLParser', 'IPython', 'OpenSSL', 'PIL', 'Queue', 'StringIO', 'android', 'annotationlib', 'argcomplete', 'attr', 'autocommand', 'backports', 'cgi', 'chardet', 'colorama', 'contextlib2', 'cryptography', 'ctags', 'distutils', 'dl', 'docutils', 'dummy_thread', 'dummy_threading', 'exceptiongroup', 'fcntl', 'filelock', 'future_builtins', 'git', 'gitdb', 'gitdb_speedups', 'google', 'grp', 'htmlentitydefs', 'httplib', 'importlib_metadata', 'importlib_resources', 'inflect', 'ini2toml', 'iniconfig', 'ipywidgets', 'jaraco', 'java', 'jinja2', 'jnius', 'keyring', 'linkify_it', 'lizard', 'lizard_ext', 'lizard_languages', 'markdown_it', 'mdurl', 'mock', 'mod', 'mod2', 'more_itertools', 'nspkg', 'ntlm', 'numpy', 'packaging', 'path', 'pathspec', 'pexpect', 'pip', 'pkg1', 'pkg_resources', 'platformdirs', 'pluggy', 'psutil', 'pwd', 'py', 'pygments', 'pygount', 'pytest', 'pyucc', 'pywintypes', 'readline', 'redis', 'resource', 'rich', 'setuptools', 'sha', 'smmap', 'socks', 'sphinx', 'target_simulator', 'thread', 'tomli', 'tomli_w', 'trove_classifiers', 'twisted', 'typeguard', 'typeshed', 'typing_extensions', 'urllib2', 'urllib3_secure_extra', 'urlparse', 'wheel', 'win32api', 'win32con', 'win32process', 'wmi', 'xmlrpclib', 'xx', 'zipp', 'zope', 'traitlets', 'gdb', 'matplotlib', 'PyInstaller'], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, scripts=['pyucc\\__main__.py'])
|
||||||
|
try:
|
||||||
|
a.hiddenimports += _pygount_hiddenimports
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='PyUcc', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, icon='PyUcc.ico', exclude_binaries=True)
|
||||||
|
coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='PyUcc')
|
||||||
@ -145,6 +145,26 @@ def main():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Inform the user early if pygount is not available (affects comment counting)
|
||||||
|
try:
|
||||||
|
from pyucc.core import countings_impl
|
||||||
|
|
||||||
|
if not getattr(countings_impl, "_HAS_PYGOUNT", False):
|
||||||
|
print(
|
||||||
|
"WARNING: the 'pygount' package is not available. Comment counts and some extended metrics may not be available.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"To fix: activate the virtualenv used to run the app and run 'pip install pygount'",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"If you ship a PyInstaller executable, rebuild it including 'pygount' (add a hook or hiddenimports).",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# Non-critical: continue execution
|
||||||
|
pass
|
||||||
# If user asked for GUI, or no positional directories were provided, launch GUI and exit
|
# If user asked for GUI, or no positional directories were provided, launch GUI and exit
|
||||||
if args.gui or len(args.baseline_dirs or []) == 0:
|
if args.gui or len(args.baseline_dirs or []) == 0:
|
||||||
try:
|
try:
|
||||||
|
|||||||
BIN
pyucc/_internal/pygount-3.0.0-py3-none-any.whl
Normal file
BIN
pyucc/_internal/pygount-3.0.0-py3-none-any.whl
Normal file
Binary file not shown.
@ -6,10 +6,10 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
# --- Version Data (Generated) ---
|
# --- Version Data (Generated) ---
|
||||||
__version__ = "v.0.0.0.19-0-g79ed9c1-dirty"
|
__version__ = "v.0.0.0.23-0-gc67280d-dirty"
|
||||||
GIT_COMMIT_HASH = "79ed9c1d728bb53ace64ca2c232eb3c038b69152"
|
GIT_COMMIT_HASH = "c67280df44cb0db1bad3815a2938adc0904a10de"
|
||||||
GIT_BRANCH = "master"
|
GIT_BRANCH = "master"
|
||||||
BUILD_TIMESTAMP = "2025-12-12T09:07:06.615288+00:00"
|
BUILD_TIMESTAMP = "2025-12-15T09:32:07.391542+00:00"
|
||||||
IS_GIT_REPO = True
|
IS_GIT_REPO = True
|
||||||
|
|
||||||
# --- Default Values (for comparison or fallback) ---
|
# --- Default Values (for comparison or fallback) ---
|
||||||
|
|||||||
@ -10,14 +10,90 @@ import subprocess
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pygount # type: ignore
|
import pygount # type: ignore
|
||||||
|
|
||||||
_HAS_PYGOUNT = True
|
_HAS_PYGOUNT = True
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# Try to load a bundled pygount wheel (offline install) — useful for PyInstaller one-folder
|
||||||
_HAS_PYGOUNT = False
|
_HAS_PYGOUNT = False
|
||||||
|
|
||||||
|
def _attempt_load_bundled_pygount() -> bool:
|
||||||
|
"""If a pygount wheel is bundled with the app (e.g. in _internal), add it to sys.path and try import.
|
||||||
|
|
||||||
|
Returns True if import succeeded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
# If running frozen (PyInstaller), packaged data are extracted to sys._MEIPASS
|
||||||
|
base = getattr(sys, "_MEIPASS", None)
|
||||||
|
if base:
|
||||||
|
# search both at root of MEIPASS and recursively (some datas end up nested)
|
||||||
|
candidates.append(os.path.join(base, "pygount*.whl"))
|
||||||
|
candidates.append(os.path.join(base, "**", "pygount*.whl"))
|
||||||
|
candidates.append(os.path.join(base, "pygount", "**", "*.py"))
|
||||||
|
candidates.append(os.path.join(base, "**", "pygount", "**", "*.py"))
|
||||||
|
# Also look into a relative _internal folder in source tree
|
||||||
|
repo_internal = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "_internal"))
|
||||||
|
candidates.append(os.path.join(repo_internal, "pygount*.whl"))
|
||||||
|
candidates.append(os.path.join(repo_internal, "pygount", "**", "*.py"))
|
||||||
|
|
||||||
|
found_paths = []
|
||||||
|
for pattern in candidates:
|
||||||
|
try:
|
||||||
|
for p in glob.glob(pattern, recursive=True):
|
||||||
|
if p and p not in found_paths:
|
||||||
|
found_paths.append(p)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prefer a wheel file if present (zipimport works for wheels)
|
||||||
|
wheel_paths = [p for p in found_paths if p.lower().endswith('.whl')]
|
||||||
|
if wheel_paths:
|
||||||
|
wheel = wheel_paths[0]
|
||||||
|
try:
|
||||||
|
if wheel not in sys.path:
|
||||||
|
sys.path.insert(0, wheel)
|
||||||
|
import pygount # type: ignore
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
# remove added path if import failed
|
||||||
|
try:
|
||||||
|
if wheel in sys.path:
|
||||||
|
sys.path.remove(wheel)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If no wheel, try adding a plain package folder
|
||||||
|
pkg_dirs = [os.path.dirname(p) for p in found_paths if p.lower().endswith('.py')]
|
||||||
|
# remove duplicates
|
||||||
|
pkg_dirs = list(dict.fromkeys(pkg_dirs))
|
||||||
|
for d in pkg_dirs:
|
||||||
|
try:
|
||||||
|
if d not in sys.path:
|
||||||
|
sys.path.insert(0, d)
|
||||||
|
import pygount # type: ignore
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
if d in sys.path:
|
||||||
|
sys.path.remove(d)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _attempt_load_bundled_pygount():
|
||||||
|
_HAS_PYGOUNT = True
|
||||||
|
except Exception:
|
||||||
|
_HAS_PYGOUNT = False
|
||||||
|
|
||||||
_LOG = logging.getLogger(__name__)
|
_LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Cache to store counting results by file content hash ONLY
|
# Cache to store counting results by file content hash ONLY
|
||||||
|
|||||||
@ -231,6 +231,44 @@ class App(tk.Tk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Check if pygount is available and warn the user if not.
|
||||||
|
try:
|
||||||
|
from ..core import countings_impl
|
||||||
|
|
||||||
|
if not getattr(countings_impl, "_HAS_PYGOUNT", False):
|
||||||
|
msg = (
|
||||||
|
"""
|
||||||
|
The 'pygount' package is not available.
|
||||||
|
|
||||||
|
Comment counting and some extended metrics may not work correctly.
|
||||||
|
|
||||||
|
Possible fixes:
|
||||||
|
1) If you are using a development environment: activate the virtualenv and run 'pip install pygount'.
|
||||||
|
2) If you are using a PyInstaller-built executable: rebuild it including 'pygount' (hook or hiddenimports).
|
||||||
|
3) Quick check: run
|
||||||
|
python -c "from pyucc.core import countings_impl; print(countings_impl._HAS_PYGOUNT)"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
messagebox.showwarning("Missing dependency: pygount", msg)
|
||||||
|
except Exception:
|
||||||
|
# If messagebox fails (headless), fallback to logging
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.log(
|
||||||
|
"pygount not available: comment counting and extended metrics disabled",
|
||||||
|
level="WARNING",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.phase_var.set("Ready (pygount missing — limited functionality)")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# ignore any unexpected error when checking for pygount
|
||||||
|
pass
|
||||||
|
|
||||||
# small helper: expose a convenient log method that forwards to
|
# small helper: expose a convenient log method that forwards to
|
||||||
# the standard logging system so messages flow through the queue.
|
# the standard logging system so messages flow through the queue.
|
||||||
def log(self, msg: str, level: str = "INFO"):
|
def log(self, msg: str, level: str = "INFO"):
|
||||||
|
|||||||
@ -62,6 +62,8 @@ class ProfileManager(tk.Toplevel):
|
|||||||
self.title("Profile Manager")
|
self.title("Profile Manager")
|
||||||
self.geometry("1100x700")
|
self.geometry("1100x700")
|
||||||
self.on_change = on_change
|
self.on_change = on_change
|
||||||
|
# currently loaded profile dict (or None for new)
|
||||||
|
self._loaded_profile = None
|
||||||
|
|
||||||
self.profiles = profiles_cfg.load_profiles()
|
self.profiles = profiles_cfg.load_profiles()
|
||||||
|
|
||||||
@ -97,7 +99,7 @@ class ProfileManager(tk.Toplevel):
|
|||||||
self.ext_frame.grid(row=0, column=1, sticky="nsew")
|
self.ext_frame.grid(row=0, column=1, sticky="nsew")
|
||||||
|
|
||||||
# Paths listbox + scrollbar inside its frame
|
# Paths listbox + scrollbar inside its frame
|
||||||
self.paths_listbox = tk.Listbox(self.paths_frame, height=12)
|
self.paths_listbox = tk.Listbox(self.paths_frame, height=12, exportselection=False)
|
||||||
self.paths_listbox.grid(row=0, column=0, sticky="nsew")
|
self.paths_listbox.grid(row=0, column=0, sticky="nsew")
|
||||||
paths_vsb = ttk.Scrollbar(
|
paths_vsb = ttk.Scrollbar(
|
||||||
self.paths_frame, orient="vertical", command=self.paths_listbox.yview
|
self.paths_frame, orient="vertical", command=self.paths_listbox.yview
|
||||||
@ -141,7 +143,7 @@ class ProfileManager(tk.Toplevel):
|
|||||||
).grid(row=1, column=1, padx=6, pady=4)
|
).grid(row=1, column=1, padx=6, pady=4)
|
||||||
|
|
||||||
# Extensions listbox + scrollbar inside its frame
|
# Extensions listbox + scrollbar inside its frame
|
||||||
self.ext_listbox = tk.Listbox(self.ext_frame, selectmode="extended", height=12)
|
self.ext_listbox = tk.Listbox(self.ext_frame, selectmode="extended", height=12, exportselection=False)
|
||||||
self.ext_listbox.grid(row=0, column=0, sticky="nsew")
|
self.ext_listbox.grid(row=0, column=0, sticky="nsew")
|
||||||
ext_vsb = ttk.Scrollbar(
|
ext_vsb = ttk.Scrollbar(
|
||||||
self.ext_frame, orient="vertical", command=self.ext_listbox.yview
|
self.ext_frame, orient="vertical", command=self.ext_listbox.yview
|
||||||
@ -215,10 +217,16 @@ class ProfileManager(tk.Toplevel):
|
|||||||
ttk.Button(btn_frame, text="Delete", command=self._delete).grid(
|
ttk.Button(btn_frame, text="Delete", command=self._delete).grid(
|
||||||
row=0, column=3, padx=4
|
row=0, column=3, padx=4
|
||||||
)
|
)
|
||||||
ttk.Button(btn_frame, text="Close", command=self.destroy).grid(
|
ttk.Button(btn_frame, text="Close", command=self._on_close).grid(
|
||||||
row=0, column=4, padx=4
|
row=0, column=4, padx=4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# handle window close (X) the same way as Close button
|
||||||
|
try:
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
right.columnconfigure(1, weight=1)
|
right.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# If no profiles exist, prefill form with sensible defaults (widgets are ready)
|
# If no profiles exist, prefill form with sensible defaults (widgets are ready)
|
||||||
@ -247,36 +255,25 @@ class ProfileManager(tk.Toplevel):
|
|||||||
if f:
|
if f:
|
||||||
self._insert_path_display("end", str(Path(f)))
|
self._insert_path_display("end", str(Path(f)))
|
||||||
|
|
||||||
def _populate_ext_listbox(self):
|
def _insert_path_display(self, index, path_str: str):
|
||||||
# Populate extension listbox with LANGUAGE_EXTENSIONS entries
|
# choose a prefix depending on whether path is dir/file/unknown
|
||||||
try:
|
p = Path(path_str)
|
||||||
from ..config.languages import LANGUAGE_EXTENSIONS
|
if p.exists():
|
||||||
except Exception:
|
prefix = "[D] " if p.is_dir() else "[F] "
|
||||||
return
|
else:
|
||||||
self.ext_listbox.delete(0, "end")
|
prefix = "[?] "
|
||||||
keys = sorted(LANGUAGE_EXTENSIONS.keys(), key=lambda s: s.lower())
|
display = f"{prefix}{path_str}"
|
||||||
for k in keys:
|
self.paths_listbox.insert(index, display)
|
||||||
exts = LANGUAGE_EXTENSIONS.get(k) or []
|
|
||||||
exts_label = f" ({', '.join(exts)})" if exts else ""
|
|
||||||
self.ext_listbox.insert("end", f"{k}{exts_label}")
|
|
||||||
|
|
||||||
def _clear_ext_selection(self):
|
def _strip_display_prefix(self, display_text: str) -> str:
|
||||||
self.ext_listbox.selection_clear(0, "end")
|
# remove known prefixes like [D] , [F] , [?] if present
|
||||||
|
if (
|
||||||
def _edit_selected_path(self):
|
display_text.startswith("[D] ")
|
||||||
sel = self.paths_listbox.curselection()
|
or display_text.startswith("[F] ")
|
||||||
if not sel:
|
or display_text.startswith("[?] ")
|
||||||
return
|
):
|
||||||
idx = sel[0]
|
return display_text[4:]
|
||||||
current = self.paths_listbox.get(idx)
|
return display_text
|
||||||
# strip prefix for editing
|
|
||||||
stripped = self._strip_display_prefix(current)
|
|
||||||
new = simpledialog.askstring(
|
|
||||||
"Edit Path", "Path:", initialvalue=stripped, parent=self
|
|
||||||
)
|
|
||||||
if new:
|
|
||||||
self.paths_listbox.delete(idx)
|
|
||||||
self._insert_path_display(idx, new)
|
|
||||||
|
|
||||||
def _remove_selected_path(self):
|
def _remove_selected_path(self):
|
||||||
sel = list(self.paths_listbox.curselection())
|
sel = list(self.paths_listbox.curselection())
|
||||||
@ -319,25 +316,107 @@ class ProfileManager(tk.Toplevel):
|
|||||||
for i in [min(size - 1, x + 1) for x in sel]:
|
for i in [min(size - 1, x + 1) for x in sel]:
|
||||||
self.paths_listbox.selection_set(i)
|
self.paths_listbox.selection_set(i)
|
||||||
|
|
||||||
def _insert_path_display(self, index, path_str: str):
|
def _populate_ext_listbox(self):
|
||||||
# choose a prefix depending on whether path is dir/file/unknown
|
# Populate extension listbox with LANGUAGE_EXTENSIONS entries
|
||||||
p = Path(path_str)
|
try:
|
||||||
if p.exists():
|
from ..config.languages import LANGUAGE_EXTENSIONS
|
||||||
prefix = "[D] " if p.is_dir() else "[F] "
|
except Exception:
|
||||||
else:
|
return
|
||||||
prefix = "[?] "
|
self.ext_listbox.delete(0, "end")
|
||||||
display = f"{prefix}{path_str}"
|
keys = sorted(LANGUAGE_EXTENSIONS.keys(), key=lambda s: s.lower())
|
||||||
self.paths_listbox.insert(index, display)
|
for k in keys:
|
||||||
|
exts = LANGUAGE_EXTENSIONS.get(k) or []
|
||||||
|
exts_label = f" ({', '.join(exts)})" if exts else ""
|
||||||
|
self.ext_listbox.insert("end", f"{k}{exts_label}")
|
||||||
|
|
||||||
|
def _clear_ext_selection(self):
|
||||||
|
self.ext_listbox.selection_clear(0, "end")
|
||||||
|
|
||||||
|
def _edit_selected_path(self):
|
||||||
|
sel = self.paths_listbox.curselection()
|
||||||
|
if not sel:
|
||||||
|
return
|
||||||
|
idx = sel[0]
|
||||||
|
current = self.paths_listbox.get(idx)
|
||||||
|
# strip prefix for editing
|
||||||
|
stripped = self._strip_display_prefix(current)
|
||||||
|
new = self._ask_edit_path(stripped)
|
||||||
|
if new:
|
||||||
|
self.paths_listbox.delete(idx)
|
||||||
|
self._insert_path_display(idx, new)
|
||||||
|
|
||||||
|
def _ask_edit_path(self, initial: str) -> str:
|
||||||
|
"""Open a small modal dialog with a larger entry widget for editing paths.
|
||||||
|
|
||||||
|
Returns the edited string, or None if cancelled.
|
||||||
|
"""
|
||||||
|
dlg = tk.Toplevel(self)
|
||||||
|
dlg.title("Edit Path")
|
||||||
|
dlg.transient(self)
|
||||||
|
dlg.grab_set()
|
||||||
|
|
||||||
|
frm = ttk.Frame(dlg)
|
||||||
|
frm.grid(row=0, column=0, padx=8, pady=8, sticky="nsew")
|
||||||
|
ttk.Label(frm, text="Path:").grid(row=0, column=0, columnspan=2, sticky="w")
|
||||||
|
val = tk.StringVar(value=initial)
|
||||||
|
entry = ttk.Entry(frm, textvariable=val, width=90)
|
||||||
|
entry.grid(row=1, column=0, sticky="ew", pady=(6, 0))
|
||||||
|
|
||||||
|
def _on_browse():
|
||||||
|
d = filedialog.askdirectory(parent=dlg)
|
||||||
|
if d:
|
||||||
|
val.set(str(Path(d)))
|
||||||
|
entry.focus_set()
|
||||||
|
entry.selection_range(0, 'end')
|
||||||
|
|
||||||
|
browse_btn = ttk.Button(frm, text="Browse...", command=_on_browse)
|
||||||
|
browse_btn.grid(row=1, column=1, padx=(6, 0), pady=(6, 0))
|
||||||
|
|
||||||
|
btns = ttk.Frame(frm)
|
||||||
|
btns.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky="e")
|
||||||
|
|
||||||
|
ok_pressed = {"ok": False}
|
||||||
|
|
||||||
|
def _on_ok():
|
||||||
|
ok_pressed["ok"] = True
|
||||||
|
try:
|
||||||
|
dlg.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_cancel():
|
||||||
|
try:
|
||||||
|
dlg.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ttk.Button(btns, text="OK", command=_on_ok).grid(row=0, column=0, padx=6)
|
||||||
|
ttk.Button(btns, text="Cancel", command=_on_cancel).grid(row=0, column=1)
|
||||||
|
|
||||||
|
frm.columnconfigure(0, weight=1)
|
||||||
|
# center dialog above parent (ProfileManager)
|
||||||
|
try:
|
||||||
|
dlg.update_idletasks()
|
||||||
|
pwx = self.winfo_rootx()
|
||||||
|
pwy = self.winfo_rooty()
|
||||||
|
pww = self.winfo_width()
|
||||||
|
pwh = self.winfo_height()
|
||||||
|
dw = dlg.winfo_width()
|
||||||
|
dh = dlg.winfo_height()
|
||||||
|
cx = pwx + max(0, (pww - dw) // 2)
|
||||||
|
cy = pwy + max(0, (pwh - dh) // 2)
|
||||||
|
dlg.geometry(f"+{cx}+{cy}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
entry.focus_set()
|
||||||
|
entry.selection_range(0, 'end')
|
||||||
|
self.wait_window(dlg)
|
||||||
|
|
||||||
|
if ok_pressed["ok"]:
|
||||||
|
return val.get().strip()
|
||||||
|
return None
|
||||||
|
|
||||||
def _strip_display_prefix(self, display_text: str) -> str:
|
|
||||||
# remove known prefixes like [D] , [F] , [?] if present
|
|
||||||
if (
|
|
||||||
display_text.startswith("[D] ")
|
|
||||||
or display_text.startswith("[F] ")
|
|
||||||
or display_text.startswith("[?] ")
|
|
||||||
):
|
|
||||||
return display_text[4:]
|
|
||||||
return display_text
|
|
||||||
|
|
||||||
def _get_paths_from_listbox(self) -> List[str]:
|
def _get_paths_from_listbox(self) -> List[str]:
|
||||||
out = []
|
out = []
|
||||||
@ -356,6 +435,8 @@ class ProfileManager(tk.Toplevel):
|
|||||||
|
|
||||||
def _load_profile(self, pr):
|
def _load_profile(self, pr):
|
||||||
self.name_var.set(pr.get("name", ""))
|
self.name_var.set(pr.get("name", ""))
|
||||||
|
# remember loaded profile so we can detect unsaved changes on close
|
||||||
|
self._loaded_profile = dict(pr)
|
||||||
# Load only new-style 'paths' list (no legacy compatibility)
|
# Load only new-style 'paths' list (no legacy compatibility)
|
||||||
self.paths_listbox.delete(0, "end")
|
self.paths_listbox.delete(0, "end")
|
||||||
paths = pr.get("paths") or []
|
paths = pr.get("paths") or []
|
||||||
@ -396,6 +477,8 @@ class ProfileManager(tk.Toplevel):
|
|||||||
self.ignore_text.delete("1.0", "end")
|
self.ignore_text.delete("1.0", "end")
|
||||||
# insert the DEFAULT_IGNORES joined by commas
|
# insert the DEFAULT_IGNORES joined by commas
|
||||||
self.ignore_text.insert("1.0", ",".join(self.DEFAULT_IGNORES))
|
self.ignore_text.insert("1.0", ",".join(self.DEFAULT_IGNORES))
|
||||||
|
# new form — no loaded profile
|
||||||
|
self._loaded_profile = None
|
||||||
|
|
||||||
def _apply_default_ignores(self):
|
def _apply_default_ignores(self):
|
||||||
"""Merge current ignore patterns with the DEFAULT_IGNORES and
|
"""Merge current ignore patterns with the DEFAULT_IGNORES and
|
||||||
@ -472,6 +555,11 @@ class ProfileManager(tk.Toplevel):
|
|||||||
messagebox.showinfo("Saved", f"Profile '{name}' saved.")
|
messagebox.showinfo("Saved", f"Profile '{name}' saved.")
|
||||||
if self.on_change:
|
if self.on_change:
|
||||||
self.on_change()
|
self.on_change()
|
||||||
|
# mark this as the currently loaded profile
|
||||||
|
try:
|
||||||
|
self._loaded_profile = dict(profile)
|
||||||
|
except Exception:
|
||||||
|
self._loaded_profile = None
|
||||||
|
|
||||||
def _delete(self):
|
def _delete(self):
|
||||||
name = self.name_var.get().strip()
|
name = self.name_var.get().strip()
|
||||||
@ -487,3 +575,71 @@ class ProfileManager(tk.Toplevel):
|
|||||||
self._new()
|
self._new()
|
||||||
if self.on_change:
|
if self.on_change:
|
||||||
self.on_change()
|
self.on_change()
|
||||||
|
self._loaded_profile = None
|
||||||
|
|
||||||
|
def _on_close(self):
|
||||||
|
"""Handle window close: if form differs from loaded profile, ask to save."""
|
||||||
|
# build current form representation
|
||||||
|
try:
|
||||||
|
cur_name = self.name_var.get().strip()
|
||||||
|
cur_paths = self._get_paths_from_listbox()
|
||||||
|
cur_langs = []
|
||||||
|
try:
|
||||||
|
sel = [self.ext_listbox.get(i) for i in self.ext_listbox.curselection()]
|
||||||
|
for s in sel:
|
||||||
|
lang_name = s.split(" (", 1)[0]
|
||||||
|
if lang_name and lang_name not in cur_langs:
|
||||||
|
cur_langs.append(lang_name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raw_custom = self.custom_text.get("1.0", "end").strip()
|
||||||
|
raw_custom = raw_custom.replace("\n", ",")
|
||||||
|
custom = [c.strip() for c in raw_custom.split(",") if c.strip()]
|
||||||
|
cur_langs.extend([c for c in custom if c])
|
||||||
|
raw_ignore = self.ignore_text.get("1.0", "end").strip()
|
||||||
|
raw_ignore = raw_ignore.replace("\n", ",")
|
||||||
|
cur_ignore = [s.strip() for s in raw_ignore.split(",") if s.strip()]
|
||||||
|
cur_profile = {
|
||||||
|
"name": cur_name,
|
||||||
|
"paths": cur_paths,
|
||||||
|
"languages": cur_langs,
|
||||||
|
"ignore": cur_ignore,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
cur_profile = None
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
if self._loaded_profile is None and any([v for v in (cur_name, cur_paths, cur_langs, cur_ignore) if v]):
|
||||||
|
changed = True
|
||||||
|
elif self._loaded_profile is not None and cur_profile is not None:
|
||||||
|
# compare simple dicts
|
||||||
|
# note: do not attempt deep-normalization beyond this
|
||||||
|
if (
|
||||||
|
self._loaded_profile.get("name") != cur_profile.get("name")
|
||||||
|
or self._loaded_profile.get("paths") != cur_profile.get("paths")
|
||||||
|
or self._loaded_profile.get("languages") != cur_profile.get("languages")
|
||||||
|
or self._loaded_profile.get("ignore") != cur_profile.get("ignore")
|
||||||
|
):
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
ans = messagebox.askyesnocancel(
|
||||||
|
"Unsaved Changes", "Save changes to profile before closing?"
|
||||||
|
)
|
||||||
|
# True => save, False => discard and close, None => cancel
|
||||||
|
if ans is None:
|
||||||
|
return
|
||||||
|
if ans:
|
||||||
|
try:
|
||||||
|
self._save()
|
||||||
|
except Exception:
|
||||||
|
# if save fails, don't close
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.destroy()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
super().destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@ -101,7 +101,26 @@ class TopBar(ttk.Frame):
|
|||||||
|
|
||||||
def _open_manager(self):
|
def _open_manager(self):
|
||||||
def _refresh():
|
def _refresh():
|
||||||
|
# remember current selection, reload profiles and restore selection
|
||||||
|
cur = self.profile_var.get()
|
||||||
self._load_profiles()
|
self._load_profiles()
|
||||||
|
vals = list(self.profile_cb["values"]) if self.profile_cb["values"] else []
|
||||||
|
if cur and cur in vals:
|
||||||
|
# keep same selection and refresh derived UI
|
||||||
|
self.profile_var.set(cur)
|
||||||
|
self._on_profile_selected()
|
||||||
|
elif vals:
|
||||||
|
# if there is at least one profile, ensure UI reflects a valid selection
|
||||||
|
# keep existing selection if already set, otherwise pick first
|
||||||
|
if not self.profile_var.get():
|
||||||
|
self.profile_var.set(vals[0])
|
||||||
|
self._on_profile_selected()
|
||||||
|
else:
|
||||||
|
# no profiles available
|
||||||
|
self.profile_var.set("")
|
||||||
|
self.current_profile = None
|
||||||
|
self.path_var.set("")
|
||||||
|
self.project_type_var.set("-")
|
||||||
|
|
||||||
pm = ProfileManager(self.master, on_change=_refresh)
|
pm = ProfileManager(self.master, on_change=_refresh)
|
||||||
pm.grab_set()
|
pm.grab_set()
|
||||||
|
|||||||
@ -316,8 +316,49 @@ def generate_differ_report(result, profile_config, baseline_id, output_path):
|
|||||||
m = counts.get("modified", 0)
|
m = counts.get("modified", 0)
|
||||||
u = counts.get("unmodified", 0)
|
u = counts.get("unmodified", 0)
|
||||||
|
|
||||||
cd = pair.get("countings_delta") or {}
|
# Prefer explicit deltas computed by the differ, but if missing
|
||||||
md = pair.get("metrics_delta") or {}
|
# (e.g. added or deleted files where only one side exists) fall
|
||||||
|
# back to using the current or baseline values so the compact
|
||||||
|
# table shows meaningful numbers for new/deleted files.
|
||||||
|
cd = pair.get("countings_delta")
|
||||||
|
if cd is None:
|
||||||
|
cur_cnt = pair.get("current_countings") or {}
|
||||||
|
base_cnt = pair.get("baseline_countings") or {}
|
||||||
|
if cur_cnt and not base_cnt:
|
||||||
|
cd = {
|
||||||
|
"physical_lines": cur_cnt.get("physical_lines", 0),
|
||||||
|
"code_lines": cur_cnt.get("code_lines", 0),
|
||||||
|
"comment_lines": cur_cnt.get("comment_lines", 0),
|
||||||
|
"blank_lines": cur_cnt.get("blank_lines", 0),
|
||||||
|
}
|
||||||
|
elif base_cnt and not cur_cnt:
|
||||||
|
cd = {
|
||||||
|
"physical_lines": -base_cnt.get("physical_lines", 0),
|
||||||
|
"code_lines": -base_cnt.get("code_lines", 0),
|
||||||
|
"comment_lines": -base_cnt.get("comment_lines", 0),
|
||||||
|
"blank_lines": -base_cnt.get("blank_lines", 0),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
cd = {}
|
||||||
|
|
||||||
|
md = pair.get("metrics_delta")
|
||||||
|
if md is None:
|
||||||
|
cur_m = pair.get("current_metrics") or {}
|
||||||
|
base_m = pair.get("baseline_metrics") or {}
|
||||||
|
if cur_m and not base_m:
|
||||||
|
md = {
|
||||||
|
"func_count": cur_m.get("func_count", 0),
|
||||||
|
"avg_cc": cur_m.get("avg_cc", 0.0),
|
||||||
|
"mi": cur_m.get("mi", 0.0),
|
||||||
|
}
|
||||||
|
elif base_m and not cur_m:
|
||||||
|
md = {
|
||||||
|
"func_count": -base_m.get("func_count", 0),
|
||||||
|
"avg_cc": -base_m.get("avg_cc", 0.0),
|
||||||
|
"mi": -base_m.get("mi", 0.0),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
md = {}
|
||||||
|
|
||||||
def fmt_int(v):
|
def fmt_int(v):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -5,8 +5,14 @@
|
|||||||
"duplicates": {
|
"duplicates": {
|
||||||
"threshold": 5.0,
|
"threshold": 5.0,
|
||||||
"extensions": [
|
"extensions": [
|
||||||
".py",
|
".inl",
|
||||||
".pyw"
|
".h",
|
||||||
|
".hh",
|
||||||
|
".c",
|
||||||
|
".cxx",
|
||||||
|
".hpp",
|
||||||
|
".cpp",
|
||||||
|
".cc"
|
||||||
],
|
],
|
||||||
"k": 25,
|
"k": 25,
|
||||||
"window": 4
|
"window": 4
|
||||||
|
|||||||
10
todo.md
10
todo.md
@ -15,9 +15,9 @@
|
|||||||
- [x] poter ordinare la tabella cliccando sulla colonna sia in ordine crescente che descrescente
|
- [x] poter ordinare la tabella cliccando sulla colonna sia in ordine crescente che descrescente
|
||||||
- [ ] aggiungere schermata di debug dove provare le singole funzioni su singolo file.
|
- [ ] aggiungere schermata di debug dove provare le singole funzioni su singolo file.
|
||||||
- [x] implementare conteggio esteso stile UCC (Whole/Embedded comments, Directives, Data Decl, Exec Instr, Logical SLOC) - vedi `pyucc/core/ucc_extended_counting.py`
|
- [x] implementare conteggio esteso stile UCC (Whole/Embedded comments, Directives, Data Decl, Exec Instr, Logical SLOC) - vedi `pyucc/core/ucc_extended_counting.py`
|
||||||
- [ ] integrare conteggio esteso nella GUI principale
|
- [x] integrare conteggio esteso nella GUI principale
|
||||||
- [ ] fare confronto con uscita UCC per quanto riguarda la complessità ciclomatica e fornire un report più approfondito usando gli stessi indicatori numerici e gli stessi indicatore tipo "low, mediuam, high, very high"
|
- [x] fare confronto con uscita UCC per quanto riguarda la complessità ciclomatica e fornire un report più approfondito usando gli stessi indicatori numerici e gli stessi indicatore tipo "low, mediuam, high, very high"
|
||||||
- [ ] inserire la ricerca delle funzioni duplicate all'interno del codice.
|
- [x] inserire la ricerca delle funzioni duplicate all'interno del codice.
|
||||||
|
|
||||||
# FIXME List
|
# FIXME List
|
||||||
|
|
||||||
@ -29,3 +29,7 @@
|
|||||||
- [x] salvare il file delle diff in automatico
|
- [x] salvare il file delle diff in automatico
|
||||||
- [x] mettere le hint per spiegare i vari parametri cosa sono
|
- [x] mettere le hint per spiegare i vari parametri cosa sono
|
||||||
- [x] ordinare le righe selezionando la colonna sia in ordine screscente che descrescente
|
- [x] ordinare le righe selezionando la colonna sia in ordine screscente che descrescente
|
||||||
|
- [x] aggiornare profilo percorso quando premo su salva
|
||||||
|
- [x] non perdere la selezione sulle estensioni quando modifico il profilo
|
||||||
|
- [x] verificare con dsp 10 e 11 le differenze con quelle di UCC intorno ai 70000 linee di codice 708 linee di codice modificato
|
||||||
|
- [x] verificare se i commenti vengo conteggiati oppure no, nei file c++, c, h ecc
|
||||||
|
|||||||
162
tools/bundle_pygount.py
Normal file
162
tools/bundle_pygount.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bundle pygount wheel into the project and update pyucc.spec.
|
||||||
|
|
||||||
|
Usage: run inside your project's virtualenv/checkout root:
|
||||||
|
python tools/bundle_pygount.py
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Locates a local `pygount-*.whl` in the current Python environment (site-packages)
|
||||||
|
- Copies it to `pyucc/_internal/`
|
||||||
|
- Updates `pyucc.spec`, adding the wheel as a `datas` entry so PyInstaller bundles it
|
||||||
|
|
||||||
|
This enables the runtime fallback added in `pyucc.core.countings_impl` to import pygount
|
||||||
|
from a bundled wheel when running on an offline target.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import glob
|
||||||
|
from pathlib import Path
|
||||||
|
import sysconfig
|
||||||
|
import site
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SPEC_PATH = REPO_ROOT / "pyucc.spec"
|
||||||
|
DEST_DIR = REPO_ROOT / "pyucc" / "_internal"
|
||||||
|
|
||||||
|
|
||||||
|
def find_local_pygount_wheel() -> Path | None:
|
||||||
|
"""Search common site-packages locations for pygount-*.whl and return first match."""
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
# Preferred: current environment's site-packages (sysconfig)
|
||||||
|
try:
|
||||||
|
purelib = sysconfig.get_paths().get("purelib")
|
||||||
|
if purelib:
|
||||||
|
candidates.append(Path(purelib))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# site.getsitepackages() may return multiple dirs
|
||||||
|
try:
|
||||||
|
for p in site.getsitepackages():
|
||||||
|
candidates.append(Path(p))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: sys.path entries
|
||||||
|
for p in sys.path:
|
||||||
|
try:
|
||||||
|
candidates.append(Path(p))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Also check repository-local wheel folder and current working dir
|
||||||
|
try:
|
||||||
|
candidates.append(REPO_ROOT / "wheels")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
candidates.append(Path.cwd())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
candidates.append(DEST_DIR)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for base in candidates:
|
||||||
|
if not base:
|
||||||
|
continue
|
||||||
|
base = base.resolve()
|
||||||
|
if str(base) in seen:
|
||||||
|
continue
|
||||||
|
seen.add(str(base))
|
||||||
|
pattern = str(base / "pygount-*.whl")
|
||||||
|
for f in glob.glob(pattern):
|
||||||
|
if f:
|
||||||
|
return Path(f).resolve()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def copy_wheel_to_internal(wheel_path: Path) -> Path:
|
||||||
|
DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest = DEST_DIR / wheel_path.name
|
||||||
|
shutil.copy2(wheel_path, dest)
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def patch_spec_add_datas(spec_path: Path, wheel_rel_path: str) -> bool:
|
||||||
|
"""Insert a datas tuple ('<wheel_rel_path>', '_internal') into all datas=[...] lists in the spec.
|
||||||
|
|
||||||
|
Returns True if file modified.
|
||||||
|
"""
|
||||||
|
text = spec_path.read_text(encoding="utf-8")
|
||||||
|
tuple_entry = f"('{wheel_rel_path}', '_internal'),"
|
||||||
|
|
||||||
|
if tuple_entry in text:
|
||||||
|
print("Spec already contains wheel datas entry; nothing to do.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Find all occurrences of 'datas=[' and insert the tuple after the opening bracket
|
||||||
|
new_text = text
|
||||||
|
inserts = 0
|
||||||
|
idx = 0
|
||||||
|
while True:
|
||||||
|
m = re.search(r"\bdatas\s*=\s*\[", new_text[idx:])
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
# m.start() relative to new_text[idx:]
|
||||||
|
start = idx + m.end()
|
||||||
|
# Insert after start
|
||||||
|
new_text = new_text[:start] + tuple_entry + new_text[start:]
|
||||||
|
inserts += 1
|
||||||
|
idx = start + len(tuple_entry)
|
||||||
|
|
||||||
|
if inserts:
|
||||||
|
backup = spec_path.with_suffix(spec_path.suffix + ".bak")
|
||||||
|
spec_path.replace(backup)
|
||||||
|
spec_path.write_text(new_text, encoding="utf-8")
|
||||||
|
print(f"Patched {spec_path} - inserted datas entry in {inserts} places (backup saved to {backup}).")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(r"No datas=\[...] occurrences found in spec; no changes made.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Repo root: {REPO_ROOT}")
|
||||||
|
|
||||||
|
wheel = find_local_pygount_wheel()
|
||||||
|
if not wheel:
|
||||||
|
print("Could not locate a local pygount wheel in this environment.")
|
||||||
|
print("Please download a pygount wheel (pygount-<ver>-py3-none-any.whl) into your environment or site-packages.")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
print(f"Found pygount wheel: {wheel}")
|
||||||
|
|
||||||
|
dest = copy_wheel_to_internal(wheel)
|
||||||
|
print(f"Copied wheel to: {dest}")
|
||||||
|
|
||||||
|
# wheel_rel_path relative to project root (use forward slashes)
|
||||||
|
wheel_rel = f"pyucc/_internal/{dest.name}"
|
||||||
|
|
||||||
|
if not SPEC_PATH.exists():
|
||||||
|
print(f"Spec file not found at {SPEC_PATH}; please run this script from project root.")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
changed = patch_spec_add_datas(SPEC_PATH, wheel_rel)
|
||||||
|
if changed:
|
||||||
|
print("Specification updated. Rebuild using: pyinstaller pyucc.spec")
|
||||||
|
else:
|
||||||
|
print("Specification unchanged.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
9
tools/check_pygount_build.py
Normal file
9
tools/check_pygount_build.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import sys, importlib
|
||||||
|
sys.path.insert(0, r'dist\\PyUcc')
|
||||||
|
from pyucc.core import countings_impl as c
|
||||||
|
print('HAS_PYGOUNT=', getattr(c, '_HAS_PYGOUNT', 'UNKNOWN'))
|
||||||
|
try:
|
||||||
|
m = importlib.import_module('pygount')
|
||||||
|
print('pygount import ok, version=', getattr(m, '__version__', '?'))
|
||||||
|
except Exception as e:
|
||||||
|
print('pygount import failed:', repr(e))
|
||||||
Loading…
Reference in New Issue
Block a user