Coverage for pytest_beehave/bootstrap.py: 100%
40 statements
« prev ^ index » next coverage.py v7.8.0, created at 2026-04-21 04:49 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2026-04-21 04:49 +0000
1"""Bootstrap logic for the features directory structure."""
3from __future__ import annotations
5from dataclasses import dataclass
6from pathlib import Path
8_CANONICAL_SUBFOLDERS: tuple[str, ...] = ("backlog", "in-progress", "completed")
11@dataclass(frozen=True, slots=True)
12class BootstrapResult:
13 """Result of bootstrapping the features directory.
15 Attributes:
16 created_subfolders: Names of subfolders that were created.
17 migrated_files: Paths of files that were migrated.
18 collision_warnings: Warning messages for name collisions.
19 """
21 created_subfolders: tuple[str, ...]
22 migrated_files: tuple[Path, ...]
23 collision_warnings: tuple[str, ...]
25 @property
26 def is_noop(self) -> bool:
27 """Return True if bootstrap made no changes."""
28 return (
29 len(self.created_subfolders) == 0
30 and len(self.migrated_files) == 0
31 and len(self.collision_warnings) == 0
32 )
35def _ensure_canonical_subfolders(features_root: Path) -> tuple[str, ...]:
36 """Create missing canonical subfolders under features_root.
38 Args:
39 features_root: The features root directory.
41 Returns:
42 Tuple of subfolder names that were created.
43 """
44 created: list[str] = []
45 for name in _CANONICAL_SUBFOLDERS:
46 subfolder = features_root / name
47 if not subfolder.exists():
48 subfolder.mkdir(parents=True, exist_ok=True)
49 created.append(name)
50 return tuple(created)
53def _migrate_loose_feature_files(
54 features_root: Path,
55) -> tuple[tuple[Path, ...], tuple[str, ...]]:
56 """Move any loose .feature files in features_root into backlog/.
58 Files that collide with an existing path in backlog/ are skipped
59 with a warning.
61 Args:
62 features_root: The features root directory.
64 Returns:
65 Tuple of (migrated_paths, warning_strings).
66 """
67 migrated: list[Path] = []
68 warnings: list[str] = []
69 backlog_dir = features_root / "backlog"
70 for item in sorted(features_root.iterdir()):
71 if item.is_dir() or item.suffix != ".feature":
72 continue
73 target = backlog_dir / item.name
74 if target.exists():
75 warnings.append(f"Cannot migrate {item}: {target} already exists")
76 continue
77 item.rename(target)
78 migrated.append(target)
79 return tuple(migrated), tuple(warnings)
82def bootstrap_features_directory(features_root: Path) -> BootstrapResult:
83 """Ensure the features directory has the canonical subfolder structure.
85 Creates backlog/, in-progress/, and completed/ if missing. Migrates
86 any loose .feature files at the root level into backlog/.
88 Args:
89 features_root: Root of the features directory.
91 Returns:
92 BootstrapResult describing what was done.
93 """
94 if not features_root.exists():
95 return BootstrapResult(
96 created_subfolders=(),
97 migrated_files=(),
98 collision_warnings=(),
99 )
100 created = _ensure_canonical_subfolders(features_root)
101 migrated, warnings = _migrate_loose_feature_files(features_root)
102 return BootstrapResult(
103 created_subfolders=created,
104 migrated_files=migrated,
105 collision_warnings=warnings,
106 )