From ea32a8f8876ec7e6e5474e84e606c116877eee90 Mon Sep 17 00:00:00 2001 From: Ahmed <76178825+Ahmedaltu@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:13:44 +0300 Subject: [PATCH 1/3] chore: add typing to cloudinit.distros.parsers hostname and resolv_conf GH-5445 Signed-off-by: Ahmed <76178825+Ahmedaltu@users.noreply.github.com> --- cloudinit/distros/parsers/hostname.py | 36 +++++++++++--------- cloudinit/distros/parsers/resolv_conf.py | 43 ++++++++++++++++-------- pyproject.toml | 2 -- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py index 7250b6a8eb2..ae1f39b81b2 100644 --- a/cloudinit/distros/parsers/hostname.py +++ b/cloudinit/distros/parsers/hostname.py @@ -5,50 +5,56 @@ # This file is part of cloud-init. See LICENSE file for license information. from io import StringIO +from typing import List, Optional, Tuple from cloudinit.distros.parsers import chop_comment # Parser that knows how to work with /etc/hostname format class HostnameConf: - def __init__(self, text): + def __init__(self, text: str) -> None: self._text = text - self._contents = None + self._contents: Optional[List[Tuple[str, List[str]]]] = None - def parse(self): + def parse(self) -> None: if self._contents is None: self._contents = self._parse(self._text) - def __str__(self): + def __str__(self) -> str: self.parse() - contents = StringIO() + assert self._contents is not None + buf = StringIO() for line_type, components in self._contents: if line_type == "blank": - contents.write("%s\n" % (components[0])) + buf.write("%s\n" % (components[0])) elif line_type == "all_comment": - contents.write("%s\n" % (components[0])) + buf.write("%s\n" % (components[0])) elif line_type == "hostname": (hostname, tail) = components - contents.write("%s%s\n" % (hostname, tail)) + buf.write("%s%s\n" % (hostname, tail)) # Ensure trailing newline - contents = contents.getvalue() - if not contents.endswith("\n"): - contents += "\n" - return contents + result = buf.getvalue() + if not result.endswith("\n"): + result += "\n" + return result @property - def hostname(self): + def hostname(self) -> Optional[str]: self.parse() + assert self._contents is not None + assert self._contents is not None for line_type, components in self._contents: if line_type == "hostname": return components[0] return None - def set_hostname(self, your_hostname): + def set_hostname(self, your_hostname: str) -> None: your_hostname = your_hostname.strip() if not your_hostname: return self.parse() + assert self._contents is not None + assert self._contents is not None replaced = False for line_type, components in self._contents: if line_type == "hostname": @@ -57,7 +63,7 @@ def set_hostname(self, your_hostname): if not replaced: self._contents.append(("hostname", [str(your_hostname), ""])) - def _parse(self, contents): + def _parse(self, contents: str) -> List[Tuple[str, List[str]]]: entries = [] hostnames_found = set() for line in contents.splitlines(): diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py index 6884c740989..9c9b023b695 100644 --- a/cloudinit/distros/parsers/resolv_conf.py +++ b/cloudinit/distros/parsers/resolv_conf.py @@ -6,6 +6,7 @@ import logging from io import StringIO +from typing import List, Optional, Tuple from cloudinit import util from cloudinit.distros.parsers import chop_comment @@ -15,37 +16,44 @@ # See: man resolv.conf class ResolvConf: - def __init__(self, text): + def __init__(self, text: str) -> None: self._text = text - self._contents = None + self._contents: Optional[List[Tuple[str, List[str]]]] = None - def parse(self): + def parse(self) -> None: if self._contents is None: self._contents = self._parse(self._text) @property - def nameservers(self): + def nameservers(self) -> List[str]: self.parse() + assert self._contents is not None + assert self._contents is not None return self._retr_option("nameserver") @property - def local_domain(self): + def local_domain(self) -> Optional[str]: self.parse() + assert self._contents is not None + assert self._contents is not None dm = self._retr_option("domain") if dm: return dm[0] return None @local_domain.setter - def local_domain(self, domain): + def local_domain(self, domain: str) -> None: self.parse() + assert self._contents is not None + assert self._contents is not None self._remove_option("domain") self._contents.append(("option", ["domain", str(domain), ""])) - return domain @property - def search_domains(self): + def search_domains(self) -> List[str]: self.parse() + assert self._contents is not None + assert self._contents is not None current_sds = self._retr_option("search") flat_sds = [] for sdlist in current_sds: @@ -54,8 +62,10 @@ def search_domains(self): flat_sds.append(sd) return flat_sds - def __str__(self): + def __str__(self) -> str: self.parse() + assert self._contents is not None + assert self._contents is not None contents = StringIO() for line_type, components in self._contents: if line_type == "blank": @@ -70,7 +80,8 @@ def __str__(self): contents.write("%s\n" % (line)) return contents.getvalue() - def _retr_option(self, opt_name): + def _retr_option(self, opt_name: str) -> List[str]: + assert self._contents is not None found = [] for line_type, components in self._contents: if line_type == "option": @@ -79,8 +90,10 @@ def _retr_option(self, opt_name): found.append(cfg_value) return found - def add_nameserver(self, ns): + def add_nameserver(self, ns: str) -> List[str]: self.parse() + assert self._contents is not None + assert self._contents is not None current_ns = self._retr_option("nameserver") new_ns = list(current_ns) new_ns.append(str(ns)) @@ -92,7 +105,7 @@ def add_nameserver(self, ns): self._contents.append(("option", ["nameserver", n, ""])) return new_ns - def _remove_option(self, opt_name): + def _remove_option(self, opt_name: str) -> None: def remove_opt(item): line_type, components = item if line_type != "option": @@ -102,13 +115,14 @@ def remove_opt(item): return False return True + assert self._contents is not None new_contents = [] for c in self._contents: if not remove_opt(c): new_contents.append(c) self._contents = new_contents - def add_search_domain(self, search_domain): + def add_search_domain(self, search_domain: str) -> List[str]: flat_sds = self.search_domains new_sds = list(flat_sds) new_sds.append(str(search_domain)) @@ -129,10 +143,11 @@ def add_search_domain(self, search_domain): "256 maximum search list character limit" % (search_domain) ) self._remove_option("search") + assert self._contents is not None self._contents.append(("option", ["search", s_list, ""])) return flat_sds - def _parse(self, contents): + def _parse(self, contents: str) -> List[Tuple[str, List[str]]]: entries = [] for i, line in enumerate(contents.splitlines()): sline = line.strip() diff --git a/pyproject.toml b/pyproject.toml index 4657424c946..6daec71fb03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,9 +54,7 @@ module = [ "cloudinit.distros.azurelinux", "cloudinit.distros.bsd", "cloudinit.distros.opensuse", - "cloudinit.distros.parsers.hostname", "cloudinit.distros.parsers.hosts", - "cloudinit.distros.parsers.resolv_conf", "cloudinit.distros.ug_util", "cloudinit.helpers", "cloudinit.net.cmdline", From 4bea5dad392cd4bad88ee81f3a1db1c074b0697e Mon Sep 17 00:00:00 2001 From: Ahmed <76178825+Ahmedaltu@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:40:50 +0300 Subject: [PATCH 2/3] fix: use _parsed flag instead of assert for type narrowing Signed-off-by: Ahmed <76178825+Ahmedaltu@users.noreply.github.com> --- cloudinit/distros/parsers/hostname.py | 11 ++++------- cloudinit/distros/parsers/resolv_conf.py | 21 ++++----------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py index ae1f39b81b2..75278c5bd52 100644 --- a/cloudinit/distros/parsers/hostname.py +++ b/cloudinit/distros/parsers/hostname.py @@ -14,15 +14,16 @@ class HostnameConf: def __init__(self, text: str) -> None: self._text = text - self._contents: Optional[List[Tuple[str, List[str]]]] = None + self._parsed: bool = False + self._contents: List[Tuple[str, List[str]]] = [] def parse(self) -> None: - if self._contents is None: + if not self._parsed: self._contents = self._parse(self._text) + self._parsed = True def __str__(self) -> str: self.parse() - assert self._contents is not None buf = StringIO() for line_type, components in self._contents: if line_type == "blank": @@ -41,8 +42,6 @@ def __str__(self) -> str: @property def hostname(self) -> Optional[str]: self.parse() - assert self._contents is not None - assert self._contents is not None for line_type, components in self._contents: if line_type == "hostname": return components[0] @@ -53,8 +52,6 @@ def set_hostname(self, your_hostname: str) -> None: if not your_hostname: return self.parse() - assert self._contents is not None - assert self._contents is not None replaced = False for line_type, components in self._contents: if line_type == "hostname": diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py index 9c9b023b695..5118075c0ec 100644 --- a/cloudinit/distros/parsers/resolv_conf.py +++ b/cloudinit/distros/parsers/resolv_conf.py @@ -18,24 +18,22 @@ class ResolvConf: def __init__(self, text: str) -> None: self._text = text - self._contents: Optional[List[Tuple[str, List[str]]]] = None + self._parsed: bool = False + self._contents: List[Tuple[str, List[str]]] = [] def parse(self) -> None: - if self._contents is None: + if not self._parsed: self._contents = self._parse(self._text) + self._parsed = True @property def nameservers(self) -> List[str]: self.parse() - assert self._contents is not None - assert self._contents is not None return self._retr_option("nameserver") @property def local_domain(self) -> Optional[str]: self.parse() - assert self._contents is not None - assert self._contents is not None dm = self._retr_option("domain") if dm: return dm[0] @@ -44,16 +42,12 @@ def local_domain(self) -> Optional[str]: @local_domain.setter def local_domain(self, domain: str) -> None: self.parse() - assert self._contents is not None - assert self._contents is not None self._remove_option("domain") self._contents.append(("option", ["domain", str(domain), ""])) @property def search_domains(self) -> List[str]: self.parse() - assert self._contents is not None - assert self._contents is not None current_sds = self._retr_option("search") flat_sds = [] for sdlist in current_sds: @@ -64,8 +58,6 @@ def search_domains(self) -> List[str]: def __str__(self) -> str: self.parse() - assert self._contents is not None - assert self._contents is not None contents = StringIO() for line_type, components in self._contents: if line_type == "blank": @@ -81,7 +73,6 @@ def __str__(self) -> str: return contents.getvalue() def _retr_option(self, opt_name: str) -> List[str]: - assert self._contents is not None found = [] for line_type, components in self._contents: if line_type == "option": @@ -92,8 +83,6 @@ def _retr_option(self, opt_name: str) -> List[str]: def add_nameserver(self, ns: str) -> List[str]: self.parse() - assert self._contents is not None - assert self._contents is not None current_ns = self._retr_option("nameserver") new_ns = list(current_ns) new_ns.append(str(ns)) @@ -115,7 +104,6 @@ def remove_opt(item): return False return True - assert self._contents is not None new_contents = [] for c in self._contents: if not remove_opt(c): @@ -143,7 +131,6 @@ def add_search_domain(self, search_domain: str) -> List[str]: "256 maximum search list character limit" % (search_domain) ) self._remove_option("search") - assert self._contents is not None self._contents.append(("option", ["search", s_list, ""])) return flat_sds From 351ed845170c15d1ec96bfce9d278789c758fc94 Mon Sep 17 00:00:00 2001 From: Ahmed <76178825+Ahmedaltu@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:23:27 +0300 Subject: [PATCH 3/3] fix: revert unnecessary variable rename in __str__ Signed-off-by: Ahmed <76178825+Ahmedaltu@users.noreply.github.com> --- cloudinit/distros/parsers/hostname.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py index 75278c5bd52..3d0535494b2 100644 --- a/cloudinit/distros/parsers/hostname.py +++ b/cloudinit/distros/parsers/hostname.py @@ -24,20 +24,20 @@ def parse(self) -> None: def __str__(self) -> str: self.parse() - buf = StringIO() + contents = StringIO() for line_type, components in self._contents: if line_type == "blank": - buf.write("%s\n" % (components[0])) + contents.write("%s\n" % (components[0])) elif line_type == "all_comment": - buf.write("%s\n" % (components[0])) + contents.write("%s\n" % (components[0])) elif line_type == "hostname": (hostname, tail) = components - buf.write("%s%s\n" % (hostname, tail)) + contents.write("%s%s\n" % (hostname, tail)) # Ensure trailing newline - result = buf.getvalue() - if not result.endswith("\n"): - result += "\n" - return result + rendered = contents.getvalue() + if not rendered.endswith("\n"): + rendered += "\n" + return rendered @property def hostname(self) -> Optional[str]: