Merge lp:~robru/gwibber/discovery into lp:~barry/gwibber/py3

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1416
Proposed branch: lp:~robru/gwibber/discovery
Merge into: lp:~barry/gwibber/py3
Diff against target: 154 lines (+35/-59)
4 files modified
gwibber/gwibber/protocols/flickr.py (+1/-1)
gwibber/gwibber/tests/test_protocols.py (+11/-15)
gwibber/gwibber/utils/modules.py (+0/-43)
gwibber/gwibber/utils/protocol.py (+23/-0)
To merge this branch: bzr merge lp:~robru/gwibber/discovery
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+122995@code.launchpad.net

Description of the change

Ugh, today was a disaster :-/

So what I've done here is merged find_all_protocols and Base protocol class into gwibber.utils.protocol. I've also done the bare minimum to convert find_all_protocols into ProtocolManager.find_all. I ran into a number of seemingly intractable problems:

1. Despite Base class no longer living in protocols directory, inspect.getmembers is still managing to yield it from find_all. My guess would be that this is happening because the flickr plugin imports Base into it's own namespace, so inspect finds it in that namespace and then yields it. So the 'value is not Base' test is still necessary, defeating much of what I set out to do today. The only way I can see around this is to give up on the idea of importing all classes, and just import the class that matches the filename (or just live with it as-is, that's probably not so bad either).

2. My idea that the ProtocolManager would actually instantiate all plugins in it's __init__ method is another utter failure, because the plugins need accounts in order to be instantiated, and the PluginManager doesn't have any access to the accounts in order to actually do that. So the best we're going to get from this is a dict mapping basenames to classes (not instances), which I am about to implement. It's going to break your elegant generator, but unfortunately that is necessary because the other bits of gwibber are going to need to do 'ProtocolManager["flickr"]' and not 'for p in ProtocolManager.find_all(): if str(p) != "flickr": continue' or some other heinous mess.

Anyway, please review & merge this, and then we can discuss how to proceed with regards to the dispatcher interacting with plugins. I'm thinking that the Account class should contain a reference to the instance of the protocol that it belongs to, but in particular we need to figure out what the lifetime and scope of protocol instances is going be. Eg, can we design it so that the instance that represents each acount is persistent in memory (similar to my @memoize magic), or is it ok to have multiple different instances of the protocol class representing a given account (eg, if two different threads happen to instantiate their own Flickr plugin for the same account, is that ok, or is that going to conflict with each other). I guess what I'm really trying to say is that we need to audit this code for thread safety.

Thanks!

To post a comment you must log in.
lp:~robru/gwibber/discovery updated
1419. By Robert Bruce Park

Make ProtocolManager a dict mapping protocol names to their classes.

Revision history for this message
Robert Bruce Park (robru) wrote :

Barry, I can't figure out why the tests aren't passing here, it works successfully when I access it from an interactive python session. Something about the unittest environment is breaking something else that's going on, but I can't figure out what because I haven't changed much from your code that did work, the biggest change is that it's executing the code immediately instead of as a generator.

So sorry for mp'ing broken code, hopefully there's something obvious that I've overlooked because I'm exhausted. Please have a look!

lp:~robru/gwibber/discovery updated
1420. By Robert Bruce Park

Drop @singleton decorator to make tests pass.

Revision history for this message
Barry Warsaw (barry) wrote :
Download full text (9.3 KiB)

On Sep 06, 2012, at 03:22 AM, Robert Bruce Park wrote:

>Ugh, today was a disaster :-/

Bummer! Today will surely be better.

>1. Despite Base class no longer living in protocols directory,
>inspect.getmembers is still managing to yield it from find_all. My guess
>would be that this is happening because the flickr plugin imports Base into
>it's own namespace, so inspect finds it in that namespace and then yields
>it.

Yep, that's exactly what's happening.

>So the 'value is not Base' test is still necessary, defeating much of what I
>set out to do today. The only way I can see around this is to give up on the
>idea of importing all classes, and just import the class that matches the
>filename (or just live with it as-is, that's probably not so bad either).

Actually, I realize that instead of iterating over all members of the module,
you should just iterate over the names in the module's __all__. That's the
intent of __all__ anyway, to explicitly export the public members of a module.
Of course it means that __all__ has to be kept up-to-date, but I think that's
a good idea anyway.

The loop will still have to do the subclass check, but it won't need to check
for non-Base-y-ness.

>2. My idea that the ProtocolManager would actually instantiate all plugins in
>it's __init__ method is another utter failure, because the plugins need
>accounts in order to be instantiated, and the PluginManager doesn't have any
>access to the accounts in order to actually do that.

We discussed this a bit in IRC. I definitely need to read up on libaccounts,
but I think we've decided that it's possible to have multiple instances of the
protocol classes with different account information. E.g. you might have two
Twitter accounts. So yeah, ProtocolManager can only find and record the
protocol classes, though depending on the outcome of future designs, it could
possibly provide an API for more convenient instantiation of the classes. I
think we're agreed that there need be only one global "account manager" per
gwibber-service.

We'll cross that bridge later though.

>So the best we're going to get from this is a dict mapping basenames to
>classes (not instances), which I am about to implement. It's going to break
>your elegant generator, but unfortunately that is necessary because the other
>bits of gwibber are going to need to do 'ProtocolManager["flickr"]' and not
>'for p in ProtocolManager.find_all(): if str(p) != "flickr": continue' or
>some other heinous mess.

I'm also not psyched about having ProtocolManager be a subclass of dict. In
general, I don't like code that does that just to provide a getitem API to
clients, but the class itself isn't intended to be used everywhere a dict is
to be used. It seems like an inappropriate use of inheritance. We use it in
the Account class (which I still don't like, but oh well), but I don't think
we should use that for ProtocolManager.

Or to put it in OOP parlance: a ProtocolManager *has a* dict, but not *is a*
dict. It's generally frowned upon to use inheritance for *has a*
relationships. I'd like to rewrite that for the merge.

One other thing I've changed. The protocol classes themselves have a 'name'
attribut...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/gwibber/protocols/flickr.py'
2--- gwibber/gwibber/protocols/flickr.py 2012-09-05 22:09:44 +0000
3+++ gwibber/gwibber/protocols/flickr.py 2012-09-06 15:16:41 +0000
4@@ -23,11 +23,11 @@
5 import logging
6
7 from gwibber.errors import AuthorizationError
8-from gwibber.protocols.base import Base
9 from gwibber.utils.authentication import Authentication
10 from gwibber.utils.download import get_json
11 from gwibber.utils.results import Results
12 from gwibber.utils.time import parsetime
13+from gwibber.utils.protocol import Base
14
15
16 log = logging.getLogger('gwibber.service')
17
18=== modified file 'gwibber/gwibber/tests/test_protocols.py'
19--- gwibber/gwibber/tests/test_protocols.py 2012-09-05 22:09:44 +0000
20+++ gwibber/gwibber/tests/test_protocols.py 2012-09-06 15:16:41 +0000
21@@ -25,12 +25,12 @@
22 import unittest
23 import threading
24
25-from gwibber.errors import AuthorizationError
26-from gwibber.protocols.base import Base
27+from gwibber.testing.mocks import FakeData
28 from gwibber.protocols.flickr import Flickr
29-from gwibber.testing.mocks import FakeData
30 from gwibber.utils.logging import initialize
31-from gwibber.utils.modules import find_all_protocols
32+from gwibber.errors import AuthorizationError
33+from gwibber.utils.protocol import Base, ProtocolManager
34+
35
36 try:
37 # Python 3.3
38@@ -43,20 +43,16 @@
39 """Test the protocol finder."""
40
41 def test_find_all_protocols(self):
42- # Search for and return all of the discovered classes implementing the
43- # known protocols.
44- flickr_class = None
45- for protocol_class in find_all_protocols():
46- if protocol_class.name == 'Flickr':
47- flickr_class = protocol_class
48- break
49- else:
50- raise AssertionError('Flickr protocol class not found')
51- self.assertEqual(flickr_class, Flickr)
52+ # ProtocolManager knows them all.
53+ manager = ProtocolManager()
54+ self.assertTrue('flickr' in manager)
55+ self.assertEqual(manager['flickr'], Flickr)
56
57 def test_doesnt_find_base(self):
58 # Make sure that the base protocol class isn't returned.
59- for protocol_class in find_all_protocols():
60+ manager = ProtocolManager()
61+ self.assertFalse('base' in manager)
62+ for protocol_class in manager.values():
63 self.assertNotEqual(protocol_class, Base)
64 self.assertIsNotNone(protocol_class.name)
65
66
67=== removed file 'gwibber/gwibber/utils/modules.py'
68--- gwibber/gwibber/utils/modules.py 2012-09-05 19:22:46 +0000
69+++ gwibber/gwibber/utils/modules.py 1970-01-01 00:00:00 +0000
70@@ -1,43 +0,0 @@
71-# Copyright (C) 2012 Canonical Ltd
72-#
73-# This program is free software: you can redistribute it and/or modify
74-# it under the terms of the GNU General Public License version 2 as
75-# published by the Free Software Foundation.
76-#
77-# This program is distributed in the hope that it will be useful,
78-# but WITHOUT ANY WARRANTY; without even the implied warranty of
79-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
80-# GNU General Public License for more details.
81-#
82-# You should have received a copy of the GNU General Public License
83-# along with this program. If not, see <http://www.gnu.org/licenses/>.
84-
85-"""Utilities for finding things in modules."""
86-
87-__all__ = [
88- 'find_all_protocols',
89- ]
90-
91-
92-import os
93-import inspect
94-import importlib
95-
96-from pkg_resources import resource_listdir
97-
98-from gwibber.protocols.base import Base
99-
100-
101-def find_all_protocols():
102- for filename in resource_listdir('gwibber', 'protocols'):
103- basename, extension = os.path.splitext(filename)
104- if extension != '.py':
105- continue
106- module_path = 'gwibber.protocols.' + basename
107- module = importlib.import_module(module_path)
108- # Now, look at all the names in the just imported module. Return all
109- # class objects that inherit from the base protocol class. Be sure
110- # not to return the base protocol class itself!
111- for name, value in inspect.getmembers(module, inspect.isclass):
112- if issubclass(value, Base) and value is not Base:
113- yield value
114
115=== renamed file 'gwibber/gwibber/protocols/base.py' => 'gwibber/gwibber/utils/protocol.py'
116--- gwibber/gwibber/protocols/base.py 2012-09-05 14:18:12 +0000
117+++ gwibber/gwibber/utils/protocol.py 2012-09-06 15:16:41 +0000
118@@ -16,9 +16,16 @@
119
120 __all__ = [
121 'Base',
122+ 'ProtocolManager',
123 ]
124
125
126+import os
127+import inspect
128+import importlib
129+
130+from pkg_resources import resource_listdir
131+
132 from threading import Thread
133
134
135@@ -89,3 +96,19 @@
136 def _locked_login(self, old_token):
137 """Login operation stub. This must be implemented by subclasses."""
138 raise NotImplementedError
139+
140+
141+class ProtocolManager(dict):
142+ """Discover and instantiate all protocols."""
143+ def __init__(self):
144+ for filename in resource_listdir('gwibber', 'protocols'):
145+ basename, extension = os.path.splitext(filename)
146+ if extension != '.py':
147+ continue
148+ module_path = 'gwibber.protocols.' + basename
149+ module = importlib.import_module(module_path)
150+ # Now, look at all the names in the just imported module. Return all
151+ # class objects that inherit from the Base protocol class.
152+ self.update((name.lower(), obj) for (name, obj)
153+ in inspect.getmembers(module, inspect.isclass)
154+ if issubclass(obj, Base) and obj is not Base)

Subscribers

People subscribed via source and target branches