diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-01-18 19:26:56 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-01-18 19:26:56 -0500 |
commit | 1af8e2491dcbed723d2cdafd44fd37f1a6908e91 (patch) | |
tree | e8da1423783c09d480905bb9fe84dc86b8bd0a0a /lib/sqlalchemy/sql | |
parent | 4dfc7fb08716c6f4995dd656a24f092ad0cc91f4 (diff) | |
download | sqlalchemy-1af8e2491dcbed723d2cdafd44fd37f1a6908e91.tar.gz |
- implement kwarg validation and type system for dialect-specific
arguments; [ticket:2866]
- add dialect specific kwarg functionality to ForeignKeyConstraint, ForeignKey
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r-- | lib/sqlalchemy/sql/base.py | 119 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/dml.py | 17 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/schema.py | 92 |
3 files changed, 183 insertions, 45 deletions
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index e6c5d6ed7..f4bfe392a 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -12,7 +12,8 @@ from .. import util, exc import itertools from .visitors import ClauseVisitor - +import re +import collections PARSE_AUTOCOMMIT = util.symbol('PARSE_AUTOCOMMIT') NO_ARG = util.symbol('NO_ARG') @@ -43,6 +44,122 @@ def _generative(fn, *args, **kw): return self +class DialectKWArgs(object): + """Establish the ability for a class to have dialect-specific arguments + with defaults and validation. + + """ + + @util.memoized_property + def dialect_kwargs(self): + """A collection of keyword arguments specified as dialect-specific + options to this construct. + + The arguments are present here in their original ``<dialect>_<kwarg>`` + format. + + .. versionadded:: 0.9.2 + + .. seealso:: + + :attr:`.DialectKWArgs.dialect_options` - nested dictionary form + + """ + + return util.immutabledict( + ( + "%s_%s" % (dialect_name, kwarg_name), + kw_dict[kwarg_name] + ) + for dialect_name, kw_dict in self.dialect_options.items() + for kwarg_name in kw_dict if kwarg_name != '*' + ) + + @property + def kwargs(self): + """Deprecated; see :attr:`.DialectKWArgs.dialect_kwargs""" + return self.dialect_kwargs + + @util.dependencies("sqlalchemy.dialects") + def _kw_reg_for_dialect(dialects, dialect_name): + dialect_cls = dialects.registry.load(dialect_name) + if dialect_cls.construct_arguments is None: + return None + return dict(dialect_cls.construct_arguments) + _kw_registry = util.PopulateDict(_kw_reg_for_dialect) + + def _kw_reg_for_dialect_cls(self, dialect_name): + construct_arg_dictionary = DialectKWArgs._kw_registry[dialect_name] + if construct_arg_dictionary is None: + return {"*": None} + else: + d = {} + for cls in reversed(self.__class__.__mro__): + if cls in construct_arg_dictionary: + d.update(construct_arg_dictionary[cls]) + return d + + @util.memoized_property + def dialect_options(self): + """A collection of keyword arguments specified as dialect-specific + options to this construct. + + This is a two-level nested registry, keyed to ``<dialect_name>`` + and ``<argument_name>``. For example, the ``postgresql_where`` argument + would be locatable as:: + + arg = my_object.dialect_options['postgresql']['where'] + + .. versionadded:: 0.9.2 + + .. seealso:: + + :attr:`.DialectKWArgs.dialect_kwargs` - flat dictionary form + + """ + + return util.PopulateDict( + util.portable_instancemethod(self._kw_reg_for_dialect_cls) + ) + + def _validate_dialect_kwargs(self, kwargs): + # validate remaining kwargs that they all specify DB prefixes + + if not kwargs: + return + + self.__dict__.pop('dialect_kwargs', None) + + for k in kwargs: + m = re.match('^(.+?)_(.+)$', k) + if m is None: + raise TypeError("Additional arguments should be " + "named <dialectname>_<argument>, got '%s'" % k) + dialect_name, arg_name = m.group(1, 2) + + try: + construct_arg_dictionary = self.dialect_options[dialect_name] + except exc.NoSuchModuleError: + util.warn( + "Can't validate argument %r; can't " + "locate any SQLAlchemy dialect named %r" % + (k, dialect_name)) + self.dialect_options[dialect_name] = { + "*": None, + arg_name: kwargs[k]} + else: + if "*" not in construct_arg_dictionary and \ + arg_name not in construct_arg_dictionary: + raise exc.ArgumentError( + "Argument %r is not accepted by " + "dialect %r on behalf of %r" % ( + k, + dialect_name, self.__class__ + )) + else: + construct_arg_dictionary[arg_name] = kwargs[k] + + class Generative(object): """Allow a ClauseElement to generate itself via the @_generative decorator. diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 22694348b..854b894ee 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -8,13 +8,13 @@ Provide :class:`.Insert`, :class:`.Update` and :class:`.Delete`. """ -from .base import Executable, _generative, _from_objects +from .base import Executable, _generative, _from_objects, DialectKWArgs from .elements import ClauseElement, _literal_as_text, Null, and_, _clone from .selectable import _interpret_as_from, _interpret_as_select, HasPrefixes from .. import util from .. import exc -class UpdateBase(HasPrefixes, Executable, ClauseElement): +class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): """Form the base for ``INSERT``, ``UPDATE``, and ``DELETE`` statements. """ @@ -23,7 +23,6 @@ class UpdateBase(HasPrefixes, Executable, ClauseElement): _execution_options = \ Executable._execution_options.union({'autocommit': True}) - kwargs = util.immutabledict() _hints = util.immutabledict() _prefixes = () @@ -417,7 +416,7 @@ class Insert(ValuesBase): prefixes=None, returning=None, return_defaults=False, - **kwargs): + **dialect_kw): """Construct an :class:`.Insert` object. Similar functionality is available via the @@ -462,7 +461,7 @@ class Insert(ValuesBase): self.select = self.select_names = None self.inline = inline self._returning = returning - self.kwargs = kwargs + self._validate_dialect_kwargs(dialect_kw) self._return_defaults = return_defaults def get_children(self, **kwargs): @@ -547,7 +546,7 @@ class Update(ValuesBase): prefixes=None, returning=None, return_defaults=False, - **kwargs): + **dialect_kw): """Construct an :class:`.Update` object. E.g.:: @@ -658,7 +657,7 @@ class Update(ValuesBase): else: self._whereclause = None self.inline = inline - self.kwargs = kwargs + self._validate_dialect_kwargs(dialect_kw) self._return_defaults = return_defaults @@ -716,7 +715,7 @@ class Delete(UpdateBase): bind=None, returning=None, prefixes=None, - **kwargs): + **dialect_kw): """Construct :class:`.Delete` object. Similar functionality is available via the @@ -746,7 +745,7 @@ class Delete(UpdateBase): else: self._whereclause = None - self.kwargs = kwargs + self._validate_dialect_kwargs(dialect_kw) def get_children(self, **kwargs): if self._whereclause is not None: diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 6ee92871a..73c2a49c8 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -28,10 +28,9 @@ as components in SQL expressions. """ -import re import inspect from .. import exc, util, event, inspection -from .base import SchemaEventTarget +from .base import SchemaEventTarget, DialectKWArgs from . import visitors from . import type_api from .base import _bind_or_error, ColumnCollection @@ -53,14 +52,6 @@ def _get_table_key(name, schema): return schema + "." + name -def _validate_dialect_kwargs(kwargs, name): - # validate remaining kwargs that they all specify DB prefixes - - for k in kwargs: - m = re.match('^(.+?)_.*', k) - if m is None: - raise TypeError("Additional arguments should be " - "named <dialectname>_<argument>, got '%s'" % k) @inspection._self_inspects class SchemaItem(SchemaEventTarget, visitors.Visitable): @@ -115,7 +106,7 @@ class SchemaItem(SchemaEventTarget, visitors.Visitable): return schema_item -class Table(SchemaItem, TableClause): +class Table(DialectKWArgs, SchemaItem, TableClause): """Represent a table in a database. e.g.:: @@ -296,9 +287,13 @@ class Table(SchemaItem, TableClause): ``quote_schema=True`` to the constructor, or use the :class:`.quoted_name` construct to specify the name. - :param useexisting: Deprecated. Use extend_existing. + :param \**kw: Additional keyword arguments not mentioned above are + dialect specific, and passed in the form ``<dialectname>_<argname>``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. + """ __visit_name__ = 'table' @@ -397,7 +392,6 @@ class Table(SchemaItem, TableClause): PrimaryKeyConstraint()._set_parent_with_dispatch(self) self.foreign_keys = set() self._extra_dependencies = set() - self.kwargs = {} if self.schema is not None: self.fullname = "%s.%s" % (self.schema, self.name) else: @@ -502,9 +496,7 @@ class Table(SchemaItem, TableClause): self._init_items(*args) def _extra_kwargs(self, **kwargs): - # validate remaining kwargs that they all specify DB prefixes - _validate_dialect_kwargs(kwargs, "Table") - self.kwargs.update(kwargs) + self._validate_dialect_kwargs(kwargs) def _init_collections(self): pass @@ -1254,7 +1246,7 @@ class Column(SchemaItem, ColumnClause): return ColumnClause.get_children(self, **kwargs) -class ForeignKey(SchemaItem): +class ForeignKey(DialectKWArgs, SchemaItem): """Defines a dependency between two columns. ``ForeignKey`` is specified as an argument to a :class:`.Column` object, @@ -1295,7 +1287,8 @@ class ForeignKey(SchemaItem): def __init__(self, column, _constraint=None, use_alter=False, name=None, onupdate=None, ondelete=None, deferrable=None, - initially=None, link_to_name=False, match=None): + initially=None, link_to_name=False, match=None, + **dialect_kw): """ Construct a column-level FOREIGN KEY. @@ -1345,6 +1338,14 @@ class ForeignKey(SchemaItem): DDL for this constraint. Typical values include SIMPLE, PARTIAL and FULL. + :param \**dialect_kw: Additional keyword arguments are dialect specific, + and passed in the form ``<dialectname>_<argname>``. The arguments + are ultimately handled by a corresponding :class:`.ForeignKeyConstraint`. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. + + .. versionadded:: 0.9.2 + """ self._colspec = column @@ -1381,6 +1382,7 @@ class ForeignKey(SchemaItem): self.initially = initially self.link_to_name = link_to_name self.match = match + self._unvalidated_dialect_kw = dialect_kw def __repr__(self): return "ForeignKey(%r)" % self._get_colspec() @@ -1410,7 +1412,8 @@ class ForeignKey(SchemaItem): deferrable=self.deferrable, initially=self.initially, link_to_name=self.link_to_name, - match=self.match + match=self.match, + **self._unvalidated_dialect_kw ) return self._schema_item_copy(fk) @@ -1651,6 +1654,7 @@ class ForeignKey(SchemaItem): onupdate=self.onupdate, ondelete=self.ondelete, deferrable=self.deferrable, initially=self.initially, match=self.match, + **self._unvalidated_dialect_kw ) self.constraint._elements[self.parent] = self self.constraint._set_parent_with_dispatch(table) @@ -2113,14 +2117,14 @@ class PassiveDefault(DefaultClause): DefaultClause.__init__(self, *arg, **kw) -class Constraint(SchemaItem): +class Constraint(DialectKWArgs, SchemaItem): """A table-level SQL constraint.""" __visit_name__ = 'constraint' def __init__(self, name=None, deferrable=None, initially=None, _create_rule=None, - **kw): + **dialect_kw): """Create a SQL constraint. :param name: @@ -2151,9 +2155,10 @@ class Constraint(SchemaItem): _create_rule is used by some types to create constraints. Currently, its call signature is subject to change at any time. - :param \**kwargs: - Dialect-specific keyword parameters, see the documentation - for various dialects and constraints regarding options here. + :param \**dialect_kw: Additional keyword arguments are dialect specific, + and passed in the form ``<dialectname>_<argname>``. See the + documentation regarding an individual dialect at :ref:`dialect_toplevel` + for detail on documented arguments. """ @@ -2162,8 +2167,7 @@ class Constraint(SchemaItem): self.initially = initially self._create_rule = _create_rule util.set_creation_order(self) - _validate_dialect_kwargs(kw, self.__class__.__name__) - self.kwargs = kw + self._validate_dialect_kwargs(dialect_kw) @property def table(self): @@ -2237,6 +2241,9 @@ class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint): Optional string. If set, emit INITIALLY <value> when issuing DDL for this constraint. + :param \**kw: other keyword arguments including dialect-specific + arguments are propagated to the :class:`.Constraint` superclass. + """ ColumnCollectionMixin.__init__(self, *columns) Constraint.__init__(self, **kw) @@ -2354,7 +2361,7 @@ class ForeignKeyConstraint(Constraint): def __init__(self, columns, refcolumns, name=None, onupdate=None, ondelete=None, deferrable=None, initially=None, use_alter=False, - link_to_name=False, match=None, table=None): + link_to_name=False, match=None, table=None, **dialect_kw): """Construct a composite-capable FOREIGN KEY. :param columns: A sequence of local column names. The named columns @@ -2399,9 +2406,16 @@ class ForeignKeyConstraint(Constraint): DDL for this constraint. Typical values include SIMPLE, PARTIAL and FULL. + :param \**dialect_kw: Additional keyword arguments are dialect specific, + and passed in the form ``<dialectname>_<argname>``. See the + documentation regarding an individual dialect at :ref:`dialect_toplevel` + for detail on documented arguments. + + .. versionadded:: 0.9.2 + """ super(ForeignKeyConstraint, self).\ - __init__(name, deferrable, initially) + __init__(name, deferrable, initially, **dialect_kw) self.onupdate = onupdate self.ondelete = ondelete @@ -2428,7 +2442,8 @@ class ForeignKeyConstraint(Constraint): link_to_name=self.link_to_name, match=self.match, deferrable=self.deferrable, - initially=self.initially + initially=self.initially, + **self.dialect_kwargs ) if table is not None: @@ -2552,7 +2567,7 @@ class UniqueConstraint(ColumnCollectionConstraint): __visit_name__ = 'unique_constraint' -class Index(ColumnCollectionMixin, SchemaItem): +class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): """A table-level INDEX. Defines a composite (one or more column) INDEX. @@ -2613,11 +2628,18 @@ class Index(ColumnCollectionMixin, SchemaItem): be arbitrary SQL expressions which ultmately refer to a :class:`.Column`. - :param unique: - Defaults to False: create a unique index. + :param unique=False: + Keyword only argument; if True, create a unique index. + + :param quote=None: + Keyword only argument; whether to apply quoting to the name of + the index. Works in the same manner as that of + :paramref:`.Column.quote`. - :param \**kw: - Other keyword arguments may be interpreted by specific dialects. + :param \**kw: Additional keyword arguments not mentioned above are + dialect specific, and passed in the form ``<dialectname>_<argname>``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. """ self.table = None @@ -2637,7 +2659,7 @@ class Index(ColumnCollectionMixin, SchemaItem): self.expressions = expressions self.name = quoted_name(name, kw.pop("quote", None)) self.unique = kw.pop('unique', False) - self.kwargs = kw + self._validate_dialect_kwargs(kw) # will call _set_parent() if table-bound column # objects are present |