At commit 06dbeef, Textualize/textual has 4299 analysed functions, 222 of which fall in the critical band — that’s one in every nineteen functions at the highest risk tier. The top-ranked function, parse in src/textual/_xterm_parser.py, is in the fire quadrant: an activity-weighted risk score of 18.79 means it is both structurally complex and actively changing right now, making it a live regression risk rather than a backlog cleanup item. I would start there, then work through four additional critical functions in markup.py, grid.py, and message_pump.py that haven’t been touched in 61 days — structural debt with a high blast radius when the next development push arrives.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
parse | src/textual/_xterm_parser.py | 18.8 | 70 | 5 | 37 |
_to_content | src/textual/markup.py | 18.1 | 58 | 6 | 27 |
arrange | src/textual/layouts/grid.py | 17.9 | 84 | 5 | 44 |
_get_dispatch_methods | src/textual/message_pump.py | 17.4 | 34 | 7 | 12 |
_process_messages_loop | src/textual/message_pump.py | 17.0 | 39 | 6 | 16 |
Large Repo Analysis
textual is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.
Quadrant overview
4,299 functions analyzed
Of the 4299 functions analysed, 150 fall in the fire quadrant — structurally complex and actively changing. The much larger debt quadrant holds 665 functions: high structural complexity, low recent activity. That ratio tells me the project has accumulated significant structural weight in code that isn’t being actively maintained, while a smaller but consequential set of hot paths is changing under load.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×10Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Exit Heavy×8Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×7God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×6Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Every function in the top five carries the complex_branching and deeply_nested patterns, and seven of the ten flagged functions across the broader analysis are classified as god functions — single functions doing too many distinct jobs. In Python, that matters more than it would in a statically typed language: duck typing means fan-out numbers undercount real coupling, because the types passed at each call site are resolved at runtime, not at the call itself.
Top 5 hotspots
parse — _xterm_parser.py
This is the only fire-quadrant function in the top five, and it earns the top position. parse is a generator-based terminal input parser: from the source excerpt, it yields Read1 or Peek1 read requests and processes the resulting characters, handling bracketed paste mode, escape sequences, key events, and resize events all within a single generator coroutine. That design is efficient, but it concentrates an enormous amount of conditional logic in one place.
A cyclomatic complexity of 70 means there are at least 70 independent execution paths through this function — 70 scenarios that each need their own test case to achieve coverage. The nesting depth of 5, combined with inner function definitions (on_token, on_key_token, reissue_sequence_as_keys) that close over shared mutable state, makes any single path hard to reason about in isolation. Fan-out of 37 is particularly significant here: in Python, those 37 call sites involve runtime type dispatch, so the real coupling surface is wider than the number suggests.
The source excerpt shows the exit_heavy and long_function patterns clearly: the generator loop accumulates logic for paste buffer flushing, escape detection, and key reissue in a single while not self.is_eof body, with multiple return and break exits scattered through nested try/except blocks. With 3 commits in the last 30 days and a last-change date of just 2 days ago, any of those 70 paths could be affected by the current development push.
The file’s bug-fix fraction of 0.286 across 14 total commits tells me that roughly one in four historical commits to this file was fixing something — not alarming in isolation, but worth weighting given the structural complexity. PR review comment density of 1.0 suggests reviewers are engaging carefully.
My recommendation: extract the bracketed-paste state machine, the escape-sequence dispatch, and the key-reissue logic into separate generator stages or helper methods. The goal is to bring each piece below CC 15, which would make independent testing feasible. Even extracting reissue_sequence_as_keys into a module-level function removes a layer of closure-captured state and makes the outer generator shorter.
_to_content — markup.py
This function is in the debt quadrant: it hasn’t been touched in 61 days and has zero commits in the last 30 days. That dormancy is not reassurance — it means the structural complexity has been accumulating interest without anyone paying it down.
_to_content converts a markup string into a Content object. From the source excerpt, it operates as a token-processing loop: it pulls tokens from a MarkupTokenizer, walks a style_stack, accumulates spans and text fragments, and handles template variable substitution via a conditionally-defined process_text lambda. The outer for token in iter_tokens loop contains a secondary inner loop (for token in iter_tokens) for processing open tags — a nested iterator pattern that is particularly hard to follow because it mutates shared state (position, spans, style_stack) across both loops.
CC 58 and ND 6 together mean there are dozens of branching paths operating through up to six levels of nesting. The exit_heavy and god_function patterns confirm that this function is doing the work of several: tokenization error recovery, style normalization, template interpolation, and span accounting all live here. Fan-out of 27 means changes ripple to over two dozen call sites.
The file-level signal worth noting: the bug-fix fraction is 1.0 across 1 total commit — the only historical commit to this file was a bug fix. With zero authors in the last 90 days, no one has touched it in that window. That combination — high structural complexity, zero recent ownership, and a file whose sole commit history is a fix — makes this overdue for refactoring before the next development push.
A close neighbour in the same file, parse_style (CC 77, ND 6, context-only), suggests markup.py as a whole is carrying more parsing complexity than a single module should.
My recommendation: split _to_content along its natural phases: tokenization, style-stack management, and content assembly. Each phase can be a standalone function with its own test coverage. The nested iterator pattern for open-tag processing is the single highest-complexity knot — extracting that into a dedicated _process_open_tag function would immediately reduce both CC and ND.
arrange — grid.py
This is the most structurally complex function in the top five by cyclomatic complexity. arrange is responsible for computing widget positions in a grid layout: from the source excerpt, it reads row and column scalars from parent styles, resolves gutter and keyline settings, handles min_column_width and max_column_width constraints, and then coordinates cell placement through nested helper closures (cell_coords, widget_coords). It also handles viewport size, auto-height detection, and span-aware widget placement — that’s at least five distinct layout concerns in a single method body.
CC 84 means 84 independent execution paths. Fan-out of 44 is the highest in the top five, and in Python’s duck-typed layout system, that number represents broad coupling across widget geometry, style resolution, and layout primitives. A change to any one of those 44 call targets — or to the constraint logic at the top of the function — risks breaking grid layout across every widget that uses it.
The function hasn’t been touched in 61 days, and zero authors have touched it in the last 90 days. This is structural debt, not active churn. But a PR review comment density of 2.0 indicates that when this code has been reviewed, reviewers had material things to say — worth keeping in mind when planning any future change.
My recommendation: decompose arrange by concern. Column and row scalar resolution, gutter/keyline setup, column-count constraint solving, and cell-to-widget assignment are each independent enough to extract. I would start with the min_column_width / max_column_width constraint block, which is self-contained and would reduce the top-level CC meaningfully without requiring changes to the placement logic.
_get_dispatch_methods — message_pump.py
_get_dispatch_methods is the function that walks the Python MRO to find message handlers, yielding (class, callable) pairs for both decorated handlers and naming-convention handlers. The source excerpt shows why ND 7 is the standout metric here: the nesting structure goes for cls in MRO → check _no_default_action → for message_class in message_mro → for method, selectors → for attribute, selector → if not match → else yield. That’s six levels deep before you get to the yield, with an inner for/else at the deepest level that is easy to misread.
CC 34 across ND 7 means many of those 34 paths run through deeply nested conditional chains. In Python’s message dispatch system, bugs at this depth are particularly hard to reproduce because they depend on specific combinations of MRO ordering, message type, decorator presence, and selector matching. The exit_heavy pattern (multiple break and continue exits through the nested structure) compounds the test-coverage burden.
Like the other debt-quadrant functions here, _get_dispatch_methods hasn’t been touched in 61 days. But its PR review comment density of 5.0 — the highest in the top five — tells me that when reviewers looked at this code, it generated substantial discussion. That’s a signal worth taking seriously before the next time someone needs to modify message dispatch behaviour. The bug-fix fraction of 1.0 on a single total commit matches the pattern seen in _to_content: the file’s only recorded commit was a fix.
My recommendation: extract the decorated-handler resolution into its own helper method, separate from the naming-convention fallback. These are two distinct lookup strategies that happen to share a loop variable, and splitting them would halve the nesting depth and make each branch independently testable. A type-annotated helper that takes a message_mro list and a decorated_handlers dict and returns matched methods would also be far easier to unit-test in isolation.
_process_messages_loop — message_pump.py
This is the core async message processing loop for every MessagePump in textual — every widget, screen, and app instance runs through this coroutine. From the source excerpt, it handles message dequeuing, message coalescing (can_replace logic), dispatch, exception handling, idle event injection, and next-callback flushing, all in a single while not self._closed body with layered try/except/finally blocks.
CC 39 and ND 6 reflect the combinatorial complexity of that structure: the outer loop has early-exit paths via MessagePumpClosed and CancelledError, the coalescing inner loop has its own early exits, and the finally block re-enters the dispatch chain to handle idle events. The god_function pattern is apt here — this function is simultaneously a scheduler, a dispatcher, an error boundary, and an idle-event generator.
Fan-out of 16 means that changes to this loop touch a wide cross-section of the framework’s runtime. Because this is Python async code, the real execution interleaving at each await point is invisible in the static fan-out count, making the actual coupling surface wider than 16 suggests.
Like _get_dispatch_methods in the same file, this function hasn’t been touched in 61 days, and the same file-level signals apply: a bug-fix fraction of 1.0, a PR review comment density of 5.0, and zero authors in the last 90 days. The message pump is foundational infrastructure; structural debt here has a high blast radius when it eventually needs to change.
My recommendation: extract the message-coalescing logic and the idle-event injection block into named async helpers. Both are self-contained enough to test independently, and removing them from the main loop body would bring CC down toward a manageable range. The _flush_next_callbacks method that already exists in the source excerpt is a good example of the right extraction pattern — apply the same approach to idle-event dispatch.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 10 |
deeply_nested | 10 |
exit_heavy | 8 |
god_function | 7 |
long_function | 6 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
Reproduce This Analysis
git clone https://github.com/Textualize/textual
cd textual
git checkout 06dbeef4bb70fb718236aa418ed658ef4667a126
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →