diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a707817..454046f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.8.0 hooks: - id: ruff args: [ --fix ] diff --git a/MethodicConfigurator/__init__.py b/MethodicConfigurator/__init__.py index 6286548..adc3027 100644 --- a/MethodicConfigurator/__init__.py +++ b/MethodicConfigurator/__init__.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Python package initialization file. Loads translations and declares version information. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/MethodicConfigurator/annotate_params.py b/MethodicConfigurator/annotate_params.py index b365b7a..84f1fbe 100755 --- a/MethodicConfigurator/annotate_params.py +++ b/MethodicConfigurator/annotate_params.py @@ -1,8 +1,9 @@ #!/usr/bin/python3 """ -This script fetches online ArduPilot parameter documentation (if not cached) and adds it to the specified file - or to all *.param and *.parm files in the specified directory. +Fetches online ArduPilot parameter documentation (if not cached) locally. + +and adds it to the specified file or to all *.param and *.parm files in the specified directory. 1. Checks if a local cache of the apm.pdef.xml file exists in the target directory or on the directory of the target file: - If it does, the script loads the file content. @@ -31,7 +32,7 @@ from sys import exc_info as sys_exc_info from sys import exit as sys_exit from types import TracebackType -from typing import Any, Optional +from typing import Any, Optional, Union from xml.etree import ElementTree as ET # no parsing, just data-structure manipulation from defusedxml import ElementTree as DET # just parsing, no data-structure manipulation @@ -116,7 +117,8 @@ def arg_parser() -> argparse.Namespace: def check_max_line_length(value: int) -> int: if value < 50 or value > 300: logging.critical("--max-line-length must be in the interval 50 .. 300, not %d", value) - raise SystemExit("Correct it and try again") + msg = "Correct it and try again" + raise SystemExit(msg) return value args.max_line_length = check_max_line_length(args.max_line_length) @@ -131,6 +133,7 @@ class Par: Attributes: value (float): The value of the parameter. comment (Optional[str]): An optional comment associated with the parameter. + """ def __init__(self, value: float, comment: Optional[str] = None) -> None: @@ -138,6 +141,7 @@ def __init__(self, value: float, comment: Optional[str] = None) -> None: self.comment = comment def __eq__(self, other: object) -> bool: + """Equality operation.""" if isinstance(other, Par): return self.value == other.value and self.comment == other.comment return False @@ -147,11 +151,12 @@ def load_param_file_into_dict(param_file: str) -> dict[str, "Par"]: """ Loads an ArduPilot parameter file into a dictionary with name, value pairs. - Parameters: - parm_file (str): The name of the parameter file to load. + Args: + param_file (str): The name of the parameter file to load. Returns: - dict: A dictionary containing the parameters from the file. + dict: A dictionary containing the parameters from the file. + """ parameter_dict: dict[str, Par] = {} try: @@ -176,32 +181,47 @@ def load_param_file_into_dict(param_file: str) -> dict[str, "Par"]: elif "\t" in line: parameter, value = line.split("\t", 1) else: - raise SystemExit(f"Missing parameter-value separator: {line} in {param_file} line {i}") + msg = f"Missing parameter-value separator: {line} in {param_file} line {i}" + raise SystemExit(msg) parameter = parameter.strip() Par.validate_parameter(param_file, parameter_dict, i, original_line, comment, parameter, value) except UnicodeDecodeError as exp: - raise SystemExit(f"Fatal error reading {param_file}: {exp}") from exp + msg = f"Fatal error reading {param_file}: {exp}" + raise SystemExit(msg) from exp return parameter_dict @staticmethod - def validate_parameter(param_file, parameter_dict, i, original_line, comment, parameter, value) -> None: # pylint: disable=too-many-arguments, too-many-positional-arguments - if len(parameter) > PARAM_NAME_MAX_LEN: - raise SystemExit(f"Too long parameter name: {parameter} in {param_file} line {i}") - if not re.match(PARAM_NAME_REGEX, parameter): - raise SystemExit(f"Invalid characters in parameter name {parameter} in {param_file} line {i}") - if parameter in parameter_dict: - raise SystemExit(f"Duplicated parameter {parameter} in {param_file} line {i}") + def validate_parameter( # pylint: disable=too-many-arguments, too-many-positional-arguments + param_file: str, + parameter_dict: dict[str, "Par"], + i: int, + original_line: str, + comment: Union[None, str], + parameter_name: str, + value: str, + ) -> None: + if len(parameter_name) > PARAM_NAME_MAX_LEN: + msg = f"Too long parameter name: {parameter_name} in {param_file} line {i}" + raise SystemExit(msg) + if not re.match(PARAM_NAME_REGEX, parameter_name): + msg = f"Invalid characters in parameter name {parameter_name} in {param_file} line {i}" + raise SystemExit(msg) + if parameter_name in parameter_dict: + msg = f"Duplicated parameter {parameter_name} in {param_file} line {i}" + raise SystemExit(msg) try: fvalue = float(value.strip()) - parameter_dict[parameter] = Par(fvalue, comment) + parameter_dict[parameter_name] = Par(fvalue, comment) except ValueError as exc: - raise SystemExit(f"Invalid parameter value {value} in {param_file} line {i}") from exc + msg = f"Invalid parameter value {value} in {param_file} line {i}" + raise SystemExit(msg) from exc except OSError as exc: _exc_type, exc_value, exc_traceback = sys_exc_info() if isinstance(exc_traceback, TracebackType): fname = os_path.split(exc_traceback.tb_frame.f_code.co_filename)[1] logging.critical("in line %s of file %s: %s", exc_traceback.tb_lineno, fname, exc_value) - raise SystemExit(f"Caused by line {i} of file {param_file}: {original_line}") from exc + msg = f"Caused by line {i} of file {param_file}: {original_line}" + raise SystemExit(msg) from exc @staticmethod def missionplanner_sort(item: str) -> tuple[str, ...]: @@ -213,6 +233,7 @@ def missionplanner_sort(item: str) -> tuple[str, ...]: Returns: A tuple representing the sorted parameter name. + """ parts = item.split("_") # Split the parameter name by underscore # Compare the parts separately @@ -227,22 +248,24 @@ def format_params(param_dict: dict[str, "Par"], file_format: str = "missionplann consisting of the parameter's name, its value, and optionally its comment. The comment is included if it is present in the parameter's 'Par' object. - Parameters: - param_dict (Dict[str, 'Par']): A dictionary of 'Par' objects. - Each key is a parameter name and each value is a 'Par' object. - Par can be a simple float or a Par object with a comment. - file_format (str): Can be "missionplanner" or "mavproxy" + Args: + param_dict (Dict[str, 'Par']): A dictionary of 'Par' objects. + Each key is a parameter name and each value is a 'Par' object. + Par can be a simple float or a Par object with a comment. + file_format (str): Can be "missionplanner" or "mavproxy" Returns: - List[str]: A list of strings, each string representing a parameter from the input dictionary - in the format "name,value # comment". + List[str]: A list of strings, each string representing a parameter from the input dictionary + in the format "name,value # comment". + """ if file_format == "missionplanner": param_dict = dict(sorted(param_dict.items(), key=lambda x: Par.missionplanner_sort(x[0]))) # sort alphabetically elif file_format == "mavproxy": param_dict = dict(sorted(param_dict.items())) # sort in ASCIIbetical order else: - raise SystemExit(f"ERROR: Unsupported file format {file_format}") + msg = f"ERROR: Unsupported file format {file_format}" + raise SystemExit(msg) formatted_params = [] if file_format == "missionplanner": @@ -272,12 +295,13 @@ def export_to_param(formatted_params: list[str], filename_out: str) -> None: """ Exports a list of formatted parameters to an ArduPilot parameter file. - Parameters: - formatted_params (List[str]): The list of formatted parameters to export. - filename_out (str): The output filename. + Args: + formatted_params (List[str]): The list of formatted parameters to export. + filename_out (str): The output filename. Returns: - None + None + """ if not formatted_params: return @@ -287,7 +311,8 @@ def export_to_param(formatted_params: list[str], filename_out: str) -> None: for line in formatted_params: output_file.write(line + "\n") except OSError as e: - raise SystemExit(f"ERROR: writing to file {filename_out}: {e}") from e + msg = f"ERROR: writing to file {filename_out}: {e}" + raise SystemExit(msg) from e @staticmethod def print_out(formatted_params: list[str], name: str) -> None: @@ -296,12 +321,13 @@ def print_out(formatted_params: list[str], name: str) -> None: If the list is too large, print only the ones that fit on the screen and wait for user input to continue. - Parameters: - formatted_params (List[str]): The list of formatted parameters to print. - name (str): A descriptive string for the list contents + Args: + formatted_params (List[str]): The list of formatted parameters to print. + name (str): A descriptive string for the list contents Returns: - None + None + """ if not formatted_params: return @@ -310,7 +336,7 @@ def print_out(formatted_params: list[str], name: str) -> None: # Get the size of the terminal if __name__ == "__main__": - rows_str, _columns = os_popen("stty size", "r").read().split() # noqa S607 + rows_str, _columns = os_popen("stty size", "r").read().split() # noqa: S605, S607 # Convert rows to integer rows = int(rows_str) - 2 # -2 for the next print and the input line @@ -320,7 +346,7 @@ def print_out(formatted_params: list[str], name: str) -> None: for i, line in enumerate(formatted_params): if i % rows == 0 and __name__ == "__main__": input(f"\n{name} list is long hit enter to continue") - rows_str, _columns = os_popen("stty size", "r").read().split() # noqa S607 + rows_str, _columns = os_popen("stty size", "r").read().split() # noqa: S605, S607 rows = int(rows_str) - 2 # -2 for the next print and the input line print(line) @@ -337,6 +363,7 @@ def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str Returns: ET.Element: The root element of the parsed XML data. + """ file_path = os_path.join(directory, filename) # Check if the locally cached file exists @@ -358,14 +385,16 @@ def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str except ImportError as exc: logging.critical("The requests package was not found") logging.critical("Please install it by running 'pip install requests' in your terminal.") - raise SystemExit("requests package is not installed") from exc + msg = "requests package is not installed" + raise SystemExit(msg) from exc try: # Send a GET request to the URL url = base_url + filename response = requests_get(url, timeout=5) if response.status_code != 200: logging.warning("Remote URL: %s", url) - raise requests_exceptions.RequestException(f"HTTP status code {response.status_code}") + msg = f"HTTP status code {response.status_code}" + raise requests_exceptions.RequestException(msg) except requests_exceptions.RequestException as e: logging.warning("Unable to fetch XML data: %s", e) # Send a GET request to the URL to the fallback (DEV) URL @@ -375,10 +404,12 @@ def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str response = requests_get(url, timeout=5) if response.status_code != 200: logging.critical("Remote URL: %s", url) - raise requests_exceptions.RequestException(f"HTTP status code {response.status_code}") + msg = f"HTTP status code {response.status_code}" + raise requests_exceptions.RequestException(msg) except requests_exceptions.RequestException as exp: logging.critical("Unable to fetch XML data: %s", exp) - raise SystemExit("unable to fetch online XML documentation") from exp + msg = "unable to fetch online XML documentation" + raise SystemExit(msg) from exp # Get the text content of the response xml_data = response.text try: @@ -387,7 +418,8 @@ def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str file.write(xml_data) except PermissionError as e: logging.critical("Permission denied to write XML data to file: %s", e) - raise SystemExit("permission denied to write online XML documentation to file") from e + msg = "permission denied to write online XML documentation to file" + raise SystemExit(msg) from e # Parse the XML data return DET.fromstring(xml_data) # type: ignore[no-any-return] @@ -414,6 +446,7 @@ def remove_prefix(text: str, prefix: str) -> str: Returns: str: The string without the prefix. + """ if text.startswith(prefix): return text[len(prefix) :] @@ -430,6 +463,7 @@ def split_into_lines(string_to_split: str, maximum_line_length: int) -> list[str Returns: List[str]: The list of lines. + """ doc_lines = re.findall(r".{1," + str(maximum_line_length) + r"}(?:\s|$)", string_to_split) # Remove trailing whitespace from each line @@ -442,9 +476,12 @@ def create_doc_dict(root: ET.Element, vehicle_type: str, max_line_length: int = Args: root (ET.Element): The root element of the parsed XML data. + vehicle_type (str): vehicle type string. + max_line_length (int): max line length Returns: Dict[str, Any]: A dictionary of parameter documentation. + """ # Dictionary to store the parameter documentation doc: dict[str, Any] = {} @@ -495,14 +532,16 @@ def create_doc_dict(root: ET.Element, vehicle_type: str, max_line_length: int = def format_columns(values: dict[str, Any], max_width: int = 105, max_columns: int = 4) -> list[str]: """ Formats a dictionary of values into column-major horizontally aligned columns. - It uses at most max_columns columns + It uses at most max_columns columns. Args: values (Dict[str, Any]): The dictionary of values to format. max_width (int, optional): The maximum number of characters on all columns. Defaults to 105. + max_columns (int): Maximum number of columns Returns: List[str]: The list of formatted strings. + """ # Convert the dictionary into a list of strings strings = [f"{k}: {v}" for k, v in values.items()] @@ -541,18 +580,14 @@ def format_columns(values: dict[str, Any], max_width: int = 105, max_columns: in def extract_parameter_name(item: str) -> str: - """ - Extract the parameter name from a line. Very simple to use in sorting - """ + """Extract the parameter name from a line. Very simple to use in sorting.""" item = item.strip() match = re.match(PARAM_NAME_REGEX, item) return match.group(0) if match else item def missionplanner_sort(item: str) -> tuple[str, ...]: - """ - MissionPlanner parameter sorting function - """ + """MissionPlanner parameter sorting function.""" # Split the parameter name by underscore parts = extract_parameter_name(item).split("_") # Compare the parts separately @@ -562,12 +597,18 @@ def missionplanner_sort(item: str) -> tuple[str, ...]: def extract_parameter_name_and_validate(line: str, filename: str, line_nr: int) -> str: """ Extracts the parameter name from a line and validates it. + Args: line (str): The line to extract the parameter name from. + filename (str): filename. + line_nr (int): line number. + Returns: str: The extracted parameter name. + Raises: SystemExit: If the line is invalid or the parameter name is too long or invalid. + """ # Extract the parameter name match = re.match(PARAM_NAME_REGEX, line) @@ -575,15 +616,18 @@ def extract_parameter_name_and_validate(line: str, filename: str, line_nr: int) param_name = match.group(0) else: logging.critical("Invalid line %d in file %s: %s", line_nr, filename, line) - raise SystemExit("Invalid line in input file") + msg = "Invalid line in input file" + raise SystemExit(msg) param_len = len(param_name) param_sep = line[param_len] # the character following the parameter name must be a separator if param_sep not in {",", " ", "\t"}: logging.critical("Invalid parameter name %s on line %d in file %s", param_name, line_nr, filename) - raise SystemExit("Invalid parameter name") + msg = "Invalid parameter name" + raise SystemExit(msg) if param_len > PARAM_NAME_MAX_LEN: logging.critical("Too long parameter name on line %d in file %s", line_nr, filename) - raise SystemExit("Too long parameter name") + msg = "Too long parameter name" + raise SystemExit(msg) return param_name @@ -592,7 +636,7 @@ def update_parameter_documentation( target: str = ".", sort_type: str = "none", param_default_dict: Optional[dict] = None, - delete_documentation_annotations=False, + delete_documentation_annotations: bool = False, ) -> None: """ Updates the parameter documentation in the target file or in all *.param,*.parm files of the target directory. @@ -611,6 +655,8 @@ def update_parameter_documentation( Can be 'none', 'missionplanner', or 'mavproxy'. Defaults to 'none'. param_default_dict (Dict, optional): A dictionary of default parameter values. Defaults to None. If None, an empty dictionary is used. + delete_documentation_annotations (bool): delete documentation annotations from file. + """ # Check if the target is a file or a directory if os_path.isfile(target): @@ -620,7 +666,8 @@ def update_parameter_documentation( # If it's a directory, process all .param and .parm files in that directory param_files = glob.glob(os_path.join(target, "*.param")) + glob.glob(os_path.join(target, "*.parm")) else: - raise ValueError(f"Target '{target}' is neither a file nor a directory.") + msg = f"Target '{target}' is neither a file nor a directory." + raise ValueError(msg) if param_default_dict is None: param_default_dict = {} @@ -640,11 +687,11 @@ def update_parameter_documentation( def update_parameter_documentation_file( # pylint: disable=too-many-locals, too-many-arguments, too-many-positional-arguments - doc, - sort_type, - param_default_dict, - param_file, - lines, + doc: dict, + sort_type: str, + param_default_dict: dict, + param_file: str, + lines: list[str], delete_documentation_annotations: bool, ) -> None: new_lines = [] @@ -703,12 +750,13 @@ def update_parameter_documentation_file( # pylint: disable=too-many-locals, too file.writelines(new_lines) -def print_read_only_params(doc) -> None: +def print_read_only_params(doc: dict) -> None: """ Print the names of read-only parameters. Args: doc (dict): A dictionary of parameter documentation. + """ logging.info("ReadOnly parameters:") for param_name, param_value in doc.items(): @@ -736,7 +784,8 @@ def get_xml_url(vehicle_type: str, firmware_version: str) -> str: try: vehicle_subdir = vehicle_parm_subdir[vehicle_type] + firmware_version except KeyError as e: - raise ValueError(f"Vehicle type '{vehicle_type}' is not supported.") from e + msg = f"Vehicle type '{vehicle_type}' is not supported." + raise ValueError(msg) from e xml_url = BASE_URL xml_url += vehicle_subdir if firmware_version else vehicle_type diff --git a/MethodicConfigurator/ardupilot_methodic_configurator.py b/MethodicConfigurator/ardupilot_methodic_configurator.py index c681ae1..ecc52f8 100755 --- a/MethodicConfigurator/ardupilot_methodic_configurator.py +++ b/MethodicConfigurator/ardupilot_methodic_configurator.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +The main application file. calls four sub-applications. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -15,6 +17,7 @@ from logging import getLevelName as logging_getLevelName from logging import info as logging_info from sys import exit as sys_exit +from typing import Union from webbrowser import open as webbrowser_open from MethodicConfigurator import _, __version__ @@ -38,6 +41,7 @@ def argument_parser() -> argparse.Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ parser = argparse.ArgumentParser( description=_( @@ -60,7 +64,7 @@ def argument_parser() -> argparse.Namespace: return add_common_arguments_and_parse(parser) -def connect_to_fc_and_read_parameters(args) -> tuple[FlightController, str]: +def connect_to_fc_and_read_parameters(args: argparse.Namespace) -> tuple[FlightController, str]: flight_controller = FlightController(args.reboot_time) error_str = flight_controller.connect(args.device, log_errors=False) @@ -86,7 +90,7 @@ def component_editor( flight_controller: FlightController, vehicle_type: str, local_filesystem: LocalFilesystem, - vehicle_dir_window, + vehicle_dir_window: Union[None, VehicleDirectorySelectionWindow], ) -> None: component_editor_window = ComponentEditorWindow(__version__, local_filesystem) if ( diff --git a/MethodicConfigurator/argparse_check_range.py b/MethodicConfigurator/argparse_check_range.py index 8f7217f..88a4f59 100644 --- a/MethodicConfigurator/argparse_check_range.py +++ b/MethodicConfigurator/argparse_check_range.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Check the range of an Argparse parameter. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Dmitriy Kovalev @@ -11,15 +11,15 @@ """ from argparse import Action, ArgumentError, ArgumentParser, Namespace +from collections.abc import Sequence from operator import ge, gt, le, lt +from typing import Any, Union from MethodicConfigurator import _ class CheckRange(Action): - """ - Check if the Argparse argument value is within the specified range - """ + """Check if the Argparse argument value is within the specified range.""" def __init__(self, *args, **kwargs) -> None: if "min" in kwargs and "inf" in kwargs: @@ -52,7 +52,13 @@ def interval(self) -> str: msg = _("valid range: {_lo}, {_up}") return msg.format(**locals()) - def __call__(self, parser: ArgumentParser, namespace: Namespace, values, option_string=None) -> None: + def __call__( + self, + parser: ArgumentParser, # noqa: ARG002 + namespace: Namespace, + values: Union[str, Sequence[Any], None], + option_string: Union[None, str] = None, # noqa: ARG002 + ) -> None: if not isinstance(values, (int, float)): raise ArgumentError(self, _("Value must be a number.")) diff --git a/MethodicConfigurator/backend_filesystem.py b/MethodicConfigurator/backend_filesystem.py index 8db0018..0c8bc2a 100644 --- a/MethodicConfigurator/backend_filesystem.py +++ b/MethodicConfigurator/backend_filesystem.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Filesystem operations. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -54,14 +54,15 @@ def is_within_tolerance(x: float, y: float, atol: float = 1e-08, rtol: float = 1 the sum of the absolute tolerance (`atol`) and the product of the relative tolerance (`rtol`) and the absolute value of `y`. - Parameters: - - x (float): The first number to compare. - - y (float): The second number to compare. - - atol (float, optional): The absolute tolerance. Defaults to 1e-08. - - rtol (float, optional): The relative tolerance. Defaults to 1e-03. + Args: + x (float): The first number to compare. + y (float): The second number to compare. + atol (float, optional): The absolute tolerance. Defaults to 1e-08. + rtol (float, optional): The relative tolerance. Defaults to 1e-03. Returns: - - bool: True if the difference is within the tolerance, False otherwise. + bool: True if the difference is within the tolerance, False otherwise. + """ return abs(x - y) <= atol + (rtol * abs(y)) @@ -74,12 +75,13 @@ class LocalFilesystem(VehicleComponents, ConfigurationSteps, ProgramSettings): reading parameters from files, and handling configuration steps. It is designed to simplify the interaction with the local filesystem for managing ArduPilot configuration files. - Attributes: + Args: vehicle_dir (str): The directory path where the vehicle configuration files are stored. vehicle_type (str): The type of the vehicle (e.g., "ArduCopter", "Rover"). file_parameters (dict): A dictionary of parameters read from intermediate parameter files. param_default_dict (dict): A dictionary of default parameter values. doc_dict (dict): A dictionary containing documentation for each parameter. + """ def __init__(self, vehicle_dir: str, vehicle_type: str, fw_version: str, allow_editing_template_files: bool) -> None: @@ -227,6 +229,7 @@ def read_params_from_files(self) -> dict[str, dict[str, "Par"]]: Returns: - Dict[str, Dict[str, 'Par']]: A dictionary with filenames as keys and as values a dictionary with (parameter names, values) pairs. + """ parameters: dict[str, dict[str, Par]] = {} if os_path.isdir(self.vehicle_dir): @@ -243,18 +246,19 @@ def read_params_from_files(self) -> dict[str, dict[str, "Par"]]: return parameters @staticmethod - def str_to_bool(s) -> Optional[bool]: + def str_to_bool(s: str) -> Optional[bool]: """ Converts a string representation of a boolean value to a boolean. This function interprets the string 'true', 'yes', '1' as True, and 'false', 'no', '0' as False. Any other input will return None. - Parameters: - - s (str): The string to convert. + Args: + s (str): The string to convert. Returns: - - Optional[bool]: True, False, or None if the string does not match any known boolean representation. + Optional[bool]: True, False, or None if the string does not match any known boolean representation. + """ if s.lower() == "true" or s.lower() == "yes" or s.lower() == "1": return True @@ -269,10 +273,11 @@ def export_to_param(self, params: dict[str, "Par"], filename_out: str, annotate_ This function formats the provided parameters into a string suitable for a .param file, writes the string to the specified output file, and optionally updates the parameter documentation. - Parameters: - - params (Dict[str, 'Par']): A dictionary of parameters to export. - - filename_out (str): The name of the output file. - - annotate_doc (bool, optional): Whether to update the parameter documentation. Defaults to True. + Args: + params (Dict[str, 'Par']): A dictionary of parameters to export. + filename_out (str): The name of the output file. + annotate_doc (bool, optional): Whether to update the parameter documentation. Defaults to True. + """ Par.export_to_param(Par.format_params(params), os_path.join(self.vehicle_dir, filename_out)) if annotate_doc: @@ -284,11 +289,12 @@ def vehicle_configuration_file_exists(self, filename: str) -> bool: """ Check if a vehicle configuration file exists in the vehicle directory. - Parameters: - - filename (str): The name of the file to check. + Args: + filename (str): The name of the file to check. Returns: - - bool: True if the file exists and is a file (not a directory), False otherwise. + bool: True if the file exists and is a file (not a directory), False otherwise. + """ return os_path.exists(os_path.join(self.vehicle_dir, filename)) and os_path.isfile( os_path.join(self.vehicle_dir, filename) @@ -304,6 +310,7 @@ def __all_intermediate_parameter_file_comments(self) -> dict[str, str]: Returns: - Dict[str, str]: A dictionary mapping parameter names to their comments. + """ ret = {} for params in self.file_parameters.values(): @@ -320,11 +327,12 @@ def annotate_intermediate_comments_to_param_dict(self, param_dict: dict[str, flo intermediate parameter files to create a new dictionary where each parameter is represented by a 'Par' object containing both the value and the comment. - Parameters: - - param_dict (Dict[str, float]): A dictionary of parameters with only values. + Args: + param_dict (Dict[str, float]): A dictionary of parameters with only values. Returns: - - Dict[str, 'Par']: A dictionary of parameters with intermediate parameter file comments. + Dict[str, 'Par']: A dictionary of parameters with intermediate parameter file comments. + """ ret = {} ip_comments = self.__all_intermediate_parameter_file_comments() @@ -341,12 +349,13 @@ def categorize_parameters(self, param: dict[str, "Par"]) -> tuple[dict[str, "Par - Non-default, writable calibrations - Non-default, writable non-calibrations - Parameters: - - param (Dict[str, 'Par']): A dictionary mapping parameter names to their 'Par' objects. + Args: + param (Dict[str, 'Par']): A dictionary mapping parameter names to their 'Par' objects. Returns: - - Tuple[Dict[str, "Par"], Dict[str, "Par"], Dict[str, "Par"]]: A tuple of three dictionaries. + Tuple[Dict[str, "Par"], Dict[str, "Par"], Dict[str, "Par"]]: A tuple of three dictionaries. Each dictionary represents one of the categories mentioned above. + """ non_default__read_only_params = {} non_default__writable_calibrations = {} @@ -374,9 +383,7 @@ def get_directory_name_from_full_path(full_path: str) -> str: normalized_path = os_path.normpath(full_path) # Split the path into head and tail, then get the basename of the tail - directory_name = os_path.basename(os_path.split(normalized_path)[1]) - - return directory_name + return os_path.basename(os_path.split(normalized_path)[1]) # Extract the vehicle name from the directory path def get_vehicle_directory_name(self) -> str: @@ -390,7 +397,7 @@ def zip_file_exists(self) -> bool: zip_file_path = self.zip_file_path() return os_path.exists(zip_file_path) and os_path.isfile(zip_file_path) - def add_configuration_file_to_zip(self, zipf, filename) -> None: + def add_configuration_file_to_zip(self, zipf: ZipFile, filename: str) -> None: if self.vehicle_configuration_file_exists(filename): zipf.write(os_path.join(self.vehicle_dir, filename), arcname=filename) @@ -403,9 +410,10 @@ def zip_files(self, files_to_zip: list[tuple[bool, str]]) -> None: intermediate parameter files. The method checks for the existence of each file before attempting to add it to the zip archive. - Parameters: - - files_to_zip (List[Tuple[bool, str]]): A list of tuples, where each tuple contains a boolean + Args: + files_to_zip (List[Tuple[bool, str]]): A list of tuples, where each tuple contains a boolean indicating if the file was written and a string for the filename. + """ zip_file_path = self.zip_file_path() with ZipFile(zip_file_path, "w") as zipf: diff --git a/MethodicConfigurator/backend_filesystem_configuration_steps.py b/MethodicConfigurator/backend_filesystem_configuration_steps.py index f457f9c..44c08d1 100644 --- a/MethodicConfigurator/backend_filesystem_configuration_steps.py +++ b/MethodicConfigurator/backend_filesystem_configuration_steps.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Manages configuration steps at the filesystem level. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -32,6 +32,7 @@ class ConfigurationSteps: Attributes: configuration_steps_filename (str): The name of the file containing documentation for the configuration files. configuration_steps (dict): A dictionary containing the configuration steps. + """ def __init__(self, _vehicle_dir: str, vehicle_type: str) -> None: diff --git a/MethodicConfigurator/backend_filesystem_program_settings.py b/MethodicConfigurator/backend_filesystem_program_settings.py index dd802a0..ba1930d 100644 --- a/MethodicConfigurator/backend_filesystem_program_settings.py +++ b/MethodicConfigurator/backend_filesystem_program_settings.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Manages program settings at the filesystem level. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -79,11 +79,12 @@ def valid_directory_name(dir_name: str) -> bool: invalid for directory names in many operating systems. It does not guarantee that the name is valid in all contexts or operating systems, as directory name validity can vary. - Parameters: - - dir_name (str): The directory name to check. + Args: + dir_name (str): The directory name to check. Returns: - - bool: True if the directory name matches the allowed pattern, False otherwise. + bool: True if the directory name matches the allowed pattern, False otherwise. + """ # Include os.sep in the pattern pattern = r"^[\w" + re_escape(os_sep) + "-]+$" @@ -100,7 +101,7 @@ def __user_config_dir() -> str: error_msg = _("The path '{user_config_directory}' is not a directory.") raise NotADirectoryError(error_msg.format(**locals())) - return user_config_directory # type: ignore[no-any-return] # workaround a mypy bug + return user_config_directory @staticmethod def __site_config_dir() -> str: @@ -115,7 +116,7 @@ def __site_config_dir() -> str: error_msg = _("The path '{site_config_directory}' is not a directory.") raise NotADirectoryError(error_msg.format(**locals())) - return site_config_directory # type: ignore[no-any-return] # workaround a mypy bug + return site_config_directory @staticmethod def __get_settings_as_dict() -> dict[str, Any]: @@ -151,7 +152,7 @@ def __get_settings_as_dict() -> dict[str, Any]: return settings @staticmethod - def __set_settings_from_dict(settings) -> None: + def __set_settings_from_dict(settings: dict) -> None: settings_path = os_path.join(ProgramSettings.__user_config_dir(), "settings.json") with open(settings_path, "w", encoding="utf-8") as settings_file: diff --git a/MethodicConfigurator/backend_filesystem_vehicle_components.py b/MethodicConfigurator/backend_filesystem_vehicle_components.py index 138431b..d04a241 100644 --- a/MethodicConfigurator/backend_filesystem_vehicle_components.py +++ b/MethodicConfigurator/backend_filesystem_vehicle_components.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Manages vehicle components at the filesystem level. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -52,7 +52,7 @@ def load_vehicle_components_json_data(self, vehicle_dir: str) -> dict[Any, Any]: self.vehicle_components = data return data - def save_vehicle_components_json_data(self, data, vehicle_dir: str) -> bool: + def save_vehicle_components_json_data(self, data: dict, vehicle_dir: str) -> bool: filepath = os_path.join(vehicle_dir, self.vehicle_components_json_filename) try: with open(filepath, "w", encoding="utf-8") as file: diff --git a/MethodicConfigurator/backend_flightcontroller.py b/MethodicConfigurator/backend_flightcontroller.py index 8d0b1ae..88775de 100644 --- a/MethodicConfigurator/backend_flightcontroller.py +++ b/MethodicConfigurator/backend_flightcontroller.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Flight controller interface. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -18,13 +18,12 @@ from os import readlink as os_readlink from time import sleep as time_sleep from time import time as time_time -from typing import NoReturn, Optional, Union +from typing import Callable, NoReturn, Optional, Union import serial.tools.list_ports import serial.tools.list_ports_common - -# import pymavlink.dialects.v20.ardupilotmega from pymavlink import mavutil +from pymavlink.dialects.v20.ardupilotmega import MAVLink_autopilot_version_message from serial.serialutil import SerialException from MethodicConfigurator import _ @@ -47,11 +46,12 @@ class FakeSerialForUnitTests: def __init__(self, device: str) -> None: self.device = device - def read(self, _len) -> str: + def read(self, _len) -> str: # noqa: ANN001 return "" - def write(self, _buf) -> NoReturn: - raise Exception("write always fails") # pylint: disable=broad-exception-raised + def write(self, _buf) -> NoReturn: # noqa: ANN001 + msg = "write always fails" + raise Exception(msg) # pylint: disable=broad-exception-raised def inWaiting(self) -> int: # pylint: disable=invalid-name return 0 @@ -68,13 +68,11 @@ class FlightController: device (str): The connection string to the flight controller. master (mavutil.mavlink_connection): The MAVLink connection object. fc_parameters (Dict[str, float]): A dictionary of flight controller parameters. + """ def __init__(self, reboot_time: int) -> None: - """ - Initialize the FlightController communication object. - - """ + """Initialize the FlightController communication object.""" # warn people about ModemManager which interferes badly with ArduPilot if os_path.exists("/usr/sbin/ModemManager"): logging_warning(_("You should uninstall ModemManager as it conflicts with ArduPilot")) @@ -99,9 +97,7 @@ def discover_connections(self) -> None: self.__connection_tuples += [(_("Add another"), _("Add another"))] def disconnect(self) -> None: - """ - Close the connection to the flight controller. - """ + """Close the connection to the flight controller.""" if self.master is not None: self.master.close() self.master = None @@ -109,9 +105,7 @@ def disconnect(self) -> None: self.info = BackendFlightcontrollerInfo() def add_connection(self, connection_string: str) -> bool: - """ - Add a new connection to the list of available connections. - """ + """Add a new connection to the list of available connections.""" if connection_string: # Check if connection_string is not the first element of any tuple in self.other_connection_tuples if all(connection_string != t[0] for t in self.__connection_tuples): @@ -123,7 +117,9 @@ def add_connection(self, connection_string: str) -> bool: logging_debug(_("Did not add empty connection")) return False - def connect(self, device: str, progress_callback=None, log_errors: bool = True) -> str: + def connect( + self, device: str, progress_callback: Union[None, Callable[[int, int], None]] = None, log_errors: bool = True + ) -> str: """ Establishes a connection to the FlightController using a specified device. @@ -137,10 +133,12 @@ def connect(self, device: str, progress_callback=None, log_errors: bool = True) is provided, the method attempts to auto-detect a serial port. progress_callback (callable, optional): A callback function to report the progress of the connection attempt. Defaults to None. + log_errors: log errors Returns: str: An error message if the connection fails, otherwise an empty string indicating a successful connection. + """ if device: if device == "none": @@ -172,7 +170,7 @@ def connect(self, device: str, progress_callback=None, log_errors: bool = True) return self.__create_connection_with_retry(progress_callback=progress_callback, log_errors=log_errors) def __request_banner(self) -> None: - """Request banner information from the flight controller""" + """Request banner information from the flight controller.""" # https://mavlink.io/en/messages/ardupilotmega.html#MAV_CMD_DO_SEND_BANNER if self.master is not None: self.master.mav.command_long_send( @@ -222,7 +220,11 @@ def __request_message(self, message_id: int) -> None: ) def __create_connection_with_retry( - self, progress_callback, retries: int = 3, timeout: int = 5, log_errors: bool = True + self, + progress_callback: Union[None, Callable[[int, int], None]], + retries: int = 3, + timeout: int = 5, + log_errors: bool = True, ) -> str: """ Attempts to create a connection to the flight controller with retries. @@ -237,10 +239,12 @@ def __create_connection_with_retry( of the connection attempt. Defaults to None. retries (int, optional): The number of retries before giving up. Defaults to 3. timeout (int, optional): The timeout in seconds for each connection attempt. Defaults to 5. + log_errors (bool): log errors. Returns: str: An error message if the connection fails after all retries, otherwise an empty string indicating a successful connection. + """ if self.comport is None or self.comport.device == "test": # FIXME for testing only pylint: disable=fixme return "" @@ -252,7 +256,8 @@ def __create_connection_with_retry( ) logging_debug(_("Waiting for MAVLink heartbeat")) if not self.master: - raise ConnectionError(f"Failed to create mavlink connect to {self.comport.device}") + msg = f"Failed to create mavlink connect to {self.comport.device}" + raise ConnectionError(msg) m = self.master.wait_heartbeat(timeout=timeout) if m is None: return _("No MAVLink heartbeat received, connection failed.") @@ -285,7 +290,7 @@ def __create_connection_with_retry( logging_error(_("Failed to connect after %d attempts."), retries) return str(e) - def __process_autopilot_version(self, m, banner_msgs) -> str: + def __process_autopilot_version(self, m: MAVLink_autopilot_version_message, banner_msgs: list[str]) -> str: if m is None: return _( "No AUTOPILOT_VERSION MAVLink message received, connection failed.\n" @@ -325,13 +330,16 @@ def __process_autopilot_version(self, m, banner_msgs) -> str: self.info.product = fc_product # force the one from the banner because it is more reliable return "" - def download_params(self, progress_callback=None) -> tuple[dict[str, float], dict[str, "Par"]]: + def download_params( + self, progress_callback: Union[None, Callable[[int, int], None]] = None + ) -> tuple[dict[str, float], dict[str, "Par"]]: """ Requests all flight controller parameters from a MAVLink connection. Returns: Dict[str, float]: A dictionary of flight controller parameters. Dict[str, Par]: A dictionary of flight controller default parameters. + """ # FIXME this entire if statement is for testing only, remove it later pylint: disable=fixme if self.master is None and self.comport is not None and self.comport.device == "test": @@ -353,7 +361,9 @@ def download_params(self, progress_callback=None) -> tuple[dict[str, float], dic logging_info(_("MAVFTP is not supported by the %s flight controller, fallback to MAVLink"), comport_device) return self.__download_params_via_mavlink(progress_callback), {} - def __download_params_via_mavlink(self, progress_callback=None) -> dict[str, float]: + def __download_params_via_mavlink( + self, progress_callback: Union[None, Callable[[int, int], None]] = None + ) -> dict[str, float]: comport_device = getattr(self.comport, "device", "") logging_debug(_("Will fetch all parameters from the %s flight controller"), comport_device) @@ -390,7 +400,9 @@ def __download_params_via_mavlink(self, progress_callback=None) -> dict[str, flo break return parameters - def download_params_via_mavftp(self, progress_callback=None) -> tuple[dict[str, float], dict[str, "Par"]]: + def download_params_via_mavftp( + self, progress_callback: Union[None, Callable[[int, int], None]] = None + ) -> tuple[dict[str, float], dict[str, "Par"]]: if self.master is None: return {}, {} mavftp = MAVFTP(self.master, target_system=self.master.target_system, target_component=self.master.target_component) @@ -423,19 +435,26 @@ def set_param(self, param_name: str, param_value: float) -> None: Args: param_name (str): The name of the parameter to set. param_value (float): The value to set the parameter to. + """ if self.master is None: # FIXME for testing only pylint: disable=fixme return self.master.param_set_send(param_name, param_value) def reset_and_reconnect( - self, reset_progress_callback=None, connection_progress_callback=None, extra_sleep_time: Optional[int] = None + self, + reset_progress_callback: Union[None, Callable[[int, int], None]] = None, + connection_progress_callback: Union[None, Callable[[int, int], None]] = None, + extra_sleep_time: Optional[int] = None, ) -> str: """ Reset the flight controller and reconnect. Args: - sleep_time (int, optional): The time in seconds to wait before reconnecting. + reset_progress_callback: reset callback function + connection_progress_callback: connection callback function + extra_sleep_time (int, optional): The time in seconds to wait before reconnecting. + """ if self.master is None: # FIXME for testing only pylint: disable=fixme return "" @@ -471,19 +490,15 @@ def reset_and_reconnect( @staticmethod def __list_serial_ports() -> list[serial.tools.list_ports_common.ListPortInfo]: - """ - List all available serial ports. - """ + """List all available serial ports.""" comports = serial.tools.list_ports.comports() - # for port in comports: - # logging_debug("ComPort - %s, Description: %s", port.device, port.description) + for port in comports: + logging_debug("ComPort - %s, Description: %s", port.device, port.description) return comports # type: ignore[no-any-return] @staticmethod def __list_network_ports() -> list[str]: - """ - List all available network ports. - """ + """List all available network ports.""" return ["tcp:127.0.0.1:5760", "udp:127.0.0.1:14550"] # pylint: disable=duplicate-code @@ -529,12 +544,12 @@ def __auto_detect_serial(self) -> list[mavutil.SerialPort]: # pylint: enable=duplicate-code def get_connection_tuples(self) -> list[tuple[str, str]]: - """ - Get all available connections. - """ + """Get all available connections.""" return self.__connection_tuples - def upload_file(self, local_filename: str, remote_filename: str, progress_callback=None) -> bool: + def upload_file( + self, local_filename: str, remote_filename: str, progress_callback: Union[None, Callable[[int, int], None]] = None + ) -> bool: """Upload a file to the flight controller.""" if self.master is None: return False diff --git a/MethodicConfigurator/backend_flightcontroller_info.py b/MethodicConfigurator/backend_flightcontroller_info.py index 2b4b1cd..43f15ab 100644 --- a/MethodicConfigurator/backend_flightcontroller_info.py +++ b/MethodicConfigurator/backend_flightcontroller_info.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Manages FC information using FC interface. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -8,6 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later """ +from collections.abc import Sequence from typing import Any, Union from pymavlink import mavutil @@ -61,33 +62,33 @@ def get_info(self) -> dict[str, Union[str, dict[str, str]]]: "Component ID": self.component_id, } - def set_system_id_and_component_id(self, system_id, component_id) -> None: + def set_system_id_and_component_id(self, system_id: str, component_id: str) -> None: self.system_id = system_id self.component_id = component_id - def set_autopilot(self, autopilot) -> None: + def set_autopilot(self, autopilot: int) -> None: self.autopilot = self.__decode_mav_autopilot(autopilot) self.is_supported = autopilot == mavutil.mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA - def set_type(self, mav_type) -> None: + def set_type(self, mav_type: int) -> None: self.vehicle_type = self.__classify_vehicle_type(mav_type) self.mav_type = self.__decode_mav_type(mav_type) - def set_flight_sw_version(self, version) -> None: + def set_flight_sw_version(self, version: int) -> None: v_major, v_minor, v_patch, v_fw_type = self.__decode_flight_sw_version(version) self.flight_sw_version = f"{v_major}.{v_minor}.{v_patch}" self.flight_sw_version_and_type = self.flight_sw_version + " " + v_fw_type - def set_board_version(self, board_version) -> None: - self.board_version = board_version + def set_board_version(self, board_version: int) -> None: + self.board_version = str(board_version) - def set_flight_custom_version(self, flight_custom_version) -> None: + def set_flight_custom_version(self, flight_custom_version: Sequence[int]) -> None: self.flight_custom_version = "".join(chr(c) for c in flight_custom_version) - def set_os_custom_version(self, os_custom_version) -> None: + def set_os_custom_version(self, os_custom_version: Sequence[int]) -> None: self.os_custom_version = "".join(chr(c) for c in os_custom_version) - def set_vendor_id_and_product_id(self, vendor_id, product_id) -> None: + def set_vendor_id_and_product_id(self, vendor_id: int, product_id: int) -> None: pid_vid_dict = self.__list_ardupilot_supported_usb_pid_vid() self.vendor_id = f"0x{vendor_id:04X}" if vendor_id else "Unknown" @@ -104,14 +105,16 @@ def set_vendor_id_and_product_id(self, vendor_id, product_id) -> None: self.product = "Unknown" self.product_and_product_id = f"{self.product} ({self.product_id})" - def set_capabilities(self, capabilities) -> None: + def set_capabilities(self, capabilities: int) -> None: self.capabilities = self.__decode_flight_capabilities(capabilities) self.is_mavftp_supported = capabilities & mavutil.mavlink.MAV_PROTOCOL_CAPABILITY_FTP @staticmethod - def __decode_flight_sw_version(flight_sw_version) -> tuple[int, int, int, str]: - """decode 32 bit flight_sw_version mavlink parameter - corresponds to ArduPilot encoding in GCS_MAVLINK::send_autopilot_version""" + def __decode_flight_sw_version(flight_sw_version: int) -> tuple[int, int, int, str]: + """ + decode 32 bit flight_sw_version mavlink parameter + corresponds to ArduPilot encoding in GCS_MAVLINK::send_autopilot_version. + """ fw_type_id = (flight_sw_version >> 0) % 256 # E221, E222 patch = (flight_sw_version >> 8) % 256 # E221, E222 minor = (flight_sw_version >> 16) % 256 # E221 @@ -131,8 +134,9 @@ def __decode_flight_sw_version(flight_sw_version) -> tuple[int, int, int, str]: return major, minor, patch, fw_type @staticmethod - def __decode_flight_capabilities(capabilities) -> dict[str, str]: - """Decode 32 bit flight controller capabilities bitmask mavlink parameter. + def __decode_flight_capabilities(capabilities: int) -> dict[str, str]: + """ + Decode 32 bit flight controller capabilities bitmask mavlink parameter. Returns a dict of concise English descriptions of each active capability. """ capabilities_dict: dict[str, str] = {} @@ -156,13 +160,13 @@ def __decode_flight_capabilities(capabilities) -> dict[str, str]: # import pymavlink.dialects.v20.ardupilotmega # pymavlink.dialects.v20.ardupilotmega.enums["MAV_TYPE"] @staticmethod - def __decode_mav_type(mav_type) -> str: + def __decode_mav_type(mav_type: int) -> str: return str( mavutil.mavlink.enums["MAV_TYPE"].get(mav_type, mavutil.mavlink.EnumEntry("None", "Unknown type")).description ) @staticmethod - def __decode_mav_autopilot(mav_autopilot) -> str: + def __decode_mav_autopilot(mav_autopilot: int) -> str: return str( mavutil.mavlink.enums["MAV_AUTOPILOT"] .get(mav_autopilot, mavutil.mavlink.EnumEntry("None", "Unknown type")) @@ -170,18 +174,19 @@ def __decode_mav_autopilot(mav_autopilot) -> str: ) @staticmethod - def __classify_vehicle_type(mav_type_int) -> str: + def __classify_vehicle_type(mav_type_int: int) -> str: """ Classify the vehicle type based on the MAV_TYPE enum. - Parameters: - mav_type_int (int): The MAV_TYPE enum value. + Args: + mav_type_int (int): The MAV_TYPE enum value. Returns: - str: The classified vehicle type. + str: The classified vehicle type. + """ # Define the mapping from MAV_TYPE_* integer to vehicle type category - mav_type_to_vehicle_type = { + mav_type_to_vehicle_type: dict[int, str] = { mavutil.mavlink.MAV_TYPE_FIXED_WING: "ArduPlane", mavutil.mavlink.MAV_TYPE_QUADROTOR: "ArduCopter", mavutil.mavlink.MAV_TYPE_COAXIAL: "Heli", diff --git a/MethodicConfigurator/backend_mavftp.py b/MethodicConfigurator/backend_mavftp.py old mode 100644 new mode 100755 index 9120759..f569f81 --- a/MethodicConfigurator/backend_mavftp.py +++ b/MethodicConfigurator/backend_mavftp.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """ -MAVLink File Transfer Protocol support - https://mavlink.io/en/services/ftp.html +MAVLink File Transfer Protocol support - https://mavlink.io/en/services/ftp.html - duplicated from MACProxy code. + Original from MAVProxy/MAVProxy/modules/mavproxy_ftp.py This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator @@ -108,16 +109,16 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen self.payload = payload # (bytes) Payload for the operation. def pack(self) -> bytearray: - """pack message""" + """Pack message.""" ret = struct.pack( " str: + """String representation of the FTP_OP.""" plen = 0 if self.payload is not None: plen = len(self.payload) @@ -155,9 +156,7 @@ def __init__(self, ofs: int, size: int) -> None: class ParamData: - """ - A class to manage parameter values and defaults for ArduPilot configuration. - """ + """A class to manage parameter values and defaults for ArduPilot configuration.""" def __init__(self) -> None: self.params: list[tuple[str, float, type]] = [] # params as (name, value, ptype) @@ -199,12 +198,14 @@ def append(self, v) -> None: self._vars[setting.name] = setting def __getattr__(self, name) -> Union[int, float]: + """Get attribute.""" try: - return self._vars[name].value # type: ignore + return self._vars[name].value # type: ignore[no-any-return] except Exception as exc: raise AttributeError from exc def __setattr__(self, name, value) -> None: + """Set attribute.""" if name[0] == "_": self.__dict__[name] = value return @@ -362,8 +363,8 @@ def __init__( self.__send(FTP_OP(self.seq, self.session, OP_ResetSessions, 0, 0, 0, 0, None)) self.process_ftp_reply("ResetSessions") - def cmd_ftp(self, args) -> MAVFTPReturn: # noqa PRL0911 pylint: disable=too-many-return-statements, too-many-branches - """FTP operations""" + def cmd_ftp(self, args) -> MAVFTPReturn: # noqa PRL0911, pylint: disable=too-many-return-statements, too-many-branches + """FTP operations.""" usage = "Usage: ftp " if len(args) < 1: logging.error(usage) @@ -397,7 +398,7 @@ def cmd_ftp(self, args) -> MAVFTPReturn: # noqa PRL0911 pylint: disable=too-man return MAVFTPReturn("FTP command", ERR_InvalidArguments) def __send(self, op) -> None: - """send a request""" + """Send a request.""" op.seq = self.seq payload = op.pack() plen = len(payload) @@ -413,7 +414,7 @@ def __send(self, op) -> None: self.last_send_time = now def __terminate_session(self) -> None: - """terminate current session""" + """Terminate current session.""" self.__send(FTP_OP(self.seq, self.session, OP_TerminateSession, 0, 0, 0, 0, None)) self.fh = None self.filename = None @@ -446,7 +447,7 @@ def __terminate_session(self) -> None: self.session = (self.session + 1) % 256 def cmd_list(self, args) -> MAVFTPReturn: - """list files""" + """List files.""" if len(args) == 0: dname = "/" elif len(args) == 1: @@ -463,7 +464,7 @@ def cmd_list(self, args) -> MAVFTPReturn: return self.process_ftp_reply("ListDirectory") def __handle_list_reply(self, op, _m) -> MAVFTPReturn: - """handle OP_ListDirectory reply""" + """Handle OP_ListDirectory reply.""" if op.opcode == OP_Ack: dentries = sorted(op.payload.split(b"\x00")) # logging.info(dentries) @@ -496,7 +497,7 @@ def __handle_list_reply(self, op, _m) -> MAVFTPReturn: return MAVFTPReturn("ListDirectory", ERR_None) def cmd_get(self, args, callback=None, progress_callback=None) -> MAVFTPReturn: - """get file""" + """Get file.""" if len(args) == 0 or len(args) > 2: logging.error("Usage: get [FILENAME ]") return MAVFTPReturn("OpenFileRO", ERR_InvalidArguments) @@ -524,7 +525,7 @@ def cmd_get(self, args, callback=None, progress_callback=None) -> MAVFTPReturn: return MAVFTPReturn("OpenFileRO", ERR_None) def __handle_open_ro_reply(self, op, _m) -> MAVFTPReturn: - """handle OP_OpenFileRO reply""" + """Handle OP_OpenFileRO reply.""" if op.opcode == OP_Ack: if self.filename is None: return MAVFTPReturn("OpenFileRO", ERR_FileNotFound) @@ -555,7 +556,7 @@ def __handle_open_ro_reply(self, op, _m) -> MAVFTPReturn: return ret def __check_read_finished(self) -> bool: - """check if download has completed""" + """Check if download has completed.""" # logging.debug("FTP: check_read_finished: %s %s", self.reached_eof, self.read_gaps) if self.reached_eof and len(self.read_gaps) == 0: ofs = self.fh.tell() @@ -576,15 +577,15 @@ def __check_read_finished(self) -> bool: return False def __write_payload(self, op) -> None: - """write payload from a read op""" + """Write payload from a read op.""" self.fh.seek(op.offset) self.fh.write(op.payload) self.read_total += len(op.payload) if self.callback_progress is not None and self.remote_file_size: self.callback_progress(self.read_total / self.remote_file_size) - def __handle_burst_read(self, op, _m) -> MAVFTPReturn: # noqa PRL0911 pylint: disable=too-many-branches, too-many-statements, too-many-return-statements - """handle OP_BurstReadFile reply""" + def __handle_burst_read(self, op, _m) -> MAVFTPReturn: # noqa PRL0911, PGH004, pylint: disable=too-many-branches, too-many-statements, too-many-return-statements + """Handle OP_BurstReadFile reply.""" if self.ftp_settings.pkt_loss_tx > 0 and random.uniform(0, 100) < self.ftp_settings.pkt_loss_tx: # noqa: S311 if self.ftp_settings.debug > 0: logging.warning("FTP: dropping TX") @@ -686,7 +687,7 @@ def __handle_burst_read(self, op, _m) -> MAVFTPReturn: # noqa PRL0911 pylint: d return MAVFTPReturn("BurstReadFile", ERR_Fail) def __handle_reply_read(self, op, _m) -> MAVFTPReturn: - """handle OP_ReadFile reply""" + """Handle OP_ReadFile reply.""" if self.fh is None or self.filename is None: if self.ftp_settings.debug > 0: logging.warning("FTP: Unexpected read reply") @@ -720,7 +721,7 @@ def __handle_reply_read(self, op, _m) -> MAVFTPReturn: return MAVFTPReturn("ReadFile", ERR_None) def cmd_put(self, args, fh=None, callback=None, progress_callback=None) -> MAVFTPReturn: - """put file""" + """Put file.""" if len(args) == 0 or len(args) > 2: logging.error("Usage: put [FILENAME ]") return MAVFTPReturn("CreateFile", ERR_InvalidArguments) @@ -773,7 +774,7 @@ def cmd_put(self, args, fh=None, callback=None, progress_callback=None) -> MAVFT return MAVFTPReturn("CreateFile", ERR_None) def __put_finished(self, flen) -> None: - """finish a put""" + """Finish a put.""" if self.put_callback_progress: self.put_callback_progress(1.0) self.put_callback_progress = None @@ -786,7 +787,7 @@ def __put_finished(self, flen) -> None: logging.info("Put %u bytes to %s file in %.2fs %.1fkByte/s", flen, self.filename, dt, rate) def __handle_create_file_reply(self, op, _m) -> MAVFTPReturn: - """handle OP_CreateFile reply""" + """Handle OP_CreateFile reply.""" if self.fh is None: self.__terminate_session() return MAVFTPReturn("CreateFile", ERR_FileNotFound) @@ -799,7 +800,7 @@ def __handle_create_file_reply(self, op, _m) -> MAVFTPReturn: return MAVFTPReturn("CreateFile", ERR_None) def __send_more_writes(self) -> None: - """send some more writes""" + """Send some more writes.""" if len(self.write_list) == 0: # all done self.__put_finished(self.write_file_size) @@ -827,7 +828,7 @@ def __send_more_writes(self) -> None: self.write_last_send = now def __handle_write_reply(self, op, _m) -> MAVFTPReturn: - """handle OP_WriteFile reply""" + """Handle OP_WriteFile reply.""" if self.fh is None: self.__terminate_session() return MAVFTPReturn("WriteFile", ERR_FileNotFound) @@ -852,7 +853,7 @@ def __handle_write_reply(self, op, _m) -> MAVFTPReturn: return MAVFTPReturn("WriteFile", ERR_None) def cmd_rm(self, args) -> MAVFTPReturn: - """remove file""" + """Remove file.""" if len(args) != 1: logging.error("Usage: rm [FILENAME]") return MAVFTPReturn("RemoveFile", ERR_InvalidArguments) @@ -864,7 +865,7 @@ def cmd_rm(self, args) -> MAVFTPReturn: return self.process_ftp_reply("RemoveFile") def cmd_rmdir(self, args) -> MAVFTPReturn: - """remove directory""" + """Remove directory.""" if len(args) != 1: logging.error("Usage: rmdir [DIRECTORYNAME]") return MAVFTPReturn("RemoveDirectory", ERR_InvalidArguments) @@ -876,11 +877,11 @@ def cmd_rmdir(self, args) -> MAVFTPReturn: return self.process_ftp_reply("RemoveDirectory") def __handle_remove_reply(self, op, _m) -> MAVFTPReturn: - """handle remove reply""" + """Handle remove reply.""" return self.__decode_ftp_ack_and_nack(op) def cmd_rename(self, args) -> MAVFTPReturn: - """rename file or directory""" + """Rename file or directory.""" if len(args) < 2: logging.error("Usage: rename [OLDNAME NEWNAME]") return MAVFTPReturn("Rename", ERR_InvalidArguments) @@ -895,11 +896,11 @@ def cmd_rename(self, args) -> MAVFTPReturn: return self.process_ftp_reply("Rename") def __handle_rename_reply(self, op, _m) -> MAVFTPReturn: - """handle rename reply""" + """Handle rename reply.""" return self.__decode_ftp_ack_and_nack(op) def cmd_mkdir(self, args) -> MAVFTPReturn: - """make directory""" + """Make directory.""" if len(args) != 1: logging.error("Usage: mkdir NAME") return MAVFTPReturn("CreateDirectory", ERR_InvalidArguments) @@ -911,11 +912,11 @@ def cmd_mkdir(self, args) -> MAVFTPReturn: return self.process_ftp_reply("CreateDirectory") def __handle_mkdir_reply(self, op, _m) -> MAVFTPReturn: - """handle mkdir reply""" + """Handle mkdir reply.""" return self.__decode_ftp_ack_and_nack(op) def cmd_crc(self, args) -> MAVFTPReturn: - """get file crc""" + """Get file crc.""" if len(args) != 1: logging.error("Usage: crc [NAME]") return MAVFTPReturn("CalcFileCRC32", ERR_InvalidArguments) @@ -929,7 +930,7 @@ def cmd_crc(self, args) -> MAVFTPReturn: return self.process_ftp_reply("CalcFileCRC32") def __handle_crc_reply(self, op, _m) -> MAVFTPReturn: - """handle crc reply""" + """Handle crc reply.""" if op.opcode == OP_Ack and op.size == 4: (crc,) = struct.unpack(" MAVFTPReturn: return self.__decode_ftp_ack_and_nack(op) def cmd_cancel(self) -> MAVFTPReturn: - """cancel any pending op""" + """Cancel any pending op.""" self.__terminate_session() return MAVFTPReturn("TerminateSession", ERR_None) def cmd_status(self) -> MAVFTPReturn: - """show status""" + """Show status.""" if self.fh is None: logging.info("No transfer in progress") else: @@ -959,14 +960,14 @@ def cmd_status(self) -> MAVFTPReturn: return MAVFTPReturn("Status", ERR_None) def __op_parse(self, m) -> FTP_OP: - """parse a FILE_TRANSFER_PROTOCOL msg""" + """Parse a FILE_TRANSFER_PROTOCOL msg.""" hdr = bytearray(m.payload[0:12]) (seq, session, opcode, size, req_opcode, burst_complete, _pad, offset) = struct.unpack(" MAVFTPReturn: # noqa PRL0911 pylint: disable=too-many-branches, too-many-return-statements - """handle a mavlink packet""" + def __mavlink_packet(self, m) -> MAVFTPReturn: # noqa PRL0911, PGH004, pylint: disable=too-many-branches, too-many-return-statements + """Handle a mavlink packet.""" operation_name = "mavlink_packet" mtype = m.get_type() if mtype != "FILE_TRANSFER_PROTOCOL": @@ -1026,7 +1027,7 @@ def __mavlink_packet(self, m) -> MAVFTPReturn: # noqa PRL0911 pylint: disable=t return MAVFTPReturn(operation_name, ERR_InvalidOpcode) def __send_gap_read(self, g) -> None: - """send a read for a gap""" + """Send a read for a gap.""" (offset, length) = g if self.ftp_settings.debug > 0: logging.info("FTP: Gap read of %u at %u rem=%u blog=%u", length, offset, len(self.read_gaps), self.backlog) @@ -1039,7 +1040,7 @@ def __send_gap_read(self, g) -> None: self.backlog += 1 def __check_read_send(self) -> None: - """see if we should send another gap read""" + """See if we should send another gap read.""" if len(self.read_gaps) == 0: return g = self.read_gaps[0] @@ -1068,7 +1069,7 @@ def __check_read_send(self) -> None: self.__send_gap_read(g) def __idle_task(self) -> bool: - """check for file gaps and lost requests""" + """Check for file gaps and lost requests.""" now = time.time() assert ( # noqa: S101 self.ftp_settings.idle_detection_time > self.ftp_settings.read_retry_time @@ -1126,11 +1127,11 @@ def __last_send_time_was_more_than_idle_detection_time_ago(self, now: float) -> return self.last_send_time is not None and now - self.last_send_time > float(self.ftp_settings.idle_detection_time) def __handle_reset_sessions_reply(self, op, _m) -> MAVFTPReturn: - """handle reset sessions reply""" + """Handle reset sessions reply.""" return self.__decode_ftp_ack_and_nack(op) def process_ftp_reply(self, operation_name, timeout=5) -> MAVFTPReturn: - """execute an FTP operation that requires processing a MAVLink response""" + """Execute an FTP operation that requires processing a MAVLink response.""" start_time = time.time() ret = MAVFTPReturn(operation_name, ERR_Fail) recv_timeout = 0.1 @@ -1156,7 +1157,7 @@ def process_ftp_reply(self, operation_name, timeout=5) -> MAVFTPReturn: return ret def __decode_ftp_ack_and_nack(self, op: FTP_OP, operation_name: str = "") -> MAVFTPReturn: - """decode FTP Acknowledge reply""" + """Decode FTP Acknowledge reply.""" system_error = 0 invalid_error_code = 0 operation_name_dict = { @@ -1221,7 +1222,7 @@ def __decode_ftp_ack_and_nack(self, op: FTP_OP, operation_name: str = "") -> MAV @staticmethod def ftp_param_decode(data) -> Union[None, ParamData]: # pylint: disable=too-many-locals - """decode parameter data, returning ParamData""" + """Decode parameter data, returning ParamData.""" pdata = ParamData() magic = 0x671B @@ -1297,14 +1298,12 @@ def ftp_param_decode(data) -> Union[None, ParamData]: # pylint: disable=too-man @staticmethod def missionplanner_sort(item: str) -> tuple[str, ...]: - """ - Sorts a parameter name according to the rules defined in the Mission Planner software. - """ + """Sorts a parameter name according to the rules defined in the Mission Planner software.""" return tuple(item.split("_")) @staticmethod def extract_params(pdata, sort_type) -> dict[str, tuple[float, str]]: - """extract parameter values to an optionally sorted dictionary of name->(value, type)""" + """Extract parameter values to an optionally sorted dictionary of name->(value, type).""" pdict = {} if pdata: for name, value, ptype in pdata: @@ -1326,7 +1325,7 @@ def save_params( add_datatype_comments: bool, add_timestamp_comment: bool, ) -> None: - """Save Ardupilot parameter information to a local file""" + """Save Ardupilot parameter information to a local file.""" if not pdict: return with open(filename, "w", encoding="utf-8") as f: @@ -1337,7 +1336,7 @@ def save_params( "4": "32-bit float", } if add_timestamp_comment: - f.write(f"# Parameters saved at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"# Parameters saved at {datetime.now(tz=None).strftime('%Y-%m-%d %H:%M:%S')}\n") for name, (value, datatype) in pdict.items(): if sort_type == "missionplanner": f.write(f"{name},{format(value, '.6f').rstrip('0').rstrip('.')}") @@ -1357,7 +1356,7 @@ def cmd_getparams( # pylint: disable=too-many-arguments, too-many-positional-ar add_datatype_comments: bool = False, add_timestamp_comment: bool = False, ) -> MAVFTPReturn: - """Decode the parameter file and save the values and defaults to disk""" + """Decode the parameter file and save the values and defaults to disk.""" def decode_and_save_params(fh) -> MAVFTPReturn: if fh is None: @@ -1586,13 +1585,13 @@ def auto_connect(device) -> mavutil.SerialPort: return comport def wait_heartbeat(m) -> None: - """wait for a heartbeat so we know the target system IDs""" + """Wait for a heartbeat so we know the target system IDs.""" logging.info("Waiting for flight controller heartbeat") m.wait_heartbeat(timeout=5) logging.info("Heartbeat from system %u, component %u", m.target_system, m.target_system) def main() -> None: - """for testing/example purposes only""" + """For testing/example purposes only.""" args = argument_parser() logging.basicConfig(level=logging.getLevelName(args.loglevel), format="%(levelname)s - %(message)s") diff --git a/MethodicConfigurator/battery_cell_voltages.py b/MethodicConfigurator/battery_cell_voltages.py index 792a438..4475455 100644 --- a/MethodicConfigurator/battery_cell_voltages.py +++ b/MethodicConfigurator/battery_cell_voltages.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Battery cell voltages management. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/MethodicConfigurator/common_arguments.py b/MethodicConfigurator/common_arguments.py index 48e974d..b91c871 100644 --- a/MethodicConfigurator/common_arguments.py +++ b/MethodicConfigurator/common_arguments.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Common arguments used by all sub applications. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/MethodicConfigurator/extract_param_defaults.py b/MethodicConfigurator/extract_param_defaults.py index fa32202..178407b 100755 --- a/MethodicConfigurator/extract_param_defaults.py +++ b/MethodicConfigurator/extract_param_defaults.py @@ -27,7 +27,7 @@ MAV_PARAM_TYPE_REAL32 = 9 -def parse_arguments(args=None) -> argparse.Namespace: +def parse_arguments(args: Union[None, argparse.Namespace] = None) -> argparse.Namespace: """ Parses command line arguments for the script. @@ -36,6 +36,7 @@ def parse_arguments(args=None) -> argparse.Namespace: Returns: Namespace object containing the parsed arguments. + """ parser = argparse.ArgumentParser(description="Extracts parameter default values from an ArduPilot .bin log file.") parser.add_argument( @@ -81,18 +82,24 @@ def parse_arguments(args=None) -> argparse.Namespace: help="Type of parameter values to extract. Defaults to %(default)s.", ) parser.add_argument("bin_file", help="The ArduPilot .bin log file to read") - args, _ = parser.parse_known_args(args) + args, _ = parser.parse_known_args(args) # type: ignore[arg-type] + + if args is None: + msg = "No arguments provided" + raise ValueError(msg) if args.sort == "": args.sort = args.format if args.format != "qgcs": if args.sysid != -1: - raise SystemExit("--sysid parameter is only relevant if --format is qgcs") + msg = "--sysid parameter is only relevant if --format is qgcs" + raise SystemExit(msg) if args.compid != -1: - raise SystemExit("--compid parameter is only relevant if --format is qgcs") + msg = "--compid parameter is only relevant if --format is qgcs" + raise SystemExit(msg) - return args # type: ignore # mypy bug + return args def extract_parameter_values(logfile: str, param_type: str = "defaults") -> dict[str, float]: # pylint: disable=too-many-branches @@ -105,11 +112,13 @@ def extract_parameter_values(logfile: str, param_type: str = "defaults") -> dict Returns: A dictionary with parameter names as keys and their values as float. + """ try: mlog = mavutil.mavlink_connection(logfile) except Exception as e: - raise SystemExit(f"Error opening the {logfile} logfile: {e!s}") from e + msg = f"Error opening the {logfile} logfile: {e!s}" + raise SystemExit(msg) from e values: dict[str, float] = {} while True: m = mlog.recv_match(type=["PARM"]) @@ -119,9 +128,11 @@ def extract_parameter_values(logfile: str, param_type: str = "defaults") -> dict return values pname = m.Name if len(pname) > PARAM_NAME_MAX_LEN: - raise SystemExit(f"Too long parameter name: {pname}") + msg = f"Too long parameter name: {pname}" + raise SystemExit(msg) if not re.match(PARAM_NAME_REGEX, pname): - raise SystemExit(f"Invalid parameter name {pname}") + msg = f"Invalid parameter name {pname}" + raise SystemExit(msg) # parameter names are supposed to be unique if pname in values: continue @@ -135,7 +146,8 @@ def extract_parameter_values(logfile: str, param_type: str = "defaults") -> dict if hasattr(m, "Value") and hasattr(m, "Default") and m.Value != m.Default: values[pname] = m.Value else: - raise SystemExit(f"Invalid type {param_type}") + msg = f"Invalid type {param_type}" + raise SystemExit(msg) def missionplanner_sort(item: str) -> tuple[str, ...]: @@ -147,6 +159,7 @@ def missionplanner_sort(item: str) -> tuple[str, ...]: Returns: A tuple representing the sorted parameter name. + """ parts = item.split("_") # Split the parameter name by underscore # Compare the parts separately @@ -162,13 +175,14 @@ def mavproxy_sort(item: str) -> str: Returns: The sorted parameter name. + """ return item def sort_params(params: dict[str, float], sort_type: str = "none") -> dict[str, float]: """ - Sorts parameter names according to sort_type + Sorts parameter names according to sort_type. Args: params: A dictionary with parameter names as keys and their values as float. @@ -176,6 +190,7 @@ def sort_params(params: dict[str, float], sort_type: str = "none") -> dict[str, Returns: A dictionary with parameter names as keys and their values as float. + """ if sort_type == "missionplanner": params = dict(sorted(params.items(), key=lambda x: missionplanner_sort(x[0]))) @@ -193,14 +208,17 @@ def output_params( compid: int = -1, ) -> None: """ - Outputs parameters names and their values to the console + Outputs parameters names and their values to the console. Args: params: A dictionary with parameter names as keys and their values as float. format_type: The output file format. Can be 'missionplanner', 'mavproxy' or 'qgcs'. + sysid: MAVLink System ID + compid: MAVLink component ID Returns: None + """ if format_type == "qgcs": if sysid == -1: @@ -209,13 +227,17 @@ def output_params( if compid == -1: compid = 1 # if unspecified, default to 1 if sysid < 0: - raise SystemExit(f"Invalid system ID parameter {sysid} must not be negative") + msg = f"Invalid system ID parameter {sysid} must not be negative" + raise SystemExit(msg) if sysid > MAVLINK_SYSID_MAX - 1: - raise SystemExit(f"Invalid system ID parameter {sysid} must be smaller than {MAVLINK_SYSID_MAX}") + msg = f"Invalid system ID parameter {sysid} must be smaller than {MAVLINK_SYSID_MAX}" + raise SystemExit(msg) if compid < 0: - raise SystemExit(f"Invalid component ID parameter {compid} must not be negative") + msg = f"Invalid component ID parameter {compid} must not be negative" + raise SystemExit(msg) if compid > MAVLINK_COMPID_MAX - 1: - raise SystemExit(f"Invalid component ID parameter {compid} must be smaller than {MAVLINK_COMPID_MAX}") + msg = f"Invalid component ID parameter {compid} must be smaller than {MAVLINK_COMPID_MAX}" + raise SystemExit(msg) # see https://dev.qgroundcontrol.com/master/en/file_formats/parameters.html print(""" # # Vehicle-Id Component-Id Name Value Type diff --git a/MethodicConfigurator/frontend_tkinter_base.py b/MethodicConfigurator/frontend_tkinter_base.py index a08dd99..e1b2adb 100644 --- a/MethodicConfigurator/frontend_tkinter_base.py +++ b/MethodicConfigurator/frontend_tkinter_base.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +TKinter base classes reused in multiple parts of the code. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -19,7 +19,7 @@ from platform import system as platform_system from tkinter import BooleanVar, messagebox, ttk from tkinter import font as tkFont -from typing import Optional +from typing import Optional, Union from PIL import Image, ImageTk @@ -52,15 +52,15 @@ def show_no_connection_error(_error_string: str) -> None: show_error_message(_("No Connection to the Flight Controller"), error_message.format(**locals())) -def show_tooltip(widget, text) -> None: - def enter(_event) -> None: +def show_tooltip(widget: tk.Widget, text: str) -> None: + def enter(_event: tk.Event) -> None: # Calculate the position of the tooltip based on the widget's position x = widget.winfo_rootx() + widget.winfo_width() // 2 y = widget.winfo_rooty() + widget.winfo_height() tooltip.geometry(f"+{x}+{y}") tooltip.deiconify() - def leave(_event) -> None: + def leave(_event: tk.Event) -> None: tooltip.withdraw() tooltip = tk.Toplevel(widget) @@ -74,7 +74,7 @@ def leave(_event) -> None: widget.bind("", leave) -def update_combobox_width(combobox) -> None: +def update_combobox_width(combobox: ttk.Combobox) -> None: # Calculate the maximum width needed for the content max_width = max(len(value) for value in combobox["values"]) # Set a minimum width for the combobox @@ -92,17 +92,20 @@ class AutoResizeCombobox(ttk.Combobox): # pylint: disable=too-many-ancestors values. It also supports displaying a tooltip when hovering over the widget. Attributes: - container: The parent container in which the Combobox is placed. + master: The parent container in which the Combobox is placed. values: A tuple of strings representing the entries in the Combobox. selected_element: The initially selected element in the Combobox. tooltip: A string representing the tooltip text to display when hovering over the widget. + """ - def __init__(self, container, values, selected_element, tooltip, *args, **kwargs) -> None: - super().__init__(container, *args, **kwargs) + def __init__( + self, master: ttk.Frame, values: list[str], selected_element: str, tooltip: Union[None, str], *args, **kwargs + ) -> None: + super().__init__(master, *args, **kwargs) self.set_entries_tupple(values, selected_element, tooltip) - def set_entries_tupple(self, values, selected_element, tooltip=None) -> None: + def set_entries_tupple(self, values: list[str], selected_element: str, tooltip: Union[None, str] = None) -> None: self["values"] = tuple(values) if selected_element: if selected_element in values: @@ -126,11 +129,11 @@ class ScrollFrame(ttk.Frame): # pylint: disable=too-many-ancestors scrollable areas within your application's GUI. """ - def __init__(self, parent) -> None: - super().__init__(parent) # create a frame (self) + def __init__(self, master) -> None: # noqa: ANN001 + super().__init__(master) # create a frame (self) # place canvas on self, copy ttk.background to tk.background - self.canvas = tk.Canvas(self, borderwidth=0, background=ttk.Style(parent).lookup("TFrame", "background")) + self.canvas = tk.Canvas(self, borderwidth=0, background=ttk.Style(master).lookup("TFrame", "background")) # place a frame on the canvas, this frame will hold the child widgets self.view_port = ttk.Frame(self.canvas) @@ -163,8 +166,8 @@ def __init__(self, parent) -> None: # perform an initial stretch on render, otherwise the scroll region has a tiny border until the first resize self.on_frame_configure(None) - def on_frame_configure(self, _event) -> None: - """Reset the scroll region to encompass the inner frame""" + def on_frame_configure(self, _event) -> None: # noqa: ANN001 + """Reset the scroll region to encompass the inner frame.""" # Whenever the size of the frame changes, alter the scroll region respectively. self.canvas.configure(scrollregion=self.canvas.bbox("all")) # Calculate the bounding box for the scroll region, starting from the second row @@ -174,13 +177,13 @@ def on_frame_configure(self, _event) -> None: # bbox = (bbox[0], bbox[1] + self.canvas.winfo_reqheight(), bbox[2], bbox[3]) # self.canvas.configure(scrollregion=bbox) - def on_canvas_configure(self, event) -> None: - """Reset the canvas window to encompass inner frame when required""" + def on_canvas_configure(self, event: tk.Event) -> None: + """Reset the canvas window to encompass inner frame when required.""" canvas_width = event.width # Whenever the size of the canvas changes alter the window region respectively. self.canvas.itemconfig(self.canvas_window, width=canvas_width) - def on_mouse_wheel(self, event) -> None: # cross platform scroll wheel event + def on_mouse_wheel(self, event: tk.Event) -> None: # cross platform scroll wheel event canvas_height = self.canvas.winfo_height() rows_height = self.canvas.bbox("all")[3] @@ -194,14 +197,14 @@ def on_mouse_wheel(self, event) -> None: # cross platform scroll wheel event elif event.num == 5: self.canvas.yview_scroll(1, "units") - def on_enter(self, _event) -> None: # bind wheel events when the cursor enters the control + def on_enter(self, _event: tk.Event) -> None: # bind wheel events when the cursor enters the control if platform_system() == "Linux": self.canvas.bind_all("", self.on_mouse_wheel) self.canvas.bind_all("", self.on_mouse_wheel) else: self.canvas.bind_all("", self.on_mouse_wheel) - def on_leave(self, _event) -> None: # unbind wheel events when the cursor leaves the control + def on_leave(self, _event: tk.Event) -> None: # unbind wheel events when the cursor leaves the control if platform_system() == "Linux": self.canvas.unbind_all("") self.canvas.unbind_all("") @@ -217,8 +220,8 @@ class ProgressWindow: a task. It includes a progress bar and a label to display the progress message. """ - def __init__(self, parent, title: str, message: str = "", width: int = 300, height: int = 80) -> None: # pylint: disable=too-many-arguments, too-many-positional-arguments - self.parent = parent + def __init__(self, master, title: str, message: str = "", width: int = 300, height: int = 80) -> None: # noqa: ANN001, pylint: disable=too-many-arguments, too-many-positional-arguments + self.parent = master self.message = message self.progress_window = tk.Toplevel(self.parent) self.progress_window.title(title) @@ -253,6 +256,7 @@ def update_progress_bar(self, current_value: int, max_value: int) -> None: Args: current_value (int): The current progress value. max_value (int): The maximum progress value, if 0 uses percentage. + """ try: self.progress_window.lift() @@ -299,6 +303,7 @@ class RichText(tk.Text): # pylint: disable=too-many-ancestors To use this widget, simply replace instances of the standard Tkinter Text widget with RichText in your UI definitions. Apply tags to text segments using the tag_add method and configure the appearance accordingly. + """ def __init__(self, *args, **kwargs) -> None: @@ -320,11 +325,14 @@ def __init__(self, *args, **kwargs) -> None: self.tag_configure("h1", font=h1_font, spacing3=default_size) -def get_widget_font(widget: tk.Widget): +def get_widget_font_family_and_size(widget: tk.Widget) -> tuple[str, int]: style = ttk.Style() widget_style = widget.cget("style") # Get the style used by the widget font_name = style.lookup(widget_style, "font") - return tkFont.nametofont(font_name).config() + font_dict = tkFont.nametofont(font_name).config() + if font_dict is None: + return "Segoe UI", 9 + return font_dict.get("family", "Segoe UI"), font_dict.get("size", 9) class BaseWindow: @@ -361,6 +369,7 @@ def center_window(window: tk.Toplevel, parent: tk.Toplevel) -> None: Args: window (tk.Toplevel): The window to center. parent (tk.Toplevel): The parent window. + """ window.update_idletasks() parent_width = parent.winfo_width() @@ -384,9 +393,9 @@ def put_image_in_label(parent: ttk.Frame, filepath: str, image_height: int = 40) photo = ImageTk.PhotoImage(resized_image) # Create a label with the resized image - image_label = ttk.Label(parent, image=photo) # type: ignore[arg-type] # workaround a mypy issue + image_label = ttk.Label(parent, image=photo) # Keep a reference to the image to prevent it from being garbage collected - image_label.image = photo # type: ignore + image_label.image = photo # type: ignore[attr-defined] return image_label diff --git a/MethodicConfigurator/frontend_tkinter_component_editor.py b/MethodicConfigurator/frontend_tkinter_component_editor.py old mode 100644 new mode 100755 index f5664fd..cfd18dc --- a/MethodicConfigurator/frontend_tkinter_component_editor.py +++ b/MethodicConfigurator/frontend_tkinter_component_editor.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Data-dependent part of the component editor GUI. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -39,6 +41,7 @@ def argument_parser() -> Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ # pylint: disable=duplicate-code parser = ArgumentParser( @@ -213,7 +216,7 @@ class ComponentEditorWindow(ComponentEditorWindowBase): for editing component configurations in the ArduPilot Methodic Configurator. """ - def __init__(self, version, local_filesystem: LocalFilesystem) -> None: + def __init__(self, version: str, local_filesystem: LocalFilesystem) -> None: self.serial_ports = ["SERIAL1", "SERIAL2", "SERIAL3", "SERIAL4", "SERIAL5", "SERIAL6", "SERIAL7", "SERIAL8"] self.can_ports = ["CAN1", "CAN2"] self.i2c_ports = ["I2C1", "I2C2", "I2C3", "I2C4"] @@ -480,7 +483,9 @@ def update_esc_protocol_combobox_entries(self, esc_connection_type: str) -> None protocol_combobox.set(protocols[0] if protocols else "") protocol_combobox.update_idletasks() # re-draw the combobox ASAP - def add_entry_or_combobox(self, value, entry_frame, path: tuple[str, str, str]) -> Union[ttk.Entry, ttk.Combobox]: + def add_entry_or_combobox( + self, value: float, entry_frame: ttk.Frame, path: tuple[str, str, str] + ) -> Union[ttk.Entry, ttk.Combobox]: # Default values for comboboxes in case the apm.pdef.xml metadata is not available fallbacks = { "RC_PROTOCOLS": [value["protocol"] for value in rc_protocols_dict.values()], @@ -541,13 +546,13 @@ def get_combobox_values(param_name: str) -> list: config = combobox_config.get(path) if config: cb = ttk.Combobox(entry_frame, values=config["values"]) - cb.bind("", lambda event, path=path: self.validate_combobox(event, path)) # type: ignore - cb.bind("", lambda event, path=path: self.validate_combobox(event, path)) # type: ignore + cb.bind("", lambda event, path=path: self.validate_combobox(event, path)) # type: ignore[misc] + cb.bind("", lambda event, path=path: self.validate_combobox(event, path)) # type: ignore[misc] if path == ("ESC", "FC Connection", "Type"): # immediate update of ESC Protocol upon ESC Type selection cb.bind( "<>", - lambda event, path=path: self.update_esc_protocol_combobox_entries(cb.get()), # type: ignore[misc] + lambda event: self.update_esc_protocol_combobox_entries(cb.get()), # noqa: ARG005 ) cb.set(value) @@ -561,7 +566,7 @@ def get_combobox_values(param_name: str) -> list: entry.insert(0, str(value)) return entry - def get_validate_function(self, entry, path) -> Union[Callable[[tk.Event], object], None]: + def get_validate_function(self, entry: ttk.Entry, path: tuple[str, str, str]) -> Union[Callable[[tk.Event], object], None]: validate_functions = { ("Frame", "Specifications", "TOW min Kg"): lambda event, entry=entry, path=path: self.validate_entry_limits( event, entry, float, (0.01, 600), "Takeoff Weight", path @@ -594,9 +599,7 @@ def get_validate_function(self, entry, path) -> Union[Callable[[tk.Event], objec return validate_functions.get(path) def validate_combobox(self, event: tk.Event, path: tuple[str, ...]) -> bool: - """ - Validates the value of a combobox. - """ + """Validates the value of a combobox.""" combobox = event.widget # Get the combobox widget that triggered the event value = combobox.get() # Get the current value of the combobox allowed_values = combobox.cget("values") # Get the list of allowed values @@ -616,12 +619,20 @@ def validate_combobox(self, event: tk.Event, path: tuple[str, ...]) -> bool: combobox.configure(style="comb_input_valid.TCombobox") return True - def validate_entry_limits(self, event, entry, data_type, limits, _name, path) -> bool: # pylint: disable=too-many-arguments, too-many-positional-arguments + def validate_entry_limits( # pylint: disable=too-many-arguments, too-many-positional-arguments + self, + event: Union[None, tk.Event], + entry: ttk.Entry, + data_type: type, + limits: tuple[float, float], + _name: str, + path: tuple[str, str, str], + ) -> bool: is_focusout_event = event and event.type == "10" try: value = entry.get() # make sure value is defined to prevent exception in the except block value = data_type(value) - if value < limits[0] or value > limits[1]: + if value < limits[0] or value > limits[1]: # type: ignore[operator] entry.configure(style="entry_input_invalid.TEntry") error_msg = _("{_name} must be a {data_type.__name__} between {limits[0]} and {limits[1]}") raise ValueError(error_msg.format(**locals())) @@ -634,10 +645,8 @@ def validate_entry_limits(self, event, entry, data_type, limits, _name, path) -> entry.configure(style="entry_input_valid.TEntry") return True - def validate_cell_voltage(self, event, entry, path) -> bool: # pylint: disable=too-many-branches - """ - Validates the value of a battery cell voltage entry. - """ + def validate_cell_voltage(self, event: Union[None, tk.Event], entry: ttk.Entry, path: tuple[str, str, str]) -> bool: # pylint: disable=too-many-branches + """Validates the value of a battery cell voltage entry.""" chemistry_path = ("Battery", "Specifications", "Chemistry") if chemistry_path not in self.entry_widgets: show_error_message(_("Error"), _("Battery Chemistry not set. Will default to Lipo.")) @@ -653,14 +662,14 @@ def validate_cell_voltage(self, event, entry, path) -> bool: # pylint: disable= if voltage < volt_limit: if is_focusout_event: entry.delete(0, tk.END) - entry.insert(0, volt_limit) + entry.insert(0, str(volt_limit)) error_msg = _("is below the {chemistry} minimum limit of {volt_limit}") raise VoltageTooLowError(error_msg.format(**locals())) volt_limit = BatteryCell.limit_max_voltage(chemistry) if voltage > volt_limit: if is_focusout_event: entry.delete(0, tk.END) - entry.insert(0, volt_limit) + entry.insert(0, str(volt_limit)) error_msg = _("is above the {chemistry} maximum limit of {volt_limit}") raise VoltageTooHighError(error_msg.format(**locals())) except (VoltageTooLowError, VoltageTooHighError) as _e: diff --git a/MethodicConfigurator/frontend_tkinter_component_editor_base.py b/MethodicConfigurator/frontend_tkinter_component_editor_base.py old mode 100644 new mode 100755 index 5c0b8f8..24b2b6c --- a/MethodicConfigurator/frontend_tkinter_component_editor_base.py +++ b/MethodicConfigurator/frontend_tkinter_component_editor_base.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Component editor GUI that is not data dependent. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -39,6 +41,7 @@ def argument_parser() -> Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ # pylint: disable=duplicate-code parser = ArgumentParser( @@ -62,7 +65,7 @@ class ComponentEditorWindowBase(BaseWindow): class, which provides basic window functionality. """ - def __init__(self, version, local_filesystem: LocalFilesystem) -> None: + def __init__(self, version: str, local_filesystem: LocalFilesystem) -> None: super().__init__() self.local_filesystem = local_filesystem @@ -156,22 +159,21 @@ def _set_component_value_and_update_ui(self, path: tuple, value: str) -> None: entry.config(state="disabled") def populate_frames(self) -> None: - """ - Populates the ScrollFrame with widgets based on the JSON data. - """ + """Populates the ScrollFrame with widgets based on the JSON data.""" if "Components" in self.data: for key, value in self.data["Components"].items(): self.__add_widget(self.scroll_frame.view_port, key, value, []) - def __add_widget(self, parent, key, value, path) -> None: + def __add_widget(self, parent: tk.Widget, key: str, value: dict, path: list) -> None: """ Adds a widget to the parent widget with the given key and value. - Parameters: - parent (tkinter.Widget): The parent widget to which the LabelFrame/Entry will be added. - key (str): The key for the LabelFrame/Entry. - value (dict): The value associated with the key. - path (list): The path to the current key in the JSON data. + Args: + parent (tkinter.Widget): The parent widget to which the LabelFrame/Entry will be added. + key (str): The key for the LabelFrame/Entry. + value (dict): The value associated with the key. + path (list): The path to the current key in the JSON data. + """ if isinstance(value, dict): # JSON non-leaf elements, add LabelFrame widget frame = ttk.LabelFrame(parent, text=_(key)) @@ -190,16 +192,14 @@ def __add_widget(self, parent, key, value, path) -> None: label = ttk.Label(entry_frame, text=_(key)) label.pack(side=tk.LEFT) - entry = self.add_entry_or_combobox(value, entry_frame, tuple([*path, key])) + entry = self.add_entry_or_combobox(value, entry_frame, (*path, key)) entry.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 5)) # Store the entry widget in the entry_widgets dictionary for later retrieval - self.entry_widgets[tuple([*path, key])] = entry + self.entry_widgets[(*path, key)] = entry def save_data(self) -> None: - """ - Saves the edited JSON data back to the file. - """ + """Saves the edited JSON data back to the file.""" confirm_message = _( "ArduPilot Methodic Configurator only operates correctly if all component properties are correct." " ArduPilot parameter values depend on the components used and their connections.\n\n" @@ -240,7 +240,9 @@ def save_data(self) -> None: self.root.destroy() # This function will be overwritten in child classes - def add_entry_or_combobox(self, value, entry_frame, _path) -> Union[ttk.Entry, ttk.Combobox]: + def add_entry_or_combobox( + self, value: float, entry_frame: ttk.Frame, _path: tuple[str, str, str] + ) -> Union[ttk.Entry, ttk.Combobox]: entry = ttk.Entry(entry_frame) entry.insert(0, str(value)) return entry diff --git a/MethodicConfigurator/frontend_tkinter_connection_selection.py b/MethodicConfigurator/frontend_tkinter_connection_selection.py old mode 100644 new mode 100755 index 3a3d822..4b312d9 --- a/MethodicConfigurator/frontend_tkinter_connection_selection.py +++ b/MethodicConfigurator/frontend_tkinter_connection_selection.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +GUI to select the connection to the FC. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -35,8 +37,8 @@ class ConnectionSelectionWidgets: # pylint: disable=too-many-instance-attribute def __init__( # pylint: disable=too-many-arguments, too-many-positional-arguments self, - parent, - parent_frame, + parent, # noqa: ANN001 ConnectionSelectionWindow can not add it here, otherwise a dependency loop will be created + parent_frame: ttk.Labelframe, flight_controller: FlightController, destroy_parent_on_connect: bool, download_params_on_connect: bool, @@ -74,7 +76,7 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen _("Select the flight controller connection\nYou can add a custom connection to the existing ones"), ) - def on_select_connection_combobox_change(self, _event) -> None: + def on_select_connection_combobox_change(self, _event: tk.Event) -> None: selected_connection = self.conn_selection_combobox.get_selected_key() error_msg = _("Connection combobox changed to: {selected_connection}") logging_debug(error_msg.format(**locals())) @@ -223,7 +225,7 @@ def __init__(self, flight_controller: FlightController, connection_result_string skip_fc_connection_button = ttk.Button( option3_label_frame, text=_("Skip FC connection, just edit the .param files on disk"), - command=lambda flight_controller=flight_controller: self.skip_fc_connection(flight_controller), # type: ignore + command=lambda fc=flight_controller: self.skip_fc_connection(fc), # type: ignore[misc] ) skip_fc_connection_button.pack(expand=False, fill=tk.X, padx=15, pady=6) show_tooltip( @@ -258,6 +260,7 @@ def argument_parser() -> Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ parser = ArgumentParser( description=_( diff --git a/MethodicConfigurator/frontend_tkinter_directory_selection.py b/MethodicConfigurator/frontend_tkinter_directory_selection.py old mode 100644 new mode 100755 index 817ce55..9498f31 --- a/MethodicConfigurator/frontend_tkinter_directory_selection.py +++ b/MethodicConfigurator/frontend_tkinter_directory_selection.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +GUI to select the directory to store the vehicle configuration files. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -118,9 +120,9 @@ class DirectoryNameWidgets: # pylint: disable=too-few-public-methods including a label and an entry field for displaying the selected directory name. """ - def __init__(self, parent_frame, initial_dir: str, label_text: str, dir_tooltip: str) -> None: + def __init__(self, master: ttk.Labelframe, initial_dir: str, label_text: str, dir_tooltip: str) -> None: # Create a new frame for the directory name selection label - self.container_frame = ttk.Frame(parent_frame) + self.container_frame = ttk.Frame(master) # Create a description label for the directory name entry directory_selection_label = ttk.Label(self.container_frame, text=label_text) @@ -481,6 +483,7 @@ def argument_parser() -> Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ parser = ArgumentParser( description=_( diff --git a/MethodicConfigurator/frontend_tkinter_entry_dynamic.py b/MethodicConfigurator/frontend_tkinter_entry_dynamic.py old mode 100644 new mode 100755 index ea3c52a..54864b6 --- a/MethodicConfigurator/frontend_tkinter_entry_dynamic.py +++ b/MethodicConfigurator/frontend_tkinter_entry_dynamic.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +GUI entry Widget with autocompletion features. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator https://code.activestate.com/recipes/580770-combobox-autocomplete/ @@ -13,12 +15,12 @@ import tkinter as tk from tkinter import Entry, Listbox, StringVar, ttk from tkinter.constants import END, HORIZONTAL, SINGLE, VERTICAL, E, N, S, W -from typing import Union +from typing import Callable, Union from MethodicConfigurator import _ -def autoscroll(sbar, first, last) -> None: +def autoscroll(sbar: ttk.Scrollbar, first: float, last: float) -> None: """Hide and show scrollbar as needed.""" first, last = float(first), float(last) if first <= 0 and last >= 1: @@ -29,15 +31,13 @@ def autoscroll(sbar, first, last) -> None: class EntryWithDynamicalyFilteredListbox(Entry): # pylint: disable=too-many-ancestors, too-many-instance-attributes - """ - Entry with dynamicaly filtered ListBox to emulate an inteligent combobox widget - """ + """Entry with dynamicaly filtered ListBox to emulate an inteligent combobox widget.""" def __init__( # pylint: disable=too-many-arguments, too-many-positional-arguments self, - master, + master: Union[tk.Tk, ttk.Frame], list_of_items: Union[None, list[str]] = None, - custom_filter_function=None, + custom_filter_function: Union[None, Callable[[str], list[str]]] = None, listbox_width: Union[None, int] = None, listbox_height: int = 12, ignorecase_match: bool = False, @@ -78,9 +78,9 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen self.bind("", self._previous) self.bind("", self.update_entry_from_listbox) - self.bind("", lambda event: self.unpost_listbox()) + self.bind("", lambda event: self.unpost_listbox) # noqa: ARG005 - def default_filter_function(self, entry_data) -> list[str]: + def default_filter_function(self, entry_data: str) -> list[str]: if self._ignorecase_match: if self._startswith_match: return [item for item in self._list_of_items if item.lower().startswith(entry_data.lower())] @@ -89,7 +89,7 @@ def default_filter_function(self, entry_data) -> list[str]: return [item for item in self._list_of_items if item.startswith(entry_data)] return [item for item in self._list_of_items if entry_data in item] - def _on_change_entry_var(self, _name, _index, _mode) -> None: + def _on_change_entry_var(self, _name, _index, _mode) -> None: # noqa: ANN001 entry_data = self._entry_var.get() if entry_data == "": @@ -113,7 +113,7 @@ def _on_change_entry_var(self, _name, _index, _mode) -> None: self.unpost_listbox() self.focus() - def _build_listbox(self, values) -> None: + def _build_listbox(self, values: list[str]) -> None: listbox_frame = ttk.Frame(self.master) self._listbox = Listbox( @@ -123,7 +123,7 @@ def _build_listbox(self, values) -> None: self._listbox.bind("", self.update_entry_from_listbox) self._listbox.bind("", self.update_entry_from_listbox) - self._listbox.bind("", lambda event: self.unpost_listbox()) + self._listbox.bind("", lambda event: self.unpost_listbox()) # noqa: ARG005 self._listbox.bind("", self._next) self._listbox.bind("", self._previous) @@ -168,10 +168,11 @@ def post_listbox(self) -> None: if values: self._build_listbox(values) - def unpost_listbox(self) -> None: + def unpost_listbox(self, _event: Union[None, tk.Event] = None) -> str: if self._listbox is not None: self._listbox.master.destroy() self._listbox = None + return "" def get_value(self) -> str: return self._entry_var.get() # type: ignore[no-any-return] # mypy bug @@ -219,7 +220,7 @@ def _previous(self, _event: Union[None, tk.Event]) -> str: self._listbox.selection_clear(index) if index == 0: - index = END # type: ignore + index = END # type: ignore[assignment] else: index -= 1 diff --git a/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py b/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py index b7c703a..cdd5adc 100644 --- a/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py +++ b/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Flight controller information GUI. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -8,26 +8,20 @@ SPDX-License-Identifier: GPL-3.0-or-later """ -# from logging import debug as logging_debug import tkinter as tk + +# from logging import debug as logging_debug from logging import info as logging_info from tkinter import ttk from MethodicConfigurator import _, __version__ - -# type-checking block from MethodicConfigurator.annotate_params import Par from MethodicConfigurator.backend_flightcontroller import FlightController - -# from MethodicConfigurator.backend_flightcontroller_info import BackendFlightcontrollerInfo -# from MethodicConfigurator.frontend_tkinter_base import show_tooltip from MethodicConfigurator.frontend_tkinter_base import BaseWindow, ProgressWindow class FlightControllerInfoWindow(BaseWindow): - """ - Display flight controller hardware, firmware and parameter information - """ + """Display flight controller hardware, firmware and parameter information.""" def __init__(self, flight_controller: FlightController) -> None: super().__init__() diff --git a/MethodicConfigurator/frontend_tkinter_pair_tuple_combobox.py b/MethodicConfigurator/frontend_tkinter_pair_tuple_combobox.py old mode 100644 new mode 100755 index 5e4270d..956036d --- a/MethodicConfigurator/frontend_tkinter_pair_tuple_combobox.py +++ b/MethodicConfigurator/frontend_tkinter_pair_tuple_combobox.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +A combobox GUI with support for complex lists. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -24,7 +26,7 @@ from MethodicConfigurator import _ from MethodicConfigurator.common_arguments import add_common_arguments_and_parse -from MethodicConfigurator.frontend_tkinter_base import get_widget_font, update_combobox_width +from MethodicConfigurator.frontend_tkinter_base import get_widget_font_family_and_size, update_combobox_width # https://dev.to/geraldew/python-tkinter-an-exercise-in-wrapping-the-combobox-ndb @@ -36,7 +38,13 @@ class PairTupleCombobox(ttk.Combobox): # pylint: disable=too-many-ancestors """ def __init__( - self, master, list_pair_tuple: list[tuple[str, str]], selected_element: Union[None, str], cb_name: str, *args, **kwargs + self, + master, # noqa: ANN001 + list_pair_tuple: list[tuple[str, str]], + selected_element: Union[None, str], + cb_name: str, + *args, + **kwargs, ) -> None: super().__init__(master, *args, **kwargs) self.cb_name = cb_name @@ -45,7 +53,7 @@ def __init__( self.set_entries_tupple(list_pair_tuple, selected_element) self.bind("", self.on_combo_configure, add="+") - def set_entries_tupple(self, list_pair_tuple, selected_element: Union[None, str]) -> None: + def set_entries_tupple(self, list_pair_tuple: list[tuple[str, str]], selected_element: Union[None, str]) -> None: if isinstance(list_pair_tuple, list): for tpl in list_pair_tuple: self.list_keys.append(tpl[0]) @@ -135,7 +143,13 @@ class PairTupleComboboxTooltip(PairTupleCombobox): # pylint: disable=too-many-a """ def __init__( - self, master, list_pair_tuple: list[tuple[str, str]], selected_element: Union[None, str], cb_name: str, *args, **kwargs + self, + master, # noqa: ANN001 + list_pair_tuple: list[tuple[str, str]], + selected_element: Union[None, str], + cb_name: str, + *args, + **kwargs, ) -> None: super().__init__(master, list_pair_tuple, selected_element, cb_name, *args, **kwargs) self.tooltip: Union[None, Toplevel] = None @@ -143,13 +157,13 @@ def __init__( # Bind events related to the dropdown pd = self.tk.call("ttk::combobox::PopdownWindow", self) lb = pd + ".f.l" - self._bind(("bind", lb), "", self.on_key_release, None) # type: ignore - self._bind(("bind", lb), "", self.on_motion, None) # type: ignore - self._bind(("bind", lb), "", self.on_escape_press, None) # type: ignore + self._bind(("bind", lb), "", self.on_key_release, None) # type: ignore[attr-defined] + self._bind(("bind", lb), "", self.on_motion, None) # type: ignore[attr-defined] + self._bind(("bind", lb), "", self.on_escape_press, None) # type: ignore[attr-defined] self.bind("<>", self.on_combobox_selected, None) def on_key_release(self, _event: Union[None, tk.Event]) -> None: - """Get the keyboard highlighted index and create a tooltip for it""" + """Get the keyboard highlighted index and create a tooltip for it.""" pd = self.tk.call("ttk::combobox::PopdownWindow", self) lb = pd + ".f.l" if self.tk.call(lb, "curselection"): @@ -157,7 +171,7 @@ def on_key_release(self, _event: Union[None, tk.Event]) -> None: self.create_tooltip_from_index(highlighted_index) def on_motion(self, event: tk.Event) -> None: - """Get the mouse highlighted index and create a tooltip for it""" + """Get the mouse highlighted index and create a tooltip for it.""" pd = self.tk.call("ttk::combobox::PopdownWindow", self) lb = pd + ".f.l" index = self.tk.call(lb, "index", f"@{event.x},{event.y}") @@ -202,6 +216,7 @@ def argument_parser() -> Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ parser = ArgumentParser( description=_( @@ -238,9 +253,9 @@ def main() -> None: tuple_pairs = [(str(i), random_string) for i, random_string in enumerate(random_strings)] combobox = PairTupleCombobox(root, tuple_pairs, None, "Random Strings") - font = get_widget_font(combobox) - font["size"] -= 2 if platform_system() == "Windows" else 1 - combobox.config(state="readonly", width=9, font=(font["family"], font["size"])) + font_family, font_size = get_widget_font_family_and_size(combobox) + font_size -= 2 if platform_system() == "Windows" else 1 + combobox.config(state="readonly", width=9, font=(font_family, font_size)) # Pack the combobox into the main window combobox.pack(pady=10, padx=10) diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor.py b/MethodicConfigurator/frontend_tkinter_parameter_editor.py old mode 100644 new mode 100755 index e74ce82..b235b99 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Parameter editor GUI. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -35,7 +37,7 @@ ProgressWindow, RichText, UsagePopupWindow, - get_widget_font, + get_widget_font_family_and_size, show_tooltip, ) from MethodicConfigurator.frontend_tkinter_directory_selection import VehicleDirectorySelectionWidgets @@ -219,11 +221,12 @@ def __create_conf_widgets(self, version: str) -> None: self.file_selection_combobox.bind("<>", self.on_param_file_combobox_change) self.file_selection_combobox.pack(side=tk.TOP, anchor=tk.NW, pady=(4, 0)) - self.legend_frame(config_subframe, get_widget_font(file_selection_label)["family"]) + font_family, _font_size = get_widget_font_family_and_size(file_selection_label) + self.legend_frame(config_subframe, font_family) image_label = BaseWindow.put_image_in_label(config_frame, LocalFilesystem.application_logo_filepath()) image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) - image_label.bind("", lambda event: show_about_window(self.main_frame, version)) + image_label.bind("", lambda event: show_about_window(self.main_frame, version)) # noqa: ARG005 show_tooltip(image_label, _("User Manual, Support Forum, Report a Bug, Licenses, Source Code")) def legend_frame(self, config_subframe: ttk.Frame, font_family: str) -> None: @@ -814,6 +817,7 @@ def argument_parser() -> Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ parser = ArgumentParser( description=_( diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor_documentation_frame.py b/MethodicConfigurator/frontend_tkinter_parameter_editor_documentation_frame.py index 897f61b..da4d92e 100644 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor_documentation_frame.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor_documentation_frame.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +The documentation frame containing the documentation for the current configuration step. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -128,10 +128,10 @@ def __update_documentation_label(self, label_key: str, text: str, url: str, url_ label = self.documentation_labels[label_key] if url: label.config(text=text, foreground="blue", cursor="hand2", underline=True) - label.bind("", lambda event, url=url: webbrowser_open(url)) # type: ignore + label.bind("", lambda url=url: webbrowser_open(url)) # type: ignore[misc] show_tooltip(label, url) else: label.config(text=text, foreground="black", cursor="arrow", underline=False) - label.bind("", lambda event: None) + label.bind("", None) if url_expected: show_tooltip(label, _("Documentation URL not available")) diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py b/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py index 66c040f..8306dbd 100644 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Parameter editor table GUI. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -26,7 +26,7 @@ # from MethodicConfigurator.backend_flightcontroller import FlightController # from MethodicConfigurator.frontend_tkinter_base import AutoResizeCombobox -from MethodicConfigurator.frontend_tkinter_base import BaseWindow, ScrollFrame, get_widget_font, show_tooltip +from MethodicConfigurator.frontend_tkinter_base import BaseWindow, ScrollFrame, get_widget_font_family_and_size, show_tooltip from MethodicConfigurator.frontend_tkinter_entry_dynamic import EntryWithDynamicalyFilteredListbox from MethodicConfigurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox @@ -41,9 +41,9 @@ class ParameterEditorTable(ScrollFrame): # pylint: disable=too-many-ancestors managing, and updating the table that displays parameters for editing. """ - def __init__(self, root, local_filesystem: LocalFilesystem, parameter_editor) -> None: - super().__init__(root) - self.root = root + def __init__(self, master, local_filesystem: LocalFilesystem, parameter_editor) -> None: # noqa: ANN001 + super().__init__(master) + self.root = master self.local_filesystem = local_filesystem self.parameter_editor = parameter_editor self.current_file = "" @@ -74,7 +74,7 @@ def compute_forced_and_derived_parameters(self) -> None: def add_forced_or_derived_parameters( self, filename: str, new_parameters: dict[str, dict[str, Par]], fc_parameters: Union[dict[str, float], None] = None ) -> None: - """Add forced parameters not yet in the parameter list to the parameter list""" + """Add forced parameters not yet in the parameter list to the parameter list.""" if filename in new_parameters: for param_name, param in new_parameters[filename].items(): if filename not in self.local_filesystem.file_parameters: @@ -301,7 +301,7 @@ def __update_new_value_entry_text(new_value_entry: ttk.Entry, value: float, para else: new_value_entry.configure(style="TEntry") - def __create_new_value_entry( # pylint: disable=too-many-arguments, too-many-positional-arguments + def __create_new_value_entry( # pylint: disable=too-many-arguments, too-many-positional-arguments, too-many-locals self, param_name: str, param: Par, @@ -354,9 +354,9 @@ def __create_new_value_entry( # pylint: disable=too-many-arguments, too-many-po else "readonly.TCombobox", ) new_value_entry.set(selected_value) - font = get_widget_font(new_value_entry) - font["size"] -= 2 if platform_system() == "Windows" else 1 - new_value_entry.config(state="readonly", width=NEW_VALUE_WIDGET_WIDTH, font=(font["family"], font["size"])) + font_family, font_size = get_widget_font_family_and_size(new_value_entry) + font_size -= 2 if platform_system() == "Windows" else 1 + new_value_entry.config(state="readonly", width=NEW_VALUE_WIDGET_WIDTH, font=(font_family, font_size)) new_value_entry.bind( # type: ignore[call-overload] # workaround a mypy issue "<>", lambda event: self.__update_combobox_style_on_selection(new_value_entry, param_default, event), @@ -412,13 +412,13 @@ def on_close() -> None: "", lambda event: self.__open_bitmask_selection_window(event, param_name, bitmask_dict, old_value) ) - def get_param_value_msg(_param_name: str, checked_keys) -> str: + def get_param_value_msg(_param_name: str, checked_keys: set) -> str: _new_decimal_value = sum(1 << key for key in checked_keys) text = _("{_param_name} Value: {_new_decimal_value}") return text.format(**locals()) def update_label() -> None: - checked_keys = [key for key, var in checkbox_vars.items() if var.get()] + checked_keys = {key for key, var in checkbox_vars.items() if var.get()} close_label.config(text=get_param_value_msg(param_name, checked_keys)) # Temporarily unbind the FocusIn event to prevent triggering the window again @@ -457,7 +457,7 @@ def update_label() -> None: def __create_unit_label(self, param_metadata: dict[str, Union[float, str]]) -> ttk.Label: unit_label = ttk.Label(self.view_port, text=param_metadata.get("unit", "") if param_metadata else "") - unit_tooltip = ( + unit_tooltip = str( param_metadata.get("unit_tooltip") if param_metadata else _("No documentation available in apm.pdef.xml for this parameter") diff --git a/MethodicConfigurator/frontend_tkinter_template_overview.py b/MethodicConfigurator/frontend_tkinter_template_overview.py old mode 100644 new mode 100755 index 85d933e..d294be4 --- a/MethodicConfigurator/frontend_tkinter_template_overview.py +++ b/MethodicConfigurator/frontend_tkinter_template_overview.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Vehicle template overview GUI. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -38,6 +40,7 @@ class TemplateOverviewWindow(BaseWindow): Methods: on_row_double_click(event): Handles the event triggered when a row in the Treeview is double-clicked, allowing the user to store the corresponding template directory. + """ def __init__(self, parent: Optional[tk.Toplevel] = None) -> None: @@ -116,9 +119,7 @@ def __init__(self, parent: Optional[tk.Toplevel] = None) -> None: self.root.mainloop() def __adjust_treeview_column_widths(self) -> None: - """ - Adjusts the column widths of the Treeview to fit the contents of each column. - """ + """Adjusts the column widths of the Treeview to fit the contents of each column.""" for col in self.tree["columns"]: max_width = 0 for subtitle in col.title().split("\n"): @@ -169,6 +170,7 @@ def argument_parser() -> argparse.Namespace: Returns: argparse.Namespace: An object containing the parsed arguments. + """ parser = argparse.ArgumentParser( description=_( diff --git a/MethodicConfigurator/internationalization.py b/MethodicConfigurator/internationalization.py index d5f097e..6b9c146 100644 --- a/MethodicConfigurator/internationalization.py +++ b/MethodicConfigurator/internationalization.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Translations to other written languages. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/MethodicConfigurator/mavftp_example.py b/MethodicConfigurator/mavftp_example.py index b72b522..fb212ee 100755 --- a/MethodicConfigurator/mavftp_example.py +++ b/MethodicConfigurator/mavftp_example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -MAVLink File Transfer Protocol support example +MAVLink File Transfer Protocol support example. SPDX-FileCopyrightText: 2024 Amilcar Lucas @@ -29,9 +29,7 @@ # pylint: disable=duplicate-code def argument_parser() -> Namespace: - """ - Parses command-line arguments for the script. - """ + """Parses command-line arguments for the script.""" parser = ArgumentParser(description="This main is just an example, adapt it to your needs") parser.add_argument("--baudrate", type=int, default=115200, help="master port baud rate. Defaults to %(default)s") parser.add_argument( @@ -88,7 +86,7 @@ def auto_detect_serial() -> list: return serial_list # type: ignore[no-any-return] -def auto_connect(device) -> mavutil.SerialPort: +def auto_connect(device) -> mavutil.SerialPort: # noqa: ANN001 comport = None if device: comport = mavutil.SerialPort(device=device, description=device) @@ -115,8 +113,8 @@ def auto_connect(device) -> mavutil.SerialPort: return comport -def wait_heartbeat(m) -> None: - """wait for a heartbeat so we know the target system IDs""" +def wait_heartbeat(m) -> None: # noqa: ANN001 + """Wait for a heartbeat so we know the target system IDs.""" logging_info("Waiting for flight controller heartbeat") m.wait_heartbeat() logging_info("Got heartbeat from system %u, component %u", m.target_system, m.target_system) @@ -193,7 +191,7 @@ def upload_script(mav_ftp: mavftp, remote_directory: str, local_filename: str, t debug_class_member_variable_changes(mav_ftp) -def debug_class_member_variable_changes(instance) -> None: +def debug_class_member_variable_changes(instance) -> None: # noqa: ANN001 return global old_mavftp_member_variable_values # noqa: PLW0603 pylint: disable=global-statement, unreachable new_mavftp_member_variable_values = instance.__dict__ @@ -216,7 +214,7 @@ def debug_class_member_variable_changes(instance) -> None: def main() -> None: - """for testing/example purposes only""" + """For testing/example purposes only.""" args = argument_parser() logging_basicConfig(level=logging_getLevelName(args.loglevel), format="%(levelname)s - %(message)s") diff --git a/MethodicConfigurator/middleware_template_overview.py b/MethodicConfigurator/middleware_template_overview.py index f8b919d..cc0ce02 100644 --- a/MethodicConfigurator/middleware_template_overview.py +++ b/MethodicConfigurator/middleware_template_overview.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python3 - """ +Middleware between the flight controller information backend and the GUI fronted. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -50,4 +50,4 @@ def columns() -> tuple[str, ...]: ) def attributes(self) -> list[str]: - return self.__dict__.keys() # type: ignore + return self.__dict__.keys() # type: ignore[return-value] diff --git a/MethodicConfigurator/param_pid_adjustment_update.py b/MethodicConfigurator/param_pid_adjustment_update.py index 2370d32..da9ab3c 100755 --- a/MethodicConfigurator/param_pid_adjustment_update.py +++ b/MethodicConfigurator/param_pid_adjustment_update.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 """ -This script updates the PID adjustment parameters to be factor of the corresponding autotuned or optimized parameters. +Updates the PID adjustment parameters to be factor of the corresponding autotuned or optimized parameters. Usage: ./param_pid_adjustment_update.py -d /path/to/directory optimized_parameter_file.param @@ -64,28 +64,29 @@ def parse_arguments() -> argparse.Namespace: "optimized_param_file", help="The name of the optimized parameter file.", ) - args = parser.parse_args() - return args + return parser.parse_args() -def ranged_type(value_type: type, min_value: Union[int, float], max_value: Union[int, float]) -> Callable: +def ranged_type(value_type: type, min_value: float, max_value: float) -> Callable: """ Return function handle of an argument type function for ArgumentParser checking a range: min_value <= arg <= max_value Args: value_type - value-type to convert arg to min_value - minimum acceptable argument value - max_value - maximum acceptable argument value + max_value - maximum acceptable argument value. """ def range_checker(arg: str) -> Union[int, float]: try: f = value_type(arg) except ValueError as exc: - raise argparse.ArgumentTypeError(f"must be a valid {value_type}") from exc + msg = f"must be a valid {value_type}" + raise argparse.ArgumentTypeError(msg) from exc if f < min_value or f > max_value: - raise argparse.ArgumentTypeError(f"must be within [{min_value}, {max_value}]") - return f # type: ignore + msg = f"must be within [{min_value}, {max_value}]" + raise argparse.ArgumentTypeError(msg) + return f # type: ignore[no-any-return] # Return function handle to checking function return range_checker @@ -98,6 +99,7 @@ class Par: Attributes: value (float): The value of the parameter. comment (str): An optional comment describing the parameter. + """ def __init__(self, value: float, comment: Optional[str] = None) -> None: @@ -125,17 +127,22 @@ def load_param_file_into_dict(param_file: str) -> tuple[dict[str, "Par"], list[s elif "\t" in line: parameter, value = line.split("\t", 1) else: - raise SystemExit(f"Missing parameter-value separator: {line} in {param_file} line {n}") + msg = f"Missing parameter-value separator: {line} in {param_file} line {n}" + raise SystemExit(msg) if len(parameter) > PARAM_NAME_MAX_LEN: - raise SystemExit(f"Too long parameter name: {parameter} in {param_file} line {n}") + msg = f"Too long parameter name: {parameter} in {param_file} line {n}" + raise SystemExit(msg) if not re.match(PARAM_NAME_REGEX, parameter): - raise SystemExit(f"Invalid characters in parameter name {parameter} in {param_file} line {n}") + msg = f"Invalid characters in parameter name {parameter} in {param_file} line {n}" + raise SystemExit(msg) try: fvalue = float(value) except ValueError as exc: - raise SystemExit(f"Invalid parameter value {value} in {param_file} line {n}") from exc + msg = f"Invalid parameter value {value} in {param_file} line {n}" + raise SystemExit(msg) from exc if parameter in parameter_dict: - raise SystemExit(f"Duplicated parameter {parameter} in {param_file} line {n}") + msg = f"Duplicated parameter {parameter} in {param_file} line {n}" + raise SystemExit(msg) parameter_dict[parameter] = Par(fvalue, comment) return parameter_dict, content @@ -169,6 +176,7 @@ def update_pid_adjustment_params( directory (str): The directory where the parameter files are located. optimized_param_file (str): The name of the optimized parameter file. adjustment_factor (float): The adjustment factor to apply to the optimized parameters. + """ default_param_file_path = os.path.join(directory, "00_default.param") optimized_param_file_path = os.path.join(directory, optimized_param_file) @@ -184,20 +192,25 @@ def update_pid_adjustment_params( pid_adjustment_params_dict, content = Par.load_param_file_into_dict(pid_adjustment_file_path) if not default_params_dict: - raise SystemExit(f"Failed to load default parameters from {default_param_file_path}") + msg = f"Failed to load default parameters from {default_param_file_path}" + raise SystemExit(msg) if not optimized_params_dict: - raise SystemExit(f"Failed to load optimized parameters from {optimized_param_file_path}") + msg = f"Failed to load optimized parameters from {optimized_param_file_path}" + raise SystemExit(msg) if not pid_adjustment_params_dict: - raise SystemExit(f"Failed to load PID adjustment parameters from {pid_adjustment_file_path}") + msg = f"Failed to load PID adjustment parameters from {pid_adjustment_file_path}" + raise SystemExit(msg) # Update the PID adjustment parameters based on the given adjustment factor for param_name, param_value in pid_adjustment_params_dict.items(): if param_name not in optimized_params_dict: - raise SystemExit(f"Parameter {param_name} is not present in {optimized_param_file_path}") + msg = f"Parameter {param_name} is not present in {optimized_param_file_path}" + raise SystemExit(msg) if param_name not in default_params_dict: - raise SystemExit(f"Parameter {param_name} is not present in {default_param_file_path}") + msg = f"Parameter {param_name} is not present in {default_param_file_path}" + raise SystemExit(msg) # adjust the parameter value param_value.value = optimized_params_dict[param_name].value * adjustment_factor if default_params_dict[param_name].value != 0: diff --git a/MethodicConfigurator/tempcal_imu.py b/MethodicConfigurator/tempcal_imu.py old mode 100644 new mode 100755 index 07096f5..0374fc7 --- a/MethodicConfigurator/tempcal_imu.py +++ b/MethodicConfigurator/tempcal_imu.py @@ -42,7 +42,7 @@ class Coefficients: # pylint: disable=too-many-instance-attributes - """class representing a set of coefficients""" + """class representing a set of coefficients.""" def __init__(self) -> None: self.acoef: dict = {} @@ -105,7 +105,7 @@ def set_enable(self, imu: int, value: int) -> None: self.enable[imu] = value def correction(self, coeff: dict, imu: int, temperature: float, axis: str, cal_temp: float) -> float: # pylint: disable=too-many-arguments, too-many-positional-arguments - """calculate correction from temperature calibration from log data using parameters""" + """Calculate correction from temperature calibration from log data using parameters.""" if self.enable[imu] != 1.0: return 0.0 if cal_temp < -80: @@ -118,8 +118,10 @@ def correction(self, coeff: dict, imu: int, temperature: float, axis: str, cal_t return poly(cal_temp - TEMP_REF) - poly(temperature - TEMP_REF) # type: ignore[no-any-return] def correction_accel(self, imu: int, temperature: float) -> Vector3: - """calculate accel correction from temperature calibration from - log data using parameters""" + """ + calculate accel correction from temperature calibration from + log data using parameters. + """ cal_temp = self.atcal.get(imu, TEMP_REF) return Vector3( self.correction(self.acoef[imu], imu, temperature, "X", cal_temp), @@ -128,8 +130,10 @@ def correction_accel(self, imu: int, temperature: float) -> Vector3: ) def correction_gyro(self, imu: int, temperature: float) -> Vector3: - """calculate gyro correction from temperature calibration from - log data using parameters""" + """ + calculate gyro correction from temperature calibration from + log data using parameters. + """ cal_temp = self.gtcal.get(imu, TEMP_REF) return Vector3( self.correction(self.gcoef[imu], imu, temperature, "X", cal_temp), @@ -152,7 +156,7 @@ def param_string(self, imu: int) -> str: class OnlineIMUfit: # pylint: disable=too-few-public-methods - """implement the online learning used in ArduPilot""" + """implement the online learning used in ArduPilot.""" def __init__(self) -> None: self.porder: int = 0 @@ -204,7 +208,7 @@ def __init__(self) -> None: self.gyro: dict[int, dict[str, np.ndarray]] = {} def IMUs(self) -> list[int]: - """return list of IMUs""" + """Return list of IMUs.""" if len(self.accel.keys()) != len(self.gyro.keys()): print("accel and gyro data doesn't match") sys.exit(1) @@ -233,13 +237,13 @@ def add_gyro(self, imu: int, temperature: float, time: float, value: Vector3) -> self.gyro[imu]["time"] = np.append(self.gyro[imu]["time"], time) def moving_average(self, data: np.ndarray, w: int) -> np.ndarray: - """apply a moving average filter over a window of width w""" + """Apply a moving average filter over a window of width w.""" ret = np.cumsum(data) ret[w:] = ret[w:] - ret[:-w] - return ret[w - 1 :] / w # type: ignore[no-any-return] # mypy bug + return ret[w - 1 :] / w def FilterArray(self, data: dict[str, np.ndarray], width_s: int) -> dict[str, np.ndarray]: - """apply moving average filter of width width_s seconds""" + """Apply moving average filter of width width_s seconds.""" nseconds = data["time"][-1] - data["time"][0] nsamples = len(data["time"]) window = int(nsamples / nseconds) * width_s @@ -249,13 +253,13 @@ def FilterArray(self, data: dict[str, np.ndarray], width_s: int) -> dict[str, np return data def Filter(self, width_s: int) -> None: - """apply moving average filter of width width_s seconds""" + """Apply moving average filter of width width_s seconds.""" for imu in self.IMUs(): self.accel[imu] = self.FilterArray(self.accel[imu], width_s) self.gyro[imu] = self.FilterArray(self.gyro[imu], width_s) def accel_at_temp(self, imu: int, axis: str, temperature: float) -> float: - """return the accel value closest to the given temperature""" + """Return the accel value closest to the given temperature.""" if temperature < self.accel[imu]["T"][0]: return self.accel[imu][axis][0] # type: ignore[no-any-return] for i in range(len(self.accel[imu]["T"]) - 1): @@ -267,7 +271,7 @@ def accel_at_temp(self, imu: int, axis: str, temperature: float) -> float: return self.accel[imu][axis][-1] # type: ignore[no-any-return] def gyro_at_temp(self, imu: int, axis: str, temperature: float) -> float: - """return the gyro value closest to the given temperature""" + """Return the gyro value closest to the given temperature.""" if temperature < self.gyro[imu]["T"][0]: return self.gyro[imu][axis][0] # type: ignore[no-any-return] for i in range(len(self.gyro[imu]["T"]) - 1): @@ -279,11 +283,10 @@ def gyro_at_temp(self, imu: int, axis: str, temperature: float) -> float: return self.gyro[imu][axis][-1] # type: ignore[no-any-return] -def constrain(value: Union[float, int], minv: Union[float, int], maxv: Union[float, int]) -> Union[float, int]: +def constrain(value: float, minv: float, maxv: float) -> Union[float, int]: """Constrain a value to a range.""" value = min(minv, value) - value = max(maxv, value) - return value + return max(maxv, value) def IMUfit( # noqa: PLR0915 pylint: disable=too-many-locals, too-many-branches, too-many-statements, too-many-arguments, too-many-positional-arguments @@ -296,7 +299,7 @@ def IMUfit( # noqa: PLR0915 pylint: disable=too-many-locals, too-many-branches, figpath: Union[str, None], progress_callback: Union[Callable[[int], None], None], ) -> None: - """find IMU calibration parameters from a log file""" + """Find IMU calibration parameters from a log file.""" print(f"Processing log {logfile}") mlog = mavutil.mavlink_connection(logfile, progress_callback=progress_callback) diff --git a/copy_magfit_pdef_to_template_dirs.py b/copy_magfit_pdef_to_template_dirs.py index 6660f88..39c337d 100755 --- a/copy_magfit_pdef_to_template_dirs.py +++ b/copy_magfit_pdef_to_template_dirs.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Copy 24_inflight_magnetometer_fit_setup.pdef.xml file to all vehicle template subdirectories. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/copy_param_files.py b/copy_param_files.py old mode 100644 new mode 100755 index 0a194a0..4f5d375 --- a/copy_param_files.py +++ b/copy_param_files.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Copy some parameter files. For dev only. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -22,7 +24,7 @@ # Function to get all subdirectories excluding the source (do not copy onto itself) -def get_subdirectories(base_dir, exclude_source=True) -> list[str]: +def get_subdirectories(base_dir: str, exclude_source: bool = True) -> list[str]: subdirs = [] for root, dirs, _ in os.walk(base_dir): rel_dir = os.path.relpath(root, base_dir) @@ -34,7 +36,7 @@ def get_subdirectories(base_dir, exclude_source=True) -> list[str]: # Function to copy files -def copy_files(source, target) -> None: +def copy_files(source: str, target: str) -> None: for file in files_to_copy: source_path = os.path.join(source, file) target_path = os.path.join(target, file) diff --git a/create_mo_files.py b/create_mo_files.py index 5bc4c2c..551bf9a 100755 --- a/create_mo_files.py +++ b/create_mo_files.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Create .mo files from the .po files. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -12,7 +14,7 @@ import subprocess -def process_locale_directory(locale_dir) -> None: +def process_locale_directory(locale_dir: str) -> None: """Process a single locale directory.""" po_file = os.path.join(locale_dir, "MethodicConfigurator.po") mo_file = os.path.join(locale_dir, "MethodicConfigurator.mo") diff --git a/create_pot_file.py b/create_pot_file.py index 51bc501..c2e9b83 100755 --- a/create_pot_file.py +++ b/create_pot_file.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Create the .pot file by extracting strings from the python source code files. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -12,7 +14,7 @@ import subprocess -def extract_strings(directory, output_dir) -> None: +def extract_strings(directory: str, output_dir: str) -> None: file_paths = [] for root, _dirs, files in os.walk(directory): for file in files: diff --git a/credits/update_credits_licenses.py b/credits/update_credits_licenses.py old mode 100644 new mode 100755 index bb3b419..7c91abf --- a/credits/update_credits_licenses.py +++ b/credits/update_credits_licenses.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -This script downloads the licenses of the direct and indirect dependencies of the project +Downloads the licenses of the direct and indirect dependencies of the project. This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator @@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later """ -import requests # type: ignore[import-untyped] +import requests # List of direct_dependencies and their license URLs direct_dependencies = [ @@ -48,7 +48,7 @@ ] -def download_license(package_name, license_url) -> None: +def download_license(package_name: str, license_url: str) -> None: try: response = requests.get(license_url, timeout=10) response.raise_for_status() # Raise an exception if the request failed diff --git a/extract_missing_translations.py b/extract_missing_translations.py old mode 100644 new mode 100755 index 1afdd4d..8b2f3b6 --- a/extract_missing_translations.py +++ b/extract_missing_translations.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Extract missing translations from a .po file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/get_release_stats.py b/get_release_stats.py old mode 100644 new mode 100755 index 21db277..c28b143 --- a/get_release_stats.py +++ b/get_release_stats.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Based on https://pagure.io/github-release-stats/blob/master/f/get_release_stats.py by Clement Verna +Based on https://pagure.io/github-release-stats/blob/master/f/get_release_stats.py by Clement Verna. This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator @@ -17,7 +17,7 @@ from github import Github -def compute_average(issues_date) -> float: +def compute_average(issues_date: list[tuple[int, int]]) -> float: sum_of_issues = sum(days for _issue, days in issues_date) if len(issues_date): diff --git a/insert_translations.py b/insert_translations.py old mode 100644 new mode 100755 index c37a63b..34aea24 --- a/insert_translations.py +++ b/insert_translations.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Insert bulk translations into an existing .po file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/param_reorder.py b/param_reorder.py index ee379d3..9496cd4 100755 --- a/param_reorder.py +++ b/param_reorder.py @@ -1,8 +1,7 @@ #!/usr/bin/python3 """ -This script inserts and/or removes parameter files in the configuration sequence -defined in the configuration_steps_ArduCopter.json file. +Inserts and/or removes parameter files in the configuration sequence defined in the configuration_steps_ArduCopter.json file. It also replaces all occurrences of the old names with the new names in all *.py and *.md files in the current directory. @@ -33,7 +32,7 @@ file_renames["00_Default_Parameters.param"] = "00_default.param" -def reorder_param_files(steps) -> dict[str, str]: +def reorder_param_files(steps: dict) -> dict[str, str]: """Reorder parameters and prepare renaming rules.""" # Iterate over the param_files and rename the keys to be in two-digit prefix ascending order param_files = list(steps) @@ -50,7 +49,7 @@ def reorder_param_files(steps) -> dict[str, str]: return renames -def loop_relevant_files(renames, steps) -> list[str]: +def loop_relevant_files(renames: dict[str, str], steps: dict) -> list[str]: param_dirs = ["."] # Search all *.py, *.json and *.md files in the current directory # and replace all occurrences of the old names with the new names @@ -64,12 +63,12 @@ def loop_relevant_files(renames, steps) -> list[str]: continue if file == SEQUENCE_FILENAME: uplate_old_filenames(renames, steps) - if file in PYTHON_FILES or file.endswith(".md") or file.endswith(".json"): + if file in PYTHON_FILES or file.endswith((".md", ".json")): update_file_contents(renames, root, file, steps) return param_dirs -def uplate_old_filenames(renames, steps) -> None: +def uplate_old_filenames(renames: dict[str, str], steps: dict) -> None: for new_name, old_name in renames.items(): if old_name != new_name: if "old_filenames" in steps[old_name]: @@ -79,7 +78,7 @@ def uplate_old_filenames(renames, steps) -> None: steps[old_name]["old_filenames"] = [old_name] -def update_file_contents(renames, root, file, steps) -> None: +def update_file_contents(renames: dict[str, str], root: str, file: str, steps: dict) -> None: with open(os.path.join(root, file), encoding="utf-8") as handle: file_content = handle.read() if file.startswith("TUNING_GUIDE_") and file.endswith(".md"): @@ -95,7 +94,7 @@ def update_file_contents(renames, root, file, steps) -> None: handle.write(file_content) -def update_configuration_steps_json_file_contents(steps, file_content, new_name, old_name) -> str: +def update_configuration_steps_json_file_contents(steps: dict, file_content: str, new_name: str, old_name: str) -> str: new_file_content = "" curr_filename = "" for line in file_content.splitlines(keepends=True): @@ -117,7 +116,7 @@ def update_configuration_steps_json_file_contents(steps, file_content, new_name, return new_file_content -def rename_file(old_name, new_name, param_dir) -> None: +def rename_file(old_name: str, new_name: str, param_dir: str) -> None: """Rename a single file.""" old_name_path = os.path.join(param_dir, old_name) new_name_path = os.path.join(param_dir, new_name) @@ -127,7 +126,7 @@ def rename_file(old_name, new_name, param_dir) -> None: print(f"Error: Could not rename file {old_name_path}, file not found") -def reorder_actual_files(renames, param_dirs) -> None: +def reorder_actual_files(renames: dict[str, str], param_dirs: list[str]) -> None: # Rename the actual files on disk based on renames re-ordering for param_dir in param_dirs: for new_name, old_name in renames.items(): @@ -138,7 +137,7 @@ def change_line_endings_for_md_files() -> None: # Change line endings of TUNING_GUIDE_*.md, README.md files to CRLF for root, _dirs, files in os.walk("."): for file in files: - if (file.startswith("README") or file.startswith("TUNING_GUIDE_")) and file.endswith(".md"): + if (file.startswith(("README", "TUNING_GUIDE_"))) and file.endswith(".md"): file_path = os.path.join(root, file) with open(file_path, "rb") as handle: content = handle.read() diff --git a/pyproject.toml b/pyproject.toml index ad655df..54a4366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,37 +85,67 @@ exclude = [ "dist", ] +# https://docs.astral.sh/ruff/rules/ lint.select = [ - "F", # pycodestyle - "E", # Pyflakes - "W", # Warnings - "U", # pyupgrade + "F", # Pyflakes + "E", # pycodestyle -Error + "W", # pycodestyle - Warning + #"C901", # maccabe "I", # isort "N", # pep8-naming - "PL", # Pylint + "D", # pydocstyle + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "ASYNC",# flake8-async + "S", # flake8-bandit + #"BLE", # flake8-blind-except + #"FBT", # flake8-boolean-trap "B", # flake8-bugbear - "RUF", # Ruff-specific codes - "PERF", # Performance-related issues - "SIM", # flake8-simplify + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "EM", # flake8-errmsg + "EXE", # flake8-executable + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions "LOG", # flake8-logging "G", # flake8-logging-format - #"C901", # maccabe - "S", # flake8-bandit + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + #"T20", # flake8-print (T20) + "PYI", # flake8-pyi (PYI) + #"PT", # flake8-pytest-style (PT) + "Q", # flake8-quotes (Q) + "RSE", # flake8-raise (RSE) + "RET", # flake8-return (RET) + "SLF", # flake8-self (SLF) + "SLOT", # flake8-slots (SLOT) + "SIM", # flake8-simplify "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking + "TC", # flake8-type-checking "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + #"PTH", # flake8-use-pathlib + #"TD", # flake8-todos + #"FIX", # flake8-fixme + #"ERA", # eradicate + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PL", # Pylint "FLY002", # flynt - #"FURB", # refurb - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "ANN", # flake8-annotations + "PERF", # Performance-related issues + "FURB", # refurb + "DOC", # pydoclint + "RUF", # Ruff-specific codes ] lint.ignore = [ "PLR0912", # too many branches "PLR0913", # too many arguments "PLR2004", # Magic value used in comparison, consider replacing `X` with a constant variable - "N801", # Class name `FTP_OP` should use CapWords convention "N802", # Function name `X` should be lowercase "N806", # Variable name `X` should be lowercase "N812", # Lowercase `x` imported as non-lowercase `X` @@ -123,17 +153,28 @@ lint.ignore = [ "N817", # CamelCase `X` imported as acronym `Y` "N999", # Invalid module name: 'MethodicConfigurator' "ISC001", # to let formatter run - "ANN001", # Missing type annotation for function argument "ANN002", "ANN003", - "ANN101", # Missing type annotation for self in method + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D107", # Missing docstring in `__init__` + "D203", # + "D205", # 1 blank line required between summary line and description + "D212", # Multi-line docstring summary should start at the first line + "D404", # First word of the docstring should not be "This" + "D401", # First line of docstring should be in imperative mood + "COM812", + "DTZ005", # `tz=None` passed to `datetime.datetime.now()` ] line-length = 127 indent-width = 4 [tool.ruff.lint.per-file-ignores] -"unittests/*" = ["UP031"] +"unittests/*" = ["D101","UP031", "ARG002", "ANN001"] +"MethodicConfigurator/backend_mavftp.py" = ["PGH004", "N801", "ANN001"] +"MethodicConfigurator/backend_mavftp_example.py" = ["ANN001"] +"MethodicConfigurator/tempcal_imu.py" = ["ANN001"] [tool.mypy] ignore_missing_imports = true diff --git a/setup.py b/setup.py index f246e2b..eb003a8 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -This script creates the MethodicConfigurator pip python package +Creates the MethodicConfigurator pip python package. This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator diff --git a/unittests/annotate_params_test.py b/unittests/annotate_params_test.py index 722eb3e..0c7ecb5 100755 --- a/unittests/annotate_params_test.py +++ b/unittests/annotate_params_test.py @@ -1,6 +1,8 @@ #!/usr/bin/python3 """ +Unittest for the annotate_params.py script. + These are the unit tests for the python script that fetches online ArduPilot parameter documentation (if not cached) and adds it to the specified file or to all *.param and *.parm files in the specified directory. @@ -109,7 +111,7 @@ def test_get_xml_data_remote_file(self, mock_get) -> None: @patch("annotate_params.Par.load_param_file_into_dict") def test_get_xml_data_script_dir_file(self, mock_load_param, mock_isfile) -> None: # Mock the isfile function to return False for the current directory and True for the script directory - def side_effect(filename) -> bool: + def side_effect(filename) -> bool: # noqa: ARG001 return True mock_isfile.side_effect = side_effect diff --git a/unittests/ardupilot_methodic_configurator_test.py b/unittests/ardupilot_methodic_configurator_test.py index 4c3b8bb..1a4f526 100755 --- a/unittests/ardupilot_methodic_configurator_test.py +++ b/unittests/ardupilot_methodic_configurator_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the ardupilot_methodic_configurator.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/backend_filesystem_test.py b/unittests/backend_filesystem_test.py index 0570462..525e4f4 100755 --- a/unittests/backend_filesystem_test.py +++ b/unittests/backend_filesystem_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the backend_filesystem.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/backend_mavftp_test.py b/unittests/backend_mavftp_test.py index 9d5e41e..5502c7b 100755 --- a/unittests/backend_mavftp_test.py +++ b/unittests/backend_mavftp_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the backend_mavftp.py file. + MAVLink File Transfer Protocol support test - https://mavlink.io/en/services/ftp.html This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator @@ -51,7 +53,7 @@ class TestMAVFTPPayloadDecoding(unittest.TestCase): - """Test MAVFTP payload decoding""" + """Test MAVFTP payload decoding.""" def setUp(self) -> None: self.log_stream = StringIO() @@ -224,7 +226,7 @@ def test_decode_ftp_ack_and_nack(self) -> None: ] for case in test_cases: - ret = self.mav_ftp._MAVFTP__decode_ftp_ack_and_nack(case["op"]) # pylint: disable=protected-access + ret = self.mav_ftp._MAVFTP__decode_ftp_ack_and_nack(case["op"]) # noqa: SLF001, pylint: disable=protected-access ret.display_message() log_output = self.log_stream.getvalue().strip() self.assertIn( @@ -245,7 +247,7 @@ def test_decode_ftp_ack_and_nack(self) -> None: # Test for unknown error code in display_message op = self.ftp_operation(seq=22, opcode=OP_Nack, req_opcode=OP_ListDirectory, payload=bytes([255])) - ret = self.mav_ftp._MAVFTP__decode_ftp_ack_and_nack(op, "ListDirectory") # pylint: disable=protected-access + ret = self.mav_ftp._MAVFTP__decode_ftp_ack_and_nack(op, "ListDirectory") # noqa: SLF001, pylint: disable=protected-access ret.error_code = 125 # Set error code to 125 to trigger unknown error message ret.display_message() log_output = self.log_stream.getvalue().strip() diff --git a/unittests/battery_cell_voltages_test.py b/unittests/battery_cell_voltages_test.py old mode 100644 new mode 100755 index 98beb80..93ba787 --- a/unittests/battery_cell_voltages_test.py +++ b/unittests/battery_cell_voltages_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the battery_cell_voltages.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/common_arguments_test.py b/unittests/common_arguments_test.py old mode 100644 new mode 100755 index 139fc22..048ed64 --- a/unittests/common_arguments_test.py +++ b/unittests/common_arguments_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the common_arguments.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/extract_param_defaults_test.py b/unittests/extract_param_defaults_test.py index 534b35d..c9fe788 100755 --- a/unittests/extract_param_defaults_test.py +++ b/unittests/extract_param_defaults_test.py @@ -1,7 +1,9 @@ #!/usr/bin/python3 """ -Extracts parameter default values from an ArduPilot .bin log file. Unittests. +Unittests for the extract_param_defaults.py file. + +Extracts parameter default values from an ArduPilot .bin log file. SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/frontend_tkinter_test.py b/unittests/frontend_tkinter_test.py index 2ca50ef..c034a77 100755 --- a/unittests/frontend_tkinter_test.py +++ b/unittests/frontend_tkinter_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittest for the frontend_tkinter.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/middleware_template_overview_test.py b/unittests/middleware_template_overview_test.py old mode 100644 new mode 100755 index 219b23f..3ab9def --- a/unittests/middleware_template_overview_test.py +++ b/unittests/middleware_template_overview_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the middleware_template_overview.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas diff --git a/unittests/param_pid_adjustment_update_test.py b/unittests/param_pid_adjustment_update_test.py index 14e12e1..322cea2 100755 --- a/unittests/param_pid_adjustment_update_test.py +++ b/unittests/param_pid_adjustment_update_test.py @@ -1,7 +1,9 @@ #!/usr/bin/python3 """ -This script updates the PID adjustment parameters to be factor of the corresponding autotuned or optimized parameters. +Unittests for the para_pid_adjustment_update.py file. + +Updates the PID adjustment parameters to be factor of the corresponding autotuned or optimized parameters. Usage: ./param_pid_adjustment_update.py -d /path/to/directory optimized_parameter_file.param diff --git a/unittests/version_test.py b/unittests/version_test.py old mode 100644 new mode 100755 index 197a6b5..bbfc9eb --- a/unittests/version_test.py +++ b/unittests/version_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ +Unittests for the __init__.py file. + This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas @@ -15,9 +17,7 @@ class TestVersion(unittest.TestCase): - """ - Test that the __version__ constant is a string and follows semantic versioning. - """ + """Test that the __version__ constant is a string and follows semantic versioning.""" def test_version_format(self) -> None: # Semantic versioning pattern diff --git a/windows/return_version.py b/windows/return_version.py index 0038229..ce69023 100755 --- a/windows/return_version.py +++ b/windows/return_version.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """ -This script returns the current version number +Returns python package current version number. + Used as part of building the Windows setup file (MethodicConfiguratorWinBuild.bat) This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator