add version, change icon file name

This commit is contained in:
VALLONGOL 2025-05-08 10:47:04 +02:00
parent ef7452a378
commit 071f34e0c3
6 changed files with 191 additions and 61 deletions

BIN
projectinitializer.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

36
projectinitializer.spec Normal file
View File

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

View File

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

View File

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

View File

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

View File

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