Abstract interfaces to subclasses of QWidget

2,214 views
Skip to first unread message

Marcus Ottosson

unread,
Oct 26, 2013, 9:25:36 AM10/26/13
to python_in...@googlegroups.com
This is more of a general programming question, but I figured it applies to Qt programming in particular and I keep encountering it without finding any nice enough solution.

I usually write an abstract interface for a set of classes and put dependencies on the interface rather than their implementations. 

When subclassing QWidget however, how can you make an interface for subclasses of an already implemented class? Is multiple inheritance a good idea in this scenario?

Currently, I'm writing the interface as a subclass of QWidget, is this a good way?

Thanks,
Marcus


--
Marcus Ottosson
konstr...@gmail.com

Marcus Ottosson

unread,
Oct 26, 2013, 10:02:15 AM10/26/13
to python_in...@googlegroups.com
An example

Subclasses of QTextEdit, QLineEdit and QTreeView all share common properties, signals and methods. I'd like to write an interface for them.

class AbstractEditor(QWidget):
   __metaclass__ = ABCMeta

   mysignal = pyqtSignal()
   
   @abstractmethod
   def load(self, file):
      """Load `file` into self"""

Then, I'd write each subclass, using this interface

class TextEditor(AbstractEditor, QTextEdit):
    """Implementation here"""

Since QTextEdit is already a subclass of QWidget, it's default implementation would get called twice, no? And if my interface wasn't derived from QWidget, or QObject for that matter, I wouldn't be allowed to use pyqtSignal()?
--
Marcus Ottosson
konstr...@gmail.com

Justin Israel

unread,
Oct 26, 2013, 4:00:08 PM10/26/13
to python_in...@googlegroups.com
I think the multiple inheritance route is just fine, to consider your abstract class as a mixin. The signal support is not a problem, since it will be processed as part of the QObject's metaclass. 
It's not legal to inherit from multiple QObjects, and you would see this when you try and call the constructor on both, which means it doesn't even really make sense to use a QObject as the base type of your mixin. 

class Abstract(QtGui.QWidget):
    pass

class Imp(QtGui.QLineEdit, Abstract):    
    def __init__(self):
        QtGui.QLineEdit.__init__(self)
        # Abstract.__init__(self) # <-- not legal
        # RuntimeError: You can't initialize an object twice!

Just use object or some other non-qt type. And the naming scheme of AbstractMixin, to me, indicates that it is to be used as a mixin and doesn't have a custom constructor. 

But, the addition of your own metaclass to the mixin will result in a conflict like this:

TypeError: Error when calling the metaclass bases
    metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

I've actually used this python recipe to support Qt mixins that have a metaclass and it worked great:



--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/CAFRtmOBL_NwPBZrMhf12FEROVPnBV_8%2B8fAqyZ2OKXqsTabujw%40mail.gmail.com.
For more options, visit https://groups.google.com/groups/opt_out.

Marcus Ottosson

unread,
Oct 27, 2013, 10:34:32 AM10/27/13
to python_in...@googlegroups.com
Thanks Justin, I'll give that a go.

I also found some interesting resources on the topic, as well as the issue of dynamically allocated signals.



For more options, visit https://groups.google.com/groups/opt_out.



--
Marcus Ottosson
konstr...@gmail.com

Marcus Ottosson

unread,
Jan 20, 2014, 3:18:09 AM1/20/14
to python_in...@googlegroups.com
Couple of months later, I've had a go with multiple inheritance and with injecting signals via a builder method.

The builder works rather well and serves the majority of needs, as "subclasses" would instead be monkey-patched and dependency injected to conform to the given interface (i.e. a fixed set of basic signals and functionality).

Signals not being inherited or transferable was solved by making my own signal class.

If anyone is interested, I expanded on some of the pros and cons of that here:
--
Marcus Ottosson
konstr...@gmail.com

Justin Israel

unread,
Jan 20, 2014, 5:08:52 AM1/20/14
to python_in...@googlegroups.com
Interesting article. I liked the part where you point out that various concepts in PyQt/PySide really just stem from the needs in the C++ world, but don't always carry the same relevance in python (QString, QVariant, ...). C++ had to do a lot more work to add the "dynamic" characteristics it provides (introspection and whatnot).  

Although, I did feel it was a bit light around the subject of what the pyqtSignal() is, and what you can't assign a fresh instance in your constructor. I haven't read the qt source code much, so I am not speaking from any sort of concrete wisdom. But I assume it's similar to the common python metaclasses. Methods start out as unbound methods, and transform into bound methods for each instance, encapsulating the reference to the instance. But with the signals, there is an added layer of needing to register with meta aspects of the Qt framework. This would be the reason you cannot replace an instance of the signal with a python object in the constructor. The underlying C++ layer has no knowledge of it. The same as how you can't replace *every* method on an existing QObject (or subclass) and expect it to be called. You can only do that with methods marked virtual.

I was curious about this bit:

"You’ll notice that if you try and send a signal from another thread, the recieving thread *might* crash on you. And therein lies the beauty of multi-threaded operations, or operations that share resources and try and access them simultaneously.

This includes any use of QThread and the Python threading module."


Can you expand on where this information comes from? What kind of crashes have you experienced, that you attribute them to the use of Signals and QThreads/Threads?






Marcus Ottosson

unread,
Jan 21, 2014, 1:53:18 AM1/21/14
to python_in...@googlegroups.com
Hey Justin, thanks for your feedback. :)
 
Interesting article. I liked the part where you point out that various concepts in PyQt/PySide really just stem from the needs in the C++ world, but don't always carry the same relevance in python (QString, QVariant, ...). C++ had to do a lot more work to add the "dynamic" characteristics it provides (introspection and whatnot).  


Glad you liked it. Perhaps I should add the bit about differences between API version 1 and 2 of QString too; that was a hurdle for me in the beginning (particularly its use in modelviews) and I'm sure there are more instances of problems like that throughout PyQt because, like you say, C++ being slightly different.
 
Although, I did feel it was a bit light around the subject of what the pyqtSignal() is, and what (reading why) you can't assign a fresh instance in your constructor.

The article really is more about the class at the top, than it is about pyqtSignal, but you're right, I wish I knew more about it than I do. The docs aren't very in-depth about this and after glancing over the PyQt source (source\qpy\QtCore\qpycore_pyqtsignal.h) I quickly decided it was over my head and decided to go for perceived experiences on this one.
 

Can you expand on where this information comes from? What kind of crashes have you experienced, that you attribute them to the use of Signals and QThreads/Threads?


Hm, it worries me that you ask. I can safely say that I've had threads crash with Signal that does work with pyqtSignal when signal is returning data, but I'm fairly new in regards to threading and my main source of attributing crashes with data exchange over threads comes from here (as the class contains no thread synchronisation or locking of resources). I would love to hear about your experiences in this area!

Best,
Marcus
 

For more options, visit https://groups.google.com/groups/opt_out.



--
Marcus Ottosson
konstr...@gmail.com

Justin Israel

unread,
Jan 21, 2014, 4:48:09 AM1/21/14
to python_in...@googlegroups.com
On Tue, Jan 21, 2014 at 7:53 PM, Marcus Ottosson <konstr...@gmail.com> wrote:
Hey Justin, thanks for your feedback. :)
 
Interesting article. I liked the part where you point out that various concepts in PyQt/PySide really just stem from the needs in the C++ world, but don't always carry the same relevance in python (QString, QVariant, ...). C++ had to do a lot more work to add the "dynamic" characteristics it provides (introspection and whatnot).  


Glad you liked it. Perhaps I should add the bit about differences between API version 1 and 2 of QString too; that was a hurdle for me in the beginning (particularly its use in modelviews) and I'm sure there are more instances of problems like that throughout PyQt because, like you say, C++ being slightly different.
 
Although, I did feel it was a bit light around the subject of what the pyqtSignal() is, and what (reading why) you can't assign a fresh instance in your constructor.

The article really is more about the class at the top, than it is about pyqtSignal, but you're right, I wish I knew more about it than I do. The docs aren't very in-depth about this and after glancing over the PyQt source (source\qpy\QtCore\qpycore_pyqtsignal.h) I quickly decided it was over my head and decided to go for perceived experiences on this one.
 

Can you expand on where this information comes from? What kind of crashes have you experienced, that you attribute them to the use of Signals and QThreads/Threads?


Hm, it worries me that you ask. I can safely say that I've had threads crash with Signal that does work with pyqtSignal when signal is returning data, but I'm fairly new in regards to threading and my main source of attributing crashes with data exchange over threads comes from here (as the class contains no thread synchronisation or locking of resources). I would love to hear about your experiences in this area!


I guess I would have needed to see a concrete example of one or more cases that cause a crash to know what is really the problem. But I disagreed with the implication that there is inherent instability when communicating data between threads using signals. I know that there are things you can do that can cause crashes, like trying to call QWidget methods from outside the main gui thread. And there are also wrong ways to set things up, where you are emitting a signal from an object in a thread that was actually created in the main thread and not moved. So you would think you are doing it threaded but really not. Or maybe you are communicating an Qt object that ends up getting garbage collected on the python side, causing an invalid pointer on the C++ side when it gets accessed. Again, its all circumstantial so without a concrete example its hard to put value into that statement. 
 

Marcus Ottosson

unread,
Jan 21, 2014, 6:19:58 AM1/21/14
to python_in...@googlegroups.com
That is some rather naive use of threads you're implying :) but you may be right. 

Trying to recreate the issue in a minimal snippet failed, but I'll post it here anyway and return once I encounter it again.

Just so I understand you correctly, you've successfully sent data between threads, without the use of pyqtSignal or using your own signal implementation, and not encountered any issues? That would be delightful, as I'm researching how to deal with thread-safety at the moment due to this exact issue. But maybe I won't have to!

The issue at hand relates to delegating computations to separate threads so as to increase the interactivity for users clicking about in a gui, so threads are run as users interact, with data being sent to and from the ui.

Thanks for your feedback Justin.

import time


from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *




class Window(QWidget):
   
def __init__(self, parent=None):
       
super(Window, self).__init__(parent)


       
self.__thread = None


       
self.button = QPushButton('Push me')
       
self.result = QLineEdit()
       
self.result.setObjectName('ResultWidget')


        layout
= QBoxLayout(QBoxLayout.TopToBottom, self)
        layout
.addWidget(self.button)
        layout
.addWidget(self.result)


       
self.button.pressed.connect(self.compute_result)


   
def compute_result(self):
       
if self.__thread:
           
self.__thread.exit()
           
self.__thread = None


       
class CascadingThread(QThread):
           
done = pyqtSignal(str)  # Emitted when finished
           
# done = Signal()
           
def run(self):
                time
.sleep(2)  # Pretend it takes quite a while
               
self.done.emit('Here is the result')


       
self.__thread = CascadingThread(self)  # Delegate computation to another thread
       
self.__thread.done.connect(self.computed_event)
       
self.__thread.start()


       
self.button.setEnabled(False)
       
self.result.setText('Computing...')


   
def computed_event(self, result):
       
self.result.setText(result)
       
self.button.setEnabled(True)








if __name__ == '__main__':
   
import sys


    app
= QApplication(sys.argv)


    win
= Window()
    win
.show()


    sys
.exit(app.exec_())

Best,
Marcus
 






To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.



--
Marcus Ottosson
konstr...@gmail.com




--
Marcus Ottosson
konstr...@gmail.com

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.



--
Marcus Ottosson
konstr...@gmail.com

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

Justin Israel

unread,
Jan 21, 2014, 2:30:04 PM1/21/14
to python_in...@googlegroups.com
On Wed, Jan 22, 2014 at 12:19 AM, Marcus Ottosson <konstr...@gmail.com> wrote:
That is some rather naive use of threads you're implying :) but you may be right. 

Well I'm not saying that you are using threads naively. I'm saying that I'm not aware of there being any validity to your blanket statement that there are inherently stability issues when passing data through signal/slots with threads. So if we take the position that the statement isn't true...then it would imply your might be doing something specific in your usage to produce instability. Although if there is some concrete evidence that it is a known issue, then you would be correct :-)
 

Trying to recreate the issue in a minimal snippet failed, but I'll post it here anyway and return once I encounter it again.

Just so I understand you correctly, you've successfully sent data between threads, without the use of pyqtSignal or using your own signal implementation, and not encountered any issues? That would be delightful, as I'm researching how to deal with thread-safety at the moment due to this exact issue. But maybe I won't have to!

I think maybe I misunderstood you previously. I thought we *were* talking about using signals to pass data between threads? But you are asking if I have done it without signals? 

Either way, I have done a mixture of all of them without a general problem. 
  • Qt Signals emitted with data from a thread that is received in a slot of the main thread
  • Threads that emit a signal without data, where the receiver then knows to go access a more complex data structure for results (instead of having received it all over the signal)
  • Threads that communicate over the standard library Queue container, containers guarded with mutexes. 
So I am not really sure where the issue is, unless there is some specific instance that produces a crash, similar to ones I had suggested in the previous post; Passing QObject references that might be invalid which produce hard crashes, etc.. There would have to be a reason for the crash, as far as I know, and not just general instability (I would thing).
 

The issue at hand relates to delegating computations to separate threads so as to increase the interactivity for users clicking about in a gui, so threads are run as users interact, with data being sent to and from the ui.

Ya, I didn't see an either with this code snippet functioning either. Would be interested to see some that crash though. I am sure there are cases where it could happen. Qt has a few different built-in approaches to threading:  The QThread directly, or subclassed. QRunnable + QThreadpool. QObject subclass that uses moveToThread.

To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/aafbc954-9049-4a6f-a631-f81cee7b9394%40googlegroups.com.

aevar.gu...@gmail.com

unread,
Jan 21, 2014, 2:49:05 PM1/21/14
to python_in...@googlegroups.com
(just butting into an interesting conversation)

Justin Wrote:
Although if there is some concrete evidence that it is a known issue,then you would be correct

  Ah!, but keep in mind that most known issues have known workarounds, a frequent crash can easily be unknown and never fully examined, often analysed from the wrong end even causing even more complications.  As in just because it’s not apparent in standard behaviour it does not make either party right or wrong.
  Just because there is no evidence to examine definitely does not equal the person observing it being wrong in any way. 

  If it helps, you can try to produce a crash using QTimer inside a QThread, that one is on my dirty list of “how on earth does that crash so often” without myself ever having been able to attribute the cause to either a threading issue or anything known that went wrong for the person who wrote it. 
  In my experience most of these cases turn out to be an OS problem rather than anything related to the scripting part of the process so I have to agree with both you guys on this one; It does crash a lot but indeed far from it being a known issue so it presumably doesn’t belong as the blanket statement it reads as.

Justin Israel

unread,
Jan 21, 2014, 3:27:54 PM1/21/14
to python_in...@googlegroups.com
On Wed, Jan 22, 2014 at 8:49 AM, <aevar.gu...@gmail.com> wrote:
(just butting into an interesting conversation)

Justin Wrote:
Although if there is some concrete evidence that it is a known issue,then you would be correct

  Ah!, but keep in mind that most known issues have known workarounds, a frequent crash can easily be unknown and never fully examined, often analysed from the wrong end even causing even more complications.  As in just because it’s not apparent in standard behaviour it does not make either party right or wrong.
  Just because there is no evidence to examine definitely does not equal the person observing it being wrong in any way. 

While I agree that people can find workarounds, I find it highly unlikely that instability with QThread + Signals would go unmentioned. I also didn't say "the person" was wrong either :-) I said that there has to be a reason for the crash. If it were random instability issues in the framework itself, I would have to believe it would be a tracked issue in Qt. My point is that if someone is going to make a statement like that, it should probably be followed with something like: "Consider the following example of what crashes..." or "Refer to these links for more information about this statement". But all in all I completely respect the opinions, findings, and experience being presented. 
 

  If it helps, you can try to produce a crash using QTimer inside a QThread, that one is on my dirty list of “how on earth does that crash so often” without myself ever having been able to attribute the cause to either a threading issue or anything known that went wrong for the person who wrote it. 
  In my experience most of these cases turn out to be an OS problem rather than anything related to the scripting part of the process so I have to agree with both you guys on this one; It does crash a lot but indeed far from it being a known issue so it presumably doesn’t belong as the blanket statement it reads as.

QTimer isn't threaded. It uses the event loop of the current thread. However, I do recall coming across reports of instability related to creating many many short singleShot timers. But I haven't looked into it much since I also don't recall encountering that problem personally.

I just in case i haven't said it recently....I love you all. 

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

Marcus Ottosson

unread,
Jan 22, 2014, 2:45:58 AM1/22/14
to python_in...@googlegroups.com
I think maybe I misunderstood you previously. I thought we *were* talking about using signals to pass data between threads? But you are asking if I have done it without signals? 

Yes, I think this is where we may have a misunderstanding. It probably stems for my ambiguity in use of the words pyqtSignal and the custom implementation Signal (as in the above article).

What I am trying to convey, is that using pyqtSignal works with threads (as far as I can tell), but Signal (which I'll refer to as CustomSignal from now on) in an identical scenario may not.

In the above example, I provided both pyqtSignal() and CustomSignal() as class attributes for the thread (CustomClass being temporarily commented out). I was trying to illustrate that taking out pyqtSignal for CustomSignal would cause a crash, but I should have been more explicit, so apologies for this.

Now I certainly see why wanted to get to the bottom of this! :) If someone made the claim that using Qt's own QThreads with pyqtSignal would be cause for crashes, I'd be doing the same thing.

In any case, I may still be completely off in my statement regarding passing data across threads causing crashes, so I'm glad we're still on the topic. In the three examples you listed, Justin, thread-safety is already inherent. What I would love to get a hold on however, is whether it is considered safe to pass data from one thread to another, or to simply trigger methods from one thread to another, by using a class as simple as my CustomSignal implementation that has no regard for locking of resources during its run.

I'm still having crashes and occasional hang-ups in my original program, and made a slightly longer version of in a minimal example, but did not succeed in reproducing the crash. Posting as is, just in case, and will once again return once I know more. 


import threading


from PyQt5.QtWidgets import *
from PyQt5.QtCore import *


PORT
= 18000




class CustomSignal:


   
def __init__(self):
       
self.__subscribers = []
     
   
def emit(self, *args, **kwargs):
       
for subs in self.__subscribers:
            subs
(*args, **kwargs)


   
def connect(self, func):
       
self.__subscribers.append(func)  
     
   
def disconnect(self, func):  
       
try:  
           
self.__subscribers.remove(func)  
       
except ValueError:  
           
print('Warning: function %s not removed '
                 
'from signal %s'%(func,self))




class Window(QWidget):


    rpc_show
= pyqtSignal()  # Causes no crash
   
# rpc_show = CustomSignal()  # Causes crash



   
def __init__(self, parent=None):
       
super(Window, self).__init__(parent)



       
self.__rpc_server = None


       
self.rpc_show.connect(self.restore)


   
def start_rpc(self):
       
from rpyc.utils.server import ThreadedServer
       
from rpyc.core import SlaveService


       
if self.__rpc_server:
           
self.__rpc_server.close()
           
self.__rpc_server = None


       
class Service(SlaveService):
           
def exposed_show(self):
               
self.show_signal.emit()


       
Service.show_signal = self.rpc_show
        server
= ThreadedServer(Service, port=PORT, reuse_addr=True)


       
self.__rpc_server = server


       
def thread():
           
self.__rpc_server.start()


       
print("Running RPC server")
        thread
= threading.Thread(target=thread, name="rpc")
        thread
.daemon = True
        thread
.start()


   
def restore(self):
       
self.activateWindow()
       
self.showNormal()




def start_application():

   
import sys
    app
= QApplication(sys.argv)


    win
= Window()
    win
.show()



    win
.start_rpc()


    sys
.exit(app.exec_())




def request_application():
   
print("Requesting a new Window")
   
   
import socket
   
import rpyc


   
try:
        proxy
= rpyc.connect('localhost', PORT)
        proxy
.root.show()
       
print("Restored existing instance of Window")


   
except socket.error:
       
print("Running new instance of Window")
        start_application
()




if __name__ == '__main__':
    request_application
()

Justin Israel

unread,
Jan 22, 2014, 3:22:12 AM1/22/14
to python_in...@googlegroups.com
Ah! Things are a lot clearer now. Thanks for explaining that. I get what you meant, and I actually see reasons why it would cause a crash :-)

I can't speak to specifically about rpyc, since I have never used it, but I can comment from a general viewpoint about some things:

In your custom signals class, when you perform the connect(), you are storing the function reference. That can be just fine for a general function, but for a method and even worse for a method of a QObject, you will end up with a problem. Storing just the method reference is not enough to prevent the object it is bound to from being garbage collected. So it is very possible in situations where you allow an object to be garbage collected, and there is no mechanism in there to disconnect the signal. it would just end up potentially calling on a dead object. Qt Signal/Slot automatically disconnects when QObjects are deleted.

Another thing is that there is no mechanism in your custom signals for when you are emitting from another thread. In Qt, if you emit from one thread to a sender in another thread, you have available the QueuedConnection type, which doesn't run the slot directly, but rather places it into the receivers event loop to be run in that thread. So your version would need to be calling those functions on thread safe functions/methods only.

I did some work extending a concept on ActiveState about weakmethods (as in weakrefs): https://gist.github.com/justinfx/6183367
It has code for doing weakmethods, as well as a system for doing arbitrary callbacks from another thread into the main threads event loops (kind of like what you are missing with QueuedConnections). Maybe that code would help in extending your custom signal design?

Like I said, I don't really know much about the rpyc library, but one thing that stands out in the example is that you are staring a threaded server, from within another thread. Is that necessary? Probably unrelated, and I don't know if a specific crash in this example involves that library or not.




--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.

Marcus Ottosson

unread,
Feb 1, 2014, 4:07:36 AM2/1/14
to python_in...@googlegroups.com
Sorry for taking so long to reply, I wanted to get some weakref experience before commenting as I've never encountered it before.

It seems weakref itself is not enough to keep threads from causing crashes, and it's my understanding that all it does is to allow objects to be garbage collected when the only references to said object are weak. It shouldn't have any affect on threading, but please correct me if I'm wrong.

Here is an example similar to what I ended up with.

Now, about your integration with threads and Qt, this may work, although I honestly didn't look through it too carefully because as I started having a signal class external to Qt, I've found other cases where this pattern is useful - i.e. in separating logic and ui, but also in lower level functionality unrelated to ui's - and are still investigating a way to decouple it from Qt altogether.

As far as CustomSignal, threading and Qt goes, I'm a few days in with trying to use both pyqtSignal and CustomSignal in tandem, pyqtSignal where I need interprocess communication (or really, where things crash) and CustomSignal everywhere else. As they share interface I haven't yet encountered a situation where it creates confusion.

About ThreadedServer. As far as I can tell, it just means that it threads each incoming task in a separate thread. The alternative would be ThreadedPoolServer which can do some queue-magic, but ultimately RPyC isn't an event-based networking engine (like say Twisted). But I'm no expert and could be wrong.



For more options, visit https://groups.google.com/groups/opt_out.



--
Marcus Ottosson
konstr...@gmail.com

Reply all
Reply to author
Forward
0 new messages