Skip to content

Commit 2a4fd71

Browse files
Release Managervbraun
authored andcommitted
Trac #21322: coerce_binop has dangerous behaviour wrt additional arguments
`@coerce_binop` is an important decorator used on binary operators to automagically apply the coercion framework on its arguments. It's widely used on e.g. `gcd` operations (`_add_`, `_sub_` etc. have coercion handled with another mechanism). The current `@coerce_binop` has the following dangerous behaviour, where, if the decorated method is given an extra unnamed argument, the `self`-argument is swallowed: {{{ sage: P.<x> = GF(5)[] sage: f = x^2 sage: g = x sage: f.gcd(g) x sage: f.gcd(g, 1) 1 }}} The reason is that `@coerce_binop` included a hack to allow calls such as `Polynomial.gcd(f,g)` where `f` and `g` are polynomials and the `gcd` method is decorated with `@coerce_binop`. The reason that this required a hack at all is that `@coerce_binop` was implemented as a class: it seems that creating decorators as classes implies a very unfortunate behaviour: {{{ class MyDec: def __init__(self, f): self.f = f def __call__(self, x): print "decorated: ", self , x self.f(x) mydec = MyDec class A: def __init__(self, a): self.a = a @mydec def met(self, x): print "x:", x myA = A(1) myA.met(5) }}} the above prints something like {{{ decorated: <__main__.MyDec instance at 0x7f63c5ab6c20> 5 }}} and then crashes with {{{ AttributeError: MyDec instance has no attribute 'a' }}} This is because the `self` in `__call__` of `MyDec` becomes the `MyDec` instance -- the `A` instance is never passed to the decorating class instance! The solution here is to rewrite `coerce_binop` as a function. At the same time, we can use `@sage_wraps` which ensures proper documentation (e.g. source file and line which was not retained in the previous `coerce_binop`). URL: https://trac.sagemath.org/21322 Reported by: jsrn Ticket author(s): Johan Rosenkilde, Xavier Caruso Reviewer(s): Miguel Marco
2 parents e009780 + 7344f3a commit 2a4fd71

File tree

4 files changed

+108
-162
lines changed

4 files changed

+108
-162
lines changed

src/sage/categories/euclidean_domains.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,9 @@ def gcd(self, other):
250250
251251
EXAMPLES::
252252
253-
sage: EuclideanDomains().ElementMethods().gcd(6,4)
254-
2
253+
sage: R.<x> = PolynomialRing(QQ, sparse=True)
254+
sage: EuclideanDomains().element_class.gcd(x,x+1)
255+
-1
255256
"""
256257
A = self
257258
B = other

src/sage/categories/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def _gcd_univariate_polynomial(self, f, g):
235235
x
236236
237237
"""
238-
ret = EuclideanDomains().ElementMethods().gcd(f,g)
238+
ret = EuclideanDomains().element_class.gcd(f,g)
239239
c = ret.leading_coefficient()
240240
if c.is_unit():
241241
return (1/c)*ret

src/sage/rings/polynomial/padics/polynomial_padic_capped_relative_dense.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,8 +1139,7 @@ def xgcd(self, right):
11391139
from sage.misc.stopgap import stopgap
11401140
stopgap("Extended gcd computations over p-adic fields are performed using the standard Euclidean algorithm which might produce mathematically incorrect results in some cases.", 13439)
11411141

1142-
from sage.rings.polynomial.polynomial_element_generic import Polynomial_generic_field
1143-
return Polynomial_generic_field.xgcd(self,right)
1142+
return Polynomial_generic_cdv.xgcd(self,right)
11441143

11451144
#def discriminant(self):
11461145
# raise NotImplementedError

src/sage/structure/element.pyx

Lines changed: 103 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ from sage.misc import sageinspect
154154
from sage.misc.classcall_metaclass cimport ClasscallMetaclass
155155
from sage.misc.superseded import deprecated_function_alias
156156
from sage.arith.numerical_approx cimport digits_to_bits
157+
from sage.misc.decorators import sage_wraps
157158

158159
# Create a dummy attribute error, using some kind of lazy error message,
159160
# so that neither the error itself not the message need to be created
@@ -3329,170 +3330,115 @@ def coercion_traceback(dump=True):
33293330
return coercion_model.exception_stack()
33303331
33313332
3332-
cdef class NamedBinopMethod:
3333-
"""
3334-
A decorator to be used on binary operation methods that should operate
3335-
on elements of the same parent. If the parents of the arguments differ,
3336-
coercion is performed, then the method is re-looked up by name on the
3337-
first argument.
3338-
3339-
In short, using the ``NamedBinopMethod`` (alias ``coerce_binop``) decorator
3340-
on a method gives it the exact same semantics of the basic arithmetic
3341-
operations like ``_add_``, ``_sub_``, etc. in that both operands are
3342-
guaranteed to have exactly the same parent.
3343-
"""
3344-
cdef _self
3345-
cdef _func
3346-
cdef _name
3347-
3348-
def __init__(self, func, name=None, obj=None):
3349-
"""
3350-
TESTS::
3351-
3352-
sage: from sage.structure.element import NamedBinopMethod
3353-
sage: NamedBinopMethod(gcd)(12, 15)
3354-
3
3355-
"""
3356-
self._func = func
3357-
if name is None:
3358-
if isinstance(func, types.FunctionType):
3359-
name = func.__name__
3360-
if isinstance(func, types.UnboundMethodType):
3361-
name = func.__func__.__name__
3362-
else:
3363-
name = func.__name__
3364-
self._name = name
3365-
self._self = obj
3366-
3367-
def __call__(self, x, y=None, **kwds):
3368-
"""
3369-
TESTS::
3370-
3371-
sage: from sage.structure.element import NamedBinopMethod
3372-
sage: test_func = NamedBinopMethod(lambda x, y, **kwds: (x, y, kwds), '_add_')
3373-
sage: class test_class(Rational):
3374-
....: def __init__(self,value):
3375-
....: self.v = value
3376-
....: @NamedBinopMethod
3377-
....: def test_add(self, other, keyword='z'):
3378-
....: return (self.v, other, keyword)
3379-
3380-
Calls func directly if the two arguments have the same parent::
3381-
3382-
sage: test_func(1, 2)
3383-
(1, 2, {})
3384-
sage: x = test_class(1)
3385-
sage: x.test_add(1/2)
3386-
(1, 1/2, 'z')
3387-
sage: x.test_add(1/2, keyword=3)
3388-
(1, 1/2, 3)
3389-
3390-
Passes through coercion and does a method lookup if the
3391-
left operand is not the same::
3392-
3393-
sage: test_func(0.5, 1)
3394-
(0.500000000000000, 1.00000000000000, {})
3395-
sage: test_func(1, 2, algorithm='fast')
3396-
(1, 2, {'algorithm': 'fast'})
3397-
sage: test_func(1, 1/2)
3398-
3/2
3399-
sage: x.test_add(2)
3400-
(1, 2, 'z')
3401-
sage: x.test_add(2, keyword=3)
3402-
(1, 2, 3)
3403-
3404-
A real example::
3405-
3406-
sage: R1=QQ['x,y']
3407-
sage: R2=QQ['x,y,z']
3408-
sage: f=R1(1)
3409-
sage: g=R1(2)
3410-
sage: h=R2(1)
3411-
sage: f.gcd(g)
3412-
1
3413-
sage: f.gcd(g,algorithm='modular')
3414-
1
3415-
sage: f.gcd(h)
3416-
1
3417-
sage: f.gcd(h,algorithm='modular')
3418-
1
3419-
sage: h.gcd(f)
3420-
1
3421-
sage: h.gcd(f,algorithm='modular')
3422-
1
3423-
"""
3424-
if y is None:
3425-
if self._self is None:
3426-
self._func(x, **kwds)
3427-
else:
3428-
x, y = self._self, x
3429-
if not have_same_parent(x, y):
3430-
old_x = x
3431-
x,y = coercion_model.canonical_coercion(x, y)
3432-
if old_x is x:
3433-
return self._func(x,y, **kwds)
3434-
else:
3435-
return getattr(x, self._name)(y, **kwds)
3436-
else:
3437-
return self._func(x,y, **kwds)
3438-
3439-
def __get__(self, obj, objtype):
3440-
"""
3441-
Used to transform from an unbound to a bound method.
3442-
3443-
TESTS::
3444-
sage: from sage.structure.element import NamedBinopMethod
3445-
sage: R.<x> = ZZ[]
3446-
sage: isinstance(x.quo_rem, NamedBinopMethod)
3447-
True
3448-
sage: x.quo_rem(x)
3449-
(1, 0)
3450-
sage: type(x).quo_rem(x,x)
3451-
(1, 0)
3452-
"""
3453-
return NamedBinopMethod(self._func, self._name, obj)
3454-
3455-
def _sage_doc_(self):
3456-
"""
3457-
Return the docstring of the wrapped object for introspection.
3458-
3459-
EXAMPLES::
3460-
3461-
sage: from sage.structure.element import NamedBinopMethod
3462-
sage: g = NamedBinopMethod(gcd)
3463-
sage: from sage.misc.sageinspect import sage_getdoc
3464-
sage: sage_getdoc(g) == sage_getdoc(gcd)
3465-
True
3466-
"""
3467-
return sageinspect._sage_getdoc_unformatted(self._func)
3333+
def coerce_binop(method):
3334+
r"""
3335+
Decorator for a binary operator method for applying coercion to the
3336+
arguments before calling the method.
3337+
3338+
Consider a parent class in the category framework, `S`, whose element class
3339+
expose a method `binop`. If `a` and `b` are elements of `S`, then
3340+
`a.binop(b)` behaves as expected. If `a` and `b` are not elements of `S`,
3341+
but rather have a common parent `T` whose element class also exposes
3342+
`binop`, we would rather expect `a.binop(b)` to compute `aa.binop(bb)`,
3343+
where `aa = T(a)` and `bb = T(b)`. This decorator ensures that behaviour
3344+
without having to otherwise modify the implementation of `binop` on the
3345+
element class of `A`.
3346+
3347+
Since coercion will be attempted on the arguments of the decorated method, a
3348+
`TypeError` will be thrown if there is no common parent between the
3349+
elements. An `AttributeError` or `NotImplementedError` or similar will be
3350+
thrown if there is a common parent of the arguments, but its element class
3351+
does not implement a method of the same name as the decorated method.
3352+
3353+
EXAMPLES:
3354+
3355+
Sparse polynomial rings uses `@coerce_binop` on `gcd`::
3356+
3357+
sage: S.<x> = PolynomialRing(ZZ,sparse=True)
3358+
sage: f = x^2
3359+
sage: g = x
3360+
sage: f.gcd(g) #indirect doctest
3361+
x
3362+
sage: T = PolynomialRing(QQ, name='x', sparse=True)
3363+
sage: h = 1/2*T(x)
3364+
sage: u = f.gcd(h); u #indirect doctest
3365+
x
3366+
sage: u.parent() == T
3367+
True
34683368
3469-
def _sage_src_(self):
3470-
"""
3471-
Return the source of the wrapped object for introspection.
3369+
Another real example::
34723370
3473-
EXAMPLES::
3371+
sage: R1=QQ['x,y']
3372+
sage: R2=QQ['x,y,z']
3373+
sage: f=R1(1)
3374+
sage: g=R1(2)
3375+
sage: h=R2(1)
3376+
sage: f.gcd(g)
3377+
1
3378+
sage: f.gcd(g,algorithm='modular')
3379+
1
3380+
sage: f.gcd(h)
3381+
1
3382+
sage: f.gcd(h,algorithm='modular')
3383+
1
3384+
sage: h.gcd(f)
3385+
1
3386+
sage: h.gcd(f,'modular')
3387+
1
34743388
3475-
sage: from sage.structure.element import NamedBinopMethod
3476-
sage: g = NamedBinopMethod(gcd)
3477-
sage: 'def gcd(' in g._sage_src_()
3478-
True
3479-
"""
3480-
return sageinspect.sage_getsource(self._func)
3389+
We demonstrate a small class using `@coerce_binop` on a method::
3390+
3391+
sage: from sage.structure.element import coerce_binop
3392+
sage: class MyRational(Rational):
3393+
....: def __init__(self,value):
3394+
....: self.v = value
3395+
....: @coerce_binop
3396+
....: def test_add(self, other, keyword='z'):
3397+
....: return (self.v, other, keyword)
3398+
3399+
Calls func directly if the two arguments have the same parent::
3400+
3401+
sage: x = MyRational(1)
3402+
sage: x.test_add(1/2)
3403+
(1, 1/2, 'z')
3404+
sage: x.test_add(1/2, keyword=3)
3405+
(1, 1/2, 3)
3406+
3407+
Passes through coercion and does a method lookup if the left operand is not
3408+
the same. If the common parent's element class does not have a method of the
3409+
same name, an exception is raised::
3410+
3411+
sage: x.test_add(2)
3412+
(1, 2, 'z')
3413+
sage: x.test_add(2, keyword=3)
3414+
(1, 2, 3)
3415+
sage: x.test_add(CC(2))
3416+
Traceback (most recent call last):
3417+
...
3418+
AttributeError: 'sage.rings.complex_number.ComplexNumber' object has no attribute 'test_add'
34813419
3482-
def _sage_argspec_(self):
3483-
"""
3484-
Return the argspec of the wrapped object for introspection.
3420+
TESTS:
34853421
3486-
EXAMPLES::
3422+
Test that additional arguments given to the method does not override the
3423+
`self` argument, see #21322::
34873424
3488-
sage: from sage.structure.element import NamedBinopMethod
3489-
sage: g = NamedBinopMethod(gcd)
3490-
sage: g._sage_argspec_()
3491-
ArgSpec(args=['a', 'b'], varargs=None, keywords='kwargs', defaults=(None,))
3492-
"""
3493-
return sageinspect.sage_getargspec(self._func)
3425+
sage: f.gcd(g, 1)
3426+
Traceback (most recent call last):
3427+
...
3428+
TypeError: algorithm 1 not supported
3429+
"""
3430+
@sage_wraps(method)
3431+
def new_method(self, other, *args, **kwargs):
3432+
if have_same_parent(self, other):
3433+
return method(self, other, *args, **kwargs)
3434+
else:
3435+
a, b = coercion_model.canonical_coercion(self, other)
3436+
if a is self:
3437+
return method(a, b, *args, **kwargs)
3438+
else:
3439+
return getattr(a, method.__name__)(b, *args, **kwargs)
3440+
return new_method
34943441
3495-
coerce_binop = NamedBinopMethod
34963442
34973443
###############################################################################
34983444

0 commit comments

Comments
 (0)