diff --git a/evidence/subtree-mutation-source-20261008/README.md b/evidence/subtree-mutation-source-20261008/README.md new file mode 100644 index 0000000000..e4c78dc601 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/README.md @@ -0,0 +1,109 @@ +# Explicit mutation geometry + +`Compositor.render_subtree_strips` keeps mandatory `admit`: it acquires the +complete original paint cohort synchronously, then returns `Size` and an +iterator of strip bands. Each band uses the original cuts and renderer, is at +most the acquired viewport height, and closes borrowed geometry before yielding. +Preceding-source callers drain before writing; retirement callers can cooperate +between bands while their original witnesses and custody remain current. The +six band controls at `57523e1b` passed (1.86s) and were not repeated here. + +Visible arrangement no longer forces complete offscreen acquisition merely to +fill an intrinsic cache entry. A partial source retains exact placed geometry. +`Compositor.acquire_subtree_geometry(root)` supplies the existing complete +`SubtreeGeometryPlacement` before writes; no paint or publication occurs. The +transaction owns that source independently of cache eviction. Missing placement +or an incomplete source returns None. Expansion to an unmodified ancestor +borrows held children; a new descendant scope inside a held complete source is +unavailable because that source does not own every descendant arrangement input. + +Screen's mutation hook now returns original roots mapped to these placements +(or None for the exact published fallback). Both resource kinds capture native +parent edges once. Shared source matching retains distinct exact/translated +placement rules; held projection filters original prune admission and native +self-paint participation along captured ancestry. Screen overlays retain their +screen clip, distinct from the acquired body's incoming/expanded paint clip. + +Mutation release calls `Screen.check_idle()`. Screen resumes its retained layout +members; the caller does not invalidate a window or manufacture a layout request. +Native scroll/source epochs remain unchanged, including capture currentness. +Layout messages record actionable intent before the message pump's queued next +callbacks run. Callback admission also derives that intent from the original +pending members after release; it can precede Idle. It uses the same acquired +mutation mapping and the existing layout-intent owner. Geometry wakeups do not +create new requests for members already consumed by that publication. + +An opaque source whose destination changes retains exact original rectangles. +It still owns captured membership and self-paint ancestry: children now covered +by their original parent's retained paint cannot return through the final held +snapshot merge. An incomplete fallback remains placed and partial. + +First affected batch at 01ccf09d: six passes, two failures. One control incorrectly +asserted that a held RLock already meant a source mutation; it now checks the +actual published mutation scope. The other exposed the real overlay clip defect: +complete explicit acquisition incorrectly returned IntrinsicSubtreeGeometry for +an overlay. The corrected clip and release checks passed (3 / 0.81s); that result +predates the shared captured-parent representation change. Initial tool output is +retained in the original conversation; neither result is overall acceptance. + +The shared source/ancestry batch then passed twelve checks and exposed one real +opaque-source regression: after scrolling, fallback restored children covered +by retained self-paint. The corrected fallback, original overlay retirement and +held-scroll checks passed (3 / 0.85s). The unchanged ancestor expansion plus +explicit descendant refusal passed (1 / 0.46s). + +Ordinary pending-member release passed in the first callback batch. Two queue +control negatives are retained: they incorrectly required layout intent still +to be false when another original admission could already have recorded it. +The final real message-pump control releases on the same pump that acquired the +transaction, queues admission within that callback delivery, and verifies the +sender stays queued until actual layout completes (1 / 0.39s). No held paint +damage hides the pending layout requirement. + +Raw logs and the existing Package AST census are retained beside this file. +The census covers production and current tests with zero parse omissions; +historical evidence scripts retain their original private API shape. + +Parent owns matching history transaction/reader lifetime and saved-history +application verification. Four matching source application paths passed. The +immutable 3c7d5ec0/de83aa39 saved-history run then completed without application +error: median 25.1ms, p95 210.7ms, maximum 369.6ms versus published 32.8/191.3/228.2ms. +The tail regressed; this pair is not published and no overall gain is claimed. +The subsequent diagnostic main-thread sample found 142 samples through ordinary +arrangement and six through explicit acquisition. Those samples neither identify +acquisition as the cause nor supply call counts or CPU durations. +Explicit complete geometry acquisition remains synchronous. +One uncached custom visual can still render a whole widget within a band; no +renderer deadline or readiness exemption is introduced. +Historical evidence scripts remain at their original source/API scope in Git. + +## Requested positions in partial geometry + +Ordinary visible arrangement previously required a complete source for every +retained position path. A matching partial source therefore could not supply a +position it already owned: the compositor bypassed the capture/reuse path and +descended again. The same original acquired path relation now carries the actual +requested positions to each ancestor. `SubtreeGeometry.matches` checks placement +first, then requires those positions in its captured geometry. Complete sources +also own absent answers; partial logical/invisible membership does not establish +a position. Missing positions use the original acquisition path, and the new +partial source can supply them on the next unchanged frame. Full capture still +requires complete scope; held-source, revision, internal scroll, clip and layer +checks remain intact. + +Source projection intersects those requests with its original immutable geometry +membership before routing them through borrowed children. It preserves original +ordinals, captured ancestry and last-assignment ordering, without scanning every +unrelated global request in every child source. The synchronous ancestor relation +still walks each requested target's path; this change does not remove that work +or make partial placed geometry translatable. + +One affected real native App check passed (0.64s): 100 rows, acquired and missing +offscreen targets, changing target membership, scroll/reversal and a child height +write. Every phase compares actual published geometry and rendered strips with +the original uncached compositor. An already covered partial source is reused; +a missing target is acquired before reuse, and that partial source still refuses +complete capture. This is geometry/paint confirmation, not saved-history latency +acceptance. The next affected saved-history run belongs to Parent; no installed +or public change is made here. Raw control output and the original Package +before/after consumer census are retained beside this file. diff --git a/evidence/subtree-mutation-source-20261008/callback-release-controls.txt b/evidence/subtree-mutation-source-20261008/callback-release-controls.txt new file mode 100644 index 0000000000..95c9b0cc89 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/callback-release-controls.txt @@ -0,0 +1,2 @@ +. [100%] +1 passed in 0.39s diff --git a/evidence/subtree-mutation-source-20261008/callback-release-initial.txt b/evidence/subtree-mutation-source-20261008/callback-release-initial.txt new file mode 100644 index 0000000000..c8207609fb --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/callback-release-initial.txt @@ -0,0 +1,230 @@ +F. [100%] +=================================== FAILURES =================================== +___________ test_released_layout_fences_queued_callbacks_before_idle ___________ + +fut = , timeout = 2 + + async def wait_for(fut, timeout): + """Wait for the single Future or coroutine to complete, with timeout. + + Coroutine will be wrapped in Task. + + Returns result of the Future or coroutine. When a timeout occurs, + it cancels the task and raises TimeoutError. To avoid the task + cancellation, wrap it in shield(). + + If the wait is cancelled, the task is also cancelled. + + If the task suppresses the cancellation and returns a value instead, + that value is returned. + + This function is a coroutine. + """ + # The special case for timeout <= 0 is for the following case: + # + # async def test_waitfor(): + # func_started = False + # + # async def func(): + # nonlocal func_started + # func_started = True + # + # try: + # await asyncio.wait_for(func(), 0) + # except asyncio.TimeoutError: + # assert not func_started + # else: + # assert False + # + # asyncio.run(test_waitfor()) + + + if timeout is not None and timeout <= 0: + fut = ensure_future(fut) + + if fut.done(): + return fut.result() + + await _cancel_and_wait(fut) + try: + return fut.result() + except exceptions.CancelledError as exc: + raise TimeoutError from exc + + async with timeouts.timeout(timeout): +> return await fut + ^^^^^^^^^ + +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/tasks.py:488: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def wait(self): + """Block until the internal flag is true. + + If the internal flag is true on entry, return True + immediately. Otherwise, block until another task calls + set() to set the flag to true, then return True. + """ + if self._value: + return True + + fut = self._get_loop().create_future() + self._waiters.append(fut) + try: +> await fut +E asyncio.exceptions.CancelledError + +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/locks.py:213: CancelledError + +The above exception was the direct cause of the following exception: + + async def test_released_layout_fences_queued_callbacks_before_idle(): + app = IndependentApp() + completed = asyncio.Event() + admitted = asyncio.Event() + order: list[str] = [] + async with app.run_test(size=(40, 8)) as pilot: + await pilot.pause() + screen = app.screen + original = app.query_one("#original") + holder = app.query_one(HeldVertical) + + def complete() -> None: + assert asyncio.current_task() is original.task + order.append("completion") + completed.set() + + async with screen.preserve_layout(holder): + # A genuine geometry-only refresh leaves no paint damage to mask + # the pending layout fence. The sender still awaits that request. + original.refresh(repaint=False, layout=True) + original.call_after_refresh(complete) + await pilot.pause() + assert screen._layout_widgets + assert not screen._refresh_pending + assert any(sender is original for _, sender in screen._callbacks) + + def queued_admission() -> None: + assert asyncio.current_task() is screen.task + assert not screen._layout_required, "This admission precedes Idle" + assert screen._layout_widgets and not screen._layout_mutation_roots() + screen._invoke_and_clear_callbacks() + assert screen._layout_required + assert any(sender is original for _, sender in screen._callbacks) + order.append("queued admission") + admitted.set() + + screen.screen_layout_refresh_signal.subscribe( + app, lambda _: order.append("layout") + ) + # Dispatch an ordinary callback message; its original next-callback + # phase runs before the pump's Idle admission, without a direct call + # to Idle or publication from this test task. + screen.call_later(lambda: order.append("message")) + screen.call_next(queued_admission) +> await asyncio.wait_for(admitted.wait(), 2) + +tests/test_batch_paint_admission.py:268: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/tasks.py:487: in wait_for + async with timeouts.timeout(timeout): + ^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = +exc_type = +exc_val = CancelledError(), exc_tb = + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + assert self._state in (_State.ENTERED, _State.EXPIRING) + + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + if self._state is _State.EXPIRING: + self._state = _State.EXPIRED + + if self._task.uncancel() <= self._cancelling and exc_type is not None: + # Since there are no new cancel requests, we're + # handling this. + if issubclass(exc_type, exceptions.CancelledError): +> raise TimeoutError from exc_val +E TimeoutError + +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/timeouts.py:114: TimeoutError + +During handling of the above exception, another exception occurred: + + async def test_released_layout_fences_queued_callbacks_before_idle(): + app = IndependentApp() + completed = asyncio.Event() + admitted = asyncio.Event() + order: list[str] = [] +> async with app.run_test(size=(40, 8)) as pilot: + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/test_batch_paint_admission.py:229: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/contextlib.py:235: in __aexit__ + await self.gen.athrow(value) +src/textual/app.py:2228: in run_test + raise self._exception +src/textual/message_pump.py:833: in _flush_next_callbacks + await invoke(callback.callback) +src/textual/_callback.py:97: in invoke + return await _invoke(callback, *params) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_callback.py:57: in _invoke + result = callback(*params[:parameter_count]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def queued_admission() -> None: + assert asyncio.current_task() is screen.task +> assert not screen._layout_required, "This admission precedes Idle" +E AssertionError: This admission precedes Idle +E assert not True +E + where True = IndependentScreen()._layout_required + +tests/test_batch_paint_admission.py:252: AssertionError +----------------------------- Captured stderr call ----------------------------- +╭───────────────────── Traceback (most recent call last) ──────────────────────╮ +│ /home/ts/wt/textual-frame-publication-20261006/tests/test_batch_paint_admiss │ +│ ion.py:252 in queued_admission │ +│ │ +│ 249 │ │ │ +│ 250 │ │ def queued_admission() -> None: │ +│ 251 │ │ │ assert asyncio.current_task() is screen.task │ +│ ❱ 252 │ │ │ assert not screen._layout_required, "This admission preced │ +│ 253 │ │ │ assert screen._layout_widgets and not screen._layout_mutat │ +│ 254 │ │ │ screen._invoke_and_clear_callbacks() │ +│ 255 │ │ │ assert screen._layout_required │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ @py_assert1 = True │ │ +│ │ @py_assert3 = False │ │ +│ │ @py_assert5 = None │ │ +│ │ @py_assert7 = None │ │ +│ │ @py_format4 = 'This admission precedes Idle\n>assert not True\n{True = │ │ +│ │ IndependentScreen()._layou'+12 │ │ +│ │ admitted = │ │ +│ │ order = ['message'] │ │ +│ │ original = Label(id='original') │ │ +│ │ screen = IndependentScreen() │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +AssertionError: This admission precedes Idle +assert not True + + where True = IndependentScreen()._layout_required +=========================== short test summary info ============================ +FAILED tests/test_batch_paint_admission.py::test_released_layout_fences_queued_callbacks_before_idle +1 failed, 1 passed in 2.71s diff --git a/evidence/subtree-mutation-source-20261008/callback-release-ordering-negative.txt b/evidence/subtree-mutation-source-20261008/callback-release-ordering-negative.txt new file mode 100644 index 0000000000..7093266d84 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/callback-release-ordering-negative.txt @@ -0,0 +1,241 @@ +F [100%] +=================================== FAILURES =================================== +___________ test_released_layout_fences_queued_callbacks_before_idle ___________ + +fut = , timeout = 2 + + async def wait_for(fut, timeout): + """Wait for the single Future or coroutine to complete, with timeout. + + Coroutine will be wrapped in Task. + + Returns result of the Future or coroutine. When a timeout occurs, + it cancels the task and raises TimeoutError. To avoid the task + cancellation, wrap it in shield(). + + If the wait is cancelled, the task is also cancelled. + + If the task suppresses the cancellation and returns a value instead, + that value is returned. + + This function is a coroutine. + """ + # The special case for timeout <= 0 is for the following case: + # + # async def test_waitfor(): + # func_started = False + # + # async def func(): + # nonlocal func_started + # func_started = True + # + # try: + # await asyncio.wait_for(func(), 0) + # except asyncio.TimeoutError: + # assert not func_started + # else: + # assert False + # + # asyncio.run(test_waitfor()) + + + if timeout is not None and timeout <= 0: + fut = ensure_future(fut) + + if fut.done(): + return fut.result() + + await _cancel_and_wait(fut) + try: + return fut.result() + except exceptions.CancelledError as exc: + raise TimeoutError from exc + + async with timeouts.timeout(timeout): +> return await fut + ^^^^^^^^^ + +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/tasks.py:488: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def wait(self): + """Block until the internal flag is true. + + If the internal flag is true on entry, return True + immediately. Otherwise, block until another task calls + set() to set the flag to true, then return True. + """ + if self._value: + return True + + fut = self._get_loop().create_future() + self._waiters.append(fut) + try: +> await fut +E asyncio.exceptions.CancelledError + +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/locks.py:213: CancelledError + +The above exception was the direct cause of the following exception: + + async def test_released_layout_fences_queued_callbacks_before_idle(): + app = IndependentApp() + completed = asyncio.Event() + admitted = asyncio.Event() + order: list[str] = [] + async with app.run_test(size=(40, 8)) as pilot: + await pilot.pause() + screen = app.screen + original = app.query_one("#original") + holder = app.query_one(HeldVertical) + transaction = screen.preserve_layout(holder) + entered = asyncio.Event() + + async def enter() -> None: + await transaction.__aenter__() + entered.set() + + def complete() -> None: + assert asyncio.current_task() is original.task + order.append("completion") + completed.set() + + screen.call_later(enter) + await asyncio.wait_for(entered.wait(), 2) + # A genuine geometry-only refresh leaves no paint damage to mask + # the pending layout fence. The sender still awaits that request. + original.refresh(repaint=False, layout=True) + original.call_after_refresh(complete) + await pilot.pause() + assert screen._layout_widgets + assert not screen._refresh_pending + assert any(sender is original for _, sender in screen._callbacks) + + def queued_admission() -> None: + assert asyncio.current_task() is screen.task + assert not screen._layout_required, "This admission precedes Idle" + assert screen._layout_widgets and not screen._layout_mutation_roots() + screen._invoke_and_clear_callbacks() + assert screen._layout_required + assert any(sender is original for _, sender in screen._callbacks) + order.append("queued admission") + admitted.set() + + screen.screen_layout_refresh_signal.subscribe( + app, lambda _: order.append("layout") + ) + + async def release() -> None: + # Enter and release on the same native pump. Its ordinary callback + # message owns this release and queues admission before Idle. + await transaction.__aexit__(None, None, None) + order.append("message") + screen.call_next(queued_admission) + + screen.call_later(release) +> await asyncio.wait_for(admitted.wait(), 2) + +tests/test_batch_paint_admission.py:279: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/tasks.py:487: in wait_for + async with timeouts.timeout(timeout): + ^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = +exc_type = +exc_val = CancelledError(), exc_tb = + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + assert self._state in (_State.ENTERED, _State.EXPIRING) + + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + if self._state is _State.EXPIRING: + self._state = _State.EXPIRED + + if self._task.uncancel() <= self._cancelling and exc_type is not None: + # Since there are no new cancel requests, we're + # handling this. + if issubclass(exc_type, exceptions.CancelledError): +> raise TimeoutError from exc_val +E TimeoutError + +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/asyncio/timeouts.py:114: TimeoutError + +During handling of the above exception, another exception occurred: + + async def test_released_layout_fences_queued_callbacks_before_idle(): + app = IndependentApp() + completed = asyncio.Event() + admitted = asyncio.Event() + order: list[str] = [] +> async with app.run_test(size=(40, 8)) as pilot: + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/test_batch_paint_admission.py:229: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../../.local/share/uv/python/cpython-3.14.2-linux-x86_64-gnu/lib/python3.14/contextlib.py:235: in __aexit__ + await self.gen.athrow(value) +src/textual/app.py:2228: in run_test + raise self._exception +src/textual/message_pump.py:833: in _flush_next_callbacks + await invoke(callback.callback) +src/textual/_callback.py:97: in invoke + return await _invoke(callback, *params) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +src/textual/_callback.py:57: in _invoke + result = callback(*params[:parameter_count]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def queued_admission() -> None: + assert asyncio.current_task() is screen.task +> assert not screen._layout_required, "This admission precedes Idle" +E AssertionError: This admission precedes Idle +E assert not True +E + where True = IndependentScreen()._layout_required + +tests/test_batch_paint_admission.py:259: AssertionError +----------------------------- Captured stderr call ----------------------------- +╭───────────────────── Traceback (most recent call last) ──────────────────────╮ +│ /home/ts/wt/textual-frame-publication-20261006/tests/test_batch_paint_admiss │ +│ ion.py:259 in queued_admission │ +│ │ +│ 256 │ │ │ +│ 257 │ │ def queued_admission() -> None: │ +│ 258 │ │ │ assert asyncio.current_task() is screen.task │ +│ ❱ 259 │ │ │ assert not screen._layout_required, "This admission preced │ +│ 260 │ │ │ assert screen._layout_widgets and not screen._layout_mutat │ +│ 261 │ │ │ screen._invoke_and_clear_callbacks() │ +│ 262 │ │ │ assert screen._layout_required │ +│ │ +│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ +│ │ @py_assert1 = True │ │ +│ │ @py_assert3 = False │ │ +│ │ @py_assert5 = None │ │ +│ │ @py_assert7 = None │ │ +│ │ @py_format4 = 'This admission precedes Idle\n>assert not True\n{True = │ │ +│ │ IndependentScreen()._layou'+12 │ │ +│ │ admitted = │ │ +│ │ order = ['message'] │ │ +│ │ original = Label(id='original') │ │ +│ │ screen = IndependentScreen() │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +AssertionError: This admission precedes Idle +assert not True + + where True = IndependentScreen()._layout_required +=========================== short test summary info ============================ +FAILED tests/test_batch_paint_admission.py::test_released_layout_fences_queued_callbacks_before_idle +1 failed in 2.51s diff --git a/evidence/subtree-mutation-source-20261008/changed-controls.txt b/evidence/subtree-mutation-source-20261008/changed-controls.txt new file mode 100644 index 0000000000..70851caaba --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/changed-controls.txt @@ -0,0 +1,2 @@ +... [100%] +3 passed in 0.81s diff --git a/evidence/subtree-mutation-source-20261008/fallback-controls.txt b/evidence/subtree-mutation-source-20261008/fallback-controls.txt new file mode 100644 index 0000000000..fd2d3e58e6 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/fallback-controls.txt @@ -0,0 +1,2 @@ +... [100%] +3 passed in 0.85s diff --git a/evidence/subtree-mutation-source-20261008/final-controls.txt b/evidence/subtree-mutation-source-20261008/final-controls.txt new file mode 100644 index 0000000000..3e701bb028 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/final-controls.txt @@ -0,0 +1,69 @@ +......F...... [100%] +=================================== FAILURES =================================== +____ test_held_original_ancestry_honors_a_nested_self_painting_owner[True] _____ + +screen_overlay = True + + @pytest.mark.parametrize("screen_overlay", [False, True]) + async def test_held_original_ancestry_honors_a_nested_self_painting_owner(screen_overlay): + from textual.content import Content + + class HeldGroup(VerticalGroup): + CACHE_SUBTREE_GEOMETRY = True + + def arrange(self, size, optimal=False): + assert self not in self.screen._layout_mutation_roots(), "Read changing child topology" + return super().arrange(size, optimal=optimal) + + class ContentBody(Static): + # This application declares a native child view for its initial text + # and a self-painted view when it acquires its actual Content resource. + @property + def is_container(self): + return not isinstance(self.content, Content) + + holder = HeldGroup(id="holder") + + class ChangingChild(Static): + def render_lines(self, crop): + assert not holder.lock.is_locked, "Read changing descendant paint" + return super().render_lines(crop) + + body = ContentBody("INITIAL_NATIVE_PARENT", id="content-body") + old = ChangingChild("OLD_NATIVE_CHILD") + if screen_overlay: + old.styles.overlay = "screen" + + class ContentScreen(TargetedScreen): + CSS = "#content-body { height: 18; } Static { height: 2; }" + + def compose(self): + with VerticalScroll(id="history"): + yield holder + + app = App() + async with app.run_test(size=(40, 10)) as pilot: + screen = ContentScreen() + screen.targets = (holder,) + await app.push_screen(screen) + await holder.mount(body) + await body.mount(old) + await pilot.pause() + compositor = screen._compositor + resource = compositor.acquire_subtree_geometry(holder).source + assert resource.contains(old) + assert resource.captured_parent(old) is body + async with app.screen.preserve_layout(holder): + body.update(Content("\n".join(["ACQUIRED_ROOT_PAINT"] * 18))) + await body.mount(ChangingChild("UNCOMMITTED_DESCENDANT")) + screen.query_one("#history").scroll_to(y=4, animate=False, immediate=True) + await pilot.pause() + assert compositor._subtree_geometry[holder] is resource +> assert old not in compositor._published_map +E AssertionError: assert ChangingChild() not in {ChangingChild(): MapGeometry(region=Region(x=0, y=0, width=38, height=2), order=((1, 0, 0),), clip=Region(x=0, y=0, w...10), virtual_region=Region(x=38, y=0, width=2, height=10), dock_gutter=Spacing(top=0, right=0, bottom=0, left=0)), ...} +E + where {ChangingChild(): MapGeometry(region=Region(x=0, y=0, width=38, height=2), order=((1, 0, 0),), clip=Region(x=0, y=0, w...10), virtual_region=Region(x=38, y=0, width=2, height=10), dock_gutter=Spacing(top=0, right=0, bottom=0, left=0)), ...} = ._published_map + +tests/test_viewport_geometry_targets.py:473: AssertionError +=========================== short test summary info ============================ +FAILED tests/test_viewport_geometry_targets.py::test_held_original_ancestry_honors_a_nested_self_painting_owner[True] +1 failed, 12 passed in 3.38s diff --git a/evidence/subtree-mutation-source-20261008/nested-scope-control.txt b/evidence/subtree-mutation-source-20261008/nested-scope-control.txt new file mode 100644 index 0000000000..babe1cf510 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/nested-scope-control.txt @@ -0,0 +1,2 @@ +. [100%] +1 passed in 0.46s diff --git a/evidence/subtree-mutation-source-20261008/owner-consumers.json b/evidence/subtree-mutation-source-20261008/owner-consumers.json new file mode 100644 index 0000000000..5f236d3945 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/owner-consumers.json @@ -0,0 +1,3953 @@ +{ + "before_revision": "57523e1b", + "after_source_tree": "f32f1472e901293eaab0516d09ed2e578a7f6800", + "scope": "Original Package parsed Git source trees; dynamic external subclasses and historical evidence scripts are not claimed migrated.", + "revisions": { + "57523e1b": { + "src": { + "modules": 250, + "omissions": [], + "classes": [ + { + "path": "src/textual/_compositor.py", + "line": 69, + "class": "SubtreeGeometryKey", + "bases": [ + "NamedTuple" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 113, + "class": "SceneClip", + "bases": [ + "ABC" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 137, + "class": "RootSceneClip", + "bases": [ + "SceneClip" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 151, + "class": "NestedSceneClip", + "bases": [ + "SceneClip" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 169, + "class": "SubtreeMapGeometry", + "bases": [ + "NamedTuple" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 202, + "class": "SubtreeGeometryPlacement", + "bases": [] + }, + { + "path": "src/textual/_compositor.py", + "line": 226, + "class": "SubtreeGeometry", + "bases": [ + "ABC", + "Generic[GeometryEntry]" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 415, + "class": "PlacedSubtreeGeometry", + "bases": [ + "SubtreeGeometry[MapGeometry]" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 434, + "class": "IntrinsicSubtreeGeometry", + "bases": [ + "SubtreeGeometry[SubtreeMapGeometry]" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 734, + "class": "Compositor", + "bases": [] + }, + { + "path": "src/textual/screen.py", + "line": 144, + "class": "Screen", + "bases": [ + "Generic[ScreenResultType]", + "Widget" + ] + } + ], + "imports": [ + [ + "src/textual/_compositor.py", + 55, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/_context.py", + 10, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/app.py", + 120, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/await_remove.py", + 19, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/command.py", + 49, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/demo/game.py", + 27, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/demo/page.py", + 11, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/dom.py", + 70, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/dom.py", + 872, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/message_pump.py", + 56, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/renderables/background_screen.py", + 12, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/screen.py", + 39, + "textual._compositor", + "Compositor", + null + ], + [ + "src/textual/selection.py", + 231, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/selection.py", + 328, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widget.py", + 735, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "src/textual/widget.py", + 735, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "src/textual/widget.py", + 1755, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widget.py", + 804, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_footer.py", + 19, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_input.py", + 16, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_key_panel.py", + 18, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_text_area.py", + 40, + "textual.screen", + "Screen", + null + ] + ], + "sites": [ + [ + "src/textual/_compositor.py", + 236, + "declaration", + "complete_arrangement" + ], + [ + "src/textual/_compositor.py", + 240, + "declaration", + "capture" + ], + [ + "src/textual/_compositor.py", + 320, + "declaration", + "matches" + ], + [ + "src/textual/_compositor.py", + 391, + "declaration", + "_paint_entries" + ], + [ + "src/textual/_compositor.py", + 418, + "declaration", + "matches" + ], + [ + "src/textual/_compositor.py", + 438, + "declaration", + "complete_arrangement" + ], + [ + "src/textual/_compositor.py", + 442, + "declaration", + "capture" + ], + [ + "src/textual/_compositor.py", + 471, + "declaration", + "matches" + ], + [ + "src/textual/_compositor.py", + 500, + "declaration", + "_paint_entries" + ], + [ + "src/textual/_compositor.py", + 1046, + "declaration", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 90, + "Load", + "_geometry_revision" + ], + [ + "src/textual/_compositor.py", + 371, + "Load", + "_paint_entries" + ], + [ + "src/textual/_compositor.py", + 468, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 907, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 957, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 2128, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 444, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1007, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 1063, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/_compositor.py", + 1400, + "Load", + "complete_arrangement" + ], + [ + "src/textual/_compositor.py", + 455, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 458, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1407, + "Load", + "matches" + ], + [ + "src/textual/_compositor.py", + 1435, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1563, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/_compositor.py", + 296, + "Load", + "source_held" + ], + [ + "src/textual/_compositor.py", + 1345, + "Load", + "matches" + ], + [ + "src/textual/_compositor.py", + 378, + "Load", + "source_held" + ], + [ + "src/textual/_compositor.py", + 1381, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1546, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/app.py", + 3811, + "declaration", + "_request_layout" + ], + [ + "src/textual/app.py", + 4410, + "declaration", + "_on_layout" + ], + [ + "src/textual/app.py", + 5052, + "declaration", + "_on_idle" + ], + [ + "src/textual/app.py", + 3813, + "Load", + "_request_layout" + ], + [ + "src/textual/app.py", + 3850, + "Load", + "_refresh_layout" + ], + [ + "src/textual/css/styles.py", + 1067, + "Load", + "_request_layout" + ], + [ + "src/textual/css/styles.py", + 1075, + "Load", + "_request_layout" + ], + [ + "src/textual/dom.py", + 705, + "declaration", + "_request_layout" + ], + [ + "src/textual/dom.py", + 150, + "Store", + "_geometry_revision" + ], + [ + "src/textual/dom.py", + 154, + "Store", + "_geometry_revision" + ], + [ + "src/textual/dom.py", + 1419, + "Load", + "_request_layout" + ], + [ + "src/textual/layouts/stream.py", + 58, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1233, + "declaration", + "_on_idle" + ], + [ + "src/textual/screen.py", + 1258, + "declaration", + "_layout_mutation_roots" + ], + [ + "src/textual/screen.py", + 1390, + "declaration", + "_invoke_and_clear_callbacks" + ], + [ + "src/textual/screen.py", + 1469, + "declaration", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1482, + "declaration", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1491, + "declaration", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1592, + "declaration", + "_on_layout" + ], + [ + "src/textual/screen.py", + 326, + "Store", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1252, + "Load", + "_invoke_and_clear_callbacks" + ], + [ + "src/textual/screen.py", + 1471, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1473, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/screen.py", + 1370, + "Load", + "_invoke_and_clear_callbacks" + ], + [ + "src/textual/screen.py", + 1450, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1541, + "Store", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1603, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1605, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1685, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1766, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1356, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1504, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1541, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1604, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1616, + "Load", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1651, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1704, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1358, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1508, + "Load", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1617, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1653, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1475, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1488, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1606, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1406, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/widget.py", + 4619, + "declaration", + "_refresh_scroll" + ], + [ + "src/textual/widget.py", + 4874, + "declaration", + "_on_idle" + ], + [ + "src/textual/widget.py", + 5037, + "declaration", + "_request_layout" + ], + [ + "src/textual/widget.py", + 2192, + "Load", + "_refresh_scroll" + ], + [ + "src/textual/widget.py", + 2200, + "Load", + "_refresh_scroll" + ], + [ + "src/textual/widget.py", + 4657, + "Load", + "_request_layout" + ], + [ + "src/textual/widgets/_data_table.py", + 1870, + "declaration", + "_on_idle" + ], + [ + "src/textual/widgets/_tree.py", + 1248, + "declaration", + "_on_idle" + ] + ] + }, + "tests": { + "modules": 481, + "omissions": [], + "classes": [ + { + "path": "tests/test_batch_paint_admission.py", + "line": 149, + "class": "IndependentScreen", + "bases": [ + "ObservedScreen" + ] + } + ], + "imports": [ + [ + "tests/command_palette/test_command_source_environment.py", + 5, + "textual.screen", + "Screen", + null + ], + [ + "tests/command_palette/test_declare_sources.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/css/test_css_reloading.py", + 5, + "textual.screen", + "Screen", + null + ], + [ + "tests/css/test_screen_css.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/layouts/test_common_layout_features.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/notifications/test_all_levels_notifications.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/shutdown_mode_installed_pilot.py", + 4, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/auto-table.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/bindings_screen_overrides_show.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/hot_reloading_app_with_screen_css.py", + 5, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/layer_order_independence.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/notification_through_modes.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/notification_through_screens.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/recompose_on_mount.py", + 4, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/screen_switch.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/text_area_alternate_screen.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/test_snapshots.py", + 33, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_app.py", + 11, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_batch_paint_admission.py", + 10, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_binding.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_binding_inheritance.py", + 18, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_closed_presentation_lifetime.py", + 5, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_closed_presentation_lifetime.py", + 9, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_command.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_complete_placed_geometry.py", + 1, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_complete_placed_geometry.py", + 1, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_compositor_frame_damage.py", + 61, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_compositor_regions_to_spans.py", + 1, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_focus.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_freeze.py", + 4, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_geometry_reader_acquisition.py", + 4, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_geometry_reader_acquisition.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_header.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_links.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_native_scene_size_publication.py", + 12, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_pilot.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_primary_selection_gesture.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_refresh_callback_owner.py", + 8, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_removed_scene_ownership.py", + 6, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_reparent.py", + 9, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screen_modes.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screen_resolution_lifetime.py", + 6, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screens.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screens.py", + 548, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 5, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 57, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 57, + "textual._compositor", + "SubtreeGeometryPlacement", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 59, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_unmount.py", + 6, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 151, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 151, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 151, + "textual._compositor", + "RootSceneClip", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 151, + "textual._compositor", + "SubtreeGeometryKey", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 151, + "textual._compositor", + "SubtreeGeometryPlacement", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 151, + "textual._compositor", + "SubtreeMapGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 261, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 261, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 370, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 488, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 488, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_viewport_layout.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_widget.py", + 329, + "textual.screen", + "Screen", + null + ] + ], + "sites": [ + [ + "tests/css/test_programmatic_style_changes.py", + 55, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 66, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 24, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 29, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 39, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 58, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 69, + "Load", + "_geometry_revision" + ], + [ + "tests/participant_content_update_pilot.py", + 46, + "Load", + "_geometry_revision" + ], + [ + "tests/participant_content_update_pilot.py", + 57, + "Load", + "_geometry_revision" + ], + [ + "tests/test_batch_paint_admission.py", + 154, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_batch_paint_admission.py", + 158, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_batch_paint_admission.py", + 207, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 54, + "Load", + "_refresh_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 196, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 244, + "Load", + "_on_idle" + ], + [ + "tests/test_batch_paint_admission.py", + 331, + "Load", + "_refresh_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 333, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 257, + "Load", + "_on_idle" + ], + [ + "tests/test_batch_paint_admission.py", + 318, + "Load", + "_refresh_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 321, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 330, + "Load", + "_on_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 314, + "Load", + "_layout_widgets" + ], + [ + "tests/test_closed_presentation_lifetime.py", + 57, + "Load", + "_layout_widgets" + ], + [ + "tests/test_compositor.py", + 251, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor.py", + 106, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor.py", + 162, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor.py", + 194, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor_frame_damage.py", + 230, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor_frame_damage.py", + 318, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor_frame_damage.py", + 335, + "Load", + "_arrange_root" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 18, + "declaration", + "_arrange_root" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 20, + "Load", + "_arrange_root" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 72, + "Load", + "_refresh_layout" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 120, + "Load", + "_refresh_layout" + ], + [ + "tests/test_height_independent_arrangement.py", + 32, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 35, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 48, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 51, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 58, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 62, + "Load", + "_geometry_revision" + ], + [ + "tests/test_native_scene_size_publication.py", + 26, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_native_scene_size_publication.py", + 32, + "declaration", + "_refresh_layout" + ], + [ + "tests/test_native_scene_size_publication.py", + 57, + "Load", + "_layout_widgets" + ], + [ + "tests/test_native_scene_size_publication.py", + 65, + "Load", + "_layout_widgets" + ], + [ + "tests/test_native_scene_size_publication.py", + 30, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_native_scene_size_publication.py", + 36, + "Load", + "_refresh_layout" + ], + [ + "tests/test_native_scene_size_publication.py", + 34, + "Load", + "_has_actionable_layout_requests" + ], + [ + "tests/test_native_scene_size_publication.py", + 34, + "Load", + "_held_layout_requests" + ], + [ + "tests/test_static.py", + 103, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 38, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 58, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 63, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 98, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 105, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 122, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 45, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 56, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 60, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 65, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 100, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 124, + "Load", + "_geometry_revision" + ], + [ + "tests/test_subtree_geometry_cache.py", + 69, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_subtree_geometry_cache.py", + 104, + "Load", + "source_held" + ], + [ + "tests/test_subtree_geometry_cache.py", + 26, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 40, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 49, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 99, + "Load", + "capture" + ], + [ + "tests/test_subtree_geometry_cache.py", + 107, + "Load", + "matches" + ], + [ + "tests/test_subtree_geometry_cache.py", + 111, + "Load", + "capture" + ], + [ + "tests/test_subtree_geometry_cache.py", + 117, + "Load", + "capture" + ], + [ + "tests/test_subtree_geometry_cache.py", + 24, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 42, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 47, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 73, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_subtree_geometry_cache.py", + 106, + "Load", + "matches" + ], + [ + "tests/test_subtree_geometry_cache.py", + 124, + "Load", + "matches" + ], + [ + "tests/test_subtree_geometry_cache.py", + 155, + "Load", + "_arrange_root" + ], + [ + "tests/test_viewport_geometry_targets.py", + 281, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_viewport_geometry_targets.py", + 390, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_viewport_geometry_targets.py", + 455, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_viewport_geometry_targets.py", + 693, + "Load", + "_arrange_root" + ], + [ + "tests/test_viewport_geometry_targets.py", + 597, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 647, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 648, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 661, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 686, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 723, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 302, + "Load", + "matches" + ], + [ + "tests/test_viewport_geometry_targets.py", + 306, + "Load", + "matches" + ], + [ + "tests/test_viewport_geometry_targets.py", + 520, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 524, + "Load", + "_arrange_root" + ], + [ + "tests/test_viewport_geometry_targets.py", + 566, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 617, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 622, + "Load", + "_refresh_layout" + ], + [ + "tests/utilities/render.py", + 20, + "Load", + "capture" + ] + ] + } + }, + "f32f1472e901293eaab0516d09ed2e578a7f6800": { + "src": { + "modules": 250, + "omissions": [], + "classes": [ + { + "path": "src/textual/_compositor.py", + "line": 67, + "class": "SubtreeGeometryKey", + "bases": [ + "NamedTuple" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 111, + "class": "SceneClip", + "bases": [ + "ABC" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 135, + "class": "RootSceneClip", + "bases": [ + "SceneClip" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 149, + "class": "NestedSceneClip", + "bases": [ + "SceneClip" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 167, + "class": "SubtreeMapGeometry", + "bases": [ + "NamedTuple" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 197, + "class": "SubtreeGeometryPlacement", + "bases": [] + }, + { + "path": "src/textual/_compositor.py", + "line": 221, + "class": "SubtreeGeometry", + "bases": [ + "ABC" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 444, + "class": "PlacedSubtreeGeometry", + "bases": [ + "SubtreeGeometry" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 455, + "class": "IntrinsicSubtreeGeometry", + "bases": [ + "SubtreeGeometry" + ] + }, + { + "path": "src/textual/_compositor.py", + "line": 715, + "class": "Compositor", + "bases": [] + }, + { + "path": "src/textual/screen.py", + "line": 145, + "class": "Screen", + "bases": [ + "Generic[ScreenResultType]", + "Widget" + ] + } + ], + "imports": [ + [ + "src/textual/_compositor.py", + 53, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/_context.py", + 10, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/app.py", + 120, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/await_remove.py", + 19, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/command.py", + 49, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/demo/game.py", + 27, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/demo/page.py", + 11, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/dom.py", + 70, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/dom.py", + 872, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/message_pump.py", + 56, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/renderables/background_screen.py", + 12, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/screen.py", + 40, + "textual._compositor", + "Compositor", + null + ], + [ + "src/textual/screen.py", + 40, + "textual._compositor", + "SubtreeGeometryPlacement", + null + ], + [ + "src/textual/selection.py", + 231, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/selection.py", + 328, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widget.py", + 735, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "src/textual/widget.py", + 735, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "src/textual/widget.py", + 1755, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widget.py", + 804, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_footer.py", + 19, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_input.py", + 16, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_key_panel.py", + 18, + "textual.screen", + "Screen", + null + ], + [ + "src/textual/widgets/_text_area.py", + 40, + "textual.screen", + "Screen", + null + ] + ], + "sites": [ + [ + "src/textual/_compositor.py", + 231, + "declaration", + "capture" + ], + [ + "src/textual/_compositor.py", + 319, + "declaration", + "matches" + ], + [ + "src/textual/_compositor.py", + 335, + "declaration", + "_matches_placement" + ], + [ + "src/textual/_compositor.py", + 408, + "declaration", + "_paint_entries" + ], + [ + "src/textual/_compositor.py", + 447, + "declaration", + "_matches_placement" + ], + [ + "src/textual/_compositor.py", + 459, + "declaration", + "capture" + ], + [ + "src/textual/_compositor.py", + 483, + "declaration", + "_matches_placement" + ], + [ + "src/textual/_compositor.py", + 1027, + "declaration", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 1707, + "declaration", + "acquire_subtree_geometry" + ], + [ + "src/textual/_compositor.py", + 88, + "Load", + "_geometry_revision" + ], + [ + "src/textual/_compositor.py", + 332, + "Load", + "_matches_placement" + ], + [ + "src/textual/_compositor.py", + 384, + "Load", + "_paint_entries" + ], + [ + "src/textual/_compositor.py", + 480, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 888, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 938, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 1046, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/_compositor.py", + 1727, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 2166, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 461, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 988, + "Load", + "_arrange_root" + ], + [ + "src/textual/_compositor.py", + 471, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 474, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1415, + "Load", + "matches" + ], + [ + "src/textual/_compositor.py", + 1440, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1574, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/_compositor.py", + 296, + "Load", + "source_held" + ], + [ + "src/textual/_compositor.py", + 1336, + "Load", + "matches" + ], + [ + "src/textual/_compositor.py", + 395, + "Load", + "source_held" + ], + [ + "src/textual/_compositor.py", + 1386, + "Load", + "capture" + ], + [ + "src/textual/_compositor.py", + 1557, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/_compositor.py", + 1371, + "Load", + "_paint_entries" + ], + [ + "src/textual/_compositor.py", + 1724, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/app.py", + 3811, + "declaration", + "_request_layout" + ], + [ + "src/textual/app.py", + 4410, + "declaration", + "_on_layout" + ], + [ + "src/textual/app.py", + 5052, + "declaration", + "_on_idle" + ], + [ + "src/textual/app.py", + 3813, + "Load", + "_request_layout" + ], + [ + "src/textual/app.py", + 3850, + "Load", + "_refresh_layout" + ], + [ + "src/textual/css/styles.py", + 1067, + "Load", + "_request_layout" + ], + [ + "src/textual/css/styles.py", + 1075, + "Load", + "_request_layout" + ], + [ + "src/textual/dom.py", + 705, + "declaration", + "_request_layout" + ], + [ + "src/textual/dom.py", + 150, + "Store", + "_geometry_revision" + ], + [ + "src/textual/dom.py", + 154, + "Store", + "_geometry_revision" + ], + [ + "src/textual/dom.py", + 1419, + "Load", + "_request_layout" + ], + [ + "src/textual/layouts/stream.py", + 58, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1234, + "declaration", + "_on_idle" + ], + [ + "src/textual/screen.py", + 1266, + "declaration", + "_layout_mutation_roots" + ], + [ + "src/textual/screen.py", + 1402, + "declaration", + "_invoke_and_clear_callbacks" + ], + [ + "src/textual/screen.py", + 1488, + "declaration", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1504, + "declaration", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1513, + "declaration", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1614, + "declaration", + "_on_layout" + ], + [ + "src/textual/screen.py", + 327, + "Store", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1260, + "Load", + "_invoke_and_clear_callbacks" + ], + [ + "src/textual/screen.py", + 1492, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1634, + "Load", + "_request_layout" + ], + [ + "src/textual/screen.py", + 1243, + "Load", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1246, + "Load", + "_request_layout" + ], + [ + "src/textual/screen.py", + 1382, + "Load", + "_invoke_and_clear_callbacks" + ], + [ + "src/textual/screen.py", + 1469, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1495, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/screen.py", + 1563, + "Store", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1624, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1626, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1635, + "Load", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1702, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1783, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1244, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1368, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1417, + "Load", + "_layout_mutation_roots" + ], + [ + "src/textual/screen.py", + 1421, + "Load", + "_request_layout" + ], + [ + "src/textual/screen.py", + 1526, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1563, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1625, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1635, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1668, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1721, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1370, + "Load", + "_refresh_layout" + ], + [ + "src/textual/screen.py", + 1422, + "Load", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1530, + "Load", + "_has_actionable_layout_requests" + ], + [ + "src/textual/screen.py", + 1670, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1423, + "Load", + "_held_layout_requests" + ], + [ + "src/textual/screen.py", + 1497, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1510, + "Load", + "_layout_widgets" + ], + [ + "src/textual/screen.py", + 1627, + "Load", + "_layout_widgets" + ], + [ + "src/textual/widget.py", + 4619, + "declaration", + "_refresh_scroll" + ], + [ + "src/textual/widget.py", + 4874, + "declaration", + "_on_idle" + ], + [ + "src/textual/widget.py", + 5037, + "declaration", + "_request_layout" + ], + [ + "src/textual/widget.py", + 2192, + "Load", + "_refresh_scroll" + ], + [ + "src/textual/widget.py", + 2200, + "Load", + "_refresh_scroll" + ], + [ + "src/textual/widget.py", + 4657, + "Load", + "_request_layout" + ], + [ + "src/textual/widgets/_data_table.py", + 1870, + "declaration", + "_on_idle" + ], + [ + "src/textual/widgets/_tree.py", + 1248, + "declaration", + "_on_idle" + ] + ] + }, + "tests": { + "modules": 481, + "omissions": [], + "classes": [ + { + "path": "tests/test_batch_paint_admission.py", + "line": 149, + "class": "IndependentScreen", + "bases": [ + "ObservedScreen" + ] + } + ], + "imports": [ + [ + "tests/command_palette/test_command_source_environment.py", + 5, + "textual.screen", + "Screen", + null + ], + [ + "tests/command_palette/test_declare_sources.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/css/test_css_reloading.py", + 5, + "textual.screen", + "Screen", + null + ], + [ + "tests/css/test_screen_css.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/layouts/test_common_layout_features.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/notifications/test_all_levels_notifications.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/shutdown_mode_installed_pilot.py", + 4, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/auto-table.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/bindings_screen_overrides_show.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/hot_reloading_app_with_screen_css.py", + 5, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/layer_order_independence.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/notification_through_modes.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/notification_through_screens.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/recompose_on_mount.py", + 4, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/screen_switch.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/snapshot_apps/text_area_alternate_screen.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/snapshot_tests/test_snapshots.py", + 33, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_app.py", + 11, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_batch_paint_admission.py", + 10, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_binding.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_binding_inheritance.py", + 18, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_closed_presentation_lifetime.py", + 5, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_closed_presentation_lifetime.py", + 9, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_command.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_complete_placed_geometry.py", + 1, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_complete_placed_geometry.py", + 1, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_compositor_frame_damage.py", + 61, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_compositor_regions_to_spans.py", + 1, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_focus.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_freeze.py", + 4, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_geometry_reader_acquisition.py", + 4, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_geometry_reader_acquisition.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_header.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_links.py", + 2, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_native_scene_size_publication.py", + 4, + "tests.test_batch_paint_admission", + "IndependentScreen", + null + ], + [ + "tests/test_pilot.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_primary_selection_gesture.py", + 7, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_refresh_callback_owner.py", + 8, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_removed_scene_ownership.py", + 6, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_reparent.py", + 9, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screen_modes.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screen_resolution_lifetime.py", + 6, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screens.py", + 13, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_screens.py", + 548, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 6, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 7, + "tests.test_batch_paint_admission", + "IndependentScreen", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 59, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 59, + "textual._compositor", + "SubtreeGeometryPlacement", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 186, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_subtree_geometry_cache.py", + 186, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "tests/test_unmount.py", + 6, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 7, + "tests.test_batch_paint_admission", + "IndependentScreen", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 10, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 154, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 154, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 154, + "textual._compositor", + "RootSceneClip", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 154, + "textual._compositor", + "SubtreeGeometryKey", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 154, + "textual._compositor", + "SubtreeGeometryPlacement", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 154, + "textual._compositor", + "SubtreeMapGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 264, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 264, + "textual._compositor", + "IntrinsicSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 370, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 485, + "textual._compositor", + "Compositor", + null + ], + [ + "tests/test_viewport_geometry_targets.py", + 485, + "textual._compositor", + "PlacedSubtreeGeometry", + null + ], + [ + "tests/test_viewport_layout.py", + 3, + "textual.screen", + "Screen", + null + ], + [ + "tests/test_widget.py", + 329, + "textual.screen", + "Screen", + null + ] + ], + "sites": [ + [ + "tests/css/test_programmatic_style_changes.py", + 55, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 66, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 24, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 29, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 39, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 58, + "Load", + "_geometry_revision" + ], + [ + "tests/css/test_programmatic_style_changes.py", + 69, + "Load", + "_geometry_revision" + ], + [ + "tests/participant_content_update_pilot.py", + 46, + "Load", + "_geometry_revision" + ], + [ + "tests/participant_content_update_pilot.py", + 57, + "Load", + "_geometry_revision" + ], + [ + "tests/test_batch_paint_admission.py", + 167, + "declaration", + "_layout_mutation_roots" + ], + [ + "tests/test_batch_paint_admission.py", + 253, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 220, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 54, + "Load", + "_refresh_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 141, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_batch_paint_admission.py", + 159, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_batch_paint_admission.py", + 171, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_batch_paint_admission.py", + 209, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 259, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 260, + "Load", + "_invoke_and_clear_callbacks" + ], + [ + "tests/test_batch_paint_admission.py", + 282, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 318, + "Load", + "_on_idle" + ], + [ + "tests/test_batch_paint_admission.py", + 405, + "Load", + "_refresh_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 407, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 331, + "Load", + "_on_idle" + ], + [ + "tests/test_batch_paint_admission.py", + 392, + "Load", + "_refresh_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 395, + "Load", + "_layout_widgets" + ], + [ + "tests/test_batch_paint_admission.py", + 404, + "Load", + "_on_layout" + ], + [ + "tests/test_batch_paint_admission.py", + 259, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_batch_paint_admission.py", + 388, + "Load", + "_layout_widgets" + ], + [ + "tests/test_closed_presentation_lifetime.py", + 57, + "Load", + "_layout_widgets" + ], + [ + "tests/test_compositor.py", + 251, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor.py", + 106, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor.py", + 162, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor.py", + 194, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor_frame_damage.py", + 230, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor_frame_damage.py", + 318, + "Load", + "_arrange_root" + ], + [ + "tests/test_compositor_frame_damage.py", + 335, + "Load", + "_arrange_root" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 18, + "declaration", + "_arrange_root" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 20, + "Load", + "_arrange_root" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 72, + "Load", + "_refresh_layout" + ], + [ + "tests/test_geometry_reader_acquisition.py", + 120, + "Load", + "_refresh_layout" + ], + [ + "tests/test_height_independent_arrangement.py", + 32, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 35, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 48, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 51, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 58, + "Load", + "_geometry_revision" + ], + [ + "tests/test_height_independent_arrangement.py", + 62, + "Load", + "_geometry_revision" + ], + [ + "tests/test_native_scene_size_publication.py", + 25, + "declaration", + "_refresh_layout" + ], + [ + "tests/test_native_scene_size_publication.py", + 50, + "Load", + "_layout_widgets" + ], + [ + "tests/test_native_scene_size_publication.py", + 59, + "Load", + "_layout_widgets" + ], + [ + "tests/test_native_scene_size_publication.py", + 29, + "Load", + "_refresh_layout" + ], + [ + "tests/test_native_scene_size_publication.py", + 42, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_native_scene_size_publication.py", + 27, + "Load", + "_has_actionable_layout_requests" + ], + [ + "tests/test_native_scene_size_publication.py", + 27, + "Load", + "_held_layout_requests" + ], + [ + "tests/test_static.py", + 103, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 38, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 58, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 63, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 98, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 105, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 122, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 45, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 56, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 60, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 65, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 100, + "Load", + "_geometry_revision" + ], + [ + "tests/test_static.py", + 124, + "Load", + "_geometry_revision" + ], + [ + "tests/test_subtree_geometry_cache.py", + 99, + "Load", + "source_held" + ], + [ + "tests/test_subtree_geometry_cache.py", + 28, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 42, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 51, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 94, + "Load", + "capture" + ], + [ + "tests/test_subtree_geometry_cache.py", + 102, + "Load", + "matches" + ], + [ + "tests/test_subtree_geometry_cache.py", + 106, + "Load", + "capture" + ], + [ + "tests/test_subtree_geometry_cache.py", + 112, + "Load", + "capture" + ], + [ + "tests/test_subtree_geometry_cache.py", + 26, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 44, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 49, + "Load", + "_arrange_root" + ], + [ + "tests/test_subtree_geometry_cache.py", + 101, + "Load", + "matches" + ], + [ + "tests/test_subtree_geometry_cache.py", + 119, + "Load", + "matches" + ], + [ + "tests/test_subtree_geometry_cache.py", + 208, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_subtree_geometry_cache.py", + 236, + "Load", + "_layout_widgets" + ], + [ + "tests/test_subtree_geometry_cache.py", + 262, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_subtree_geometry_cache.py", + 268, + "Load", + "source_held" + ], + [ + "tests/test_subtree_geometry_cache.py", + 261, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_subtree_geometry_cache.py", + 150, + "Load", + "_arrange_root" + ], + [ + "tests/test_viewport_geometry_targets.py", + 690, + "Load", + "_arrange_root" + ], + [ + "tests/test_viewport_geometry_targets.py", + 594, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 644, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 645, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 658, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 683, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 720, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 297, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_viewport_geometry_targets.py", + 302, + "Load", + "matches" + ], + [ + "tests/test_viewport_geometry_targets.py", + 306, + "Load", + "matches" + ], + [ + "tests/test_viewport_geometry_targets.py", + 397, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_viewport_geometry_targets.py", + 464, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_viewport_geometry_targets.py", + 517, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 521, + "Load", + "_arrange_root" + ], + [ + "tests/test_viewport_geometry_targets.py", + 558, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/test_viewport_geometry_targets.py", + 563, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 614, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 270, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_viewport_geometry_targets.py", + 376, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_viewport_geometry_targets.py", + 426, + "Load", + "_layout_mutation_roots" + ], + [ + "tests/test_viewport_geometry_targets.py", + 619, + "Load", + "_refresh_layout" + ], + [ + "tests/test_viewport_geometry_targets.py", + 520, + "Load", + "acquire_subtree_geometry" + ], + [ + "tests/utilities/render.py", + 20, + "Load", + "capture" + ] + ] + } + } + }, + "retained_position_followup": { + "before_revision": "3c7d5ec036056a94c57d554104b31c8a0689417a", + "after_source_tree": "5d286c94eb501080c46043bf949c58ce6920478b", + "relation": "Original partial geometry may supply requested positions it owns; full capture still requires complete scope. Projection routes retained paths through immutable captured geometry membership. Placement, revisions, internal scrolling and held-source checks are unchanged.", + "scope": "Original Package src/tests census. Lexical sites are read against the actual source owner; external dynamic subclasses and private callers are not proven by this census.", + "revisions": { + "3c7d5ec036056a94c57d554104b31c8a0689417a": { + "src": { + "modules": 250, + "omissions": [], + "declarations": [ + "src/textual/_compositor.py:266 contains(self, node: Widget)", + "src/textual/_compositor.py:319 matches(self, key: SubtreeGeometryKey, *, require_complete: bool=False, source_held: bool=False)", + "src/textual/_compositor.py:335 _matches_placement(self, key: SubtreeGeometryKey)", + "src/textual/_compositor.py:337 restore_into(self, geometry: CompositorMap, widgets: set[Widget], invisible_widgets: set[Widget], key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget, *, visible_only: bool, retained: set[Widget], bounds: Region, source_held: bool=False)", + "src/textual/_compositor.py:349 project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget, *, visible_only: bool, retained: set[Widget], bounds: Region, source_held: bool=False, require_root: bool=True)", + "src/textual/_compositor.py:447 _matches_placement(self, key: SubtreeGeometryKey)", + "src/textual/_compositor.py:483 _matches_placement(self, key: SubtreeGeometryKey)", + "src/textual/geometry.py:270 contains(self, x: int, y: int)", + "src/textual/geometry.py:682 contains(self, x: int, y: int)", + "src/textual/layout.py:83 get_visible_placements(self, region: Region, *, retain: Iterable[Widget]=())", + "src/textual/screen.py:1484 _layout_geometry_targets(self)" + ], + "sites": [ + "src/textual/_compositor.py:204 Load SubtreeGeometry", + "src/textual/_compositor.py:332 Load _matches_placement", + "src/textual/_compositor.py:343 Load project_into", + "src/textual/_compositor.py:392 Load project_into", + "src/textual/_compositor.py:444 Load SubtreeGeometry", + "src/textual/_compositor.py:455 Load SubtreeGeometry", + "src/textual/_compositor.py:461 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:468 Load IntrinsicSubtreeGeometry", + "src/textual/_compositor.py:471 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:474 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:755 Load SubtreeGeometry", + "src/textual/_compositor.py:889 Load retain_geometry", + "src/textual/_compositor.py:939 Load retain_geometry", + "src/textual/_compositor.py:1093 Store retained_paths", + "src/textual/_compositor.py:1098 Load retain_geometry", + "src/textual/_compositor.py:1106 Load retained_paths", + "src/textual/_compositor.py:1110 Load retained_paths", + "src/textual/_compositor.py:1111 Load retained_paths", + "src/textual/_compositor.py:1193 Load get_visible_placements", + "src/textual/_compositor.py:1195 Load retained_paths", + "src/textual/_compositor.py:1336 Load contains", + "src/textual/_compositor.py:1336 Load matches", + "src/textual/_compositor.py:1349 Load restore_into", + "src/textual/_compositor.py:1352 Load retained_paths", + "src/textual/_compositor.py:1359 Load contains", + "src/textual/_compositor.py:1373 Load contains", + "src/textual/_compositor.py:1386 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:1414 Load retained_paths", + "src/textual/_compositor.py:1415 Load matches", + "src/textual/_compositor.py:1454 Load restore_into", + "src/textual/_compositor.py:1456 Load retained_paths", + "src/textual/_compositor.py:1555 Load contains", + "src/textual/_compositor.py:1558 Load contains", + "src/textual/_compositor.py:1575 Load contains", + "src/textual/_compositor.py:1723 Load contains", + "src/textual/_compositor.py:1732 Load contains", + "src/textual/screen.py:1525 Load _layout_geometry_targets", + "src/textual/widget.py:737 Load IntrinsicSubtreeGeometry", + "src/textual/widget.py:737 Load PlacedSubtreeGeometry" + ] + }, + "tests": { + "modules": 481, + "omissions": [], + "declarations": [ + "tests/test_viewport_geometry_targets.py:251 _layout_geometry_targets(self)" + ], + "sites": [ + "tests/test_complete_placed_geometry.py:34 Load PlacedSubtreeGeometry", + "tests/test_geometry_reader_acquisition.py:19 Load retain_geometry", + "tests/test_subtree_geometry_cache.py:86 Load contains", + "tests/test_subtree_geometry_cache.py:101 Load matches", + "tests/test_subtree_geometry_cache.py:102 Load matches", + "tests/test_subtree_geometry_cache.py:106 Load PlacedSubtreeGeometry", + "tests/test_subtree_geometry_cache.py:116 Load contains", + "tests/test_subtree_geometry_cache.py:117 Load contains", + "tests/test_subtree_geometry_cache.py:119 Load matches", + "tests/test_subtree_geometry_cache.py:130 Load contains", + "tests/test_subtree_geometry_cache.py:164 Load contains", + "tests/test_subtree_geometry_cache.py:175 Load contains", + "tests/test_subtree_geometry_cache.py:210 Load PlacedSubtreeGeometry", + "tests/test_subtree_geometry_cache.py:211 Load contains", + "tests/test_subtree_geometry_cache.py:217 Load IntrinsicSubtreeGeometry", + "tests/test_subtree_geometry_cache.py:218 Load contains", + "tests/test_subtree_geometry_cache.py:264 Load contains", + "tests/test_subtree_geometry_cache.py:265 Load contains", + "tests/test_subtree_geometry_cache.py:266 Load contains", + "tests/test_viewport_geometry_targets.py:174 Load IntrinsicSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:182 Load PlacedSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:189 Load contains", + "tests/test_viewport_geometry_targets.py:190 Load contains", + "tests/test_viewport_geometry_targets.py:191 Load contains", + "tests/test_viewport_geometry_targets.py:192 Load contains", + "tests/test_viewport_geometry_targets.py:197 Load project_into", + "tests/test_viewport_geometry_targets.py:298 Load IntrinsicSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:299 Load contains", + "tests/test_viewport_geometry_targets.py:302 Load matches", + "tests/test_viewport_geometry_targets.py:306 Load matches", + "tests/test_viewport_geometry_targets.py:398 Load PlacedSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:405 Load contains", + "tests/test_viewport_geometry_targets.py:465 Load contains", + "tests/test_viewport_geometry_targets.py:520 Load PlacedSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:559 Load contains", + "tests/test_viewport_geometry_targets.py:568 Load contains" + ] + } + }, + "5d286c94eb501080c46043bf949c58ce6920478b": { + "src": { + "modules": 250, + "omissions": [], + "declarations": [ + "src/textual/_compositor.py:267 contains(self, node: Widget)", + "src/textual/_compositor.py:320 matches(self, key: SubtreeGeometryKey, *, require_complete: bool=False, source_held: bool=False, required_geometry: Iterable[Widget]=())", + "src/textual/_compositor.py:342 _matches_placement(self, key: SubtreeGeometryKey)", + "src/textual/_compositor.py:344 restore_into(self, geometry: CompositorMap, widgets: set[Widget], invisible_widgets: set[Widget], key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget, *, visible_only: bool, retained: AbstractSet[Widget], bounds: Region, source_held: bool=False)", + "src/textual/_compositor.py:356 project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget, *, visible_only: bool, retained: AbstractSet[Widget], bounds: Region, source_held: bool=False, require_root: bool=True)", + "src/textual/_compositor.py:456 _matches_placement(self, key: SubtreeGeometryKey)", + "src/textual/_compositor.py:492 _matches_placement(self, key: SubtreeGeometryKey)", + "src/textual/geometry.py:270 contains(self, x: int, y: int)", + "src/textual/geometry.py:682 contains(self, x: int, y: int)", + "src/textual/layout.py:83 get_visible_placements(self, region: Region, *, retain: Iterable[Widget]=())", + "src/textual/screen.py:1484 _layout_geometry_targets(self)" + ], + "sites": [ + "src/textual/_compositor.py:205 Load SubtreeGeometry", + "src/textual/_compositor.py:334 Load _matches_placement", + "src/textual/_compositor.py:339 Load contains", + "src/textual/_compositor.py:339 Load required_geometry", + "src/textual/_compositor.py:350 Load project_into", + "src/textual/_compositor.py:401 Load project_into", + "src/textual/_compositor.py:453 Load SubtreeGeometry", + "src/textual/_compositor.py:464 Load SubtreeGeometry", + "src/textual/_compositor.py:470 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:477 Load IntrinsicSubtreeGeometry", + "src/textual/_compositor.py:480 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:483 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:764 Load SubtreeGeometry", + "src/textual/_compositor.py:898 Load retain_geometry", + "src/textual/_compositor.py:948 Load retain_geometry", + "src/textual/_compositor.py:1102 Store retained_paths", + "src/textual/_compositor.py:1107 Load retain_geometry", + "src/textual/_compositor.py:1118 Load retained_paths", + "src/textual/_compositor.py:1200 Load get_visible_placements", + "src/textual/_compositor.py:1202 Load retained_paths", + "src/textual/_compositor.py:1343 Load contains", + "src/textual/_compositor.py:1343 Load matches", + "src/textual/_compositor.py:1356 Load restore_into", + "src/textual/_compositor.py:1359 Load retained_paths", + "src/textual/_compositor.py:1366 Load contains", + "src/textual/_compositor.py:1380 Load contains", + "src/textual/_compositor.py:1393 Load PlacedSubtreeGeometry", + "src/textual/_compositor.py:1421 Load matches", + "src/textual/_compositor.py:1423 Load retained_paths", + "src/textual/_compositor.py:1456 Load restore_into", + "src/textual/_compositor.py:1458 Load retained_paths", + "src/textual/_compositor.py:1557 Load contains", + "src/textual/_compositor.py:1560 Load contains", + "src/textual/_compositor.py:1577 Load contains", + "src/textual/_compositor.py:1725 Load contains", + "src/textual/_compositor.py:1734 Load contains", + "src/textual/screen.py:1525 Load _layout_geometry_targets", + "src/textual/widget.py:737 Load IntrinsicSubtreeGeometry", + "src/textual/widget.py:737 Load PlacedSubtreeGeometry" + ] + }, + "tests": { + "modules": 481, + "omissions": [], + "declarations": [ + "tests/test_subtree_geometry_cache.py:44 _layout_geometry_targets(self)", + "tests/test_viewport_geometry_targets.py:251 _layout_geometry_targets(self)" + ], + "sites": [ + "tests/test_complete_placed_geometry.py:34 Load PlacedSubtreeGeometry", + "tests/test_geometry_reader_acquisition.py:19 Load retain_geometry", + "tests/test_subtree_geometry_cache.py:54 Load contains", + "tests/test_subtree_geometry_cache.py:55 Load contains", + "tests/test_subtree_geometry_cache.py:70 Load matches", + "tests/test_subtree_geometry_cache.py:82 Load contains", + "tests/test_subtree_geometry_cache.py:176 Load contains", + "tests/test_subtree_geometry_cache.py:191 Load matches", + "tests/test_subtree_geometry_cache.py:192 Load matches", + "tests/test_subtree_geometry_cache.py:196 Load PlacedSubtreeGeometry", + "tests/test_subtree_geometry_cache.py:206 Load contains", + "tests/test_subtree_geometry_cache.py:207 Load contains", + "tests/test_subtree_geometry_cache.py:209 Load matches", + "tests/test_subtree_geometry_cache.py:220 Load contains", + "tests/test_subtree_geometry_cache.py:254 Load contains", + "tests/test_subtree_geometry_cache.py:265 Load contains", + "tests/test_subtree_geometry_cache.py:300 Load PlacedSubtreeGeometry", + "tests/test_subtree_geometry_cache.py:301 Load contains", + "tests/test_subtree_geometry_cache.py:307 Load IntrinsicSubtreeGeometry", + "tests/test_subtree_geometry_cache.py:308 Load contains", + "tests/test_subtree_geometry_cache.py:354 Load contains", + "tests/test_subtree_geometry_cache.py:355 Load contains", + "tests/test_subtree_geometry_cache.py:356 Load contains", + "tests/test_viewport_geometry_targets.py:174 Load IntrinsicSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:182 Load PlacedSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:189 Load contains", + "tests/test_viewport_geometry_targets.py:190 Load contains", + "tests/test_viewport_geometry_targets.py:191 Load contains", + "tests/test_viewport_geometry_targets.py:192 Load contains", + "tests/test_viewport_geometry_targets.py:197 Load project_into", + "tests/test_viewport_geometry_targets.py:298 Load IntrinsicSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:299 Load contains", + "tests/test_viewport_geometry_targets.py:302 Load matches", + "tests/test_viewport_geometry_targets.py:306 Load matches", + "tests/test_viewport_geometry_targets.py:398 Load PlacedSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:405 Load contains", + "tests/test_viewport_geometry_targets.py:465 Load contains", + "tests/test_viewport_geometry_targets.py:520 Load PlacedSubtreeGeometry", + "tests/test_viewport_geometry_targets.py:559 Load contains", + "tests/test_viewport_geometry_targets.py:568 Load contains" + ] + } + } + } + } +} diff --git a/evidence/subtree-mutation-source-20261008/pending_pointer_tasks.py b/evidence/subtree-mutation-source-20261008/pending_pointer_tasks.py new file mode 100644 index 0000000000..9ad0e5157b --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/pending_pointer_tasks.py @@ -0,0 +1,118 @@ +"""Read original pending pointer receipts on the application's event loop. + +Used by the existing saved-history observer during a slow delivery. No handler, +queue, task, future or timer is replaced; nothing is awaited or scheduled here. +Only identities, declaration locations and native custody are returned, never +event text, widget content or arbitrary frame locals. +""" + +import asyncio +import gc +import inspect +from functools import partial + +from textual.message import Message +from textual.message_pump import MessagePump +from textual.await_complete import AwaitCompletion + + +def capture_pointer_wait(app): + """Capture current App forwarding and the FIFO which owns its receipt.""" + tasks = asyncio.all_tasks() + frames_by_task = {} + for task in tasks: + operation = task.get_coro() + frames, visited = [], set() + while operation is not None and id(operation) not in visited: + visited.add(id(operation)) + if inspect.iscoroutine(operation): + frame, operation = operation.cr_frame, operation.cr_await + elif inspect.isgenerator(operation): + frame, operation = operation.gi_frame, operation.gi_yieldfrom + else: + # Coroutine wrappers expose the original coroutine to GC, + # whereas Future iterators do not own another Python frame. + operation = next((item for item in gc.get_referents(operation) + if inspect.iscoroutine(item) or inspect.isgenerator(item)), None) + continue + if frame is not None: + frames.append(frame) + frames_by_task[task] = frames + + def identity(value): + return None if value is None else f"{type(value).__qualname__}@{id(value):x}" + + def future_state(future): + if future is None: + return None + state = {"identity": identity(future), "done": future.done(), + "cancelled": future.cancelled()} + if isinstance(future, asyncio.Task): + state["name"] = future.get_name() + if children := getattr(future, "_children", ()): + state["children"] = [future_state(child) for child in children] + return state + + def task_state(task): + if task is None: + return None + chain = [] + for frame in frames_by_task.get(task, ()): + entry = {"file": frame.f_code.co_filename, + "function": frame.f_code.co_qualname, "line": frame.f_lineno} + owner = frame.f_locals.get("self") + if isinstance(owner, MessagePump): + entry["pump"] = identity(owner) + queue = owner.__dict__.get("_message_queue") + entry["queued"] = 0 if queue is None else queue.qsize() + message = frame.f_locals.get("message", frame.f_locals.get("event", frame.f_locals.get("callback"))) + if isinstance(message, Message): + entry["message"] = identity(message) + callback = getattr(message, "callback", None) + else: + callback = frame.f_locals.get("callback") + while isinstance(callback, partial): + callback = callback.func + if callback is not None: + entry["callback"] = getattr(callback, "__qualname__", type(callback).__qualname__) + if isinstance(owner, AwaitCompletion): + # Observe an already acquired receipt without calling _start: + # removal may otherwise begin its optional completion here. + values = vars(owner) + entry["receipt"] = identity(owner) + entry["receipt_future"] = future_state(values.get("_future", values.get("_completion"))) + chain.append(entry) + state = future_state(task) + state["await_chain"] = chain + # This is the actual Task waiter, including a gather's original children. + # An Event / executor waiter does not prove which external operation + # will release it; the Python await chain remains the evidence there. + state["waiter"] = future_state(getattr(task, "_fut_waiter", None)) + return state + + receipts = [] + for task, frames in frames_by_task.items(): + for frame in frames: + if frame.f_code.co_qualname != "MessagePump._post_message_and_wait..wait_for_dispatch": + continue + message = frame.f_locals["message"] + custody = message._dispatch_completion + if custody is None: + continue + receiver, caller, completion = custody + if caller is not app._task or completion.done(): + continue + queue = receiver.__dict__.get("_message_queue") + receipts.append({ + "message": identity(message), "receiver": identity(receiver), + "queued_on_receiver": 0 if queue is None else queue.qsize(), + "receiver_task": task_state(receiver._task), + "receipt_wait_task": task_state(task), + "completion": future_state(completion), + }) + return { + "app_task": task_state(app._task), "forwarded_receipts": receipts, + # Preserve original concurrent await chains for mount / worker joins; + # no inferred dependency edges are manufactured from similar names. + "tasks": [task_state(task) for task in tasks], + } diff --git a/evidence/subtree-mutation-source-20261008/retained-position-control.txt b/evidence/subtree-mutation-source-20261008/retained-position-control.txt new file mode 100644 index 0000000000..dea674fa44 --- /dev/null +++ b/evidence/subtree-mutation-source-20261008/retained-position-control.txt @@ -0,0 +1,2 @@ +. [100%] +1 passed in 0.64s diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 0c9ab27530..0f3005f8e7 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -22,14 +22,13 @@ from types import MappingProxyType from typing import ( TYPE_CHECKING, + AbstractSet, Callable, - Generic, Iterable, Iterator, Mapping, NamedTuple, Sequence, - TypeVar, cast, ) @@ -195,9 +194,6 @@ def project(self, original: SubtreeGeometryKey, current: SubtreeGeometryKey, ), clip -GeometryEntry = TypeVar("GeometryEntry", MapGeometry, SubtreeMapGeometry) - - @dataclass(frozen=True) class SubtreeGeometryPlacement: """A borrowed child source at its original acquired parent placement. @@ -223,25 +219,30 @@ def region(self) -> Region: @dataclass(frozen=True) -class SubtreeGeometry(ABC, Generic[GeometryEntry]): +class SubtreeGeometry(ABC): """One immutable native arrangement resource in the compositor's cache.""" key: SubtreeGeometryKey - geometry: Mapping[Widget, tuple[int, GeometryEntry | SubtreeGeometryPlacement]] + geometry: Mapping[Widget, tuple[int, SubtreeMapGeometry | SubtreeGeometryPlacement]] """Ordered local placements and borrowed child sources, without flattening.""" widgets: frozenset[Widget] invisible_widgets: frozenset[Widget] - @classmethod - def complete_arrangement(cls, enclosing_complete: bool) -> bool: - return enclosing_complete - @classmethod def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_coordinates): - return cls(key, MappingProxyType({ - node: (ordinal, entry) - for ordinal, (node, entry) in enumerate(geometry.items()) - }), widgets, invisible_widgets) + routes = cls._route_geometry(geometry) + captured = {} + for ordinal, (node, entry) in enumerate(geometry.items()): + parent = node.parent + parent = parent._render_widget if isinstance(parent, Widget) else None + parent = parent if parent is not node and parent in routes else None + if isinstance(entry, MapGeometry): + entry = SubtreeMapGeometry(entry, (), parent) + else: + entry = (replace(entry, parent=parent) if isinstance(entry, SubtreeGeometryPlacement) + else entry._replace(parent=parent)) + captured[node] = ordinal, entry + return cls(key, MappingProxyType(captured), widgets, invisible_widgets) @staticmethod def _route_geometry(geometry) -> Mapping[Widget, tuple[Widget, ...]]: @@ -264,7 +265,7 @@ def _geometry_routes(self) -> Mapping[Widget, tuple[Widget, ...]]: return self._route_geometry({node: entry for node, (_, entry) in self.geometry.items()}) def contains(self, node: Widget) -> bool: - """Membership in this complete source, including borrowed children.""" + """Membership in this acquired geometry, including borrowed children.""" return node in self._geometry_routes def captured_parent(self, node: Widget) -> Widget | None: @@ -316,26 +317,45 @@ def _spatial_map(self) -> SpatialMap[tuple[int, Widget]]: ) return spatial_map - @abstractmethod def matches(self, key: SubtreeGeometryKey, *, require_complete: bool = False, - source_held: bool = False) -> bool: - ... + source_held: bool = False, + required_geometry: Iterable[Widget] = ()) -> bool: + if not self.reusable and not source_held: + return False + if require_complete and not self.complete: + return False + if source_held: + # Original source custody owns revisions during the write. Size, + # internal scrolling, layers and destination placement still agree. + key = key._replace(geometry_revision=self.key.geometry_revision, + nodes_revision=self.key.nodes_revision) + if self.complete and key.visible_only: + key = key._replace(visible_only=False) + if not self._matches_placement(key): + return False + # A position demand needs its original placed answer, not every + # offscreen descendant. A complete source also owns absent answers; + # an incomplete source must contain each explicitly requested target. + return self.complete or all(self.contains(node) for node in required_geometry) + + @abstractmethod + def _matches_placement(self, key: SubtreeGeometryKey) -> bool: ... def restore_into( self, geometry: CompositorMap, widgets: set[Widget], invisible_widgets: set[Widget], key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip], - root: Widget, *, visible_only: bool, retained: set[Widget], bounds: Region, + root: Widget, *, visible_only: bool, retained: AbstractSet[Widget], bounds: Region, source_held: bool = False, ) -> None: self.project_into(geometry, key, clip, clips, root, visible_only=visible_only, retained=retained, bounds=bounds, source_held=source_held) - widgets.update(self.members()) - invisible_widgets.update(self.members(invisible=True)) + widgets.update(node for node in self.members() if not node._pruning) + invisible_widgets.update(node for node in self.members(invisible=True) if not node._pruning) def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, clip: SceneClip, clips: dict[Widget, SceneClip], root: Widget, - *, visible_only: bool, retained: set[Widget], bounds: Region, + *, visible_only: bool, retained: AbstractSet[Widget], bounds: Region, source_held: bool = False, require_root: bool = True) -> None: """Publish the requested scene without retiring complete source geometry. @@ -349,7 +369,9 @@ def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, candidates = dict(self._spatial_map.get_values_in_region(source_bounds)) if (indexed_root := self.geometry.get(root)) is not None: candidates[indexed_root[0]] = root - for node in retained: + # Captured membership scopes this source's demands. Unrelated + # retained paths must not be rediscovered in every borrowed child. + for node in (retained & self._geometry_routes.keys() if retained else ()): if (indexed := self.geometry.get(node)) is not None: candidates[indexed[0]] = node if isinstance(indexed[1], SubtreeGeometryPlacement): @@ -369,6 +391,10 @@ def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, entries = ((node, entry) for node, (_, entry) in self.geometry.items()) no_demand: set[Widget] = set() for node, entry in self._paint_entries(entries, source_held=source_held): + # The source can outlive its cache entry. Original prune admission + # still retires native participation before pump teardown. + if node._pruning: + continue required = not visible_only or (node is root and require_root) or node in retained if isinstance(entry, SubtreeGeometryPlacement): child_key, child_clip = self._project_child(entry, key, clip) @@ -389,8 +415,20 @@ def project_into(self, geometry: CompositorMap, key: SubtreeGeometryKey, clips[node] = node_clip def _paint_entries(self, entries, *, source_held: bool): - """Placed sources retain their original native participation.""" - return entries + """Retained self-paint follows the source's original captured ancestry.""" + if not source_held: + return entries + participation: dict[Widget, bool] = {} + + def participates(node: Widget) -> bool: + if node not in participation: + parent = self.captured_parent(node) + participation[node] = node._render_widget is node and ( + parent is None or (parent.is_container and participates(parent)) + ) + return participation[node] + + return ((node, entry) for node, entry in entries if participates(node)) def _project_child(self, entry: SubtreeGeometryPlacement, key: SubtreeGeometryKey, clip: SceneClip) -> tuple[SubtreeGeometryKey, SceneClip]: @@ -398,7 +436,7 @@ def _project_child(self, entry: SubtreeGeometryPlacement, key: SubtreeGeometryKe return entry.key, entry.clip @abstractmethod - def _project_entry(self, entry: GeometryEntry, key: SubtreeGeometryKey, + def _project_entry(self, entry: SubtreeMapGeometry, key: SubtreeGeometryKey, clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]: ... @@ -412,40 +450,27 @@ def references_retired(self, owner: Widget, retired: set[Widget]) -> bool: @dataclass(frozen=True) -class PlacedSubtreeGeometry(SubtreeGeometry[MapGeometry]): +class PlacedSubtreeGeometry(SubtreeGeometry): """A culled or screen-dependent arrangement keeps its exact placement.""" - def matches(self, key: SubtreeGeometryKey, *, require_complete: bool = False, - source_held: bool = False) -> bool: - if not self.reusable and not source_held: - return False - if require_complete and not self.complete: - return False - return self.key == key or ( - not self.key.visible_only and key.visible_only - and self.key._replace(visible_only=True) == key - ) + def _matches_placement(self, key: SubtreeGeometryKey) -> bool: + return self.key == key - def _project_entry(self, entry: MapGeometry, key: SubtreeGeometryKey, + def _project_entry(self, entry: SubtreeMapGeometry, key: SubtreeGeometryKey, clip: SceneClip, *, root: bool) -> tuple[MapGeometry, SceneClip]: - return entry, RootSceneClip(entry.clip) + return entry.geometry, RootSceneClip(entry.geometry.clip) @dataclass(frozen=True) -class IntrinsicSubtreeGeometry(SubtreeGeometry[SubtreeMapGeometry]): +class IntrinsicSubtreeGeometry(SubtreeGeometry): """The same bounded resource, reusable under a new original outer clip.""" - @classmethod - def complete_arrangement(cls, enclosing_complete: bool) -> bool: - return True - @classmethod def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_coordinates): - if not (widgets | invisible_widgets).isdisjoint(screen_coordinates): + if key.visible_only or not (widgets | invisible_widgets).isdisjoint(screen_coordinates): return PlacedSubtreeGeometry.capture( key, geometry, widgets, invisible_widgets, clips, clip, screen_coordinates) intrinsic = {} origin = key.region.offset - geometry_routes = cls._route_geometry(geometry) for node, entry in geometry.items(): child = isinstance(entry, SubtreeGeometryPlacement) scope, bounds = (entry.clip if child else clips[node]).relative_bounds(clip, origin) @@ -457,29 +482,14 @@ def capture(cls, key, geometry, widgets, invisible_widgets, clips, clip, screen_ if scope is not clip: return PlacedSubtreeGeometry.capture( key, geometry, widgets, invisible_widgets, clips, clip, screen_coordinates) - parent = node.parent - parent = parent._render_widget if isinstance(parent, Widget) else None - parent = (parent if parent is not node and parent in geometry_routes - else None) intrinsic[node] = ( - replace(entry, parent=parent, clip_bounds=bounds) if child - else SubtreeMapGeometry(entry, bounds, parent) + replace(entry, clip_bounds=bounds) if child + else SubtreeMapGeometry(entry, bounds, None) ) return super().capture(key, intrinsic, widgets, invisible_widgets, clips, clip, screen_coordinates) - def matches(self, key: SubtreeGeometryKey, *, require_complete: bool = False, - source_held: bool = False) -> bool: - if not self.reusable and not source_held: - return False - if require_complete and not self.complete: - return False - if source_held: - # The mutation owner lends the original source, not the changing - # DOM. Internal scroll and all arrangement inputs still agree; - # only source revisions belong to the ongoing mutation. - key = key._replace(geometry_revision=self.key.geometry_revision, - nodes_revision=self.key.nodes_revision) + def _matches_placement(self, key: SubtreeGeometryKey) -> bool: return self.key.intrinsic() == key.intrinsic() def _project_entry(self, entry: SubtreeMapGeometry, key: SubtreeGeometryKey, @@ -497,26 +507,6 @@ def _project_child(self, entry, key, clip): clip=clip.region, ), clip - def _paint_entries(self, entries, *, source_held: bool): - if not source_held: - return entries - participation: dict[Widget, bool] = {} - - def participates(node: Widget) -> bool: - if node not in participation: - parent = self.captured_parent(node) - participation[node] = node._render_widget is node and ( - parent is None or (parent.is_container and participates(parent)) - ) - return participation[node] - - # A retained-paint owner can now render itself rather than its old - # native children. Consult that declaration along captured ancestry, - # never walk or arrange the changing child tree. Each parent answers - # once within this projection; the resource remains immutable. - return ((node, entry) for node, entry in entries if participates(node)) - - class CompositorUpdate: """An update generated by the compositor, which also doubles as console renderables.""" @@ -904,7 +894,7 @@ def reflow( old_map = previous_map old_widgets = old_map.keys() - map, widgets = self._arrange_root( + map, widgets, _ = self._arrange_root( parent, size, visible_only=visible_only, retain_geometry=retain_geometry, ) @@ -954,7 +944,7 @@ def reflow_visible( # Keep a copy of the old map because we're going to compare it with the update old_map = self._visible_map or {} - map, widgets = self._arrange_root( + map, widgets, _ = self._arrange_root( parent, size, visible_only=True, retain_geometry=retain_geometry, ) @@ -1004,7 +994,7 @@ def full_map(self) -> CompositorMap: if self.root is None: return {} if self._full_map_invalidated and not self._arranging: - map, _widgets = self._arrange_root(self.root, self.size, visible_only=False) + map, _widgets, _ = self._arrange_root(self.root, self.size, visible_only=False) # A geometry query also publishes this scene. Retain the damage # from its original visible coordinates before replacing the map; # a later reflow can no longer recover that previous geometry. @@ -1047,20 +1037,23 @@ def _arrange_root( self, root: Widget, size: Size, visible_only: bool = True, retain_geometry: Iterable[Widget] = (), *, root_geometry: MapGeometry | None = None, - ) -> tuple[CompositorMap, set[Widget]]: + root_clip: SceneClip | None = None, + ) -> tuple[CompositorMap, set[Widget], SubtreeGeometryPlacement | None]: """Arrange a widget's children based on its layout attribute. Args: root: Top level widget. size: Size of visible area (screen). visible_only: Only update visible widgets (used in scrolling). - root_geometry: Original placement when arranging a complete body. + root_geometry: Original placement for explicit subtree acquisition. + root_clip: Original outer clip, or complete body bounds for painting. Returns: - Compositor map and set of widgets. + Compositor map, logical members, and the explicitly acquired source. """ - mutation_roots = set(root.screen._layout_mutation_roots()) + mutation_sources = root.screen._layout_mutation_roots() + mutation_roots = set(mutation_sources) previous = self._published_map held: dict[Widget, CompositorMap] = {owner: {} for owner in mutation_roots} mutation_paths: set[Widget] = set() @@ -1074,6 +1067,8 @@ def _arrange_root( self._subtree_geometry.pop(owner, None) map: dict[Widget, MapGeometry | SubtreeGeometryPlacement] = {} capturing_source = False + capture_root = root_geometry is not None + acquired: SubtreeGeometryPlacement | None = None # Transient declaration paths manufacture the one retained resource. # The published MapGeometry continues to own the actual clipped scene. clips: dict[Widget, SceneClip] = {} @@ -1091,7 +1086,11 @@ def store_geometry(node: Widget, geometry: MapGeometry, clip: SceneClip) -> None size, size, size.region, NULL_SPACING, ) layer_order = root_geometry.order[-1][2] - no_clip = RootSceneClip(root_geometry.region) + # Screen overlays retain their original screen clip, even when a body + # is acquired with expanded paint bounds or its own incoming clip. + no_clip = RootSceneClip(size.region) + incoming_clip = (root_clip if root_clip is not None else + RootSceneClip(root_geometry.region) if capture_root else no_clip) # Widget owns layer inheritance. Acquire external ancestry only at the # root, then carry that original declaration through this traversal. root_layers = root._get_layer_order(root.walk_ancestors(with_self=True)) @@ -1100,7 +1099,7 @@ def store_geometry(node: Widget, geometry: MapGeometry, clip: SceneClip) -> None else {name: index for index, name in enumerate(root_layers)} ) default_layers = {"default": 0} - retained_paths: set[Widget] = set() + retained_paths: dict[Widget, set[Widget]] = {} if visible_only: # A held root still needs its new outer placement when scrolling # takes it offscreen. Its resource owns descendants; this acquires @@ -1108,17 +1107,15 @@ def store_geometry(node: Widget, geometry: MapGeometry, clip: SceneClip) -> None for target in (*retain_geometry, *mutation_roots): path: list[Widget] = [] node = target - # Existing members already own a path to this same root. - # Shared ancestors need admission once, not per target. - while ( - isinstance(node, Widget) - and node is not root - and node not in retained_paths - ): + while isinstance(node, Widget) and node is not root: path.append(node) node = node.parent - if node is root or node in retained_paths: - retained_paths.update(path) + if node is root: + # The same acquired relation supplies path admission and + # the targets required within each source. Keys are routing + # ancestors, values are original requested positions. + for member in path: + retained_paths.setdefault(member, set()).add(target) def arrange_widget( widget: Widget, @@ -1202,7 +1199,7 @@ def arrange_widget( if visible_only and not complete: placements = arrange_result.get_visible_placements( sub_clip.region - child_region.offset + widget.scroll_offset, - retain=retained_paths, + retain=retained_paths.keys(), ) else: placements = arrange_result.placements @@ -1332,11 +1329,12 @@ def arrange_widget( def add_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, complete, inherited_layers): - nonlocal map, widgets, invisible_widgets, capturing_source + nonlocal map, widgets, invisible_widgets, capturing_source, acquired if inherited_layers is None and (order_names := widget._get_layer_order((widget,))) is not None: inherited_layers = {name: index for index, name in enumerate(order_names)} if widget in held: - resource = self._subtree_geometry.get(widget) + placement = mutation_sources[widget] + resource = placement.source if placement is not None else None key = SubtreeGeometryKey.from_widget( widget, virtual_region, region, order, layer_order, clip.region, visible, dock_gutter, size, False, @@ -1345,6 +1343,8 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible if resource is not None and resource.contains(widget._render_widget) and resource.matches( key, require_complete=True, source_held=True, ): + if capture_root and widget is root: + acquired = SubtreeGeometryPlacement(resource, key, clip, source_held=True) # Complete intrinsic geometry retains original descendants # and clip declarations. Ancestor scrolling changes only # their placement; no mutating DOM arrangement is read. @@ -1356,7 +1356,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible resource.restore_into( map, widgets, invisible_widgets, key, clip, clips, widget._render_widget, visible_only=visible_only and not complete, - retained=retained_paths, bounds=root_geometry.region, + retained=retained_paths.keys(), bounds=root_geometry.region, source_held=True, ) # The complete resource has replaced these old viewport @@ -1368,11 +1368,23 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible else: # Placed, resized or missing sources cannot authorize a # projection. Keep their exact committed hit/paint bounds. + snapshot = held[widget] + if resource is not None and resource.complete: + # A changed destination refuses translation, not the + # original source's membership and self-paint lifetime. + # Filter the committed rectangles along captured parent + # edges; never infer ancestry from the changing DOM. + snapshot = dict( + resource._paint_entries( + ((node, geometry) for node, geometry in snapshot.items() + if resource.contains(node)), + source_held=True, + ) + ) if capturing_source: # The published fallback is a partial placed source, # not an invented complete or translatable subtree. # Keep that lending scope on the same child edge. - snapshot = held[widget] partial_key = SubtreeGeometryKey.from_widget( widget, virtual_region, region, order, layer_order, clip.region, visible, dock_gutter, size, True, @@ -1386,36 +1398,34 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible source, partial_key, clip, source_held=True ) else: - for node, geometry in held[widget].items(): + for node, geometry in snapshot.items(): store_geometry(node, geometry, RootSceneClip(geometry.clip)) - widgets.update(held[widget]) + widgets.update(snapshot) + # The filtered original membership replaces this held + # fallback; the outer merge must not resurrect old children. + held[widget].clear() return - if (widget in mutation_paths or not self.max_subtree_geometry_entries - or not widget.CACHE_SUBTREE_GEOMETRY or not widget._is_mounted): + explicit_root = capture_root and widget is root + cache_source = bool(self.max_subtree_geometry_entries and widget.CACHE_SUBTREE_GEOMETRY) + if not explicit_root and (widget in mutation_paths or not cache_source or not widget._is_mounted): arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, complete, inherited_layers) # noqa: F821 -- closure cleared after traversal return resource_type = widget.subtree_geometry_resource() enclosing_complete = complete - complete = resource_type.complete_arrangement(complete) key = SubtreeGeometryKey.from_widget(widget, virtual_region, region, order, layer_order, clip.region, visible, dock_gutter, size, visible_only and not complete, tuple(inherited_layers.items()) if inherited_layers is not None else ()) resource = self._subtree_geometry.get(widget) - require_complete = widget in retained_paths and not complete - matches = resource is not None and resource.matches(key, require_complete=require_complete) - # A complete captured source already owns every requested path. - # A partial or changed source still acquires the original path; - # do not retain a partial capture whose key omits that request. - if require_complete and not matches: - arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, - complete, inherited_layers) # noqa: F821 -- closure cleared after traversal - return + matches = resource is not None and resource.matches( + key, require_complete=explicit_root and not visible_only, + required_geometry=retained_paths.get(widget, ()), + ) if not matches: - # Capture complete source geometry before its viewport publication. - # Enclosing complete captures borrow all child entries; ordinary - # viewport publication retains only exposed and required boxes. + # Capture this acquisition before its viewport publication. + # Enclosing captures borrow the original child sources; + # viewport acquisition includes exposed and requested boxes. parent_map, parent_widgets, parent_invisible, parent_capture = ( map, widgets, invisible_widgets, capturing_source ) @@ -1429,12 +1439,15 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible finally: map, widgets, invisible_widgets = parent_map, parent_widgets, parent_invisible capturing_source = parent_capture - if (widget not in self._subtree_geometry - and len(self._subtree_geometry) >= self.max_subtree_geometry_entries): - self._subtree_geometry.pop(next(iter(self._subtree_geometry))) resource = resource_type.capture( key, geometry, added_widgets, added_invisible, clips, clip, screen_coordinates) - self._subtree_geometry[widget] = resource + if cache_source: + if (widget not in self._subtree_geometry + and len(self._subtree_geometry) >= self.max_subtree_geometry_entries): + self._subtree_geometry.pop(next(iter(self._subtree_geometry))) + self._subtree_geometry[widget] = resource + if explicit_root: + acquired = SubtreeGeometryPlacement(resource, key, clip) if capturing_source: # Retain the child source and its placement. A containing # capture does not consume a second flat copy of descendants. @@ -1442,7 +1455,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible else: resource.restore_into(map, widgets, invisible_widgets, key, clip, clips, widget._render_widget, visible_only=visible_only and not enclosing_complete, - retained=retained_paths, bounds=root_geometry.region) + retained=retained_paths.keys(), bounds=root_geometry.region) # Add top level (root) widget previous_layout_geometry = self._layout_geometry @@ -1458,7 +1471,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible root_geometry.region, root_geometry.order, layer_order, - no_clip, + incoming_clip, True, root_geometry.dock_gutter, False, @@ -1476,7 +1489,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible map.update(geometry) widgets.update(geometry) widgets -= invisible_widgets - return cast(CompositorMap, map), widgets + return cast(CompositorMap, map), widgets, acquired @property def layers(self) -> list[tuple[Widget, MapGeometry]]: @@ -1693,6 +1706,33 @@ def find_widget(self, widget: Widget) -> MapGeometry: raise errors.NoWidget("Widget is not in layout") return geometry + def acquire_subtree_geometry(self, root: Widget) -> SubtreeGeometryPlacement | None: + """Acquire the complete original source before a mutation begins. + + The transaction retains this placement, independently of cache eviction. + Nested acquisition lends existing held sources while arranging only the + unmodified remainder. It never publishes or paints a replacement scene. + An absent root or incomplete held source supplies no complete resource. + """ + geometry = self._get_published_geometry(root) + if geometry is None or self._arranging: + return None + # An enclosing held source owns this original member. Acquiring a new + # descendant scope would read its changing DOM; the containing source + # does not retain every descendant's independent arrangement inputs. + # Expansion to an unmodified ancestor remains a complete acquisition. + if any( + owner is not root and placement is not None and placement.source.contains(root) + for owner, placement in root.screen._layout_mutation_roots().items() + ): + return None + _, _, placement = self._arrange_root( + root, self.size, visible_only=False, root_geometry=geometry, + root_clip=RootSceneClip(geometry.clip), + ) + return placement if (placement is not None and placement.source.complete + and placement.source.contains(root._render_widget)) else None + def acquire_geometry(self, widgets: Iterable[Widget]) -> bool: """Acquire missing reader paths in one original scene publication. @@ -2100,8 +2140,9 @@ def published_geometry( yield root, geometry def render_subtree_strips( - self, root: Widget, root_geometry: MapGeometry, - ) -> tuple[Size, list[Strip]]: + self, root: Widget, root_geometry: MapGeometry, *, + admit: Callable[[Mapping[Widget, tuple[Region, Region]]], bool], + ) -> tuple[Size, Iterator[list[Strip]]] | None: """Paint a body using its borrowed original published placement. The caller acquires the placement from published_geometry and consumes @@ -2109,17 +2150,46 @@ def render_subtree_strips( or manufactures a scene to decide whether a body can be retired. The same original arrangement/line/chop algorithm supplies complete rows without replacing any published map or constructing another compositor. + + The synchronous admission callback receives the exact complete paint + mapping, including offscreen participants and excluding hidden or clipped + children. It may refuse capture when those participants' resources are + unavailable. Admission and painting borrow the same geometry; no second + DOM walk or arrangement chooses another capture cohort. + + The returned iterator paints viewport-height bands. A preceding-source + writer may drain it synchronously; a retirement task may yield between + bands. The caller retains and validates its original source witness + before advancing and before committing the complete result. Geometry + lending ends before each band is returned, so ordinary input and frame + publication never borrow a suspended capture's private arrangement. """ bounds = root_geometry.region - geometry, _ = self._arrange_root( + geometry, _, _ = self._arrange_root( root, self.size, visible_only=False, root_geometry=root_geometry, ) - widgets = self._paint_regions(self._ordered_geometry(geometry), bounds) - cuts = self._cuts_for_regions(bounds, widgets) + widgets = MappingProxyType(self._paint_regions(self._ordered_geometry(geometry), bounds)) with self._using_geometry(root, geometry): - chops = self._render_chops(bounds, self._regions_to_spans((bounds,)), - widgets=widgets, cuts=cuts, bounds=bounds) - return bounds.size, [Strip.join(chop.values()) for chop in chops] + if not admit(widgets): + return None + band_height = max(1, self.size.height) + + def render_bands() -> Iterator[list[Strip]]: + for y in range(bounds.y, bounds.bottom, band_height): + band = Region(bounds.x, y, bounds.width, min(band_height, bounds.bottom - y)) + participants = { + widget: (region, paint) + for widget, (region, original_paint) in widgets.items() + if (paint := original_paint.intersection(band)) + } + with self._using_geometry(root, geometry): + cuts = self._cuts_for_regions(band, participants) + chops = self._render_chops(band, self._regions_to_spans((band,)), + widgets=participants, cuts=cuts, bounds=band) + strips = [Strip.join(chop.values()) for chop in chops] + yield strips + + return bounds.size, render_bands() def _render_chops( self, diff --git a/src/textual/screen.py b/src/textual/screen.py index b8095fa4d5..472115386d 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -22,6 +22,7 @@ Iterable, Iterator, Literal, + Mapping, NamedTuple, Optional, TypeVar, @@ -36,7 +37,7 @@ from textual._arrange import arrange from textual._auto_scroll import get_auto_scroll_regions from textual._callback import invoke -from textual._compositor import Compositor, MapGeometry +from textual._compositor import Compositor, MapGeometry, SubtreeGeometryPlacement from textual._context import active_message_pump, visible_screen_stack from textual._path import ( CSSPathType, @@ -1236,6 +1237,13 @@ async def _on_idle(self, event: events.Idle) -> None: # Borrow only within this synchronous admission. Paint reacquires its # own cohort after queued source/layout work has had a chance to run. roots: tuple[Widget, ...] | None = None + # A mutation release wakes this owner with check_idle(). Its original + # retained members now become actionable; no new source invalidation + # or window-sized layout request is needed to resume them. + if not self._layout_required and self._has_actionable_layout_requests( + self._held_layout_requests() + ): + self._request_layout() if not self.app._batch_count and self.is_current: if self._refresh_requested or self._dirty_widgets: self._update_timer.resume() @@ -1255,9 +1263,13 @@ def _prepare_compositor_refresh(self) -> tuple[Widget, ...]: """Prepare the frame and return original subtrees whose paint is held.""" return () - def _layout_mutation_roots(self) -> tuple[Widget, ...]: - """Subtrees borrowing their committed geometry during source mutation.""" - return () + def _layout_mutation_roots(self) -> Mapping[Widget, SubtreeGeometryPlacement | None]: + """Mutation owners lending sources acquired before their writes. + + None holds only the exact placed publication; it cannot authorize a + complete or translated source. Release wakes this screen with check_idle. + """ + return {} def _on_frame_published(self, deferred_roots: tuple[Widget, ...]) -> None: """Observe an actual displayed update with its acquired admission roots.""" @@ -1401,9 +1413,16 @@ def _invoke_and_clear_callbacks(self, roots: tuple[Widget, ...] | None = None) - # That publication does not complete the mutating sender's work. # Acquire source custody once for this synchronous admission, # separately from the original paint-readiness roots. - roots = (*roots, *(root - for screen in (*self.app._background_screens, self) - for root in screen._layout_mutation_roots())) + for screen in (*self.app._background_screens, self): + mutation_sources = screen._layout_mutation_roots() + roots = (*roots, *mutation_sources) + # A queued next callback may run before Idle following source + # release. Retained requests must fence that admission too. + screen._request_layout( + screen._has_actionable_layout_requests( + screen._held_layout_requests(mutation_sources) + ) + ) index = 0 for _ in range(len(self._callbacks)): if self.app._batch_count or index >= len(self._callbacks): @@ -1466,11 +1485,14 @@ def _layout_geometry_targets(self) -> tuple[Widget, ...]: """Additional native geometry required by a viewport-layout transaction.""" return () - def _held_layout_requests(self) -> dict[DOMNode, set[Widget]]: + def _held_layout_requests( + self, mutation_roots: Iterable[Widget] | None = None, + ) -> dict[DOMNode, set[Widget]]: """Borrow pending requests whose subtree still owns committed geometry.""" if not self._layout_widgets: return {} - mutation_roots = self._layout_mutation_roots() + if mutation_roots is None: + mutation_roots = self._layout_mutation_roots() return { owner: held for owner, members in self._layout_widgets.items() if (held := { @@ -1593,7 +1615,6 @@ async def _on_layout(self, message: messages.Layout) -> None: message.stop() message.prevent_default() - layout_required = False widget: DOMNode = message.widget if widget._pruning or widget._closed: return @@ -1604,20 +1625,16 @@ async def _on_layout(self, message: messages.Layout) -> None: self._layout_widgets[ancestor] = set() if widget not in self._layout_widgets: self._layout_widgets[ancestor].add(widget) - layout_required = True if not ancestor.styles.auto_dimensions: break widget = ancestor - if not self._layout_required: - # A prior frame keeps held requests but consumes its timer intent. - # Membership is not a new-source notification: after release the - # SAME pending request becomes actionable under current roots. - if layout_required or self._has_actionable_layout_requests( - self._held_layout_requests() - ): - self._layout_required = True - self.check_idle() + # Next callbacks can follow this message before Idle; record the + # original pending layout intent before their sender admission. + self._request_layout( + self._has_actionable_layout_requests(self._held_layout_requests()) + ) + self.check_idle() async def _on_update_scroll(self, message: messages.UpdateScroll) -> None: message.stop() diff --git a/tests/test_batch_paint_admission.py b/tests/test_batch_paint_admission.py index 138c3b3941..fb649ef2e5 100644 --- a/tests/test_batch_paint_admission.py +++ b/tests/test_batch_paint_admission.py @@ -1,7 +1,7 @@ """Real framework lifecycle checks, not installed terminal acceptance.""" import asyncio -from contextlib import nullcontext +from contextlib import asynccontextmanager, nullcontext import pytest @@ -138,7 +138,7 @@ class HeldVertical(Vertical): """A real changing subtree refuses layout and paint until its owner releases.""" def arrange(self, size, optimal=False): - assert not self.lock.is_locked, "Layout descended into a mutation" + assert self not in self.screen._layout_mutation_roots(), "Layout descended into a mutation" return super().arrange(size, optimal=optimal) def render_lines(self, crop): @@ -150,12 +150,25 @@ class IndependentScreen(ObservedScreen): def __init__(self): super().__init__() self.publications = [] + self.held_geometry = {} + + @asynccontextmanager + async def preserve_layout(self, root): + async with root.lock: + previous = self.held_geometry + placement = self._compositor.acquire_subtree_geometry(root) + self.held_geometry = {**previous, root: placement} + try: + yield placement + finally: + self.held_geometry = previous + self.check_idle() def _layout_mutation_roots(self): - return tuple(root for root in self.query(HeldVertical) if root.lock.is_locked) + return self.held_geometry def _prepare_compositor_refresh(self): - return self._layout_mutation_roots() + return tuple(self._layout_mutation_roots()) def _on_frame_published(self, deferred_roots): self.publications.append(deferred_roots) @@ -186,7 +199,7 @@ async def test_released_pending_layout_resumes_on_its_existing_owner_request(): holder = app.query_one(HeldVertical) original = app.query_one("#original") committed = screen._compositor.find_widget(original) - async with holder.lock: + async with holder.screen.preserve_layout(holder): new = Label("RELEASED_SOURCE", id="released") await holder.mount(new) # A child source change makes the holder itself a pending owner, @@ -199,15 +212,76 @@ async def test_released_pending_layout_resumes_on_its_existing_owner_request(): assert new not in screen._compositor._published_map # Subscribe only after the held frame, then exercise the SAME owner's - # refresh. No sidebar update, geometry query or manual timer wakeup. + # idle wakeup. No new source invalidation, geometry query or timer. screen.screen_layout_refresh_signal.subscribe(app, lambda _: completed.set()) - holder.refresh(layout=True) + screen.check_idle() await asyncio.wait_for(completed.wait(), 2) assert new in screen._compositor._published_map assert not screen._layout_widgets assert not app._exception +async def test_released_layout_fences_queued_callbacks_before_idle(): + app = IndependentApp() + completed = asyncio.Event() + admitted = asyncio.Event() + order: list[str] = [] + async with app.run_test(size=(40, 8)) as pilot: + await pilot.pause() + screen = app.screen + original = app.query_one("#original") + holder = app.query_one(HeldVertical) + transaction = screen.preserve_layout(holder) + entered = asyncio.Event() + + async def enter() -> None: + await transaction.__aenter__() + entered.set() + + def complete() -> None: + assert asyncio.current_task() is original.task + order.append("completion") + completed.set() + + screen.call_later(enter) + await asyncio.wait_for(entered.wait(), 2) + # A genuine geometry-only refresh leaves no paint damage to mask + # the pending layout fence. The sender still awaits that request. + original.refresh(repaint=False, layout=True) + original.call_after_refresh(complete) + await pilot.pause() + assert screen._layout_widgets + assert not screen._refresh_pending + assert any(sender is original for _, sender in screen._callbacks) + + def queued_admission() -> None: + assert asyncio.current_task() is screen.task + assert screen._layout_widgets and not screen._layout_mutation_roots() + screen._invoke_and_clear_callbacks() + assert screen._layout_required + assert any(sender is original for _, sender in screen._callbacks) + order.append("queued admission") + admitted.set() + + screen.screen_layout_refresh_signal.subscribe( + app, lambda _: order.append("layout") + ) + + async def release() -> None: + # Enter and release on the same native pump. Its ordinary callback + # message owns this release and queues admission before Idle. + await transaction.__aexit__(None, None, None) + order.append("message") + screen.call_next(queued_admission) + + screen.call_later(release) + await asyncio.wait_for(admitted.wait(), 2) + await asyncio.wait_for(completed.wait(), 2) + assert order[:2] == ["message", "queued admission"] + assert order.index("layout") < order.index("completion") + assert not screen._layout_widgets and not app._exception + + @pytest.mark.parametrize("held", [False, True]) async def test_idle_shares_acquired_roots_and_paint_reacquires(held): from textual import events @@ -248,7 +322,7 @@ def get_default_screen(self): assert len(screen.acquisitions) > before await pilot.pause() - async with (holder.lock if held else nullcontext()): + async with (screen.preserve_layout(holder) if held else nullcontext()): assert not screen._refresh_pending compositor._dirty_regions.add((holder if held else sidebar).region) screen._invoke_later(completed.set, app) @@ -294,7 +368,7 @@ async def test_partial_publication_keeps_geometry_and_owner_callbacks(): original_hit = compositor.get_widget_at(15, 1)[0] app.frames.clear() screen.publications.clear() - async with holder.lock: + async with holder.screen.preserve_layout(holder): await holder.mount(Label("NEW_HELD", id="new")) sidebar.update("SIDEBAR_NEW") original.call_after_refresh(held_done.set) @@ -380,7 +454,7 @@ async def test_inline_partial_cells_preserve_held_content_and_clear_after_releas await pilot.pause() compositor = app.screen._compositor holder = app.query_one(HeldVertical) - async with holder.lock: + async with holder.screen.preserve_layout(holder): update = compositor.render_inline(app.size, clear=True, excluded_regions=compositor.deferred_regions((holder,))) assert isinstance(update, InlineUpdate) @@ -406,7 +480,7 @@ async def test_partial_translucent_publication_never_reads_held_backdrop(): await app.push_screen(ModalScreen()) await pilot.pause() foreground = app.screen - async with holder.lock: + async with holder.screen.preserve_layout(holder): compositor = foreground._compositor compositor._dirty_regions.add(compositor.size.region) regions = source._compositor.deferred_regions((holder,)) diff --git a/tests/test_compositor.py b/tests/test_compositor.py index 202ca38f85..ab7568de97 100644 --- a/tests/test_compositor.py +++ b/tests/test_compositor.py @@ -248,7 +248,7 @@ def compose(self): await pilot.pause() compositor = app.screen._compositor geometry = compositor.full_map - captured, _ = compositor._arrange_root( + captured, _, _ = compositor._arrange_root( inner, app.screen.size, visible_only=False, root_geometry=geometry[inner], ) diff --git a/tests/test_compositor_frame_damage.py b/tests/test_compositor_frame_damage.py index dba0f4b48f..e4dd83edec 100644 --- a/tests/test_compositor_frame_damage.py +++ b/tests/test_compositor_frame_damage.py @@ -212,7 +212,8 @@ def compose(self): body = app.query_one("#body") [(original, placement)] = list(compositor.published_geometry([body])) assert placement.region.y == 4 - size, strips = compositor.render_subtree_strips(original, placement) + size, bands = compositor.render_subtree_strips(original, placement, admit=lambda participants: True) + strips = [strip for band in bands for strip in band] assert size == Size(12, 3) assert [strip.text.rstrip() for strip in strips] == ["ABC界DEF", "SECOND_ROW", "THIRD_ROW"] @@ -226,13 +227,14 @@ def compose(self): assert compositor.visible_widgets[body] == ( placement.region, Region(0, 0, 10, 2), ) - geometry, _ = compositor._arrange_root( + geometry, _, _ = compositor._arrange_root( original, compositor.size, visible_only=False, root_geometry=placement, ) paint = compositor._paint_regions(compositor._ordered_geometry(geometry), placement.region) assert paint[body] == (placement.region, placement.region) assert compositor._cuts_for_regions(placement.region, paint) == [[-2, 10]] * 3 - size, strips = compositor.render_subtree_strips(original, placement) + size, bands = compositor.render_subtree_strips(original, placement, admit=lambda participants: True) + strips = [strip for band in bands for strip in band] assert size == Size(12, 3) assert [strip.text.rstrip() for strip in strips] == ["ABC界DEF", "SECOND_ROW", "THIRD_ROW"] @@ -313,7 +315,7 @@ def compose(self): publication = compositor._visible_map assert far not in publication [(original, placement)] = compositor.published_geometry((body,)) - capture, _ = compositor._arrange_root( + capture, _, _ = compositor._arrange_root( original, compositor.size, visible_only=False, root_geometry=placement, ) assert far in capture and omitted not in capture @@ -330,7 +332,7 @@ def compose(self): assert compositor._full_map_invalidated # A failed nested paint must restore the enclosing original resource. - nested, _ = compositor._arrange_root( + nested, _, _ = compositor._arrange_root( far, compositor.size, visible_only=False, root_geometry=capture[far], ) with pytest.raises(RuntimeError, match="native renderer failed"): diff --git a/tests/test_native_scene_size_publication.py b/tests/test_native_scene_size_publication.py index f656070379..cff07396cc 100644 --- a/tests/test_native_scene_size_publication.py +++ b/tests/test_native_scene_size_publication.py @@ -1,6 +1,7 @@ """Exercise scene transitions and size receipts through original native Apps.""" from textual import events +from tests.test_batch_paint_admission import IndependentScreen from textual.app import App from textual.containers import VerticalScroll from textual.geometry import Size @@ -9,12 +10,10 @@ async def test_scroll_intent_survives_layout_and_held_sources(): - from textual.screen import Screen history = VerticalScroll(Static("\n".join(str(n) for n in range(50)))) - class IntentScreen(Screen): - held = False + class IntentScreen(IndependentScreen): def __init__(self): super().__init__() @@ -23,12 +22,6 @@ def __init__(self): def compose(self): yield history - def _layout_mutation_roots(self): - return (history,) if self.held else () - - def _prepare_compositor_refresh(self): - return self._layout_mutation_roots() - def _refresh_layout(self, size=None, scroll=False): self.admissions.append(( scroll, self._has_actionable_layout_requests(self._held_layout_requests()) @@ -46,7 +39,7 @@ def get_default_screen(self): await pilot.pause() screen = app.screen original = screen._compositor.find_widget(history.children[0]) - screen.held = True + screen.held_geometry = {history: screen._compositor.acquire_subtree_geometry(history)} screen.admissions.clear() history.refresh(layout=True) history.scroll_to(y=10, animate=False, immediate=True) @@ -55,7 +48,8 @@ def get_default_screen(self): assert (True, False) in screen.admissions assert screen._compositor.find_widget(history.children[0]) == original assert screen._layout_widgets - screen.held = False + screen.held_geometry = {} + screen.check_idle() screen.admissions.clear() history.refresh(layout=True) history.scroll_to(y=15, animate=False, immediate=True) diff --git a/tests/test_subtree_geometry_cache.py b/tests/test_subtree_geometry_cache.py index 03e7ec30ad..8c6564b928 100644 --- a/tests/test_subtree_geometry_cache.py +++ b/tests/test_subtree_geometry_cache.py @@ -1,8 +1,10 @@ +import asyncio from unittest.mock import patch import pytest from textual._compositor import Compositor +from tests.test_batch_paint_admission import IndependentScreen from textual.app import App from textual.containers import VerticalGroup from textual.widgets import Static @@ -12,6 +14,96 @@ class CachedGroup(VerticalGroup): CACHE_SUBTREE_GEOMETRY = True +async def test_retained_positions_reuse_covered_partial_source_and_acquire_missing_paths(): + from textual.containers import VerticalScroll + + class ObservedGroup(CachedGroup): + arrangements = 0 + + def arrange(self, size, optimal=False): + self.arrangements += 1 + return super().arrange(size, optimal=optimal) + + rows = [Static(f"ORIGINAL_ROW_{index:03}") for index in range(100)] + source = ObservedGroup(*rows, id="source") + + class PositionScreen(IndependentScreen): + CSS = """ + VerticalScroll { height: 8; } + #source { height: 100; width: 1fr; } + Static { height: 1; } + """ + targets = () + + def compose(self): + yield VerticalScroll(source, id="scroller") + + def _use_viewport_layout(self): + return True + + def _layout_geometry_targets(self): + return self.targets + + app = App() + async with app.run_test(size=(40, 12)) as pilot: + screen = PositionScreen() + await app.push_screen(screen) + await pilot.pause() + compositor = screen._compositor + partial = compositor._subtree_geometry[source] + assert not partial.complete and partial.contains(rows[0]) + assert not partial.contains(rows[90]) + + def compare_original_geometry(): + reference = Compositor(max_subtree_geometry_entries=0) + reference.reflow_visible(screen, app.size, retain_geometry=screen.targets) + assert compositor._published_map == reference._published_map + assert tuple(compositor.render_strips()) == tuple(reference.render_strips()) + + # The partial source owns this existing position. Naming it does not + # turn the request into a complete offscreen descendant acquisition. + screen.targets = (rows[0],) + source.arrangements = 0 + screen._refresh_layout(scroll=True) + assert source.arrangements == 0 + assert compositor._subtree_geometry[source] is partial + assert not partial.matches(partial.key, require_complete=True, + required_geometry=screen.targets) + compare_original_geometry() + + # A newly requested missing path must be acquired. The resulting + # partial source owns both explicit positions on the following frame. + for targets in ((rows[0], rows[90]), (rows[90], rows[40])): + screen.targets = targets + source.arrangements = 0 + screen._refresh_layout(scroll=True) + assert source.arrangements > 0 + acquired = compositor._subtree_geometry[source] + assert not acquired.complete and all(acquired.contains(row) for row in targets) + compare_original_geometry() + source.arrangements = 0 + screen._refresh_layout(scroll=True) + assert source.arrangements == 0 + assert compositor._subtree_geometry[source] is acquired + compare_original_geometry() + + # Placement and child mutation still retire exact partial answers. + scroller = screen.query_one("#scroller", VerticalScroll) + for position in (15, 0): + scroller.scroll_to(y=position, animate=False, immediate=True) + await pilot.pause() + screen._refresh_layout(scroll=True) + compare_original_geometry() + before = compositor._subtree_geometry[source] + rows[40].styles.height = 3 + await pilot.pause() + screen._refresh_layout(scroll=True) + assert compositor._subtree_geometry[source] is not before + assert compositor._published_map[rows[40]].region.height == 3 + compare_original_geometry() + assert not app._exception + + @pytest.mark.parametrize("capacity", [0, 1, 2, 5]) async def test_geometry_budget_bounds_retention_without_changing_the_scene(capacity): app = App() @@ -56,22 +148,15 @@ async def test_changed_parent_borrows_original_children_without_flat_capture(): from dataclasses import replace from textual._compositor import PlacedSubtreeGeometry, SubtreeGeometryPlacement from textual.containers import VerticalScroll - from textual.screen import Screen rows = [CachedGroup(Static(f"row {index}"), Static("wrapped " * 7)) for index in range(20)] history = CachedGroup(*rows) - class SourceScreen(Screen): + class SourceScreen(IndependentScreen): def compose(self): yield VerticalScroll(history) - def _layout_mutation_roots(self): - return (history,) if history.lock.is_locked else () - - def _prepare_compositor_refresh(self): - return self._layout_mutation_roots() - class SourceApp(App): CSS = "VerticalScroll { height: 10; } Static { height: auto; }" @@ -157,7 +242,7 @@ def required_scene(source): )[1] assert compositor.find_widget(child) == reference.find_widget(child) held_source = compositor._subtree_geometry[history] - async with history.lock: + async with app.screen.preserve_layout(history): incomplete = CachedGroup(Static("NOT_COMMITTED")) await history.mount(incomplete) await pilot.pause() @@ -185,3 +270,97 @@ def required_scene(source): def test_geometry_budget_rejects_invalid_capacities(capacity): with pytest.raises(ValueError, match="non-negative integer"): Compositor(max_subtree_geometry_entries=capacity) + + +async def test_visible_source_stays_partial_and_mutation_keeps_explicit_complete_loan(): + from textual._compositor import PlacedSubtreeGeometry, IntrinsicSubtreeGeometry + from textual.containers import VerticalScroll + + rows = [CachedGroup(Static(f"ROW_{index:03}")) for index in range(120)] + history = CachedGroup(*rows, id="history-source") + + class ViewportScreen(IndependentScreen): + CSS = "VerticalScroll { height: 8; } Static { height: 1; }" + + def compose(self): + yield VerticalScroll(history, id="scroller") + yield Static("UNRELATED_PAINT", id="sidebar") + + def _use_viewport_layout(self): + return True + + app = App() + async with app.run_test(size=(40, 12)) as pilot: + screen = ViewportScreen() + await app.push_screen(screen) + await pilot.pause() + compositor = screen._compositor + assert compositor.acquire_subtree_geometry(Static("UNPLACED")) is None + partial = compositor._subtree_geometry[history] + assert isinstance(partial, PlacedSubtreeGeometry) + assert not partial.complete and not partial.contains(rows[-1]) + original_scene = dict(compositor._published_map) + compositor.max_subtree_geometry_entries = 0 + completed = asyncio.Event() + async with screen.preserve_layout(history) as placement: + assert placement.source.complete + assert isinstance(placement.source, IntrinsicSubtreeGeometry) + assert placement.source.contains(rows[-1].children[0]) + assert not compositor._subtree_geometry + assert compositor._published_map == original_scene + added = CachedGroup(Static("UNCOMMITTED")) + await history.mount(added) + history.call_after_refresh(completed.set) + screen.query_one("#sidebar").update("UNRELATED_CHANGED") + scroller = screen.query_one("#scroller", VerticalScroll) + for position in (70, 5, 115, 0): + scroller.scroll_to(y=position, animate=False, immediate=True) + await pilot.pause() + assert added not in compositor._published_map + rendered = '\n'.join(strip.text for strip in compositor.render_strips()) + assert "UNCOMMITTED" not in rendered + assert "UNRELATED_CHANGED" in rendered + assert not completed.is_set() + await asyncio.wait_for(completed.wait(), 2) + assert added in compositor.widgets + assert not screen._layout_widgets and not app._exception + + +async def test_nested_source_expansion_borrows_original_held_child_before_sibling_arrangement(): + from textual.containers import VerticalScroll + + original = Static("ORIGINAL_CHILD") + child = CachedGroup(original) + sibling = CachedGroup(Static("UNMODIFIED_SIBLING")) + history = CachedGroup(child, sibling) + + class SourceScreen(IndependentScreen): + def compose(self): + yield VerticalScroll(history) + + app = App() + async with app.run_test(size=(30, 8)) as pilot: + screen = SourceScreen() + await app.push_screen(screen) + await pilot.pause() + compositor = screen._compositor + async with screen.preserve_layout(child) as first: + incoming = Static("NEW_UNCOMMITTED_CHILD") + await child.mount(incoming) + await pilot.pause() + assert compositor.acquire_subtree_geometry(original) is None + expanded = compositor.acquire_subtree_geometry(history) + assert expanded is not None and expanded.source.complete + assert expanded.source.contains(original) + assert expanded.source.contains(sibling.children[0]) + assert not expanded.source.contains(incoming) + loan = expanded.source.geometry[child][1] + assert loan.source is first.source and loan.source_held + previous = screen.held_geometry + screen.held_geometry = {history: expanded} + screen.query_one(VerticalScroll).scroll_to(y=1, animate=False, immediate=True) + await pilot.pause() + assert incoming not in compositor._published_map + screen.held_geometry = previous + await pilot.pause() + assert incoming in compositor.widgets and not app._exception diff --git a/tests/test_viewport_geometry_targets.py b/tests/test_viewport_geometry_targets.py index e53b24dc78..86c1ad3e22 100644 --- a/tests/test_viewport_geometry_targets.py +++ b/tests/test_viewport_geometry_targets.py @@ -1,13 +1,153 @@ import asyncio from unittest.mock import patch +import pytest + from textual import errors +from tests.test_batch_paint_admission import IndependentScreen from textual.app import App from textual.containers import VerticalGroup, VerticalScroll from textual.screen import Screen from textual.widgets import Static +async def test_capture_admits_original_complete_paint_cohort_and_refuses_hidden_children(): + from textual.widgets import Collapsible + + shown = Static("\n".join(f"PAINT_{row}" for row in range(24))) + hidden = Static("HIDDEN_RESOURCE") + expanded = Collapsible(shown, title="Expanded", collapsed=False) + collapsed = Collapsible(hidden, title="Collapsed") + footer = Static("Session details") + + class Body(VerticalGroup): + CACHE_SUBTREE_GEOMETRY = True + + body = Body(expanded, collapsed, footer) + + class CaptureScreen(Screen): + def _use_viewport_layout(self): + return True + + def compose(self): + with VerticalScroll(): + yield body + + app = App() + async with app.run_test(size=(60, 8)) as pilot: + await app.push_screen(CaptureScreen()) + await pilot.pause() + compositor = app.screen._compositor + _, placement = next(compositor.published_geometry((body,))) + published = compositor._published_map + seen = [] + + def refuse(participants): + seen.append(tuple(participants)) + assert shown in participants + assert hidden not in participants + assert collapsed.query_one(Collapsible.Contents) not in participants + assert footer in participants + # Complete capture admits the actual offscreen paragraph, without + # arranging a second scene when its geometry is read by admission. + assert participants[shown][1].height == 24 + assert compositor.find_widget(footer).region.y >= app.size.height + return False + + assert compositor.render_subtree_strips(body, placement, admit=refuse) is None + assert len(seen) == 1 + assert compositor._published_map is published + assert compositor._render_geometry is None + + expanded.collapsed = True + await pilot.pause() + _, current = next(compositor.published_geometry((body,))) + assert current.region.height < placement.region.height + + def admit(participants): + assert shown not in participants + assert hidden not in participants + assert expanded._title in participants + assert collapsed._title in participants + assert footer in participants + return True + + result = compositor.render_subtree_strips(body, current, admit=admit) + assert result is not None + size, bands = result + strips = [strip for band in bands for strip in band] + assert size == current.region.size + assert len(strips) == size.height + assert not any("PAINT_" in row.text or "HIDDEN_RESOURCE" in row.text for row in strips) + assert any("Session details" in row.text for row in strips) + assert compositor._render_geometry is None + + +async def test_banded_capture_releases_geometry_for_input_and_unrelated_paint(): + from textual.geometry import Region + + class PaintedRows(Static): + def __init__(self): + super().__init__("\n".join(f"ROW_{row:02} 界" for row in range(37))) + self.crops = [] + + def render_lines(self, crop): + self.crops.append(crop) + return super().render_lines(crop) + + body = PaintedRows() + status = Static("INPUT_0", id="status") + + class CaptureScreen(Screen): + CSS = "#status { height: 1; } VerticalScroll { height: 1fr; }" + + def _use_viewport_layout(self): + return True + + def compose(self): + yield status + with VerticalScroll(): + yield body + + class CaptureApp(App): + received = 0 + + def on_key(self, event): + if event.key == "a": + self.received += 1 + status.update(f"INPUT_{self.received}") + + app = CaptureApp() + async with app.run_test(size=(40, 8)) as pilot: + await app.push_screen(CaptureScreen()) + await pilot.pause() + compositor = app.screen._compositor + _, placement = next(compositor.published_geometry((body,))) + published = compositor._published_map + body.crops.clear() + size, bands = compositor.render_subtree_strips(body, placement, admit=lambda participants: True) + assert body.crops == [] # Acquisition does not paint the complete body. + assert compositor._published_map is published + assert compositor._render_geometry is None + + rows = [] + for band in bands: + assert 0 < len(band) <= app.size.height + assert compositor._render_geometry is None + rows.extend(band) + if len(rows) < size.height: + # Real key delivery and visible paint may complete while the + # original offscreen capture still owns unfinished rows. + await pilot.press("a") + assert f"INPUT_{app.received}" in compositor.render_strips()[0].text + assert compositor.find_widget(status).region == Region(0, 0, 40, 1) + assert app.received == 4 + assert len(rows) == size.height == 37 + assert [row.text.rstrip() for row in rows] == [f"ROW_{row:02} 界" for row in range(37)] + assert all(crop.height <= app.size.height for crop in body.crops) + assert compositor._render_geometry is None + + def test_borrowed_geometry_routes_preserve_membership_ancestry_and_assignment_order(): from types import MappingProxyType @@ -43,7 +183,7 @@ def source(owner, y): key, MappingProxyType({ first: (0, SubtreeGeometryPlacement(first_source, key, clip, root)), second: (1, SubtreeGeometryPlacement(second_source, key, clip, root)), - root: (2, placed(9)), + root: (2, SubtreeMapGeometry(placed(9), (), None)), }), frozenset((root,)), frozenset(), ) assert resource.contains(shared) @@ -90,7 +230,8 @@ def compose(self): assert tables[-1] not in compositor._published_map _, placement = next(compositor.published_geometry((body,))) published = compositor._full_map, compositor._visible_map - _, strips = compositor.render_subtree_strips(body, placement) + _, bands = compositor.render_subtree_strips(body, placement, admit=lambda participants: True) + strips = [strip for band in bands for strip in band] rows = [strip.text for strip in strips] for index in range(12): row = next(row for row in rows if f"TABLE_{index:02}" in row) @@ -100,7 +241,7 @@ def compose(self): assert tables[-1].outer_size.height == 0 -class TargetedScreen(Screen): +class TargetedScreen(IndependentScreen): CSS = "VerticalScroll { width: 1fr; } VerticalGroup, Static { height: auto; }" targets = () @@ -126,7 +267,7 @@ class HeldBody(VerticalGroup): CACHE_SUBTREE_GEOMETRY = True def arrange(self, size, optimal=False): - assert not self.lock.is_locked, "Read changing body layout" + assert self not in self.screen._layout_mutation_roots(), "Read changing body layout" return super().arrange(size, optimal=optimal) rows = [Static(f"COMMITTED_ROW_{index:02}", id=f"held-row-{index}") @@ -140,9 +281,6 @@ class HeldScreen(TargetedScreen): #outside-body { height: 30; } """ - def _layout_mutation_roots(self): - return (body,) if body.lock.is_locked else () - def compose(self): with VerticalScroll(id="history"): yield body @@ -156,7 +294,7 @@ def compose(self): await pilot.pause() history = screen.query_one("#history", VerticalScroll) compositor = screen._compositor - resource = compositor._subtree_geometry[body] + resource = compositor.acquire_subtree_geometry(body).source assert isinstance(resource, IntrinsicSubtreeGeometry) assert all(resource.contains(row) for row in rows) # Holding revisions does not permit resizing or changing the source's @@ -188,7 +326,7 @@ def compose(self): completed = asyncio.Event() unrelated = asyncio.Event() - async with body.lock: + async with app.screen.preserve_layout(body): incomplete = Static("UNCOMMITTED_CHILD", id="incomplete") await body.mount(incomplete) body.call_after_refresh(completed.set) @@ -235,7 +373,7 @@ class HeldBody(VerticalGroup): CACHE_SUBTREE_GEOMETRY = True def arrange(self, size, optimal=False): - assert not self.lock.is_locked, "Read changing overlay layout" + assert self not in self.screen._layout_mutation_roots(), "Read changing overlay layout" return super().arrange(size, optimal=optimal) overlay = Static("SCREEN_OVERLAY", id="overlay") @@ -249,9 +387,6 @@ def compose(self): with VerticalScroll(id="history"): yield body - def _layout_mutation_roots(self): - return (body,) if body.lock.is_locked else () - app = App() async with app.run_test(size=(40, 10)) as pilot: screen = HeldScreen() @@ -259,10 +394,10 @@ def _layout_mutation_roots(self): await app.push_screen(screen) await pilot.pause() compositor = screen._compositor - resource = compositor._subtree_geometry[body] + resource = compositor.acquire_subtree_geometry(body).source assert isinstance(resource, PlacedSubtreeGeometry) original = dict(compositor._published_map) - async with body.lock: + async with app.screen.preserve_layout(body): await body.mount(Static("INCOMPLETE")) screen.query_one("#history").scroll_to(y=10, animate=False, immediate=True) await pilot.pause() @@ -280,14 +415,15 @@ def _layout_mutation_roots(self): assert not app._exception -async def test_held_original_ancestry_honors_a_nested_self_painting_owner(): +@pytest.mark.parametrize("screen_overlay", [False, True]) +async def test_held_original_ancestry_honors_a_nested_self_painting_owner(screen_overlay): from textual.content import Content class HeldGroup(VerticalGroup): CACHE_SUBTREE_GEOMETRY = True def arrange(self, size, optimal=False): - assert not self.lock.is_locked, "Read changing child topology" + assert self not in self.screen._layout_mutation_roots(), "Read changing child topology" return super().arrange(size, optimal=optimal) class ContentBody(Static): @@ -306,6 +442,8 @@ def render_lines(self, crop): body = ContentBody("INITIAL_NATIVE_PARENT", id="content-body") old = ChangingChild("OLD_NATIVE_CHILD") + if screen_overlay: + old.styles.overlay = "screen" class ContentScreen(TargetedScreen): CSS = "#content-body { height: 18; } Static { height: 2; }" @@ -314,9 +452,6 @@ def compose(self): with VerticalScroll(id="history"): yield holder - def _layout_mutation_roots(self): - return (holder,) if holder.lock.is_locked else () - app = App() async with app.run_test(size=(40, 10)) as pilot: screen = ContentScreen() @@ -326,10 +461,10 @@ def _layout_mutation_roots(self): await body.mount(old) await pilot.pause() compositor = screen._compositor - resource = compositor._subtree_geometry[holder] + resource = compositor.acquire_subtree_geometry(holder).source assert resource.contains(old) assert resource.captured_parent(old) is body - async with holder.lock: + async with app.screen.preserve_layout(holder): body.update(Content("\n".join(["ACQUIRED_ROOT_PAINT"] * 18))) await body.mount(ChangingChild("UNCOMMITTED_DESCENDANT")) screen.query_one("#history").scroll_to(y=4, animate=False, immediate=True) @@ -382,8 +517,8 @@ def compose(self): screen._refresh_layout(app.size, scroll=True) # The overlay's clip reaches the outer screen, so this resource # keeps placed geometry rather than manufacturing an intrinsic clip. - assert isinstance(compositor._subtree_geometry[body], PlacedSubtreeGeometry) - expected, _ = Compositor(max_subtree_geometry_entries=0)._arrange_root( + assert isinstance(compositor.acquire_subtree_geometry(body).source, PlacedSubtreeGeometry) + expected, _, _ = Compositor(max_subtree_geometry_entries=0)._arrange_root( screen, app.size, visible_only=False, ) paint = compositor._paint_regions(compositor._ordered_geometry(expected), app.size.region) @@ -420,7 +555,7 @@ def compose(self): await pilot.pause() history = screen.query_one("#history", VerticalScroll) compositor = screen._compositor - resource = compositor._subtree_geometry[body] + resource = compositor.acquire_subtree_geometry(body).source assert all(resource.contains(row) for row in rows) for position in (0, 200, 0): @@ -441,7 +576,8 @@ def compose(self): assert rows[235] not in {node for _, node in candidates} assert len(candidates) < len(resource.geometry) # Capture remains complete without replacing the published viewport. - size, strips = compositor.render_subtree_strips(body, placement) + size, bands = compositor.render_subtree_strips(body, placement, admit=lambda participants: True) + strips = [strip for band in bands for strip in band] assert size.height == 240 assert all(f"Original row {index}" in strip.text for index, strip in enumerate(strips)) @@ -564,7 +700,8 @@ def render_row(crop): return render_lines(crop) with patch.object(compositor, "_arrange_root", side_effect=arrange_body), patch.object(row, "render_lines", side_effect=render_row): - size, strips = compositor.render_subtree_strips(body, placement) + size, bands = compositor.render_subtree_strips(body, placement, admit=lambda participants: True) + strips = [strip for band in bands for strip in band] assert size == bounds.size assert len(strips) == size.height assert all(strip.cell_length == size.width for strip in strips) @@ -595,7 +732,8 @@ def render_with_hidden_query(crop): return render_lines(crop) with patch.object(compositor, "_arrange_root", side_effect=arrange_body), patch.object(row, "render_lines", side_effect=render_with_hidden_query): - compositor.render_subtree_strips(body, placement) + _, bands = compositor.render_subtree_strips(body, placement, admit=lambda participants: True) + tuple(bands) assert scoped_misses assert compositor._render_geometry is None assert compositor._full_map is published[0] @@ -605,7 +743,8 @@ def render_with_hidden_query(crop): # ordinary screen queries with their original publication. with patch.object(row, "render_lines", side_effect=ValueError("render failed")): try: - compositor.render_subtree_strips(body, placement) + _, bands = compositor.render_subtree_strips(body, placement, admit=lambda participants: True) + tuple(bands) except ValueError as error: assert str(error) == "render failed" else: