Path Management Guide¶
Professional Handling of File Paths in GehaSoftwareHub
Table of Contents¶
- Introduction
- Why a New Path System?
- Architecture Overview
- PathDef and PathType
- Defining New Paths
- Using Paths in Code
- Migration from Old System
- Deployment Behavior
- Best Practices
- Complete Examples
Introduction¶
GehaSoftwareHub uses a centralized path management system that provides:
- Type-safe path definitions - Compile-time validation via PathDef
- Dev/Deploy awareness - Automatic path resolution based on environment
- Version-aware updates - Smart handling of app updates
- Nuitka compatibility - Full support for bundled deployments
- IDE integration - Hover documentation and autocomplete
Quick Start¶
# Step 1: Import the path constant and resolver
from src.shared_services.constants.paths import Icons, Logging
from src.shared_services.path_management.api import get_path, get_path_str
# Step 2: Use get_path() for pathlib operations
log_dir = get_path(Logging.Directory)
for log_file in log_dir.glob("*.log"):
print(log_file.name)
# Step 3: For icons, use IconRegistry (theme-aware)
from src.shared_services.rendering.icons.api import IconRegistry, IconColors
registry = IconRegistry.instance()
registry.register(my_button, Icons.Logos.GitHub, color=IconColors.Primary)
# Step 4: Use get_path_str() for other Qt APIs needing strings
start_dir = get_path_str(UserData.ExportDirectory)
Why a New Path System?¶
Problems with the Old Approach¶
# The old path_helper approach
from SharedServices.Helpers.RessourceHandling.path_helper_singleton import get_path_helper
path_helper = get_path_helper(mode="dev")
# Problems with this approach:
logo_path = path_helper.get_resource_path(".app_data/icons/logos/github-mark.svg")
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# Raw string! No validation!
# Typo? Wrong path? Silent failure!
Issues: - Raw strings can contain typos - No IDE autocomplete for paths - No compile-time validation - Scattered path definitions across codebase - Hard to track which paths exist - No documentation for what each path is
The New Approach¶
# The new PathDef approach
from src.shared_services.constants.paths import Icons
from src.shared_services.path_management.api import get_path_str
# Type-safe! IDE shows documentation on hover!
logo_path = get_path_str(Icons.Logos.GitHub)
# ^^^^^^^^^^^^^^^^^^
# - Autocomplete works!
# - Hover shows description!
# - Typo = immediate error!
# - All paths in one file!
Benefits:
- All paths defined in ONE file (src/shared_services/constants/paths.py)
- IDE autocomplete and hover documentation
- Type validation - passing a string to get_path() raises TypeError
- Clear categorization (Icons, Security, Logging, etc.)
- Protected vs Replaceable classification for updates
Architecture Overview¶
Module Structure¶
src/shared_services/
constants/
paths.py # ALL path definitions (PathDef constants)
path_management/
api.py # Public API: get_path(), get_path_str(), initialize_paths()
path_manager.py # Internal: PathManager class
path_types.py # PathDef dataclass, PathType enum
exceptions.py # Custom exceptions
Initialization Flow¶
GehaSoftwareHub.py
|
v
initialize_paths() <-- MUST be called before any imports that use get_path()
|
v
PathManager._detect_mode() --> "dev" or "deploy"
|
v
PathManager._setup_paths()
|
+-- Dev Mode: data_root = project_root / "data"
|
+-- Deploy Mode: data_root = AppData/GehaSoftwareHub
+ version check
+ copy/update directories as needed
Path Resolution¶
PathDef("app_logs/", PathType.PROTECTED)
|
v
get_path(Logging.Directory)
|
v
PathManager.resolve()
|
+-- Dev: C:/Users/.../GehaSoftwareHub/data/app_logs/
|
+-- Deploy: C:/Users/.../AppData/Local/GehaSoftwareHub/app_logs/
PathDef and PathType¶
PathDef Dataclass¶
PathDef is an immutable dataclass that defines a path and its behavior:
from dataclasses import dataclass
from typing import Final
@dataclass(frozen=True, slots=True)
class PathDef:
"""Definition of an application path."""
relative_path: str # Path relative to data root
path_type: PathType # PROTECTED or REPLACEABLE
is_directory: bool = False
create_parents: bool = False
description: str = "" # For IDE documentation (optional)
PathType Enum¶
PathType controls how directories behave during app updates:
class PathType(Enum):
"""Classification of path behavior during updates."""
PROTECTED = "protected"
# User data that must NEVER be overwritten
# Examples: logs, user settings, saved projects
# On update: Keep existing, don't touch
REPLACEABLE = "replaceable"
# App resources that should be refreshed on update
# Examples: icons, stylesheets, templates
# On update: Delete and re-copy from bundle
Directory Prefixes¶
The system automatically classifies directories based on their prefix:
# Protected directories (user data)
PROTECTED_PREFIXES = (
"app_logs/",
"persistent_data/",
)
# Replaceable directories (app resources)
REPLACEABLE_PREFIXES = (
".app_data/",
".app_temp/",
".security/",
)
Defining New Paths¶
All paths MUST be defined in src/shared_services/constants/paths.py.
Basic Pattern¶
from typing import Final
from src.shared_services.path_management.path_types import PathDef, PathType
class CategoryName:
"""Description of this category."""
PathName: Final = PathDef(
"prefix/path/to/resource.ext",
PathType.REPLACEABLE, # or PROTECTED
)
"""Description shown on hover in IDE."""
File Path Example¶
class Security:
"""Security-related files."""
ZipKey: Final = PathDef(
".security/zip_key.enc",
PathType.REPLACEABLE,
)
"""Encrypted key for ZIP archive operations. Static, bundled with app."""
Directory Path Example¶
class Logging:
"""Logging-related paths."""
Directory: Final = PathDef(
"app_logs/",
PathType.PROTECTED,
is_directory=True,
)
"""Directory where application log files are stored. Protected from updates."""
Nested Categories¶
class Icons:
"""Application icons organized by category."""
class Logos:
"""Brand and logo images."""
GitHub: Final = PathDef(
".app_data/icons/logos/github-mark.svg",
PathType.REPLACEABLE,
)
"""GitHub logo for OAuth login and external links."""
CompanyLogo: Final = PathDef(
".app_data/icons/logos/company.svg",
PathType.REPLACEABLE,
)
"""Company branding logo."""
class Actions:
"""Action and toolbar icons."""
Save: Final = PathDef(
".app_data/icons/actions/save.svg",
PathType.REPLACEABLE,
)
"""Save action icon."""
Path with Auto-Create¶
For paths where parent directories should be created automatically:
class UserData:
"""User-generated data."""
ExportDirectory: Final = PathDef(
"persistent_data/exports/",
PathType.PROTECTED,
is_directory=True,
create_parents=True, # Creates directory on first access
)
"""Export output directory. Created automatically if needed."""
Using Paths in Code¶
Import Pattern¶
# Always import from constants and api
from src.shared_services.constants.paths import Logging, Icons, Security
from src.shared_services.path_management.api import get_path, get_path_str
get_path() - For pathlib Operations¶
Returns a pathlib.Path object for filesystem operations:
from src.shared_services.constants.paths import Logging
from src.shared_services.path_management.api import get_path
# Get the directory path
log_dir = get_path(Logging.Directory)
# Use pathlib methods
for log_file in log_dir.glob("*.log"):
content = log_file.read_text()
# Create files
new_log = log_dir / "new_session.log"
new_log.write_text("Log started")
# Check existence
if log_dir.exists():
print(f"Logs at: {log_dir}")
get_path_str() - For Qt and String APIs¶
Returns an absolute path string for APIs that expect strings:
from src.shared_services.constants.paths import Icons, UserData
from src.shared_services.path_management.api import get_path_str
# For icons, prefer IconRegistry for theme-aware rendering:
from src.shared_services.rendering.icons.api import IconRegistry, IconColors
registry = IconRegistry.instance()
registry.register(my_button, Icons.Logos.GitHub, color=IconColors.Primary)
# Use get_path_str() for non-icon Qt APIs that need string paths
from PySide6.QtWidgets import QFileDialog
QFileDialog.getOpenFileName(
self,
"Open File",
get_path_str(UserData.ExportDirectory), # Start directory
)
Error Handling¶
from src.shared_services.path_management.api import get_path
from src.shared_services.path_management.exceptions import (
PathNotInitializedError,
PathError,
)
try:
path = get_path(Logging.Directory)
except PathNotInitializedError:
# initialize_paths() was not called
print("Path system not initialized!")
except PathError as e:
# General path error
print(f"Path error: {e}")
WRONG: Do Not Use Raw Strings¶
# WRONG - Bypasses the system entirely
path = Path("data/app_logs/") # No validation!
# WRONG - TypeError at runtime
get_path("app_logs/") # get_path requires PathDef, not string!
# WRONG - Direct string without get_path_str
icon = QIcon(".app_data/icons/logo.svg") # Relative path won't work!
Migration from Old System¶
Step 1: Add PathDef to paths.py¶
# In src/shared_services/constants/paths.py
class Templates:
"""Template files."""
ReportTemplate: Final = PathDef(
".app_data/templates/report.html",
PathType.REPLACEABLE,
)
"""HTML template for report generation."""
Step 2: Update Imports in Your Module¶
# BEFORE (old system)
from SharedServices.Helpers.RessourceHandling.path_helper_singleton import get_path_helper
path_helper = get_path_helper(mode="dev")
template_path = path_helper.get_resource_path(".app_data/templates/report.html")
# AFTER (new system)
from src.shared_services.constants.paths import Templates
from src.shared_services.path_management.api import get_path_str
template_path = get_path_str(Templates.ReportTemplate)
Step 3: Coexistence Period¶
Both systems can coexist during migration:
# Old system still works for unmigrated code
from SharedServices.Helpers.RessourceHandling.path_helper_singleton import get_path_helper
# New system for migrated code
from src.shared_services.path_management.api import get_path
# Both resolve to the same physical location
Migration Checklist¶
When migrating a file:
- Identify all path strings used in the file
- Add PathDef constants to
paths.py(if not already defined) - Replace
path_helper.get_resource_path()withget_path()orget_path_str() - Remove old imports when no longer needed
- Test the file works in both dev and deploy modes
Deployment Behavior¶
Dev Mode (Running from Source)¶
Mode: "dev" (detected automatically when sys.frozen is False)
data_root = project_root / "data"
= C:/Users/.../GehaSoftwareHub/data/
All paths resolve relative to this directory.
No copying, no version checks.
Deploy Mode (Nuitka Bundle)¶
Mode: "deploy" (detected when sys.frozen is True)
bundle_root = exe_directory / "data"
= C:/Program Files/GehaSoftwareHub/data/
data_root = AppData/Local/GehaSoftwareHub
= C:/Users/.../AppData/Local/GehaSoftwareHub/
Version file: data_root / "version.txt"
First Launch (Deploy Mode)¶
- Create
data_rootdirectory - Create empty PROTECTED directories (app_logs/, persistent_data/)
- Copy REPLACEABLE directories from bundle to data_root
- Write version.txt with current app version
App Update (Deploy Mode)¶
When version.txt differs from current app version:
- Delete all REPLACEABLE directories in data_root
- Re-copy REPLACEABLE directories from new bundle
- PROTECTED directories are NOT touched (user data preserved)
- Update version.txt
Best Practices¶
1. Define ALL Paths Centrally¶
# All paths go in src/shared_services/constants/paths.py
# NEVER use raw path strings elsewhere in the code
2. Use Descriptive Names and Categories¶
# GOOD - Clear hierarchy and naming
class Icons:
class Toolbar:
Save: Final = PathDef(...)
Open: Final = PathDef(...)
class Status:
Success: Final = PathDef(...)
Error: Final = PathDef(...)
# BAD - Flat, unclear
Icon1: Final = PathDef(...)
Icon2: Final = PathDef(...)
3. Add Hover Documentation¶
# Use PEP 224-style docstrings (string after assignment)
LogDirectory: Final = PathDef(
"app_logs/",
PathType.PROTECTED,
is_directory=True,
)
"""Application log directory. Contains session logs, cleaned automatically."""
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# This appears when hovering over LogDirectory in PyCharm/VS Code
4. Choose PathType Correctly¶
# PROTECTED: User data that must survive updates
# - Logs, settings, saved projects, user preferences
# REPLACEABLE: App resources that should be updated
# - Icons, stylesheets, templates, bundled data
5. Use Appropriate Resolver¶
# Use get_path() for pathlib operations
log_dir = get_path(Logging.Directory)
files = list(log_dir.iterdir())
# Use get_path_str() for Qt/string APIs
icon = QIcon(get_path_str(Icons.Logo))
6. Handle Initialization Order¶
# In GehaSoftwareHub.py - initialize FIRST
import os
import sys
from src.shared_services.path_management.api import initialize_paths
initialize_paths() # BEFORE any imports that use get_path()
# Then other imports
from some_module import SomeClass # This module can now use get_path()
7. No Path Logic in init.py¶
Complete Examples¶
Example 1: Logger Using Path System¶
# src/shared_services/logging/async_app_logger.py
from pathlib import Path
from src.shared_services.path_management.api import get_path
from src.shared_services.constants.paths import Logging
class AsyncAppLogger:
"""Async application logger using centralized paths."""
def __init__(self, app_name: str):
self.app_name = app_name
self.log_dir = get_path(Logging.Directory)
self._setup_log_file()
def _setup_log_file(self) -> None:
"""Create log file in the logging directory."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_file = self.log_dir / f"{self.app_name}_{timestamp}.log"
# Directory is auto-created by PathManager if is_directory=True
self.log_file.touch()
Example 2: Icon Loading with IconRegistry¶
# view/main_window_view.py
from PySide6.QtWidgets import QMainWindow, QToolBar, QPushButton
from src.shared_services.rendering.icons.api import IconRegistry, Icons, IconColors
class MainWindowView(QMainWindow):
"""Main window with toolbar icons."""
def _setup_toolbar(self) -> None:
toolbar = QToolBar("Main Toolbar")
# Create buttons for the toolbar
self._save_button = QPushButton("Save")
self._save_button.setObjectName("toolbarSaveButton")
self._open_button = QPushButton("Open")
self._open_button.setObjectName("toolbarOpenButton")
toolbar.addWidget(self._save_button)
toolbar.addWidget(self._open_button)
self.addToolBar(toolbar)
# Register icons via IconRegistry for theme-aware updates
registry = IconRegistry.instance()
registry.register(self._save_button, Icons.Toolbar.Save, color=IconColors.Primary)
registry.register(self._open_button, Icons.Toolbar.Open, color=IconColors.Primary)
Example 3: Reading Configuration Files¶
# config/app_config.py
from src.shared_services.constants.paths import Config
from src.shared_services.file_operations.api import load_json, save_json
def load_app_config() -> dict:
"""Load application configuration."""
try:
return load_json(Config.AppSettings)
except FileNotFoundError:
# Return defaults if no config file
return {
"theme": "light",
"language": "de",
}
def save_app_config(config: dict) -> None:
"""Save application configuration."""
save_json(Config.AppSettings, config)
Example 4: Adding New Path Category¶
# Step 1: Define in src/shared_services/constants/paths.py
class Reports:
"""Report generation paths."""
class Templates:
"""Report templates."""
Monthly: Final = PathDef(
".app_data/reports/templates/monthly.html",
PathType.REPLACEABLE,
)
"""Monthly report HTML template."""
Yearly: Final = PathDef(
".app_data/reports/templates/yearly.html",
PathType.REPLACEABLE,
)
"""Yearly report HTML template."""
OutputDirectory: Final = PathDef(
"persistent_data/reports/",
PathType.PROTECTED,
is_directory=True,
create_parents=True,
)
"""Generated reports output directory. User data, protected."""
# Step 2: Use in your code
from src.shared_services.constants.paths import Reports
from src.shared_services.path_management.api import get_path
def generate_monthly_report(data: dict) -> Path:
"""Generate monthly report from data."""
# Load template
template_path = get_path(Reports.Templates.Monthly)
template = template_path.read_text()
# Generate report
report_html = template.format(**data)
# Save to output directory
output_dir = get_path(Reports.OutputDirectory)
output_file = output_dir / f"monthly_{datetime.now():%Y%m}.html"
output_file.write_text(report_html)
return output_file
Summary¶
Key Points¶
| Aspect | Recommendation |
|---|---|
| Path definitions | Always in src/shared_services/constants/paths.py |
| Path resolution | Use get_path() or get_path_str() from api.py |
| User data | PathType.PROTECTED (never overwritten) |
| App resources | PathType.REPLACEABLE (updated with app) |
| Qt integration | Use get_path_str() for string-expecting APIs |
| Filesystem ops | Use get_path() for pathlib.Path object |
| Documentation | Add docstring after PathDef assignment |
| Initialization | Call initialize_paths() before dependent imports |
Decision Matrix¶
| Need | Use |
|---|---|
| Define a new path | Add PathDef to paths.py |
| Use path for file I/O | get_path(PathConstant) |
| Use path for QIcon/Qt | get_path_str(PathConstant) |
| User-generated data | PathType.PROTECTED |
| Bundled resources | PathType.REPLACEABLE |
| Auto-create directory | is_directory=True, create_parents=True |
Professional path management: Define once, use everywhere, never hardcode.