Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/23-pr-unit-distro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
lxc exec alpine -- ping -c 1 dl-cdn.alpinelinux.org || true
- name: Install dependencies
run: lxc exec alpine -- apk add py3-tox git tzdata
run: lxc exec alpine -- apk add py3-tox py3-python-discovery git tzdata

- name: Mount source into container directory
run: lxc config device add alpine gitdir disk source=$(pwd) path=/root/cloud-init-ro
Expand Down
6 changes: 4 additions & 2 deletions cloudinit/cmd/devel/make_mime.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ 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!") % (
err_msg = (
"content type %r for attachment %s may be incorrect!"
) % (
Comment on lines +35 to +37
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is odd that this type is not a string in the first place. Creating a tuple to append to a list of strings is unnecessary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, this doesn't need to turn into refactoring. I'd just name the later re-assignment something else like "mime_msg".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback!

Just to clarify my understanding: (content_type, i + 1) does form a tuple as the right operand to %, which is standard for %-formatting with multiple arguments. However, the % operator itself returns a str directly, so what gets appended to errors is already a string, not a tuple. I believe the parentheses around the template string on the left were purely for line wrapping clarity. I have done a quick test to verify the typing and this assumption seems to be correct!

That said, I'm happy to rename the variable from err_msg to mime_msg or to split the formatting logic into several variables if it helps making the code clearer.

What do you think is the best option?

content_type,
i + 1,
)
errors.append(msg)
errors.append(err_msg)
sub_messages.append(sub_message)
combined_message = MIMEMultipart()
for msg in sub_messages:
Expand Down
6 changes: 4 additions & 2 deletions cloudinit/cmd/devel/net_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
network_manager,
network_state,
networkd,
renderer,
sysconfig,
)

Expand Down Expand Up @@ -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":
Expand Down
53 changes: 39 additions & 14 deletions cloudinit/cmd/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +83,13 @@ class SubcommandAwareArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._raw_args = None
self._subparsers_action = None

def add_subparsers(self, **kwargs):
"""Override to capture the subparsers action for use in error()."""
action = super().add_subparsers(**kwargs)
self._subparsers_action = action
return action

def parse_args(self, args=None, namespace=None):
"""Override parse_args to store raw arguments for error handling."""
Expand All @@ -98,18 +105,20 @@ def error(self, message):
if not self._raw_args:
self._raw_args = sys.argv[1:]
subcommand = None
if self._raw_args:
for arg in self._raw_args:
if arg in self._subparsers._group_actions[0].choices:
subcommand = arg
break
# Check if the subcommand exists and show its help
if not self._subparsers_action:
self.print_help(file=sys.stderr)
sys.exit(2)

choices = self._subparsers_action.choices
for arg in self._raw_args:
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
# Print subcommand help to stderr
choices[subcommand].print_help(file=sys.stderr)
else:
self.print_help(file=sys.stderr)
sys.exit(2)
Expand Down Expand Up @@ -546,6 +555,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:
Expand Down Expand Up @@ -613,6 +629,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.",
Expand Down Expand Up @@ -912,13 +935,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(),
Expand Down Expand Up @@ -955,6 +978,8 @@ def status_wrapper(name, args):
lambda h: isinstance(h, loggers.LogExporter), root_logger.handlers
)
)
if not isinstance(handler, loggers.LogExporter):
raise RuntimeError("LogExporter handler not found in root logger")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferred to avoid changing control flow. Same comment elsewhere in this file.

Copy link
Copy Markdown
Author

@jaborvs jaborvs May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the "elsewhere" applies to the changes in lines 557-633 and 631-637 (let me know if I am missing some other blocks of code).

In this case, the three things that come to mind to solve this mypy error are using if statements, asserts or casts. I believe that the later two options are not very good practices and should be avoided. Therefore, let me explain why I chose to use an if in every situation:

  • init.datasource can be None — lines 566, 638: After init.fetch(), datasource is Optional[DataSource], which implies no datasource found is a real runtime case. In case it is None, then it will not have the dsmode attribute. The code must handle this check before attempting to access the attribute, so a None check seems unavoidable to me.
  • handler is typed as logging.Handler, not LogExporter — line 954: next(filter(...)) returns logging.Handler. Mypy has no way to know it's a LogExporter without a runtime isinstance check or a cast. I believe that if we want to avoid casting there is no annotation-only fix here.

If you have some other fix in mind I am skipping, please let me know!

preexisting_recoverable_errors = handler.export_logs()

# Write status.json prior to running init / module code
Expand Down Expand Up @@ -1052,7 +1077,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"
Expand Down
4 changes: 2 additions & 2 deletions cloudinit/net/eni.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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[Optional[Mapping[str, Any]]] = None):
if not config:
config = {}
self.eni_path = config.get("eni_path", "etc/network/interfaces")
Expand Down
3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +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",
"cloudinit.config.cc_ca_certs",
"cloudinit.config.cc_growpart",
Expand Down
Loading