WriterAgent uses Astral’s ty on the plugin/ tree. This document covers what changed in the code (especially UNO and extension patterns), where it landed, and the minimum tooling needed to run the checker. Quick-reference annotation rules are in Other recurring fixes below.
| Stage | Notes |
|---|---|
| Initial | On the order of 1000+ diagnostics before scoping (including vendored plugin/contrib and noisy test-only code). |
| After narrowing | Excluding plugin/contrib, plugin/lib (vendored wheels / pip --target trees), and plugin/tests via pyproject.toml focused work on application code; one documented pass fixed on the order of ~141 categorized issues in that scope. |
| Final | ty check reports no errors for the configured include set. make check runs ty only; make typecheck runs ty, mypy, and pyright in sequence; make test runs those three, then pytest and LO tests (types before tests). make release calls make test first, then the release bundle (see Makefile). |
Static checking does not prove LibreOffice runtime behavior: UNO remains highly dynamic. The goal is consistent annotations, usable stubs, and fewer accidental mistakes in Python code.
pyproject.toml—[tool.ty.src]:include = ["plugin"],exclude = ["plugin/contrib", "plugin/lib", "plugin/tests"].Makefile—make ty: ensuresimport uno(viamake fix-unoif needed), thenpython -m ty check --exclude plugin/contrib/ --exclude plugin/lib/.- Dev dependency:
types-unopy(LibreOffice API stubs).make fix-unolinks system UNO into.venvsounoandcom.sun.starresolve; without that, the checker cannot see extension types. - Windows / ARM64 note:
make fix-unois a static-analysis helper, not proof that external Python can run PyUNO. On Windows, especially ARM64, LibreOffice'spyuno.pydstill depends on matching Python ABI and native DLL loading; the extension runtime uses LibreOffice's own Python.
make mypy— same prelude asty(make manifest,import uno→make fix-uno), thenpython -m mypyusing[tool.mypy]inpyproject.toml.- Not part of
make checkormake buildalone; it is part ofmake typecheckandmake test.make releaserunsmake testfirst, so mypy runs there too. Use standalonemake mypyto compare againstty. Mypy often reports issuestydoes not (and vice versa). - Scope:
packages = ["plugin"]with pathexcludeplus[[tool.mypy.overrides]]ignore_errors = trueforplugin.contrib.*,plugin.lib.*, andplugin.tests.*. Plainexcludealone does not stop mypy from checking vendored trees when resolving thepluginpackage, so the overrides mirror ty’s “no contrib / no lib / no tests” intent. - Stubs:
types-requests,types-unopy, and overrides forofficehelperare configured for a usable first run; remaining diagnostics are normal application code until you tighten further. Venv-only packages (e.g.sounddevicefor sidebar recording) are type-checked only when imported insideplugin/scripting/venv/.
make pyright— same prelude asty/mypy(make manifest, thenimport uno→make fix-unoif needed), thenpython -m pyrightusing[tool.pyright]inpyproject.toml(include/excludemirrorty).- Not part of
make checkormake buildalone; it is part ofmake typecheckandmake test(and thusmake release). Diagnostics overlap Pylance in the editor; use standalonemake pyrightfor a quick CLI pass. - Status (CLI): On the same scoped tree as
ty, Pyright can be driven to zero errors; remaining noise is usuallyreportMissingModuleSourceforcom.sun.star.*imports (stubs exist for typing, but Pyright still warns that it cannot resolve “source” for those modules). That is typically safe to ignore if runtime UNO works.
- Pyrefly — Meta’s Rust-based type checker and language server;
make pyreflyrunspython -m pyrefly checkwith the sameimport uno/make fix-unoprelude asmake pyright. - Not part of
make check,make typecheck, ormake test. Use as an optional fourth opinion while triaging;[tool.pyrefly]inpyproject.tomlsetsproject-includes,project-excludes, andpython-version(see Pyrefly configuration). - Like other static checkers, Pyrefly treats
typing.TYPE_CHECKINGas true, so imports and types underif TYPE_CHECKING:participate in analysis. Config setssearch-path = ["."]sofrom plugin...in those blocks resolves from the repo root, andcheck-unannotated-defs,infer-return-types, andinfer-with-first-usematch Pyrefly’s defaults for full analysis of checked modules. - Until the project drives Pyrefly to zero errors, CI should not gate on it; treat output as experimental signal alongside ty/mypy/pyright.
All three tools share types-unopy, make fix-uno, and the same plugin/ scope (contrib, lib, and tests excluded). make check and make build run ty only (fast gate). make test runs ty, mypy, and pyright, then tests. make release runs make test (same gate) before building the release .oxt. A full Pyright pass still found real issues and strictness gaps that ty (and mypy) often did not report on the same codebase, or reported much less loudly.
reportOptionalMemberAccess: Pyright is aggressive about calling methods on values that may beNone. Example:DrawBridge.get_active_page()can returnNoneat runtime; without an explicitif page is None: ...early exit, uses likepage.getCount()were errors under Pyright even whentyaccepted the file. Fix: guard or assert before use (Draw shape tools inplugin/draw/shapes.py).reportPossiblyUnboundVariable: Assignments only on some branches (e.g.compiledonly insideif use_regex:, orrestore_snapshotonly insidetry) can be flagged when a later branch uses the name. Fix: initialize before the branch (compiled = None) or declarerestore_snapshot: dict[str, Any] | None = Nonebeforetry, then assign (search.py,testing_runner.py).
reportIncompatibleMethodOverride: Pyright checks return types and container types againsttypes-unopystrictly. Examples:XDispatchProvider.queryDispatchreturningNonewhere the stub expectsXDispatch, orqueryDispatchesreturning a list where the stub expects a tuple. Runtime UNO often allows this; fix is either to match the stub shape or a targeted# pyright: ignore[reportIncompatibleMethodOverride]on that method (DispatchHandlerinmain.py).reportGeneralTypeIssues: A second base class loaded from the Java/IDL bridge (e.g.XPromptFunctionfromorg.extension.writeragent) is not always treated as a valid class base. Fix: stub base inheritingunohelper.BaseforImportErrorfallbacks, plus# pyright: ignore[reportGeneralTypeIssues]on the concrete class when the real IDL base is present (prompt_addin.py/python/addin.py).reportIncompatibleVariableOverride: Multiple mixins declaring the same attribute (e.g.client) with types that Pyright considers incompatible under invariance (mutableProtocolfields vs concrete class).tymay not emit the same diagnostic; resolving it may require aligning annotations, widening aProtocolfield, or structural refactors (chatbot panel / mixins).listinvariance (Pyright / strict typing): Passinglist[ChatMessage]where an API is typed aslist[ChatMessage | dict[...]]can fail in Pyright;tymay be looser. Fix:cast(...)or widen the target API type (smol_modelpaths).
reportArgumentTypeonint(...):get_config(key)is effectively JSON-shaped (Any/ wide unions). Pyright rejectsint(get_config(...))when the inferred type includes non-numeric shapes. Fix: useget_config_int/get_config_strwith an explicit-> int(orstr) helper signature (config.py, call sites such asprompt_function.pyfor=PROMPT()).
- If the first assignments build a
dict[str, str], laterpayload["details"] = {...}can fail in Pyright.tymay not flag the same. Fix: annotatepayload: dict[str, Any]orcast(dict[str, Any], ...)(format_error_payload,tool_registrymerges).
- Nested patterns like
getattr(ctx_any, "ServiceManager", getattr(ctx_any, "getServiceManager", lambda: None)())triggeredreportAttributeAccessIssue/ optional access onAny. Fix: small helper or sequentialgetattr+callablechecks,assert smgr is not None, thencast(Any, smgr).createInstanceWithContext(...)(uno_context,dialogs._load_xdl,image_tools,queue_executor,mainicon loading).
urllib: Importingurllib.error(or similar) inside a function that also usesurllib.request/urllib.parsecan make Pyright narrow theurllibpackage incorrectly. Prefer module-level imports forurllibsubmodules.try/except ImportError: Fallback functions likedef is_writer(model): return Falsecan be inferred as-> Literal[False]while the imported symbol is(Any) -> bool, producingreportAssignmentTypewhen both arms assign into one logical “slot”. Fix: explicit-> boolon the fallbacks (testing_runner.py).
sqlite3may be typed as optional; Pyright wantsassert sqlite3 is not Noneon paths that use it afterHAS_SQLITE.user_config_dir()asstr | None: filesystem stores shouldraise ConfigErrorrather than joining onNone(MemoryStore,SkillsStore).
model_outputnot alwaysstr: guard withisinstanceorstr(...)beforestrip()(several agent/chat paths).EffectInterpreter.current_state: declareSendHandlerState | NonewhereNoneis a real state (send_handlers).
After Pyright-driven edits, run make ty (or make test) anyway: fixes for Pyright do not always change ty, and occasionally one tool will disagree. make build enforces ty; make test / make release enforce all three tools.
These are the recurring themes that dominated the cleanup, beyond “add str | None everywhere.”
Many modules import constants from com.sun.star.*. Stubs or resolution can fail; some code paths must run without LibreOffice (tests, analysis). The pattern is: try real imports, else cast(Any, …) integer stand-ins so the rest of the module still type-checks.
See plugin/calc/error_detector.py (and similarly analyzer/inspector): CellContentType, FormulaResult, and a fallback branch with cast(Any, 0) … cast(Any, 4).
Some imports stay as # type: ignore[unresolved-import] where the checker still cannot resolve a particular com.sun.star module path.
uno.createUnoStruct("com.sun.star.beans.PropertyValue") and similar return values that stubs treat loosely. The codebase uses cast(Any, …) where a struct is built and passed through (e.g. plugin/writer/format_support.py).
plugin/framework/queue_executor.py passes uno.Any("void", None) into UNO callbacks; that line is explicitly ignored where the stub contract does not match pyuno’s usage.
types-unopy expects the same parameter names as the .pyi stubs. Implementations of XActionListener, XEventListener, etc. must use names like rEvent and Source, not arbitrary ev / e, or ty raises invalid-method-override.
Examples: plugin/chatbot/dialogs.py (TabListener: actionPerformed(self, rEvent), disposing(self, Source)), plugin/chatbot/panel_resize.py (on_window_resized(self, rEvent) and use of rEvent.Source).
Runtime UNO uses queryInterface heavily; return types are often opaque. Class-based queryInterface can be unreliable under pyuno (see AGENTS.md); typing-wise, code may need # type: ignore[attr-defined] or narrow casts after a successful query. Draw/Writer code that obtains XSelectionSupplier and similar follows this pattern.
ToolCallingMixin and send handlers are mixed into large panel classes. ToolLoopHost in plugin/chatbot/tool_loop.py and SendHandlerHost in plugin/chatbot/send_handlers.py declare the attributes and methods the mixin expects so self is checkable without circular imports.
Heavy or circular imports (e.g. LlmClient, ChatSession) are imported under if TYPE_CHECKING: at the top of the mixin modules so runtime import order stays unchanged but static analysis sees the types.
When attaching extra fields to objects (e.g. approval flows on events), the code uses setattr / getattr so the analyzer does not treat unknown attributes as errors—see tool-loop paths that set things like query_override on events (plugin/chatbot/tool_loop.py).
plugin/framework/i18n.py uses cast(Any, ctx).getServiceManager() (or similar) because the UNO context type surface does not always expose what we need cleanly in stubs.
Prefer specific ignore codes (attr-defined, override, unresolved-import, …) over blanket ignores. Reserve them for pyuno/UNO boundaries, third-party quirks, or legacy hotspots—not for silencing ordinary Python mistakes.
Protocolfor mixin hosts (e.g. tool-loop mixins).TYPE_CHECKING+ruffTCrules for imports used only in hints.- Explicit generics:
list[str],dict[str, Any],str | Noneinstead of untyped collections. - Narrowing:
if x is not Nonebefore use; avoid forcing the checker to assume values are defined. cast(Any, …)/cast(Iterable, …)where stubs are thin or generators are not inferred as iterable.- UNO interface overrides: match stub parameter names exactly (e.g.
actionPerformed(self, rEvent)) orty/pyright reportinvalid-method-override. - Registry / service construction: dynamic class registration may need small ignores where instantiation is reflection-like (
plugin/framework/service_registry.py). threading.Lock | None(and similar): On Python before 3.13,threading.Lockis a factory (builtin_function_or_method), not a class — evaluatingLock | Noneat import time raisesTypeErrorand can abort whole module loads (e.g. ChatbotModule → missinglibrarian_onboarding). Preferfrom __future__ import annotationsso PEP 604 unions are not evaluated at runtime (seeplugin/chatbot/web_research.py). Dev.venvis 3.13+ whereLockis a real type, so this bug is easy to miss locally; LibreOffice’s bundled Python is often older.
Roughly 40+ files were edited; groupings below match the original tracking notes.
Framework
plugin/framework/errors.py,image_utils.py,legacy_ui.py,logging.py,service.py,settings_dialog.py,smol_agent.py,tool.py
Entry / backends
Calc
plugin/calc/analyzer.py,error_detector.py,formulas.py,inspector.py,editselection.py,manipulator.py
Chatbot / sidebar
- Panel, factory, wiring, resize, state machine, send handlers, tool loop, web research, history, audio paths, etc. under
plugin/chatbot/
Writer / HTTP / infra
- Writer tools and format paths; HTTP client/errors; plus build/docs updates (
Makefile,AGENTS.md, locales where relevant).
Narrow ignores at UNO boundaries
obj.method_call() # type: ignore[attr-defined]Explicit annotations where the body is still dynamic
def process_data(data: Any) -> Any:
return data.process() # type: ignore[no-any-return]Unions and optional values
variable: str | int | None = get_value()
if obj is not None:
obj.method()Override compatibility with stubs
def actionPerformed(self, rEvent: ActionEvent) -> None: # type: ignore[override]
...(Prefer matching stub parameter names exactly so ignore[override] is unnecessary when possible.)
- Incremental fixes (small batches +
ty check) beat large single dumps. - Many errors share one pattern (especially overrides and
com.sun.starimports). - UNO needs explicit boundaries: ignores and casts at pyuno edges, not scattered through pure Python logic.
- Keep stub names for listeners/interfaces aligned with
types-unopy.
make fix-unowhenimport unofails in the venv.make tyormake checkbefore quick iterations;make test(ormake releasebefore shipping) forty+ mypy + pyright plus pytest and LO tests.- When adding features, follow the UNO patterns above, Other recurring fixes, and—if you use Pyright—the Pyright vs
tyand mypy section for strictness that may not show up inmake ty.