Skip to content

Context.user_config typed as object|None — inconsistent with other generic fields #105

Description

@codeforester

Problem

Context is already generic over three TypeVars — ConfigT, ApplicationStateT, ServicesT — giving consumers full type-safety for their config, application state, and service objects. However, user_config on Context is typed as object | None:

# context.py:63
user_config: object | None = None

And in profile.py, the UserConfigLoader protocol is:

def __call__(self, user_config: object | None) -> Path | None: ...

This means consumers who load typed user configuration (e.g., a dataclass with workspace paths, IDE preferences, etc.) have no way to express the type at the Context level — they must cast it in every command function:

def my_command(ctx: base_cli.Context) -> int:
    user_cfg = cast(MyUserConfig, ctx.user_config)  # repeated everywhere
    ...

Options

Option A — Add a fourth TypeVar UserConfigT

UserConfigT = TypeVar("UserConfigT")

@dataclass
class Context(Generic[ConfigT, ApplicationStateT, ServicesT, UserConfigT]):
    ...
    user_config: UserConfigT | None = None

App would then wire UserConfigT from the CliProfile.load_user_config return type. This is consistent with how the other three generics work, but adds complexity to an already 4-parameter generic type.

Option B — Document user_config as intentionally opaque with a typed accessor pattern

Leave the field as object | None but document that consumers should access it through a typed property on a custom Context subclass or a typed helper. This keeps the Context signature simpler but provides no IDE support out of the box.

Option C — Use a Protocol-bounded TypeVar

Introduce a UserConfigProtocol that user config types can satisfy, and bound UserConfigT to it. Provides structural typing without requiring inheritance.

Recommendation

Option A gives the most consistent experience with how ConfigT, ApplicationStateT, and ServicesT already work. The downside (a 4-parameter generic) is mitigated by the fact that most consumers will use Context[dict, None, None, MyUserConfig] or a type alias.

Even if Option B or C is chosen, the current object | None annotation should have an explicit docstring note explaining the design choice and the recommended typed-access pattern.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or product improvement

Type

No type

Projects

Status
Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions