|
37 | 37 | from rich.syntax import Syntax |
38 | 38 |
|
39 | 39 | from molecule.console import console |
40 | | -from molecule.constants import MOLECULE_HEADER |
| 40 | +from molecule.constants import ( |
| 41 | + MOLECULE_COLLECTION_GLOB, |
| 42 | + MOLECULE_COLLECTION_ROOT, |
| 43 | + MOLECULE_GLOB, |
| 44 | + MOLECULE_HEADER, |
| 45 | + MOLECULE_ROOT, |
| 46 | +) |
41 | 47 | from molecule.exceptions import MoleculeError |
42 | 48 |
|
43 | 49 |
|
|
47 | 53 | from typing import Any, AnyStr, NoReturn, TypeVar |
48 | 54 | from warnings import WarningMessage |
49 | 55 |
|
50 | | - from molecule.types import CommandArgs, ConfigData, Options, PlatformData |
| 56 | + from molecule.types import CollectionData, CommandArgs, ConfigData, Options, PlatformData |
51 | 57 |
|
52 | 58 | NestedDict = MutableMapping[str, Any] |
53 | 59 | _T = TypeVar("_T", bound=NestedDict) |
@@ -459,16 +465,26 @@ def lookup_config_file(filename: str) -> str | None: |
459 | 465 | """Return config file PATH. |
460 | 466 |
|
461 | 467 | Args: |
462 | | - filename: Config file name to find.and |
| 468 | + filename: Config file name to find. |
463 | 469 |
|
464 | 470 | Returns: |
465 | 471 | Path to config file or None if not found. |
466 | 472 | """ |
467 | | - for path in [find_vcs_root(default="~"), "~"]: |
468 | | - f = (Path(path) / filename).expanduser() |
469 | | - if f.is_file(): |
470 | | - LOG.info("Found config file %s", f) |
471 | | - return str(f) |
| 473 | + # project root |
| 474 | + search_paths = [Path(find_vcs_root(default="~")) / filename] |
| 475 | + |
| 476 | + # collection path |
| 477 | + collection_dir, _ = get_collection_metadata() |
| 478 | + if collection_dir: |
| 479 | + search_paths.append(Path(collection_dir / MOLECULE_COLLECTION_ROOT / Path(filename).name)) |
| 480 | + |
| 481 | + # home directory |
| 482 | + search_paths.append(Path.home() / filename) |
| 483 | + |
| 484 | + for path in search_paths: |
| 485 | + if path.is_file(): |
| 486 | + LOG.info("Found config file %s", path) |
| 487 | + return str(path) |
472 | 488 | return None |
473 | 489 |
|
474 | 490 |
|
@@ -573,3 +589,74 @@ def oxford_comma(listed: Iterable[bool | str | Path], condition: str = "and") -> |
573 | 589 | return f"{', '.join(s for s in front)}, {condition} {back}" |
574 | 590 | case _: |
575 | 591 | return "" |
| 592 | + |
| 593 | + |
| 594 | +@cache |
| 595 | +def get_collection_metadata() -> tuple[Path, CollectionData] | tuple[None, None]: |
| 596 | + """Get collection directory and metadata. |
| 597 | +
|
| 598 | + Returns: |
| 599 | + Tuple of (collection_directory, collection_data) if in a valid collection, |
| 600 | + or (None, None) if not in collection or galaxy.yml is invalid. |
| 601 | + Collection data dict contains at least 'name' and 'namespace' keys. |
| 602 | + Only returns collection info when galaxy.yml is valid. |
| 603 | + """ |
| 604 | + cwd = Path.cwd() |
| 605 | + galaxy_file = cwd / "galaxy.yml" |
| 606 | + |
| 607 | + try: |
| 608 | + galaxy_data: CollectionData = safe_load_file(galaxy_file) |
| 609 | + important_keys = {"name", "namespace"} |
| 610 | + |
| 611 | + if not isinstance(galaxy_data, dict): |
| 612 | + LOG.warning("Invalid galaxy.yml format at %s", galaxy_file) |
| 613 | + return None, None |
| 614 | + |
| 615 | + if missing_keys := important_keys.difference(galaxy_data.keys()): |
| 616 | + LOG.warning( |
| 617 | + "galaxy.yml at %s is missing required fields: %s", |
| 618 | + galaxy_file, |
| 619 | + oxford_comma(missing_keys), |
| 620 | + ) |
| 621 | + return None, None |
| 622 | + except FileNotFoundError: |
| 623 | + LOG.warning("No galaxy.yml found at %s", galaxy_file) |
| 624 | + return None, None |
| 625 | + except (OSError, yaml.YAMLError, MoleculeError) as exc: |
| 626 | + LOG.warning("Failed to load galaxy.yml at %s: %s", galaxy_file, exc) |
| 627 | + return None, None |
| 628 | + else: |
| 629 | + LOG.debug("Found galaxy.yml at %s", galaxy_file) |
| 630 | + return cwd, galaxy_data |
| 631 | + |
| 632 | + |
| 633 | +@cache |
| 634 | +def get_effective_molecule_glob() -> str: |
| 635 | + """Get the appropriate glob pattern. |
| 636 | +
|
| 637 | + Returns: |
| 638 | + Glob pattern string for finding molecule.yml files. |
| 639 | + Returns MOLECULE_COLLECTION_GLOB if in collection, |
| 640 | + otherwise returns MOLECULE_GLOB. |
| 641 | + """ |
| 642 | + # User provided |
| 643 | + if "MOLECULE_GLOB" in os.environ: |
| 644 | + return os.environ["MOLECULE_GLOB"] |
| 645 | + |
| 646 | + # No collection detected |
| 647 | + collection_dir, collection_data = get_collection_metadata() |
| 648 | + if not collection_dir or not collection_data: |
| 649 | + return MOLECULE_GLOB |
| 650 | + |
| 651 | + # Molecule found in root of collection |
| 652 | + if (Path.cwd() / MOLECULE_ROOT).exists(): |
| 653 | + msg = f"Molecule scenarios should migrate to '{MOLECULE_COLLECTION_ROOT}'" |
| 654 | + LOG.warning(msg) |
| 655 | + return MOLECULE_GLOB |
| 656 | + |
| 657 | + # Molecule not found in collection use extensions |
| 658 | + msg = f"Collection '{collection_data['name']}.{collection_data['namespace']}' detected." |
| 659 | + LOG.info(msg) |
| 660 | + msg = f"Scenarios will be used from '{MOLECULE_COLLECTION_ROOT}'" |
| 661 | + LOG.info(msg) |
| 662 | + return MOLECULE_COLLECTION_GLOB |
0 commit comments