Skip to content

Development Guidelines

This guide describes how to make maintainable changes to History Workbench.

The user-facing UI uses CAD-oriented terminology while internal code keeps Git/diff terms for the time being.

UI termInternal term
ProjectGit repository
IterationGit commit
Save Iterationgit commit operation
Reviewed AreaGit staging/index
Current Files AreaGit working tree
Tree Comparisonsnapshot/tree/property diff
3D Comparisonvisual/BREP diff
History PanelDiff panel window

Contributor guidance:

  • User-facing text must use UI terms and translation-safe literals.
  • Internal code may keep Git/diff terms until additional refactoring phase.
  • Do not rename internal domain/application/infrastructure classes (GitService, DiffEngine, etc.).

Run all checks before submitting changes:

Terminal window
task check

Run tests (includes unit and integration):

Terminal window
task test
freecad/history_wb/
├── application/ # Use cases and dependency injection container
├── domain/ # Core models, services, settings, ports, snapshot and diff logic
├── entrypoints/ # FreeCAD workbench and command integration
├── infrastructure/ # FreeCAD, git, and persistence adapters
├── resources/ # Icons, translations, UI resources
└── ui/ # Qt views, presenters, UI state, and signal wiring
tests/
├── unit/ # Fast tests using fakes and pure Python behavior
├── integration/ # FreeCAD runtime tests
├── fakes/ # Test doubles for ports, views, loggers, and repositories
└── freecad/ # Test FreeCAD documents

Keep tests close to the source structure they cover. For example, behavior in freecad/history_wb/domain/diff/engine.py belongs under tests/unit/domain/diff/ unless it needs the FreeCAD runtime.

  • Keep changes minimal and focused.
  • Prefer readable functions and direct data flow over new abstractions.
  • Do not add compatibility layers unless there is a real persisted-data, shipped-API, or external-consumer need.
  • Do not add comments that describe old bugs or temporary phases.
  • Add comments only when code would otherwise be hard to understand.
  • Use ASCII in new text unless the file already uses non-ASCII or the content needs it.
  • Keep user-facing English strings extractable with translate("History", "...") or QT_TRANSLATE_NOOP(...) in-place.
  • Logs do not require translation.

Every Python file must start with a responsibility comment:

# File responsibility: Explains what this file owns.

Every package __init__.py must start with a module responsibility comment:

# Module responsibility: Explains what this package exposes or coordinates.

Keep those comments accurate when files change.

Keep function complexity at B (5-10) or better. Functions rated C (10-20) or higher must be refactored by extracting helper methods.

Check complexity before submitting changes:

Terminal window
uv run radon cc --min C freecad/history_wb --no-assert -s

Target: No C-rated functions in the codebase.

Create dependencies at composition roots and pass them into classes or actions. Do not create FreeCAD, git, filesystem, or UI dependencies deep inside domain or application logic.

Preferred pattern:

class CreateDiffAction:
def __init__(self, diff_engine: DiffEngine) -> None:
self._diff_engine = diff_engine
def execute(self, old_snapshot: Snapshot, new_snapshot: Snapshot) -> Result[DiffResult]:
diff = self._diff_engine.compute_diff(old_snapshot, new_snapshot)
return Result.success(diff)

Guidelines:

  • Application actions receive services and ports in constructors.
  • Domain services receive protocols or value objects, not concrete infrastructure classes.
  • UI presenters receive the specific view objects, action objects, and UI state they need.
  • Infrastructure adapters wrap external APIs.
  • ApplicationContainer wires application actions and domain services.
  • compose_and_register_panel() wires views, presenters, DialogView, and UIState.
  • ui/wiring.py binds public component signals to presenter listener methods.

Use classes when identity, state, interface contracts, or dependency injection make behavior clearer.

Good class use cases:

  • Domain models and dataclasses: Snapshot, TreeNode, Property, DiffResult.
  • Protocols and interfaces: GitPort, SettingsRepository, FreeCadPort.
  • Services with injected dependencies: DiffEngine, GitService.
  • Stateful infrastructure adapters: GitPortAdapter, FreeCADSettingsRepository.
  • UI presenters and views.

Use functions for stateless, deterministic behavior.

Good function use cases:

  • Pure transformations.
  • Small comparison helpers.
  • Serialization helpers.
  • Test data builders.
  • Local helpers that do not need injected dependencies.

Do not wrap a simple pure algorithm in a class only to make it look architectural.

Prefer package-level imports when symbols are exported by __init__.py:

from freecad.history_wb.domain.diff import DiffEngine
from freecad.history_wb.domain.snapshots import Snapshot
from freecad.history_wb.domain.tree import Property

Direct module imports are fine when a symbol is intentionally not re-exported:

from freecad.history_wb.infrastructure.git.git_port_adapter import GitPortAdapter

Use __all__ for clear module APIs when a package or module exposes a stable set of public symbols. Internal helpers should use a leading underscore.

Use Log from freecad.history_wb.utils for project logging:

from freecad.history_wb.utils import Log
Log.info("Repository refreshed")
Log.warning("No active document")
Log.error("Snapshot could not be loaded")

Behavior:

  • Before FreeCAD initialization, logging falls back to stdout/stderr.
  • After first workbench activation, logs go through FreeCADLogger to the FreeCAD console.
  • Tests can install FakeLogger with set_logger().
  • Log messages are developer-facing and do not need translation.

All user-facing English UI text should be defined at the usage site so Qt extraction can detect literal strings.

Pattern:

  • Translate immediate UI strings with literal calls: translate("History", "...").
  • Use QT_TRANSLATE_NOOP for deferred strings with explicit context:
    • Command GetResources() strings use exact command-name contexts (for example, DiffCommit).
    • Workbench menu/toolbar strings use Workbench context.
  • Use Qt-style placeholders such as %1 and %2 in templates.
  • Presenters should pass raw data rather than formatted translated messages.
  • Keep translation template at freecad/history_wb/resources/translations/History.ts with locale files named History_<locale>.ts.

Example:

REPOSITORY_INFO_TEMPLATE = "Repository: %1"

Use literals directly in views and entry points for extractor visibility; avoid unextracted variable indirection.

Write tests that protect real behavior and document useful contracts. Avoid tests that only freeze implementation details.

Good tests:

  • Verify public behavior and result contracts.
  • Verify components are wired correctly through observable outcomes.
  • Verify domain algorithms with meaningful examples.
  • Verify integration between application actions and services.
  • Verify FreeCAD runtime behavior when stubs or fakes are insufficient.

Avoid tests that:

  • Check non-existence of fields or classes (negative tests).
  • Duplicate existing coverage without adding a new failure mode.
  • Assert private implementation details like _git_port, _git_service, or internal action wiring.
  • Test fake internals, protocol compliance, or method existence.
  • Verify dataclass defaults, hasattr checks, repr output, or construction trivia.
  • Preserve temporary development phases in filenames or test names.

Each behavior should have one owning layer. Duplicate tests across layers create noise when failures occur.

  • Domain tests own pure business behavior and algorithms: models, services, diff logic, snapshot extraction rules, git workflow rules.
  • Application tests own orchestration, result contracts, and dependency forwarding only when forwarding is part of the action contract.
  • Infrastructure tests own adapter parsing, command construction, subprocess invocation shape, and external error mapping.
  • UI tests own observable presenter/view behavior, state updates, and signal wiring. Do not test private Qt styling details unless styling is an explicit product contract.
  • Integration tests own behavior that requires real FreeCAD, Qt runtime, real document structure, workbench activation, or real runtime wiring.

Integration tests run with FreeCAD’s Python interpreter and real App runtime. In CLI/headless runs where FreeCADGui is unavailable, integration fixtures provide a GuiLike mock adapter that implements the required port surface (getDocument(), isModified(), getViewProvider()) against real App documents. This keeps integration coverage stable without requiring a full interactive GUI session.

Skipped tests should not remain in the suite long-term. If a unit test needs FreeCAD or Qt runtime, move useful coverage to integration tests and delete the skipped version. Debug-only or permanently skipped files provide no value and should be removed.

Use @pytest.mark.parametrize to keep edge-case coverage concise. When multiple tests differ only in input values or expected outputs, consolidate them into a single parametrized test. This reduces file size, prevents repetitive failures, and makes the test suite easier to scan.

When patching standard library modules (subprocess, os, pathlib) in unit tests, use unittest.mock.patch context managers instead of pytest.monkeypatch.setattr. Context managers limit the scope of patches and prevent global state from leaking into IDE pytest hooks.

For application-specific modules, monkeypatch is appropriate and convenient.

Location: tests/unit/

Use unit tests for fast feedback on pure behavior. Unit tests should not require FreeCAD to be running. Follow the ownership rules above: domain algorithms, application orchestration, infrastructure adapters (without real subprocess or git), and UI presenter logic with fakes.

Do not use /tmp or /var/tmp paths in tests. Use stable fake paths such as /home/user/dir/... to prevent FreeCAD Addons Report static analyzer failures for insecure temporary path usage.

Run tests (includes unit and integration):

Terminal window
task test
  1. Add a focused action class under the appropriate subdirectory in freecad/history_wb/application/actions/ (e.g. git_repo/, diffs/, documents/).
  2. Return an existing result model or add a small result model when needed.
  3. Inject domain services or ports through the constructor.
  4. Wire the action in application/container.py.
  5. Add unit tests under the matching subdirectory in tests/unit/application/actions/.
  1. Add translated UI text at the display site using translate("History", "...").
  2. For deferred text, define literal with QT_TRANSLATE_NOOP in correct context.
  3. Keep placeholders (%1, %2) in the source literal and replace after translation.
  4. Avoid formatting translated strings in presenters.
  1. Add a command class in entrypoints/commands.py.
  2. Keep FreeCAD-specific code in the command.
  3. Delegate behavior to a presenter or application action.
  4. Register the command in register_commands().
  5. Add the command name to the workbench toolbox if it should appear in toolbar/menu.
  6. Add command tests under tests/unit/entrypoints/.
  1. Add default values in domain/config.py if needed.
  2. Update settings models or codecs under domain/settings/.
  3. Update FreeCADSettingsRepository persistence behavior.
  4. Update preferences UI in ui/views/settings_preferences_page.py.
  5. Add tests for model/codec behavior and repository persistence behavior.
  1. Put pure comparison behavior under domain/diff/.
  2. Put snapshot model or serialization behavior under domain/snapshots/ or infrastructure/persistence/ depending on whether it is core model behavior or file format behavior.
  3. Use focused unit tests for algorithms.
  4. Add integration tests if real FreeCAD documents or runtime APIs are required.

FreeCAD type stubs are installed by uv under .venv/lib/python3.11/site-packages/. Check them before assuming FreeCAD signatures.

Use the extracted AppImage runtime for live API checks:

Terminal window
./run_with_freecad.sh python -c "import FreeCAD; print(dir(FreeCAD))"

The FreeCADGui module is not generally available through run_with_freecad.sh. GUI behavior often needs manual FreeCAD testing or integration tests that run through FreeCAD’s runtime.