From: Mike Bayer Date: Wed, 28 Sep 2011 17:04:42 +0000 (-0400) Subject: - Enhanced the instrumentation in the ORM to support X-Git-Tag: rel_0_7_3~17 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4f1321c3e97f9bb1c92b378452a7810874927c71;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Enhanced the instrumentation in the ORM to support Py3K's new argument style of "required kw arguments", i.e. fn(a, b, *, c, d), fn(a, b, *args, c, d). Argument signatures of mapped object's __init__ method will be preserved, including required kw rules. [ticket:2237] --- diff --git a/CHANGES b/CHANGES index cbe87341ba..16333d494b 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,13 @@ CHANGES attribute, provided the name is the same as that of the entity mapped column. + - Enhanced the instrumentation in the ORM to support + Py3K's new argument style of "required kw arguments", + i.e. fn(a, b, *, c, d), fn(a, b, *args, c, d). + Argument signatures of mapped object's __init__ + method will be preserved, including required kw rules. + [ticket:2237] + - Fixed bug in unit of work whereby detection of "cycles" among classes in highly interlinked patterns would not produce a deterministic diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index f14b752533..89e9ae8ae2 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -656,6 +656,7 @@ def __init__(%(apply_pos)s): # Py3K #func_defaults = getattr(original__init__, '__defaults__', None) + #func_kw_defaults = getattr(original__init__, '__kwdefaults__', None) # Py2K func = getattr(original__init__, 'im_func', original__init__) func_defaults = getattr(func, 'func_defaults', None) @@ -667,4 +668,7 @@ def __init__(%(apply_pos)s): __init__.__doc__ = original__init__.__doc__ if func_defaults: __init__.func_defaults = func_defaults + # Py3K + #if func_kw_defaults: + # __init__.__kwdefaults__ = func_kw_defaults return __init__ diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index 866c0c2167..380e3a9a59 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -89,6 +89,11 @@ if sys.version_info < (2, 6): else: from urlparse import parse_qsl +if py3k: + from inspect import getfullargspec as inspect_getfullargspec +else: + from inspect import getargspec as inspect_getfullargspec + if py3k: # they're bringing it back in 3.2. brilliant ! def callable(fn): diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index cf8b2acac8..722003796d 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -15,7 +15,7 @@ import re import sys import types import warnings -from compat import update_wrapper, set_types, threading +from compat import update_wrapper, set_types, threading, callable, inspect_getfullargspec, py3k from sqlalchemy import exc def _unique_symbols(used, *bases): @@ -38,7 +38,7 @@ def decorator(target): def decorate(fn): if not inspect.isfunction(fn): raise Exception("not a decoratable function") - spec = inspect.getargspec(fn) + spec = inspect_getfullargspec(fn) names = tuple(spec[0]) + spec[1:3] + (fn.func_name,) targ_name, fn_name = _unique_symbols(names, 'target', 'fn') @@ -149,7 +149,11 @@ def format_argspec_plus(fn, grouped=True): 'apply_pos': '(self, a, b, c, **d)'} """ - spec = callable(fn) and inspect.getargspec(fn) or fn + if callable(fn): + spec = inspect_getfullargspec(fn) + else: + # we accept an existing argspec... + spec = fn args = inspect.formatargspec(*spec) if spec[0]: self_arg = spec[0][0] @@ -157,9 +161,28 @@ def format_argspec_plus(fn, grouped=True): self_arg = '%s[0]' % spec[1] else: self_arg = None - apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2]) - defaulted_vals = spec[3] is not None and spec[0][0-len(spec[3]):] or () - apply_kw = inspect.formatargspec(spec[0], spec[1], spec[2], defaulted_vals, + + if py3k: + apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2], None, spec[4]) + num_defaults = 0 + if spec[3]: + num_defaults += len(spec[3]) + if spec[4]: + num_defaults += len(spec[4]) + name_args = spec[0] + spec[4] + else: + apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2]) + num_defaults = 0 + if spec[3]: + num_defaults += len(spec[3]) + name_args = spec[0] + + if num_defaults: + defaulted_vals = name_args[0-num_defaults:] + else: + defaulted_vals = () + + apply_kw = inspect.formatargspec(name_args, spec[1], spec[2], defaulted_vals, formatvalue=lambda x: '=' + x) if grouped: return dict(args=args, self_arg=self_arg, diff --git a/test/lib/requires.py b/test/lib/requires.py index 640af671e9..e27d0193c6 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -328,6 +328,15 @@ def python2(fn): ) ) +def python3(fn): + return _chain_decorators_on( + fn, + skip_if( + lambda: sys.version_info < (3,), + "Python version 3.xx is required." + ) + ) + def python26(fn): return _chain_decorators_on( fn, diff --git a/test/orm/test_instrumentation.py b/test/orm/test_instrumentation.py index cb952b4b33..685791ba3c 100644 --- a/test/orm/test_instrumentation.py +++ b/test/orm/test_instrumentation.py @@ -8,7 +8,7 @@ from test.lib.schema import Table from test.lib.schema import Column from test.lib.testing import eq_, ne_ from test.lib.util import decorator -from test.lib import fixtures +from test.lib import fixtures, testing @decorator def modifies_instrumentation_finders(fn, *args, **kw): @@ -622,6 +622,82 @@ class NativeInstrumentationTest(fixtures.ORMTest): class T(object): pass assert_raises(KeyError, mapper, T, t) +class Py3KFunctionInstTest(fixtures.ORMTest): + __requires__ = ("python3", ) + + # Py3K + #def _kw_only_fixture(self): + # class A(object): + # def __init__(self, a, *, b, c): + # self.a = a + # self.b = b + # self.c = c + # return self._instrument(A) + # + #def _kw_plus_posn_fixture(self): + # class A(object): + # def __init__(self, a, *args, b, c): + # self.a = a + # self.b = b + # self.c = c + # return self._instrument(A) + # + #def _kw_opt_fixture(self): + # class A(object): + # def __init__(self, a, *, b, c="c"): + # self.a = a + # self.b = b + # self.c = c + # return self._instrument(A) + + def _instrument(self, cls): + manager = instrumentation.register_class(cls) + canary = [] + def check(target, args, kwargs): + canary.append((args, kwargs)) + event.listen(manager, "init", check) + return cls, canary + + def test_kw_only_args(self): + cls, canary = self._kw_only_fixture() + + a = cls("a", b="b", c="c") + eq_(canary, [(('a', ), {'b':'b','c':'c'})]) + + def test_kw_plus_posn_args(self): + cls, canary = self._kw_plus_posn_fixture() + + a = cls("a", 1, 2, 3, b="b", c="c") + eq_(canary, [(('a', 1, 2, 3), {'b':'b','c':'c'})]) + + def test_kw_only_args_plus_opt(self): + cls, canary = self._kw_opt_fixture() + + a = cls("a", b="b") + eq_(canary, [(('a', ), {'b':'b','c':'c'})]) + + canary[:] = [] + a = cls("a", b="b", c="d") + eq_(canary, [(('a', ), {'b':'b','c':'d'})]) + + def test_kw_only_sig(self): + cls, canary = self._kw_only_fixture() + assert_raises( + TypeError, + cls, "a", "b", "c" + ) + + def test_kw_plus_opt_sig(self): + cls, canary = self._kw_only_fixture() + assert_raises( + TypeError, + cls, "a", "b", "c" + ) + + assert_raises( + TypeError, + cls, "a", "b", c="c" + ) class MiscTest(fixtures.ORMTest): """Seems basic, but not directly covered elsewhere!"""