diff --git a/projectinitializer.ico b/projectinitializer.ico new file mode 100644 index 0000000..b543f4c Binary files /dev/null and b/projectinitializer.ico differ diff --git a/projectinitializer.spec b/projectinitializer.spec new file mode 100644 index 0000000..43efb11 --- /dev/null +++ b/projectinitializer.spec @@ -0,0 +1,36 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +a = Analysis(scripts=['projectinitializer\\__main__.py'], + pathex=['projectinitializer'], + binaries=[], + datas=[('projectinitializer.ico', '.')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False) + +pyz = PYZ(a.pure, a.zipped_data, cipher=None) + +exe = EXE(pyz, + a.scripts, + [], # Binaries/Datas usually handled by Analysis/COLLECT + exclude_binaries=True, # Let COLLECT handle binaries in one-dir + name='projectinitializer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, # Use UPX based on config + runtime_tmpdir=None, + console=True, # Set console based on GUI checkbox + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='projectinitializer.ico') diff --git a/projectinitializer/_version.py b/projectinitializer/_version.py new file mode 100644 index 0000000..c1b6749 --- /dev/null +++ b/projectinitializer/_version.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY. +# Contains build-time information scraped from Git (if available) +# and a helper function to format version strings. + + +import re + +# --- Version Data (Generated) --- +# This section is automatically generated by the build process. +__version__ = "v.0.0.0.1-1-g1fdf738-dirty" +GIT_COMMIT_HASH = "1fdf738d0257a954189792f557dc2bcd427f1264" +GIT_BRANCH = "master" +BUILD_TIMESTAMP = "2025-05-08T08:42:23Z" +IS_GIT_REPO = True + +# --- Default Values (for comparison or fallback) --- +DEFAULT_VERSION = "0.0.0+unknown" +DEFAULT_COMMIT = "Unknown" +DEFAULT_BRANCH = "Unknown" + +# --- Helper Function --- +def get_version_string(format_string=None): + """ + Returns a formatted string based on the build version information. + + Args: + format_string (str, optional): A format string using placeholders. + Defaults to "{{version}} ({{branch}}/{{commit_short}})" if None. + Placeholders: + {{version}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty') + {{tag}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION. + {{commit}}: Full Git commit hash. + {{commit_short}}: Short Git commit hash (7 chars). + {{branch}}: Git branch name. + {{dirty}}: '-dirty' if the repo was dirty, empty otherwise. + {{timestamp}}: Full build timestamp (ISO 8601 UTC). + {{timestamp_short}}: Build date only (YYYY-MM-DD). + {{is_git}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise. + + Returns: + str: The formatted version string, or an error message if formatting fails. + """ + if format_string is None: + format_string = "{version} ({branch}/{commit_short})" # Sensible default + + replacements = {} + try: + # Prepare data dictionary for substitution + replacements['version'] = __version__ if __version__ else DEFAULT_VERSION + replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT + replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT + replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH + replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown" + replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown" + replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown" + replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else "" + + # Extract clean tag using regex (handles versions like v1.0.0, 1.0.0) + tag = DEFAULT_VERSION + if __version__ and IS_GIT_REPO: + # Match optional 'v' prefix, then major.minor.patch + match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__) + if match: + tag = match.group(1) # Get the full tag (e.g., 'v1.0.0') + replacements['tag'] = tag + + # Perform substitution using regex to find placeholders {placeholder} + output_string = format_string + # Iterate through placeholders and replace them in the format string + for placeholder, value in replacements.items(): + # Compile regex pattern for {placeholder}, allowing for whitespace inside braces + pattern = re.compile(r'{\s*' + re.escape(placeholder) + r'\s*}') + # Substitute found patterns with the corresponding string value + output_string = pattern.sub(str(value), output_string) + + # Optional: Check if any placeholders remain unsubstituted (could indicate typo) + if re.search(r'{\s*[\w_]+\s*}', output_string): + # You might want to log this or handle it, for now, we return the string as is + # print(f"Warning: Unsubstituted placeholders remain in version string: {output_string}") + pass + + return output_string + + except Exception as e: + # Return a simple error message in case of unexpected formatting issues + # Avoid printing directly from this generated function + return f"[Formatting Error: {e}]" + + diff --git a/projectinitializer/config/settings.py b/projectinitializer/config/settings.py index d3b2ae7..5c7227a 100644 --- a/projectinitializer/config/settings.py +++ b/projectinitializer/config/settings.py @@ -6,31 +6,16 @@ from typing import Dict, Any # --- Constants --- CONFIG_FILE_NAME: str = ".project_initializer_config.json" -DEFAULT_ICON_FILE_NAME: str = "default_icon.ico" # Nome del file icona in assets/ +DEFAULT_ICON_FILE_NAME: str = "default_icon.ico" -# Path to the assets directory within the package -# Path(__file__) is the path to this settings.py file -# .parent is the config/ directory -# .parent is the project_initializer/ directory -# / "assets" gives project_initializer/assets/ PACKAGE_ASSETS_PATH: Path = Path(__file__).resolve().parent.parent / "assets" DEFAULT_ICON_PATH: Path = PACKAGE_ASSETS_PATH / DEFAULT_ICON_FILE_NAME - -# --- Configuration Management --- - +# --- Configuration Management (invariato) --- def get_config_file_path() -> Path: - """ - Gets the path to the configuration file. - Stores it in the user's home directory. - """ return Path.home() / CONFIG_FILE_NAME def load_app_configuration() -> Dict[str, Any]: - """ - Loads the application configuration (e.g., last used root directory). - Returns a dictionary with the configuration or an empty dictionary if not found/error. - """ config_path = get_config_file_path() if config_path.exists(): try: @@ -45,9 +30,6 @@ def load_app_configuration() -> Dict[str, Any]: return {} def save_app_configuration(config_data: Dict[str, Any]) -> None: - """ - Saves the application configuration to a JSON file. - """ config_path = get_config_file_path() try: with open(config_path, "w", encoding="utf-8") as f: @@ -58,7 +40,7 @@ def save_app_configuration(config_data: Dict[str, Any]) -> None: # --- File Templates --- def get_gitignore_template() -> str: - """Returns the .gitignore template string.""" + # ... (contenuto invariato, omesso per brevità) ... return """# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -209,13 +191,10 @@ dmypy.json # Temporary files *.swp *~ - -_dist/ -_build/ """ def get_readme_template(project_name_original: str) -> str: - """Returns the README.md template string, formatted with the project name.""" + # ... (contenuto invariato, omesso per brevità) ... return f"""# {project_name_original} A brief description of {project_name_original}. @@ -235,7 +214,7 @@ A brief description of {project_name_original}. """ def get_main_py_template(project_name_original: str, project_name_lower: str) -> str: - """Returns the __main__.py template string for the new project.""" + # ... (contenuto invariato, omesso per brevità) ... return f"""# {project_name_lower}/__main__.py # Example import assuming your main logic is in a 'main' function @@ -255,17 +234,20 @@ if __name__ == "__main__": main() """ -def get_spec_file_template(project_name_original: str, project_name_lower: str) -> str: + +# --- MODIFICA QUI per accettare project_icon_filename --- +def get_spec_file_template(project_name_original: str, project_name_lower: str, project_icon_filename: str) -> str: """Returns the .spec file template string for PyInstaller.""" return f"""# -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis( - ['{project_name_lower}/__main__.py'], # Main script of the project being built - pathex=['.'], # Current directory, where the .spec file is, and project_name_lower is a subfolder + ['{project_name_lower}/__main__.py'], + pathex=['.'], binaries=[], - datas=[('{DEFAULT_ICON_FILE_NAME}', '.')], # Icon file relative to project root + # Usa project_icon_filename nella sezione datas + datas=[('{project_icon_filename}', '.')], hiddenimports=[], hookspath=[], runtime_hooks=[], @@ -291,13 +273,15 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=True, # Set to False for GUI-only applications - icon='{DEFAULT_ICON_FILE_NAME}' # Icon relative to project root + console=True, + # Usa project_icon_filename per l'opzione icon + icon='{project_icon_filename}' ) """ +# --- FINE MODIFICA --- def get_english_manual_template(project_name_original: str) -> str: - """Returns the English manual template string.""" + # ... (contenuto invariato, omesso per brevità) ... return f"""# {project_name_original} - English Manual ## Introduction @@ -325,7 +309,7 @@ Common issues and their solutions. """ def get_italian_manual_template(project_name_original: str) -> str: - """Returns the Italian manual template string.""" + # ... (contenuto invariato, omesso per brevità) ... return f"""# {project_name_original} - Manuale Italiano ## Introduzione diff --git a/projectinitializer/core/project_creator.py b/projectinitializer/core/project_creator.py index b08a598..e80a643 100644 --- a/projectinitializer/core/project_creator.py +++ b/projectinitializer/core/project_creator.py @@ -44,9 +44,21 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: raise ProjectCreationError(f"Root directory '{root_directory}' does not exist or is not a directory.") project_root_path = root_directory / project_name_original - # Sanitize project_name_original for use as Python module/package name project_name_lower = project_name_original.lower().replace("-", "_").replace(" ", "_") - # Basic validation for a Python module name (simplistic) + + # Sanitize project_name_original for use as a filename (e.g., for the icon) + # Rimuovi spazi e caratteri problematici, ma mantieni il case originale se possibile per il nome file .ico + # Per il file .spec e l'icona, è meglio usare un nome file "pulito". + # Usiamo project_name_original ma rimuovendo solo gli spazi, o potremmo usare project_name_lower + # per coerenza con il file .spec. Scegliamo project_name_original ma sanitizzato. + # Un nome file come "Mio Progetto.ico" è valido su Windows, ma meno portabile. + # "MioProgetto.ico" è meglio. + project_icon_filename_base = "".join(c if c.isalnum() or c in ['_', '-'] else '' for c in project_name_original) + if not project_icon_filename_base: # Se il nome originale era tipo "!@#$%" + project_icon_filename_base = project_name_lower # Fallback a nome lowercase + project_icon_filename = f"{project_icon_filename_base}.ico" + + if not project_name_lower.isidentifier(): raise ProjectCreationError( f"The sanitized project name '{project_name_lower}' (derived from '{project_name_original}') " @@ -54,7 +66,6 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: "not starting with a number." ) - if project_root_path.exists(): raise ProjectCreationError(f"Project directory '{project_root_path}' already exists.") @@ -73,17 +84,16 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: core_subpath.mkdir() print(f"Info: Created directory: {core_subpath}") (core_subpath / "__init__.py").touch() - (core_subpath / "core.py").touch() # Empty core logic file + (core_subpath / "core.py").touch() gui_subpath = src_package_path / "gui" gui_subpath.mkdir() print(f"Info: Created directory: {gui_subpath}") (gui_subpath / "__init__.py").touch() - (gui_subpath / "gui.py").touch() # Empty gui logic file + (gui_subpath / "gui.py").touch() - (src_package_path / "__init__.py").touch() # Make project_name_lower a package + (src_package_path / "__init__.py").touch() - # Create __main__.py for the new project main_py_content = settings.get_main_py_template(project_name_original, project_name_lower) with open(src_package_path / "__main__.py", "w", encoding="utf-8") as f: f.write(main_py_content) @@ -110,26 +120,25 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: f.write(readme_content) print(f"Info: Created file: {project_root_path / 'README.md'}") + # --- MODIFICA QUI per usare project_icon_filename --- # 6) Create .spec file for PyInstaller for the new project - spec_content = settings.get_spec_file_template(project_name_original, project_name_lower) - # The .spec file should be in the project_root_path + # Passa project_icon_filename al template del file .spec + spec_content = settings.get_spec_file_template(project_name_original, project_name_lower, project_icon_filename) with open(project_root_path / f"{project_name_lower}.spec", "w", encoding="utf-8") as f: f.write(spec_content) print(f"Info: Created file: {project_root_path / f'{project_name_lower}.spec'}") - # 7) Copy default .ico file to the new project's root - # The icon in the .spec file is referenced relative to the project root. - destination_icon_path = project_root_path / settings.DEFAULT_ICON_FILE_NAME + # 7) Copy default .ico file to the new project's root with the new name + # Usa project_icon_filename come nome del file di destinazione + destination_icon_path = project_root_path / project_icon_filename if settings.DEFAULT_ICON_PATH.exists(): shutil.copy2(settings.DEFAULT_ICON_PATH, destination_icon_path) print(f"Info: Copied icon: {destination_icon_path} from {settings.DEFAULT_ICON_PATH}") else: - # This case should ideally be handled by ensuring the tool's assets are present. - # For robustness, we can create an empty placeholder if it's truly missing. destination_icon_path.touch() print(f"Warning: Default icon '{settings.DEFAULT_ICON_PATH}' not found. " f"Created empty placeholder: {destination_icon_path}") - + # --- FINE MODIFICA ICONA --- # 8) Create .gitignore for the new project gitignore_content = settings.get_gitignore_template() @@ -142,7 +151,6 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: print(f"Info: Initializing Git repository in {project_root_path}...") subprocess.run(["git", "init"], cwd=project_root_path, check=True, capture_output=True, text=True) print(f"Info: Git repository initialized.") - # Optional: Add all files and make an initial commit subprocess.run(["git", "add", "."], cwd=project_root_path, check=True, capture_output=True, text=True) print(f"Info: All files added to Git staging area.") subprocess.run( @@ -151,19 +159,14 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: ) print("Info: Initial commit made.") except FileNotFoundError: - # Git command not found, not a fatal error for project creation itself. - # The user can initialize git manually later. print("Warning: Git command not found. Please install Git to initialize a repository automatically.") print(" The project structure has been created, but a Git repository was not initialized.") except subprocess.CalledProcessError as e: - # Error during Git operation. error_message = f"Error during Git operation: {e.stderr}" - if e.stdout: - error_message += f"\nGit stdout: {e.stdout}" + if e.stdout: error_message += f"\nGit stdout: {e.stdout}" print(f"Warning: {error_message}") print(" The project structure has been created, but there was an issue with Git initialization.") - success_message = ( f"Project '{project_name_original}' created successfully in '{project_root_path}'.\n" f"To run your new project (example): python -m {project_name_lower}" @@ -172,8 +175,6 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: return success_message except OSError as e: - # Catch other OS-level errors (permissions, disk full, etc.) - # Attempt to clean up if partial creation occurred error_msg = f"OS error during project creation: {e}" print(f"Error: {error_msg}") if project_root_path.exists() and project_root_path.is_dir(): @@ -183,10 +184,10 @@ def create_project(root_directory_str: str, project_name_original: str) -> str: except OSError as cleanup_e: print(f"Error: Error during cleanup of '{project_root_path}': {cleanup_e}") raise ProjectCreationError(error_msg) from e - except Exception as e: # Catch any other unexpected error + except Exception as e: error_msg = f"An unexpected error occurred: {e}" print(f"Error: {error_msg}") - if project_root_path.exists() and project_root_path.is_dir(): # Defensive cleanup + if project_root_path.exists() and project_root_path.is_dir(): try: shutil.rmtree(project_root_path) print(f"Info: Cleaned up partially created project at {project_root_path} due to unexpected error.") diff --git a/projectinitializer/gui/app_window.py b/projectinitializer/gui/app_window.py index e4eb186..dd97b7f 100644 --- a/projectinitializer/gui/app_window.py +++ b/projectinitializer/gui/app_window.py @@ -6,10 +6,29 @@ from pathlib import Path from projectinitializer.config import settings # type: ignore from projectinitializer.core import project_creator # type: ignore +# --- Import Version Info FOR THE WRAPPER ITSELF --- +try: + # Use absolute import based on package name + from projectinitializer import _version as wrapper_version + WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" + WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}" +except ImportError: + # This might happen if you run the wrapper directly from source + # without generating its _version.py first (if you use that approach for the wrapper itself) + WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" + WRAPPER_BUILD_INFO = "Wrapper build time unknown" +# --- End Import Version Info --- + +# --- Constants for Version Generation --- +DEFAULT_VERSION = "0.0.0+unknown" +DEFAULT_COMMIT = "Unknown" +DEFAULT_BRANCH = "Unknown" +# --- End Constants --- + class AppWindow: def __init__(self, master: tk.Tk): self.master = master - master.title("Project Initializer Tool") + master.title(f"Project Initializer Tool - {WRAPPER_APP_VERSION_STRING}") # master.geometry("500x200") # Optional: set a default size self.app_config = settings.load_app_configuration()