+[docs]
+defget_header_footer(rich_open,header_file=None,footer_file=None):
+"""
+ Adds a header and/or footer to the given content.
+
+ Args:
+ content (str): The content to which the header and/or footer will be added.
+ rich_open (function): A function used to open files.
+ header_file (str, optional): The file containing the header content. Defaults to None.
+ footer_file (str, optional): The file containing the footer content. Defaults to None.
+
+ Returns:
+ str: The content with the header and/or footer added.
+ """
+
+ defgetFile(file):
+ ifnotfile:
+ returnNone
+ withrich_open(file,"r",description=f"Reading {file}")asfile:
+ returnfile.read()
+
+ returngetFile(header_file),getFile(footer_file)
+
+
+
+
+[docs]
+deffind_duplicate_titles(data):
+"""
+ Check if there are any duplicate titles in dictionaries of lists of dictionaries.
+
+ Args:
+ data (dict): A dictionary containing lists of dictionaries with items
+ indexed by "title".
+
+ Returns:
+ bool: True if there are duplicate "title" values, False otherwise.
+ """
+ titles=[entry["title"]forcategoryindataforentryindata[category]]
+ returnnotlen(set(titles))==len(titles)
+[docs]
+definit():
+"""
+ Initialize configuration files for the application.
+
+ This function creates site and user configuration files, and optionally
+ a project configuration file based on user input.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ >>> init()
+ Do you want to create a project config file? [y/N]: y
+ """
+ ifSettings.enable_experimental_features:
+ conf_files=[
+ brassy.utils.settings_manager.get_site_config_file_path("brassy"),
+ brassy.utils.settings_manager.get_user_config_file_path("brassy"),
+ ]
+ else:
+ conf_files=[]
+ forconf_fileinconf_files:
+ brassy.utils.settings_manager.create_config_file(conf_file)
+ ifbrassy.utils.messages.boolean_prompt(
+ "Do you want to create a project config file?"
+ ):
+ brassy.utils.settings_manager.create_config_file(
+ brassy.utils.settings_manager.get_project_config_file_path("brassy")
+ )
+[docs]
+defprune_empty(data,prune_lists=True,key=""):
+"""
+ Recursively remove empty values from a nested dictionary or list.
+
+ Parameters
+ ----------
+ data : dict or list
+ The data structure to prune.
+ prune_lists : bool, optional
+ Indicates whether to prune empty lists. Currently unused.
+ key : str, optional
+ The key associated with the current data item, used for special cases.
+
+ Returns
+ -------
+ dict or list or None
+ The pruned data structure, or None if it is empty.
+
+ Notes
+ -----
+ The function considers the following values as empty: `None`, empty strings,
+ empty dictionaries, and empty lists. If a value is `0` and the key is
+ `"number"`, it is also considered empty to address the related issues
+ field which was previously set to 0 instead of null.
+
+ Examples
+ --------
+ >>> data = {'a': None, 'b': '', 'c': {'d': [], 'e': 'value'}}
+ >>> prune_empty(data)
+ {'c': {'e': 'value'}}
+ """
+ nulls=(None,"",{},[])
+ ifisinstance(data,dict):
+ pruned={k:prune_empty(v,key=k)fork,vindata.items()}
+ pruned={k:vfork,vinpruned.items()ifvnotinnulls}
+ returnprunedifprunedelseNone
+ elifisinstance(data,list):
+ pruned=[prune_empty(item)foritemindata]
+ pruned=[itemforiteminprunedifitemnotinnulls]
+ returnprunedifprunedelseNone
+ elifdata==0andkey=="number":
+ returnNone
+ else:
+ returndata
+
+
+
+
+[docs]
+defprune_yaml_file(yaml_file_path,console):
+"""
+ Prune empty values from a YAML file and overwrite it with the pruned content.
+
+ Parameters
+ ----------
+ yaml_file_path : str
+ The file path to the YAML file to be pruned.
+ console : Console
+ An object used for printing messages to the console.
+
+ Returns
+ -------
+ None
+
+ Notes
+ -----
+ This function reads the YAML file, prunes empty values using `prune_empty`,
+ and writes the pruned content back to the same file.
+
+ Examples
+ --------
+ >>> prune_yaml_file('config.yaml', console)
+ Pruned config.yaml
+ """
+ withopen(yaml_file_path,"r+")asfile:
+ content=yaml.safe_load(file)
+ file.seek(0)
+ file.write(
+ yaml.dump(
+ prune_empty(content,prune_lists=False),
+ sort_keys=False,
+ default_flow_style=False,
+ )
+ )
+ file.truncate()
+ console.print(f"Pruned {yaml_file_path}")
+
+
+
+
+[docs]
+defdirect_pruning_of_files(input_files_or_folders,console,working_dir):
+"""
+ Prune empty values from YAML files specified by input paths.
+
+ Parameters
+ ----------
+ input_files_or_folders : list of str
+ A list of file paths or directories containing YAML files to prune.
+ console : Console
+ An object used for printing messages to the console.
+ working_dir : str
+ The working directory path.
+
+ Returns
+ -------
+ None
+
+ Notes
+ -----
+ This function collects YAML files from the specified input paths and
+ prunes each file using `prune_yaml_file`.
+
+ Examples
+ --------
+ >>> direct_pruning_of_files(['configs/'], console, '/home/user')
+ Pruned configs/config1.yaml
+ Pruned configs/config2.yaml
+ """
+ importbrassy.utils.CLI# here to prevent circular import
+
+ yaml_files=brassy.utils.CLI.get_file_list_from_cli_input(
+ input_files_or_folders,console,working_dir=working_dir
+ )
+ foryaml_fileinyaml_files:
+ prune_yaml_file(yaml_file,console)
+"""
+This module provides functionality to generate release notes from YAML files.
+It reads YAML files, parses their content, and formats the parsed data into release notes in .rst format.
+The release notes can be written to an output file.
+"""
+
+importargparse
+importos
+importrich.progress
+importyaml
+fromdatetimeimportdatetime
+
+importpygit2
+
+importbrassy.utils.settings_managerassettings_manager
+
+Settings=settings_manager.get_settings("brassy")
+
+
+
+[docs]
+ @model_validator(mode="after")
+ defcheck_at_least_one_field(self):
+ ifnotany(
+ getattr(self,field)forfieldin["deleted","moved","added","modified"]
+ ):
+ raiseValueError(
+ "At least one of deleted, moved, added, or modified must have a value"
+ )
+ returnself
+[docs]
+ @model_validator(mode="before")
+ defempty_str_to_none(values):
+ forvaluein["title","description"]:
+ ifvalues[value]=="":
+ values[value]=None
+ # if not values["title"] and not values["description"]:
+ # if not values == ReleaseNote():
+ # raise ValueError("Missing title and description")
+ returnvalues
+
+
+
+
+
+[docs]
+classReleaseNote(RootModel[Dict[str,List[ChangeItem]]]):
+"""
+ ReleaseNote is a root model containing a dictionary that maps category names to lists of ChangeItems.
+ """
+
+ pass
+
+
+
+frombrassy.utils.settings_managerimportget_settings
+
+Settings=get_settings("brassy")
+
+# List of categories stored in a variable
+categories=Settings.change_categories
+
+# This module is for the CLI only portions of brassy.
+# Brassy can be run without this file, and importing it should
+
+importargparse
+importos
+
+fromrich_argparseimportRichHelpFormatter
+
+importbrassy.actions.build_release_notes
+importbrassy.actions.init
+importbrassy.actions.prune_yaml
+importbrassy.utils.file_handler
+importbrassy.utils.git_handler
+frombrassy.utils.settings_managerimportget_settings
+importbrassy.utils.messagesasmessages
+
+Settings=get_settings("brassy")
+
+
+
+[docs]
+defget_parser():
+"""
+ Returns an ArgumentParser object with predefined arguments for generating release notes from YAML files.
+
+ Returns:
+ argparse.ArgumentParser: The ArgumentParser object with predefined arguments.
+ """
+ parser=argparse.ArgumentParser(
+ description="Generate release notes from YAML files."
+ +" Entries are sorted by order in yaml files, "
+ +"and by order of yaml files provided via the command line.",
+ formatter_class=RichHelpFormatter,
+ )
+ parser.add_argument(
+ "-t",
+ "--write-yaml-template",
+ type=str,
+ help="Write template YAML to provided file."
+ +" If folder provided, place template in folder with current "
+ +"git branch name as file name.",
+ nargs="?",
+ default=argparse.SUPPRESS,
+ )
+ parser.add_argument(
+ "-c",
+ "--get-changed-files",
+ type=str,
+ help="Print git tracked file changes against main."
+ +" If directory provided, use that directories checked-out branch.",
+ nargs="?",
+ default=argparse.SUPPRESS,
+ )
+ parser.add_argument(
+ "input_files_or_folders",
+ type=str,
+ nargs="*",
+ help="The folder(s) containing YAML files and/or YAML files. "
+ +"Folders will be searched recursively.",
+ )
+ parser.add_argument(
+ "-r",
+ "--release-version",
+ type=str,
+ default="[UNKNOWN]",
+ help="Version number of the release. Default is '[UNKNOWN]'.",
+ required=False,
+ dest="version",
+ )
+ parser.add_argument(
+ "-d",
+ "--release-date",
+ type=str,
+ help="Date of the release. Default is current system time.",
+ required=False,
+ )
+ parser.add_argument(
+ "-nc",
+ "--no-color",
+ action="store_true",
+ default=Settings.use_color,
+ help="Disable text formatting for CLI output.",
+ )
+ parser.add_argument(
+ "-p",
+ "--prefix-file",
+ type=str,
+ help="A header file to prepend to the release notes.",
+ )
+ parser.add_argument(
+ "-s",
+ "--suffix-file",
+ type=str,
+ help="A footer file to suffix to the release notes.",
+ )
+ parser.add_argument(
+ "-o","--output-file",type=str,help="The output file for release notes."
+ )
+ ifSettings.default_yaml_pathandSettings.enable_experimental_features:
+ yaml_path=os.path.join(".",Settings.default_yaml_path)
+ else:
+ yaml_path="."
+ parser.add_argument(
+ "-yd",
+ "--yaml-dir",
+ type=str,
+ help="Directory to write yaml files to",
+ default=yaml_path,
+ )
+ parser.add_argument(
+ "--output-to-console",
+ action="store_true",
+ default=False,
+ help="Write generated release notes to console.",
+ )
+ parser.add_argument(
+ "-nr","--no-rich",action="store_true",help="Disable rich text output"
+ )
+ parser.add_argument("-q","--quiet",action="store_true",help="Only output errors")
+ parser.add_argument(
+ "-pr",
+ "--prune",
+ action="store_true",
+ help="Prune provided yaml file(s) of empty sections",
+ )
+ parser.add_argument(
+ "--init",
+ action="store_true",
+ help="Initialize brassy and generate config files",
+ default=False,
+ )
+ parser.add_argument(
+ "--version",
+ action="store_true",
+ dest="print_version",
+ help="Print program version and exit",
+ default=False,
+ )
+ returnparser
+
+
+
+
+[docs]
+defparse_arguments():
+"""
+ Parse command line arguments for input folder and output file.
+
+ Returns
+ -------
+ argparse.Namespace
+ Parsed arguments containing input_folder and output_file.
+ """
+ parser=get_parser()
+ returnparser.parse_args(),parser
+
+
+
+
+[docs]
+defprint_version_and_exit():
+ messages.RichConsole.print(f"Brassy is at version {brassy.__version__}")
+ exit(0)
+
+
+
+
+[docs]
+defexit_on_invalid_arguments(args,parser,console):
+"""
+ Validate the argparse arguments.
+
+ This function validates the provided argparse arguments to ensure
+ that the required input files/folders and output file are provided.
+ If arguments are invalid, it prints an error message and exits the program.
+
+ Parameters
+ ----------
+ args : argparse.Namespace
+ Parsed arguments.
+
+ parser : argparse.ArgumentParser
+ The ArgumentParser object used to parse the command-line arguments.
+ """
+ if(
+ bool(args.input_files_or_folders)
+ or"get_changed_files"inargs
+ orargs.init
+ orargs.version
+ ):
+ return
+
+ if"write_yaml_template"inargs:
+ return
+
+ console.print("[bold red]Invalid arguments.\n")
+ parser.print_help()
+ exit(1)
+
+
+
+
+[docs]
+defget_yaml_files_from_input(input_files_or_folders):
+"""
+ Get a list of YAML files from the given input files or folders.
+
+ Parameters
+ ----------
+ input_files_or_folders : list
+ List of paths to input files or folders.
+
+ Returns
+ -------
+ list
+ List of paths to YAML files.
+
+ Raises
+ ------
+ ValueError
+ If a file is not a YAML file or if no YAML files are found in a directory.
+ """
+ yaml_files=[]
+ forpathininput_files_or_folders:
+ ifos.path.isfile(path):
+ ifnotpath.endswith(".yaml")orpath.endswith(".yml"):
+ raiseValueError(f"File {path} is not a YAML file.")
+ yaml_files.append(path)
+ elifos.path.isdir(path):
+ dir_yaml_files=[]
+ forroot,dirs,filesinos.walk(path):
+ forfileinfiles:
+ iffile.endswith(".yaml")orfile.endswith(".yml"):
+ dir_yaml_files.append(os.path.join(root,file))
+ iflen(dir_yaml_files)==0:
+ raiseFileExistsError(f"No YAML files found in directory {path}.")
+ yaml_files.extend(dir_yaml_files)
+ else:
+ raiseFileNotFoundError(path)
+ returnyaml_files
+[docs]
+defget_yaml_template_path(file_path_arg,working_dir=os.getcwd()):
+"""
+ Returns the path of the YAML template file based on the given file path argument.
+
+ Args:
+ file_path_arg (str): The file path argument provided by the user.
+
+ Returns:
+ str: The path of the YAML template file.
+
+ """
+ iffile_path_argisNone:
+ filename=f"{git_handler.get_current_git_branch()}.yaml"
+ returnos.path.join(working_dir,filename)
+ if"/"infile_path_argor"\\"infile_path_argorPath(file_path_arg).is_file():
+ returnfile_path_arg
+ returnos.path.join(working_dir,file_path_arg)
+
+
+
+
+[docs]
+defcreate_blank_template_yaml_file(file_path_arg,console,working_dir="."):
+"""
+ Creates a blank YAML template file with a predefined structure.
+
+ This function generates a YAML file at the specified path with a default
+ template. It handles special characters required for YAML compatibility and writes
+ the file to disk.
+
+ Parameters
+ ----------
+ file_path_arg : str
+ The file path of the YAML template as passed via the CLI.
+ console : rich.console.Console
+ A Rich Console object used for displaying messages and errors to the user.
+ working_dir : str, optional
+ The working directory path. Defaults to the current directory ".".
+
+ Raises
+ ------
+ SystemExit
+ If a Git repo is not found in the current working directory and no file path
+ is provided, the program exits with an error message.
+
+ Notes
+ -----
+ This function performs a string replacement to insert a "|" due to an issue with
+ YAML's handling of pipe symbols. For more details, see:
+ https://github.com/yaml/pyyaml/pull/822
+ """
+ pipe_replace_string="REPLACE_ME_WITH_PIPE"
+ frombrassy.templates.release_yaml_templateimportReleaseNote
+
+ default_yaml={
+ category:[
+ {
+ "title":"",
+ "description":(
+ pipe_replace_string
+ ifSettings.description_populates_with_pipe
+ else""
+ ),
+ "files":{change:[""]forchangeinSettings.valid_changes},
+ "related-issue":{"number":None,"repo_url":""},
+ # in time, extract from the first and last commit
+ "date":{"start":None,"finish":None},
+ }
+ ]
+ forcategoryinSettings.change_categories
+ }
+ try:
+ yaml_template_path=get_yaml_template_path(file_path_arg,working_dir)
+ exceptGitError:
+ console.print(
+ "[bold red]Could not find a git repo. Please run in a "
+ +"git repo or pass a file path for the yaml template "
+ +"(eg '-t /path/to/file.yaml')."
+ )
+ exit(1)
+ withopen(yaml_template_path,"w")asfile:
+ yaml_text=yaml.safe_dump(
+ default_yaml,sort_keys=False,default_flow_style=False
+ )
+ ifSettings.description_populates_with_pipe:
+ yaml_text=yaml_text.replace(pipe_replace_string,"|\n replace_me")
+ file.write(yaml_text)
+
+
+
+
+[docs]
+defvalue_error_on_invalid_yaml(content,file_path):
+"""
+ Check if the YAML content follows the correct schema.
+
+ Parameters
+ ----------
+ content : dict
+ Parsed content of the YAML file.
+ file_path : str
+ Path to the YAML file.
+
+ Raises
+ ------
+ ValueError
+ If the YAML content does not follow the correct schema.
+ """
+ ifcontentisNone:
+ raiseValueError(f"No valid brassy-related YAML. Please populate {file_path}")
+ frombrassy.templates.release_yaml_templateimportReleaseNote
+
+ ReleaseNote(**content)
+
+
+
+
+[docs]
+defread_yaml_files(input_files,rich_open):
+"""
+ Read and parse the given list of YAML files.
+
+ Parameters
+ ----------
+ input_files : list
+ List of paths to the YAML files.
+
+ Returns
+ -------
+ dict
+ Parsed content of all YAML files categorized by type of change.
+
+ Examples
+ --------
+ >>> read_yaml_files(["file1.yaml", "file2.yaml"])
+ {'bug-fix': [
+ {'title': 'fixed explosions',
+ 'description': 'This fixed the explosion mechanism'},
+ {'title': 'fixed cats not being cute',
+ 'description': 'This made the cats WAY cuter'}
+ ]
+ }
+ """
+ data={}
+ forfile_pathininput_files:
+ withrich_open(file_path,"r",description=f"Reading {file_path}")asfile:
+ content=yaml.safe_load(file)
+ value_error_on_invalid_yaml(content,file_path)
+ forcategory,entriesincontent.items():
+ entries=[
+ entry
+ forentryinentries
+ ifnot(entry["title"]==""orentry["description"]=="")
+ ]
+ ifcategorynotindataandlen(entries)>0:
+ data[category]=[]
+ iflen(entries)>0:
+ data[category].extend(entries)
+ returndata
+
+
+
+
+[docs]
+defwrite_output_file(output_file,content):
+"""
+ Write the formatted release notes to the output file.
+
+ Parameters
+ ----------
+ output_file : str
+ Path to the output .rst file.
+ content : str
+ Formatted release notes.
+ """
+ withopen(output_file,"w")asfile:
+ file.write(content)
+[docs]
+defget_git_status(repo_path="."):
+"""
+ Retrieves the status of files in the given Git repository.
+
+ Parameters:
+ repo_path (str): The path to the Git repository. Defaults to the current directory.
+
+ Returns:
+ dict: A dictionary with keys 'added', 'modified', 'deleted', and 'renamed',
+ each containing a list of file paths that match the respective status.
+ """
+
+ # Open the repository
+ repo=pygit2.Repository(repo_path)
+
+ # Get the current branch reference
+ try:
+ current_branch=repo.head
+ exceptpygit2.GitError:
+ raisepygit2.GitError(f"{repo_path} is not a git repo or does not have a head")
+
+ # Get the main branch reference
+ main_branch=repo.branches["main"]
+
+ # Get the commit objects
+ current_commit=repo[current_branch.target]
+ main_commit=repo[main_branch.target]
+
+ # Get the diff between the current commit and the main branch commit
+ diff=repo.diff(main_commit,current_commit)
+
+ # Prepare dictionaries to store file statuses
+ status={
+ "added":[],
+ "modified":[],
+ "deleted":[],
+ "moved":[],
+ }
+
+ # Process the diff
+ fordeltaindiff.deltas:
+ ifdelta.status==pygit2.GIT_DELTA_ADDED:
+ status["added"].append(delta.new_file.path)
+ elifdelta.status==pygit2.GIT_DELTA_MODIFIED:
+ status["modified"].append(delta.new_file.path)
+ elifdelta.status==pygit2.GIT_DELTA_DELETED:
+ status["deleted"].append(delta.old_file.path)
+ elifdelta.status==pygit2.GIT_DELTA_RENAMED:
+ status["moved"].append((delta.old_file.path,delta.new_file.path))
+
+ return{
+ "added":status["added"],
+ "modified":status["modified"],
+ "deleted":status["deleted"],
+ "moved":status["moved"],
+ }
+[docs]
+defget_current_git_branch():
+"""
+ Get the current dirs git branch name.
+
+ Returns
+ -------
+ str
+ The name of the current git branch.
+ """
+ repo=pygit2.Repository(".")
+ returnrepo.head.shorthand
+[docs]
+definit_logger(use_rich):
+"""
+ Initialize and configure the logger.
+
+ Parameters
+ ----------
+ use_rich : bool
+ If True, sets up rich logging else use standard stream logging
+
+ Returns
+ -------
+ logger : logging.Logger
+ The configured logger instance
+ """
+ logger=logging.getLogger("build_docs")
+ ifuse_rich:
+ install_rich_tracebacks()
+ logging_handlers=[RichHandler(rich_tracebacks=True)]
+ else:
+ logging_handlers=[logging.StreamHandler()]
+
+ logging.basicConfig(level=logging.DEBUG,datefmt="[%X]",handlers=logging_handlers)
+ logger.debug("Program initialized")
+ returnlogger
+
+
+
+
+[docs]
+defget_rich_opener(no_format=False):
+"""
+ Returns the appropriate opener function for rich progress bar.
+
+ Args:
+ no_format (bool, optional): If True, returns the opener function without any formatting.
+ If False, returns the opener function with formatting. Defaults to False.
+
+ Returns:
+ function: The opener function for rich progress bar.
+ """
+ ifno_format:
+ returnrich.progress.Progress().open
+ else:
+ returnrich.progress.open
+
+
+
+
+[docs]
+defsetup_console(no_format=False,quiet=False):
+"""
+ Set up and return the console for printing messages.
+
+ Args:
+ no_format (bool, optional): Whether to disable formatting. Defaults to False.
+ quiet (bool, optional): Whether to suppress console output. Defaults to False.
+
+ Returns:
+ Console: The configured rich console object.
+ """
+ ifnotno_format:
+ install_rich_tracebacks()
+ console=rich_console(quiet=quiet,no_color=(no_formatorquiet))
+ returnconsole
+[docs]
+defget_git_repo_root(path="."):
+"""
+ Get the root directory of the Git repository containing the given path.
+
+ Parameters
+ ----------
+ path : str, optional
+ A path within the Git repository. Defaults to the current directory.
+
+ Returns
+ -------
+ str
+ Absolute path to the root of the Git repository. This is usually the
+ path containing the .git folder.
+ """
+ returnos.path.abspath(os.path.join(pygit2.Repository(path).path,".."))
+
+
+
+
+[docs]
+defget_project_config_file_path(app_name):
+"""
+ Retrieve the project-specific configuration file path for the application.
+
+ Parameters
+ ----------
+ app_name : str
+ Name of the application.
+
+ Returns
+ -------
+ str
+ Path to the project's configuration file.
+ """
+ project_file=f".{app_name}"
+ ifos.path.isfile(project_file):
+ returnproject_file
+ try:
+ returnos.path.join(get_git_repo_root(),project_file)
+ exceptpygit2.GitError:
+ returnproject_file
+
+
+
+
+[docs]
+defget_user_config_file_path(app_name):
+"""
+ Retrieve the user-specific configuration file path for the application.
+
+ Parameters
+ ----------
+ app_name : str
+ Name of the application.
+
+ Returns
+ -------
+ str
+ Path to the user's configuration file.
+ """
+ returnos.path.join(platformdirs.user_config_dir(app_name),"user.config")
+
+
+
+
+[docs]
+defget_site_config_file_path(app_name):
+"""
+ Retrieve the site-specific configuration file path for the application.
+
+ Parameters
+ ----------
+ app_name : str
+ Name of the application.
+
+ Returns
+ -------
+ str
+ Path to the site's configuration file.
+ """
+ returnos.path.join(platformdirs.site_config_dir(app_name),"site.config")
+
+
+
+
+[docs]
+defget_config_files(app_name):
+"""
+ Get a list of configuration file paths in order of increasing precedence.
+
+ Parameters
+ ----------
+ app_name : str
+ Name of the application.
+
+ Returns
+ -------
+ list of str
+ List of configuration file paths.
+ """
+ config_files=[]
+ forfin[
+ get_site_config_file_path,
+ get_user_config_file_path,
+ get_project_config_file_path,
+ ]:
+ path=f(app_name)
+ config_files.append(path)
+ returnconfig_files
+
+
+
+
+[docs]
+defcreate_config_file(config_file):
+"""
+ Create a configuration file with default settings.
+
+ Parameters
+ ----------
+ config_file : str
+ Path where the configuration file will be created.
+ """
+ default_settings=SettingsTemplate()
+ config_dir=os.path.dirname(config_file)
+ ifconfig_dir:
+ os.makedirs(config_dir,exist_ok=True)
+ withopen(config_file,"wt")asf:
+ yaml.dump(default_settings.dict(),f)
+[docs]
+defmerge_and_validate_config_files(config_files):
+"""
+ Merge settings from multiple configuration files and validate them.
+
+ Parameters
+ ----------
+ config_files : list of str
+ List of configuration file paths. The order of the files matters.
+ Each file overwrites the values of the previous.
+
+ Returns
+ -------
+ dict
+ Merged and validated configuration settings.
+
+ Raises
+ ------
+ ValidationError
+ If any of the settings do not conform to the `Settings` model.
+ """
+ settings={}
+ forconfig_fileinconfig_files:
+ file_settings=read_config_file(config_file,create_file_if_not_exist=False)
+ try:
+ SettingsTemplate(**file_settings)
+ exceptValidationErrorase:
+ print(f"Failed to validate {config_file}")
+ print(repr(e.errors()[0]))
+ raisee
+ settings.update(file_settings)
+ returnsettings
+
+
+
+
+[docs]
+defget_settings_from_config_files(app_name):
+"""
+ Retrieve settings from configuration files without environment overrides.
+
+ Parameters
+ ----------
+ app_name : str
+ Name of the application.
+
+ Returns
+ -------
+ dict
+ Configuration settings merged from files.
+ """
+ returnmerge_and_validate_config_files(get_config_files(app_name))
+[docs]
+defget_settings(app_name):
+"""
+ Return application settings from config files and environment variables.
+
+ Parameters
+ ----------
+ app_name : str
+ Name of the application.
+
+ Returns
+ -------
+ Settings
+ An instance of the `Settings` model with all configurations applied.
+
+ Raises
+ ------
+ ValidationError
+ If the final settings do not conform to the `Settings` model.
+ """
+ file_settings=override_dict_with_environmental_variables(
+ get_settings_from_config_files(app_name)
+ )
+ Settings=SettingsTemplate(**file_settings)
+ returnSettings
Recursively remove empty values from a nested dictionary or list.
+
+
Parameters:
+
+
data (dict or list) – The data structure to prune.
+
prune_lists (bool, optional) – Indicates whether to prune empty lists. Currently unused.
+
key (str, optional) – The key associated with the current data item, used for special cases.
+
+
+
Returns:
+
The pruned data structure, or None if it is empty.
+
+
Return type:
+
dict or list or None
+
+
+
Notes
+
The function considers the following values as empty: None, empty strings,
+empty dictionaries, and empty lists. If a value is 0 and the key is
+“number”, it is also considered empty to address the related issues
+field which was previously set to 0 instead of null.
This function validates the provided argparse arguments to ensure
+that the required input files/folders and output file are provided.
+If arguments are invalid, it prints an error message and exits the program.
+
+
Parameters:
+
+
args (argparse.Namespace) – Parsed arguments.
+
parser (argparse.ArgumentParser) – The ArgumentParser object used to parse the command-line arguments.
Returns the appropriate opener function for rich progress bar.
+
+
Parameters:
+
no_format (bool, optional) – If True, returns the opener function without any formatting.
+If False, returns the opener function with formatting. Defaults to False.
Creates a blank YAML template file with a predefined structure.
+
This function generates a YAML file at the specified path with a default
+template. It handles special characters required for YAML compatibility and writes
+the file to disk.
+
+
Parameters:
+
+
file_path_arg (str) – The file path of the YAML template as passed via the CLI.
+
console (rich.console.Console) – A Rich Console object used for displaying messages and errors to the user.
+
working_dir (str, optional) – The working directory path. Defaults to the current directory “.”.
+
+
+
Raises:
+
SystemExit – If a Git repo is not found in the current working directory and no file path
+ is provided, the program exits with an error message.
+
+
+
Notes
+
This function performs a string replacement to insert a “|” due to an issue with
+YAML’s handling of pipe symbols. For more details, see:
+https://github.com/yaml/pyyaml/pull/822
This module provides functionality to generate release notes from YAML files.
+It reads YAML files, parses their content, and formats the parsed data into release notes in .rst format.
+The release notes can be written to an output file.
Some organizations use internal repos that cannot be accessed by CI. This change adds an “internal” field for the
+related issue so that users can add strings directly rather than use the URL+issue number combo previously provided. The
+strings are rendered directly, but must fit a “repo#number - description” pattern. The field is named “internal” to
+suggest to users that when possible the URL option should be used to access future functionality.
Title is now at top of template because users were mixing up description and title. This change is backwards compatible,
+and no updates to yaml files are needed.
Builds now have sections with either a missing title or description¶
+
Previously, if yaml files only had title OR description populated, they would silently not get built into release notes.
+Now they are incorporated into release notes with a warning.
Brassy can create blank yaml templates for release notes. By default, brassy will name the file after your current git
-branch name. You can also specify a name manually, and .yaml will be appended if you do not end your file name with
+
Brassy can create blank yaml templates for release notes.
+By default, brassy will name the file after your current git
+branch name. You can also specify a name manually, and
+.yaml will be appended if you do not end your file name with
.yml or .yaml. You can do this with the following command:
brassy--write-yaml-templaterelease-note.yaml
+# or
+brassy-trelease-note.yaml
+# or leave blank to name after current git branch
+brassy-t
By default, the yaml template will be populated with the following fields:
@@ -54,33 +61,49 @@
Create YAML template
+
You can configure this in your .brassy file. See also Settings.
For example, the section for bug-fix will look like this:
You can do anything that is valid yaml in these fields. For example:
-
bug-fix:
--title:""
-description:|
+
For example:
+
bug fix:
+-title:'Fixelephantrelatedcrash'
+description:|- Fixed a bug where the program would crash when the user thought of elephants.
-- Fixed a bug where the program would ``segfault``
-when the user looked at the button.
-
Help!¶<
A footer file to suffix to the release notes.
-o, --output-file OUTPUT_FILE
The output file for release notes.
+ -yd, --yaml-dir YAML_DIR
+ Directory to write yaml files to
--output-to-console Write generated release notes to console.
-nr, --no-rich Disable rich text output
-q, --quiet Only output errors
+ -pr, --prune Prune provided yaml file(s) of empty sections
+ --init Initialize brassy and generate config files
+ --version Print program version and exit