Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

style: update type hints and enable type checking in ci for apt.py #151

Conversation

james-garner-canonical
Copy link
Contributor

@james-garner-canonical james-garner-canonical commented Jan 29, 2025

Enable static analysis of apt.py in ci. LIBPATCH will be bumped after one more follow up PR handling ruff linting for this lib.

@james-garner-canonical james-garner-canonical force-pushed the 2025-01/types/enable-type-checking-for-apt branch from 69379d7 to 2453b0f Compare January 29, 2025 03:28
Comment on lines -349 to +353
matches = epoch_matcher.search(version).groupdict()
return matches.get("epoch", ""), matches.get("version")
result = epoch_matcher.search(version)
assert result is not None
matches = result.groupdict()
return matches.get("epoch", ""), matches["version"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This issue here is that re.Pattern.search(...) can return None. Presumably it never does on well formed version strings, so we can just assert.

Note also the change to returning matches["version"] instead of matches.get("version"), as the latter is also typed as possibly returning None, which contradicts the function signature. The regex match should always have an entry for "version" since it matches .*.

Comment on lines -425 to +454
try:
matches = dpkg_matcher.search(line).groupdict()
package_status = matches["package_status"]

if not package_status.endswith("i"):
logger.debug(
"package '%s' in dpkg output but not installed, status: '%s'",
package,
package_status,
)
break

epoch, split_version = DebianPackage._get_epoch_from_version(matches["version"])
pkg = DebianPackage(
matches["package_name"],
split_version,
epoch,
matches["arch"],
PackageState.Present,
)
if (pkg.arch == "all" or pkg.arch == arch) and (
version == "" or str(pkg.version) == version
):
return pkg
except AttributeError:
result = dpkg_matcher.search(line)
if result is None:
logger.warning("dpkg matcher could not parse line: %s", line)
continue
matches = result.groupdict()
package_status = matches["package_status"]

if not package_status.endswith("i"):
logger.debug(
"package '%s' in dpkg output but not installed, status: '%s'",
package,
package_status,
)
break

epoch, split_version = DebianPackage._get_epoch_from_version(matches["version"])
pkg = DebianPackage(
name=matches["package_name"],
version=split_version,
epoch=epoch,
arch=matches["arch"],
state=PackageState.Present,
)
if (pkg.arch == "all" or pkg.arch == arch) and (
version == "" or str(pkg.version) == version
):
return pkg
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Originally the whole loop body is wrapped in a try ... except AttributeError, again due to re.Pattern.search(...) potentially returning None instead of a re.Match. This change explicitly checks if the search result is None, allowing the loop body to be dedented.

Comment on lines -499 to +507
vals["Package"],
split_version,
epoch,
vals["Architecture"],
PackageState.Available,
name=vals["Package"],
version=split_version,
epoch=epoch,
arch=vals["Architecture"],
state=PackageState.Available,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just using keyword arguments because why not

Comment on lines -558 to +563
def _listify(self, revision: str) -> list[str]:
"""Split a revision string into a listself.
def _listify(self, revision: str) -> list[str | int]:
"""Split a revision string into a list.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Return type is a list that alternates between strings and ints. This will come up later

@@ -651,31 +657,38 @@ def _compare_revision_strings(self, first: str, second: str): # noqa: C901
# explicitly raise IndexError if we've fallen off the edge of list2
if i >= len(second_list):
raise IndexError
other = second_list[i]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use a local variable to allow type narrowing -- and it's more efficient anyway

Comment on lines +666 to +674
assert isinstance(other, int)
if item > other:
return 1
if item < second_list[i]:
if item < other:
return -1
else:
# string comparison
return self._dstringcmp(item, second_list[i])
assert isinstance(other, str)
return self._dstringcmp(item, other)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is that list of alternating integers and strings -- well a pair of them being compared

Comment on lines 675 to 689
except IndexError:
# rev1 is longer than rev2 but otherwise equal, hence greater
# ...except for goddamn tildes
if first_list[len(second_list)][0][0] == "~":
# FIXME: bug?? we return 1 in both cases
# FIXME: first_list[len(second_list)] should be a string, why are we indexing to 0 twice?
if first_list[len(second_list)][0][0] == "~": # type: ignore
return 1
return 1
# rev1 is shorter than rev2 but otherwise equal, hence lesser
# ...except for goddamn tildes
if second_list[len(first_list)][0][0] == "~":
# FIXME: bug?? we return -1 in both cases
# FIXME: first_list[len(second_list)] should be a string, why are we indexing to 0 twice?
if second_list[len(first_list)][0][0] == "~": # type: ignore
return -1
return -1
Copy link
Contributor Author

Choose a reason for hiding this comment

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

type: ignore due to the str | int ambiguity -- and this is weird and potentially buggy so I'd like to save further changes for a separate PR (unit test coverage of these comparison functions is spotty)

Comment on lines -705 to 722
def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
"""Equality magic method impl."""
if not isinstance(other, Version):
return False
return self._compare_version(other) == 0
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically a behaviour change, so maybe this should be dropped -- maybe it's better to get an AttributeError when you try to compare a Version for equality with something else?

@@ -748,8 +777,6 @@ def add_package(
update()
cache_refreshed = True

packages = {"success": [], "retry": [], "failed": []}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I separate these into individual variables for better typing

Copy link
Collaborator

Choose a reason for hiding this comment

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

Simpler too!

lib/charms/operator_libs_linux/v0/apt.py Outdated Show resolved Hide resolved
@@ -748,8 +777,6 @@ def add_package(
update()
cache_refreshed = True

packages = {"success": [], "retry": [], "failed": []}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Simpler too!

Copy link
Collaborator

@benhoyt benhoyt left a comment

Choose a reason for hiding this comment

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

Thanks!

@james-garner-canonical james-garner-canonical merged commit 06633c2 into canonical:main Jan 29, 2025
7 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