Skip to content

Commit c2a7c13

Browse files
committed
refactor(python): drop Python 3.9 support, require Python 3.10+
BREAKING CHANGE: Python 3.9 is EOL and no longer supported. - Update requires-python to >=3.10 in pyproject.toml - Remove Python 3.9 classifier and 3.9-only matplotlib dependency - Set ruff target-version to py310 - Update CI matrix minimum version from 3.9 to 3.10 (pytest, pylint, pyright workflows) - Modernize 512 typing annotations: Optional[X]?X|None, Union[X,Y]?X|Y - Remove redundant Optional/Union imports from typing module - Update test asserting UnionType origin to use types.UnionType instead of typing.Unionfix tests
1 parent f41b8e1 commit c2a7c13

106 files changed

Lines changed: 599 additions & 623 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
runs-on: ubuntu-latest
2424
strategy:
2525
matrix:
26-
python-version: ["3.9", "3.14"]
26+
python-version: ["3.10", "3.14"]
2727

2828
steps:
2929
- name: Harden the runner (Audit all outbound calls)

.github/workflows/pyright.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
runs-on: ubuntu-latest
2424
strategy:
2525
matrix:
26-
python-version: ["3.9", "3.14"]
26+
python-version: ["3.10", "3.14"]
2727

2828
steps:
2929
- name: Harden the runner (Audit all outbound calls)

.github/workflows/pytest.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
python-version: ["3.14"] # to make sure the latest AMC supported python version does not break
3232
include:
3333
- os: ubuntu-latest
34-
python-version: "3.9" # to make sure the minimum AMC supported python version does not break
34+
python-version: "3.10" # to make sure the minimum AMC supported python version does not break
3535
# this is also the version used to report test coverage, other versions do not report coverage
3636

3737
steps:
@@ -269,7 +269,7 @@ jobs:
269269
with:
270270
github-token: ${{ secrets.GITHUB_TOKEN }}
271271
parallel-finished: true
272-
carryforward: "run-ubuntu-latest-py3.9,run-ubuntu-latest-py3.14,run-windows-latest-py3.14,run-macos-latest-py3.14"
272+
carryforward: "run-ubuntu-latest-py3.10,run-ubuntu-latest-py3.14,run-windows-latest-py3.14,run-macos-latest-py3.14"
273273

274274
# TODO: create a badge that presents the result of the Upload coverage xml report step
275275

@@ -345,14 +345,14 @@ jobs:
345345
if: needs.pytest.result != 'skipped'
346346
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
347347
with:
348-
name: coverage-ubuntu-latest-3.9
348+
name: coverage-ubuntu-latest-3.10
349349

350350
# https://docs.astral.sh/uv/guides/integration/github/
351351
- name: Install uv and set the Python version
352352
if: needs.pytest.result != 'skipped'
353353
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
354354
with:
355-
python-version: '3.9' # Match with the coverage report Python version
355+
python-version: '3.10' # Match with the coverage report Python version
356356
activate-environment: true
357357
enable-cache: true
358358
cache-dependency-glob: "pyproject.toml"
@@ -389,7 +389,7 @@ jobs:
389389
- name: Download coverage xml report
390390
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
391391
with:
392-
name: coverage-ubuntu-latest-3.9-xml
392+
name: coverage-ubuntu-latest-3.10-xml
393393

394394
- name: Get Cover
395395
uses: orgoro/coverage@71cf993a407154ad9d8dd027c88a374b0ed002a9 # v3.3

ardupilot_methodic_configurator/__main__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from logging import warning as logging_warning
3030
from pathlib import Path
3131
from sys import exit as sys_exit
32-
from typing import Union
3332

3433
import argcomplete
3534

@@ -121,7 +120,7 @@ def __init__(self, args: argparse.Namespace) -> None:
121120
self.vehicle_type: str = ""
122121
self.param_default_values: ParDict = ParDict()
123122
self.local_filesystem: LocalFilesystem = None # type: ignore[assignment]
124-
self.vehicle_project_manager: Union[VehicleProjectManager, None] = None
123+
self.vehicle_project_manager: VehicleProjectManager | None = None
125124
self.param_default_values_dirty: bool = False
126125

127126

@@ -401,7 +400,7 @@ def initialize_flight_controller_and_filesystem(state: ApplicationState) -> None
401400
initialize_filesystem(state)
402401

403402

404-
def vehicle_directory_selection(state: ApplicationState) -> Union[VehicleProjectOpenerWindow, None]:
403+
def vehicle_directory_selection(state: ApplicationState) -> VehicleProjectOpenerWindow | None:
405404
"""
406405
Handle vehicle directory selection if no parameter files are found in the current working directory.
407406
@@ -444,7 +443,7 @@ def create_and_configure_component_editor(
444443
local_filesystem: LocalFilesystem,
445444
flight_controller: FlightController,
446445
vehicle_type: str,
447-
vehicle_project_manager: Union[None, VehicleProjectManager],
446+
vehicle_project_manager: None | VehicleProjectManager,
448447
) -> ComponentEditorWindow:
449448
"""
450449
Create and configure the component editor window.

ardupilot_methodic_configurator/annotate_params.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import re
3131
from os import path as os_path
3232
from sys import exit as sys_exit
33-
from typing import Any, Optional
33+
from typing import Any
3434
from xml.etree import ElementTree as ET # no parsing, just data-structure manipulation
3535

3636
import argcomplete
@@ -139,7 +139,7 @@ def check_max_line_length(value: int) -> int:
139139

140140

141141
def get_xml_data(
142-
base_url: str, directory: str, filename: str, vehicle_type: str, fallback_xml_url: Optional[str] = None
142+
base_url: str, directory: str, filename: str, vehicle_type: str, fallback_xml_url: str | None = None
143143
) -> ET.Element:
144144
"""
145145
Fetch XML data from a local file or a URL.
@@ -418,7 +418,7 @@ def update_parameter_documentation(
418418
doc: dict[str, Any],
419419
target: str = ".",
420420
sort_type: str = "none",
421-
param_default_dict: Optional[ParDict] = None,
421+
param_default_dict: ParDict | None = None,
422422
delete_documentation_annotations: bool = False,
423423
) -> None:
424424
"""
@@ -595,7 +595,7 @@ def get_fallback_xml_url(vehicle_type: str, firmware_version: str) -> str:
595595

596596

597597
def parse_parameter_metadata( # pylint: disable=too-many-arguments, too-many-positional-arguments
598-
xml_url: str, xml_dir: str, xml_file: str, vehicle_type: str, max_line_length: int, fallback_xml_url: Optional[str] = None
598+
xml_url: str, xml_dir: str, xml_file: str, vehicle_type: str, max_line_length: int, fallback_xml_url: str | None = None
599599
) -> dict[str, Any]:
600600
xml_root = get_xml_data(xml_url, xml_dir, xml_file, vehicle_type, fallback_xml_url)
601601
return create_doc_dict(xml_root, vehicle_type, max_line_length)

ardupilot_methodic_configurator/argparse_check_range.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from argparse import Action, ArgumentError, ArgumentParser, Namespace
1414
from collections.abc import Sequence
1515
from operator import ge, gt, le, lt
16-
from typing import Any, Union
16+
from typing import Any
1717

1818
from ardupilot_methodic_configurator import _
1919

@@ -56,8 +56,8 @@ def __call__(
5656
self,
5757
parser: ArgumentParser, # noqa: ARG002
5858
namespace: Namespace,
59-
values: Union[str, Sequence[Any], None],
60-
option_string: Union[None, str] = None, # noqa: ARG002
59+
values: str | Sequence[Any] | None,
60+
option_string: None | str = None, # noqa: ARG002
6161
) -> None:
6262
if not isinstance(values, (int, float)):
6363
raise ArgumentError(self, _("Value must be a number."))

ardupilot_methodic_configurator/backend_filesystem.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from shutil import copytree as shutil_copytree
3131
from shutil import rmtree as shutil_rmtree
3232
from subprocess import SubprocessError, run
33-
from typing import Any, Optional, Union
33+
from typing import Any
3434
from zipfile import ZipFile
3535

3636
from argcomplete.completers import DirectoriesCompleter
@@ -201,7 +201,7 @@ def _format_columns_sorted_numerically( # pylint: disable=too-many-locals
201201
return []
202202

203203
# Sort values numerically by key
204-
def sort_key(item: tuple[str, Any]) -> tuple[int, Union[int, float, str]]:
204+
def sort_key(item: tuple[str, Any]) -> tuple[int, int | float | str]:
205205
key = item[0]
206206
try:
207207
return (0, int(key)) # sort integers and floats together
@@ -358,7 +358,7 @@ def read_params_from_files(self) -> dict[str, ParDict]:
358358
logging_error(_("Error: %s is not a directory."), self.vehicle_dir)
359359
return parameters
360360

361-
def compound_params(self, last_filename: Optional[str] = None, skip_default: bool = True) -> tuple[ParDict, Optional[str]]:
361+
def compound_params(self, last_filename: str | None = None, skip_default: bool = True) -> tuple[ParDict, str | None]:
362362
"""
363363
Compound parameters from multiple .param files into a single ParDict.
364364
@@ -399,7 +399,7 @@ def compound_params(self, last_filename: Optional[str] = None, skip_default: boo
399399
return compound, first_config_step_filename
400400

401401
@staticmethod
402-
def str_to_bool(s: str) -> Optional[bool]:
402+
def str_to_bool(s: str) -> bool | None:
403403
"""
404404
Converts a string representation of a boolean value to a boolean.
405405
@@ -606,7 +606,7 @@ def copy_template_files_to_new_vehicle_dir( # pylint: disable=too-many-argument
606606
blank_change_reason: bool,
607607
copy_vehicle_image: bool,
608608
use_fc_params: bool = False,
609-
fc_parameters: Optional[dict[str, float]] = None,
609+
fc_parameters: dict[str, float] | None = None,
610610
) -> str:
611611
# Copy the template files to the new vehicle directory
612612
try:
@@ -662,7 +662,7 @@ def _transform_param_dict(
662662
params: ParDict,
663663
blank_change_reason: bool,
664664
use_fc_params: bool,
665-
fc_parameters: Optional[dict[str, float]],
665+
fc_parameters: dict[str, float] | None,
666666
) -> None:
667667
"""
668668
Apply in-place transformations to a parameter dict during template copy.
@@ -845,7 +845,7 @@ def get_eval_variables(self) -> dict[str, dict[str, Any]]:
845845
def calculate_derived_and_forced_param_changes(
846846
self,
847847
fc_param_names: list[str],
848-
fc_parameters: Optional[dict[str, float]] = None,
848+
fc_parameters: dict[str, float] | None = None,
849849
) -> dict[str, ParDict]:
850850
"""
851851
Compute updated parameter values for all configuration files.
@@ -968,8 +968,8 @@ def merge_forced_or_derived_parameters(
968968
self,
969969
filename: str,
970970
new_parameters: dict[str, ParDict],
971-
fc_param_names: Optional[list[str]],
972-
target: Optional[ParDict] = None,
971+
fc_param_names: list[str] | None,
972+
target: ParDict | None = None,
973973
) -> bool:
974974
"""
975975
Merge forced or derived parameter values into a target parameter dict.

ardupilot_methodic_configurator/backend_filesystem_configuration_steps.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from math import isfinite
1717
from os import path as os_path
1818
from re import search as re_search
19-
from typing import Any, Optional, TypedDict, Union
19+
from typing import Any, TypedDict
2020

2121
# from sys import exit as sys_exit
2222
from jsonschema import validate as json_validate
@@ -310,7 +310,7 @@ def _ensure_file_entry(destination: dict[str, ParDict], filename: str) -> None:
310310

311311
def _eval_new_value( # pylint: disable=too-many-arguments, too-many-positional-arguments
312312
self,
313-
new_value_expr: Union[str, float, bool],
313+
new_value_expr: str | float | bool,
314314
filename: str,
315315
parameter: str,
316316
parameter_type: str,
@@ -528,7 +528,7 @@ def compute_parameters( # pylint: disable=too-many-arguments, too-many-position
528528
return "\n".join(errors)
529529

530530
def compute_add_parameters(
531-
self, filename: str, file_info: dict, variables: dict, existing_params: Optional[ParDict] = None
531+
self, filename: str, file_info: dict, variables: dict, existing_params: ParDict | None = None
532532
) -> None:
533533
"""
534534
Compute the add_parameters for a given configuration file.
@@ -669,7 +669,7 @@ def get_sorted_phases_with_end_and_weight(self, total_files: int) -> dict[str, P
669669

670670
return sorted_phases
671671

672-
def get_component(self, selected_file: str) -> Optional[str]:
672+
def get_component(self, selected_file: str) -> str | None:
673673
"""
674674
Get the component name for the selected file.
675675
@@ -684,7 +684,7 @@ def get_component(self, selected_file: str) -> Optional[str]:
684684
return self.configuration_steps[selected_file].get("component")
685685
return None
686686

687-
def get_plugin(self, selected_file: str) -> Optional[dict]:
687+
def get_plugin(self, selected_file: str) -> dict | None:
688688
"""
689689
Get the plugin configuration for the selected file.
690690
@@ -699,7 +699,7 @@ def get_plugin(self, selected_file: str) -> Optional[dict]:
699699
return self.configuration_steps[selected_file].get("plugin")
700700
return None
701701

702-
def get_instructions_popup(self, selected_file: str) -> Optional[dict]:
702+
def get_instructions_popup(self, selected_file: str) -> dict | None:
703703
"""Get the instructions popup configuration for the selected file."""
704704
if selected_file in self.configuration_steps:
705705
return self.configuration_steps[selected_file].get("instructions_popup")

ardupilot_methodic_configurator/backend_filesystem_freedesktop.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from os import path as os_path
2525
from shutil import which as shutil_which
2626
from sys import platform as sys_platform
27-
from typing import Optional, Union
2827

2928
from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
3029

@@ -57,7 +56,7 @@ def _desktop_icon_exists(desktop_file_path: str) -> bool:
5756
return os_path.exists(desktop_file_path)
5857

5958
@staticmethod
60-
def _get_virtual_env_path() -> Optional[str]:
59+
def _get_virtual_env_path() -> str | None:
6160
"""Get the virtual environment path from environment variables."""
6261
return os_environ.get("VIRTUAL_ENV")
6362

@@ -157,7 +156,7 @@ def create_desktop_icon_if_needed() -> None:
157156
logging_error("Failed to create application launch desktop icon")
158157

159158
@staticmethod
160-
def _get_desktop_startup_id() -> Union[str, None]:
159+
def _get_desktop_startup_id() -> str | None:
161160
"""
162161
Get the DESKTOP_STARTUP_ID environment variable.
163162

ardupilot_methodic_configurator/backend_filesystem_json_with_schema.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from logging import debug as logging_debug
1919
from logging import error as logging_error
2020
from os import path as os_path
21-
from typing import Any, Union
21+
from typing import Any
2222

2323
from jsonschema import ValidationError, validate, validators
2424

@@ -32,8 +32,8 @@ class FilesystemJSONWithSchema:
3232
def __init__(self, json_filename: str, schema_filename: str) -> None:
3333
self.json_filename = json_filename
3434
self.schema_filename = schema_filename
35-
self.data: Union[None, dict[str, Any]] = None
36-
self.schema: Union[None, dict[Any, Any]] = None
35+
self.data: None | dict[str, Any] = None
36+
self.schema: None | dict[Any, Any] = None
3737
self._data_on_disk: dict[str, Any] = {}
3838

3939
def load_schema(self) -> dict:
@@ -169,7 +169,7 @@ def save_json_data(self, data: dict, data_dir: str) -> tuple[bool, str]: # noqa
169169

170170
return False, ""
171171

172-
def has_unsaved_changes(self, key: Union[str, None] = None) -> bool:
172+
def has_unsaved_changes(self, key: str | None = None) -> bool:
173173
"""
174174
Return True if in-memory data differs from what was last loaded or saved to disk.
175175

0 commit comments

Comments
 (0)