Skip to content

step

step #

Step — one span of work: a label, a parent, a start time, and optionally a count.

  1. Open. step(label, total) opens a step beneath the ambient one (a context variable) and makes it ambient for its block, so steps nest the way calls do, and a pmap worker, running in a copy of the caller's context, attaches its steps beneath the caller's.
  2. Advance. advance, track and note change the record under its lock and mark it dirty; nothing else happens on the calling thread.
  3. Watch. The registry tells its watchers when a step opens or closes and hands them a read of any open step when they ask.

active_step = ContextVar('cg_active_step', default=None) module-attribute #

The step the current context is inside, if any; a new step's parent.

Reading dataclass #

A step's count and note at one instant.

Attributes:

Name Type Description
done int

How many units the step has advanced through.

total int | None

How many it will, if known.

message str | None

The last note, if any.

fields dict[str, JSONValue]

The named values the notes carried, latest per name.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
@dataclass(frozen=True, slots=True)
class Reading:
    """A step's count and note at one instant.

    Attributes:
        done: How many units the step has advanced through.
        total: How many it will, if known.
        message: The last ``note``, if any.
        fields: The named values the notes carried, latest per name.
    """

    done: int
    total: int | None
    message: str | None
    fields: dict[str, JSONValue]

Step #

One span of work; step() opens one and the methods below advance it.

Attributes:

Name Type Description
id

A fresh identifier.

parent_id

The ambient step's id when this one opened, if any.

label

What the step is doing, as a page or terminal shows it.

identity

The function identity a memoized call reports, else None.

call_key

The call key a memoized call reports, else None.

workspace

The workspace a memoized call computes in, else None.

scratch Scratch | None

The scratch the step reports to, if one was active when it opened.

started_at

When the step opened, as time.time().

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
class Step:
    """One span of work; ``step()`` opens one and the methods below advance it.

    Attributes:
        id: A fresh identifier.
        parent_id: The ambient step's ``id`` when this one opened, if any.
        label: What the step is doing, as a page or terminal shows it.
        identity: The function identity a memoized call reports, else ``None``.
        call_key: The call key a memoized call reports, else ``None``.
        workspace: The workspace a memoized call computes in, else ``None``.
        scratch: The scratch the step reports to, if one was active when it opened.
        started_at: When the step opened, as ``time.time()``.
    """

    __slots__ = (
        "_dirty",
        "_done",
        "_fields",
        "_lock",
        "_message",
        "call_key",
        "id",
        "identity",
        "label",
        "parent_id",
        "scratch",
        "started_at",
        "total",
        "workspace",
    )

    def __init__(
        self,
        label: str,
        total: int | None = None,
        *,
        identity: str | None = None,
        call_key: CallKey | None = None,
        workspace: Path | None = None,
    ) -> None:
        """A step not yet open, beneath the ambient step and reporting to the active scratch."""
        parent = active_step.get()
        self.id = uuid.uuid4().hex
        self.parent_id = parent.id if parent is not None else None
        self.label = label
        self.total = total
        self.identity = identity
        self.call_key = call_key
        self.workspace = workspace
        self.scratch: Scratch | None = current_scratch()
        self.started_at = time.time()
        self._done = 0
        self._message: str | None = None
        self._fields: dict[str, JSONValue] = {}
        self._dirty = True
        self._lock = threading.Lock()

    def advance(self, count: int = 1) -> None:
        """Count ``count`` more units done."""
        with self._lock:
            self._done += count
            self._dirty = True

    def track[T](self, iterable: Iterable[T]) -> Iterator[T]:
        """``iterable``, advancing this step by one as each element is consumed.

        ```python
        for batch in training.track(batches):
            ...
        ```
        """
        for item in iterable:
            yield item
            self.advance()

    def note(self, message: str | None = None, **fields: JSONValue) -> None:
        """Say what the step is doing: the last message wins, ``fields`` update by name.

        ```python
        training.note(f"loss {loss:.3f}", loss=loss, psnr=psnr)
        ```
        """
        with self._lock:
            if message is not None:
                self._message = message
            self._fields.update(fields)
            self._dirty = True

    @property
    def elapsed(self) -> float:
        """Seconds since the step opened."""
        return time.time() - self.started_at

    def read(self, *, settle: bool = False) -> Reading:
        """The count and note right now; ``settle`` also clears the dirty mark."""
        with self._lock:
            if settle:
                self._dirty = False
            return Reading(self._done, self.total, self._message, dict(self._fields))

    @property
    def dirty(self) -> bool:
        """Whether the step has changed since the last settling ``read``."""
        return self._dirty

    def __repr__(self) -> str:
        """``Step(<label>)``."""
        return f"Step({self.label!r})"

dirty property #

Whether the step has changed since the last settling read.

elapsed property #

Seconds since the step opened.

__init__(label, total=None, *, identity=None, call_key=None, workspace=None) #

A step not yet open, beneath the ambient step and reporting to the active scratch.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def __init__(
    self,
    label: str,
    total: int | None = None,
    *,
    identity: str | None = None,
    call_key: CallKey | None = None,
    workspace: Path | None = None,
) -> None:
    """A step not yet open, beneath the ambient step and reporting to the active scratch."""
    parent = active_step.get()
    self.id = uuid.uuid4().hex
    self.parent_id = parent.id if parent is not None else None
    self.label = label
    self.total = total
    self.identity = identity
    self.call_key = call_key
    self.workspace = workspace
    self.scratch: Scratch | None = current_scratch()
    self.started_at = time.time()
    self._done = 0
    self._message: str | None = None
    self._fields: dict[str, JSONValue] = {}
    self._dirty = True
    self._lock = threading.Lock()

__repr__() #

Step(<label>).

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def __repr__(self) -> str:
    """``Step(<label>)``."""
    return f"Step({self.label!r})"

advance(count=1) #

Count count more units done.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def advance(self, count: int = 1) -> None:
    """Count ``count`` more units done."""
    with self._lock:
        self._done += count
        self._dirty = True

note(message=None, **fields) #

Say what the step is doing: the last message wins, fields update by name.

training.note(f"loss {loss:.3f}", loss=loss, psnr=psnr)
Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def note(self, message: str | None = None, **fields: JSONValue) -> None:
    """Say what the step is doing: the last message wins, ``fields`` update by name.

    ```python
    training.note(f"loss {loss:.3f}", loss=loss, psnr=psnr)
    ```
    """
    with self._lock:
        if message is not None:
            self._message = message
        self._fields.update(fields)
        self._dirty = True

read(*, settle=False) #

The count and note right now; settle also clears the dirty mark.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def read(self, *, settle: bool = False) -> Reading:
    """The count and note right now; ``settle`` also clears the dirty mark."""
    with self._lock:
        if settle:
            self._dirty = False
        return Reading(self._done, self.total, self._message, dict(self._fields))

track(iterable) #

iterable, advancing this step by one as each element is consumed.

for batch in training.track(batches):
    ...
Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def track[T](self, iterable: Iterable[T]) -> Iterator[T]:
    """``iterable``, advancing this step by one as each element is consumed.

    ```python
    for batch in training.track(batches):
        ...
    ```
    """
    for item in iterable:
        yield item
        self.advance()

label_of(function) #

The label a step takes from function: its name.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
def label_of(function: Callable[..., object]) -> str:
    """The label a step takes from ``function``: its name."""
    name = getattr(function, "__name__", None)
    return name if isinstance(name, str) else type(function).__name__

opened(step) #

step open and ambient for the block, registered for the watchers.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
@contextmanager
def opened(step: Step) -> Iterator[Step]:
    """``step`` open and ambient for the block, registered for the watchers."""
    registry.open(step)
    token = active_step.set(step)
    try:
        yield step
    finally:
        active_step.reset(token)
        registry.close(step)

step(label, total=None) #

Open a step beneath the ambient one for the block.

with cg.step("Training splat", total=iterations) as training:
    for batch in training.track(batches):
        ...
        training.note(f"loss {loss:.3f}", loss=loss)

Parameters:

Name Type Description Default
label str

What the block is doing.

required
total int | None

How many units the block will advance through, if known.

None

Yields:

Type Description
Step

The open step.

Source code in capturegraph-lib/capturegraph/recipes/progress/step.py
@contextmanager
def step(label: str, total: int | None = None) -> Iterator[Step]:
    """Open a step beneath the ambient one for the block.

    ```python
    with cg.step("Training splat", total=iterations) as training:
        for batch in training.track(batches):
            ...
            training.note(f"loss {loss:.3f}", loss=loss)
    ```

    Args:
        label: What the block is doing.
        total: How many units the block will ``advance`` through, if known.

    Yields:
        The open step.
    """
    with opened(Step(label, total)) as current:
        yield current