Pure python Undo / Redo command stack
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_undo_stack-1.1.0.tar.gz.
File metadata
- Download URL: py_undo_stack-1.1.0.tar.gz
- Upload date:
- Size: 18.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb4c2a8b86bcf77830376aa3f70216d77daf942bef47a1617274b24358d1841c
|
|
| MD5 |
ee4b67ec4a09987a8544da06f3e465c6
|
|
| BLAKE2b-256 |
eddab592ed225cdb61701aee43ccc31e88a94d27d7e2727979ff2ecd8aab885e
|
Provenance
The following attestation bundles were made for py_undo_stack-1.1.0.tar.gz:
Publisher:
test_and_release.yml on KitwareMedical/py-undo-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_undo_stack-1.1.0.tar.gz -
Subject digest:
cb4c2a8b86bcf77830376aa3f70216d77daf942bef47a1617274b24358d1841c - Sigstore transparency entry: 611900799
- Sigstore integration time:
-
Permalink:
KitwareMedical/py-undo-stack@d9c3bd3d048b7c12ede41d9c9a4d673152f324e0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/KitwareMedical
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
test_and_release.yml@d9c3bd3d048b7c12ede41d9c9a4d673152f324e0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file py_undo_stack-1.1.0-py3-none-any.whl.
File metadata
- Download URL: py_undo_stack-1.1.0-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27369d3620f15547c015d1f4e653df82fec47c58ff961c51c09c716703a28d09
|
|
| MD5 |
2f6d1d3d5b0013f073219278744b9498
|
|
| BLAKE2b-256 |
a7a734e6ecaea836dd39c9a454bed086ad5dfe4221f58fbf02c142ff1cfcfc15
|
Provenance
The following attestation bundles were made for py_undo_stack-1.1.0-py3-none-any.whl:
Publisher:
test_and_release.yml on KitwareMedical/py-undo-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_undo_stack-1.1.0-py3-none-any.whl -
Subject digest:
27369d3620f15547c015d1f4e653df82fec47c58ff961c51c09c716703a28d09 - Sigstore transparency entry: 611900801
- Sigstore integration time:
-
Permalink:
KitwareMedical/py-undo-stack@d9c3bd3d048b7c12ede41d9c9a4d673152f324e0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/KitwareMedical
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
test_and_release.yml@d9c3bd3d048b7c12ede41d9c9a4d673152f324e0 -
Trigger Event:
push
-
Statement type: