teng-lin--notebooklm-py
09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
200 行
9.5 KiB
Python
200 行
9.5 KiB
Python
"""``make_fake_core`` factory — constructor-injection substrate for sub-clients.
|
|
|
|
This module provides a single entry point — :func:`make_fake_core` — that
|
|
returns a ``FakeSession`` instance shaped to satisfy the **shared
|
|
capability Protocols** in :mod:`notebooklm._runtime.contracts`
|
|
(``RpcCaller``, ``LoopGuard``, ``Kernel``) plus the single-consumer
|
|
Protocols inlined into their owning feature modules in issue #1327
|
|
(``AuthMetadata`` in ``notebooklm._source.upload``,
|
|
``OperationScopeProvider`` in ``notebooklm._artifact.polling``). Feature APIs that
|
|
need more than one capability take their direct collaborators by
|
|
keyword-only constructor argument (``ChatAPI`` in ``notebooklm._chat.api``,
|
|
``ArtifactsAPI`` in ``_artifacts.py``, ``SourceUploadPipeline`` in
|
|
``notebooklm._source.upload``); the feature-local composite Protocols
|
|
``ArtifactsRuntime`` and ``UploadRuntime`` (and their adapter
|
|
dataclasses) were retired once it was clear they only hid three stable
|
|
collaborators with one production satisfier. (``ChatRuntime`` was
|
|
deleted earlier on the same grounds — ADR-0014 Rule 2 Corollary.) The
|
|
``RpcCaller`` surface is exposed two ways: directly as
|
|
``fake.rpc_call`` (legacy single-attribute access path that some tests
|
|
still use) AND as ``fake.rpc_executor.rpc_call`` mirroring the
|
|
production composition where ``NotebookLMClient`` stores
|
|
``composed.executor`` as ``self._rpc_executor`` and passes it to every
|
|
feature API. Both attributes are wired to the same underlying mock so
|
|
``fake.rpc_call.assert_awaited`` and
|
|
``fake.rpc_executor.rpc_call.assert_awaited`` observe the same calls.
|
|
Tests pass the result to a sub-client constructor (e.g.
|
|
``NotebooksAPI(fake.rpc_executor)``) instead of constructing a real
|
|
client/runtime stack and mutating its attributes after the fact.
|
|
|
|
Phase 7 (refactor-history.md §Migration Plan step 10) deleted the broad
|
|
``Session`` Protocol that this factory's defaults dict previously
|
|
mirrored member-for-member. The dict now lists only the attribute slots
|
|
features actually exercise — promoting an attribute requires a real
|
|
test-site consumer, mirroring the ADR-0013 promotion criterion for
|
|
shared Protocols.
|
|
|
|
See :doc:`docs/adr/0007-test-monkeypatch-policy.md` for the policy that
|
|
makes this factory the only sanctioned substitute for the forbidden
|
|
``monkeypatch.setattr("notebooklm.…")`` and
|
|
``target.rpc_call = AsyncMock(…)`` patterns.
|
|
|
|
Design choices (documented in ADR-0007 "Alternatives considered"):
|
|
|
|
- ``FakeSession`` is a plain class with explicit attribute storage
|
|
(``types.SimpleNamespace``-shaped). It is *not* a spec-based
|
|
``MagicMock`` because spec-based mocks silently auto-vivify
|
|
attributes and would tie the factory to a single concrete class
|
|
shape rather than the open set of narrow Protocols.
|
|
- Async-surface defaults use :class:`unittest.mock.AsyncMock`;
|
|
sync-surface defaults use :class:`unittest.mock.MagicMock`. Both are
|
|
configured with benign return values so a test that only exercises one
|
|
attribute does not have to define the others.
|
|
- Overrides are keyword-only — positional arguments would conflict with
|
|
the ``**overrides`` extension point if new attributes are added later.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import httpx
|
|
|
|
|
|
class FakeSession:
|
|
"""A duck-typed stand-in for capability-Protocol collaborators in tests.
|
|
|
|
Named ``FakeSession`` for backward compatibility with the broad-
|
|
``Session``-era test sites; the class itself is just an explicit
|
|
attribute bag and is not pinned to any single Protocol shape.
|
|
|
|
Attribute storage is explicit (the constructor only sets what's
|
|
passed in) so that accessing an attribute the production code does
|
|
not actually use surfaces as a clear ``AttributeError`` rather than
|
|
as a silent auto-vivified ``MagicMock``. The canonical schema lives
|
|
in :func:`make_fake_core`'s ``defaults`` dict — one source of truth
|
|
so the schema cannot drift between two declarations.
|
|
|
|
Most tests should construct instances via :func:`make_fake_core`,
|
|
which fills in benign defaults; direct construction is also
|
|
supported when a test wants to assert that no defaults are read.
|
|
"""
|
|
|
|
def __init__(self, **attrs: Any) -> None:
|
|
for name, value in attrs.items():
|
|
setattr(self, name, value)
|
|
|
|
|
|
def make_fake_core(**overrides: Any) -> FakeSession:
|
|
"""Return a :class:`FakeSession` with benign defaults overridden.
|
|
|
|
All overrides are keyword-only and replace the corresponding default.
|
|
Passing an unknown keyword raises ``TypeError`` early so test typos
|
|
don't silently no-op.
|
|
|
|
The historical ``rpc_call=`` keyword is preserved as a convenience —
|
|
it is unwrapped into ``rpc_executor=SimpleNamespace(rpc_call=<value>)``
|
|
so the live ``RpcCaller`` Protocol surface on the fake matches the
|
|
production shape (``NotebookLMClient.__init__`` stores
|
|
``composed.executor`` as ``self._rpc_executor`` and passes it to
|
|
every feature API).
|
|
|
|
Example::
|
|
|
|
fake = make_fake_core(rpc_call=AsyncMock(return_value=[payload]))
|
|
api = NotebooksAPI(fake.rpc_executor)
|
|
result = await api.list()
|
|
fake.rpc_executor.rpc_call.assert_awaited_once()
|
|
"""
|
|
|
|
def _operation_scope(_label: str):
|
|
@asynccontextmanager
|
|
async def scope() -> AsyncIterator[None]:
|
|
yield None
|
|
|
|
return scope()
|
|
|
|
live_cookies = httpx.Cookies()
|
|
fake_http_client = SimpleNamespace(cookies=live_cookies)
|
|
auth = SimpleNamespace(authuser=0, account_email=None)
|
|
kernel = SimpleNamespace(
|
|
cookies=live_cookies,
|
|
get_http_client=MagicMock(return_value=fake_http_client),
|
|
)
|
|
|
|
# Phase 7 (refactor-history.md §Migration Plan step 10) shrunk this dict from
|
|
# the broad-Session-era 25+ entries to the minimum set that satisfies
|
|
# the post-refactor capability and feature-local runtime Protocols.
|
|
# New entries should only be added when a real test site exercises
|
|
# the attribute — mirroring the ADR-0013 promotion criterion for
|
|
# shared Protocols (≥2 consumers).
|
|
# ``rpc_call`` is shared between the direct ``fake.rpc_call`` and the
|
|
# ``fake.rpc_executor.rpc_call`` mirror so both attribute paths see
|
|
# the same observed calls. Fresh list per call so tests can mutate
|
|
# the response without bleeding into siblings.
|
|
rpc_call_mock = AsyncMock(side_effect=lambda *a, **kw: [])
|
|
|
|
defaults: dict[str, Any] = {
|
|
# AuthMetadata + Kernel — consumed by SourceUploadPipeline test sites.
|
|
"auth": auth,
|
|
"kernel": kernel,
|
|
# RpcCaller — every feature API uses this. The fake exposes the
|
|
# executor as a SimpleNamespace mirror so test sites address it
|
|
# the same way production code does (``fake.rpc_executor.rpc_call``
|
|
# mirrors ``client._rpc_executor.rpc_call``); the direct
|
|
# ``rpc_call`` attribute is kept for legacy single-attribute test sites
|
|
# that still treat the fake as a single bag-of-attributes.
|
|
"rpc_call": rpc_call_mock,
|
|
"rpc_executor": SimpleNamespace(rpc_call=rpc_call_mock),
|
|
# LoopGuard + OperationScopeProvider (the latter lives in
|
|
# ``notebooklm._artifact.polling`` after #1327) — used by ArtifactsAPI polling
|
|
# and SourceUploadPipeline.
|
|
"assert_bound_loop": MagicMock(return_value=None),
|
|
"operation_scope": MagicMock(side_effect=_operation_scope),
|
|
# DrainHookRegistration (local in ``_artifacts.py``) — close-time
|
|
# hook the artifacts runtime registers against in
|
|
# ``ArtifactsAPI.__init__``. Wave 2 of session-decoupling moved
|
|
# the storage onto ``TransportDrainTracker`` (ADR-0014 Rule 1); we
|
|
# keep ``_drain_hooks`` as a public attribute on the fake so test
|
|
# sites that previously read ``fake._drain_hooks["name"]`` still
|
|
# work (the fake doesn't have a real ``_drain_tracker``).
|
|
"_drain_hooks": {},
|
|
"register_drain_hook": MagicMock(return_value=None),
|
|
# Upload-pipeline glue: queue-wait recorder consumed by the
|
|
# ``SourceUploadPipeline`` upload metrics path. Kept on the bag
|
|
# so test sites that wire a SourcesAPI + uploader pair against a
|
|
# single FakeSession can rely on it.
|
|
"record_upload_queue_wait": MagicMock(return_value=None),
|
|
# NotebookSourceLister stub — exercised by ``test_notebooks.py``
|
|
# paths that resolve source IDs through the lister collaborator.
|
|
"get_source_ids": AsyncMock(side_effect=lambda *a, **kw: []),
|
|
}
|
|
|
|
def _register_drain_hook(name: str, hook: Any) -> None:
|
|
defaults["_drain_hooks"][name] = hook
|
|
|
|
defaults["register_drain_hook"] = MagicMock(side_effect=_register_drain_hook)
|
|
|
|
# Convenience: ``rpc_call=AsyncMock(...)`` overrides BOTH the direct
|
|
# ``rpc_call`` attribute AND the ``rpc_executor.rpc_call`` mirror with
|
|
# the same mock so test idioms using either path observe the same
|
|
# interactions.
|
|
if "rpc_call" in overrides:
|
|
overrides["rpc_executor"] = SimpleNamespace(rpc_call=overrides["rpc_call"])
|
|
|
|
# Validate overrides early so a typo like ``rpc_cal=`` fails loudly
|
|
# rather than landing as an unread attribute.
|
|
unknown = set(overrides) - set(defaults)
|
|
if unknown:
|
|
raise TypeError(
|
|
"make_fake_core() got unexpected keyword(s): "
|
|
f"{sorted(unknown)!r}. Known attributes: {sorted(defaults)!r}"
|
|
)
|
|
|
|
defaults.update(overrides)
|
|
return FakeSession(**defaults)
|