From b6ac0257523ea6d9add136ba9c2a75ac736b8fa7 Mon Sep 17 00:00:00 2001 From: Andrei Pashkin Date: Wed, 18 Mar 2020 12:46:38 +0300 Subject: [PATCH] Rework aiopg.sa.result module with Cython. --- .travis.yml | 2 +- aiopg/sa/{result.py => result.pyx} | 58 +++++++++++++++++++----------- setup.py | 16 +++++++-- 3 files changed, 52 insertions(+), 24 deletions(-) rename aiopg/sa/{result.py => result.pyx} (92%) diff --git a/.travis.yml b/.travis.yml index 99dada51..a95faa2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ jobs: env: install: -- pip install -U setuptools +- pip install -U setuptools cython - python setup.py install - pip install -Ur requirements.txt - pip install codecov diff --git a/aiopg/sa/result.py b/aiopg/sa/result.pyx similarity index 92% rename from aiopg/sa/result.py rename to aiopg/sa/result.pyx index d58bf7b6..98e7bf6d 100644 --- a/aiopg/sa/result.py +++ b/aiopg/sa/result.pyx @@ -1,15 +1,16 @@ -import weakref -from collections.abc import Mapping, Sequence - +#cython: language_level=3 from sqlalchemy.sql import expression, sqltypes from . import exc -class RowProxy(Mapping): - __slots__ = ('_result_proxy', '_row', '_processors', '_keymap') +cdef class RowProxy: + cdef ResultMetaData _result_proxy + cdef tuple _row + cdef list _processors + cdef dict _keymap - def __init__(self, result_proxy, row, processors, keymap): + def __cinit__(self, result_proxy, row, processors, keymap): """RowProxy objects are constructed by ResultProxy objects.""" self._result_proxy = result_proxy self._row = row @@ -22,11 +23,12 @@ def __iter__(self): def __len__(self): return len(self._row) - def __getitem__(self, key): + cdef object _getitem(self, key): + cdef int index try: - processor, obj, index = self._keymap[key] + processor, _, index = self._keymap[key] except KeyError: - processor, obj, index = self._result_proxy._key_fallback(key) + processor, _, index = self._result_proxy._key_fallback(key) # Do we need slicing at all? RowProxy now is Mapping not Sequence # except TypeError: # if isinstance(key, slice): @@ -49,21 +51,27 @@ def __getitem__(self, key): else: return self._row[index] - def __getattr__(self, name): + def __getitem__(self, item): + return self._getitem(item) + + cdef object _getattr(self, name): try: return self[name] except KeyError as e: raise AttributeError(e.args[0]) + def __getattr__(self, item): + return self._getattr(item) + def __contains__(self, key): return self._result_proxy._has_key(self._row, key) __hash__ = None def __eq__(self, other): - if isinstance(other, RowProxy): + if hasattr(other, 'as_tuple'): return self.as_tuple() == other.as_tuple() - elif isinstance(other, Sequence): + elif hasattr(other, 'index') and hasattr(other, 'count'): return self.as_tuple() == other else: return NotImplemented @@ -71,16 +79,19 @@ def __eq__(self, other): def __ne__(self, other): return not self == other - def as_tuple(self): + cdef tuple as_tuple(self): return tuple(self[k] for k in self) def __repr__(self): return repr(self.as_tuple()) -class ResultMetaData(object): +cdef class ResultMetaData: """Handle cursor.description, applying additional info from an execution context.""" + cdef public list _processors + cdef public list keys + cdef public dict _keymap def __init__(self, result_proxy, cursor_description): self._processors = processors = [] @@ -209,7 +220,7 @@ def _has_key(self, row, key): return self._key_fallback(key, False) is not None -class ResultProxy: +cdef class ResultProxy: """Wraps a DB-API cursor object to provide easier access to row columns. Individual columns may be accessed by their integer position, @@ -228,15 +239,20 @@ class ResultProxy: data using sqlalchemy TypeEngine objects, which are referenced from the originating SQL statement that produced this result set. """ - - def __init__(self, connection, cursor, dialect, result_map=None): + cdef object _dialect + cdef public object _result_map + cdef object _cursor + cdef object _connection + cdef object _rowcount + cdef object _metadata + + def __cinit__(self, connection, cursor, dialect, result_map=None): self._dialect = dialect self._result_map = result_map self._cursor = cursor self._connection = connection self._rowcount = cursor.rowcount self._metadata = None - self._weak = None self._init_metadata() @property @@ -290,10 +306,8 @@ def _init_metadata(self): cursor_description = self.cursor.description if cursor_description is not None: self._metadata = ResultMetaData(self, cursor_description) - self._weak = weakref.ref(self, lambda wr: self.cursor.close()) else: self.close() - self._weak = None @property def returns_rows(self): @@ -333,7 +347,9 @@ def close(self): self.cursor.close() # allow consistent errors self._cursor = None - self._weak = None + + def __del__(self): + self.close() def __aiter__(self): return self diff --git a/setup.py b/setup.py index c4e41fe4..b8601418 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,10 @@ import os import re -from setuptools import setup, find_packages +from setuptools import setup, find_packages, Extension +from Cython.Build import cythonize + +CFLAGS = ['-O3'] install_requires = ['psycopg2-binary>=2.7.0'] extras_require = {'sa': ['sqlalchemy[postgresql_psycopg2binary]>=1.1']} @@ -50,6 +53,14 @@ def read_changelog(path='CHANGES.txt'): 'Framework :: AsyncIO', ] +EXTENSIONS = [ + Extension( + "aiopg.sa.result", + ["aiopg/sa/result.pyx"], + extra_compile_args=CFLAGS + ) +] + setup( name='aiopg', version=read_version(), @@ -76,5 +87,6 @@ def read_changelog(path='CHANGES.txt'): packages=find_packages(), install_requires=install_requires, extras_require=extras_require, - include_package_data=True + include_package_data=True, + ext_modules=cythonize(EXTENSIONS) )