Decorator Base Class: Needs improvement.

R

Ron_Adam

Hi, Thanks again for all the helping me understand the details of
decorators.

I put together a class to create decorators that could make them a lot
easier to use.

It still has a few glitches in it that needs to be addressed.

(1) The test for the 'function' object needs to not test for a string
but an object type instead.

(2) If the same decorator instance is stacked, it will get locked in a
loop. But stacking different instances created from the same
decorator object works fine.

(3) It has trouble if a decorator has more than one argument.


But I think all of these things can be fixed. Would this be something
that could go in the builtins library? (After any issues are fixed
first of course.)

When these are stacked, they process all the prepossess's first, call
the decorator, then process all the postprocess's. It just worked out
that way, which was a nice surprise and makes this work a bit
different than the standard decorators.

Cheers,
Ron

#---start---

class Decorator(object):
"""
Decorator - A class to make decorators with.

self.function - name of function decorated.
self.arglist - arguments of decorator
self.preprocess - over ride to preprocess function arguments.
self.postprocess - over ride to postprocess function
return value.

Example use:
class mydecorator(Decorate):
def self.preprocess(self, args):
# process args
return args
def self.postprocess(self, results):
# process results
return results

deco = mydecorator()

@deco
def function(args):
# function body
return args
"""
function = None
arglist = []
def __call__(self, arg):
self.arglist.append(arg)
def _wrapper( args):
pre_args = self.preprocess(args)
result = self.function(pre_args)
return self.postprocess(result)
if 'function' in str(arg):
self.arglist = self.arglist[:-1]
self.function = arg
return _wrapper
return self
def preprocess(self, args):
return args
def postprocess(self, result):
return result

class mydecorator(Decorater):
def preprocess(self, args):
args = 2*args
return args
def postprocess(self, args):
args = args.upper()
args = args + str(self.arglist[0])
return args
deco = mydecorator()

@deco('xyz')
def foo(text):
return text

print foo('abc')


#---end---
 
R

Ron_Adam

Ok, that post may have a few(dozen?) problems in it. I got glitched
by idles not clearing variables between runs, so it worked for me
because it was getting values from a previous run.

This should work better, fixed a few things, too.

The decorators can now take more than one argument.
The function and arguments lists initialize correctly now.

It doesn't work with functions with more than one variable. It seems
tuples don't unpack when given to a function as an argument. Any way
to force it?


class Decorator(object):
"""
Decorator - A base class to make decorators with.

self.function - name of function decorated.
self.arglist - arguments of decorator
self.preprocess - over ride to preprocess function arguments.
self.postprocess - over ride to postprocess function
return value.

Example use:
class mydecorator(Decorate):
def self.preprocess(self, args):
# process args
return args
def self.postprocess(self, results):
# process results
return results

deco = mydecorator()

@deco
def function(args):
# function body
return args
"""
def __init__(self):
self.function = None
self.arglist = []
def __call__(self, *arg):
if len(arg) == 1:
arg = arg[0]
self.arglist.append(arg)
def _wrapper( *args):
if len(args) == 1:
args = args[0]
pre_args = self.preprocess(args)
result = self.function(pre_args)
return self.postprocess(result)
if 'function' in str(arg):
self.arglist = self.arglist[:-1]
self.function = arg
return _wrapper
return self
def preprocess(self, args):
return args
def postprocess(self, result):
return result


#---3---
class mydecorator(Decorator):
def preprocess(self, args):
args = 2*args
return args
def postprocess(self, args):
args = args.upper()
args = args + str(self.arglist[0])
return args
deco = mydecorator()

@deco('xyz')
def foo(text):
return text
print foo('abc')


#---2---
class decorator2(Decorator):
def preprocess(self, args):
return args+sum(self.arglist[0])
def postprocess(self, args):
return args
deco2 = decorator2()

@deco2(1,2)
def foo(a):
return a
print foo(1)

# This one doesn't work yet.
#---3---
class decorator3(Decorator):
pass
deco3 = decorator3()

@deco3
def foo(a,b):
return a,b
print foo(1,3)
 
S

Steve Holden

Ron_Adam said:
Ok, that post may have a few(dozen?) problems in it. I got glitched
by idles not clearing variables between runs, so it worked for me
because it was getting values from a previous run.

This should work better, fixed a few things, too.

The decorators can now take more than one argument.
The function and arguments lists initialize correctly now.
Ron:

I've followed your attempts to understand decorators with interest, and
have seen you engage in conversation with many luminaries of the Python
community, so I hesitate at this point to interject my own remarks.

In a spirit of helpfulness, however, I have to ask whether your
understanding of decorators is different from mine because you don't
understand them or because I don't.

You have several times mentioned the possibility of a decorator taking
more than one argument, but in my understanding of decorators this just
wouldn't make sense. A decorator should (shouldn't it) take precisely
one argument (a function or a method) and return precisely one value (a
decorated function or method).
It doesn't work with functions with more than one variable. It seems
tuples don't unpack when given to a function as an argument. Any way
to force it?


class Decorator(object):
[...]

Perhaps we need to get back to basics?

Do you understand what I mean when I say a decorator should take one
function as its argument and it should return a function?

regards
Steve
 
K

Kay Schluehr

Steve said:
You have several times mentioned the possibility of a decorator taking
more than one argument, but in my understanding of decorators this just
wouldn't make sense. A decorator should (shouldn't it) take precisely
one argument (a function or a method) and return precisely one value (a
decorated function or method).

Yes. I think this sould be fixed into the minds of the people exacly
this way You state it:

When writing

@decorator(x,y)
def f():
....

not the so called "decorator" function but decorator(x,y) is the
decorating function and decorator(x,y) is nothing but a callable object
that takes f as parameter. A little correcture of Your statement: it is
NOT nessacary that a function or method will be returned from a
decorator.

def decorator(x,y):
def inner(func):
return x+y
return inner

@decorator(1,2)
def f():pass
3

This is perfectly valid allthough not very usefull ;)

Regards,
Kay
 
B

Bengt Richter

I agree from an English language point of view. I.e., a verber is
something that does the verbing, so a decorator ought to be the thing
that does the decorating, which is the function/callable(s) resulting
on stack from the evaluation of the @-line.

In the case of a single @deco name, the evaluation is trivial, and
the difference between the @-expression and the resulting callable
might be overlooked. Full-fledged general expressions after the '@'
are for some reason disallowed, but it is handy to allow attribute
access and calling in the syntax, so the relevant Grammar rules are:

From the 2.4 Grammar, the key part seems to be

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
funcdef: [decorators] 'def' NAME parameters ':' suite

and further on

dotted_name: NAME ('.' NAME)*

So the Python Grammar's name for the @-expression is just plain "decorator"
which conflicts with my English-based reading of the word ;-/

So it appears the intent is to call the entire @-line the "decorator"
and I guess to have a name for what evaluating the @-line returns on the
stack, we could call it the "decorating callable" since it is what takes
the function parameter as its first parameter and decorates the function
and returns the decorated function.

But I don't like it, English-wise. I would rather call the @-line
the "decorator expression" and what it evaluates to the "decorator."

Can we change the grammar with s/decorator/decorator_expr/ ?
Yes. I think this sould be fixed into the minds of the people exacly
this way You state it:
I think we may be agreeing in principle but with different words ;-)
When writing

@decorator(x,y)
def f():
....

not the so called "decorator" function but decorator(x,y) is the
^--(the result of evaluating)
decorating function and decorator(x,y) is nothing but a callable object
^--(the result of evaluating)
that takes f as parameter. A little correcture of Your statement: it is
NOT nessacary that a function or method will be returned from a
decorator.

Yes, in fact it could even perversely be colluding with a known succeeding
decorator callable to pass info strangely, doing strange things, e.g.,
>>> trick = ['spam', 'eggs']
>>> def choose_name(tup):
... nx, f = tup
... f.func_name = trick[nx]
... return f
... ... return lambda f, nx=nx:(nx, f)
... ... @namedeco()
... def foo(): pass
... ... @namedeco(0)
... def foo(): pass
... <function spam at 0x02EE8DF4>

I.e., namedeco evaluates to the lambda as decorator function,
and that passes a perverse (nx, f) tuple on to choose_name, instead
of a normal f.
def decorator(x,y):
def inner(func):
return x+y
return inner

@decorator(1,2)
def f():pass

3

This is perfectly valid allthough not very usefull ;)

Perhaps even less useful, the final decorator can return something
arbitrary, as only the name in the def matters at that point in
the execution (as a binding target name), so:
... def foo(): pass
... 'something dumb'

Hm, maybe some use ...
... return lambda f: (name, f)
... ... def foo(): pass
... ... def bar(): pass
...
{'pooh_foo': <function foo at 0x02EE8DBC>, 'tigger_bar': <function bar at 0x02EE8DF4>}

.... nah ;-)


Anyway, I think a different name for what comes after the "@" and the
callable that that (very limited) expression is supposed to return
would clarify things. My conceptual model is

@decorator_expression # => decorator
def decorating_target(...):
...

Regards,
Bengt Richter
 
K

Kay Schluehr

Bengt said:
From the 2.4 Grammar, the key part seems to be

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
funcdef: [decorators] 'def' NAME parameters ':' suite

and further on

dotted_name: NAME ('.' NAME)*

So the Python Grammar's name for the @-expression is just plain "decorator"
which conflicts with my English-based reading of the word ;-/

What about playing with the words decorator/decoration? The allegoric
meaning of the decorator

def deco(f):
pass

would be: f is "vanishing under decoration" - all is vanity.

In the example deco would be both a decorator and a decoration. In
other examples deco were a decorator and deco(x,y) the decoration.

Regards,
Kay
 
K

Kay Schluehr

Bengt said:
From the 2.4 Grammar, the key part seems to be

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
funcdef: [decorators] 'def' NAME parameters ':' suite

and further on

dotted_name: NAME ('.' NAME)*

So the Python Grammar's name for the @-expression is just plain "decorator"
which conflicts with my English-based reading of the word ;-/

What about playing with the words decorator/decoration? The allegoric
meaning of the decorator

def deco(f):
pass

would be: f is "vanishing under decoration" - all is vanity.

In the example deco would be both a decorator and a decoration. In
other cases deco were a decorator and deco(x,y) the decoration.

Regards,
Kay
 
R

Ron_Adam

Ron:

I've followed your attempts to understand decorators with interest, and
have seen you engage in conversation with many luminaries of the Python
community, so I hesitate at this point to interject my own remarks.

I don't mind. It might help me communicate my ideas better.
In a spirit of helpfulness, however, I have to ask whether your
understanding of decorators is different from mine because you don't
understand them or because I don't.

Or it's just a communication problem, and we both understand.
Communicating is not my strongest point. But I am always willing to
clarify something I say.
You have several times mentioned the possibility of a decorator taking
more than one argument, but in my understanding of decorators this just
wouldn't make sense. A decorator should (shouldn't it) take precisely
one argument (a function or a method) and return precisely one value (a
decorated function or method).

What I was referring to is the case:

@decorator(x,y,z)

As being a decorator expression with more than one argument. and not:

@decorator(x)(y)

This would give a syntax error if you tried it.
SyntaxError: invalid syntax

The problem I had with tuple unpacking had nothing to do with
decorators. I was referring to a function within the class, and I
needed to be consistent with my use of tuples as arguments to
functions and the use of the '*' indicator.
Do you understand what I mean when I say a decorator should take one
function as its argument and it should return a function?

regards
Steve

Hope this clarifies things a bit.

Cheers,
Ron
 
S

Scott David Daniels

Ron_Adam said:
What I was referring to is the case:
@decorator(x,y,z)
As being a decorator expression with more than one argument.
But, we generally say this is a call to a function named decorator
that returns a decorator. If you called it:
@make_decorator(x,y)
def .....
We'd be sure we were all on the same page.

How about this as an example:

def tweakdoc(name):
def decorator(function):
function.__doc__ = 'Tweak(%s) %r' % (name, function.__doc__)
return function
return decorator

What is confusing us about what you write is that you are referring to
tweakdoc as a decorator, when it is a function returning a decorator.
and not:
@decorator(x)(y)

This is only prevented by syntax (probably a good idea, otherwise
we would see some very complicated expressions before function
declarations).

--Scott David Daniels
(e-mail address removed)
 
R

Ron_Adam

But, we generally say this is a call to a function named decorator
that returns a decorator. If you called it:
@make_decorator(x,y)
def .....
We'd be sure we were all on the same page.

Good point, I agree. :)

Or alternatively
@call_decorator(x,y)

Using either one would be good practice.
How about this as an example:

def tweakdoc(name):
def decorator(function):
function.__doc__ = 'Tweak(%s) %r' % (name, function.__doc__)
return function
return decorator

What is confusing us about what you write is that you are referring to
tweakdoc as a decorator, when it is a function returning a decorator.

Bengt Richter is also pointing out there is an inconsistency in
Pythons documents in the use of decorator. I've been trying to start
referring to the "@___" as the decorator-exression, but that still
doesn't quite describe what it does either. Decorarator-caller might
be better. Then the decorator-function as the part that defines the
decorated-function.

Another alternative is to call the entire process what it is,
function-wrapping. Then the "@____" statement would be the
wrapper-caller, which calls the wrapper-function, which defines the
wrapped-function. That's much more descriptive to me.

If we do that then we could agree to use decorator as a general term
to describe a function as decorated. Meaning it is wrapped and get
away from the decorator/decoratoree discussions. But I think that the
terminology has been hashed out quite a bit before, so I don't expect
it to change. I'll just have to try to be clearer in how I discuss
it.
This is only prevented by syntax (probably a good idea, otherwise
we would see some very complicated expressions before function
declarations).

--Scott David Daniels
(e-mail address removed)

And this isn't allowed either, although it represents more closely the
nesting that takes place when decorator-expressions are stacked.

@make_deco1
@make_deco2
@make_deco3
def function1(n):
n+=1
return n


This is allowed, but its not pretty.

@make_deco1
@ make_deco2
@ make_deco3
def function1(n):
n+=1
return n


Cheers,
Ron
 
S

Steve Holden

Ron_Adam said:
I don't mind. It might help me communicate my ideas better.




Or it's just a communication problem, and we both understand.
Communicating is not my strongest point. But I am always willing to
clarify something I say.




What I was referring to is the case:

@decorator(x,y,z)

As being a decorator expression with more than one argument. and not:

@decorator(x)(y)

This would give a syntax error if you tried it.



SyntaxError: invalid syntax

The problem I had with tuple unpacking had nothing to do with
decorators. I was referring to a function within the class, and I
needed to be consistent with my use of tuples as arguments to
functions and the use of the '*' indicator.




Hope this clarifies things a bit.

Cheers,
Ron
So what you are saying is that you would like to be able to use
arbitrarily complex expressions after the :at" sign, as long as they
return a decorator? If so, you've been "pronounced" :)

regards
Steve
 
E

El Pitonero

Scott said:
def tweakdoc(name):
def decorator(function):
function.__doc__ = 'Tweak(%s) %r' % (name, function.__doc__)
return function
return decorator

What is confusing us about what you write is that you are referring to
tweakdoc as a decorator, when it is a function returning a decorator.

"Decorator factory" would be a shorter name for "a function returning a
decorator".
 
R

Ron_Adam

So what you are saying is that you would like to be able to use
arbitrarily complex expressions after the :at" sign, as long as they
return a decorator? If so, you've been "pronounced" :)

regards
Steve

No not at all, I never said that. But.. ;-)


If we get into what I would like, as in my personal wish list, that's
a whole other topic. <g>


I would have preferred the @ symbol to be used as an inline assert
introducer. Which would have allowed us to put debug code anywhere we
need. Such as @print total @. Then I can switch on and off
debugging statements by setting __debug__ to True or False where ever
I need it.

And as far as decorators go. I would of preferred a keyword, possibly
wrap, with a colon after it. Something like this.

def function_name(x):
return x

wrap function_name:
wrapper1()
wrapper2()
wrapper3()

A wrap command could more directly accomplish the wrapping, so that
def statements within def statements aren't needed. (Unless you
want'ed too for some reason.)

And as far as arbitrary complex expressions go.. Actually I think that
it's quite possible to do as it is. ;-)

But this is just a few of my current thoughts which may very well
change. It's an ever changing list. <g>

Cheers,
Ron
 
G

Greg Ewing

Ron_Adam said:
I would have preferred the @ symbol to be used as an inline assert
introducer. Which would have allowed us to put debug code anywhere we
need. Such as @print total @.

Don't lose heart, there are still two unused characters
left, $ and ?.

? might even be more mnemonic for this purpose, as

?print "foo =", foo

has a nice hint of "WT?%$%$ is going on at this point?"
to it.
 
R

Ron_Adam

Don't lose heart, there are still two unused characters
left, $ and ?.

? might even be more mnemonic for this purpose, as

?print "foo =", foo

has a nice hint of "WT?%$%$ is going on at this point?"
to it.

LOL, yes, it does. :)
 
B

Bengt Richter

"Decorator factory" would be a shorter name for "a function returning a
decorator".
True, but tweakdoc doesn't have to be a function, so IMO we need a better
name for the @-line, unless you want to use many various specific names
like factory. E.g.,

(Preliminary definition):
... class __metaclass__(type):
... def __getattribute__(cls, name):
... if hasattr(type(cls), name): return type.__getattribute__(cls, name)
... def decorator(f):
... f.func_name = '%s_%s'%(f.func_name, name)
... return f
... return decorator
...
Ok, now what do you call this @-line? Freaky decorator factory invocation?
Decoratoriferous expression? ;-) Lexical decoration?
Decorator{-producing,-generating, ??} expression ?

Decorator expression for short? vs the decorator callable produced.
... def foo(): pass
... ... @deco.two
... def bar():pass
... <function bar_two_three at 0x02EE8E9C>

Regards,
Bengt Richter
 
B

Bengt Richter

No not at all, I never said that. But.. ;-)


If we get into what I would like, as in my personal wish list, that's
a whole other topic. <g>


I would have preferred the @ symbol to be used as an inline assert
introducer. Which would have allowed us to put debug code anywhere we
need. Such as @print total @. Then I can switch on and off
debugging statements by setting __debug__ to True or False where ever
I need it.

And as far as decorators go. I would of preferred a keyword, possibly
wrap, with a colon after it. Something like this.

I don't understand your seeming fixation with wrappers and wrapping. That's not
the only use for decorators. See Raymond Hettinger's optimizing decorators
in the cookbook for instance. Decorators are something like metaclasses for functions,
with much more general possibilities than wrapping, IMO.
def function_name(x):
return x

wrap function_name:
wrapper1()
wrapper2()
wrapper3()

A wrap command could more directly accomplish the wrapping, so that
def statements within def statements aren't needed. (Unless you
want'ed too for some reason.)
I think you'll have to show some convincing use cases showing a clear
advantage over current decoration coding if you want converts ;-)
And as far as arbitrary complex expressions go.. Actually I think that
it's quite possible to do as it is. ;-)
Yes and no. You can always write @deco and refer to an arbitrarily complex
callable deco ..., but the actual @-line expression has to abide by the language grammar,
and that does not allow arbitrary expressions. But it does provide enough rope.
E.g. see my most recent reply to El Pitonero.
But this is just a few of my current thoughts which may very well
change. It's an ever changing list. <g>
I can see that ;-)

Regards,
Bengt Richter
 
E

El Pitonero

Bengt said:
True, but tweakdoc doesn't have to be a function, so IMO we need a better
name for the @-line, unless you want to use many various specific names
like factory. E.g.,

There are two things:

(1) The "tweadoc" object in the example, which no doubt can be called a
decorator factory.

(2) The @-line, which you called a "decorator expression" and that's
fine with me. My preference would be something like the "decorator
header". A more clear statement would be something like: a "decorator
header expression" or the "expression in the decorator header", though
your proposed "decorator expression" would be clear enough, too.

I was addressing (1). You jumped in with (2), which I was aware of and
was not dissenting.
 
S

Scott David Daniels

Bengt said:
@deco.one
> def foo(): pass
Ok, now what do you call this @-line? Freaky decorator factory invocation?
Decoratoriferous expression? ;-)
This one has a certain je ne sais quoi ;-), but I'd call "deco.one"
the decorator function (since it _must_ evaluate to a function), and
if it needs more clarity, of your non-oderiferous proposals, I'd take:
Decorator expression for short?

--Scott David Daniels
(e-mail address removed)
 
R

Ron_Adam

I don't understand your seeming fixation with wrappers and wrapping.

Fixated implies, I'm stuck on a single thing, but I'm not. I am
learning as I go, and exploring some possibilities as well. :)
That's not
the only use for decorators. See Raymond Hettinger's optimizing decorators
in the cookbook for instance.

Thanks, I'll look for it. Is it in the new edition? I haven't picked
it up yet.
Decorators are something like metaclasses for functions,
with much more general possibilities than wrapping, IMO.

I'm not sure I see the metaclass relation. What more general things
can be done with Decorators, that can't be done with a wrapper?

Wrapping, and the @decorator expressions, interest me because I see a
lot of potential in it's use, and so I'm trying to learn them, and at
the same time, there are things about the @ expression that seems (to
me), that it's not the most practical way to do what it was intended
for.

On the plus side, it's kind of cute with the little curly thing
propped up on top of the function. It's a neat trick that it does what
it does with a minimal amount of changes to the language by taking
advantage of pythons existing function perimeter and object passing
properties. It saves a bit of typing because we don't have to retype
the function name a few times. (Several people have referred to it as
'sugar', and now I am starting to agree with that opinion.)

On the minus side, it's not intuitive to use. It is attached to the
function definitions so they are limited, they can't be easily
unwrapped and rewrapped without redefining the function also. The
recursive nature of stacked @ statements is not visible.

So my opinion of @ as a whole is currently: -1

I think you'll have to show some convincing use cases showing a clear
advantage over current decoration coding if you want converts ;-)

What about the following? :)

# Using this simple wrapper class:
class wrapper(object):
def __call__(self,x):
# preprocess x
x*=2 # Make a change so we can see it
result = self.function(x)
# postprocuess result
return result

# A function to apply the wrapper:
def wrap(function,wrapper):
w = wrapper()
w.function = function
return w

# The function
def fn(x):
return x

print fn(5) # Before

# Wrapit.
fn = wrap(fn,wrapper)

print fn(5) # After

# Unwrap it.
fn = fn.function

print fn(5) # And back again

#prints
#5
#10
#5

It has several advantages over @ expression. It doesn't need the
triple nested defines to get the function name and argument list, the
wrapper is simpler, It can be placed on a function and then removed,
when and where it's needed, instead of at the point where the function
is defined.

The following behaves more closely to the existing @ expression in
that it has the same nesting behavior for stacked wrappers.

I'm looking into a way to do sequential non-nested stacked wrappers at
this point, where the output of one goes to the input of the next.
That can't be done currently with the @ decorator expression.

This stacks a list of 10 wrappers on 10 different functions and
reverses the order of the stack every other function. In this case
they are all the same, but they could all be differnt.

Cheers,
Ron

#---start---
class wrapper(object):
def __call__(self,*x):
# preprocess
x = [x[0]+1,]
print 'preprocess', x[0], self.args
# call function
result = self.function(*x)
# postprocess
result +=1
print 'postprocess', result, self.args
return result

def wrap(f,w,shape='forward'):
if shape=='reverse':
w.reverse()
for ww in w:
nw = wrapper()
try:
nw.args = ww[1]
except TypeError:
wf = ww[0]
nw.function = f
f = nw
return f

# Make a list of ten wrappers with an id number as an additional
# wrapper perimeter.
w = []
for n in xrange(10):
w.append((wrapper,n))

# Wrap 10 functions, 10 times, in reversing order.
def func0(x): return x
def func1(x): return x
def func2(x): return x
def func3(x): return x
def func4(x): return x
def func5(x): return x
def func6(x): return x
def func7(x): return x
def func8(x): return x
def func9(x): return x

func0 = wrap(func0,w)
func1 = wrap(func1,w,'reverse')
func2 = wrap(func2,w)
func3 = wrap(func3,w,'reverse')
func4 = wrap(func4,w)
func5 = wrap(func5,w,'reverse')
func6 = wrap(func6,w)
func7 = wrap(func7,w,'reverse')
func8 = wrap(func8,w)
func9 = wrap(func9,w,'reverse')

print func0(0)
print func1(0)
print func2(0)
print func3(0)
print func4(0)
print func5(0)
print func6(0)
print func7(0)
print func8(0)
print func9(0)

#--end--
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,776
Messages
2,569,602
Members
45,185
Latest member
GluceaReviews

Latest Threads

Top