diff --git a/CHANGES.rst b/CHANGES.rst index ad732f2..355d16a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,12 +4,14 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Utils release. -0.29.10 (2015-04-xx) -^^^^^^^^^^^^^^^^^^^^ +0.30.0 (2015-04-15) +^^^^^^^^^^^^^^^^^^^ - Added __hash__ method to Country class - Made Country validate itself during object initialization - Made Country string coercible +- Removed deprecated function generates +- Fixed observes function to work with simple column properties 0.29.9 (2015-04-07) diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 5717885..f90e2a8 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -7,7 +7,6 @@ from .asserts import ( # noqa assert_nullable ) from .batch import batch_fetch, with_backrefs # noqa -from .decorators import generates # noqa from .exceptions import ImproperlyConfigured # noqa from .expression_parser import ExpressionParser # noqa from .functions import ( # noqa diff --git a/sqlalchemy_utils/decorators.py b/sqlalchemy_utils/decorators.py deleted file mode 100644 index 342a202..0000000 --- a/sqlalchemy_utils/decorators.py +++ /dev/null @@ -1,201 +0,0 @@ -import itertools -from collections import defaultdict - -import six -import sqlalchemy as sa - -from .functions import getdotattr - - -class AttributeValueGenerator(object): - def __init__(self): - self.listener_args = [ - ( - sa.orm.mapper, - 'mapper_configured', - self.update_generator_registry - ), - ( - sa.orm.session.Session, - 'before_flush', - self.update_generated_properties - ) - ] - self.reset() - - def reset(self): - if ( - hasattr(self, 'listeners_registered') and - self.listeners_registered - ): - for args in self.listener_args: - sa.event.remove(*args) - - self.listeners_registered = False - # TODO: make the registry a WeakKey dict - self.generator_registry = defaultdict(list) - - def generator_wrapper(self, func, attr, source): - def wrapper(self, *args, **kwargs): - return func(self, *args, **kwargs) - - if isinstance(attr, sa.orm.attributes.InstrumentedAttribute): - self.generator_registry[attr.class_].append(wrapper) - wrapper.__generates__ = attr, source - else: - wrapper.__generates__ = attr, source - return wrapper - - def register_listeners(self): - if not self.listeners_registered: - for args in self.listener_args: - sa.event.listen(*args) - self.listeners_registered = True - - def update_generator_registry(self, mapper, class_): - """ - Adds generator functions to generator_registry. - """ - - for generator in class_.__dict__.values(): - if hasattr(generator, '__generates__'): - self.generator_registry[class_].append(generator) - - def update_generated_properties(self, session, ctx, instances): - for obj in itertools.chain(session.new, session.dirty): - class_ = obj.__class__ - if class_ in self.generator_registry: - for func in self.generator_registry[class_]: - attr, source = func.__generates__ - if not isinstance(attr, six.string_types): - attr = attr.name - - if source is None: - setattr(obj, attr, func(obj)) - else: - setattr(obj, attr, func(obj, getdotattr(obj, source))) - - -generator = AttributeValueGenerator() - - -def generates(attr, source=None, generator=generator): - """ - .. deprecated:: 0.28.0 - Use :func:`.observer.observes` instead. - - Decorator that marks given function as attribute value generator. - - Many times you may have generated property values. Usual cases include - slugs from names or resized thumbnails from images. - - SQLAlchemy-Utils provides a way to do this easily with `generates` - decorator: - - :: - - - class Article(Base): - __tablename__ = 'article' - id = sa.Column(sa.Integer, primary_key=True) - name = sa.Column(sa.Unicode(255)) - slug = sa.Column(sa.Unicode(255)) - - @generates(slug) - def _create_slug(self): - return self.name.lower().replace(' ', '-') - - - article = self.Article() - article.name = u'some article name' - self.session.add(article) - self.session.flush() - assert article.slug == u'some-article-name' - - - You can also pass the attribute name as a string argument for `generates`: - - :: - - class Article(Base): - ... - - @generates('slug') - def _create_slug(self): - return self.name.lower().replace(' ', '-') - - - These property generators can even be defined outside classes: - - :: - - - class Article(Base): - __tablename__ = 'article' - id = sa.Column(sa.Integer, primary_key=True) - name = sa.Column(sa.Unicode(255)) - slug = sa.Column(sa.Unicode(255)) - - - @generates(Article.slug) - def _create_article_slug(article): - return article.name.lower().replace(' ', '-') - - - Property generators can have sources outside: - - :: - - - class Document(self.Base): - __tablename__ = 'document' - id = sa.Column(sa.Integer, primary_key=True) - name = sa.Column(sa.Unicode(255)) - locale = sa.Column(sa.String(10)) - - - class Section(self.Base): - __tablename__ = 'section' - id = sa.Column(sa.Integer, primary_key=True) - name = sa.Column(sa.Unicode(255)) - locale = sa.Column(sa.String(10)) - - document_id = sa.Column( - sa.Integer, sa.ForeignKey(Document.id) - ) - - document = sa.orm.relationship(Document) - - @generates(locale, source='document') - def copy_locale(self, document): - return document.locale - - - You can also use dotted attribute paths for deep relationship paths: - - :: - - - class SubSection(self.Base): - __tablename__ = 'subsection' - id = sa.Column(sa.Integer, primary_key=True) - name = sa.Column(sa.Unicode(255)) - locale = sa.Column(sa.String(10)) - - section_id = sa.Column( - sa.Integer, sa.ForeignKey(Section.id) - ) - - section = sa.orm.relationship(Section) - - @generates(locale, source='section.document') - def copy_locale(self, document): - return document.locale - - """ - - generator.register_listeners() - - def wraps(func): - return generator.generator_wrapper(func, attr, source) - return wraps diff --git a/sqlalchemy_utils/observer.py b/sqlalchemy_utils/observer.py index d531b80..c109480 100644 --- a/sqlalchemy_utils/observer.py +++ b/sqlalchemy_utils/observer.py @@ -67,11 +67,11 @@ things. However performance wise you should take the following things into consideration: * :func:`observes` works always inside transaction and deals with objects. If - the relationship observer is observing has a large number of objects it's better - to use :func:`.aggregates.aggregated`. + the relationship observer is observing has a large number of objects it's + better to use :func:`.aggregates.aggregated`. * :func:`.aggregates.aggregated` always executes one additional query per - aggregate so in scenarios where the observed relationship has only a handful of - objects it's better to use :func:`observes` instead. + aggregate so in scenarios where the observed relationship has only a handful + of objects it's better to use :func:`observes` instead. Example 1. Movie with many ratings @@ -223,15 +223,17 @@ class PropertyObserver(object): for index in range(len(path)): i = index + 1 - prop_class = path[index].property.mapper.class_ - self.callback_map[prop_class].append( - Callback( - func=callback, - path=path[i:], - backref=~ (path[:i]), - fullpath=path + prop = path[index].property + if isinstance(prop, sa.orm.RelationshipProperty): + prop_class = path[index].property.mapper.class_ + self.callback_map[prop_class].append( + Callback( + func=callback, + path=path[i:], + backref=~ (path[:i]), + fullpath=path + ) ) - ) def gather_callback_args(self, obj, callbacks): session = sa.orm.object_session(obj) diff --git a/tests/observes/test_column_property.py b/tests/observes/test_column_property.py new file mode 100644 index 0000000..8339f4b --- /dev/null +++ b/tests/observes/test_column_property.py @@ -0,0 +1,26 @@ +import sqlalchemy as sa + +from sqlalchemy_utils.observer import observes +from tests import TestCase + + +class TestObservesForColumn(TestCase): + dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' + + def create_models(self): + class Product(self.Base): + __tablename__ = 'product' + id = sa.Column(sa.Integer, primary_key=True) + price = sa.Column(sa.Integer) + + @observes('price') + def product_price_observer(self, price): + self.price = price * 2 + + self.Product = Product + + def test_simple_insert(self): + product = self.Product(price=100) + self.session.add(product) + self.session.flush() + assert product.price == 200