From a8e69fb94987220ab3cbd8d2e198ab03f970fa7c Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Mon, 4 May 2026 12:14:52 +0200 Subject: [PATCH 1/8] fix: Enable mypy strict checking for cloudinit.cmd.devel.make_mime --- cloudinit/cmd/devel/make_mime.py | 8 ++++---- pyproject.toml | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cloudinit/cmd/devel/make_mime.py b/cloudinit/cmd/devel/make_mime.py index 5411ad602d1..40784119850 100755 --- a/cloudinit/cmd/devel/make_mime.py +++ b/cloudinit/cmd/devel/make_mime.py @@ -32,11 +32,11 @@ def create_mime_message(files): ) content_type = sub_message.get_content_type().lower() if content_type not in get_content_types(): - msg = ("content type %r for attachment %s may be incorrect!") % ( - content_type, - i + 1, + err_msg = ( + f"content type {content_type} for attachment" + f" {i + 1} may be incorrect!" ) - errors.append(msg) + errors.append(err_msg) sub_messages.append(sub_message) combined_message = MIMEMultipart() for msg in sub_messages: diff --git a/pyproject.toml b/pyproject.toml index 9a3373709ed..e8a6ef058c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ no_implicit_optional = true # See GH-5445 [[tool.mypy.overrides]] module = [ - "cloudinit.cmd.devel.make_mime", "cloudinit.cmd.devel.net_convert", "cloudinit.cmd.main", "cloudinit.config.cc_apt_configure", From 544d63b95b7ace5059178f4fb2dc73dc10f6a68c Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Mon, 4 May 2026 13:12:14 +0200 Subject: [PATCH 2/8] fix: Enable mypy strict checking for cloudinit.cmd.devel.net_convert --- cloudinit/cmd/devel/net_convert.py | 6 ++++-- cloudinit/net/eni.py | 4 ++-- pyproject.toml | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py index eafb11f16e9..e64f0de5444 100755 --- a/cloudinit/cmd/devel/net_convert.py +++ b/cloudinit/cmd/devel/net_convert.py @@ -18,6 +18,7 @@ network_manager, network_state, networkd, + renderer, sysconfig, ) @@ -158,13 +159,14 @@ def handle_args(name, args): apply_network_config_for_secondary_ips=True, ) elif args.kind == "vmware-imc": - config = guestcust_util.Config( + vmware_config = guestcust_util.Config( guestcust_util.ConfigFile(args.network_data.name) ) pre_ns = guestcust_util.get_network_data_from_vmware_cust_cfg( - config, False + vmware_config, False ) + r_cls: type[renderer.Renderer] distro_cls = distros.fetch(args.distro) distro = distro_cls(args.distro, {}, None) if args.output_kind == "eni": diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 89292597145..fb8bc1dfb69 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -7,7 +7,7 @@ import os import re from contextlib import suppress -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Mapping, Optional from cloudinit import performance, subp, util from cloudinit.net import ( @@ -453,7 +453,7 @@ def has_same_ip_version(addr_or_net: str, is_ipv6: bool) -> bool: class Renderer(renderer.Renderer): """Renders network information in a /etc/network/interfaces format.""" - def __init__(self, config: Optional[dict] = None): + def __init__(self, config: Optional[Mapping[str, Any]] = None): if not config: config = {} self.eni_path = config.get("eni_path", "etc/network/interfaces") diff --git a/pyproject.toml b/pyproject.toml index e8a6ef058c0..ee9e2774ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ no_implicit_optional = true # See GH-5445 [[tool.mypy.overrides]] module = [ - "cloudinit.cmd.devel.net_convert", "cloudinit.cmd.main", "cloudinit.config.cc_apt_configure", "cloudinit.config.cc_ca_certs", From 11d0a56b2cfa386f5e331c08f1289f29acc55719 Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Mon, 4 May 2026 14:27:17 +0200 Subject: [PATCH 3/8] fix: Enable mypy strict checking for cloudinit.cmd.main --- cloudinit/cmd/main.py | 40 ++++++++++++++++++++++++++++------------ pyproject.toml | 1 - 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index 042d89420e2..b631749dab2 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -19,7 +19,7 @@ import traceback import logging import yaml -from typing import Optional, Tuple, Callable, Union +from typing import Any, Optional, Tuple, Callable, Union from cloudinit import features, netinfo from cloudinit import signal_handler @@ -98,18 +98,20 @@ def error(self, message): if not self._raw_args: self._raw_args = sys.argv[1:] subcommand = None + subparsers_action = ( + self._subparsers._group_actions[0] if self._subparsers else None + ) + choices = getattr(subparsers_action, "choices", None) or {} if self._raw_args: for arg in self._raw_args: - if arg in self._subparsers._group_actions[0].choices: + if arg in choices: subcommand = arg break # Check if the subcommand exists and show its help if subcommand: - subparser = self._subparsers._group_actions[0].choices[subcommand] - subparser.print_help( - file=sys.stderr - ) # Print subcommand help to stderr + subparser = choices[subcommand] + subparser.print_help(file=sys.stderr) else: self.print_help(file=sys.stderr) sys.exit(2) @@ -546,6 +548,13 @@ def main_init(name, args): bring_up_interfaces = _should_bring_up_interfaces(init, args) try: init.fetch(existing=existing) + if init.datasource is None: + LOG.debug( + "[%s] Exiting. datasource is None after fetch," + " cannot continue.", + mode, + ) + return (None, []) # if in network mode, and the datasource is local # then work was done at that stage. if mode == sources.DSMODE_NETWORK and init.datasource.dsmode != mode: @@ -613,6 +622,13 @@ def main_init(name, args): ) util.write_file(init.paths.get_runpath(".skip-network"), "") + if init.datasource is None: + LOG.debug( + "[%s] Exiting. datasource is None in local mode," + " cannot check dsmode.", + mode, + ) + return (None, []) if init.datasource.dsmode != mode: LOG.debug( "[%s] Exiting. datasource %s not in local mode.", @@ -912,13 +928,13 @@ def status_wrapper(name, args): "Invalid cloud init mode specified '{0}'".format(mode) ) - nullstatus = { + nullstatus: dict[str, list[Any] | dict[str, Any] | None] = { "errors": [], "recoverable_errors": {}, "start": None, "finished": None, } - status = { + status: dict[str, Any] = { "v1": { "datasource": None, "init": nullstatus.copy(), @@ -951,10 +967,10 @@ def status_wrapper(name, args): v1[mode]["start"] = float(util.uptime()) handler = next( - filter( - lambda h: isinstance(h, loggers.LogExporter), root_logger.handlers - ) + h for h in root_logger.handlers if isinstance(h, loggers.LogExporter) ) + if not isinstance(handler, loggers.LogExporter): + raise RuntimeError("LogExporter handler not found in root logger") preexisting_recoverable_errors = handler.export_logs() # Write status.json prior to running init / module code @@ -1052,7 +1068,7 @@ def _maybe_set_hostname(init, stage, retry_stage): ) if hostname: # meta-data or user-data hostname content try: - cc_set_hostname.handle("set_hostname", init.cfg, cloud, None) + cc_set_hostname.handle("set_hostname", init.cfg, cloud, []) except cc_set_hostname.SetHostnameError as e: LOG.debug( "Failed setting hostname in %s stage. Will" diff --git a/pyproject.toml b/pyproject.toml index ee9e2774ac4..b4839e7385a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ no_implicit_optional = true # See GH-5445 [[tool.mypy.overrides]] module = [ - "cloudinit.cmd.main", "cloudinit.config.cc_apt_configure", "cloudinit.config.cc_ca_certs", "cloudinit.config.cc_growpart", From d1654578e80590fd89cde63caa64392a96f1ab12 Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Thu, 28 May 2026 10:04:04 +0200 Subject: [PATCH 4/8] fix: Document init.fetch() return type as non-optional dataSource --- cloudinit/stages.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 5abb19ab95a..69283dd83a0 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -558,10 +558,8 @@ def is_new_instance(self): or previous != self.ds.get_instance_id() ) - def fetch(self, existing="check"): - """optionally load datasource from cache, otherwise discover - datasource - """ + def fetch(self, existing="check") -> sources.DataSource: + """Load datasource from cache, otherwise discover datasource""" return self._get_data_source(existing=existing) def instancify(self): From 0130b22dccc1db9800c864f40bf083f524079bdc Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Wed, 3 Jun 2026 10:50:36 +0200 Subject: [PATCH 5/8] fix: Use union type annotation compatible with python 3.9+ --- cloudinit/cmd/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index b631749dab2..744e69667d3 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -928,7 +928,7 @@ def status_wrapper(name, args): "Invalid cloud init mode specified '{0}'".format(mode) ) - nullstatus: dict[str, list[Any] | dict[str, Any] | None] = { + nullstatus: dict[str, Union[list[Any], dict[str, Any], None]] = { "errors": [], "recoverable_errors": {}, "start": None, From 96590d8480722ec5704759dd16eda3edd1803660 Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Wed, 3 Jun 2026 10:53:50 +0200 Subject: [PATCH 6/8] fix: Handle missing LogExporter handler without StopIteration --- cloudinit/cmd/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index 744e69667d3..c5df56db84d 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -967,7 +967,8 @@ def status_wrapper(name, args): v1[mode]["start"] = float(util.uptime()) handler = next( - h for h in root_logger.handlers if isinstance(h, loggers.LogExporter) + (h for h in root_logger.handlers if isinstance(h, loggers.LogExporter)), + None, ) if not isinstance(handler, loggers.LogExporter): raise RuntimeError("LogExporter handler not found in root logger") From b6fc06bc6e1e9eb92bc254f010224b2d791e3380 Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Wed, 3 Jun 2026 11:03:21 +0200 Subject: [PATCH 7/8] fix: wrap long generator expression to comply with line length limit --- cloudinit/cmd/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index c5df56db84d..742d66b41b3 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -967,7 +967,11 @@ def status_wrapper(name, args): v1[mode]["start"] = float(util.uptime()) handler = next( - (h for h in root_logger.handlers if isinstance(h, loggers.LogExporter)), + ( + h + for h in root_logger.handlers + if isinstance(h, loggers.LogExporter) + ), None, ) if not isinstance(handler, loggers.LogExporter): From c11422283a152bbfd3b33eb5f8f05c4c315906be Mon Sep 17 00:00:00 2001 From: Borja Velasco Santamaria Date: Thu, 4 Jun 2026 08:41:47 +0200 Subject: [PATCH 8/8] fix: use !r for content_type in attachment warning for unambiguous output --- cloudinit/cmd/devel/make_mime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/cmd/devel/make_mime.py b/cloudinit/cmd/devel/make_mime.py index 40784119850..e233df04c65 100755 --- a/cloudinit/cmd/devel/make_mime.py +++ b/cloudinit/cmd/devel/make_mime.py @@ -33,7 +33,7 @@ def create_mime_message(files): content_type = sub_message.get_content_type().lower() if content_type not in get_content_types(): err_msg = ( - f"content type {content_type} for attachment" + f"content type {content_type!r} for attachment" f" {i + 1} may be incorrect!" ) errors.append(err_msg)