oc_chat
Loading...
Searching...
No Matches
__init__.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_ui/terminal/__init__.py
4
5"""
6Terminal UI components for the oc_ui package.
7
8Purpose
9-------
10Re-exports the concrete presenter classes and prompt driver that make
11up OC Chat's terminal frontend. Importing ``oc_ui.terminal`` is the
12canonical way for the engine to obtain a presenter without naming the
13specific implementation.
14
15Responsibilities
16----------------
17- Re-export :class:`ConsolePresenter`, :class:`RichPresenter`,
18 :class:`LoggingPresenter`, :class:`PromptDriver`, and
19 :class:`CommandCompleter` (alias for :class:`OCChatCompleter`).
20- Define ``__all__`` so ``from oc_ui.terminal import *`` works as
21 documented, and ``__dir__`` so the lazy names stay discoverable.
22- Keep the names backed by an OPTIONAL distribution out of the import
23 path until something actually asks for them.
24
25Eager / lazy split (plan 233)
26-----------------------------
27Python executes a package ``__init__`` before any of its submodules,
28so an eager re-export here decides whether the whole package is
29importable. The split is therefore per-submodule, decided by what that
30submodule imports at MODULE level:
31
32===================== ========= ====================================
33Submodule Binding Why
34===================== ========= ====================================
35``presenter`` eager stdlib + oc_diagnostics + oc_ui only
36``input`` eager stdlib + oc_diagnostics + oc_ui only;
37 ``prompt_toolkit`` is imported inside
38 ``PromptDriver.__init__``, behind its
39 own ``except ImportError`` guard
40``logging_presenter`` eager stdlib + oc_diagnostics + oc_ui only
41``rich_presenter`` lazy imports ``rich`` at module level
42``completers`` lazy imports ``prompt_toolkit`` at module
43 level
44===================== ========= ====================================
45
46Keeping ``presenter`` eager is the point of the exercise:
47:class:`ConsolePresenter` is the deliberately rich-free fallback, and
48``oc_chat.cli`` imports it at module level. It must stay importable
49with zero optional dependencies installed.
50
51Diagnostics
52-----------
53Domain: OC_UI.TERMINAL
54Levels:
55 L2 — lifecycle events
56 L3 — semantic details
57 L4 — deep tracing
58
59Contracts
60---------
61- This module has no logic of its own beyond name resolution — the
62 eager re-imports, plus the PEP 562 ``__getattr__``/``__dir__`` pair
63 that resolves the lazy ones.
64- Importing it must not start any threads, open any files, or import
65 any optional distribution.
66- A lazy name resolves to the SAME object as an import from its
67 defining module, and is cached in the module globals on first
68 access, so ``oc_ui.terminal.RichPresenter is
69 oc_ui.terminal.rich_presenter.RichPresenter`` holds and the import
70 runs once.
71- When the backing distribution is absent, resolving a lazy name
72 raises :exc:`ImportError` AT THE ACCESS SITE — deliberately, so the
73 callers that already catch it (``oc_chat/cli.py``) can fall back to
74 the plain presenter instead of the process dying at import.
75
76Dependencies
77------------
78- oc_diagnostics (structured debug logging via _dbg)
79- oc_ui.terminal.presenter (ConsolePresenter, re-exported eagerly)
80- oc_ui.terminal.input (PromptDriver, re-exported eagerly)
81- oc_ui.terminal.logging_presenter (LoggingPresenter, re-exported eagerly)
82- oc_ui.terminal.completers (lazy; OCChatCompleter, re-exported as CommandCompleter on first access)
83- oc_ui.terminal.rich_presenter (lazy; RichPresenter, re-exported on first access)
84"""
85
86from importlib import import_module
87from typing import TYPE_CHECKING
88
89from oc_diagnostics import _dbg
90
91from .presenter import ConsolePresenter
92from .input import PromptDriver
93from .logging_presenter import LoggingPresenter
94
95if TYPE_CHECKING: # pragma: no cover - import-time typing only
96 from .completers import OCChatCompleter as CommandCompleter
97 from .rich_presenter import RichPresenter
98
99# Public name -> (submodule relative to this package, attribute in it).
100# Every entry costs an optional distribution to resolve.
101_LAZY_EXPORTS = {
102 'RichPresenter': ('.rich_presenter', 'RichPresenter'),
103 'CommandCompleter': ('.completers', 'OCChatCompleter'),
104}
105
106_dbg("OC_UI.TERMINAL", "module", "loaded", 4)
107
108__all__ = [
109 'ConsolePresenter',
110 'RichPresenter',
111 'PromptDriver',
112 'CommandCompleter',
113 'LoggingPresenter',
114]
115
116
117def __getattr__(name: str) -> object:
118 """Resolve a lazily re-exported name on first access (PEP 562).
119
120 Parameters
121 ----------
122 name:
123 The attribute Python failed to find in the module globals.
124
125 Returns
126 -------
127 object
128 The attribute from the defining submodule, also cached into
129 this module's globals so later lookups never reach here.
130
131 Raises
132 ------
133 AttributeError
134 ``name`` is not a lazy export — the normal miss.
135 ImportError
136 The optional distribution backing ``name`` is not installed.
137 Propagated unchanged: callers such as ``oc_chat.cli`` catch it
138 and degrade to the plain presenter.
139 """
140 func = "__getattr__"
141 target = _LAZY_EXPORTS.get(name)
142 if target is None:
143 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
144
145 module_name, attribute = target
146 _dbg("OC_UI.TERMINAL", func, f"resolving lazy export {name}", 4)
147 value = getattr(import_module(module_name, __name__), attribute)
148 globals()[name] = value
149 _dbg("OC_UI.TERMINAL", func, f"resolved {name} from {module_name}", 4)
150 return value
151
152
153def __dir__() -> list[str]:
154 """List the public surface, including the not-yet-resolved names."""
155 return sorted(set(globals()) | set(_LAZY_EXPORTS))
list[str] __dir__()
Definition __init__.py:153
object __getattr__(str name)
Definition __init__.py:117