diff options
-rw-r--r-- | CHANGES | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/schema.py | 52 | ||||
-rw-r--r-- | test/sql/test_defaults.py | 30 |
3 files changed, 68 insertions, 19 deletions
@@ -442,6 +442,11 @@ underneath "0.7.xx". name. The deprecated fold_equivalents() feature is removed [ticket:1729]. + - [bug] Fixes to the interpretation of the + Column "default" parameter as a callable + to not pass ExecutionContext into a keyword + argument parameter. [ticket:2520] + - [bug] All of UniqueConstraint, ForeignKeyConstraint, CheckConstraint, and PrimaryKeyConstraint will attach themselves to their parent table automatically diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index f2ca45894..4ce27582b 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -771,11 +771,12 @@ class Column(SchemaItem, expression.ColumnClause): at :ref:`post_update`. :param default: A scalar, Python callable, or - :class:`~sqlalchemy.sql.expression.ClauseElement` representing the + :class:`.ColumnElement` expression representing the *default value* for this column, which will be invoked upon insert if this column is otherwise not specified in the VALUES clause of the insert. This is a shortcut to using :class:`.ColumnDefault` as - a positional argument. + a positional argument; see that class for full detail on the + structure of the argument. Contrast this argument to ``server_default`` which creates a default generator on the database side. @@ -1532,6 +1533,31 @@ class ColumnDefault(DefaultGenerator): """ def __init__(self, arg, **kwargs): + """"Construct a new :class:`.ColumnDefault`. + + + :param arg: argument representing the default value. + May be one of the following: + + * a plain non-callable Python value, such as a + string, integer, boolean, or other simple type. + The default value will be used as is each time. + * a SQL expression, that is one which derives from + :class:`.ColumnElement`. The SQL expression will + be rendered into the INSERT or UPDATE statement, + or in the case of a primary key column when + RETURNING is not used may be + pre-executed before an INSERT within a SELECT. + * A Python callable. The function will be invoked for each + new row subject to an INSERT or UPDATE. + The callable must accept exactly + zero or one positional arguments. The one-argument form + will receive an instance of the :class:`.ExecutionContext`, + which provides contextual information as to the current + :class:`.Connection` in use as well as the current + statement and parameters. + + """ super(ColumnDefault, self).__init__(**kwargs) if isinstance(arg, FetchedValue): raise exc.ArgumentError( @@ -1555,8 +1581,18 @@ class ColumnDefault(DefaultGenerator): not self.is_sequence def _maybe_wrap_callable(self, fn): - """Backward compat: Wrap callables that don't accept a context.""" + """Wrap callables that don't accept a context. + The alternative here is to require that + a simple callable passed to "default" would need + to be of the form "default=lambda ctx: datetime.now". + That is the more "correct" way to go, but the case + of using a zero-arg callable for "default" is so + much more prominent than the context-specific one + I'm having trouble justifying putting that inconvenience + on everyone. + + """ if inspect.isfunction(fn): inspectable = fn elif inspect.isclass(fn): @@ -1571,7 +1607,8 @@ class ColumnDefault(DefaultGenerator): except TypeError: return lambda ctx: fn() - positionals = len(argspec[0]) + defaulted = argspec[3] is not None and len(argspec[3]) or 0 + positionals = len(argspec[0]) - defaulted # Py3K compat - no unbound methods if inspect.ismethod(inspectable) or inspect.isclass(fn): @@ -1579,13 +1616,12 @@ class ColumnDefault(DefaultGenerator): if positionals == 0: return lambda ctx: fn() - - defaulted = argspec[3] is not None and len(argspec[3]) or 0 - if positionals - defaulted > 1: + elif positionals == 1: + return fn + else: raise exc.ArgumentError( "ColumnDefault Python function takes zero or one " "positional arguments") - return fn def _visit_name(self): if self.for_update: diff --git a/test/sql/test_defaults.py b/test/sql/test_defaults.py index 7a6c6d009..55aa86633 100644 --- a/test/sql/test_defaults.py +++ b/test/sql/test_defaults.py @@ -132,10 +132,13 @@ class DefaultTest(fixtures.TestBase): def test_bad_arg_signature(self): ex_msg = \ - "ColumnDefault Python function takes zero or one positional arguments" + "ColumnDefault Python function takes zero "\ + "or one positional arguments" - def fn1(x, y): pass - def fn2(x, y, z=3): pass + def fn1(x, y): + pass + def fn2(x, y, z=3): + pass class fn3(object): def __init__(self, x, y): pass @@ -150,28 +153,33 @@ class DefaultTest(fixtures.TestBase): sa.ColumnDefault, fn) def test_arg_signature(self): - def fn1(): pass - def fn2(): pass - def fn3(x=1): pass - def fn4(x=1, y=2, z=3): pass + def fn1(): + pass + def fn2(): + pass + def fn3(x=1): + eq_(x, 1) + def fn4(x=1, y=2, z=3): + eq_(x, 1) fn5 = list class fn6(object): def __init__(self, x): - pass + eq_(x, "context") class fn6(object): def __init__(self, x, y=3): - pass + eq_(x, "context") class FN7(object): def __call__(self, x): - pass + eq_(x, "context") fn7 = FN7() class FN8(object): def __call__(self, x, y=3): - pass + eq_(x, "context") fn8 = FN8() for fn in fn1, fn2, fn3, fn4, fn5, fn6, fn7, fn8: c = sa.ColumnDefault(fn) + c.arg("context") @testing.fails_on('firebird', 'Data type unknown') def test_standalone(self): |