pytest_beehave.bootstrap

Bootstrap logic for the features directory structure.

  1"""Bootstrap logic for the features directory structure."""
  2
  3from __future__ import annotations
  4
  5from dataclasses import dataclass
  6from pathlib import Path
  7
  8_CANONICAL_SUBFOLDERS: tuple[str, ...] = ("backlog", "in-progress", "completed")
  9
 10
 11@dataclass(frozen=True, slots=True)
 12class BootstrapResult:
 13    """Result of bootstrapping the features directory.
 14
 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    """
 20
 21    created_subfolders: tuple[str, ...]
 22    migrated_files: tuple[Path, ...]
 23    collision_warnings: tuple[str, ...]
 24
 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        )
 33
 34
 35def _ensure_canonical_subfolders(features_root: Path) -> tuple[str, ...]:
 36    """Create missing canonical subfolders under features_root.
 37
 38    Args:
 39        features_root: The features root directory.
 40
 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)
 51
 52
 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/.
 57
 58    Files that collide with an existing path in backlog/ are skipped
 59    with a warning.
 60
 61    Args:
 62        features_root: The features root directory.
 63
 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)
 80
 81
 82def bootstrap_features_directory(features_root: Path) -> BootstrapResult:
 83    """Ensure the features directory has the canonical subfolder structure.
 84
 85    Creates backlog/, in-progress/, and completed/ if missing. Migrates
 86    any loose .feature files at the root level into backlog/.
 87
 88    Args:
 89        features_root: Root of the features directory.
 90
 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    )
@dataclass(frozen=True, slots=True)
class BootstrapResult:
12@dataclass(frozen=True, slots=True)
13class BootstrapResult:
14    """Result of bootstrapping the features directory.
15
16    Attributes:
17        created_subfolders: Names of subfolders that were created.
18        migrated_files: Paths of files that were migrated.
19        collision_warnings: Warning messages for name collisions.
20    """
21
22    created_subfolders: tuple[str, ...]
23    migrated_files: tuple[Path, ...]
24    collision_warnings: tuple[str, ...]
25
26    @property
27    def is_noop(self) -> bool:
28        """Return True if bootstrap made no changes."""
29        return (
30            len(self.created_subfolders) == 0
31            and len(self.migrated_files) == 0
32            and len(self.collision_warnings) == 0
33        )

Result of bootstrapping the features directory.

Attributes: created_subfolders: Names of subfolders that were created. migrated_files: Paths of files that were migrated. collision_warnings: Warning messages for name collisions.

BootstrapResult( created_subfolders: tuple[str, ...], migrated_files: tuple[pathlib._local.Path, ...], collision_warnings: tuple[str, ...])
created_subfolders: tuple[str, ...]
migrated_files: tuple[pathlib._local.Path, ...]
collision_warnings: tuple[str, ...]
is_noop: bool
26    @property
27    def is_noop(self) -> bool:
28        """Return True if bootstrap made no changes."""
29        return (
30            len(self.created_subfolders) == 0
31            and len(self.migrated_files) == 0
32            and len(self.collision_warnings) == 0
33        )

Return True if bootstrap made no changes.

def bootstrap_features_directory( features_root: pathlib._local.Path) -> BootstrapResult:
 83def bootstrap_features_directory(features_root: Path) -> BootstrapResult:
 84    """Ensure the features directory has the canonical subfolder structure.
 85
 86    Creates backlog/, in-progress/, and completed/ if missing. Migrates
 87    any loose .feature files at the root level into backlog/.
 88
 89    Args:
 90        features_root: Root of the features directory.
 91
 92    Returns:
 93        BootstrapResult describing what was done.
 94    """
 95    if not features_root.exists():
 96        return BootstrapResult(
 97            created_subfolders=(),
 98            migrated_files=(),
 99            collision_warnings=(),
100        )
101    created = _ensure_canonical_subfolders(features_root)
102    migrated, warnings = _migrate_loose_feature_files(features_root)
103    return BootstrapResult(
104        created_subfolders=created,
105        migrated_files=migrated,
106        collision_warnings=warnings,
107    )

Ensure the features directory has the canonical subfolder structure.

Creates backlog/, in-progress/, and completed/ if missing. Migrates any loose .feature files at the root level into backlog/.

Args: features_root: Root of the features directory.

Returns: BootstrapResult describing what was done.