Skip to content

Path Management Guide

Professional Handling of File Paths in GehaSoftwareHub


Table of Contents

  1. Introduction
  2. Why a New Path System?
  3. Architecture Overview
  4. PathDef and PathType
  5. Defining New Paths
  6. Using Paths in Code
  7. Migration from Old System
  8. Deployment Behavior
  9. Best Practices
  10. 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:

  1. Identify all path strings used in the file
  2. Add PathDef constants to paths.py (if not already defined)
  3. Replace path_helper.get_resource_path() with get_path() or get_path_str()
  4. Remove old imports when no longer needed
  5. 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)

  1. Create data_root directory
  2. Create empty PROTECTED directories (app_logs/, persistent_data/)
  3. Copy REPLACEABLE directories from bundle to data_root
  4. Write version.txt with current app version

App Update (Deploy Mode)

When version.txt differs from current app version:

  1. Delete all REPLACEABLE directories in data_root
  2. Re-copy REPLACEABLE directories from new bundle
  3. PROTECTED directories are NOT touched (user data preserved)
  4. 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

# __init__.py files should be empty
# Never put path definitions or get_path() calls 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.