3939)
4040from .errors import InvalidTypeError , MissingFileWarning , UserMessageError
4141
42+ # Default answers file names, in order of precedence
43+ DEFAULT_ANSWERS_FILE_YML = ".copier-answers.yml"
44+ DEFAULT_ANSWERS_FILE_YAML = ".copier-answers.yaml"
45+
4246
4347# TODO Remove these two functions as well as DEFAULT_DATA in a future release
4448def _now () -> datetime :
@@ -585,11 +589,50 @@ def parse_yaml_list(string: str) -> list[str]:
585589
586590def load_answersfile_data (
587591 dst_path : StrOrPath ,
588- answers_file : StrOrPath = ".copier-answers.yml" ,
592+ answers_file : StrOrPath | None = None ,
589593 * ,
590594 warn_on_missing : bool = False ,
591595) -> AnyByStrDict :
592- """Load answers data from a `$dst_path/$answers_file` file if it exists."""
596+ """Load answers data from a `$dst_path/$answers_file` file if it exists.
597+
598+ Args:
599+ dst_path: Path to the destination directory.
600+ answers_file: Path to the answers file relative to dst_path.
601+ If None, auto-detects by checking for .copier-answers.yml first,
602+ then .copier-answers.yaml. The .yml extension takes precedence.
603+ warn_on_missing: If True, warn when the answers file is not found.
604+
605+ Returns:
606+ The loaded answers data, or an empty dict if not found.
607+ """
608+ dst = Path (dst_path )
609+
610+ # If answers_file is None, auto-detect
611+ if answers_file is None :
612+ yml_path = dst / DEFAULT_ANSWERS_FILE_YML
613+ yaml_path = dst / DEFAULT_ANSWERS_FILE_YAML
614+
615+ yml_exists = yml_path .is_file ()
616+ yaml_exists = yaml_path .is_file ()
617+
618+ # Warn if both files exist
619+ if yml_exists and yaml_exists :
620+ warnings .warn (
621+ f"Both { DEFAULT_ANSWERS_FILE_YML } and { DEFAULT_ANSWERS_FILE_YAML } "
622+ f"exist in { dst_path } . Using { DEFAULT_ANSWERS_FILE_YML } . "
623+ "Please remove the duplicate file." ,
624+ stacklevel = 2 ,
625+ )
626+
627+ # .yml takes precedence, fall back to .yaml
628+ if yml_exists :
629+ answers_file = DEFAULT_ANSWERS_FILE_YML
630+ elif yaml_exists :
631+ answers_file = DEFAULT_ANSWERS_FILE_YAML
632+ else :
633+ # Default to .yml when neither exists
634+ answers_file = DEFAULT_ANSWERS_FILE_YML
635+
593636 try :
594637 with Path (dst_path , answers_file ).open ("rb" ) as fd :
595638 return yaml .safe_load (fd )
@@ -602,6 +645,46 @@ def load_answersfile_data(
602645 return {}
603646
604647
648+ def resolve_answersfile_path (dst_path : StrOrPath ) -> Path :
649+ """Resolve which answers file to use for a given destination path.
650+
651+ For reading: Returns the existing answers file (.yml takes precedence over .yaml).
652+ For writing: Returns the existing file to update, or .yml as default for new files.
653+
654+ Warns if both .yml and .yaml files exist.
655+
656+ Args:
657+ dst_path: Path to the destination directory.
658+
659+ Returns:
660+ Relative path to the answers file to use.
661+ """
662+ dst = Path (dst_path )
663+ yml_path = dst / DEFAULT_ANSWERS_FILE_YML
664+ yaml_path = dst / DEFAULT_ANSWERS_FILE_YAML
665+
666+ yml_exists = yml_path .is_file ()
667+ yaml_exists = yaml_path .is_file ()
668+
669+ # Warn if both files exist
670+ if yml_exists and yaml_exists :
671+ warnings .warn (
672+ f"Both { DEFAULT_ANSWERS_FILE_YML } and { DEFAULT_ANSWERS_FILE_YAML } "
673+ f"exist in { dst_path } . Using { DEFAULT_ANSWERS_FILE_YML } . "
674+ "Please remove the duplicate file." ,
675+ stacklevel = 2 ,
676+ )
677+
678+ # .yml takes precedence for reading/updating
679+ if yml_exists :
680+ return Path (DEFAULT_ANSWERS_FILE_YML )
681+ # If only .yaml exists, use it (maintains backwards compatibility for .yaml projects)
682+ elif yaml_exists :
683+ return Path (DEFAULT_ANSWERS_FILE_YAML )
684+ # Default to .yml for new files
685+ return Path (DEFAULT_ANSWERS_FILE_YML )
686+
687+
605688CAST_STR_TO_NATIVE : Mapping [str , Callable [[str ], Any ]] = {
606689 "bool" : cast_to_bool ,
607690 "float" : float ,
0 commit comments