diff options
Diffstat (limited to 'lib/sqlalchemy/engine')
-rw-r--r-- | lib/sqlalchemy/engine/_py_processors.py | 106 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/_py_row.py | 138 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/_py_util.py | 54 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/processors.py | 44 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/result.py | 19 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/row.py | 160 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/util.py | 61 |
7 files changed, 363 insertions, 219 deletions
diff --git a/lib/sqlalchemy/engine/_py_processors.py b/lib/sqlalchemy/engine/_py_processors.py new file mode 100644 index 000000000..db722a978 --- /dev/null +++ b/lib/sqlalchemy/engine/_py_processors.py @@ -0,0 +1,106 @@ +# sqlalchemy/processors.py +# Copyright (C) 2010-2021 the SQLAlchemy authors and contributors +# <see AUTHORS file> +# Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +"""defines generic type conversion functions, as used in bind and result +processors. + +They all share one common characteristic: None is passed through unchanged. + +""" + +import datetime +import re + +from .. import util + + +def str_to_datetime_processor_factory(regexp, type_): + rmatch = regexp.match + # Even on python2.6 datetime.strptime is both slower than this code + # and it does not support microseconds. + has_named_groups = bool(regexp.groupindex) + + def process(value): + if value is None: + return None + else: + try: + m = rmatch(value) + except TypeError as err: + util.raise_( + ValueError( + "Couldn't parse %s string '%r' " + "- value is not a string." % (type_.__name__, value) + ), + from_=err, + ) + if m is None: + raise ValueError( + "Couldn't parse %s string: " + "'%s'" % (type_.__name__, value) + ) + if has_named_groups: + groups = m.groupdict(0) + return type_( + **dict( + list( + zip( + iter(groups.keys()), + list(map(int, iter(groups.values()))), + ) + ) + ) + ) + else: + return type_(*list(map(int, m.groups(0)))) + + return process + + +def to_decimal_processor_factory(target_class, scale): + fstring = "%%.%df" % scale + + def process(value): + if value is None: + return None + else: + return target_class(fstring % value) + + return process + + +def to_float(value): + if value is None: + return None + else: + return float(value) + + +def to_str(value): + if value is None: + return None + else: + return str(value) + + +def int_to_boolean(value): + if value is None: + return None + else: + return bool(value) + + +DATETIME_RE = re.compile(r"(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)(?:\.(\d+))?") +TIME_RE = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d+))?") +DATE_RE = re.compile(r"(\d+)-(\d+)-(\d+)") + +str_to_datetime = str_to_datetime_processor_factory( + DATETIME_RE, datetime.datetime +) +str_to_time = str_to_datetime_processor_factory(TIME_RE, datetime.time) +str_to_date = str_to_datetime_processor_factory(DATE_RE, datetime.date) diff --git a/lib/sqlalchemy/engine/_py_row.py b/lib/sqlalchemy/engine/_py_row.py new file mode 100644 index 000000000..981b6e0b2 --- /dev/null +++ b/lib/sqlalchemy/engine/_py_row.py @@ -0,0 +1,138 @@ +import operator + +MD_INDEX = 0 # integer index in cursor.description + +KEY_INTEGER_ONLY = 0 +"""__getitem__ only allows integer values and slices, raises TypeError + otherwise""" + +KEY_OBJECTS_ONLY = 1 +"""__getitem__ only allows string/object values, raises TypeError otherwise""" + +sqlalchemy_engine_row = None + + +class BaseRow: + Row = None + __slots__ = ("_parent", "_data", "_keymap", "_key_style") + + def __init__(self, parent, processors, keymap, key_style, data): + """Row objects are constructed by CursorResult objects.""" + + object.__setattr__(self, "_parent", parent) + + if processors: + object.__setattr__( + self, + "_data", + tuple( + [ + proc(value) if proc else value + for proc, value in zip(processors, data) + ] + ), + ) + else: + object.__setattr__(self, "_data", tuple(data)) + + object.__setattr__(self, "_keymap", keymap) + + object.__setattr__(self, "_key_style", key_style) + + def __reduce__(self): + return ( + rowproxy_reconstructor, + (self.__class__, self.__getstate__()), + ) + + def __getstate__(self): + return { + "_parent": self._parent, + "_data": self._data, + "_key_style": self._key_style, + } + + def __setstate__(self, state): + parent = state["_parent"] + object.__setattr__(self, "_parent", parent) + object.__setattr__(self, "_data", state["_data"]) + object.__setattr__(self, "_keymap", parent._keymap) + object.__setattr__(self, "_key_style", state["_key_style"]) + + def _filter_on_values(self, filters): + global sqlalchemy_engine_row + if sqlalchemy_engine_row is None: + from sqlalchemy.engine.row import Row as sqlalchemy_engine_row + + return sqlalchemy_engine_row( + self._parent, + filters, + self._keymap, + self._key_style, + self._data, + ) + + def _values_impl(self): + return list(self) + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __hash__(self): + return hash(self._data) + + def _get_by_int_impl(self, key): + return self._data[key] + + def _get_by_key_impl(self, key): + # keep two isinstance since it's noticeably faster in the int case + if isinstance(key, int) or isinstance(key, slice): + return self._data[key] + + self._parent._raise_for_nonint(key) + + # The original 1.4 plan was that Row would not allow row["str"] + # access, however as the C extensions were inadvertently allowing + # this coupled with the fact that orm Session sets future=True, + # this allows a softer upgrade path. see #6218 + __getitem__ = _get_by_key_impl + + def _get_by_key_impl_mapping(self, key): + try: + rec = self._keymap[key] + except KeyError as ke: + rec = self._parent._key_fallback(key, ke) + + mdindex = rec[MD_INDEX] + if mdindex is None: + self._parent._raise_for_ambiguous_column_name(rec) + elif self._key_style == KEY_OBJECTS_ONLY and isinstance(key, int): + raise KeyError(key) + + return self._data[mdindex] + + def __getattr__(self, name): + try: + return self._get_by_key_impl_mapping(name) + except KeyError as e: + raise AttributeError(e.args[0]) from e + + +# This reconstructor is necessary so that pickles with the Cy extension or +# without use the same Binary format. +def rowproxy_reconstructor(cls, state): + obj = cls.__new__(cls) + obj.__setstate__(state) + return obj + + +def tuplegetter(*indexes): + it = operator.itemgetter(*indexes) + + if len(indexes) > 1: + return it + else: + return lambda row: (it(row),) diff --git a/lib/sqlalchemy/engine/_py_util.py b/lib/sqlalchemy/engine/_py_util.py new file mode 100644 index 000000000..2db6c049b --- /dev/null +++ b/lib/sqlalchemy/engine/_py_util.py @@ -0,0 +1,54 @@ +from collections import abc as collections_abc + +from .. import exc + +_no_tuple = () + + +def _distill_params_20(params): + if params is None: + return _no_tuple + # Assume list is more likely than tuple + elif isinstance(params, list) or isinstance(params, tuple): + # collections_abc.MutableSequence): # avoid abc.__instancecheck__ + if params and not isinstance( + params[0], (tuple, collections_abc.Mapping) + ): + raise exc.ArgumentError( + "List argument must consist only of tuples or dictionaries" + ) + + return params + elif isinstance(params, dict) or isinstance( + # only do immutabledict or abc.__instancecheck__ for Mapping after + # we've checked for plain dictionaries and would otherwise raise + params, + collections_abc.Mapping, + ): + return [params] + else: + raise exc.ArgumentError("mapping or list expected for parameters") + + +def _distill_raw_params(params): + if params is None: + return _no_tuple + elif isinstance(params, list): + # collections_abc.MutableSequence): # avoid abc.__instancecheck__ + if params and not isinstance( + params[0], (tuple, collections_abc.Mapping) + ): + raise exc.ArgumentError( + "List argument must consist only of tuples or dictionaries" + ) + + return params + elif isinstance(params, (tuple, dict)) or isinstance( + # only do abc.__instancecheck__ for Mapping after we've checked + # for plain dictionaries and would otherwise raise + params, + collections_abc.Mapping, + ): + return [params] + else: + raise exc.ArgumentError("mapping or sequence expected for parameters") diff --git a/lib/sqlalchemy/engine/processors.py b/lib/sqlalchemy/engine/processors.py new file mode 100644 index 000000000..023444d10 --- /dev/null +++ b/lib/sqlalchemy/engine/processors.py @@ -0,0 +1,44 @@ +# sqlalchemy/processors.py +# Copyright (C) 2010-2021 the SQLAlchemy authors and contributors +# <see AUTHORS file> +# Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +"""defines generic type conversion functions, as used in bind and result +processors. + +They all share one common characteristic: None is passed through unchanged. + +""" +from ._py_processors import str_to_datetime_processor_factory # noqa + +try: + from sqlalchemy.cyextension.processors import ( + DecimalResultProcessor, + ) # noqa + from sqlalchemy.cyextension.processors import int_to_boolean # noqa + from sqlalchemy.cyextension.processors import str_to_date # noqa + from sqlalchemy.cyextension.processors import str_to_datetime # noqa + from sqlalchemy.cyextension.processors import str_to_time # noqa + from sqlalchemy.cyextension.processors import to_float # noqa + from sqlalchemy.cyextension.processors import to_str # noqa + + def to_decimal_processor_factory(target_class, scale): + # Note that the scale argument is not taken into account for integer + # values in the C implementation while it is in the Python one. + # For example, the Python implementation might return + # Decimal('5.00000') whereas the C implementation will + # return Decimal('5'). These are equivalent of course. + return DecimalResultProcessor(target_class, "%%.%df" % scale).process + + +except ImportError: + from ._py_processors import int_to_boolean # noqa + from ._py_processors import str_to_date # noqa + from ._py_processors import str_to_datetime # noqa + from ._py_processors import str_to_time # noqa + from ._py_processors import to_decimal_processor_factory # noqa + from ._py_processors import to_float # noqa + from ._py_processors import to_str # noqa diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index e2f4033e0..ff292c7d7 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -13,7 +13,6 @@ import functools import itertools import operator -from .row import _baserow_usecext from .row import Row from .. import exc from .. import util @@ -21,18 +20,10 @@ from ..sql.base import _generative from ..sql.base import HasMemoized from ..sql.base import InPlaceGenerative - -if _baserow_usecext: - from sqlalchemy.cresultproxy import tuplegetter -else: - - def tuplegetter(*indexes): - it = operator.itemgetter(*indexes) - - if len(indexes) > 1: - return it - else: - return lambda row: (it(row),) +try: + from sqlalchemy.cyextension.resultproxy import tuplegetter +except ImportError: + from ._py_row import tuplegetter class ResultMetaData: @@ -104,7 +95,7 @@ class RMKeyView(collections_abc.KeysView): return iter(self._keys) def __contains__(self, item): - if not _baserow_usecext and isinstance(item, int): + if isinstance(item, int): return False # note this also includes special key fallback behaviors diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py index 43ef093d6..47f5ac1cd 100644 --- a/lib/sqlalchemy/engine/row.py +++ b/lib/sqlalchemy/engine/row.py @@ -11,143 +11,17 @@ import collections.abc as collections_abc import operator -from .. import util from ..sql import util as sql_util -MD_INDEX = 0 # integer index in cursor.description -# This reconstructor is necessary so that pickles with the C extension or -# without use the same Binary format. try: - # We need a different reconstructor on the C extension so that we can - # add extra checks that fields have correctly been initialized by - # __setstate__. - from sqlalchemy.cresultproxy import safe_rowproxy_reconstructor - - # The extra function embedding is needed so that the - # reconstructor function has the same signature whether or not - # the extension is present. - def rowproxy_reconstructor(cls, state): - return safe_rowproxy_reconstructor(cls, state) - - + from sqlalchemy.cyextension.resultproxy import BaseRow + from sqlalchemy.cyextension.resultproxy import KEY_INTEGER_ONLY + from sqlalchemy.cyextension.resultproxy import KEY_OBJECTS_ONLY except ImportError: - - def rowproxy_reconstructor(cls, state): - obj = cls.__new__(cls) - obj.__setstate__(state) - return obj - - -KEY_INTEGER_ONLY = 0 -"""__getitem__ only allows integer values and slices, raises TypeError - otherwise""" - -KEY_OBJECTS_ONLY = 1 -"""__getitem__ only allows string/object values, raises TypeError otherwise""" - -try: - from sqlalchemy.cresultproxy import BaseRow - - _baserow_usecext = True -except ImportError: - _baserow_usecext = False - - class BaseRow: - __slots__ = ("_parent", "_data", "_keymap", "_key_style") - - def __init__(self, parent, processors, keymap, key_style, data): - """Row objects are constructed by CursorResult objects.""" - - object.__setattr__(self, "_parent", parent) - - if processors: - object.__setattr__( - self, - "_data", - tuple( - [ - proc(value) if proc else value - for proc, value in zip(processors, data) - ] - ), - ) - else: - object.__setattr__(self, "_data", tuple(data)) - - object.__setattr__(self, "_keymap", keymap) - - object.__setattr__(self, "_key_style", key_style) - - def __reduce__(self): - return ( - rowproxy_reconstructor, - (self.__class__, self.__getstate__()), - ) - - def _filter_on_values(self, filters): - return Row( - self._parent, - filters, - self._keymap, - self._key_style, - self._data, - ) - - def _values_impl(self): - return list(self) - - def __iter__(self): - return iter(self._data) - - def __len__(self): - return len(self._data) - - def __hash__(self): - return hash(self._data) - - def _get_by_int_impl(self, key): - return self._data[key] - - def _get_by_key_impl(self, key): - if int in key.__class__.__mro__: - return self._data[key] - - assert self._key_style == KEY_INTEGER_ONLY - - if isinstance(key, slice): - return tuple(self._data[key]) - - self._parent._raise_for_nonint(key) - - # The original 1.4 plan was that Row would not allow row["str"] - # access, however as the C extensions were inadvertently allowing - # this coupled with the fact that orm Session sets future=True, - # this allows a softer upgrade path. see #6218 - __getitem__ = _get_by_key_impl - - def _get_by_key_impl_mapping(self, key): - try: - rec = self._keymap[key] - except KeyError as ke: - rec = self._parent._key_fallback(key, ke) - - mdindex = rec[MD_INDEX] - if mdindex is None: - self._parent._raise_for_ambiguous_column_name(rec) - elif ( - self._key_style == KEY_OBJECTS_ONLY - and int in key.__class__.__mro__ - ): - raise KeyError(key) - - return self._data[mdindex] - - def __getattr__(self, name): - try: - return self._get_by_key_impl_mapping(name) - except KeyError as e: - util.raise_(AttributeError(e.args[0]), replace_context=e) + from ._py_row import BaseRow + from ._py_row import KEY_INTEGER_ONLY + from ._py_row import KEY_OBJECTS_ONLY class Row(BaseRow, collections_abc.Sequence): @@ -235,20 +109,6 @@ class Row(BaseRow, collections_abc.Sequence): def __contains__(self, key): return key in self._data - def __getstate__(self): - return { - "_parent": self._parent, - "_data": self._data, - "_key_style": self._key_style, - } - - def __setstate__(self, state): - parent = state["_parent"] - object.__setattr__(self, "_parent", parent) - object.__setattr__(self, "_data", state["_data"]) - object.__setattr__(self, "_keymap", parent._keymap) - object.__setattr__(self, "_key_style", state["_key_style"]) - def _op(self, other, op): return ( op(tuple(self), tuple(other)) @@ -392,12 +252,10 @@ class RowMapping(BaseRow, collections_abc.Mapping): _default_key_style = KEY_OBJECTS_ONLY - if not _baserow_usecext: - - __getitem__ = BaseRow._get_by_key_impl_mapping + __getitem__ = BaseRow._get_by_key_impl_mapping - def _values_impl(self): - return list(self._data) + def _values_impl(self): + return list(self._data) def __iter__(self): return (k for k in self._parent.keys if k is not None) diff --git a/lib/sqlalchemy/engine/util.py b/lib/sqlalchemy/engine/util.py index e88b9ebf3..4cc7df790 100644 --- a/lib/sqlalchemy/engine/util.py +++ b/lib/sqlalchemy/engine/util.py @@ -5,11 +5,15 @@ # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php -import collections.abc as collections_abc - from .. import exc from .. import util -from ..util import immutabledict + +try: + from sqlalchemy.cyextension.util import _distill_params_20 # noqa + from sqlalchemy.cyextension.util import _distill_raw_params # noqa +except ImportError: + from ._py_util import _distill_params_20 # noqa + from ._py_util import _distill_raw_params # noqa def connection_memoize(key): @@ -31,57 +35,6 @@ def connection_memoize(key): return decorated -_no_tuple = () - - -def _distill_params_20(params): - if params is None: - return _no_tuple - elif isinstance(params, (list, tuple)): - # collections_abc.MutableSequence): # avoid abc.__instancecheck__ - if params and not isinstance( - params[0], (collections_abc.Mapping, tuple) - ): - raise exc.ArgumentError( - "List argument must consist only of tuples or dictionaries" - ) - - return params - elif isinstance( - params, - (dict, immutabledict), - # only do abc.__instancecheck__ for Mapping after we've checked - # for plain dictionaries and would otherwise raise - ) or isinstance(params, collections_abc.Mapping): - return [params] - else: - raise exc.ArgumentError("mapping or sequence expected for parameters") - - -def _distill_raw_params(params): - if params is None: - return _no_tuple - elif isinstance(params, (list,)): - # collections_abc.MutableSequence): # avoid abc.__instancecheck__ - if params and not isinstance( - params[0], (collections_abc.Mapping, tuple) - ): - raise exc.ArgumentError( - "List argument must consist only of tuples or dictionaries" - ) - - return params - elif isinstance( - params, - (tuple, dict, immutabledict), - # only do abc.__instancecheck__ for Mapping after we've checked - # for plain dictionaries and would otherwise raise - ) or isinstance(params, collections_abc.Mapping): - return [params] - else: - raise exc.ArgumentError("mapping or sequence expected for parameters") - - class TransactionalContext: """Apply Python context manager behavior to transaction objects. |