Skip to content

Type the tree so mypy can enforce it - #206

Merged
willyborankin merged 47 commits into
Aiven-Open:masterfrom
mbertheau:mbertheau-typing
Aug 25, 2026
Merged

Type the tree so mypy can enforce it#206
willyborankin merged 47 commits into
Aiven-Open:masterfrom
mbertheau:mbertheau-typing

Conversation

@mbertheau

Copy link
Copy Markdown
Contributor

Based on #203.

Annotate the package, tests, and scripts, then turn on disallow_untyped_defs, disallow_untyped_calls, warn_unused_ignores, and disallow_any_generics. Stub packages cover typed third-party libraries; re2, systemd, and zstandard stay ignored because they ship no types.

Made with Cursor

mbertheau and others added 30 commits August 22, 2026 11:01
The mypy hook ran in an isolated venv with only types-requests and
types-geoip2, so everything else became Any — including packages that
already ship types. It also pinned 1.19.1 while requirements.dev.txt
installed a different mypy.

Run mypy from the project environment, as pylint already does, over the
whole tree: a whole-program checker on one file at a time is
order-dependent. types-geoip2 goes away; geoip2 ships py.typed.

Results now depend on the developer's installed deps, as pylint's
already do. CI installs both requirements files before pre-commit, so
the merge gate still sees everything. mypy's version floats with
requirements.dev.txt, so a release can break lint with no change on our
side.

Co-authored-by: Cursor <cursoragent@cursor.com>
GeoIPProtocol declared no members, so city() and the Reader constructor
were unchecked, and `type[GeoIPProtocol] | None` made construction a
type error. Declare __init__(filename) and city() — plus the
City/Location shapes they return, the contract the docstring already
claimed — and drop | None at the only call site.

Co-authored-by: Cursor <cursoragent@cursor.com>
main() stored the argparse Namespace in args, then reused that name for
the remainder of an input line. The same loop rebound msg from the raw
line to the dict send_message expects. action_args for the split
remainder, raw for the unparsed line, msg only for the dict.

Co-authored-by: Cursor <cursoragent@cursor.com>
_connect() keeps the last OSError from getaddrinfo results, then raises
it. An empty list leaves that value None and the raise is TypeError.
POSIX is not specified to return empty, but mypy flags it. Raise OSError
so the failure stays the type the connect loop already produces.

Co-authored-by: Cursor <cursoragent@cursor.com>
time.monotonic() returns float; initializing the same kind of value
with 0 made mypy infer int. 0.0 is the same sentinel and still falsy
for the "never failed" check.

Co-authored-by: Cursor <cursoragent@cursor.com>
stats, rsyslog_client, kafka_producer, websocket, runner, and the
systest rsyslogd Popen start as None. mypy infers Any | None, and Any
does not narrow under `if not x`.

Annotate T | None and re-check `is None` after init. The second check
is also runtime: _init_* can still return None once running is False;
returning False then matches the existing send-failed path instead of
AttributeError. Skip the main-loop stats metric if handle_new_config
has not run; _wait_until_running says so if start() was skipped.

comms_channel_round rebound `task` after asyncio.wait; pending_task is
a fresh name so consumer_task can stay None.

Co-authored-by: Cursor <cursoragent@cursor.com>
Empty and mixed literals inferred types that rejected later operations
(converters as object, the Cloud Logging body as Collection[str], empty
collections with no element type). Websocket connect() cannot take
those mixed kwargs as a splat; pass the known arguments by name and
splat only the extras.

The Cloud Logging body is dict[str, Any]: a JSON payload for a
third-party API, not a structure we own.

Co-authored-by: Cursor <cursoragent@cursor.com>
_generate_request_builder typed log entries as dict[str, str]; callers
pass nested jsonPayload, and sometimes timestamp and severity. Empty
in_queue, pending, senders, and the senders-less reader config need an
element type so later appends and JournalReader construction typecheck.

Co-authored-by: Cursor <cursoragent@cursor.com>
mypy skips those bodies by default, which is one reason
reader.unregister_from_poll(self.poller) survived in configure_readers
(the other is that readers was an untyped {}). check_untyped_defs
checks them anyway.

Aiven-Open#202

Co-authored-by: Cursor <cursoragent@cursor.com>
disallow_untyped_defs and disallow_incomplete_defs would fail 293 times
on this tree. A global True plus per-module overrides keeps new files
checked and existing modules exempt until they are annotated.

Co-authored-by: Cursor <cursoragent@cursor.com>
_float_hex_to_int returns None when |f| >= 1. encode() already rejects
|lat| >= 90 and wraps lon into [-180, 180), so that None is unreachable
from encode; the assertions exist only for mypy.

Co-authored-by: Cursor <cursoragent@cursor.com>
requests is imported inside get_requests_session on purpose (it and its
dependencies are heavy), so Session is imported under TYPE_CHECKING.
default_json_serialization now returns None explicitly — same value,
required once the return type is written. Logplex-Msg-Count goes out as
a decimal string; requests types headers as str | bytes | None.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Empty hostname now becomes NILVALUE, matching the other optional
fields. PID is stringified before formatting because journal fields may
hand it as int. The unit-test socket stub only implements sendall/close,
so assigning it to socket | None is ignored there.

Co-authored-by: Cursor <cursoragent@cursor.com>
send_messages returns bool: True accepted, False retry. The base
implementation returns False instead of implicit None, which the run
loop already treated as failure. Tests install stub classes that are
not LogSender subclasses, so the registry is type[Any].
rsyslog_server is required, same as file_output and logplex_token;
config.get made it Optional once config was dict[str, Any].

Co-authored-by: Cursor <cursoragent@cursor.com>
Logplex now returns True after a successful post. It previously fell
off the end with None, so the run loop treated a completed send as a
retry. inspect.getmembers does not prove the Kafka error classes are
exception types, so that helper returns tuple[Any, ...].

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
load_cert_chain and the SOCKS connect call do not accept None. When SSL
is on, ca/keyfile/certfile must be set; a proxied URI must include host
and port. Those were TypeError/AttributeError; they are ValueError now.

Co-authored-by: Cursor <cursoragent@cursor.com>
…gLevel

convert_realtime takes Any because the journal hands timestamps as
bytes or int. Tests used an integer cursor stand-in, now "10".

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
stats may still be None when a message is truncated; skip the metric
rather than call increase on None.

Co-authored-by: Cursor <cursoragent@cursor.com>
gauge/increase are skipped when stats is None rather than called on
None. ip_to_geohash returns "" when geoip or lat/lon is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Tests that wrote config through pytest's legacy tmpdir now use
tmp_path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Tests that wrote config through tmpdir now use tmp_path.

Co-authored-by: Cursor <cursoragent@cursor.com>
mbertheau and others added 17 commits August 22, 2026 11:06
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Tests that wrote config through pytest's legacy tmpdir now use
tmp_path.

Co-authored-by: Cursor <cursoragent@cursor.com>
journalpump_initialized returns a bool; a live pump with senders was
already truthy via running-and-senders. Every module now requires
complete annotations.

Co-authored-by: Cursor <cursoragent@cursor.com>
google-auth's from_service_account_info has no annotations, so the
service-account path needs an ignore. google.auth.default returns
(credentials, project); the ADC branch passed that tuple to the
logging client.

Co-authored-by: Cursor <cursoragent@cursor.com>
The re2/stdlib re switch is not a redefinition under mypy 2.3.1, so
the no-redef ignores on those imports did nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Popen env values are strings; LOGS_DIRECTORY and RUNTIME_DIRECTORY were
Paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
An unpinned bump can fail the lint job with no change on our side.
These are the versions CI and this tree already run.

Co-authored-by: Cursor <cursoragent@cursor.com>
pre-commit appended staged .py paths to `make pylint`. Make treated
those as extra goals and only succeeded because the files already
existed. The recipe always lints PYLINT_DIRS.

Co-authored-by: Cursor <cursoragent@cursor.com>
A blanket ignore_missing_imports hides a dependency that starts
shipping types. systemd, kafka, botocore, googleapiclient, snappy,
re2, zstandard, httplib2, and setuptools are the ones still without
stubs or a py.typed marker.

Co-authored-by: Cursor <cursoragent@cursor.com>
Left unpinned; we do not pin setuptools itself.

Co-authored-by: Cursor <cursoragent@cursor.com>
Left unpinned; we do not pin httplib2.

Co-authored-by: Cursor <cursoragent@cursor.com>
Left unpinned; we do not pin botocore.

Co-authored-by: Cursor <cursoragent@cursor.com>
The stubs describe snappy.compress; callers used the internal
snappy.snappy module. Left unpinned; we do not pin python-snappy.

Co-authored-by: Cursor <cursoragent@cursor.com>
entries().write expects a WriteLogEntriesRequest TypedDict. Left
unpinned; we do not pin google-api-python-client.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pinned to 2.3.x to match kafka-python<2.4.0. The producer stub has no
_COMPRESSORS; getattr keeps the zstd probe. FutureRecordMetadata.get is
untyped on the stub.

Co-authored-by: Cursor <cursoragent@cursor.com>
pytest tmp_path is already a Path; converting to str at every JournalPump
call site existed only to satisfy ServiceDaemon's string assert. Convert
argv and the JSON state-file setting at those boundaries and keep Path
from there on.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mbertheau
mbertheau force-pushed the mbertheau-typing branch 2 times, most recently from e9febb3 to fd4fcc2 Compare August 22, 2026 09:27
@mbertheau
mbertheau marked this pull request as ready for review August 22, 2026 09:42
@willyborankin
willyborankin merged commit 7fb9aea into Aiven-Open:master Aug 25, 2026
6 checks passed
@mbertheau
mbertheau deleted the mbertheau-typing branch August 25, 2026 07:54
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