Skip to content

Progress

matchlab.progress

Progress reporting for a collection.

collect() walks a plan and runs the steps whose artifact is not already stored. This module shows that walk while it runs.

The tree is what makes a position mean anything. Steps have no names. They are identified by position (matchlab.lineage.number), so a line reading [step 7] Ran in 0.041s means nothing on its own. You need the tree to know which node 7 is. Everything here follows from that. The plan has to be put somewhere findable, and the per-step detail has to quote the position.

A collection draws the plan in exactly one place. Never two.

  • A live tree, at a terminal or in a notebook. One frame, redrawn in place, so a fifty-step plan doesn't scroll the shell away. The row that lights up is the row numbered [7], so the frame carries both the key and the progress at once. It stays on screen throughout, and is left there in full when the run ends, so a [step 7] in the log beside it always has something to resolve against.
  • A logged tree, when nothing is drawn, in CI, a job runner, a redirected stream. One multi-line record, the way a traceback is, so it can't be interleaved apart.

A drawn tree already answers what a logged one would, so a collection never prints both. Printing the plan twice into the same screen would just be noise.

The per-step records are logged either way, each prefixed with its position, along with a closing summary of what the collection did and what the store now costs. They land on the matchlab logger.

Which of the two you get is the interactive argument to Step.collect, and that argument is honoured as given. It is named for the assumption behind it (someone is watching) rather than the widget it produces. Drawing means someone is watching, so the plan can be a thing on screen that the session throws away. interactive=False is what puts the tree in the log instead, for a run whose output outlives the session.

Live output and logging share a terminal automatically. A handler holding a raw stream would write straight past Rich and land in the middle of the frame being redrawn. That describes the handler logging.basicConfig() builds, the one nearly every caller has. A report therefore borrows those handlers for as long as it owns the terminal, points them at the console, and puts them back afterwards. See matchlab.core.logging.through_console. Records interleave above the frame instead of through it, with no extra setup required. ConsoleHandler does the same thing up front, for a handler you are building yourself.

A plan taller than the terminal is windowed, not cropped. Rich would otherwise crop a live region to its first rows (rich.live_render), which hides the very step you are waiting on. The frame instead shows the rows around whichever step is running, captioned with how much sits either side. The window follows the run down the tree, and the last frame, with nothing left to redraw in place, shows the tree in full.

The terminal decides only how much of the frame you see. It has no say in whether a tree is drawn, or in what the per-step records say.

Step.collect decides what runs, and reports what happened. This module only shows it.

Classes:

  • Progress

    Receives a collection's events and reports them.

Functions:

  • report

    Build the reporter for collecting root.

Progress

Progress(root: Step, steps: Sequence[Step], *, live: bool = False, nested: bool = False, store: Store | None = None)

Receives a collection's events and reports them.

One object owns everything a running collection does besides the work itself. That includes what is drawn, what is logged, which position a record is attributed to, and, while a frame is up, where the application's own handlers write. They are grouped together because they must agree. A frame and a log disagreeing about which step is running would be worse than either on its own.

Used as a context manager by Step.collect. Constructing one costs nothing and reports nothing. begin and end drive it.

Prepare a report for collecting root, whose plan is steps.

Parameters:

  • root

    (Step) –

    The step being collected, the root the tree is drawn from.

  • steps

    (Sequence[Step]) –

    Its plan in walk order, which is what gives each step its position.

  • live

    (bool, default: False ) –

    Draw the tree as a live frame. Independent of what gets logged.

  • nested

    (bool, default: False ) –

    Whether an outer collection is already reporting. A nested report does not claim the console. It logs as any other does.

  • store

    (Store | None, default: None ) –

    Where the collection stores what it computes, so the summary can say how much room it is taking and how much this run added. Optional, since without one the summary simply drops that clause.

Methods:

  • begin

    Mark step as the one now running, and publish its position.

  • end

    Record how step finished, how long it took, and log a line for it.

  • tree

    The plan, as a tree, in whatever state the collection has reached.

  • summary

    One line describing what the collection did, and what it cost to keep.

Attributes:

root instance-attribute

root = root

steps instance-attribute

steps = tuple(steps)

positions instance-attribute

positions = {(id(step)): position for position, step in (enumerate(steps))}

state instance-attribute

state: dict[int, StepState] = {(id(step)): (StepState(status=PENDING)) for step in (steps)}

begin

begin(step: Step) -> None

Mark step as the one now running, and publish its position.

Publishing the position is what attributes the records the step's own code emits, a linker counting matches, a source reading the warehouse. That code cannot name its own position, because a position belongs to the walk, not the step. The walk holds it here, and passes it on.

end

end(step: Step, status: StepStatus) -> None

Record how step finished, how long it took, and log a line for it.

tree

tree() -> str

The plan, as a tree, in whatever state the collection has reached.

summary

summary() -> str

One line describing what the collection did, and what it cost to keep.

Carries the cached count because those per-step records sit at DEBUG. This is where an INFO reader learns what the run skipped.

It also carries the store's size, because nothing else does. A store keeps everything collected into it and lives in a cache directory nobody opens, so the first sign of it is a full disk with nothing to blame. The delta is the part that assigns blame. It says what this run added, which is what connects a size to the edit that caused it.

report

report(root: Step, steps: Sequence[Step], interactive: bool | None, store: Store | None = None) -> Progress

Build the reporter for collecting root.

Parameters:

  • root

    (Step) –

    The step being collected.

  • steps

    (Sequence[Step]) –

    Its plan, in walk order.

  • interactive

    (bool | None) –

    Whether someone is watching, so the plan should be drawn rather than just logged. None, the default, takes a terminal or a notebook as a yes, and anything else as a no. How tall the plan is doesn't enter into it. One too tall to show is windowed, not cropped.

  • store

    (Store | None, default: None ) –

    Where the collection stores what it computes, so the summary can report the store's size and what this run added to it.