diff --git a/hooks/hook-pygount.py b/hooks/hook-pygount.py new file mode 100644 index 0000000..ec70b9d --- /dev/null +++ b/hooks/hook-pygount.py @@ -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'] diff --git a/pyucc.spec.bak b/pyucc.spec.bak new file mode 100644 index 0000000..adec851 --- /dev/null +++ b/pyucc.spec.bak @@ -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') diff --git a/pyucc/__main__.py b/pyucc/__main__.py index 7c14aed..0598240 100644 --- a/pyucc/__main__.py +++ b/pyucc/__main__.py @@ -151,19 +151,19 @@ def main(): if not getattr(countings_impl, "_HAS_PYGOUNT", False): print( - "ATTENZIONE: il pacchetto 'pygount' non è disponibile. I conteggi dei commenti e le metriche estese potrebbero non essere disponibili.", + "WARNING: the 'pygount' package is not available. Comment counts and some extended metrics may not be available.", file=sys.stderr, ) print( - "Per risolvere: attiva la virtualenv usata per eseguire l'app e esegui 'pip install pygount'", + "To fix: activate the virtualenv used to run the app and run 'pip install pygount'", file=sys.stderr, ) print( - "Se distribuisci con PyInstaller, ricostruisci l'eseguibile includendo 'pygount' (aggiungi un hook o hiddenimports).", + "If you ship a PyInstaller executable, rebuild it including 'pygount' (add a hook or hiddenimports).", file=sys.stderr, ) except Exception: - # Non critico: continuiamo senza interrompere l'esecuzione + # Non-critical: continue execution pass # 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: diff --git a/pyucc/_internal/pygount-3.0.0-py3-none-any.whl b/pyucc/_internal/pygount-3.0.0-py3-none-any.whl new file mode 100644 index 0000000..b796969 Binary files /dev/null and b/pyucc/_internal/pygount-3.0.0-py3-none-any.whl differ diff --git a/pyucc/_version.py b/pyucc/_version.py index fb6f085..eb8d0ed 100644 --- a/pyucc/_version.py +++ b/pyucc/_version.py @@ -6,10 +6,10 @@ import re # --- Version Data (Generated) --- -__version__ = "v.0.0.0.22-0-gea1d31b-dirty" -GIT_COMMIT_HASH = "ea1d31b82e9fa64a3bb809c001f8c465fbcb0294" +__version__ = "v.0.0.0.23-0-gc67280d-dirty" +GIT_COMMIT_HASH = "c67280df44cb0db1bad3815a2938adc0904a10de" GIT_BRANCH = "master" -BUILD_TIMESTAMP = "2025-12-15T07:30:39.921932+00:00" +BUILD_TIMESTAMP = "2025-12-15T09:32:07.391542+00:00" IS_GIT_REPO = True # --- Default Values (for comparison or fallback) --- diff --git a/pyucc/core/countings_impl.py b/pyucc/core/countings_impl.py index 76ea415..b57c076 100644 --- a/pyucc/core/countings_impl.py +++ b/pyucc/core/countings_impl.py @@ -10,14 +10,90 @@ import subprocess import logging import threading import hashlib +import os +import sys +import glob try: import pygount # type: ignore _HAS_PYGOUNT = True except Exception: + # Try to load a bundled pygount wheel (offline install) — useful for PyInstaller one-folder _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__) # Cache to store counting results by file content hash ONLY diff --git a/pyucc/gui/gui.py b/pyucc/gui/gui.py index a0a7b24..ac3e422 100644 --- a/pyucc/gui/gui.py +++ b/pyucc/gui/gui.py @@ -238,25 +238,25 @@ class App(tk.Tk): if not getattr(countings_impl, "_HAS_PYGOUNT", False): msg = ( """ -Il pacchetto 'pygount' non è disponibile. +The 'pygount' package is not available. -I conteggi dei commenti e alcune metriche estese potrebbero non funzionare correttamente. +Comment counting and some extended metrics may not work correctly. -Soluzioni possibili: - 1) Se usi l'ambiente di sviluppo: attiva la virtualenv e esegui 'pip install pygount'. - 2) Se usi l'eseguibile PyInstaller: ricostruiscilo includendo 'pygount' (hook o hiddenimports). - 3) Per controllare rapidamente: esegui - python -c 'from pyucc.core import countings_impl; print(countings_impl._HAS_PYGOUNT)' +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("Dipendenza mancante: pygount", msg) + messagebox.showwarning("Missing dependency: pygount", msg) except Exception: # If messagebox fails (headless), fallback to logging pass try: self.log( - "pygount non disponibile: conteggio commenti e metriche estese disabilitate", + "pygount not available: comment counting and extended metrics disabled", level="WARNING", ) except Exception: diff --git a/settings.json b/settings.json index bd40b8f..f84150e 100644 --- a/settings.json +++ b/settings.json @@ -5,8 +5,14 @@ "duplicates": { "threshold": 5.0, "extensions": [ - ".py", - ".pyw" + ".inl", + ".h", + ".hh", + ".c", + ".cxx", + ".hpp", + ".cpp", + ".cc" ], "k": 25, "window": 4 diff --git a/tools/bundle_pygount.py b/tools/bundle_pygount.py new file mode 100644 index 0000000..00ada66 --- /dev/null +++ b/tools/bundle_pygount.py @@ -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 ('', '_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--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() diff --git a/tools/check_pygount_build.py b/tools/check_pygount_build.py new file mode 100644 index 0000000..3c5bda1 --- /dev/null +++ b/tools/check_pygount_build.py @@ -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))