Skip to content

Add comprehensive type annotations to util module#2980

Merged
jonathangreen merged 7 commits intomainfrom
chore/type-hint-util
Jan 15, 2026
Merged

Add comprehensive type annotations to util module#2980
jonathangreen merged 7 commits intomainfrom
chore/type-hint-util

Conversation

@jonathangreen
Copy link
Member

@jonathangreen jonathangreen commented Jan 11, 2026

Description

This PR adds comprehensive type hints to the entire util module and removes these modules from the mypy ignore list. The changes improve code quality and type safety while discovering and fixing several bugs. In the process of type hinting, I found several unused sections of code that were removed.

Motivation and Context

The util module lacked type hints, making it harder to catch type-related bugs and reducing IDE support. Adding type hints:

  • Improves code maintainability and readability
  • Enables better IDE autocomplete and static analysis
  • Catches potential bugs at development time
  • Removes util modules from mypy ignore list in pyproject.toml

Bugs Fixed

  1. Mutable default arguments in flask_util.py - Fixed dangerous mutable default headers={}
  2. Incorrect classmethod parameter in permanent_work_id.py - Changed self to cls
  3. None comparison - Updated to use is None instead of == None
  4. __dict__ method in opds_writer.py - Renamed to __getstate__ and fixed .items() call
  5. Missing None checks - Added defensive None handling in multiple locations

Type Improvements

  • Replaced overly broad Any types with specific types (re.Match[str], TextBlob, Contributor)
  • Added proper generic types: Counter[str], dict[str, float], list[str]
  • Used TYPE_CHECKING pattern to avoid circular imports
  • Added inline type annotations with explanatory comments for untyped libraries (Crypto, lxml)

How Has This Been Tested?

  • mypy: Success - no issues found in 1017 source files
  • Tests: All 313 util module tests passed
  • Pre-commit hooks: All checks passed

Checklist

  • I have updated the documentation accordingly.
  • All new and existing tests passed.

@codecov
Copy link

codecov bot commented Jan 11, 2026

Codecov Report

❌ Patch coverage is 88.43537% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.94%. Comparing base (42a82b2) to head (e609746).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
src/palace/manager/util/languages.py 84.37% 2 Missing and 3 partials ⚠️
src/palace/manager/util/opds_writer.py 82.60% 4 Missing ⚠️
src/palace/manager/util/__init__.py 88.88% 2 Missing ⚠️
src/palace/manager/util/flask_util.py 66.66% 1 Missing and 1 partial ⚠️
src/palace/manager/util/summary.py 91.30% 1 Missing and 1 partial ⚠️
...lace/manager/integration/catalog/marc/annotator.py 66.66% 0 Missing and 1 partial ⚠️
src/palace/manager/util/personal_names.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2980      +/-   ##
==========================================
+ Coverage   92.81%   92.94%   +0.13%     
==========================================
  Files         454      453       -1     
  Lines       42954    42818     -136     
  Branches     6000     5973      -27     
==========================================
- Hits        39866    39799      -67     
+ Misses       2018     1952      -66     
+ Partials     1070     1067       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jonathangreen jonathangreen marked this pull request as draft January 11, 2026 16:20
Improve type annotations in util module

- Replace Any with specific types (re.Match[str], TextBlob)
- Simplify type annotation in OPDSMessage.__init__
- Remove unnecessary defensive checks in regex replacement functions
- Remove unused Any imports

Replace Any with Contributor type for author parameters

- Add TYPE_CHECKING import for Contributor model
- Update _wordbags_for_author to use Contributor instead of Any
- Update author_name_similarity to use Iterable[Contributor]
- Avoids circular import by using TYPE_CHECKING

Fix web publication manifest type annotations

- Add JSONLDContext type alias for JSON-LD @context values
- Allow context parameter to accept str, dict, or list of str/dict
- Allow href parameters to accept None (filtered by _append method)
- Add explicit type annotation to context_with_extension in FindawayManifest
- Fixes mypy errors without using generic Any types
similarity = self.similarity_to(candidate)
if similarity >= threshold:
yield candidate

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed because these functions were unused.

from palace.manager.sqlalchemy.model.contributor import Contributor


class MetadataSimilarity:
Copy link
Member Author

Choose a reason for hiding this comment

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

Big chunks of this class were removed because they were unused, except for in tests.

# The main title and the full title were the same.
return None
return subtitle

Copy link
Member Author

Choose a reason for hiding this comment

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

Unused

def __init__(self, bigrams: Counter[str]) -> None:
self.bigrams = bigrams
self.proportional = Counter()
self.proportional: dict[str, float] = {}
Copy link
Member Author

Choose a reason for hiding this comment

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

Using dict instead of Counter because Counter is typed for int values, and we need to store float scores. It does appear to work at runtime, but I'm not sure its actually intentional behavior. Using dict + sorted() is cleaner than adding multiple type ignores for Counter.most_common().

}
english_bigrams = Bigrams(Counter())
english_bigrams.proportional = Counter(english_bigram_frequencies)
english_bigrams.proportional = dict(english_bigram_frequencies)
Copy link
Member Author

Choose a reason for hiding this comment

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

"""A helper object for creating etree elements."""

def __dict__(self):
def __getstate__(self) -> dict[str, Any]:
Copy link
Member Author

Choose a reason for hiding this comment

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

This is old code, and it rightly fails type checking because this isn't how __dict__ is defined. Since it mentions pickling, it seems like __getstate__ was actually the intended override here. 🤷🏻

"""
if not match or len(match.groups()) < 1:
return match

Copy link
Member Author

Choose a reason for hiding this comment

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

This method must return a str, so this defensive code is incorrect. It seems to be un-needed where it us used. We could also modify this to raise instead, although in that case match.groups()[0] + "MD" will raise. So I think we are okay to drop this defensive code

"""
if not match or len(match.groups()) < 1:
return match

Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above

"""
if not match or len(match.groups()) < 3:
return match

Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above

def best_choices(self, n: int = 3) -> list[tuple[str, float]]:
"""Choose the best `n` choices among the current summaries."""
scores = Counter()
scores: dict[str, float] = {}
Copy link
Member Author

Choose a reason for hiding this comment

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

- Remove empty TYPE_CHECKING block from util/__init__.py
- Change AtomFeed.__str__() to return empty string instead of "None" when feed is None
@jonathangreen jonathangreen changed the title Fully type hint util module Add comprehensive type annotations to util module Jan 12, 2026
@jonathangreen jonathangreen requested a review from a team January 12, 2026 21:25
@jonathangreen jonathangreen marked this pull request as ready for review January 12, 2026 21:25
@jonathangreen
Copy link
Member Author

Codecov is complaining about patch coverage, but the coverage gaps were pre-existing, so I think we can ignore it on this PR.

Copy link
Contributor

@dbernstein dbernstein left a comment

Choose a reason for hiding this comment

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

Amen.

@jonathangreen jonathangreen merged commit 16d65c0 into main Jan 15, 2026
18 of 19 checks passed
@jonathangreen jonathangreen deleted the chore/type-hint-util branch January 15, 2026 18:10
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