Skip to content

chore: add type annotations to cloudinit.distros.parsers.hosts#6916

Merged
holmanb merged 6 commits into
canonical:mainfrom
PhinehasNarh:fix/type-annotate-parsers-hosts
Jun 30, 2026
Merged

chore: add type annotations to cloudinit.distros.parsers.hosts#6916
holmanb merged 6 commits into
canonical:mainfrom
PhinehasNarh:fix/type-annotate-parsers-hosts

Conversation

@PhinehasNarh

Copy link
Copy Markdown
Contributor

Summary

Adds full type annotations to cloudinit/distros/parsers/hosts.py so it can be removed from the check_untyped_defs = false mypy override in pyproject.toml. Part of #5445.

  • Replace the None-sentinel _contents with a _parsed: bool flag and an empty-list default, consistent with the approach used in resolv_conf.py (chore: add typing to cloudinit.distros.parsers hostname and resolv_conf #6909), to avoid needing Optional and narrowing asserts
  • Annotate all method signatures (parameter types and return types)
  • Use List, Tuple, Any from typing for Python 3.8 compatibility
  • Clean up the variable shadowing in __str__ where pieces was reassigned across incompatible types; use distinct raw_pieces / str_pieces names instead
  • Change add_entry to store option entries as a list rather than a tuple, keeping _contents type consistent with what _parse produces
  • Remove both cloudinit.distros.parsers.hosts and tests.unittests.distros.test_hosts from the mypy override block

Test plan

  • Existing tests in tests/unittests/distros/test_hosts.py pass unchanged
  • mypy cloudinit/distros/parsers/hosts.py reports no errors

Enable mypy check_untyped_defs for cloudinit/distros/parsers/hosts.py
and its test module by adding full type annotations.

Changes:
- Replace None-sentinel _contents with a _parsed bool flag and an
  empty-list default, matching the pattern used in resolv_conf.py (canonicalGH-6909)
- Annotate all method signatures with parameter and return types
- Use List/Tuple/Any from typing for Python 3.8 compatibility
- Rename shadowed variable in __str__ to avoid reassignment type mismatch
- Change add_entry to store option components as a list instead of a tuple
  to keep _contents type consistent
- Remove cloudinit.distros.parsers.hosts and tests.unittests.distros.test_hosts
  from the mypy check_untyped_defs=false overrides in pyproject.toml

Closes part of canonical#5445

@holmanb holmanb left a comment

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.

This looks like it is going the right direction, thanks. I left a few nitpicks. Please make sure to sign the CLA. Also please run tox -e do_format to fix formatting issues.

Comment thread cloudinit/distros/parsers/hosts.py Outdated
pieces = [str(p) for p in pieces]
pieces = "\t".join(pieces)
contents.write("%s%s\n" % (pieces, tail))
(raw_pieces, tail) = components

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.

Please remove the parenthesis around the assignment - they are not needed.

Comment thread cloudinit/distros/parsers/hosts.py Outdated
contents.write("%s%s\n" % (pieces, tail))
(raw_pieces, tail) = components
str_pieces = [str(p) for p in raw_pieces]
contents.write("%s%s\n" % ("\t".join(str_pieces), tail))

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.

I think an f-string here would make this easier to read.

Comment on lines +60 to +61
def _parse(self, contents: str) -> List[Tuple[str, List[Any]]]:
entries: List[Tuple[str, List[Any]]] = []

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.

Is there any reason this can't be List[Tuple[str, List[str]]]?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. The inner list can't be List[str] because option entries store [head.split(None), tail] - that's [list[str], str], so the two elements are different types. Changing it to List[str] would make mypy flag every place we do pieces, _tail = components (it would see a list of strings being unpacked into two str variables, losing the fact that pieces is itself a list). List[Any] is the minimal correct annotation here without introducing a custom TypeAlias or a NamedTuple. Happy to tighten it further if you'd prefer a TypeAlias approach.

Comment thread cloudinit/distros/parsers/hosts.py Outdated
def parse(self):
if self._contents is None:
def parse(self) -> None:
if not self._parsed:

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.

Rather than introducing the new attribute self._parsed, can we just check the truthiness of self._contents?

- Drop the _parsed flag; rely on truthiness of _contents instead
  (an empty list is falsy, so parse() only runs once on a non-empty file)
- Remove unnecessary parentheses from all tuple-unpacking assignments
- Swap the %-format in __str__ for an f-string (extract join to a
  variable first, since backslash in f-string expressions requires 3.12+)
- Remove spurious parens around single-arg % format in blank/all_comment
  branches (black-clean, no behaviour change)
@holmanb

holmanb commented Jun 30, 2026

Copy link
Copy Markdown
Member

@PhinehasNarh It looks like a CLA signature is still required.

Line 57 of test_cc_phone_home.py was 89 characters, causing ruff E501
in the check_format CI step. Trimmed the trailing clause from the
comment; the context above (GH pytest-dev/pytest#14650 reference on the
next line) already explains the full intent.
@PhinehasNarh

Copy link
Copy Markdown
Contributor Author

Thanks for the nudge. The CLA is still outstanding on my end - I'll get that signed.

On the check_format failure: the ruff E501 was in tests/unittests/config/test_cc_phone_home.py:57 (a comment 89 chars long, not introduced by this PR). I've trimmed it in the latest commit so the format check should clear.

@holmanb

holmanb commented Jun 30, 2026

Copy link
Copy Markdown
Member

I think the main branch needs to be merged in.

@PhinehasNarh

Copy link
Copy Markdown
Contributor Author

Done - just merged main in (resolved a conflict in test_cc_phone_home.py where upstream had already fixed the same E501 line in a slightly different way; took the upstream version). CI runs are in action_required state waiting for approval.

mypy now checks test_hosts.py (removed from the override list in this
PR). Line 24 reused 'eh' for str(eh), causing an incompatible-types
error since mypy inferred 'eh' as HostsConf from the construction on
line 18. Rename to 'eh_str' to give the string its own binding.
@PhinehasNarh

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the mypy errors - test_hosts.py:24 was reusing eh as both HostsConf and str, which mypy flags as an incompatible assignment now that the test file is no longer in the override list. Renamed to eh_str. CI needs another approval to run.

@holmanb holmanb left a comment

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.

Thanks!

@holmanb holmanb merged commit 8217935 into canonical:main Jun 30, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants