Services¶
Services are long-lived dependencies that handlers (and other services) can request via dependency injection. This page lists the services that ship in this repo and where they are registered. Availability depends on whether the Extension Runner provides the service, a preset declares it, or an extension activates it.
Core services (always available)¶
These services are registered by the Extension Runner at startup and are available in every handler without extra configuration.
| Interface | Default implementation | Notes |
|---|---|---|
finecode_extension_api.interfaces.ilogger.ILogger |
loguru.logger via finecode_extension_runner.impls.loguru_logger.get_logger |
Logging (trace/debug/info/warn/error/exception). |
finecode_extension_api.interfaces.icommandrunner.ICommandRunner |
finecode_extension_runner.impls.command_runner.CommandRunner |
Async and sync subprocess execution. |
finecode_extension_api.interfaces.ifilemanager.IFileManager |
finecode_extension_runner.impls.file_manager.FileManager |
File system IO abstraction (read/write/list/create/delete). |
finecode_extension_api.interfaces.ifileeditor.IFileEditor |
finecode_extension_runner.impls.file_editor.FileEditor |
Open-file tracking, change subscriptions, read/write with editor awareness. |
finecode_extension_api.interfaces.icache.ICache |
finecode_extension_runner.impls.inmemory_cache.InMemoryCache |
In-memory, file-versioned cache. |
finecode_extension_api.interfaces.iprojectactionrunner.IProjectActionRunner |
finecode_extension_runner.impls.project_action_runner.ProjectActionRunnerImpl |
Run an action at project scope, routing through WM so the correct env-runner is chosen. If all handlers are in the current environment, communication with WM is omitted. |
finecode_extension_api.interfaces.iworkspaceactionrunner.IWorkspaceActionRunner |
finecode_extension_runner.impls.workspace_action_runner.WorkspaceActionRunnerImpl |
Fan-out an action across all workspace projects. |
finecode_extension_api.interfaces.irepositorycredentialsprovider.IRepositoryCredentialsProvider |
finecode_extension_runner.impls.repository_credentials_provider.ConfigRepositoryCredentialsProvider |
In-memory repository credentials and registry list. |
finecode_extension_api.interfaces.iprojectinfoprovider.IProjectInfoProvider |
finecode_extension_runner.impls.project_info_provider.ProjectInfoProvider |
Current project paths and raw config access. |
finecode_extension_api.interfaces.iextensionrunnerinfoprovider.IExtensionRunnerInfoProvider |
finecode_extension_runner.impls.extension_runner_info_provider.ExtensionRunnerInfoProvider |
Runtime env info (venv paths, cache dir). |
finecode_extension_api.interfaces.iworkspaceactionregistry.IWorkspaceActionRegistry |
finecode_extension_runner.impls.workspace_action_registry.WorkspaceActionRegistryImpl |
Read-only view of every action and handler in the workspace, across all projects and envs. See Reading the action registry. |
Caching with ICache¶
FineCode has no framework-level rebuilder: deciding whether a cached result lets a handler skip recomputation is the handler's own responsibility. For the rationale behind this design, see Caching is the handler's responsibility. ICache is the service that makes it convenient.
The protocol is intentionally small and file-versioned:
class ICache(Protocol):
async def save_file_cache(
self, file_path: Path, file_version: str, key: str, value: Any
) -> None: ...
async def get_file_cache(self, file_path: Path, key: str) -> Any: ...
class CacheMissException(Exception):
pass
keynamespaces the cached value so independent handlers (and independent computations within one handler) never collide. By convention a handler declares aCACHE_KEYclass attribute.file_versionis the version of the file the value was computed from — obtained fromIFileEditor(file_info.version). The cache stores the value against that version.get_file_cacheraisesCacheMissExceptionwhen there is no entry or when the file has changed since the entry was stored. A handler treats the miss as "do the real work".
The default implementation (InMemoryCache) compares the cached file version against the file's current version on every read, and on write discards a value whose source file already changed while the work was in flight — so a stale result is never served or stored.
Handler pattern (read-through cache, one file at a time):
class Flake8LintFilesHandler(...):
CACHE_KEY = "flake8"
async def run_on_single_file(self, file_uri):
file_path = resource_uri_to_path(file_uri)
try:
cached = await self.cache.get_file_cache(file_path, self.CACHE_KEY)
return DiagnosticFilesRunResult(messages={file_uri: cached})
except icache.CacheMissException:
pass
# read the current file version, compute the result, then cache it
file_version = ... # from IFileEditor
messages = await self._lint(file_path)
await self.cache.save_file_cache(
file_path, file_version, self.CACHE_KEY, messages
)
return DiagnosticFilesRunResult(messages={file_uri: messages})
ICache is the default, not the only option¶
The default InMemoryCache lives for the Extension Runner's lifetime, so cache hits span repeated invocations within one session (for example, an IDE relinting on every keystroke) but do not persist across runner restarts. Caching is a handler concern resolved through dependency injection, so you are free to go further:
- Swap the
ICacheimplementation. Bind your ownICachevia[[tool.finecode.service]]— for example a persistent, on-disk store that survives restarts, or a content-addressed cache. Every handler that injectsICachepicks it up with no code change. A persistent built-in cache is a planned FineCode direction. - Use a different caching service entirely. A handler is not required to use
ICacheat all. It may inject a caching service of its own design (with whatever key model and invalidation rules its work warrants), or rely on the underlying tool's native cache. The framework imposes no single caching mechanism.
Reading the action registry with IWorkspaceActionRegistry¶
IWorkspaceActionRegistry.list_actions() returns every action in the workspace — across all projects and all envs — as a list of ActionInfo. An Extension Runner only knows the actions its own env executes, so this whole-workspace picture is the only way a handler can reason about actions it does not itself run. It is what powers tooling like the knowledge extractor and which_handlers.
actions = await self.registry.list_actions()
for action in actions:
for handler in action.handlers:
...
ActionInfo carries name, source, canonical_source, scope, project, language, parent_action_source, file_loc, and handlers (a list of HandlerInfo, each with name, source, canonical_source, env, and file_loc).
Two properties of this data decide whether your code is correct:
source is an alias; canonical_source is the identity. source is the string as written in the definition file. For handlers it is almost always a package-level re-export (fine_python_ruff.RuffLintFilesHandler), not the module the class is defined in (fine_python_ruff.lint_handler.RuffLintFilesHandler). The same class can be re-exported under several aliases, so two different source values may name one real handler. canonical_source is cls.__module__ + "." + cls.__qualname__ as the runner that hosts the class resolved it, and it is the same string no matter which alias was used. Key by canonical_source; treat source as a display/config label.
Fields are resolved lazily, so canonical_source may be None. It is populated per env by that env's own runner, and it stays None until that runner has started, or permanently if the class cannot be imported there. A newly written action or handler that the running instance has not picked up yet is None for the same reason — it exists in source and in config, but no runner has resolved it.
The consequence is that one real class can be resolved on some rows and None on others at the same instant, because it is registered by many projects. So do not key each row on row.canonical_source or row.source — that splits one class into two identities, one canonical-keyed and one alias-keyed. Build an alias→canonical index across the whole list first, then key everything through it, under one rule: an unresolved row must never overwrite a resolved mapping. A single resolving runner anywhere then fixes the key everywhere, regardless of row order. (fine_knowledge's _index in providers/wm_registry.py is a worked example.)
Registry rows are also not deduplicated: a PROJECT-scope action appears once per project that registers it, so expect many rows per logical action.
Preset-provided services¶
These services are declared in presets in this repo. They are available when the preset is active, or when you copy the same [[tool.finecode.service]] entry into your own config.
| Interface | Implementation | Declared by |
|---|---|---|
finecode_extension_api.interfaces.ijsonrpcclient.IJsonRpcClient |
finecode_jsonrpc.jsonrpc_client.JsonRpcClientImpl |
presets/fine_python_lint |
finecode_extension_api.interfaces.ilspclient.ILspClient |
finecode_extension_runner.impls.lsp_client.LspClientImpl |
presets/fine_python_lint (wraps IJsonRpcClient) |
Extension-activated services¶
Extensions can register services via the finecode.activator entry point using IServiceRegistry. The following activators ship in this repo and register services when their packages are installed.
| Extension package | Interface | Implementation |
|---|---|---|
finecode_httpclient |
finecode_extension_api.interfaces.ihttpclient.IHttpClient |
finecode_httpclient.client.HttpClient |
fine_python_ast |
fine_python_ast.iast_provider.IPythonSingleAstProvider |
fine_python_ast.ast_provider.PythonSingleAstProvider |
fine_python_mypy |
fine_python_mypy.iast_provider.IMypySingleAstProvider |
fine_python_mypy.ast_provider.MypySingleAstProvider |
fine_python_package_info |
fine_python_package_info.ipypackagelayoutinfoprovider.IPyPackageLayoutInfoProvider |
fine_python_package_info.py_package_layout_info_provider.PyPackageLayoutInfoProvider |
fine_python_package_info |
finecode_extension_api.interfaces.isrcartifactfileclassifier.ISrcArtifactFileClassifier |
fine_python_package_info.py_src_artifact_file_classifier.PySrcArtifactFileClassifier |
fine_python_ruff |
fine_python_ruff.ruff_lsp_service.RuffLspService |
fine_python_ruff.ruff_lsp_service.RuffLspService |
fine_python_pyrefly |
fine_python_pyrefly.pyrefly_lsp_service.PyreflyLspService |
fine_python_pyrefly.pyrefly_lsp_service.PyreflyLspService |
Service registry for extensions¶
Extension activators receive an IServiceRegistry instance (not injected into handlers) and call register_impl() to bind interfaces to implementations. See finecode_extension_api.interfaces.iserviceregistry.IServiceRegistry for the protocol and the activators above for concrete examples.
How services are registered and resolved¶
A service binding is stored in one of two ways inside the Extension Runner's DI registry:
- As a ready instance — the core services are constructed at runner bootstrap and registered as instances.
- As a factory — every binding created through
IServiceRegistry.register_impl()(extension activators) and every[[tool.finecode.service]]declaration registers a factory keyed by interface. The implementation is constructed lazily on first injection, then cached as a singleton (andService.init()runs at that point).
When a handler requests an interface, the registry returns a cached instance if one exists, otherwise it invokes the factory. Instances therefore take priority over factories, so the core services registered as instances (logger, command runner, file manager, etc.) are fixed and cannot be rebound by activators or config.
Precedence — what overrides what¶
Bindings stored as factories are keyed by interface, and the last registration for an interface wins. Registration happens in this order at runner startup:
- Core services (Extension Runner bootstrap)
- Extension activators (
finecode.activatorentry points) [[tool.finecode.service]]declarations (merged from presets, then the project'spyproject.tomlon top)
So a [[tool.finecode.service]] declaration overrides an activator-registered default for the same interface. To replace the default IHttpClient implementation, for example, declare the same interface with your own source in pyproject.toml — it is applied last and wins.
Activation phases¶
Extension activators run in two phases.
Eager activation happens at runner startup. The runner seeds from the handler packages active in the current env, walks their declared dependency graph, and immediately activates every reachable package that exposes a finecode.activator entry point. Handler package activators (which register their own LSP services, AST providers, etc.) are activated here.
Deferred activation handles the rest. All installed packages that expose a finecode.activator entry point but were not reached by the eager walk are queued in alphabetical order. When a handler requests a service interface for which no factory is registered yet, the runner tries these deferred activators one by one in alphabetical order, stopping as soon as the interface is registered. Each deferred activator fires at most once. This is how service-only packages such as finecode_httpclient are activated: they have no handlers (so they are never a seed or a reachable dep), but their activator runs the first time IHttpClient is requested.
The full precedence — last registration for a given interface wins within a phase, and later phases cannot override earlier ones because deferred activators only fire on a miss:
- Core services (registered as instances at startup — cannot be rebound)
- Eager activators (registered as factories at startup)
[[tool.finecode.service]]declarations (applied last at startup, overrides eager activators)- Deferred activators (fired on first request, only when no factory exists — cannot override the above)
Where to register a reusable service¶
For a service whose interface lives in finecode_extension_api and whose implementation is a separate, replaceable package (for example IHttpClient/finecode_httpclient):
- Ship the default binding in the implementation package's own activator. It will be picked up via deferred activation whenever the interface is first requested, with no dependency coupling between consumer packages and the implementation package.
- Reserve
[[tool.finecode.service]]for overrides — swapping in an alternative implementation where being explicit is the point. A service declaration is applied at startup (phase 3) and therefore prevents the deferred activator from firing at all.
For a service whose interface and implementation are owned by the same extension (the AST providers above), register it directly in that extension's activator; it will be activated eagerly when the extension is active.