commit dc0d4b18d17efbadfac79d97d9e742d8a0b12683 Author: VALLONGOL Date: Wed May 7 13:21:14 2025 +0200 Chore: Stop tracking files based on .gitignore update. Untracked files matching the following rules: - Rule "*.pyc": 10 files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd13c12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.svn +*.pyc +_dist/ +_build/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..461de5b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Python Debugger: Module", + "type": "debugpy", + "request": "launch", + "module": "projectinitializer" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e889a5a --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Project Initializer Tool + +A Python application to initialize standard project structures for new Python projects, +suitable for use with Git. + +## Features + +- Creates a defined directory structure for source code (`project_name/core`, `project_name/gui`). +- Generates basic files: `__init__.py`, `__main__.py`, `core.py`, `gui.py`. +- Creates a `doc/` directory with template `English-manual.md` and `Italian-manual.md`. +- Generates a basic `README.md` for the new project. +- Generates a `.spec` file template for PyInstaller. +- Copies a default `.ico` file. +- Creates a standard `.gitignore` file. +- Initializes a local Git repository with an initial commit. +- Supports both Command Line Interface (CLI) and Graphical User Interface (GUI) via Tkinter. +- Remembers the last used root directory for convenience in GUI mode. + +## Structure of this Tool (`ProjectInitializerTool`) + +This tool itself is organized as follows: + +- `project_initializer/`: Main Python package for the tool. + - `__main__.py`: Entry point (`python -m project_initializer`). + - `core/`: Core logic for project creation. + - `project_creator.py`: Handles the actual file and directory generation. + - `config/`: Configuration and file templates. + - `settings.py`: Manages app config (like last used directory) and stores file templates. + - `gui/`: Tkinter GUI components. + - `app_window.py`: Defines the main application window. + - `cli/`: Command-line interface components. + - `interface.py`: Handles argument parsing and CLI interaction. + - `assets/`: Static assets for the tool. + - `default_icon.ico`: The default icon copied to new projects. + +## Usage + +### Prerequisites +- Python 3.7+ (ideally 3.8+ for `pathlib` features and type hinting usage) +- Git (optional, for repository initialization in new projects) + +### Running the Tool + +1. Navigate to the `ProjectInitializerTool` directory (the one containing this README). +2. Execute the tool using the Python module execution: + + **GUI Mode (Recommended for interactive use):** + ```bash + python -m project_initializer + ``` + This will launch the graphical interface. + + **CLI Mode (For scripting or command-line preference):** + ```bash + python -m project_initializer + ``` + - ``: The directory where the new project folder (named ``) will be created. + - ``: The desired name for your new project (e.g., "MyNewApp"). + + Example: + ```bash + python -m project_initializer /home/user/dev/projects MyCoolProject + ``` + + This will create a folder `/home/user/dev/projects/MyCoolProject` with the standard structure. + +### Configuration +The tool saves the last used root directory in a configuration file (`.project_initializer_config.json`) in your user's home directory. + +## Development (of this `ProjectInitializerTool`) + +- Ensure you have Python installed. +- The `default_icon.ico` file should be present in `project_initializer/assets/`. +- To run during development: `python -m project_initializer` from the `ProjectInitializerTool` directory. \ No newline at end of file diff --git a/projectinitializer/__init__.py b/projectinitializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projectinitializer/__main__.py b/projectinitializer/__main__.py new file mode 100644 index 0000000..ea5a11d --- /dev/null +++ b/projectinitializer/__main__.py @@ -0,0 +1,124 @@ +# ProjectInitializerTool/project_initializer/__main__.py + +import sys +import argparse +from pathlib import Path + +# Per lanciare il tool, la directory ProjectInitializerTool deve essere nel PYTHONPATH +# o si deve essere in ProjectInitializerTool/ e lanciare python -m project_initializer +# Questo permette gli import relativi corretti. +from projectinitializer.gui import app_window # type: ignore +from projectinitializer.cli import interface # type: ignore +from projectinitializer.config import settings # type: ignore + + +def ensure_default_icon_exists(): + """ + Checks if the default icon exists in the package's assets. + Creates a dummy one if not, for development convenience of the tool itself. + This helps prevent errors if the asset is accidentally deleted during development. + """ + icon_path = settings.DEFAULT_ICON_PATH + if not icon_path.exists(): + print(f"Warning: Default icon '{icon_path}' not found in package assets.", file=sys.stderr) + print(f"Attempting to create a dummy empty icon file at that location.", file=sys.stderr) + try: + icon_path.parent.mkdir(parents=True, exist_ok=True) # Ensure assets directory exists + icon_path.touch() + print(f"Dummy '{settings.DEFAULT_ICON_FILE_NAME}' created at '{icon_path}'. " + "Please replace it with a real .ico file for the tool to function correctly.", file=sys.stderr) + except OSError as e: + print(f"Critical Error: Could not create dummy icon at '{icon_path}': {e}. " + "The tool might not be able to copy the icon to new projects.", file=sys.stderr) + +def main(): + """ + Main entry point for the Project Initializer tool. + Determines whether to run in CLI or GUI mode. + """ + # Ensure the default icon asset is available for copying + ensure_default_icon_exists() + + # Argument parser to decide mode (and for CLI args if that's the mode) + # We use a basic parser here just to check for presence of CLI-specific args + # or a potential --gui / --cli flag if we add one. + # The full CLI parsing is delegated to cli.interface. + + parser = argparse.ArgumentParser( + description="Python Project Initializer Tool.", + add_help=False # We'll add help in a context-specific way or let sub-parsers do it + ) + # These arguments are for the CLI mode. If they are present, we assume CLI. + # nargs='?' makes them optional for this initial parse, so GUI mode doesn't fail. + parser.add_argument("root_directory", nargs="?", help=argparse.SUPPRESS) # Suppress from top-level help + parser.add_argument("project_name", nargs="?", help=argparse.SUPPRESS) # Suppress from top-level help + + # Optional explicit mode flags (could be useful in future) + # mode_group = parser.add_mutually_exclusive_group() + # mode_group.add_argument("--cli", action="store_true", help="Force Command Line Interface mode.") + # mode_group.add_argument("--gui", action="store_true", help="Force Graphical User Interface mode.") + + # Parse known arguments without exiting on error for missing positional ones yet. + # This allows us to check if any arguments were passed that might indicate CLI intent. + # All arguments including script name are in sys.argv + + # Heuristic: if there are arguments beyond the script name itself, + # and they are not flags like --help (which cli.interface would handle), + # it's likely an attempt to run CLI. + # A very simple check: if sys.argv has more than 1 element (script name + something else) + # A slightly better one: check if root_directory and project_name are likely provided. + + # If sys.argv has more than 1 element (meaning some args were passed) + # AND those arguments are not specific GUI launch flags (if we add them later) + # then, we assume it's for the CLI. Otherwise, default to GUI. + + # Let's try parsing the arguments. If root_directory and project_name are provided, + # it's CLI mode. Otherwise, it's GUI. This relies on argparse's behavior. + # sys.argv[0] is script name or -m module_name + + # If any arguments are passed (beyond script name), assume CLI intent. + # The cli.interface.handle_cli_invocation will perform full parsing and error handling. + # If no arguments are passed, launch GUI. + # This is a simplification. A --cli or --gui flag would be more robust. + + # Check if sys.argv contains arguments that are likely positional arguments for CLI + # sys.argv = ['project_initializer/__main__.py', 'arg1', 'arg2'] for `python -m project_initializer arg1 arg2` + # sys.argv = ['script.py'] for `python script.py` + + # A common pattern for this is to try to parse, and if specific args are found, run one mode. + # For simplicity now: if arguments are passed, assume CLI. cli.interface will validate them. + # If no arguments are passed (only script name), assume GUI. + + args_to_parse = sys.argv[1:] + + # Heuristic: if the first argument doesn't start with '-' and there are two arguments, + # it's highly probable they are the positional 'root_directory' and 'project_name'. + # This avoids accidentally going to CLI if someone types `python -m project_initializer --some-unknown-gui-flag`. + is_cli_intent = False + if len(args_to_parse) >= 2: + # Check if the first two args are likely our positional CLI args + # (i.e., they don't look like options/flags) + if not args_to_parse[0].startswith('-') and not args_to_parse[1].startswith('-'): + is_cli_intent = True + elif len(args_to_parse) == 1 and (args_to_parse[0] == "--help" or args_to_parse[0] == "-h"): + # If only help is requested, let CLI handler show its help message + is_cli_intent = True + + + if is_cli_intent: + # Pass all arguments (sys.argv[1:]) to the CLI handler + interface.handle_cli_invocation(args_to_parse) + else: + # No arguments or arguments that don't match CLI pattern, launch GUI + if args_to_parse and not (len(args_to_parse) == 1 and (args_to_parse[0] == "--help" or args_to_parse[0] == "-h")): + # Arguments were passed that didn't fit the CLI pattern, + # and it wasn't a simple help request. Show a warning/help. + print("Info: Unrecognized arguments for CLI mode. Defaulting to GUI mode.", file=sys.stdout) + print("For CLI, provide: ", file=sys.stdout) + print("Example: python -m project_initializer /path/to/projects MyNewApp\n", file=sys.stdout) + + print("Launching GUI mode...") # Useful feedback if launched from terminal + app_window.launch_gui_application() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/projectinitializer/assets/default_icon.ico b/projectinitializer/assets/default_icon.ico new file mode 100644 index 0000000..e69de29 diff --git a/projectinitializer/cli/__init__.py b/projectinitializer/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projectinitializer/cli/interface.py b/projectinitializer/cli/interface.py new file mode 100644 index 0000000..d769699 --- /dev/null +++ b/projectinitializer/cli/interface.py @@ -0,0 +1,70 @@ +# ProjectInitializerTool/project_initializer/cli/interface.py + +import argparse +import sys +from pathlib import Path +from projectinitializer.core import project_creator # type: ignore +# Settings non è direttamente usato qui, ma il core sì +# from project_initializer.config import settings + +def handle_cli_invocation(cli_args: list[str] | None = None) -> None: + """ + Parses command-line arguments and triggers project creation. + Manages CLI-specific output and error handling. + + Args: + cli_args (list[str] | None, optional): A list of command line arguments + (e.g. from sys.argv[1:]). + If None, sys.argv[1:] is used. + """ + parser = argparse.ArgumentParser( + description="Python Project Initializer Tool (CLI Mode). " + "Creates a standard Python project structure." + ) + parser.add_argument( + "root_directory", + type=str, + help="The absolute or relative root directory where the new project folder will be created." + ) + parser.add_argument( + "project_name", + type=str, + help="The name of the new project (e.g., 'MyAwesomeProject'). " + "This will be used for the main project folder and, in a sanitized form, " + "for the Python package name." + ) + # Aggiungere un flag per forzare la modalità CLI se __main__.py diventa più complesso + # parser.add_argument("--cli", action="store_true", help="Force CLI mode if auto-detection is ambiguous.") + + if cli_args is None: + args = parser.parse_args() # Uses sys.argv[1:] by default + else: + args = parser.parse_args(cli_args) + + + root_dir_path = Path(args.root_directory).resolve() # Resolve to absolute path for clarity + project_name_str = args.project_name.strip() + + if not project_name_str: + print("Error: Project name cannot be empty.", file=sys.stderr) + sys.exit(1) + + # La validazione di root_dir_path (se esiste ed è una directory) + # e la validazione più approfondita di project_name (es. isidentifier) + # sono gestite dalla funzione project_creator.create_project. + # Qui facciamo solo controlli di base sugli argomenti forniti. + + try: + print(f"Attempting to create project '{project_name_str}' in '{root_dir_path}'...") + success_message = project_creator.create_project(str(root_dir_path), project_name_str) + # Il core creator stampa già i suoi messaggi di info/successo. + # Potremmo voler sopprimere quelli e stampare solo il messaggio finale qui, + # ma per ora va bene così. + # print(f"\n{success_message}") # Già stampato dal core. + sys.exit(0) # Success + except project_creator.ProjectCreationError as e: + print(f"Error: Project creation failed: {e}", file=sys.stderr) + sys.exit(1) # Failure + except Exception as e: # Catch-all for other unexpected errors from core or here + print(f"Error: An unexpected error occurred: {e}", file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/projectinitializer/config/__init__.py b/projectinitializer/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projectinitializer/config/settings.py b/projectinitializer/config/settings.py new file mode 100644 index 0000000..d3b2ae7 --- /dev/null +++ b/projectinitializer/config/settings.py @@ -0,0 +1,353 @@ +# ProjectInitializerTool/project_initializer/config/settings.py + +import json +from pathlib import Path +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/ + +# 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 --- + +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: + with open(config_path, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError: + print(f"Warning: Configuration file {config_path} is corrupted. Using defaults.") + return {} + except IOError: + print(f"Warning: Could not read configuration file {config_path}. Using defaults.") + return {} + 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: + json.dump(config_data, f, indent=4) + except IOError: + print(f"Error: Could not write configuration file {config_path}.") + +# --- File Templates --- + +def get_gitignore_template() -> str: + """Returns the .gitignore template string.""" + return """# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a CI server in a temp folder. +# Then everything is copied to shipping folder during release. +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# PEP 582; __pypackages__ +__pypackages__/ + +# PEP 621; pyproject.toml sections +.pdm.toml +.pdm.lock +# .venv + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# static analysis tool +.flake8 + +# VS Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/ + +# sublime +*.sublime-workspace +*.sublime-project + +# Kate +.kateproject +.kateproject.lock +.katenewfile. Neuen Filenamensvorschlag merken. + +# 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.""" + return f"""# {project_name_original} + +A brief description of {project_name_original}. + +## Features +- Feature 1 +- Feature 2 + +## Getting Started +... + +## Contributing +... + +## License +... +""" + +def get_main_py_template(project_name_original: str, project_name_lower: str) -> str: + """Returns the __main__.py template string for the new project.""" + return f"""# {project_name_lower}/__main__.py + +# Example import assuming your main logic is in a 'main' function +# within a 'app' module in your '{project_name_lower}.core' package. +# from {project_name_lower}.core.app import main as start_application +# +# Or, if you have a function in {project_name_lower}.core.core: +# from {project_name_lower}.core.core import main_function + +def main(): + print(f"Running {project_name_original}...") + # Placeholder: Replace with your application's entry point + # Example: start_application() + print("To customize, edit '{project_name_lower}/__main__.py' and your core modules.") + +if __name__ == "__main__": + main() +""" + +def get_spec_file_template(project_name_original: str, project_name_lower: 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 + binaries=[], + datas=[('{DEFAULT_ICON_FILE_NAME}', '.')], # Icon file relative to project root + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='{project_name_original}', + debug=False, + bootloader_ignore_signals=False, + strip=False, + 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 +) +""" + +def get_english_manual_template(project_name_original: str) -> str: + """Returns the English manual template string.""" + return f"""# {project_name_original} - English Manual + +## Introduction +Welcome to {project_name_original}. This document provides an overview of how to install, use, and understand the project. + +## Installation +Describe the installation steps here. For example: +1. Clone the repository: `git clone ` +2. Navigate to the project directory: `cd {project_name_original}` +3. Install dependencies: `pip install -r requirements.txt` (if applicable) + +## Usage +Explain how to run and use the application. +- To run the application: `python -m {project_name_original.lower().replace(" ", "_").replace("-", "_")}` +- Command-line arguments (if any). +- GUI interaction (if any). + +## Development +Information for developers contributing to the project. +- Code structure. +- How to run tests. + +## Troubleshooting +Common issues and their solutions. +""" + +def get_italian_manual_template(project_name_original: str) -> str: + """Returns the Italian manual template string.""" + return f"""# {project_name_original} - Manuale Italiano + +## Introduzione +Benvenuto in {project_name_original}. Questo documento fornisce una panoramica su come installare, utilizzare e comprendere il progetto. + +## Installazione +Descrivi i passaggi di installazione qui. Ad esempio: +1. Clona il repository: `git clone ` +2. Naviga nella directory del progetto: `cd {project_name_original}` +3. Installa le dipendenze: `pip install -r requirements.txt` (se applicabile) + +## Utilizzo +Spiega come eseguire e utilizzare l'applicazione. +- Per eseguire l'applicazione: `python -m {project_name_original.lower().replace(" ", "_").replace("-", "_")}` +- Argomenti da riga di comando (se presenti). +- Interazione con la GUI (se presente). + +## Sviluppo +Informazioni per gli sviluppatori che contribuiscono al progetto. +- Struttura del codice. +- Come eseguire i test. + +## Risoluzione dei problemi +Problemi comuni e relative soluzioni. +""" \ No newline at end of file diff --git a/projectinitializer/core/__init__.py b/projectinitializer/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projectinitializer/core/project_creator.py b/projectinitializer/core/project_creator.py new file mode 100644 index 0000000..b08a598 --- /dev/null +++ b/projectinitializer/core/project_creator.py @@ -0,0 +1,195 @@ +# ProjectInitializerTool/project_initializer/core/project_creator.py + +import os +import shutil +import subprocess +from pathlib import Path +# Importa i settings per accedere ai template e al percorso dell'icona +from projectinitializer.config import settings # type: ignore + +# Tkinter messagebox è specifico della GUI. +# Per il core, dovremmo restituire successo/fallimento e messaggi, +# lasciando che sia il chiamante (GUI o CLI) a visualizzarli. +# Tuttavia, per mantenere una certa familiarità con il codice precedente +# e dato che l'utente potrebbe voler vedere feedback immediato anche se chiamato +# da una GUI che poi mostra un suo popup, lo lascio per ora, +# ma idealmente questo modulo non dovrebbe dipendere da tkinter. +# Una soluzione migliore sarebbe un sistema di logging o callback. +# Per semplicità, al momento, se la GUI è in uso, i messaggi appariranno +# anche dalla console se la GUI non sopprime stdout. +# Se la GUI non è in uso (CLI), print() è l'output atteso. +# Rimuoviamo la dipendenza diretta da messagebox per il core. +# Il chiamante (GUI/CLI) gestirà la notifica utente. + +class ProjectCreationError(Exception): + """Custom exception for errors during project creation.""" + pass + +def create_project(root_directory_str: str, project_name_original: str) -> str: + """ + Creates the project directory structure and initial files. + + Args: + root_directory_str (str): The root directory where the project folder will be created. + project_name_original (str): The name of the project (can have mixed case). + + Returns: + str: A success message. + + Raises: + ProjectCreationError: If any error occurs during project creation. + """ + root_directory = Path(root_directory_str) + if not root_directory.is_dir(): + 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) + if not project_name_lower.isidentifier(): + raise ProjectCreationError( + f"The sanitized project name '{project_name_lower}' (derived from '{project_name_original}') " + "is not a valid Python identifier. Please use letters, numbers, and underscores, " + "not starting with a number." + ) + + + if project_root_path.exists(): + raise ProjectCreationError(f"Project directory '{project_root_path}' already exists.") + + try: + # 1) Create main project folder + project_root_path.mkdir(parents=True, exist_ok=False) + print(f"Info: Created directory: {project_root_path}") + + # 2) Create source code subfolder (package name) + src_package_path = project_root_path / project_name_lower + src_package_path.mkdir() + print(f"Info: Created directory: {src_package_path}") + + # 3) Create source subdirectories and __init__.py files for the new project + core_subpath = src_package_path / "core" + core_subpath.mkdir() + print(f"Info: Created directory: {core_subpath}") + (core_subpath / "__init__.py").touch() + (core_subpath / "core.py").touch() # Empty core logic file + + 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 + + (src_package_path / "__init__.py").touch() # Make project_name_lower a package + + # 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) + print(f"Info: Created file: {src_package_path / '__main__.py'}") + + # 4) Create 'doc' folder and manual files + doc_path = project_root_path / "doc" + doc_path.mkdir() + print(f"Info: Created directory: {doc_path}") + + english_manual_content = settings.get_english_manual_template(project_name_original) + with open(doc_path / "English-manual.md", "w", encoding="utf-8") as f: + f.write(english_manual_content) + print(f"Info: Created file: {doc_path / 'English-manual.md'}") + + italian_manual_content = settings.get_italian_manual_template(project_name_original) + with open(doc_path / "Italian-manual.md", "w", encoding="utf-8") as f: + f.write(italian_manual_content) + print(f"Info: Created file: {doc_path / 'Italian-manual.md'}") + + # 5) Create README.md for the new project + readme_content = settings.get_readme_template(project_name_original) + with open(project_root_path / "README.md", "w", encoding="utf-8") as f: + f.write(readme_content) + print(f"Info: Created file: {project_root_path / 'README.md'}") + + # 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 + 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 + 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}") + + + # 8) Create .gitignore for the new project + gitignore_content = settings.get_gitignore_template() + with open(project_root_path / ".gitignore", "w", encoding="utf-8") as f: + f.write(gitignore_content) + print(f"Info: Created file: {project_root_path / '.gitignore'}") + + # 9) Initialize Git repository + try: + 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( + ["git", "commit", "-m", "Initial project structure created by ProjectInitializerTool"], + cwd=project_root_path, check=True, capture_output=True, text=True + ) + 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}" + 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}" + ) + print(f"\n{success_message}") + 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(): + try: + shutil.rmtree(project_root_path) + print(f"Info: Cleaned up partially created project at {project_root_path}") + 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 + 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 + try: + shutil.rmtree(project_root_path) + print(f"Info: Cleaned up partially created project at {project_root_path} due to unexpected error.") + except OSError as cleanup_e: + print(f"Error: Error during cleanup of '{project_root_path}': {cleanup_e}") + raise ProjectCreationError(error_msg) from e \ No newline at end of file diff --git a/projectinitializer/gui/__init__.py b/projectinitializer/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projectinitializer/gui/app_window.py b/projectinitializer/gui/app_window.py new file mode 100644 index 0000000..e4eb186 --- /dev/null +++ b/projectinitializer/gui/app_window.py @@ -0,0 +1,129 @@ +# ProjectInitializerTool/project_initializer/gui/app_window.py + +import tkinter as tk +from tkinter import filedialog, messagebox +from pathlib import Path +from projectinitializer.config import settings # type: ignore +from projectinitializer.core import project_creator # type: ignore + +class AppWindow: + def __init__(self, master: tk.Tk): + self.master = master + master.title("Project Initializer Tool") + # master.geometry("500x200") # Optional: set a default size + + self.app_config = settings.load_app_configuration() + self.last_root_dir = self.app_config.get("last_root_directory", str(Path.home())) + + # --- Widgets --- + # Root Directory Frame + root_dir_frame = tk.Frame(master, padx=5, pady=5) + root_dir_frame.pack(fill=tk.X, expand=False) + + tk.Label(root_dir_frame, text="Root Directory:").pack(side=tk.LEFT, padx=(0, 5)) + self.root_dir_entry = tk.Entry(root_dir_frame) + self.root_dir_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + self.root_dir_entry.insert(0, self.last_root_dir) + self.browse_button = tk.Button(root_dir_frame, text="Browse...", command=self._browse_root_directory) + self.browse_button.pack(side=tk.LEFT, padx=(5, 0)) + + # Project Name Frame + project_name_frame = tk.Frame(master, padx=5, pady=5) + project_name_frame.pack(fill=tk.X, expand=False) + + tk.Label(project_name_frame, text="Project Name:").pack(side=tk.LEFT, padx=(0, 5)) + self.project_name_entry = tk.Entry(project_name_frame) + self.project_name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Ensure project name entry also expands if window is resized + project_name_frame.grid_columnconfigure(1, weight=1) + + + # Proceed Button Frame (for centering or specific layout) + button_frame = tk.Frame(master, pady=10) + button_frame.pack() + + self.proceed_button = tk.Button(button_frame, text="Create Project", command=self._proceed_action, width=20) + self.proceed_button.pack() + + # Set focus to project name entry initially + self.project_name_entry.focus_set() + + # Configure resizing behavior for the main window content + # Let the entry fields expand with the window width + # Not strictly necessary with pack, but good practice for grid or more complex layouts + # master.grid_columnconfigure(0, weight=1) # if using grid for frames + + def _browse_root_directory(self) -> None: + """Opens a dialog to select the root directory.""" + initial_dir = self.root_dir_entry.get() or str(Path.home()) + if not Path(initial_dir).is_dir(): # check if path is valid, otherwise default to home + initial_dir = str(Path.home()) + + directory = filedialog.askdirectory( + initialdir=initial_dir, + title="Select Root Directory" + ) + if directory: # If a directory is selected (not cancelled) + self.root_dir_entry.delete(0, tk.END) + self.root_dir_entry.insert(0, directory) + + def _validate_inputs(self) -> bool: + """Validates user inputs from the GUI.""" + self.root_dir = self.root_dir_entry.get().strip() + self.project_name = self.project_name_entry.get().strip() + + if not self.root_dir: + messagebox.showerror("Input Error", "Root directory cannot be empty.") + self.root_dir_entry.focus_set() + return False + if not Path(self.root_dir).is_dir(): + messagebox.showerror("Input Error", "The selected root directory is not a valid directory.") + self.root_dir_entry.focus_set() + return False + if not self.project_name: + messagebox.showerror("Input Error", "Project name cannot be empty.") + self.project_name_entry.focus_set() + return False + + # Basic check for project name characters. + # More complex validation (like isidentifier for the sanitized version) + # is handled in the core, but a simple GUI check can be useful. + # For now, we rely on the core's validation. + + return True + + def _proceed_action(self) -> None: + """Handles the 'Create Project' button click.""" + if not self._validate_inputs(): + return + + # Save current root_dir for next time if it's valid and different + if Path(self.root_dir).is_dir() and self.root_dir != self.last_root_dir: + self.app_config["last_root_directory"] = self.root_dir + settings.save_app_configuration(self.app_config) + self.last_root_dir = self.root_dir # Update internal state + + self.proceed_button.config(state=tk.DISABLED) + self.master.update_idletasks() # Ensure button state updates immediately + + try: + # Call the core logic + success_message = project_creator.create_project(self.root_dir, self.project_name) + messagebox.showinfo("Success", success_message) + # Optionally clear fields or perform other actions on success + self.project_name_entry.delete(0, tk.END) # Clear project name for the next one + self.project_name_entry.focus_set() + except project_creator.ProjectCreationError as e: + messagebox.showerror("Project Creation Failed", str(e)) + except Exception as e: # Catch any other unexpected errors + messagebox.showerror("Unexpected Error", f"An unexpected error occurred: {str(e)}") + finally: + # Re-enable button + self.proceed_button.config(state=tk.NORMAL) + +def launch_gui_application() -> None: + """Initializes and runs the Tkinter GUI application.""" + root = tk.Tk() + app = AppWindow(root) + root.mainloop() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29