Commits

Mike Bayer committed 5e85a7e

- [bug] Fixes to the interpretation of the
Column "default" parameter as a callable
to not pass ExecutionContext into a keyword
argument parameter. [ticket:2520]

  • Participants
  • Parent commits 86d2bdc

Comments (0)

Files changed (3)

     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

lib/sqlalchemy/schema.py

               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.
     """
 
     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(
                     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):
         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):
 
         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:

test/sql/test_defaults.py

 
     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
                                      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):