- get util.get_callable_argspec() to be completely bulletproof for 2.6-3.4,

methods, classes, builtins, functools.partial(), everything known so far
- use get_callable_argspec() within ColumnDefault._maybe_wrap_callable, re: #2979
This commit is contained in:
Mike Bayer
2014-03-02 13:59:06 -05:00
parent 6750c39a4f
commit bf89ca2e10
4 changed files with 123 additions and 40 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ class _DispatchDescriptor(RefCollection):
if self.legacy_signatures:
try:
argspec = util.get_callable_argspec(fn, no_self=True)
except ValueError:
except TypeError:
pass
else:
fn = legacy._wrap_fn_for_legacy(self, fn, argspec)
+3 -27
View File
@@ -1836,42 +1836,18 @@ class ColumnDefault(DefaultGenerator):
def _maybe_wrap_callable(self, fn):
"""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.
This is to allow easy compatiblity with default callables
that aren't specific to accepting of a context.
"""
# TODO: why aren't we using a util.langhelpers function
# for this? e.g. get_callable_argspec
if isinstance(fn, (types.BuiltinMethodType, types.BuiltinFunctionType)):
return lambda ctx: fn()
elif inspect.isfunction(fn) or inspect.ismethod(fn):
inspectable = fn
elif inspect.isclass(fn):
inspectable = fn.__init__
elif hasattr(fn, '__call__'):
inspectable = fn.__call__
else:
# probably not inspectable, try anyways.
inspectable = fn
try:
argspec = inspect.getargspec(inspectable)
argspec = util.get_callable_argspec(fn, no_self=True)
except TypeError:
return lambda ctx: fn()
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):
positionals -= 1
if positionals == 0:
return lambda ctx: fn()
elif positionals == 1:
+33 -11
View File
@@ -260,20 +260,42 @@ def get_func_kwargs(func):
return compat.inspect_getargspec(func)[0]
def get_callable_argspec(fn, no_self=False):
if isinstance(fn, types.FunctionType):
return compat.inspect_getargspec(fn)
elif isinstance(fn, types.MethodType) and no_self:
spec = compat.inspect_getargspec(fn.__func__)
return compat.ArgSpec(spec.args[1:], spec.varargs, spec.keywords, spec.defaults)
def get_callable_argspec(fn, no_self=False, _is_init=False):
"""Return the argument signature for any callable.
All pure-Python callables are accepted, including
functions, methods, classes, objects with __call__;
builtins and other edge cases like functools.partial() objects
raise a TypeError.
"""
if inspect.isbuiltin(fn):
raise TypeError("Can't inspect builtin: %s" % fn)
elif inspect.isfunction(fn):
if _is_init and no_self:
spec = compat.inspect_getargspec(fn)
return compat.ArgSpec(spec.args[1:], spec.varargs,
spec.keywords, spec.defaults)
else:
return compat.inspect_getargspec(fn)
elif inspect.ismethod(fn):
if no_self and (_is_init or fn.__self__):
spec = compat.inspect_getargspec(fn.__func__)
return compat.ArgSpec(spec.args[1:], spec.varargs,
spec.keywords, spec.defaults)
else:
return compat.inspect_getargspec(fn.__func__)
elif inspect.isclass(fn):
return get_callable_argspec(fn.__init__, no_self=no_self, _is_init=True)
elif hasattr(fn, '__func__'):
return compat.inspect_getargspec(fn.__func__)
elif hasattr(fn, '__call__') and \
not hasattr(fn.__call__, '__call__'): # functools.partial does this;
# not much we can do
return get_callable_argspec(fn.__call__)
elif hasattr(fn, '__call__'):
if inspect.ismethod(fn.__call__):
return get_callable_argspec(fn.__call__, no_self=no_self)
else:
raise TypeError("Can't inspect callable: %s" % fn)
else:
raise ValueError("Can't inspect function: %s" % fn)
raise TypeError("Can't inspect callable: %s" % fn)
def format_argspec_plus(fn, grouped=True):
"""Returns a dictionary of formatted, introspected function arguments.
+86 -1
View File
@@ -1377,6 +1377,35 @@ class ArgInspectionTest(fixtures.TestBase):
(['x', 'y'], None, 'kw', None)
)
def test_callable_argspec_fn_no_self(self):
def foo(x, y, **kw):
pass
eq_(
get_callable_argspec(foo, no_self=True),
(['x', 'y'], None, 'kw', None)
)
def test_callable_argspec_fn_no_self_but_self(self):
def foo(self, x, y, **kw):
pass
eq_(
get_callable_argspec(foo, no_self=True),
(['self', 'x', 'y'], None, 'kw', None)
)
def test_callable_argspec_py_builtin(self):
import datetime
assert_raises(
TypeError,
get_callable_argspec, datetime.datetime.now
)
def test_callable_argspec_obj_init(self):
assert_raises(
TypeError,
get_callable_argspec, object
)
def test_callable_argspec_method(self):
class Foo(object):
def foo(self, x, y, **kw):
@@ -1386,6 +1415,62 @@ class ArgInspectionTest(fixtures.TestBase):
(['self', 'x', 'y'], None, 'kw', None)
)
def test_callable_argspec_instance_method_no_self(self):
class Foo(object):
def foo(self, x, y, **kw):
pass
eq_(
get_callable_argspec(Foo().foo, no_self=True),
(['x', 'y'], None, 'kw', None)
)
def test_callable_argspec_unbound_method_no_self(self):
class Foo(object):
def foo(self, x, y, **kw):
pass
eq_(
get_callable_argspec(Foo.foo, no_self=True),
(['self', 'x', 'y'], None, 'kw', None)
)
def test_callable_argspec_init(self):
class Foo(object):
def __init__(self, x, y):
pass
eq_(
get_callable_argspec(Foo),
(['self', 'x', 'y'], None, None, None)
)
def test_callable_argspec_init_no_self(self):
class Foo(object):
def __init__(self, x, y):
pass
eq_(
get_callable_argspec(Foo, no_self=True),
(['x', 'y'], None, None, None)
)
def test_callable_argspec_call(self):
class Foo(object):
def __call__(self, x, y):
pass
eq_(
get_callable_argspec(Foo()),
(['self', 'x', 'y'], None, None, None)
)
def test_callable_argspec_call_no_self(self):
class Foo(object):
def __call__(self, x, y):
pass
eq_(
get_callable_argspec(Foo(), no_self=True),
(['x', 'y'], None, None, None)
)
def test_callable_argspec_partial(self):
from functools import partial
def foo(x, y, z, **kw):
@@ -1393,7 +1478,7 @@ class ArgInspectionTest(fixtures.TestBase):
bar = partial(foo, 5)
assert_raises(
ValueError,
TypeError,
get_callable_argspec, bar
)