Chore: Stop tracking files based on .gitignore update.

Untracked files matching the following rules:
- Rule "*.pyc": 10 files
This commit is contained in:
VALLONGOL 2025-05-07 13:21:14 +02:00
commit dc0d4b18d1
15 changed files with 964 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.svn
*.pyc
_dist/
_build/

15
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

74
README.md Normal file
View File

@ -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 <root_directory> <ProjectName>
```
- `<root_directory>`: The directory where the new project folder (named `<ProjectName>`) will be created.
- `<ProjectName>`: 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.

View File

View File

@ -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: <root_directory> <project_name>", 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()

View File

View File

@ -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)

View File

View File

@ -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 <repository_url>`
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 <repository_url>`
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.
"""

View File

View File

@ -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

View File

View File

@ -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()

0
requirements.txt Normal file
View File