Skip to main content

py-undo-stack

This library implements a pure python undo / redo stack pattern.

Overview

This library revolves around the concept of UndoStack and UndoCommand and is an implementation of the Command pattern. When an action needs to be undoable, an UndoCommand is created which encapsulates how to go from an undo state to a redo state.

UndoCommands are pushed to the UndoStack and the UndoStack is responsible for managing the sequences of undo / redo and the stack size depending on what is pushed on the stack.

UndoGroup

UndoGroup can be used to manage multiple UndoStack. One stack can be active at once per UndoGroup. When calling the UndoGroup API, the calls are delegated to the active stack for processing.

Signal

Signal class provides a basic signal / slot implementation. Signals are triggered in the UndoStack and UndoGroup to inform that their states have changed. The signals are emitted regardless of async or not async variations.

Compression

UndoCommand provides a mechanism to compress commands with one another. Commands pushed to the stack try to merge with previous commands provided that their do_try_merge methods return True.

When commands are merged, the pushed command is discarded and only the previous command is kept containing both commands information.

Obsolete

Commands can be marked as obsolete. When a command is obsolete it will be removed from the stack automatically.

Async

UndoStack and UndoGroup provide async variations for their push undo and redo methods. UndoCommand provides async variations for its undo and redo methods.

When async undo / redo methods are called, the actions are awaited.

Examples

"""
This example shows an example of undo / redo for mutating lists and dicts.
"""

import logging
from contextlib import contextmanager
from copy import deepcopy

from undo_stack import UndoCommand, UndoStack

logging.basicConfig(level=logging.DEBUG)


class ListDictUndoCommand(UndoCommand):
    """
    We create a command which will save the state of the object before and after.
    We also create a utility context manager to make usage easier in the code.
    """

    def __init__(self, obj_before, obj_after, obj):
        super().__init__()
        self._before = obj_before
        self._after = obj_after
        self._obj = obj

    def is_obsolete(self) -> bool:
        """
        If the before and after states are strictly the same, then we can discard this undo / redo.
        """
        return self._before == self._after

    def _restore(self, state) -> None:
        """Generic method to restore the state while keeping the original instance."""
        if isinstance(self._obj, dict):
            self._obj.clear()
            self._obj.update(state)
        elif isinstance(self._obj, list):
            self._obj.clear()
            self._obj.extend(state)
        else:
            raise NotImplementedError()

    def undo(self) -> None:
        """Restore the previous state."""
        self._restore(self._before)

    def redo(self) -> None:
        """Reapply the modified state."""
        self._restore(self._after)

    @classmethod
    @contextmanager
    def save_for_undo(cls, undo_stack: UndoStack, obj):
        """
        In the context manager, we save the state before, and state after yield.
        The before and after states will allow us to restore the state of the object.
        """
        before = deepcopy(obj)
        yield
        after = deepcopy(obj)
        undo_stack.push(cls(before, after, obj))


"""
In our basic application, we create one undo stack.
With this undo stack we will monitor the changes of a list and a dict.
"""

undo_stack = UndoStack()

a_list = []
a_dict = {}

# With our context manager, we can track changes to the dict and list
with ListDictUndoCommand.save_for_undo(undo_stack, a_dict):
    a_dict["hello"] = "world"

with ListDictUndoCommand.save_for_undo(undo_stack, a_list):
    a_list.append(42)

logging.info(undo_stack.n_commands())
# >>> 2

logging.info(undo_stack.can_undo())
# >>> True

# Our undo stack is ordered. Undoing will undo the latest undo on the stack which is the list append.
undo_stack.undo()
logging.info(a_list)
# >>> []

undo_stack.redo()
logging.info(a_list)
# >>> [42]

# We can undo further to reset the content of the dict
undo_stack.undo()
undo_stack.undo()

logging.info(a_dict)
# >>> {}

undo_stack.redo()
logging.info(a_dict)
# >>> {"hello": "world"}

# Pushing in the stack will make the list modification obsolete
with ListDictUndoCommand.save_for_undo(undo_stack, a_dict):
    a_dict["hello"] = str(reversed("world"))

logging.info(undo_stack.can_redo())
# >>> False

Acknowledgments

This implementation is inspired by Qt's QUndoStack, QUndoCommand, QUndoGroup. For usage in Qt based application, direct usage of Qt's classes is recommended.

Metadata

Release files for py-undo-stack 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py-undo-stack 1.1.0
File Size Uploaded Provenance Actions
py_undo_stack-1.1.0.tar.gz 18.0 kB With provenance Details for py_undo_stack-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for py-undo-stack 1.1.0
File Interpreter ABI Platform Actions
py_undo_stack-1.1.0-py3-none-any.whl Python 3 none any Details for py_undo_stack-1.1.0-py3-none-any.whl

Total release size: 30.2 kB

Release files / py_undo_stack-1.1.0.tar.gz

Download URL py_undo_stack-1.1.0.tar.gz
Size 18.0 kB
Tags Source
SHA-256 checksum
How to use checksums
cb4c2a8b86bcf77830376aa3f70216d77daf942bef47a1617274b24358d1841c
BLAKE2b-256 checksum
How to use checksums
eddab592ed225cdb61701aee43ccc31e88a94d27d7e2727979ff2ecd8aab885e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2025.

Transparency log

Release files / py_undo_stack-1.1.0-py3-none-any.whl

Download URL py_undo_stack-1.1.0-py3-none-any.whl
Size 12.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
27369d3620f15547c015d1f4e653df82fec47c58ff961c51c09c716703a28d09
BLAKE2b-256 checksum
How to use checksums
a7a734e6ecaea836dd39c9a454bed086ad5dfe4221f58fbf02c142ff1cfcfc15
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2025.

Transparency log

Security for py-undo-stack 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for py_undo_stack-1.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2025.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page