Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the ORM #88

Merged
merged 13 commits into from
Oct 15, 2019
Prev Previous commit
Next Next commit
optimize the Model objects
This commit replaces the `make_DelegatingCaster` class factory with a single `ModelCaster` class. The column names of a `Model` subclass are now stored at the class level instead of the object level, so the objetcs use less memory.
  • Loading branch information
Changaco committed Oct 15, 2019
commit 466533b0484f3f2c47c648cf0851a20e7c9a517a
114 changes: 50 additions & 64 deletions postgres/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 165,14 @@
"""
from __future__ import absolute_import, division, print_function, unicode_literals

from collections import namedtuple
from collections import OrderedDict, namedtuple
from inspect import isclass
import sys

import psycopg2
from psycopg2 import DataError, InterfaceError, ProgrammingError
from psycopg2.extras import register_composite, CompositeCaster
from psycopg2.extensions import register_type
from psycopg2.extras import CompositeCaster
from psycopg2_pool import ThreadSafeConnectionPool

from postgres.context_managers import (
Expand Down Expand Up @@ -336,7 337,6 @@ def __init__(self, url='', minconn=1, maxconn=10, idle_timeout=600,
# ===================

self.model_registry = {}
self.DelegatingCaster = make_DelegatingCaster(self)


def run(self, sql, parameters=None, **kw):
Expand Down Expand Up @@ -519,20 519,12 @@ def register_model(self, ModelSubclass, typname=None):
raise AlreadyRegistered(existing_model, typname)

# register a composite
with self.get_connection() as conn:
cursor = conn.cursor()
name = typname
if sys.version_info[0] < 3:
name = name.encode('UTF-8')
try:
register_composite(
name, cursor, globally=True, factory=self.DelegatingCaster
)
except ProgrammingError:
raise NoSuchType(typname)
caster = ModelCaster._from_db(self, typname, ModelSubclass)
register_type(caster.typecaster)
if caster.array_typecaster is not None:
register_type(caster.array_typecaster)

self.model_registry[typname] = ModelSubclass
ModelSubclass.db = self


def unregister_model(self, ModelSubclass):
Expand Down Expand Up @@ -653,58 645,52 @@ def get_cursor(self, cursor=None, **kw):
return Connection


def make_DelegatingCaster(postgres):
"""Define a :class:`~psycopg2.extras.CompositeCaster` subclass that
delegates to :attr:`~postgres.Postgres.model_registry`.

:param postgres: the :class:`~postgres.Postgres` instance to bind to
:returns: a :class:`DelegatingCaster` class

The class we return will use the :attr:`model_registry` of the given
:class:`~postgres.Postgres` instance to look up a
:class:`~postgres.orm.Model` subclass to use in mapping
:mod:`psycopg2` return values to higher-order Python objects. Yeah, it's
a little squirrelly. :-/

class ModelCaster(CompositeCaster):
"""A :class:`~psycopg2.extras.CompositeCaster` subclass for :class:`.Model`.
"""
class DelegatingCaster(CompositeCaster):

def parse(self, s, curs, retry=True):
# Override to protect against race conditions:
# https://github.com/chadwhitacre/postgres.py/issues/26

try:
return super(DelegatingCaster, self).parse(s, curs)
except (DataError, ValueError):
if not retry:
raise
# Re-fetch the type info and retry once
self._refetch_type_info(curs)
return self.parse(s, curs, False)

def make(self, values):
# Override to delegate to the model registry.
@classmethod
def _from_db(cls, db, typname, ModelSubclass):
# Override to set custom attributes.
with db.get_cursor(autocommit=True, readonly=True) as cursor:
name = typname
if sys.version_info[0] < 3:
name = name.encode('UTF-8')
try:
ModelSubclass = postgres.model_registry[self.name]
except KeyError:
# This is probably a bug, not a normal user error. It means
# we've called register_composite for this typname without also
# registering with model_registry.
raise RuntimeError("%r isn't in model_registry" % self.name)
instance = ModelSubclass(self.attnames, values)
return instance

def _create_type(self):
# Override to avoid creation of unused namedtuple class
pass

def _refetch_type_info(self, curs):
"""Given a cursor, update the current object with a fresh type definition.
"""
new_self = self._from_db(self.name, curs)
self.__dict__.update(new_self.__dict__)

return DelegatingCaster
caster = super(ModelCaster, cls)._from_db(name, cursor)
except ProgrammingError:
raise NoSuchType(typname)
caster.db = ModelSubclass.db = db
caster.ModelSubclass = ModelSubclass
ModelSubclass._read_only_attributes = OrderedDict.fromkeys(caster.attnames)
return caster

def parse(self, s, curs, retry=True):
# Override to protect against some race conditions:
# https://github.com/chadwhitacre/postgres.py/issues/26
try:
return super(ModelCaster, self).parse(s, curs)
except (DataError, ValueError):
if not retry:
raise
# Re-fetch the type info and retry once
self._refetch_type_info(curs)
return self.parse(s, curs, False)

def make(self, values):
""""""
# Override to use ModelSubclass instead of a namedtuple class.
return self.ModelSubclass(values)

def _create_type(self, name, attnames):
# Override to avoid the creation of an unwanted namedtuple class.
pass

def _refetch_type_info(self, curs):
"""Given a cursor, update the current object with a fresh type definition.
"""
new_self = self._from_db(self.db, self.name, self.ModelSubclass)
self.__dict__.update(new_self.__dict__)


if __name__ == '__main__':
Expand Down
10 changes: 5 additions & 5 deletions postgres/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 216,16 @@ class Model(object):

typname = None # an entry in pg_type
db = None # will be set to a Postgres object
_read_only_attributes = None # set in ModelCaster._from_db()

def __init__(self, colnames, values):
def __init__(self, values):
if self.db is None:
raise NotBound(self)
self.db.check_registration(self.__class__, include_subsubclasses=True)
self.__read_only_attributes = set(colnames)
self.__dict__.update(zip(colnames, values))
self.__dict__.update(zip(self._read_only_attributes, values))

def __setattr__(self, name, value):
if name in self.__read_only_attributes:
if name in self._read_only_attributes:
raise ReadOnly(name)
return super(Model, self).__setattr__(name, value)

Expand All @@ -244,7 244,7 @@ def set_attributes(self, **kw):
"""
unknown = None
for name in kw:
if name not in self.__read_only_attributes:
if name not in self._read_only_attributes:
if unknown is None:
unknown = [name]
else:
Expand Down
8 changes: 4 additions & 4 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 365,9 @@ class MyModel(Model):

typname = "foo"

def __init__(self, record):
Model.__init__(self, record)
self.bar_from_init = record['bar']
def __init__(self, values):
Model.__init__(self, values)
self.bar_from_init = self.bar

def update_bar(self, bar):
self.db.run("UPDATE foo SET bar=%s WHERE bar=%s", (bar, self.bar))
Expand Down Expand Up @@ -419,7 419,7 @@ def assign():
self.assertRaises(ReadOnly, assign)

def test_check_register_raises_if_passed_a_model_instance(self):
obj = self.MyModel({'bar': 'baz'})
obj = self.MyModel(['baz'])
raises(NotAModel, self.db.check_registration, obj)

def test_check_register_doesnt_include_subsubclasses(self):
Expand Down