diff --git a/registrasion/controllers/batch.py b/registrasion/controllers/batch.py new file mode 100644 index 00000000..2e5e0241 --- /dev/null +++ b/registrasion/controllers/batch.py @@ -0,0 +1,85 @@ +import contextlib +import functools + + +class BatchController(object): + ''' Batches are sets of operations where certain queries for users may be + repeated, but are also unlikely change within the boundaries of the batch. + + Batches are keyed per-user. You can mark the edge of the batch with the + ``batch`` context manager. If you nest calls to ``batch``, only the + outermost call will have the effect of ending the batch. + + Batches store results for functions wrapped with ``memoise``. These results + for the user are flushed at the end of the batch. + + If a return for a memoised function has a callable attribute called + ``end_batch``, that attribute will be called at the end of the batch. + + ''' + + _user_caches = {} + + @classmethod + @contextlib.contextmanager + def batch(cls, user): + ''' Marks the entry point for a batch for the given user. ''' + pass + # TODO: store nesting count *inside* the cache object. You know it + # makes sense. + + @classmethod + def memoise(cls, func): + ''' Decorator that stores the result of the stored function in the + user's results cache until the batch completes. + + Arguments: + func (callable(user, *a, **k)): The function whose results we want + to store. ``user`` must be the first argument; this is used as + the cache key. + + Returns: + callable(user, *a, **k): The memosing version of ``func``. + + ''' + + @functools.wraps(func) + def f(user, *a, **k): + + cache = cls.get_cache(user) + if func not in cache: + cache[func] = func(user, *a, **k) + + return cache[func] + + return f + + @classmethod + def get_cache(cls, user): + if user not in cls._user_caches: + return {} # Return blank cache here, we'll just discard :) + + return cls._user_caches[user] + + +''' +TODO: memoise CartController.for_user +TODO: memoise user_remainders (Product, Category) +TODO: memoise _filtered_flags +TODO: memoise FlagCounter.count() (doesn't take user, but it'll do for now) +TODO: memoise _filtered_discounts + +Tests: +- Correct nesting behaviour + - do we get different cache objects every time we get a cache in non-batched + contexts? + - do we get the same cache object for nested caches? + - do we get different cache objects when we back out of a batch and enter a + new one +- are cache clears independent for different users? +- ``end_batch`` behaviour for CartController (use for_user *A LOT*) + - discounts not calculated until outermost batch point exits. + - Revision number shouldn't change until outermost batch point exits. +- Make sure memoisation ONLY happens when we're in a batch. + +''' diff --git a/registrasion/controllers/discount.py b/registrasion/controllers/discount.py index f0df1b07..29bc1ec6 100644 --- a/registrasion/controllers/discount.py +++ b/registrasion/controllers/discount.py @@ -50,7 +50,7 @@ class DiscountController(object): categories and products. The discounts also list the available quantity for this user, not including products that are pending purchase. ''' - filtered_clauses = cls._filtered_clauses(user, categories, products) + filtered_clauses = cls._filtered_clauses(user) # clauses that match provided categories categories = set(categories) @@ -103,8 +103,8 @@ class DiscountController(object): ''' Returns: - Sequence[discountbase]: All discounts that passed the filter - function. + Sequence[DiscountForProduct | DiscountForCategory]: All clauses + that passed the filter function. '''