Skip to content

Small tool for deferring actions and operations on objects and classes

License

Notifications You must be signed in to change notification settings

maarten-dp/mimics

Repository files navigation

Build Status codecov PyPI version

Introduction

Mimic is a tool with the intention to defer actions done on objects or classes. These actions can then be executed at a later date. It's main goal is to solve the chicken-and-egg design conundrum.

When you find yourself in a chicken-and-egg situation within your own code, it's most likely attributed to a sub-optimal project design. If this is the case, it's probably advisable to rethink your project structure.

Sometimes, though, when working with 3rd party libraries, you just don't have the choice, and the design of one library does not mesh with that of another. Out of spite (I'm looking at you <insert most libraries that require an initialized instance to define global scoped decorators>), I started writing this library so that I had control over "when" I initialized "what", while being able to do it in a controlled local scope without losing the ability to use global definitions.

BIG FAT DISCLAIMER: I wouldn't use this lib in production code, not in its current state at least :) It needs some more battle testing before I can comfortably say it's stable. Feel free to contribute to this battle testing.

Quickstart

The core of this library is the Deferred object, that basically behaves like a mock object. The only difference is that the Deferred object does not have reserved names, so you can do literally any operation on it.

Of course, that means that you, the user, won't be able to use this deferred object as the driver. For this we'll need a handler object that set things in motion, and ties things together when needed.

from mimics import Mimic

# Make the handler object
mimic = Mimic()
# Make an object, using the factory on the handler object, that will record all actions
husk = mimic.husk()

# Do the deferred operations you want to do
result = husk + 3

# Replay anything done on the deferred object onto another object
mimic.absorb(husk).as_being(5)
# Doing an additional `is True` to ensure to result is a boolean and not a deferred object
# (because, yes, even these actions are deferred before playing)
assert (result == 8) is True

A more elaborate example

This example won't make much sense, as Flask-SQLAlchemy plays quite nicely when it comes to having control over the local scope while still performing global actions, but I thought it was a nice example of what the library is capable of. Here we'll defer the creation, initialization and persistence of an SQLAlchemy model.

Once we've done all we wanted, we can play it whenever it suits us best.

# Make the handler and deferred object
mimic = Mimic()
husk = mimic.husk()

# Defer the making of an SQLA model using the deferred object
class MyModel(husk.Model):
    id = husk.Column(husk.Integer, primary_key=True)
    name = husk.Column(husk.String(255), nullable=False, unique=True)

# Defer the db creation
husk.create_all()
# Defer the initialization and persisting of an instance
my_model = MyModel(name="test")
husk.session.add(my_model)
husk.session.commit()

# Make the actual SQLA db object
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///:memory:"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# Replay deferred actions as being the db
mimic.absorb(husk).as_being(db)

# Verify it worked
models = MyModel.query.all()
assert len(models) == 1
assert models[0].name == "test"

How it works

As mentioned above, the entire library revolves around the Deferred object. Due to being able to do, well, almost anything with this object, it's important that you don't initialize a deferred object yourself (unless you know what you're doing). It's important that you make an instance of this class through the factory method that is available on a Mimic instance.

Basically the Deferred object can be in two states:

  • suspended
  • unsuspended

A shocker, I know. Whenever the Deferred object is suspended, it will record any action done upon it. Whenever an attribute is accessed, a method is called, or anything that returns a value, it will create and return a new instance of the Deferred object, that in turn also records actions done with it.

Once you're ready to play your deferred actions, bringing it to an unsuspended state, the recorded actions will be re-played on the chosen object. From then on, the spawned deferred objects will work as a proxy that forwards any request to the subject it's related to.

Pitfalls

Proxy object

Because this library is not doing black magic (or at least, not an aweful lot 😉), it's important to know that any subject that the Deferred object shadows, will never truly be itself after unsuspending. We're not manipulating the virtual memory, manipulating local and global variables or patching imported modules (mind you, I've thought about it), as that distorts readability and the ability to follow the exection flow.

While it may look like you're interacting with the subject itself, you'll always be interacting with a proxy object that looks and feels like its subject. As such, some kinks might pop up in certain cases (cfr. BIG FAT DISCLAIMER).

The Deferred object's limits

It's important to note that only operations performed on deferred objects are allowed. Performing operations with deferred objects will go horribly wrong.

For instance

husk = Mimic().husk()
result = 5 + husk

will result in a TypeError

About

Small tool for deferring actions and operations on objects and classes

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages