diff --git a/examples/01-individual-rules/string-concat/README.md b/examples/01-individual-rules/string-concat/README.md index cd64737..990ba4e 100644 --- a/examples/01-individual-rules/string-concat/README.md +++ b/examples/01-individual-rules/string-concat/README.md @@ -31,8 +31,7 @@ def format_data(data): **After:** ```python def greet(name, age): - message = f"Hello {name}, you are {age} years old" - return message + return f"Hello {name}, you are {age} years old" def format_data(data): result = f"Data: {data}" diff --git a/examples/01-individual-rules/string-concat/after.py b/examples/01-individual-rules/string-concat/after.py index 39f35b1..d808f2d 100644 --- a/examples/01-individual-rules/string-concat/after.py +++ b/examples/01-individual-rules/string-concat/after.py @@ -3,8 +3,7 @@ def greet(name, age): """Greet someone.""" - message = f"Hello {name}, you are {age} years old" - return message + return f"Hello {name}, you are {age} years old" def format_data(data): diff --git a/examples/06-api-usage/example.py b/examples/06-api-usage/example.py index 9a5e110..938a5f5 100644 --- a/examples/06-api-usage/example.py +++ b/examples/06-api-usage/example.py @@ -6,6 +6,13 @@ from prefact.config import Config from prefact.engine import RefactoringEngine +from prefact.models import PipelineResult + + +def run_engine(config: Config) -> PipelineResult: + """Create the engine for a config and run it.""" + engine = RefactoringEngine(config) + return engine.run() def run_prefact_example( @@ -33,35 +40,38 @@ def run_prefact_example( print(f"Dry run: {dry_run}") print("-" * 50) - engine = RefactoringEngine(config) - result = engine.run() + result = run_engine(config) # Display results print("\nšŸ“Š Results:") - print(f" Files scanned: {result.files_scanned}") print(f" Total issues: {result.total_issues}") print(f" Issues fixed: {result.total_fixed}") print(f" Validation passed: {result.all_valid}") # Show issues by rule - if result.issues_by_rule: + issues_by_rule = {} + for issue in result.issues_found: + issues_by_rule.setdefault(issue.rule_id, []).append(issue) + + if issues_by_rule: print("\nšŸ“‹ Issues by rule:") - for rule_id, issues in result.issues_by_rule.items(): + for rule_id, issues in issues_by_rule.items(): print(f" {rule_id}: {len(issues)} issues") # Show fix details - if result.fixes: + if result.fixes_applied: print("\nšŸ”§ Fixes applied:") - for fix in result.fixes[:5]: # Show first 5 - print(f" {fix.path}:{fix.line} - {fix.description}") - if len(result.fixes) > 5: - print(f" ... and {len(result.fixes) - 5} more") + for fix in result.fixes_applied[:5]: # Show first 5 + print(f" {fix.file}:{fix.issue.line} - {fix.issue.message}") + if len(result.fixes_applied) > 5: + print(f" ... and {len(result.fixes_applied) - 5} more") # Show validation failures - if result.validation_failures: + failed_validations = [v for v in result.validations if not v.passed] + if failed_validations: print("\nāŒ Validation failures:") - for failure in result.validation_failures: - print(f" {failure.path}: {failure.message}") + for failure in failed_validations: + print(f" {failure.file}: {', '.join(failure.errors)}") return result @@ -104,12 +114,11 @@ def another_function(): return # Run with custom rules - engine = RefactoringEngine(config) - result = engine.run() + result = run_engine(config) print("\nCustom rule results:") print( - f" TODO comments found: {len([i for i in result.all_issues if 'todo' in i.rule_id])}" + f" TODO comments found: {len([i for i in result.issues_found if 'todo' in i.rule_id])}" ) # Cleanup @@ -140,14 +149,13 @@ def batch_processing_example(): config.project_root = project.resolve() config.dry_run = True # Don't actually fix - engine = RefactoringEngine(config) - result = engine.run() + result = run_engine(config) results.append( { "project": project.name, "issues": result.total_issues, - "fixable": len([i for i in result.all_issues if i.fixable]), + "fixable": len([i for i in result.issues_found if i.suggested]), } ) else: diff --git a/examples/generate_examples.py b/examples/generate_examples.py index 2847977..332c701 100644 --- a/examples/generate_examples.py +++ b/examples/generate_examples.py @@ -151,8 +151,7 @@ def format_data(data): def greet(name, age): """Greet someone.""" - message = f"Hello {name}, you are {age} years old" - return message + return f"Hello {name}, you are {age} years old" def format_data(data): """Format data.""" diff --git a/examples/sample-project/cli.py b/examples/sample-project/cli.py index a775e26..85a7502 100644 --- a/examples/sample-project/cli.py +++ b/examples/sample-project/cli.py @@ -22,14 +22,14 @@ def main(name, email): sys.exit(1) # Create user - user = User(id="", name=name, email=email, created_at=datetime.now()) + new_user = User(id="", name=name, email=email, created_at=datetime.now()) - print("Created user: " + user.name) + print("Created user: " + new_user.name) # Process some data processor = DataProcessor() - processor.add_item(user.name) - processor.add_item(user.email) + processor.add_item(new_user.name) + processor.add_item(new_user.email) result = process_data("test data") print("Processing result: " + str(result)) @@ -54,8 +54,8 @@ def users(): User("2", "Bob", "bob@example.com", datetime.now()), ] - for user in users: - print("User: " + user.name + " (" + user.email + ")") + for entry in users: + print("User: " + entry.name + " (" + entry.email + ")") if __name__ == "__main__": diff --git a/examples/sample-project/models.py b/examples/sample-project/models.py index 41ed97f..cecaabc 100644 --- a/examples/sample-project/models.py +++ b/examples/sample-project/models.py @@ -44,8 +44,8 @@ def get_summary(self): def create_user(name, email): """Create a new user.""" - user = User(id="", name=name, email=email, created_at=datetime.now()) - return user + new_user = User(id="", name=name, email=email, created_at=datetime.now()) + return new_user def load_users_from_file(filepath): @@ -54,6 +54,6 @@ def load_users_from_file(filepath): data = json.load(f) users = [] for item in data: - user = User(**item) - users.append(user) + loaded = User(**item) + users.append(loaded) return users diff --git a/regix.yaml b/regix.yaml index 77e28d7..0245158 100644 --- a/regix.yaml +++ b/regix.yaml @@ -39,6 +39,9 @@ regix: coverage: pytest-cov quality: none docstring: builtin + # mi_granularity: module # or "function" — score each function's own + # # span so helper extraction (lower CC, more LOC) + # # stops dropping module MI below the gate # ── File filtering ───────────────────────────────────────── exclude: diff --git a/src/prefact/cli.py b/src/prefact/cli.py index b65b3c2..60395bc 100644 --- a/src/prefact/cli.py +++ b/src/prefact/cli.py @@ -4,6 +4,7 @@ import click +from prefact._base import console from prefact.autonomous import AutonomousRefact from prefact.config import Config from prefact.config_extended import ExtendedConfig @@ -337,10 +338,6 @@ def autonomous_cmd( Automatically initializes prefact.yaml if missing, runs examples, scans for issues, and creates tickets in planfile.yaml. """ - from rich.console import Console - - console = Console() - # Initialize autonomous prefact auto = AutonomousRefact( Path(project_path), exclude_patterns=list(exclude) if exclude else None @@ -439,12 +436,10 @@ def testql_cmd( @main.command() def rules() -> None: """List all available rules.""" - from rich.console import Console from rich.table import Table from prefact.rules import get_all_rules - console = Console() table = Table(title="Available Rules") table.add_column("Rule ID", style="bold") table.add_column("Description") diff --git a/src/prefact/config_extended/config.py b/src/prefact/config_extended/config.py new file mode 100644 index 0000000..0449de8 --- /dev/null +++ b/src/prefact/config_extended/config.py @@ -0,0 +1,142 @@ +"""Extended configuration model and helpers.""" + +from pathlib import Path +from typing import Any, Dict, List, Optional + +import yaml + +from prefact.config import Config, RuleConfig + +from .constants import DEFAULT_EXCLUDE, DEFAULT_INCLUDE + + +class ExtendedConfig(Config): + """Extended configuration with additional features.""" + + def __init__( + self, + project_root: Optional[Path] = None, + package_name: str = "", + include: Optional[List[str]] = None, + exclude: Optional[List[str]] = None, + rules: Optional[Dict[str, Any]] = None, + tools: Optional[Dict[str, Any]] = None, + performance: Optional[Dict[str, Any]] = None, + plugins: Optional[Dict[str, Any]] = None, + environments: Optional[Dict[str, Any]] = None, + **kwargs, + ): + # Call parent dataclass __init__ with proper values + super().__init__( + project_root=project_root or Path.cwd(), + package_name=package_name, + include=include or ["**/*.py"], + exclude=exclude or [], + rules=rules or {}, + ) + self.tools: Dict[str, Any] = tools or {} + self.performance: Dict[str, Any] = performance or {} + self.plugins: Dict[str, Any] = plugins or {} + self.environments: Dict[str, Any] = environments or {} + for key, value in kwargs.items(): + setattr(self, key, value) + + @classmethod + def from_yaml( + cls, path: Path, environment: Optional[str] = None + ) -> "ExtendedConfig": + """Load configuration from YAML file with environment support.""" + if not path.exists(): + return cls(project_root=Path.cwd()) + + with open(path) as f: + raw = yaml.safe_load(f) or {} + + if environment and "environments" in raw: + env_config = raw["environments"].get(environment, {}) + raw = cls._deep_merge(raw, env_config) + + rules = cls._parse_rules(raw.pop("rules", {})) + tools = raw.pop("tools", {}) + performance = raw.pop("performance", {}) + plugins = raw.pop("plugins", {}) + environments = raw.pop("environments", {}) + include = raw.pop("include", DEFAULT_INCLUDE) + exclude = raw.pop("exclude", DEFAULT_EXCLUDE) + + instance = cls( + project_root=Path(raw.pop("project_root", Path.cwd())), + package_name=raw.pop("package_name", ""), + include=include, + exclude=exclude, + rules=rules, + tools=tools, + performance=performance, + plugins=plugins, + environments=environments, + **{k: v for k, v in raw.items() if k in cls.__dataclass_fields__}, + ) + # Force set include/exclude to ensure they're not None + instance.include = include + instance.exclude = exclude + return instance + + @staticmethod + def _parse_rules(rules_raw: Dict[str, Any]) -> Dict[str, RuleConfig]: + """Parse rules from raw configuration.""" + rules = {} + for rule_id, rule_raw in rules_raw.items(): + if isinstance(rule_raw, bool): + rules[rule_id] = RuleConfig(enabled=rule_raw) + elif isinstance(rule_raw, dict): + basic_fields = { + k: v + for k, v in rule_raw.items() + if k in {"enabled", "severity", "options"} + } + rules[rule_id] = RuleConfig(**basic_fields) + if hasattr(rules[rule_id], "_extended"): + rules[rule_id]._extended.update(rule_raw) + else: + rules[rule_id]._extended = { + k: v + for k, v in rule_raw.items() + if k not in {"enabled", "severity", "options"} + } + return rules + + @staticmethod + def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: + """Deep merge two dictionaries.""" + result = base.copy() + for key, value in override.items(): + if ( + key in result + and isinstance(result[key], dict) + and isinstance(value, dict) + ): + result[key] = ExtendedConfig._deep_merge(result[key], value) + else: + result[key] = value + return result + + def get_tool_config(self, tool_name: str) -> Dict[str, Any]: + return self.tools.get(tool_name, {}) + + def get_performance_setting(self, key: str, default: Any = None) -> Any: + return self.performance.get(key, default) + + def get_plugin_config(self, plugin_name: str) -> Dict[str, Any]: + return self.plugins.get(plugin_name, {}) + + def to_dict(self) -> Dict[str, Any]: + result = super().to_dict() + result.update( + { + "tools": self.tools, + "performance": self.performance, + "plugins": self.plugins, + "environments": self.environments, + } + ) + return result diff --git a/src/prefact/config_extended/models.py b/src/prefact/config_extended/models.py index 64dec31..d48e415 100644 --- a/src/prefact/config_extended/models.py +++ b/src/prefact/config_extended/models.py @@ -1,83 +1,9 @@ -from pathlib import Path -from typing import Any, Dict, Optional +"""Backward-compatible alias for the extended configuration model. -import yaml +The canonical :class:`ExtendedConfig` lives in :mod:`prefact.config_extended.config`; +it is re-exported here so existing ``config_extended.models`` imports keep working. +""" -from prefact.config import Config, RuleConfig +from .config import ExtendedConfig -from .utils import deep_merge - - -class ExtendedConfig(Config): - def __init__( - self, - project_root=None, - package_name="", - include=None, - exclude=None, - rules=None, - tools=None, - performance=None, - plugins=None, - environments=None, - **kwargs, - ): - super().__init__(project_root, package_name, include, exclude, rules) - self.tools = tools or {} - self.performance = performance or {} - self.plugins = plugins or {} - self.environments = environments or {} - for key, value in kwargs.items(): - setattr(self, key, value) - - @classmethod - def from_yaml( - cls, path: Path, environment: Optional[str] = None - ) -> "ExtendedConfig": - if not path.exists(): - return cls(project_root=Path.cwd()) - with open(path) as f: - raw = yaml.safe_load(f) or {} - if environment and "environments" in raw: - raw = deep_merge(raw, raw["environments"].get(environment, {})) - rules = {} - for rule_id, rule_raw in raw.pop("rules", {}).items(): - if isinstance(rule_raw, bool): - rules[rule_id] = RuleConfig(enabled=rule_raw) - elif isinstance(rule_raw, dict): - basic = { - k: v - for k, v in rule_raw.items() - if k in ["enabled", "severity", "options"] - } - rules[rule_id] = RuleConfig(**basic) - if not hasattr(rules[rule_id], "_extended"): - rules[rule_id]._extended = { - k: v - for k, v in rule_raw.items() - if k not in ["enabled", "severity", "options"] - } - return cls( - project_root=Path(raw.pop("project_root", Path.cwd())), - package_name=raw.pop("package_name", ""), - include=raw.pop("include", None), - exclude=raw.pop("exclude", None), - rules=rules, - tools=raw.pop("tools", {}), - performance=raw.pop("performance", {}), - plugins=raw.pop("plugins", {}), - environments=raw.pop("environments", {}), - **raw, - ) - - def to_dict(self) -> Dict[str, Any]: - result = super().to_dict() - result.update( - { - "tools": self.tools, - "performance": self.performance, - "plugins": self.plugins, - "environments": self.environments, - } - ) - return result +__all__ = ["ExtendedConfig"] diff --git a/src/prefact/cqrs/__init__.py b/src/prefact/cqrs/__init__.py index 150a7ec..0c310ee 100644 --- a/src/prefact/cqrs/__init__.py +++ b/src/prefact/cqrs/__init__.py @@ -15,7 +15,7 @@ replayed (event sourcing). """ -from prefact.cqrs.bus import EventBus +from prefact.cqrs.bus import EventBus, Subscription from prefact.cqrs.commands.refactoring import FixFile, RefactoringCommandHandler from prefact.cqrs.events import ( FixApplied, @@ -38,6 +38,7 @@ __all__ = [ "EventBus", + "Subscription", "EventStore", "InMemoryEventStore", "JsonlEventStore", diff --git a/src/prefact/cqrs/bus.py b/src/prefact/cqrs/bus.py index e7d1537..30c2bad 100644 --- a/src/prefact/cqrs/bus.py +++ b/src/prefact/cqrs/bus.py @@ -11,6 +11,7 @@ from collections import defaultdict from collections.abc import Callable +from typing import NamedTuple from prefact.cqrs.events.base import DomainEvent from prefact.cqrs.store import EventStore @@ -18,6 +19,13 @@ EventHandler = Callable[[DomainEvent], None] +class Subscription(NamedTuple): + """Identity of a handler registered for one event name.""" + + event_name: str + handler: EventHandler + + class EventBus: """Synchronous pub/sub dispatcher with optional event-store persistence.""" @@ -25,15 +33,15 @@ def __init__(self, store: EventStore | None = None) -> None: self._handlers: dict[str, list[EventHandler]] = defaultdict(list) self.store = store - def subscribe(self, event_name: str, handler: EventHandler) -> None: - """Register *handler* to be called for events named *event_name*.""" - self._handlers[event_name].append(handler) + def subscribe(self, subscription: Subscription) -> None: + """Register the subscription's handler for its event name.""" + self._handlers[subscription.event_name].append(subscription.handler) - def unsubscribe(self, event_name: str, handler: EventHandler) -> None: - """Remove a previously registered handler.""" - handlers = self._handlers.get(event_name) - if handlers and handler in handlers: - handlers.remove(handler) + def unsubscribe(self, subscription: Subscription) -> None: + """Remove a previously registered subscription.""" + handlers = self._handlers.get(subscription.event_name) + if handlers and subscription.handler in handlers: + handlers.remove(subscription.handler) def publish(self, event: DomainEvent) -> None: """Persist (if a store is attached) and dispatch *event*.""" diff --git a/src/prefact/git_hooks.py b/src/prefact/git_hooks.py index 78a2859..914b2cf 100644 --- a/src/prefact/git_hooks.py +++ b/src/prefact/git_hooks.py @@ -185,33 +185,28 @@ def uninstall_hooks(self, hook_types: Optional[List[str]] = None) -> None: hook_path = self.hooks_dir / hook_type backup_path = hook_path.with_suffix(".prefact.bak") - if hook_path.exists(): - # Check if it's a prefact hook - content = hook_path.read_text() - if "prefact" in content: - hook_path.unlink() - - # Restore backup if it exists - if backup_path.exists(): - backup_path.rename(hook_path) - print(f"Restored original {hook_type} hook") - else: - print(f"Removed {hook_type} hook") + if self._is_prefact_hook(hook_path): + hook_path.unlink() + + # Restore backup if it exists + if backup_path.exists(): + backup_path.rename(hook_path) + print(f"Restored original {hook_type} hook") + else: + print(f"Removed {hook_type} hook") + + @staticmethod + def _is_prefact_hook(hook_path: Path) -> bool: + """Check whether a hook file was installed by prefact.""" + return hook_path.exists() and "prefact" in hook_path.read_text() def list_hooks(self) -> Dict[str, bool]: """List status of all hooks.""" hook_types = ["pre-commit", "pre-push", "commit-msg"] - status = {} - - for hook_type in hook_types: - hook_path = self.hooks_dir / hook_type - if hook_path.exists(): - content = hook_path.read_text() - status[hook_type] = "prefact" in content - else: - status[hook_type] = False - - return status + return { + hook_type: self._is_prefact_hook(self.hooks_dir / hook_type) + for hook_type in hook_types + } def test_hook(self, hook_type: str) -> bool: """Test if a hook is working correctly.""" @@ -349,12 +344,11 @@ def list_git_hooks(repo_root: Optional[Path] = None) -> None: repo_root = Path.cwd() hooks = GitHooks(repo_root) - status = hooks.list_hooks() print("Git hooks status:") - for hook_type, installed in status.items(): - status_str = "āœ“ Installed (prefact)" if installed else "āœ— Not installed" - print(f" {hook_type}: {status_str}") + for hook_type, installed in hooks.list_hooks().items(): + label = "āœ“ Installed (prefact)" if installed else "āœ— Not installed" + print(f" {hook_type}: {label}") # CLI commands @@ -379,5 +373,5 @@ def main() -> None: hooks = GitHooks(args.path) for hook_type in args.hooks or ["pre-commit", "pre-push", "commit-msg"]: result = hooks.test_hook(hook_type) - status = "āœ“ Working" if result else "āœ— Not working" - print(f"{hook_type}: {status}") + label = "āœ“ Working" if result else "āœ— Not working" + print(f"{hook_type}: {label}") diff --git a/src/prefact/performance/__init__.py b/src/prefact/performance/__init__.py index 7bd3089..cd1ea71 100644 --- a/src/prefact/performance/__init__.py +++ b/src/prefact/performance/__init__.py @@ -16,6 +16,7 @@ initialize_cache, ) from prefact.performance.parallel import ( + FileBatch, ParallelEngine, ParallelScanner, ParallelScanTask, @@ -38,6 +39,7 @@ "ParallelEngine", "ParallelScanner", "ParallelScanTask", + "FileBatch", "ScanResultCache", "ScanResultKey", "RuleResultKey", diff --git a/src/prefact/performance/parallel.py b/src/prefact/performance/parallel.py index 737ff77..58dfcab 100644 --- a/src/prefact/performance/parallel.py +++ b/src/prefact/performance/parallel.py @@ -11,7 +11,7 @@ import time from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, NamedTuple, Optional, Tuple from prefact.config import Config from prefact.engine import RefactoringEngine @@ -21,6 +21,13 @@ CONSTANT_3600 = 3600 +class FileBatch(NamedTuple): + """A batch of files to process with the selected rules.""" + + file_paths: Tuple[Path, ...] + rule_ids: Tuple[str, ...] + + class ParallelScanTask: """A task for parallel scanning.""" @@ -89,16 +96,14 @@ def __init__(self, config: Config): self.chunk_size = config.get_rule_option("_performance", "chunk_size", 10) self.cache_enabled = config.get_rule_option("_performance", "cache", True) - def scan_files( - self, file_paths: List[Path], rule_ids: Optional[List[str]] = None - ) -> List[Dict[str, Any]]: + def scan_files(self, batch: FileBatch) -> List[Dict[str, Any]]: """Scan multiple files in parallel.""" + file_paths = list(batch.file_paths) if not file_paths: return [] # Use all enabled rules if none specified - if rule_ids is None: - rule_ids = self._get_enabled_rule_ids() + rule_ids = list(batch.rule_ids) or self._get_enabled_rule_ids() # Create tasks tasks = [ @@ -204,19 +209,17 @@ def _get_enabled_rule_ids(self) -> List[str]: return enabled_rules - def fix_files( - self, file_paths: List[Path], rule_ids: Optional[List[str]] = None - ) -> List[Dict[str, Any]]: + def fix_files(self, batch: FileBatch) -> List[Dict[str, Any]]: """Fix multiple files in parallel.""" # For fixing, we need to be more careful about file conflicts # So we'll process sequentially but in parallel for scanning results = [] - for file_path in file_paths: + for file_path in batch.file_paths: try: config = Config.from_dict(self.config.to_dict()) # type: ignore[attr-defined] engine = RefactoringEngine(config) - result = engine.run_file(file_path, rule_ids) # type: ignore[misc] + result = engine.run_file(file_path, list(batch.rule_ids)) # type: ignore[misc] results.append(result) except Exception as e: error_result = { @@ -260,7 +263,9 @@ def scan_directory( file_paths.append(file_path) # Scan in parallel - return self.engine.scan_files(file_paths, rule_ids) + return self.engine.scan_files( + FileBatch(tuple(file_paths), tuple(rule_ids or ())) + ) def scan_workspace( self, rule_ids: Optional[List[str]] = None diff --git a/tests/test_cqrs.py b/tests/test_cqrs.py index f54daa9..e4920b4 100644 --- a/tests/test_cqrs.py +++ b/tests/test_cqrs.py @@ -16,6 +16,7 @@ RefactoringCommandHandler, ScanCompleted, ScanStarted, + Subscription, ValidateFile, ) from prefact.cqrs.events import from_dict @@ -28,7 +29,9 @@ def test_bus_dispatches_to_subscribers() -> None: bus = EventBus() seen: list[ScanStarted] = [] - bus.subscribe("analysis.scan.started", lambda event: seen.append(event)) + bus.subscribe( + Subscription("analysis.scan.started", lambda event: seen.append(event)) + ) bus.publish(ScanStarted(file_count=2)) assert len(seen) == 1 assert seen[0].file_count == 2 @@ -46,8 +49,8 @@ def test_bus_unsubscribe() -> None: bus = EventBus() seen: list[ScanStarted] = [] handler = lambda event: seen.append(event) # noqa: E731 - bus.subscribe("analysis.scan.started", handler) - bus.unsubscribe("analysis.scan.started", handler) + bus.subscribe(Subscription("analysis.scan.started", handler)) + bus.unsubscribe(Subscription("analysis.scan.started", handler)) bus.publish(ScanStarted(file_count=1)) assert seen == [] @@ -117,7 +120,9 @@ def test_command_handler_emits_fix_applied() -> None: bus = EventBus() handler = RefactoringCommandHandler(_FakeFixer(), bus) seen: list[FixApplied] = [] - bus.subscribe("refactoring.fix.applied", lambda event: seen.append(event)) + bus.subscribe( + Subscription("refactoring.fix.applied", lambda event: seen.append(event)) + ) issue = _issue() _source, fixes = handler.handle( @@ -132,7 +137,9 @@ def test_command_handler_emits_fix_failed() -> None: bus = EventBus() handler = RefactoringCommandHandler(_FakeFixer(), bus) seen: list[FixFailed] = [] - bus.subscribe("refactoring.fix.failed", lambda event: seen.append(event)) + bus.subscribe( + Subscription("refactoring.fix.failed", lambda event: seen.append(event)) + ) issue = _issue(rule_id="failing") handler.handle(FixFile(path=Path("f.py"), source="x", issues=[issue])) @@ -172,9 +179,15 @@ def test_query_handler_emits_scan_events() -> None: started: list[ScanStarted] = [] detected: list[IssueDetected] = [] completed: list[ScanCompleted] = [] - bus.subscribe("analysis.scan.started", lambda event: started.append(event)) - bus.subscribe("analysis.issue.detected", lambda event: detected.append(event)) - bus.subscribe("analysis.scan.completed", lambda event: completed.append(event)) + bus.subscribe( + Subscription("analysis.scan.started", lambda event: started.append(event)) + ) + bus.subscribe( + Subscription("analysis.issue.detected", lambda event: detected.append(event)) + ) + bus.subscribe( + Subscription("analysis.scan.completed", lambda event: completed.append(event)) + ) from prefact.cqrs.queries.analysis import ScanSources @@ -190,12 +203,12 @@ def test_query_handler_emits_validation_event() -> None: bus = EventBus() handler = AnalysisQueryHandler(_FakeScanner(), _FakeValidator(), bus) seen: list[object] = [] - bus.subscribe("analysis.validation.completed", lambda event: seen.append(event)) + bus.subscribe( + Subscription("analysis.validation.completed", lambda event: seen.append(event)) + ) results = handler.handle( - ValidateFile( - path=Path("f.py"), original="a", fixed="b", issues=[_issue()] - ) + ValidateFile(path=Path("f.py"), original="a", fixed="b", issues=[_issue()]) ) assert results[0].passed is True assert len(seen) == 1