Question about coercion

38 views
Skip to first unread message

Gonzalo Tornaria

unread,
Jun 7, 2011, 3:25:53 PM6/7/11
to sage-...@googlegroups.com
In sage/structure/parent_base.pyx there is a function
"check_old_coerce" with the following definition:

cdef inline check_old_coerce(parent.Parent p):
   if p._element_constructor is not None:
       raise RuntimeError, "%s still using old coercion framework" % p


This function is called from a few methods in class ParentWithBase
(e.g. from _richcmp, from _coerce_c_impl, and from base_extend).

In my class (which inherits from ParentWithBase) I've defined
_element_constructor_ instead, but the init for ParentWithBase insist
on copying this method onto _element_constructor -- thus the
"check_old_coerce" function always fails... It's impossible to
avoid!!!

What is the base class that one is supposed to use instead of ParentWithBase?

The only reference for the coercion model that I was able to find is
http://www.sagemath.org/doc/reference/coercion.html#example, which
even shows an example, but the example inherits from Ring, which
ultimately inherits from ParentWithBase anyway.

Gonzalo

Simon King

unread,
Jun 7, 2011, 4:18:21 PM6/7/11
to sage-devel
Hi Gonzalo,

On 7 Jun., 21:25, Gonzalo Tornaria <torna...@math.utexas.edu> wrote:
> What is the base class that one is supposed to use instead of ParentWithBase?

I think it is Parent. The Parent.__init__ accepts an argument base.

Here is a small example:

from sage.structure.parent import Parent
from sage.structure.element import Element

class MyElement(Element):
def __init__(self,n,parent):
Element.__init__(self,parent)
self.n = n
def _repr_(self):
return repr(self.n)

class MyParent(Parent):
Element = MyElement
def __init__(self,B):
from sage.all import Sets
Parent.__init__(self,base=B, category=Sets())
def _repr_(self):
return "Parent over %s"%self.base()

And if you attach that code, then you can do

sage: P = MyParent(QQ)
sage: P
Parent over Rational Field
sage: P.base()
Rational Field

As you see, the base() method exists and works correctly.

As for element constructor, in my minimal example I just provide a
class as an attribute called "Element" to the parent class. Then, an
element constructor is correctly inferred (that's one reason to
initialise the category of the parent):

sage: P(3)
3
sage: type(_)
<class '__main__.MyParent_with_category.element_class'>
sage: isinstance(P(3),MyElement)
True
sage: P._element_constructor_??
Type: instancemethod
...
def _element_constructor_from_element_class(self, *args,
**keywords):
"""
The default constructor for elements of this parent

I hope that answers your question.

Best regards,
Simon

Robert Bradshaw

unread,
Jun 7, 2011, 4:18:08 PM6/7/11
to sage-...@googlegroups.com
On Tue, Jun 7, 2011 at 12:25 PM, Gonzalo Tornaria
<torn...@math.utexas.edu> wrote:
> In sage/structure/parent_base.pyx there is a function
> "check_old_coerce" with the following definition:
>
> cdef inline check_old_coerce(parent.Parent p):
>    if p._element_constructor is not None:
>        raise RuntimeError, "%s still using old coercion framework" % p
>
>
> This function is called from a few methods in class ParentWithBase
> (e.g. from _richcmp, from _coerce_c_impl, and from base_extend).
>
> In my class (which inherits from ParentWithBase) I've defined
> _element_constructor_ instead, but the init for ParentWithBase insist
> on copying this method onto _element_constructor -- thus the
> "check_old_coerce" function always fails... It's impossible to
> avoid!!!
>
> What is the base class that one is supposed to use instead of ParentWithBase?

Just Parent, if one can.

> The only reference for the coercion model that I was able to find is
> http://www.sagemath.org/doc/reference/coercion.html#example, which
> even shows an example, but the example inherits from Ring, which
> ultimately inherits from ParentWithBase anyway.

What this is really telling you is that you're using an old coercon
method from a class that's trying to do new-style coercion. (I was
hoping to have all this ripped out years ago, but it lingers on...)

In particular, _coerce_c_impl should never be called, nor should
_richcmp or the generic base_extend. A full traceback would help see
what the problem is.

- Robert

Simon King

unread,
Jun 7, 2011, 4:20:55 PM6/7/11
to sage-devel
PS:

On 7 Jun., 22:18, Simon King <simon.k...@uni-jena.de> wrote:
> As for element constructor, in my minimal example I just provide a
> class as an attribute called "Element" to the parent class. Then, an
> element constructor is correctly inferred (that's one reason to
> initialise the category of the parent):

To avoid misunderstanding: I did not intend to imply that the element
constructor *has* to be provided in that way. You could as well
implement your own _element_constructor_, and it should work.

Gonzalo Tornaria

unread,
Jun 7, 2011, 8:14:32 PM6/7/11
to sage-...@googlegroups.com
On Tue, Jun 7, 2011 at 5:18 PM, Simon King <simon...@uni-jena.de> wrote:
> Hi Gonzalo,
>
> On 7 Jun., 21:25, Gonzalo Tornaria <torna...@math.utexas.edu> wrote:
>> What is the base class that one is supposed to use instead of ParentWithBase?
>
> I think it is Parent. The Parent.__init__ accepts an argument base.
>
> Here is a small example:

Thanks for the example. I'll try it.

One caveat, though. The standard "Ring" and "Algebra" parents both
inherit from the ParentWithBase. Am I allowed to mix an element class
derived from AlgebraElements with a parent class derived from Parent?

Also, is it my impression or this model seems to require a lot of
boilerplate if I wanted to implement it fully (with categories,
functors, etc)?

Gonzalo

Robert Bradshaw

unread,
Jun 8, 2011, 1:04:21 AM6/8/11
to sage-...@googlegroups.com
On Tue, Jun 7, 2011 at 5:14 PM, Gonzalo Tornaria
<torn...@math.utexas.edu> wrote:
> On Tue, Jun 7, 2011 at 5:18 PM, Simon King <simon...@uni-jena.de> wrote:
>> Hi Gonzalo,
>>
>> On 7 Jun., 21:25, Gonzalo Tornaria <torna...@math.utexas.edu> wrote:
>>> What is the base class that one is supposed to use instead of ParentWithBase?
>>
>> I think it is Parent. The Parent.__init__ accepts an argument base.
>>
>> Here is a small example:
>
> Thanks for the example. I'll try it.
>
> One caveat, though. The standard "Ring" and "Algebra" parents both
> inherit from the ParentWithBase. Am I allowed to mix an element class
> derived from AlgebraElements with a parent class derived from Parent?

I can't say for sure that there aren't any hidden assumptions, but I
don't think the Parent's class hierarchy constrains the Element's.

> Also, is it my impression or this model seems to require a lot of
> boilerplate if I wanted to implement it fully (with categories,
> functors, etc)?

I've tried to limit the amount of boilerplate one needs to write. For
example, if a category is not given, it will guess one for you (often
Sets). You simply have to declare that a coercion exists and it will
create the Morphisms for you. You've read
http://www.sagemath.org/doc/reference/coercion.html I presume? Not
much, but probably the best we have.

- Robert

Simon King

unread,
Jun 8, 2011, 4:38:49 AM6/8/11
to sage-devel
Hi Gonzalo,

On 8 Jun., 07:04, Robert Bradshaw <rober...@math.washington.edu>
wrote:
> > One caveat, though. The standard "Ring" and "Algebra" parents both
> > inherit from the ParentWithBase. Am I allowed to mix an element class
> > derived from AlgebraElements with a parent class derived from Parent?
>
> I can't say for sure that there aren't any hidden assumptions, but I
> don't think the Parent's class hierarchy constrains the Element's.

I agree with Robert. As much as I understand, ring elements do not do
any special assumption on their parents.

> > Also, is it my impression or this model seems to require a lot of
> > boilerplate if I wanted to implement it fully (with categories,
> > functors, etc)?

I didn't mention functors, although I love the stuff in
sage.categories.pushout :))

The full "boilerplate", if you want to do all of coercion model and
category framework, should roughly be as described below.

Let MyParent be the parent class and MyElement a class for the
elements of a MyParent instance. Let P and E denote instances of
MyParent and MyElement, respectively.

I start with a summary of the steps, telling how important and how
difficult they usually are, IMO:
0. Classes to inherit from: Easy, HAS to be done.
1. "Magic" Python methods: Mainly easy, you just need to write _repr_
instead of __repr__, etc. __cmp__ may be a bit more tricky.
2.a) Category for parents. Mainly easy: Just choose a category and
initialise your parent with it. Should/Has to be done.
2.b) Category for elements: Mainly easy. Just do
class MyParent(Parent):
Element = MyElement
def __init__...
Should be done, if you use Python or wait until #11115 is merged.
You may be required to provide certain element or parent methods, but
that depends on the category you chose.
3. Basic coercion. Can range from "nothing to do at all" to "tedious".
It is a must-have!
3.a) Conversion: 2.b) may actually be sufficient for it, otherwise
implement _element_constructor_
3.b) Coercion maps: Done with implementing one method, namely
_coerce_map_from_.
4. Not-so-basic coercion with functors. Tricky, can be seen as cherry
on the cake (so, not necessary but sometimes nice to have).

Here are the gory details.

0. Classes to inherit from
MyParent should inherit from sage.structure.parent.Parent and
MyElement from sage.structure.element.Element, or of course from a sub-
class.

1. "Magic" Python methods
Do not use __repr__, __add__, __mul__ etc with double underscore, but
with single underscore (_repr_, _add_,_mul_). The double underscore
methods are inherited, and the generic code should not be overridden
since it provides coercion. The case of __cmp__ is a bit more
complicated -- please read the comments in the vicinity of
sage.structure.parent.Parent.__cmp__.

2. Category framework
2.a) Initialisation of the parent class
In MyParent.__init__, you should construct a category (or pass it as
an argument to __init__), and you should call
Parent.__init__(self,...,category=your_category). That is usually a
small amount of work, since probably you will find an existing
category that suites your needs --- Sets(), Algebras(basering),
Rngs(), Rings(), etc.

NOTE: If MyParent is written in Python, then you will find that its
instances belong to a class MyParent_with_category -- that is a
subclass of MyParent that is automatically obtained by adding stuff
from the category framework

2.b) Initialisation of the element class
The class MyParent should have an attribute called `Element`, and its
value should be MyElement. The background is that the attribute
Element is automatically mixed with stuff from the category and turned
into a new class available as P.element_class (where P is an instance
of MyParent). Note that this only works if MyParent is written in
Python --- or you may apply #11115, because then it also works with
Cython.

2.c) Required abstract methods
Some categories require that you implement certain methods for
MyParent or MyElement, such as lift() if your parent is a quotient.

2.d) Make the test suites work
You are supposed to provide doc tests of the form TestSuite(P).run().
To make them work, you may need to implement further things, like
MyParent._an_element_. The error messages of TestSuite may tell you
what is missing. But you may actually be lucky, sometimes there are
generic methods that do the job for you.

3. Coercion - the basics
3.a) Element conversion
Note that this is CONVERSION. So, the aim is that P(bla) returns an
element of P to the given argument bla -- but it is not necessarily
the case that there is a coercion map from parent(bla) to P.
3.a).(i) The default
It may be enough to do *nothing* at all: You provided MyParent.Element
= MyElement in step 2.b), and the default is that P(bla) returns
P.element_class(bla), and that will use the initialisation from
MyElement like MyElement(P,bla). So, if MyElement.__init__ is able to
understand anything convertible to P then you can rely on the default!
3.b).(ii) _element_constructor_
If you want to keep MyElement.__init__ simple, then you should provide
MyParent with a method _element_constructor_. It should transform
given arguments to something you can use to initialise MyElement. BUT
NOTE: You should NOT return MyElement(P,...)!! Instead, return
P.element_class(P,...)!
3.c) Coercion maps
A coercion of "bla" into P is more than just a conversion P(bla),
because in a coercion you must have a structure preserving map from
the parent of bla to P. Think of the integers: You can convert 1.0
into ZZ, but there is no coercion from RR (the parent of 1.0) to ZZ.
You should provide MyParent with a method _coerce_map_from_.
Requirements: If there is a coercion from a parent S to P, then
P._coerce_map_from_(S) should either return True or an actual map from
S to P.
(i) If it returns a map f, then an element e of S is coerced into
the element f(e) of P
(ii) If it just returns True, then P.coerce_map_from(S) will
automatically create a map for you. Coercion of an element e of S into
P boils down to calling P._element_constructor_(e).

4. Coercion -- the advanced stuff.
This step is needed if you want to do arithmetic with elements from P
and elements from S, where neither S coerces into P nor P coerces into
S. A typical example is P=ZZ[x] and S=QQ. The result of an arithmetic
operation between S and P lives in QQ[x], hence, neither in S nor in
P.
4.a) The construction of P
It is assumed that you can construct P out of a simpler parent by
means of a construction that is supposed to be functorial (e.g., the
construction of "forming a polynomial ring with variable x" transforms
*any* ring R into a ring R[x], and any map from R to S yields a map
from R[x] to S[x]).
Then, you should provide MyParent with a method construction(), that
returns a pair F,R, where F is a so-called construction functor, and
F(R) == P.
4.b) The construction functor F
See the examples in sage.categories.pushout. Basically, you need to
implement a class MyFunctor inherited from
sage.categories.pushout.ConstructionFunctor, and provide it with a
method _apply_functor
4.c) Pushout of functors
Assume that F1 is an instance of MyFunctor and F2 is any other
construction functor, and assume that there is neither a coercion from
F1(R) to F2(R) nor from F2(R) to F1(R). Assume further that you can
think of a "canonical" parent S that can be obtained from R, such that
both F1(R) and F2(R) coerce into S. Then, F1.pushout(F2) should return
a functor F3 such that S==F3(R).

Example from above: F1 is the fraction field constructor, F2 is the
polynomial ring constructor with variable x, and R is ZZ. Then,
F1(R)==QQ and F2(R)==ZZ[x] . You want S = QQ[x], hence, F3 = F2(F1),
such that S==F3(R)=F2(F1(R)).

Fortunately, this can be achieved very easily, using the default
method pushout() of ConstructionFunctor. You just provide MyFunctor
with an attribute "rank". If F1.rank is smaller than F2.rank then F3
will be "first F1, then F2". Example:
sage: R = ZZ
sage: F1 = QQ.construction()[0]
sage: F2 = R['x'].construction()[0]
sage: F1
FractionField
sage: F2
Poly[x]
sage: F1.rank
5
sage: F2.rank
9
sage: F1.pushout(F2)
Poly[x](FractionField(...))
sage: F2.pushout(F1)
Poly[x](FractionField(...))

4.d) Merging functors
This is when functors F1 and F2 are of the same rank.
It may be that your parent comes in different implementations. For
example, ZZ[x] can be in a dense or sparse implementation, based on
NTL or FLINT. The construction functor should know about these
details. And then, MyFunctor should be provided with a method merge()
with the following properties: If F1 and F2 are two construction
functors of the same rank, such that F1(R) and F2(R) are isomorphic
objects in different implementation for ANY R, then F1.merge(F2)
should return a functor F3 such that F3(R) is isomophic to F1(R) and
F2(R) and there is a coercion from both F1(R) and F2(R) to F3(R).
Otherwise, None should be returned.
Example: If F1 returns dense 3x3 matrix spaces and F2 returns sparse
3x3 matrix spaces, and you decide that dense is the default, then
F1.merge(F2) should return F1.
sage: MD = MatrixSpace(QQ,3,3,sparse=False)
sage: MS = MatrixSpace(QQ,3,3,sparse=True)
sage: FD,_=MD.construction()
sage: FS,_=MS.construction()
sage: FD.rank
10
sage: FS.rank
10
sage: FD.is_sparse
False
sage: FS.is_sparse
True
sage: FD.merge(FS).is_sparse
False
sage: FS.merge(FD).is_sparse
False

So, if you want the full programme then it's much to do. But often,
well-chosen parts of it are sufficient.

Best regards,
Simon

Jason Grout

unread,
Jun 8, 2011, 8:04:02 AM6/8/11
to sage-...@googlegroups.com
On 6/8/11 3:38 AM, Simon King wrote:
> Hi Gonzalo,
>
> On 8 Jun., 07:04, Robert Bradshaw<rober...@math.washington.edu>
> wrote:
>>> One caveat, though. The standard "Ring" and "Algebra" parents both
>>> inherit from the ParentWithBase. Am I allowed to mix an element class
>>> derived from AlgebraElements with a parent class derived from Parent?
>>
>> I can't say for sure that there aren't any hidden assumptions, but I
>> don't think the Parent's class hierarchy constrains the Element's.
>
> I agree with Robert. As much as I understand, ring elements do not do
> any special assumption on their parents.
>
>>> Also, is it my impression or this model seems to require a lot of
>>> boilerplate if I wanted to implement it fully (with categories,
>>> functors, etc)?
>
> I didn't mention functors, although I love the stuff in
> sage.categories.pushout :))
>
> The full "boilerplate", if you want to do all of coercion model and
> category framework, should roughly be as described below.


This is great stuff! Have you considered contributing this to the docs?

Jason

Simon King

unread,
Jun 8, 2011, 8:26:00 AM6/8/11
to sage-devel
Hi Jason,

On 8 Jun., 14:04, Jason Grout <jason-s...@creativetrax.com> wrote:
> This is great stuff!  Have you considered contributing this to the docs?

Thanks!

Yes, why not? I guess it should be considered an extension of
http://www.sagemath.org/doc/reference/coercion.html. So I'll start to
work on that text.

Cheers,
Simon


Simon King

unread,
Jun 8, 2011, 8:31:21 AM6/8/11
to sage-devel
On 8 Jun., 14:26, Simon King <simon.k...@uni-jena.de> wrote:
> Yes, why not? I guess it should be considered an extension ofhttp://www.sagemath.org/doc/reference/coercion.html. So I'll start to
> work on that text.

Or perhaps it should be in the "Sage constructions" or in a "Thematic
tutorial", rather than in the reference manual?
What do you think suits best?

Jason Grout

unread,
Jun 8, 2011, 8:41:42 AM6/8/11
to sage-...@googlegroups.com

I'd rather have a one-stop place for coercion documentation, so I'd
rather have it in the coercion section of the reference manual. It
might be good to put pointers in the other places for "A short guide to
implementing coercion" in the reference.

Jason

Simon King

unread,
Jun 8, 2011, 10:14:59 AM6/8/11
to sage-devel
Hi Jason,

On 8 Jun., 14:41, Jason Grout <jason-s...@creativetrax.com> wrote:
> I'd rather have a one-stop place for coercion documentation, so I'd
> rather have it in the coercion section of the reference manual.  It
> might be good to put pointers in the other places for "A short guide to
> implementing coercion" in the reference.

I really wonder if it makes sense to fit a text on the implementation
of parents and elements with coercion and category *into the reference
manual*? I doubt that the result could be called a one-step place.

The point is that the reference manual seems mainly structured
according to the modules of Sage. Note that http://www.sagemath.org/doc/reference/coercion.html
already slightly violates that rule, because it provides a common text
for at least three modules of Sage, namely sage.structure.coerce,
sage.structure.coerce_actions and sage.structure.coerce_maps

In addition to that, the text that I'm writing will relate with
sage.structure.parent, sage.structure.element, and
sage.categories.pushout.

I believe that a text using stuff from at least 6 modules would not
fit to the idea of a reference manual.

Therefore: Why not have a new thematic tutorial called "How to
implement basic algebraic structures in Sage", with sub-sections on
base classes, categories and coercion?
It would of course be related with various modules of Sage, and there
should be pointers from each of these modules to the new thematic
tutorial -- so that it would be easy to find. Is that what you would
call a one-step place?

Cheers,
Simon

John Cremona

unread,
Jun 8, 2011, 10:32:22 AM6/8/11
to sage-...@googlegroups.com

+1 (or +100000000), especially if Simon is volunteering to write it!
The docstrings & hence ref manual can refer to it.

John

>
> Cheers,
> Simon
>
> --
> To post to this group, send an email to sage-...@googlegroups.com
> To unsubscribe from this group, send an email to sage-devel+...@googlegroups.com
> For more options, visit this group at http://groups.google.com/group/sage-devel
> URL: http://www.sagemath.org
>

Jason Grout

unread,
Jun 8, 2011, 11:06:26 AM6/8/11
to sage-...@googlegroups.com
On 6/8/11 9:14 AM, Simon King wrote:
> Therefore: Why not have a new thematic tutorial called "How to
> implement basic algebraic structures in Sage", with sub-sections on
> base classes, categories and coercion?
> It would of course be related with various modules of Sage, and there
> should be pointers from each of these modules to the new thematic
> tutorial -- so that it would be easy to find. Is that what you would
> call a one-step place?

Sounds great!

Jason


Volker Braun

unread,
Jun 8, 2011, 2:18:13 PM6/8/11
to sage-...@googlegroups.com
Sounds like you want to add a section to the developer's guide (http://sagemath.org/doc/developer)? A good introduction into parent/elements and categories is really missing there. 

Simon King

unread,
Jun 8, 2011, 2:40:29 PM6/8/11
to sage-devel
Hi Volker,

On 8 Jun., 20:18, Volker Braun <vbraun.n...@gmail.com> wrote:
> Sounds like you want to add a section to the developer's guide
> (http://sagemath.org/doc/developer)?

I guess it depends on the size. If the size is moderate then it could
become a subsection of http://www.sagemath.org/doc/developer/writing_code.html,
if it is too large then it should be a thematic tutorial.

Cheers,
Simon

Kwankyu Lee

unread,
Jun 9, 2011, 4:47:36 AM6/9/11
to sage-...@googlegroups.com
Hi Simon,

I am one of those who eagerly hope the thematic tutorial to be written and maintained by the core developers of Sage. The article should explain on parents, elements, categories, coercion, morphisms, homsets, and the canonical way to program an algebraic structure on the basic concepts. I think a great amount of time of developers is wasted in learning the concepts in a harsh way, that is, by reading the source codes. 


Kwankyu

Nicolas M. Thiery

unread,
Jun 10, 2011, 5:29:04 PM6/10/11
to sage-...@googlegroups.com
Dear Simon, dear all,

+1 on Simon extending his notes to a full tutorial!

Note that we also have quite some tutorial material on development on
the Sage-Combinat queue, and should go into Sage at some point. We
should think about their respective perimeter. In any cases, please
feel free to recycle any part of them!

Thematic tutorial Index:
http://combinat.sagemath.org/doc/thematic_tutorials/index.html

Tutorial: Implementing Algebraic Structures
http://combinat.sagemath.org/doc/thematic_tutorials/tutorial-implementing-algebraic-structures.html

Elements, parents, and categories in Sage: a (draft of) primer (this document is for the most part already in Sage)
http://combinat.sagemath.org/doc/reference/sage/categories/primer.html

Implementing a new parent: a (draft of) tutorial
http://combinat.sagemath.org/doc/reference/sage/categories/tutorial.html


A note: an advantage of putting documentation in the reference manual,
and more precisely directly within the source, is that you can access
it from the command line with e.g.:

sage.categories.tutorial?

and also easily link to it from other spots in the reference manual
with e.g. see :mod:`sage.categories.tutorial`.

Cheers,
Nicolas
--
Nicolas M. Thi�ry "Isil" <nth...@users.sf.net>
http://Nicolas.Thiery.name/

Simon King

unread,
Jun 11, 2011, 2:54:20 AM6/11/11
to sage-devel
Hi All,

On 10 Jun., 23:29, "Nicolas M. Thiery" <Nicolas.Thi...@u-psud.fr>
wrote:
> Note that we also have quite some tutorial material on development on
> the Sage-Combinat queue, and should go into Sage at some point.

Better sooner than later, I'd say. Why is it not in the thematic
tutorials yet? Is a review missing?

If the problem is about reviews, I suggest to use a similar procedure
for tutorials than for spkgs: There could be tutorials marked
"experimental", or maybe "preliminary" is a better word.

By a preliminary tutorial, I mean one that is not fully reviewed, but
one trusts that the author is expert enough to not write complete
nonsense, and thus makes it publicly available.

The table of contents at http://sagemath.org/doc/thematic_tutorials/index.html
would link to all tutorials, but the link to a preliminary tutorial
would be marked by a (red?) label. In that way, the users can already
benefit from the texts, but know that things may change over time.

Perhaps there could be finer labels, such as "draft". A tutorial would
be "preliminary" if it seems complete but potetially not final, and
"draft" if the author admits that it is incomplete but will not have
time to finish it in the near future.

In that way, all the tutorials from the sage.combinat branch could be
exposed to people *soon*.

Concerning the documents Nicolas pointed at:

Of course some of the subjects overlap with what I am about to write.
But my didactical approach is very different from these documents, to
the extent that there will be no overlap of the actual content of our
documents. Hence, I would not see a problem to eventually have
(preliminary) pointers in http://sagemath.org/doc/thematic_tutorials/index.html
to both the documents in the combinat branch and my to-be-finished
worksheet/text.

But let me finish things first...

Cheers,
Simon
Reply all
Reply to author
Forward
0 new messages