Skip to content

saurabh0719/object-tracker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


A pure python object state tracker. Monitor all changes in your object's lifecycle, query the history changelog, and trigger callback functions to capture them. đź“ť

$ pip install object-tracker

Tested for python 3.7 and above.

Key Features

  • Determine if a python object has changed state during it's lifecycle.
  • Investigate change history by querying a structured changelog.
  • Trigger callback functions whenever an (or any) attribute has changed.
  • Use it as a decorator, a class mixin or on its own.

Table of Contents :

Basic Usage

Use the @track decorator to track an object's attributes.

from object_tracker import track

def observer(attr, old, new):
    print(f"Observer : {attr} -> {old} - {new}")

@track('name', 'age', observers=[observer,])
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

user = User(name='Alice', age=30)
user.name = 'Bob'
# Observer : name -> Alice - Bob
print(user.tracker.has_changed()) 
# True
print(user.tracker.has_attribute_changed('name'))
# True

Or use the Tracker class

class MyClass:
        pass
    
obj = MyClass()
tracker = Tracker(obj)
obj.attribute = 'new_value'
print(tracker.has_changed(obj))
# True

Or use it with the mixin class TrackerMixin

from object_tracker import TrackerMixin, Tracker
    
class User(TrackerMixin):
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.tracker = Tracker()

How does it work?

The decorator @track and the mixin TrackerMixin implement the __setattr__ and __setitem__ dunder methods to intercept and log a change to an attribute.

The tracker is an instance of the Tracker class which logs all changes to the ChangeLog

The entire module is roughly 310 LOC, don't hesitate to read from source directly!

Go back to the table of contents

API

@track

def track(
    *attributes: List[str],
    observers: List[ObserverType] = None,
    attribute_observer_map: Dict[str, List[ObserverType]] = None,
    auto_notify: bool = True,
    stack_trace: bool = True,
    tracker_attribute: str = 'tracker',
    changes_only: bool = False,
):
    """
    Decorator for tracking attribute changes in a class.

    ```
    from object_tracker import track

    @track('name', 'age')
    class User:
        def __init__(self, name, age):
            self.name = name
            self.age = age

    user = User('Alice', 30)
    user.name = 'Bob'
    print(user.tracker.has_changed('name')) # True
    ```

    Args:
        *attributes: 
            The attributes to track.

        observers (list, optional): 
            The observers to notify when an attribute changes. Default is None.

        attribute_observer_map (dict, optional): 
            A map of attributes to observers.Default is None.

        auto_notify (bool, optional):
            Whether to automatically notify observers when an attribute changes.
            Default is True.

        stack_trace (bool, optional):
            Whether to store the call stack when an attribute changes. Default is True.

        tracker_attribute (str, optional):
            The attribute holding the Tracker object. Default is 'tracker'.

        changes_only (bool, optional):
            Whether to track only changes to attributes or all assignments.
            Default is False.

    Returns:
        The decorated class with attribute tracking.
    """

Go back to the table of contents

TrackerMixin

class TrackerMixin:
    """
    Mixin class for tracking attribute changes.
    Overrides the `__setattr__` and `__setitem__` methods to track changes.

    It uses the `Tracker` object, stored in `tracker_attr`, to record changes.
    Modify the `tracker_attr` attribute to change the attribute name.

    ```
    from object_tracker import TrackerMixin, Tracker
    
    class User(TrackerMixin):
        def __init__(self, name, age):
            self.name = name
            self.age = age
            self.tracker = Tracker()
    ```

    Attributes:
        tracker_attr (str):
            The attribute holding the Tracker object. Default is `tracker`.
    """

Go back to the table of contents

Tracker

class Tracker:
    """
    The Tracker class is responsible for tracking changes to an object's attributes.
    ```
    from object_tracker import Tracker

    # Track changes to an object's attributes.
    class MyClass:
        pass
    
    obj = MyClass()
    tracker = Tracker(obj)
    obj.attribute = 'new_value'
    print(tracker.has_changed(obj))


    # Manually calling the track method to track changes to an attribute.
    tracker = Tracker()
    tracker.track('attribute', 'old_value', 'new_value')
    print(tracker.has_attribute_changed('attribute'))
    ```
    """

    def __init__(
        self,
        initial_state: any = None,
        attributes: List[str] = None,
        observers: List[ObserverType] = None,
        attribute_observer_map: Dict[str, List[ObserverType]] = None,
        auto_notify: bool = True,
        stack_trace: bool = True,
        changes_only: bool = False,
    ) -> None:
        """
        Initializes the Tracker instance.

        Args:
            initial_state (any):
                The initial state of the object to be tracked. Default is None.

            attributes (List[str]): 
                The attributes to track. Default is None ie. all attributes are tracked.

            observers (List[ObserverType]):
                The list of observers to notify on attribute change. Default is None.

            attribute_observer_map (Dict[str, List[ObserverType]]):
                A map of attributes to observers. Default is None.

            auto_notify (bool):
                Whether to automatically notify observers on attribute change.
                Default is True.

            stack_trace (bool):
                Whether to store the call stack when an attribute changes.
                Default is True.

            changes_only (bool):
                Whether to track only the attributes that have changed.
                Default is False.

        Attributes:
            log (ChangeLog):
                The log to store attribute changes.
        """

Go back to the table of contents

ChangeLog

The tracker instance has the log inside it containing a list of Entry objs

Each Entry in a change log has -

  • attr - The attribute that was changed
  • old - copy of the old value
  • new - copy of the new value
  • timestamp - UTC datetime
  • stack - a list of frames from inspect leading up to the change.
class ChangeLog:
    """
    The ChangeLog class is responsible for storing and managing a log of attribute changes.

    This class provides methods to add new entries to the log, filter the log based on attribute names, 
    exclude certain attributes from the log, and clear the log.

    Methods:

    - push(attr, old, new, stack=None): Pushes a new entry to the log.

    - filter(*attrs, changes_only=False): Filters the log based on the given attributes.

    - exclude(*attrs, changes_only=False): Excludes the given attributes from the log.

    - first(): Returns the first log entry.

    - last(): Returns the last log entry.

    - all(): Returns all log entries.

    - count(): Returns the number of log entries.

    - replay(): A generator to print the logs in a human-readable format.

    - get_unique_attributes(): Returns all attributes in the log.

    - has_changed(attr): Checks if any attribute of the object has been changed by verifying against the log.

    - reset_buffer(): Resets the buffer.

    Eg.

        The `tracker` obj has the `log` attribute which is an instance of the `ChangeLog` class.

        tracker.log.filter('name', 'age') -> Returns logs for 'name' and 'age' attributes

        tracker.log.exclude('name') -> Excludes logs for 'name' attribute

        tracker.log.first() -> Returns the first log entry

        tracker.filter('name').count() -> Returns the number of log entries for 'name' attribute
    """

Go back to the table of contents

Examples

The replay() method was repurposed from Madison May's tracker.

from object_tracker import track

@track('name', 'age')
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

user = User(name='Alice', age=30)
user.name = 'Bob'
user.name = 'John'
user.age = 31

print("\n Has object changed - ", user.tracker.has_changed()) # True
print("\n Has 'name' changed - ", user.tracker.has_attribute_changed('name')) # True
print("\n Num of changes in 'name' - ", user.tracker.log.filter('name').count()) # 2
print("\n Num of changes in 'age' - ", user.tracker.log.filter('age').count()) # 1

print("\n All logs - ")
for log in user.tracker.log.replay():
    print(log)

Output -


 Has object changed -  True

 Has 'name' changed -  True

 Num of changes in 'name' -  2

 Num of changes in 'age' -  1

 All logs - 
--------------------------------------------------
name = 'Bob'

    trial.py: 10 - <module>
    user.name = 'Bob'

--------------------------------------------------
name = 'John'

    trial.py: 11 - <module>
    user.name = 'John'

--------------------------------------------------
age = 31

    trial.py: 12 - <module>
    user.age = 31


Tests

Run this command inside the base directory to execute all tests inside the tests folder:

$ python -m unittest -v

Go back to the table of contents


Release notes

  • Latest - v2.0.0

View object-tracker's detailed release history.

Go back to the table of contents


License

Copyright (c) Saurabh Pujari
All rights reserved.

This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree.

Go back to the table of contents

About

A pure python Object state change tracker. Monitor all changes in the object's lifecycle and trigger callback functions. đź“ť

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages