diff --git a/sqlalchemy_imageattach/entity.py b/sqlalchemy_imageattach/entity.py index 5c62e0f..31025b4 100644 --- a/sqlalchemy_imageattach/entity.py +++ b/sqlalchemy_imageattach/entity.py @@ -76,6 +76,7 @@ class User(Base): import io import numbers import shutil +import threading import uuid from sqlalchemy import Column @@ -96,7 +97,7 @@ class User(Base): from .context import current_store, get_current_store, store_context from .file import ReusableFileProxy from .store import Store -from .util import append_docstring_attributes +from .util import append_docstring_attributes, classproperty __all__ = ('VECTOR_TYPES', 'BaseImageSet', 'BaseImageQuery', 'Image', 'ImageSet', 'ImageSubset', 'MultipleImageSet', 'SingleImageSet', @@ -373,18 +374,32 @@ class BaseImageQuery(Query): """ - #: (:class:`collections.abc.MutableSet`) The set of instances that their - #: image files are stored but the ongoing transaction isn't committed. - #: When the transaction might fail and rollback, image files in the - #: set are deleted back in the storage. - _stored_images = set() - - #: (:class:`collections.abc.MutableSet`) The set of instanced marked - #: as deleted. If the ongoing transaction is successfully committed - #: the actual files in the storages will be deleted as well. - #: When the transaction might fail and rollback, image files won't - #: deleted and the set will be empty. - _deleted_images = set() + _local = threading.local() + + @classproperty + def _stored_images(cls): + """(:class:`collections.abc.MutableSet`) The set of instances that + their image files are stored but the ongoing transaction isn't + committed. When the transaction might fail and rollback, image files + in the set are deleted back in the storage. This set is thread-local. + + """ + if not getattr(cls._local, '_stored_images', None): + cls._local._stored_images = set() + return cls._local._stored_images + + @classproperty + def _deleted_images(cls): + """(:class:`collections.abc.MutableSet`) The set of instanced marked + as deleted. If the ongoing transaction is successfully committed + the actual files in the storages will be deleted as well. When the + transaction might fail and rollback, image files won't deleted and + the set will be empty. This set is thread-local. + + """ + if not getattr(cls._local, '_deleted_images', None): + cls._local._deleted_images = set() + return cls._local._deleted_images @classmethod def _mark_image_file_stored(cls, mapper, connection, target): diff --git a/sqlalchemy_imageattach/util.py b/sqlalchemy_imageattach/util.py index eb0a070..a2f2315 100644 --- a/sqlalchemy_imageattach/util.py +++ b/sqlalchemy_imageattach/util.py @@ -10,7 +10,7 @@ import textwrap __all__ = ('append_docstring', 'append_docstring_attributes', - 'get_minimum_indent') + 'classproperty', 'get_minimum_indent') def get_minimum_indent(docstring, ignore_before=1): @@ -102,3 +102,11 @@ class Entity(object): *lines ) return docstring + + +class classproperty(object): + def __init__(self, getter): + self.getter = getter + + def __get__(self, instance, owner): + return self.getter(owner) diff --git a/tests/util_test.py b/tests/util_test.py index 59ab906..6f7811a 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import -from sqlalchemy_imageattach.util import append_docstring, get_minimum_indent +from sqlalchemy_imageattach.util import (append_docstring, classproperty, + get_minimum_indent) def test_minimum_indent(): @@ -52,3 +53,14 @@ def test_func(): Appended docstring! '''.rstrip() + + +def test_classproperty(): + + class Foo(object): + @classproperty + def bar(cls): + return 'baz' + + assert Foo.bar == 'baz' + assert Foo().bar == 'baz'