Skip to content

Conversation

@svazquezco
Copy link
Collaborator

@svazquezco svazquezco commented Jan 19, 2026

Closes MPT-16846

  • Add CLI --fake option to mark a specific migration as applied without executing it
  • Implement ApplyMigrationUseCase to locate or create migration state and mark it as fake
  • Add Migration.fake() to set applied_at and clear started_at
  • Introduce FileStateManager (mpt_tool/managers/file_state.py) for JSON-based migration state persistence and StateJSONEncoder (mpt_tool/managers/encoders.py) for serialization
  • Expose FileMigrationManager and FileStateManager via mpt_tool.managers.init
  • Update FileMigrationManager.load_migration() to return a migration instance (BaseCommand) and improve import/error handling
  • Reorganize errors: move manager errors to mpt_tool.managers.errors and use-case errors (including ApplyMigrationError) to mpt_tool.use_cases.errors; remove duplicate subclasses from mpt_tool/errors.py
  • Update use cases and CLI to import the new error modules and operate on loaded migration instances directly
  • Update CLI migrate entrypoint signature and flow to support --fake and handle ApplyMigrationError
  • Add and update tests (tests/test_cli.py) covering fake migration success and failure scenarios

@svazquezco svazquezco requested a review from a team as a code owner January 19, 2026 13:07
@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

Adds a --fake CLI option and ApplyMigrationUseCase to mark migrations applied without running them; moves detailed errors into manager/use-case modules; introduces FileStateManager and StateJSONEncoder for JSON state; refactors file migration loading to return migration instances; updates tests and exports.

Changes

Cohort / File(s) Summary
CLI & tests
mpt_tool/cli.py, tests/test_cli.py
Adds --fake Typer option to migrate(...), wires to ApplyMigrationUseCase, handles ApplyMigrationError; adds fixtures and tests for fake migration flows and updates existing CLI tests.
Apply migration use case
mpt_tool/use_cases/apply_migration.py, mpt_tool/use_cases/errors.py
New ApplyMigrationUseCase.execute(migration_id) implementing validate/find/create-or-load-state/fake/persist flow; introduces UseCaseError, NewMigrationError, RunMigrationError, ApplyMigrationError.
Managers: state & encoders
mpt_tool/managers/file_state.py, mpt_tool/managers/encoders.py, mpt_tool/managers/__init__.py
Adds FileStateManager for JSON-backed migration state persistence and StateJSONEncoder for serializing Migration; exports FileMigrationManager and FileStateManager.
File migration refactor
mpt_tool/managers/file_migration.py
load_migration now returns an instantiated migration (BaseCommand) instead of a module; adds new_migration(...); removes embedded JSON state handling and shifts errors to mpt_tool.managers.errors.
Managers errors
mpt_tool/managers/errors.py
Adds manager-scoped error hierarchy: ManagerError, CreateMigrationError, InvalidStateError, LoadMigrationError, MigrationFolderError, StateNotFoundError.
Root errors reduced
mpt_tool/errors.py
Removes several fine-grained exception subclasses (retains BaseError).
Use-cases: run/new adjustments
mpt_tool/use_cases/run_migrations.py, mpt_tool/use_cases/new_migration.py
Updates imports to manager/use-case error modules; uses returned migration instances directly; broadens exception handling around run() to wrap failures as RunMigrationError.
Models
mpt_tool/models.py
Adds Migration.fake() to mark a migration as fake (sets started_at to None and updates applied_at).
Config / Lint
pyproject.toml
Adds per-file WPS232 ignore for mpt_tool/cli.py.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Jira Issue Key In Title ✅ Passed The PR title contains exactly one Jira issue key (MPT-16846) in the required MPT-XXXX format.
Test Coverage Required ✅ Passed PR modifies 13 code files and includes changes to tests/test_cli.py with 60 new lines including 4 new test functions and fixture for new fake migration feature.
Single Commit Required ✅ Passed The PR contains exactly one commit, satisfying the single commit requirement with a clean git history.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@mpt_tool/cli.py`:
- Around line 74-83: The except block handling ApplyMigrationError should
suppress exception chaining when aborting; update the handler for
ApplyMigrationUseCase().execute (catching ApplyMigrationError) to call raise
typer.Abort from None instead of raise typer.Abort so the chain is not shown
(and keep the existing typer.secho error message before abort).

In `@mpt_tool/use_cases/apply_migration.py`:
- Around line 31-38: Currently the code unconditionally calls state.fake() and
self.state_manager.save_state(state), which overwrites applied_at for migrations
that already exist; change the flow so after retrieving state via
self.state_manager.get_by_id(migration_id) (and after creating it in the
StateNotFoundError branch), you only call state.fake() and
self.state_manager.save_state(state) when the state indicates it is not already
applied (e.g., state.applied_at is None or state.is_applied() is False). Keep
the StateNotFoundError branch that creates the new state via
self.file_migration_manager.load_migration(...) and self.state_manager.new(...),
but ensure existing applied states are treated as a no-op to preserve audit
timestamps.
- Around line 19-36: When creating a new state after StateNotFoundError, wrap
the call to file_migration_manager.load_migration so LoadMigrationError is
caught and translated into the use-case error contract: catch LoadMigrationError
and raise ApplyMigrationError(...) from the caught error (preserving the
original exception), or broaden the except to handle both StateNotFoundError and
LoadMigrationError; reference file_migration_manager.load_migration,
state_manager.new, LoadMigrationError and ApplyMigrationError so the load path
is explicitly wrapped and no LoadMigrationError escapes the ApplyMigration
use-case.
🧹 Nitpick comments (2)
mpt_tool/managers/file_migration.py (2)

44-44: Add isinstance validation for BaseCommand in load_migration.

The function advertises a BaseCommand return type but doesn't enforce it. While the template ensures new migrations inherit from BaseCommand via DataBaseCommand or SchemaBaseCommand, manually crafted or corrupted migration files could bypass this. Add a fast-fail guard to catch such cases immediately.

Proposed safeguard
         try:
             migration_instance = migration_module.Command()
         except (TypeError, AttributeError) as error:
             raise LoadMigrationError(f"Invalid migration Command: {error!s}")
 
+        if not isinstance(migration_instance, BaseCommand):
+            raise LoadMigrationError("Migration Command must extend BaseCommand.")
+
         return migration_instance

35-42: Consider chaining exceptions to preserve stack traces for debugging.

While B904 is already ignored in the project configuration, exception chaining with from error remains a best practice. It ensures the root cause is preserved in stack traces, making troubleshooting easier during development and debugging.

🔧 Suggested change
         try:
             spec.loader.exec_module(migration_module)
         except ImportError as error:
-            raise LoadMigrationError(f"Failed to import migration module: {error!s}")
+            raise LoadMigrationError(
+                f"Failed to import migration module: {error!s}"
+            ) from error
 
         try:
             migration_instance = migration_module.Command()
         except (TypeError, AttributeError) as error:
-            raise LoadMigrationError(f"Invalid migration Command: {error!s}")
+            raise LoadMigrationError(
+                f"Invalid migration Command: {error!s}"
+            ) from error

@svazquezco svazquezco changed the title MPT 16846 implement fake option MPT 16846: implement fake option Jan 19, 2026
@svazquezco svazquezco changed the title MPT 16846: implement fake option MPT-16846: implement fake option Jan 19, 2026
@svazquezco svazquezco force-pushed the MPT-16846-implement-fake-option branch from 5f29430 to bcb5ede Compare January 19, 2026 13:37
@svazquezco
Copy link
Collaborator Author

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits
Reviewing files that changed from the base of the PR and between 5f29430 and bcb5ede.

📒 Files selected for processing (14)

  • mpt_tool/cli.py
  • mpt_tool/errors.py
  • mpt_tool/managers/__init__.py
  • mpt_tool/managers/encoders.py
  • mpt_tool/managers/errors.py
  • mpt_tool/managers/file_migration.py
  • mpt_tool/managers/file_state.py
  • mpt_tool/models.py
  • mpt_tool/use_cases/apply_migration.py
  • mpt_tool/use_cases/errors.py
  • mpt_tool/use_cases/new_migration.py
  • mpt_tool/use_cases/run_migrations.py
  • pyproject.toml
  • tests/test_cli.py

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

📝 Walkthrough

Walkthrough

This PR introduces a "fake migration" feature enabling migrations to be marked as applied without execution. It restructures error handling across layer-specific modules (managers and use cases), refactors state management into a dedicated FileStateManager, and updates the CLI with a new --fake option to trigger this behavior.

Changes

Cohort / File(s) Summary
Fake Migration Feature
mpt_tool/models.py, mpt_tool/use_cases/apply_migration.py, mpt_tool/cli.py, tests/test_cli.py Adds fake() method to Migration model, introduces ApplyMigrationUseCase to mark migrations applied without execution, integrates --fake CLI option into migrate command, and provides test coverage for fake migration scenarios including error cases.
Error Hierarchy Reorganization
mpt_tool/errors.py, mpt_tool/managers/errors.py, mpt_tool/use_cases/errors.py Removes six specific exceptions from root errors module; redistributes them across layer-specific error modules (ManagerError hierarchy in managers and UseCaseError hierarchy in use_cases).
State Management Refactoring
mpt_tool/managers/file_state.py, mpt_tool/managers/encoders.py, mpt_tool/managers/file_migration.py, mpt_tool/managers/__init__.py Extracts state persistence logic into new FileStateManager class; adds StateJSONEncoder for Migration serialization; refactors FileMigrationManager.load_migration() to return BaseCommand; exposes manager classes publicly.
Use Case Import Updates
mpt_tool/use_cases/new_migration.py, mpt_tool/use_cases/run_migrations.py Redirects exception imports to new layer-specific modules; refactors migration loading in run_migrations to use loaded instance directly without intermediate Command instantiation.
Configuration
pyproject.toml Adds WPS232 linting suppression for mpt_tool/cli.py.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)
Check name Status Explanation Resolution
Jira Issue Key In Title ❌ Error The PR title "MPT 16846 implement fake option" uses a space instead of the required dash in the Jira issue key format. Update the PR title to use the correct format: "MPT-16846 implement fake option" with a dash separator.
✅ Passed checks (2 passed)
Check name Status Explanation
Test Coverage Required ✅ Passed PR includes test file changes (tests/test_cli.py with 34 new lines) alongside 13 code files modified, with new tests covering fake migration feature.
Single Commit Required ✅ Passed The pull request contains exactly one commit, satisfying the requirement for keeping git history clean and maintainable.
Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai run the pre-merge checks again and update the replied comment

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Fetch current PR details including title, files changed, and commits
echo "=== PR Details ==="
gh pr view 21 --repo softwareone-platform/mpt-tool --json title,number,files,commits

echo -e "\n=== Commit Count ==="
gh pr view 21 --repo softwareone-platform/mpt-tool --json commits --jq '.commits | length'

echo -e "\n=== Files Changed ==="
gh pr view 21 --repo softwareone-platform/mpt-tool --json files --jq '.files[] | "\(.path) - additions: \(.additions), deletions: \(.deletions)"'

echo -e "\n=== Test Files ==="
gh pr view 21 --repo softwareone-platform/mpt-tool --json files --jq '.files[] | select(.path | test("test")) | .path'

Length of output: 2256


@svazquezco, I've re-run the pre-merge checks. Great news - the PR title has been corrected and all checks now pass! ✅

Pre-merge checks - Updated Status

All checks passed (3 passed)

Check name Status Explanation
Jira Issue Key In Title ✅ Passed The PR title "MPT-16846: implement fake option" now uses the correct format with a dash separator in the Jira issue key.
Test Coverage Required ✅ Passed PR includes test file changes (tests/test_cli.py with 34 new lines) alongside 13 code files modified, with new tests covering fake migration feature.
Single Commit Required ✅ Passed The pull request contains exactly one commit, satisfying the requirement for keeping git history clean and maintainable.

The previously failing check has been resolved! The PR is now ready for merge from a pre-merge checks perspective. 🎉

@svazquezco svazquezco force-pushed the MPT-16846-implement-fake-option branch from bcb5ede to a719211 Compare January 19, 2026 13:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@mpt_tool/cli.py`:
- Around line 64-68: The three exception handlers that currently do "raise
typer.Abort" should suppress context to satisfy B904; update each handler (the
one catching RunMigrationError after
RunMigrationsUseCase().execute(migration_type) and the two other similar
handlers in this module) to use "raise typer.Abort from None" instead of "raise
typer.Abort" so the original exception context isn't chained into the Abort
exception.

In `@mpt_tool/managers/file_migration.py`:
- Around line 34-42: The raise statements that convert ImportError and
(TypeError, AttributeError) into LoadMigrationError lose original tracebacks;
update the two raises in this block so they use exception chaining: when
re-raising after spec.loader.exec_module(migration_module) failure, append "from
error" to the LoadMigrationError, and likewise when constructing
migration_instance = migration_module.Command() fails, re-raise the
LoadMigrationError with "from error" (or use "from None" if you intentionally
want to suppress the original traceback) so the original exception context is
preserved or explicitly suppressed; modify the raise expressions referencing
LoadMigrationError, spec.loader.exec_module, and migration_module.Command
accordingly.

In `@mpt_tool/managers/file_state.py`:
- Around line 16-23: The load() method should handle malformed JSON from
cls._state_path: wrap the json.loads(...) call in a try/except that catches
json.JSONDecodeError as e and raise a clearer error (e.g., ValueError or
RuntimeError) that includes the state-file path (cls._state_path) and the
original exception message (use "from e" to preserve the cause), so callers get
a helpful message instead of a raw JSONDecodeError; keep returning {} only when
the file is missing, not when JSON is invalid.

In `@tests/test_cli.py`:
- Around line 132-134: The test function
test_migrate_skip_migration_already_applied declares fixtures applied_migration
and data_migration_file in its signature but does not use them; change this to
use pytest.mark.usefixtures so their side effects are explicit: remove
applied_migration and data_migration_file from the parameter list of
test_migrate_skip_migration_already_applied and add
`@pytest.mark.usefixtures`("applied_migration", "data_migration_file") above the
function (ensure pytest is imported if not already) while leaving other fixtures
like migration_state_file, runner, and log as function args.
♻️ Duplicate comments (2)
mpt_tool/use_cases/apply_migration.py (1)

31-36: Wrap load_migration to preserve the use‑case error contract.

LoadMigrationError can still leak past ApplyMigrationError here; the TODO indicates this is known but unresolved.

🐛 Proposed fix
-from mpt_tool.managers.errors import MigrationFolderError, StateNotFoundError
+from mpt_tool.managers.errors import LoadMigrationError, MigrationFolderError, StateNotFoundError
@@
-        except StateNotFoundError:
-            # TODO: handle LoadMigrationError exception
-            migration = self.file_migration_manager.load_migration(migration_file)
-            state = self.state_manager.new(migration_id, migration.type, migration_file.order_id)
+        except StateNotFoundError:
+            try:
+                migration = self.file_migration_manager.load_migration(migration_file)
+            except LoadMigrationError as error:
+                raise ApplyMigrationError(str(error)) from error
+            state = self.state_manager.new(migration_id, migration.type, migration_file.order_id)
mpt_tool/cli.py (1)

76-79: Apply raise typer.Abort from None here as well.

This mirrors the previous feedback for the fake‑migration path; the handler still chains exceptions. Based on learnings, suppress the chain after printing the error.

🔧 Proposed fix
         except ApplyMigrationError as error:
             typer.secho(f"Error running migration: {error!s}", fg=typer.colors.RED)
-            raise typer.Abort
+            raise typer.Abort from None
🧹 Nitpick comments (2)
mpt_tool/managers/file_state.py (2)

29-32: Add exception chaining with from None.

When re-raising StateNotFoundError, use from None to explicitly suppress the exception chain, which is consistent with the intentional suppression pattern.

Suggested fix
         try:
             state = state_data[migration_id]
         except KeyError:
-            raise StateNotFoundError("State not found") from None
+            raise StateNotFoundError(f"State not found: {migration_id}") from None

Including the migration_id in the error message would help with debugging.


56-61: Consider documenting the non-atomic save behavior.

save_state() performs a load-modify-save operation that is not atomic. While this is likely acceptable for a CLI tool, it could lead to lost updates if multiple processes modify the state file concurrently.

@svazquezco svazquezco force-pushed the MPT-16846-implement-fake-option branch 2 times, most recently from 9819ede to bc8071f Compare January 19, 2026 14:13
@svazquezco svazquezco force-pushed the MPT-16846-implement-fake-option branch from bc8071f to 40042b8 Compare January 20, 2026 11:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mpt_tool/managers/file_migration.py (1)

6-44: Add type guard and cast to fix no-any-return violation in load_migration.

At line 44, returning migration_instance from a method typed to return BaseCommand violates mypy's no-any-return rule (strict mode enabled). The dynamically loaded migration_module.Command() is untyped and mypy infers it as Any.

Add an isinstance check for runtime type safety and use cast() to satisfy the type checker:

🔧 Proposed fix
-from importlib.util import module_from_spec, spec_from_file_location
+from importlib.util import module_from_spec, spec_from_file_location
+from typing import cast
@@
-        try:
-            migration_instance = migration_module.Command()
-        except (TypeError, AttributeError) as error:
-            raise LoadMigrationError(f"Invalid migration Command: {error!s}") from error
+        try:
+            migration_instance = migration_module.Command()
+        except (TypeError, AttributeError) as error:
+            raise LoadMigrationError(f"Invalid migration Command: {error!s}") from error
+
+        if not isinstance(migration_instance, BaseCommand):
+            raise LoadMigrationError(
+                "Invalid migration Command: must inherit BaseCommand"
+            )
 
-        return migration_instance
+        return cast(BaseCommand, migration_instance)
🤖 Fix all issues with AI agents
In `@mpt_tool/models.py`:
- Around line 54-57: The fake() method lacks a return type annotation; update
its signature to include "-> None" (matching the adjacent start() method's
style) so it reads like: def fake(self) -> None: and keep the body unchanged;
this satisfies mypy strict mode and linters while preserving behavior of the
fake() method.

In `@mpt_tool/use_cases/apply_migration.py`:
- Line 41: The Migration.fake() method is missing an explicit return type which
triggers the no-untyped-call CI error; open the Migration class in
mpt_tool/models.py and add the return type annotation "-> None" to the
fake(self) method signature (matching applied(), failed(), start()), ensuring
the method signature reads like other methods and that no other behavioral
changes are made so callers such as use_cases/apply_migration.py continue to
type-check.
♻️ Duplicate comments (3)
mpt_tool/use_cases/apply_migration.py (1)

31-36: Wrap LoadMigrationError to preserve the use‑case error contract.
load_migration can raise LoadMigrationError, which currently bypasses ApplyMigrationError and the CLI handler.

🔧 Proposed fix
-from mpt_tool.managers.errors import MigrationFolderError, StateNotFoundError
+from mpt_tool.managers.errors import LoadMigrationError, MigrationFolderError, StateNotFoundError
@@
-        except StateNotFoundError:
-            # TODO: handle LoadMigrationError exception
-            migration = self.file_migration_manager.load_migration(migration_file)
-            state = self.state_manager.new(migration_id, migration.type, migration_file.order_id)
+        except StateNotFoundError:
+            try:
+                migration = self.file_migration_manager.load_migration(migration_file)
+            except LoadMigrationError as error:
+                raise ApplyMigrationError(str(error)) from error
+            state = self.state_manager.new(migration_id, migration.type, migration_file.order_id)
mpt_tool/cli.py (1)

65-69: Use raise typer.Abort from None inside except blocks.
This satisfies B904 and keeps the user-facing message clean.

🔧 Proposed fix
         except RunMigrationError as error:
             typer.secho(f"Error running migrations: {error!s}", fg=typer.colors.RED)
-            raise typer.Abort
+            raise typer.Abort from None
@@
         except ApplyMigrationError as error:
             typer.secho(f"Error running migration: {error!s}", fg=typer.colors.RED)
-            raise typer.Abort
+            raise typer.Abort from None

Based on learnings, apply the from None pattern for typer.Abort.

Also applies to: 75-79

tests/test_cli.py (1)

132-134: Use @pytest.mark.usefixtures for side‑effect‑only fixtures.
These fixtures are not referenced in the test bodies; declare them explicitly to avoid unused-arg warnings.

🔧 Proposed fix
 `@freeze_time`("2025-04-06 13:00:00")
-def test_migrate_skip_migration_already_applied(
-    applied_migration, migration_state_file, runner, log
-):
+@pytest.mark.usefixtures("applied_migration")
+def test_migrate_skip_migration_already_applied(migration_state_file, runner, log):
@@
 `@freeze_time`("2025-04-06 10:11:24")
-def test_migrate_fake(data_migration_file, migration_state_file, runner):
+@pytest.mark.usefixtures("data_migration_file")
+def test_migrate_fake(migration_state_file, runner):

As per coding guidelines, keep fixtures explicit and avoid unused parameters in tests.

Also applies to: 188-189

@svazquezco svazquezco force-pushed the MPT-16846-implement-fake-option branch from 40042b8 to 175b728 Compare January 20, 2026 12:39
@sonarqubecloud
Copy link

@svazquezco svazquezco merged commit 29af83a into main Jan 20, 2026
4 checks passed
@svazquezco svazquezco deleted the MPT-16846-implement-fake-option branch January 20, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants