Merge lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb into lp:ubuntu/karmic/desktopcouch
- Karmic (9.10)
- spb
- Merge into karmic
Status: | Rejected |
---|---|
Rejected by: | James Westby |
Proposed branch: | lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb |
Merge into: | lp:ubuntu/karmic/desktopcouch |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby (community) | Disapprove | ||
Review via email: mp+11778@code.launchpad.net |
Commit message
Description of the change
James Westby (james-w) wrote : | # |
Elliot Murphy (statik) wrote : | # |
> This should go in after the alpha I think. Pushing it in now
> may require a respin, so we only want to do that if it is
> critical to have it for the alpha.
+1 on waiting until after the alpha. I haven't reviewed the branch yet, what might require a respin?
James Westby (james-w) wrote : | # |
> > This should go in after the alpha I think. Pushing it in now
> > may require a respin, so we only want to do that if it is
> > critical to have it for the alpha.
>
> +1 on waiting until after the alpha. I haven't reviewed the branch yet, what
> might require a respin?
Getting a new package on the CDs at this point requires a respin of the CDs.
Putting in a new version of the package that is on the CDs and not respinning
is undesirable, as it can lead to headaches if there is a respin, or when
testing.
[ I realise my original comment was ambiguous now. I said "may require
a respin", but meant "may require a respin that isn't going to happen
for other reasons." We could have put the package in as things hadn't
settled down yet, but with all the chaos in the last 24 hours I'm loath
to risk adding to it ]
Thanks,
James
James Westby (james-w) wrote : | # |
Hi,
lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb
is not based on lp:ubuntu/desktopcouch, so I can't do the
requested merge.
It is instead based on lp:~ubuntu-desktop/ubuntu/karmic/desktopcouch/karmic
which I could merge to, except I'm not in ~ubuntu-desktop, and
the last upload is not recorded there.
If you would like to continue using the ~ubuntu-desktop branch
then we can accommodate that, and soon make lp:ubuntu/desktopcouch
point to it.
Thanks,
James
Elliot Murphy (statik) wrote : | # |
On 09/18/2009 06:25 AM, James Westby wrote:
>
> If you would like to continue using the ~ubuntu-desktop branch
> then we can accommodate that, and soon make lp:ubuntu/desktopcouch
> point to it.
>
I wanted to move away from the ~ubuntu-desktop branch since nobody in
ubuntuone-hackers could merge to it. Probably this just needs some
merging to get consistent with the last upload from lp:ubuntu/desktopcouch
--
Elliot Murphy | https:/
Unmerged revisions
- 61. By Chad Miller
-
Merge 0.4.2 from trunk.
- 60. By Chad Miller
-
* Log to correct place in service, ~/.cache/
desktop- couch/ .
* Abandon service modules giving host and port info separate from the
db prefixes, which now give a URI.
* Let the replicator gather its own OAuth data to use to connect to far end.
* Make service more resilient to errors, and make breakable parts in smaller
granules.
* Support Python 2.5.
* Added basic notes record API to include NOTE_RECORD_TYPE.
* Make create-contacts script agnostic of desktop vs cloud and oauth vs noauth.
* New upstream release (LP: #435429)
* Depend on couchdb-bin instead of couchdb (LP: #427036) - 59. By Chad Miller
-
Add replication_
services to python-desktopcouch package. - 58. By Chad Miller
-
desktopcouch (0.4-0ubuntu1) karmic; urgency=low
* Packaging: desktopcouch-tools installed by default in Karmic (LP: #427421)
* Forcing desktopcouch auth on. (LP: #427446)
* Requiring new version of couchdb that supports authentication properly.
* Pairing updates couchdb replication system. (LP: #397663)
* Added pairing of desktop Couches to desktopcouch-tools (LP: #404087)
* Admin users in the system couchdb are no longer inherited by desktopcouch
couchdbs (LP: #424330)
* Fixed failing tests in desktopcouch (LP: #405612)
* Creating login details on initial desktopcouch setup (LP: #416413)
* Starting replication to paired servers on desktopcouch startup.
(LP: #416581)
* Unpaired couchdb peers are reconciled with replication. (LP: #424386)
* At pairing time, changing couchdb pairing address to public. (LP: #419969)
* In replication daemon, verifying local couchdb bind address is not 127/8 .
(LP: #419973)
* getPort no longer suceeds when desktopcouch isn't running. (LP: #422127)-- Chad Miller <email address hidden>Mon, 14 Sep 2009 19:24:08 -0400
- 57. By Chad Miller
-
Merge from desktopcouch trunk for 0.4 release.
- 56. By Chad Miller
-
desktopcouch (0.3.1-0ubuntu1) karmic; urgency=low
- Added depends on python-desktopcouch- records. (LP: #422179)
* New upstream release.
* Fix getPort failure. (LP: #420911, LP: #422127)
* Check couchdb bind-port in replicator. (LP: #419973)
* Change couchdb bind-port in pairing tool. (LP: #419969) - 55. By Chad Miller
-
Release 0.3.1.
- 54. By Chad Miller
-
Merge fixes for getPort failures.
- 53. By Chad Miller
-
desktopcouch (0.3.1-0ubuntu1) UNRELEASED; urgency=low
[Ken VanDine]
* debian/control
[Chad Miller]
* Changed Vcs-Bzr links in control file.
* Changed version dependency on couchdb.
* Converting to a full-blown source-package branch.
-- Chad Miller <email address hidden>Tue, 01 Sep 2009 11:57:25 -0400 - 52. By Chad Miller
-
Taking over packaging branch from
lp:~ubuntu-desktop/ubuntu/karmic/desktopcouch/karmic at revno:82,
revid:<email address hidden>
Preview Diff
1 | === added file 'COPYING' | |||
2 | --- COPYING 1970-01-01 00:00:00 +0000 | |||
3 | +++ COPYING 2009-07-09 20:17:15 +0000 | |||
4 | @@ -0,0 +1,676 @@ | |||
5 | 1 | |||
6 | 2 | GNU GENERAL PUBLIC LICENSE | ||
7 | 3 | Version 3, 29 June 2007 | ||
8 | 4 | |||
9 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> | ||
10 | 6 | Everyone is permitted to copy and distribute verbatim copies | ||
11 | 7 | of this license document, but changing it is not allowed. | ||
12 | 8 | |||
13 | 9 | Preamble | ||
14 | 10 | |||
15 | 11 | The GNU General Public License is a free, copyleft license for | ||
16 | 12 | software and other kinds of works. | ||
17 | 13 | |||
18 | 14 | The licenses for most software and other practical works are designed | ||
19 | 15 | to take away your freedom to share and change the works. By contrast, | ||
20 | 16 | the GNU General Public License is intended to guarantee your freedom to | ||
21 | 17 | share and change all versions of a program--to make sure it remains free | ||
22 | 18 | software for all its users. We, the Free Software Foundation, use the | ||
23 | 19 | GNU General Public License for most of our software; it applies also to | ||
24 | 20 | any other work released this way by its authors. You can apply it to | ||
25 | 21 | your programs, too. | ||
26 | 22 | |||
27 | 23 | When we speak of free software, we are referring to freedom, not | ||
28 | 24 | price. Our General Public Licenses are designed to make sure that you | ||
29 | 25 | have the freedom to distribute copies of free software (and charge for | ||
30 | 26 | them if you wish), that you receive source code or can get it if you | ||
31 | 27 | want it, that you can change the software or use pieces of it in new | ||
32 | 28 | free programs, and that you know you can do these things. | ||
33 | 29 | |||
34 | 30 | To protect your rights, we need to prevent others from denying you | ||
35 | 31 | these rights or asking you to surrender the rights. Therefore, you have | ||
36 | 32 | certain responsibilities if you distribute copies of the software, or if | ||
37 | 33 | you modify it: responsibilities to respect the freedom of others. | ||
38 | 34 | |||
39 | 35 | For example, if you distribute copies of such a program, whether | ||
40 | 36 | gratis or for a fee, you must pass on to the recipients the same | ||
41 | 37 | freedoms that you received. You must make sure that they, too, receive | ||
42 | 38 | or can get the source code. And you must show them these terms so they | ||
43 | 39 | know their rights. | ||
44 | 40 | |||
45 | 41 | Developers that use the GNU GPL protect your rights with two steps: | ||
46 | 42 | (1) assert copyright on the software, and (2) offer you this License | ||
47 | 43 | giving you legal permission to copy, distribute and/or modify it. | ||
48 | 44 | |||
49 | 45 | For the developers' and authors' protection, the GPL clearly explains | ||
50 | 46 | that there is no warranty for this free software. For both users' and | ||
51 | 47 | authors' sake, the GPL requires that modified versions be marked as | ||
52 | 48 | changed, so that their problems will not be attributed erroneously to | ||
53 | 49 | authors of previous versions. | ||
54 | 50 | |||
55 | 51 | Some devices are designed to deny users access to install or run | ||
56 | 52 | modified versions of the software inside them, although the manufacturer | ||
57 | 53 | can do so. This is fundamentally incompatible with the aim of | ||
58 | 54 | protecting users' freedom to change the software. The systematic | ||
59 | 55 | pattern of such abuse occurs in the area of products for individuals to | ||
60 | 56 | use, which is precisely where it is most unacceptable. Therefore, we | ||
61 | 57 | have designed this version of the GPL to prohibit the practice for those | ||
62 | 58 | products. If such problems arise substantially in other domains, we | ||
63 | 59 | stand ready to extend this provision to those domains in future versions | ||
64 | 60 | of the GPL, as needed to protect the freedom of users. | ||
65 | 61 | |||
66 | 62 | Finally, every program is threatened constantly by software patents. | ||
67 | 63 | States should not allow patents to restrict development and use of | ||
68 | 64 | software on general-purpose computers, but in those that do, we wish to | ||
69 | 65 | avoid the special danger that patents applied to a free program could | ||
70 | 66 | make it effectively proprietary. To prevent this, the GPL assures that | ||
71 | 67 | patents cannot be used to render the program non-free. | ||
72 | 68 | |||
73 | 69 | The precise terms and conditions for copying, distribution and | ||
74 | 70 | modification follow. | ||
75 | 71 | |||
76 | 72 | TERMS AND CONDITIONS | ||
77 | 73 | |||
78 | 74 | 0. Definitions. | ||
79 | 75 | |||
80 | 76 | "This License" refers to version 3 of the GNU General Public License. | ||
81 | 77 | |||
82 | 78 | "Copyright" also means copyright-like laws that apply to other kinds of | ||
83 | 79 | works, such as semiconductor masks. | ||
84 | 80 | |||
85 | 81 | "The Program" refers to any copyrightable work licensed under this | ||
86 | 82 | License. Each licensee is addressed as "you". "Licensees" and | ||
87 | 83 | "recipients" may be individuals or organizations. | ||
88 | 84 | |||
89 | 85 | To "modify" a work means to copy from or adapt all or part of the work | ||
90 | 86 | in a fashion requiring copyright permission, other than the making of an | ||
91 | 87 | exact copy. The resulting work is called a "modified version" of the | ||
92 | 88 | earlier work or a work "based on" the earlier work. | ||
93 | 89 | |||
94 | 90 | A "covered work" means either the unmodified Program or a work based | ||
95 | 91 | on the Program. | ||
96 | 92 | |||
97 | 93 | To "propagate" a work means to do anything with it that, without | ||
98 | 94 | permission, would make you directly or secondarily liable for | ||
99 | 95 | infringement under applicable copyright law, except executing it on a | ||
100 | 96 | computer or modifying a private copy. Propagation includes copying, | ||
101 | 97 | distribution (with or without modification), making available to the | ||
102 | 98 | public, and in some countries other activities as well. | ||
103 | 99 | |||
104 | 100 | To "convey" a work means any kind of propagation that enables other | ||
105 | 101 | parties to make or receive copies. Mere interaction with a user through | ||
106 | 102 | a computer network, with no transfer of a copy, is not conveying. | ||
107 | 103 | |||
108 | 104 | An interactive user interface displays "Appropriate Legal Notices" | ||
109 | 105 | to the extent that it includes a convenient and prominently visible | ||
110 | 106 | feature that (1) displays an appropriate copyright notice, and (2) | ||
111 | 107 | tells the user that there is no warranty for the work (except to the | ||
112 | 108 | extent that warranties are provided), that licensees may convey the | ||
113 | 109 | work under this License, and how to view a copy of this License. If | ||
114 | 110 | the interface presents a list of user commands or options, such as a | ||
115 | 111 | menu, a prominent item in the list meets this criterion. | ||
116 | 112 | |||
117 | 113 | 1. Source Code. | ||
118 | 114 | |||
119 | 115 | The "source code" for a work means the preferred form of the work | ||
120 | 116 | for making modifications to it. "Object code" means any non-source | ||
121 | 117 | form of a work. | ||
122 | 118 | |||
123 | 119 | A "Standard Interface" means an interface that either is an official | ||
124 | 120 | standard defined by a recognized standards body, or, in the case of | ||
125 | 121 | interfaces specified for a particular programming language, one that | ||
126 | 122 | is widely used among developers working in that language. | ||
127 | 123 | |||
128 | 124 | The "System Libraries" of an executable work include anything, other | ||
129 | 125 | than the work as a whole, that (a) is included in the normal form of | ||
130 | 126 | packaging a Major Component, but which is not part of that Major | ||
131 | 127 | Component, and (b) serves only to enable use of the work with that | ||
132 | 128 | Major Component, or to implement a Standard Interface for which an | ||
133 | 129 | implementation is available to the public in source code form. A | ||
134 | 130 | "Major Component", in this context, means a major essential component | ||
135 | 131 | (kernel, window system, and so on) of the specific operating system | ||
136 | 132 | (if any) on which the executable work runs, or a compiler used to | ||
137 | 133 | produce the work, or an object code interpreter used to run it. | ||
138 | 134 | |||
139 | 135 | The "Corresponding Source" for a work in object code form means all | ||
140 | 136 | the source code needed to generate, install, and (for an executable | ||
141 | 137 | work) run the object code and to modify the work, including scripts to | ||
142 | 138 | control those activities. However, it does not include the work's | ||
143 | 139 | System Libraries, or general-purpose tools or generally available free | ||
144 | 140 | programs which are used unmodified in performing those activities but | ||
145 | 141 | which are not part of the work. For example, Corresponding Source | ||
146 | 142 | includes interface definition files associated with source files for | ||
147 | 143 | the work, and the source code for shared libraries and dynamically | ||
148 | 144 | linked subprograms that the work is specifically designed to require, | ||
149 | 145 | such as by intimate data communication or control flow between those | ||
150 | 146 | subprograms and other parts of the work. | ||
151 | 147 | |||
152 | 148 | The Corresponding Source need not include anything that users | ||
153 | 149 | can regenerate automatically from other parts of the Corresponding | ||
154 | 150 | Source. | ||
155 | 151 | |||
156 | 152 | The Corresponding Source for a work in source code form is that | ||
157 | 153 | same work. | ||
158 | 154 | |||
159 | 155 | 2. Basic Permissions. | ||
160 | 156 | |||
161 | 157 | All rights granted under this License are granted for the term of | ||
162 | 158 | copyright on the Program, and are irrevocable provided the stated | ||
163 | 159 | conditions are met. This License explicitly affirms your unlimited | ||
164 | 160 | permission to run the unmodified Program. The output from running a | ||
165 | 161 | covered work is covered by this License only if the output, given its | ||
166 | 162 | content, constitutes a covered work. This License acknowledges your | ||
167 | 163 | rights of fair use or other equivalent, as provided by copyright law. | ||
168 | 164 | |||
169 | 165 | You may make, run and propagate covered works that you do not | ||
170 | 166 | convey, without conditions so long as your license otherwise remains | ||
171 | 167 | in force. You may convey covered works to others for the sole purpose | ||
172 | 168 | of having them make modifications exclusively for you, or provide you | ||
173 | 169 | with facilities for running those works, provided that you comply with | ||
174 | 170 | the terms of this License in conveying all material for which you do | ||
175 | 171 | not control copyright. Those thus making or running the covered works | ||
176 | 172 | for you must do so exclusively on your behalf, under your direction | ||
177 | 173 | and control, on terms that prohibit them from making any copies of | ||
178 | 174 | your copyrighted material outside their relationship with you. | ||
179 | 175 | |||
180 | 176 | Conveying under any other circumstances is permitted solely under | ||
181 | 177 | the conditions stated below. Sublicensing is not allowed; section 10 | ||
182 | 178 | makes it unnecessary. | ||
183 | 179 | |||
184 | 180 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||
185 | 181 | |||
186 | 182 | No covered work shall be deemed part of an effective technological | ||
187 | 183 | measure under any applicable law fulfilling obligations under article | ||
188 | 184 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||
189 | 185 | similar laws prohibiting or restricting circumvention of such | ||
190 | 186 | measures. | ||
191 | 187 | |||
192 | 188 | When you convey a covered work, you waive any legal power to forbid | ||
193 | 189 | circumvention of technological measures to the extent such circumvention | ||
194 | 190 | is effected by exercising rights under this License with respect to | ||
195 | 191 | the covered work, and you disclaim any intention to limit operation or | ||
196 | 192 | modification of the work as a means of enforcing, against the work's | ||
197 | 193 | users, your or third parties' legal rights to forbid circumvention of | ||
198 | 194 | technological measures. | ||
199 | 195 | |||
200 | 196 | 4. Conveying Verbatim Copies. | ||
201 | 197 | |||
202 | 198 | You may convey verbatim copies of the Program's source code as you | ||
203 | 199 | receive it, in any medium, provided that you conspicuously and | ||
204 | 200 | appropriately publish on each copy an appropriate copyright notice; | ||
205 | 201 | keep intact all notices stating that this License and any | ||
206 | 202 | non-permissive terms added in accord with section 7 apply to the code; | ||
207 | 203 | keep intact all notices of the absence of any warranty; and give all | ||
208 | 204 | recipients a copy of this License along with the Program. | ||
209 | 205 | |||
210 | 206 | You may charge any price or no price for each copy that you convey, | ||
211 | 207 | and you may offer support or warranty protection for a fee. | ||
212 | 208 | |||
213 | 209 | 5. Conveying Modified Source Versions. | ||
214 | 210 | |||
215 | 211 | You may convey a work based on the Program, or the modifications to | ||
216 | 212 | produce it from the Program, in the form of source code under the | ||
217 | 213 | terms of section 4, provided that you also meet all of these conditions: | ||
218 | 214 | |||
219 | 215 | a) The work must carry prominent notices stating that you modified | ||
220 | 216 | it, and giving a relevant date. | ||
221 | 217 | |||
222 | 218 | b) The work must carry prominent notices stating that it is | ||
223 | 219 | released under this License and any conditions added under section | ||
224 | 220 | 7. This requirement modifies the requirement in section 4 to | ||
225 | 221 | "keep intact all notices". | ||
226 | 222 | |||
227 | 223 | c) You must license the entire work, as a whole, under this | ||
228 | 224 | License to anyone who comes into possession of a copy. This | ||
229 | 225 | License will therefore apply, along with any applicable section 7 | ||
230 | 226 | additional terms, to the whole of the work, and all its parts, | ||
231 | 227 | regardless of how they are packaged. This License gives no | ||
232 | 228 | permission to license the work in any other way, but it does not | ||
233 | 229 | invalidate such permission if you have separately received it. | ||
234 | 230 | |||
235 | 231 | d) If the work has interactive user interfaces, each must display | ||
236 | 232 | Appropriate Legal Notices; however, if the Program has interactive | ||
237 | 233 | interfaces that do not display Appropriate Legal Notices, your | ||
238 | 234 | work need not make them do so. | ||
239 | 235 | |||
240 | 236 | A compilation of a covered work with other separate and independent | ||
241 | 237 | works, which are not by their nature extensions of the covered work, | ||
242 | 238 | and which are not combined with it such as to form a larger program, | ||
243 | 239 | in or on a volume of a storage or distribution medium, is called an | ||
244 | 240 | "aggregate" if the compilation and its resulting copyright are not | ||
245 | 241 | used to limit the access or legal rights of the compilation's users | ||
246 | 242 | beyond what the individual works permit. Inclusion of a covered work | ||
247 | 243 | in an aggregate does not cause this License to apply to the other | ||
248 | 244 | parts of the aggregate. | ||
249 | 245 | |||
250 | 246 | 6. Conveying Non-Source Forms. | ||
251 | 247 | |||
252 | 248 | You may convey a covered work in object code form under the terms | ||
253 | 249 | of sections 4 and 5, provided that you also convey the | ||
254 | 250 | machine-readable Corresponding Source under the terms of this License, | ||
255 | 251 | in one of these ways: | ||
256 | 252 | |||
257 | 253 | a) Convey the object code in, or embodied in, a physical product | ||
258 | 254 | (including a physical distribution medium), accompanied by the | ||
259 | 255 | Corresponding Source fixed on a durable physical medium | ||
260 | 256 | customarily used for software interchange. | ||
261 | 257 | |||
262 | 258 | b) Convey the object code in, or embodied in, a physical product | ||
263 | 259 | (including a physical distribution medium), accompanied by a | ||
264 | 260 | written offer, valid for at least three years and valid for as | ||
265 | 261 | long as you offer spare parts or customer support for that product | ||
266 | 262 | model, to give anyone who possesses the object code either (1) a | ||
267 | 263 | copy of the Corresponding Source for all the software in the | ||
268 | 264 | product that is covered by this License, on a durable physical | ||
269 | 265 | medium customarily used for software interchange, for a price no | ||
270 | 266 | more than your reasonable cost of physically performing this | ||
271 | 267 | conveying of source, or (2) access to copy the | ||
272 | 268 | Corresponding Source from a network server at no charge. | ||
273 | 269 | |||
274 | 270 | c) Convey individual copies of the object code with a copy of the | ||
275 | 271 | written offer to provide the Corresponding Source. This | ||
276 | 272 | alternative is allowed only occasionally and noncommercially, and | ||
277 | 273 | only if you received the object code with such an offer, in accord | ||
278 | 274 | with subsection 6b. | ||
279 | 275 | |||
280 | 276 | d) Convey the object code by offering access from a designated | ||
281 | 277 | place (gratis or for a charge), and offer equivalent access to the | ||
282 | 278 | Corresponding Source in the same way through the same place at no | ||
283 | 279 | further charge. You need not require recipients to copy the | ||
284 | 280 | Corresponding Source along with the object code. If the place to | ||
285 | 281 | copy the object code is a network server, the Corresponding Source | ||
286 | 282 | may be on a different server (operated by you or a third party) | ||
287 | 283 | that supports equivalent copying facilities, provided you maintain | ||
288 | 284 | clear directions next to the object code saying where to find the | ||
289 | 285 | Corresponding Source. Regardless of what server hosts the | ||
290 | 286 | Corresponding Source, you remain obligated to ensure that it is | ||
291 | 287 | available for as long as needed to satisfy these requirements. | ||
292 | 288 | |||
293 | 289 | e) Convey the object code using peer-to-peer transmission, provided | ||
294 | 290 | you inform other peers where the object code and Corresponding | ||
295 | 291 | Source of the work are being offered to the general public at no | ||
296 | 292 | charge under subsection 6d. | ||
297 | 293 | |||
298 | 294 | A separable portion of the object code, whose source code is excluded | ||
299 | 295 | from the Corresponding Source as a System Library, need not be | ||
300 | 296 | included in conveying the object code work. | ||
301 | 297 | |||
302 | 298 | A "User Product" is either (1) a "consumer product", which means any | ||
303 | 299 | tangible personal property which is normally used for personal, family, | ||
304 | 300 | or household purposes, or (2) anything designed or sold for incorporation | ||
305 | 301 | into a dwelling. In determining whether a product is a consumer product, | ||
306 | 302 | doubtful cases shall be resolved in favor of coverage. For a particular | ||
307 | 303 | product received by a particular user, "normally used" refers to a | ||
308 | 304 | typical or common use of that class of product, regardless of the status | ||
309 | 305 | of the particular user or of the way in which the particular user | ||
310 | 306 | actually uses, or expects or is expected to use, the product. A product | ||
311 | 307 | is a consumer product regardless of whether the product has substantial | ||
312 | 308 | commercial, industrial or non-consumer uses, unless such uses represent | ||
313 | 309 | the only significant mode of use of the product. | ||
314 | 310 | |||
315 | 311 | "Installation Information" for a User Product means any methods, | ||
316 | 312 | procedures, authorization keys, or other information required to install | ||
317 | 313 | and execute modified versions of a covered work in that User Product from | ||
318 | 314 | a modified version of its Corresponding Source. The information must | ||
319 | 315 | suffice to ensure that the continued functioning of the modified object | ||
320 | 316 | code is in no case prevented or interfered with solely because | ||
321 | 317 | modification has been made. | ||
322 | 318 | |||
323 | 319 | If you convey an object code work under this section in, or with, or | ||
324 | 320 | specifically for use in, a User Product, and the conveying occurs as | ||
325 | 321 | part of a transaction in which the right of possession and use of the | ||
326 | 322 | User Product is transferred to the recipient in perpetuity or for a | ||
327 | 323 | fixed term (regardless of how the transaction is characterized), the | ||
328 | 324 | Corresponding Source conveyed under this section must be accompanied | ||
329 | 325 | by the Installation Information. But this requirement does not apply | ||
330 | 326 | if neither you nor any third party retains the ability to install | ||
331 | 327 | modified object code on the User Product (for example, the work has | ||
332 | 328 | been installed in ROM). | ||
333 | 329 | |||
334 | 330 | The requirement to provide Installation Information does not include a | ||
335 | 331 | requirement to continue to provide support service, warranty, or updates | ||
336 | 332 | for a work that has been modified or installed by the recipient, or for | ||
337 | 333 | the User Product in which it has been modified or installed. Access to a | ||
338 | 334 | network may be denied when the modification itself materially and | ||
339 | 335 | adversely affects the operation of the network or violates the rules and | ||
340 | 336 | protocols for communication across the network. | ||
341 | 337 | |||
342 | 338 | Corresponding Source conveyed, and Installation Information provided, | ||
343 | 339 | in accord with this section must be in a format that is publicly | ||
344 | 340 | documented (and with an implementation available to the public in | ||
345 | 341 | source code form), and must require no special password or key for | ||
346 | 342 | unpacking, reading or copying. | ||
347 | 343 | |||
348 | 344 | 7. Additional Terms. | ||
349 | 345 | |||
350 | 346 | "Additional permissions" are terms that supplement the terms of this | ||
351 | 347 | License by making exceptions from one or more of its conditions. | ||
352 | 348 | Additional permissions that are applicable to the entire Program shall | ||
353 | 349 | be treated as though they were included in this License, to the extent | ||
354 | 350 | that they are valid under applicable law. If additional permissions | ||
355 | 351 | apply only to part of the Program, that part may be used separately | ||
356 | 352 | under those permissions, but the entire Program remains governed by | ||
357 | 353 | this License without regard to the additional permissions. | ||
358 | 354 | |||
359 | 355 | When you convey a copy of a covered work, you may at your option | ||
360 | 356 | remove any additional permissions from that copy, or from any part of | ||
361 | 357 | it. (Additional permissions may be written to require their own | ||
362 | 358 | removal in certain cases when you modify the work.) You may place | ||
363 | 359 | additional permissions on material, added by you to a covered work, | ||
364 | 360 | for which you have or can give appropriate copyright permission. | ||
365 | 361 | |||
366 | 362 | Notwithstanding any other provision of this License, for material you | ||
367 | 363 | add to a covered work, you may (if authorized by the copyright holders of | ||
368 | 364 | that material) supplement the terms of this License with terms: | ||
369 | 365 | |||
370 | 366 | a) Disclaiming warranty or limiting liability differently from the | ||
371 | 367 | terms of sections 15 and 16 of this License; or | ||
372 | 368 | |||
373 | 369 | b) Requiring preservation of specified reasonable legal notices or | ||
374 | 370 | author attributions in that material or in the Appropriate Legal | ||
375 | 371 | Notices displayed by works containing it; or | ||
376 | 372 | |||
377 | 373 | c) Prohibiting misrepresentation of the origin of that material, or | ||
378 | 374 | requiring that modified versions of such material be marked in | ||
379 | 375 | reasonable ways as different from the original version; or | ||
380 | 376 | |||
381 | 377 | d) Limiting the use for publicity purposes of names of licensors or | ||
382 | 378 | authors of the material; or | ||
383 | 379 | |||
384 | 380 | e) Declining to grant rights under trademark law for use of some | ||
385 | 381 | trade names, trademarks, or service marks; or | ||
386 | 382 | |||
387 | 383 | f) Requiring indemnification of licensors and authors of that | ||
388 | 384 | material by anyone who conveys the material (or modified versions of | ||
389 | 385 | it) with contractual assumptions of liability to the recipient, for | ||
390 | 386 | any liability that these contractual assumptions directly impose on | ||
391 | 387 | those licensors and authors. | ||
392 | 388 | |||
393 | 389 | All other non-permissive additional terms are considered "further | ||
394 | 390 | restrictions" within the meaning of section 10. If the Program as you | ||
395 | 391 | received it, or any part of it, contains a notice stating that it is | ||
396 | 392 | governed by this License along with a term that is a further | ||
397 | 393 | restriction, you may remove that term. If a license document contains | ||
398 | 394 | a further restriction but permits relicensing or conveying under this | ||
399 | 395 | License, you may add to a covered work material governed by the terms | ||
400 | 396 | of that license document, provided that the further restriction does | ||
401 | 397 | not survive such relicensing or conveying. | ||
402 | 398 | |||
403 | 399 | If you add terms to a covered work in accord with this section, you | ||
404 | 400 | must place, in the relevant source files, a statement of the | ||
405 | 401 | additional terms that apply to those files, or a notice indicating | ||
406 | 402 | where to find the applicable terms. | ||
407 | 403 | |||
408 | 404 | Additional terms, permissive or non-permissive, may be stated in the | ||
409 | 405 | form of a separately written license, or stated as exceptions; | ||
410 | 406 | the above requirements apply either way. | ||
411 | 407 | |||
412 | 408 | 8. Termination. | ||
413 | 409 | |||
414 | 410 | You may not propagate or modify a covered work except as expressly | ||
415 | 411 | provided under this License. Any attempt otherwise to propagate or | ||
416 | 412 | modify it is void, and will automatically terminate your rights under | ||
417 | 413 | this License (including any patent licenses granted under the third | ||
418 | 414 | paragraph of section 11). | ||
419 | 415 | |||
420 | 416 | However, if you cease all violation of this License, then your | ||
421 | 417 | license from a particular copyright holder is reinstated (a) | ||
422 | 418 | provisionally, unless and until the copyright holder explicitly and | ||
423 | 419 | finally terminates your license, and (b) permanently, if the copyright | ||
424 | 420 | holder fails to notify you of the violation by some reasonable means | ||
425 | 421 | prior to 60 days after the cessation. | ||
426 | 422 | |||
427 | 423 | Moreover, your license from a particular copyright holder is | ||
428 | 424 | reinstated permanently if the copyright holder notifies you of the | ||
429 | 425 | violation by some reasonable means, this is the first time you have | ||
430 | 426 | received notice of violation of this License (for any work) from that | ||
431 | 427 | copyright holder, and you cure the violation prior to 30 days after | ||
432 | 428 | your receipt of the notice. | ||
433 | 429 | |||
434 | 430 | Termination of your rights under this section does not terminate the | ||
435 | 431 | licenses of parties who have received copies or rights from you under | ||
436 | 432 | this License. If your rights have been terminated and not permanently | ||
437 | 433 | reinstated, you do not qualify to receive new licenses for the same | ||
438 | 434 | material under section 10. | ||
439 | 435 | |||
440 | 436 | 9. Acceptance Not Required for Having Copies. | ||
441 | 437 | |||
442 | 438 | You are not required to accept this License in order to receive or | ||
443 | 439 | run a copy of the Program. Ancillary propagation of a covered work | ||
444 | 440 | occurring solely as a consequence of using peer-to-peer transmission | ||
445 | 441 | to receive a copy likewise does not require acceptance. However, | ||
446 | 442 | nothing other than this License grants you permission to propagate or | ||
447 | 443 | modify any covered work. These actions infringe copyright if you do | ||
448 | 444 | not accept this License. Therefore, by modifying or propagating a | ||
449 | 445 | covered work, you indicate your acceptance of this License to do so. | ||
450 | 446 | |||
451 | 447 | 10. Automatic Licensing of Downstream Recipients. | ||
452 | 448 | |||
453 | 449 | Each time you convey a covered work, the recipient automatically | ||
454 | 450 | receives a license from the original licensors, to run, modify and | ||
455 | 451 | propagate that work, subject to this License. You are not responsible | ||
456 | 452 | for enforcing compliance by third parties with this License. | ||
457 | 453 | |||
458 | 454 | An "entity transaction" is a transaction transferring control of an | ||
459 | 455 | organization, or substantially all assets of one, or subdividing an | ||
460 | 456 | organization, or merging organizations. If propagation of a covered | ||
461 | 457 | work results from an entity transaction, each party to that | ||
462 | 458 | transaction who receives a copy of the work also receives whatever | ||
463 | 459 | licenses to the work the party's predecessor in interest had or could | ||
464 | 460 | give under the previous paragraph, plus a right to possession of the | ||
465 | 461 | Corresponding Source of the work from the predecessor in interest, if | ||
466 | 462 | the predecessor has it or can get it with reasonable efforts. | ||
467 | 463 | |||
468 | 464 | You may not impose any further restrictions on the exercise of the | ||
469 | 465 | rights granted or affirmed under this License. For example, you may | ||
470 | 466 | not impose a license fee, royalty, or other charge for exercise of | ||
471 | 467 | rights granted under this License, and you may not initiate litigation | ||
472 | 468 | (including a cross-claim or counterclaim in a lawsuit) alleging that | ||
473 | 469 | any patent claim is infringed by making, using, selling, offering for | ||
474 | 470 | sale, or importing the Program or any portion of it. | ||
475 | 471 | |||
476 | 472 | 11. Patents. | ||
477 | 473 | |||
478 | 474 | A "contributor" is a copyright holder who authorizes use under this | ||
479 | 475 | License of the Program or a work on which the Program is based. The | ||
480 | 476 | work thus licensed is called the contributor's "contributor version". | ||
481 | 477 | |||
482 | 478 | A contributor's "essential patent claims" are all patent claims | ||
483 | 479 | owned or controlled by the contributor, whether already acquired or | ||
484 | 480 | hereafter acquired, that would be infringed by some manner, permitted | ||
485 | 481 | by this License, of making, using, or selling its contributor version, | ||
486 | 482 | but do not include claims that would be infringed only as a | ||
487 | 483 | consequence of further modification of the contributor version. For | ||
488 | 484 | purposes of this definition, "control" includes the right to grant | ||
489 | 485 | patent sublicenses in a manner consistent with the requirements of | ||
490 | 486 | this License. | ||
491 | 487 | |||
492 | 488 | Each contributor grants you a non-exclusive, worldwide, royalty-free | ||
493 | 489 | patent license under the contributor's essential patent claims, to | ||
494 | 490 | make, use, sell, offer for sale, import and otherwise run, modify and | ||
495 | 491 | propagate the contents of its contributor version. | ||
496 | 492 | |||
497 | 493 | In the following three paragraphs, a "patent license" is any express | ||
498 | 494 | agreement or commitment, however denominated, not to enforce a patent | ||
499 | 495 | (such as an express permission to practice a patent or covenant not to | ||
500 | 496 | sue for patent infringement). To "grant" such a patent license to a | ||
501 | 497 | party means to make such an agreement or commitment not to enforce a | ||
502 | 498 | patent against the party. | ||
503 | 499 | |||
504 | 500 | If you convey a covered work, knowingly relying on a patent license, | ||
505 | 501 | and the Corresponding Source of the work is not available for anyone | ||
506 | 502 | to copy, free of charge and under the terms of this License, through a | ||
507 | 503 | publicly available network server or other readily accessible means, | ||
508 | 504 | then you must either (1) cause the Corresponding Source to be so | ||
509 | 505 | available, or (2) arrange to deprive yourself of the benefit of the | ||
510 | 506 | patent license for this particular work, or (3) arrange, in a manner | ||
511 | 507 | consistent with the requirements of this License, to extend the patent | ||
512 | 508 | license to downstream recipients. "Knowingly relying" means you have | ||
513 | 509 | actual knowledge that, but for the patent license, your conveying the | ||
514 | 510 | covered work in a country, or your recipient's use of the covered work | ||
515 | 511 | in a country, would infringe one or more identifiable patents in that | ||
516 | 512 | country that you have reason to believe are valid. | ||
517 | 513 | |||
518 | 514 | If, pursuant to or in connection with a single transaction or | ||
519 | 515 | arrangement, you convey, or propagate by procuring conveyance of, a | ||
520 | 516 | covered work, and grant a patent license to some of the parties | ||
521 | 517 | receiving the covered work authorizing them to use, propagate, modify | ||
522 | 518 | or convey a specific copy of the covered work, then the patent license | ||
523 | 519 | you grant is automatically extended to all recipients of the covered | ||
524 | 520 | work and works based on it. | ||
525 | 521 | |||
526 | 522 | A patent license is "discriminatory" if it does not include within | ||
527 | 523 | the scope of its coverage, prohibits the exercise of, or is | ||
528 | 524 | conditioned on the non-exercise of one or more of the rights that are | ||
529 | 525 | specifically granted under this License. You may not convey a covered | ||
530 | 526 | work if you are a party to an arrangement with a third party that is | ||
531 | 527 | in the business of distributing software, under which you make payment | ||
532 | 528 | to the third party based on the extent of your activity of conveying | ||
533 | 529 | the work, and under which the third party grants, to any of the | ||
534 | 530 | parties who would receive the covered work from you, a discriminatory | ||
535 | 531 | patent license (a) in connection with copies of the covered work | ||
536 | 532 | conveyed by you (or copies made from those copies), or (b) primarily | ||
537 | 533 | for and in connection with specific products or compilations that | ||
538 | 534 | contain the covered work, unless you entered into that arrangement, | ||
539 | 535 | or that patent license was granted, prior to 28 March 2007. | ||
540 | 536 | |||
541 | 537 | Nothing in this License shall be construed as excluding or limiting | ||
542 | 538 | any implied license or other defenses to infringement that may | ||
543 | 539 | otherwise be available to you under applicable patent law. | ||
544 | 540 | |||
545 | 541 | 12. No Surrender of Others' Freedom. | ||
546 | 542 | |||
547 | 543 | If conditions are imposed on you (whether by court order, agreement or | ||
548 | 544 | otherwise) that contradict the conditions of this License, they do not | ||
549 | 545 | excuse you from the conditions of this License. If you cannot convey a | ||
550 | 546 | covered work so as to satisfy simultaneously your obligations under this | ||
551 | 547 | License and any other pertinent obligations, then as a consequence you may | ||
552 | 548 | not convey it at all. For example, if you agree to terms that obligate you | ||
553 | 549 | to collect a royalty for further conveying from those to whom you convey | ||
554 | 550 | the Program, the only way you could satisfy both those terms and this | ||
555 | 551 | License would be to refrain entirely from conveying the Program. | ||
556 | 552 | |||
557 | 553 | 13. Use with the GNU Affero General Public License. | ||
558 | 554 | |||
559 | 555 | Notwithstanding any other provision of this License, you have | ||
560 | 556 | permission to link or combine any covered work with a work licensed | ||
561 | 557 | under version 3 of the GNU Affero General Public License into a single | ||
562 | 558 | combined work, and to convey the resulting work. The terms of this | ||
563 | 559 | License will continue to apply to the part which is the covered work, | ||
564 | 560 | but the special requirements of the GNU Affero General Public License, | ||
565 | 561 | section 13, concerning interaction through a network will apply to the | ||
566 | 562 | combination as such. | ||
567 | 563 | |||
568 | 564 | 14. Revised Versions of this License. | ||
569 | 565 | |||
570 | 566 | The Free Software Foundation may publish revised and/or new versions of | ||
571 | 567 | the GNU General Public License from time to time. Such new versions will | ||
572 | 568 | be similar in spirit to the present version, but may differ in detail to | ||
573 | 569 | address new problems or concerns. | ||
574 | 570 | |||
575 | 571 | Each version is given a distinguishing version number. If the | ||
576 | 572 | Program specifies that a certain numbered version of the GNU General | ||
577 | 573 | Public License "or any later version" applies to it, you have the | ||
578 | 574 | option of following the terms and conditions either of that numbered | ||
579 | 575 | version or of any later version published by the Free Software | ||
580 | 576 | Foundation. If the Program does not specify a version number of the | ||
581 | 577 | GNU General Public License, you may choose any version ever published | ||
582 | 578 | by the Free Software Foundation. | ||
583 | 579 | |||
584 | 580 | If the Program specifies that a proxy can decide which future | ||
585 | 581 | versions of the GNU General Public License can be used, that proxy's | ||
586 | 582 | public statement of acceptance of a version permanently authorizes you | ||
587 | 583 | to choose that version for the Program. | ||
588 | 584 | |||
589 | 585 | Later license versions may give you additional or different | ||
590 | 586 | permissions. However, no additional obligations are imposed on any | ||
591 | 587 | author or copyright holder as a result of your choosing to follow a | ||
592 | 588 | later version. | ||
593 | 589 | |||
594 | 590 | 15. Disclaimer of Warranty. | ||
595 | 591 | |||
596 | 592 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||
597 | 593 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||
598 | 594 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||
599 | 595 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||
600 | 596 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||
601 | 597 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||
602 | 598 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||
603 | 599 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||
604 | 600 | |||
605 | 601 | 16. Limitation of Liability. | ||
606 | 602 | |||
607 | 603 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||
608 | 604 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||
609 | 605 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||
610 | 606 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||
611 | 607 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||
612 | 608 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||
613 | 609 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||
614 | 610 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||
615 | 611 | SUCH DAMAGES. | ||
616 | 612 | |||
617 | 613 | 17. Interpretation of Sections 15 and 16. | ||
618 | 614 | |||
619 | 615 | If the disclaimer of warranty and limitation of liability provided | ||
620 | 616 | above cannot be given local legal effect according to their terms, | ||
621 | 617 | reviewing courts shall apply local law that most closely approximates | ||
622 | 618 | an absolute waiver of all civil liability in connection with the | ||
623 | 619 | Program, unless a warranty or assumption of liability accompanies a | ||
624 | 620 | copy of the Program in return for a fee. | ||
625 | 621 | |||
626 | 622 | END OF TERMS AND CONDITIONS | ||
627 | 623 | |||
628 | 624 | How to Apply These Terms to Your New Programs | ||
629 | 625 | |||
630 | 626 | If you develop a new program, and you want it to be of the greatest | ||
631 | 627 | possible use to the public, the best way to achieve this is to make it | ||
632 | 628 | free software which everyone can redistribute and change under these terms. | ||
633 | 629 | |||
634 | 630 | To do so, attach the following notices to the program. It is safest | ||
635 | 631 | to attach them to the start of each source file to most effectively | ||
636 | 632 | state the exclusion of warranty; and each file should have at least | ||
637 | 633 | the "copyright" line and a pointer to where the full notice is found. | ||
638 | 634 | |||
639 | 635 | <one line to give the program's name and a brief idea of what it does.> | ||
640 | 636 | Copyright (C) <year> <name of author> | ||
641 | 637 | |||
642 | 638 | This program is free software: you can redistribute it and/or modify | ||
643 | 639 | it under the terms of the GNU General Public License as published by | ||
644 | 640 | the Free Software Foundation, either version 3 of the License, or | ||
645 | 641 | (at your option) any later version. | ||
646 | 642 | |||
647 | 643 | This program is distributed in the hope that it will be useful, | ||
648 | 644 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
649 | 645 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
650 | 646 | GNU General Public License for more details. | ||
651 | 647 | |||
652 | 648 | You should have received a copy of the GNU General Public License | ||
653 | 649 | along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
654 | 650 | |||
655 | 651 | Also add information on how to contact you by electronic and paper mail. | ||
656 | 652 | |||
657 | 653 | If the program does terminal interaction, make it output a short | ||
658 | 654 | notice like this when it starts in an interactive mode: | ||
659 | 655 | |||
660 | 656 | <program> Copyright (C) <year> <name of author> | ||
661 | 657 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||
662 | 658 | This is free software, and you are welcome to redistribute it | ||
663 | 659 | under certain conditions; type `show c' for details. | ||
664 | 660 | |||
665 | 661 | The hypothetical commands `show w' and `show c' should show the appropriate | ||
666 | 662 | parts of the General Public License. Of course, your program's commands | ||
667 | 663 | might be different; for a GUI interface, you would use an "about box". | ||
668 | 664 | |||
669 | 665 | You should also get your employer (if you work as a programmer) or school, | ||
670 | 666 | if any, to sign a "copyright disclaimer" for the program, if necessary. | ||
671 | 667 | For more information on this, and how to apply and follow the GNU GPL, see | ||
672 | 668 | <http://www.gnu.org/licenses/>. | ||
673 | 669 | |||
674 | 670 | The GNU General Public License does not permit incorporating your program | ||
675 | 671 | into proprietary programs. If your program is a subroutine library, you | ||
676 | 672 | may consider it more useful to permit linking proprietary applications with | ||
677 | 673 | the library. If this is what you want to do, use the GNU Lesser General | ||
678 | 674 | Public License instead of this License. But first, please read | ||
679 | 675 | <http://www.gnu.org/philosophy/why-not-lgpl.html>. | ||
680 | 676 | |||
681 | 0 | 677 | ||
682 | === added file 'COPYING.LESSER' | |||
683 | --- COPYING.LESSER 1970-01-01 00:00:00 +0000 | |||
684 | +++ COPYING.LESSER 2009-07-13 15:53:45 +0000 | |||
685 | @@ -0,0 +1,165 @@ | |||
686 | 1 | GNU LESSER GENERAL PUBLIC LICENSE | ||
687 | 2 | Version 3, 29 June 2007 | ||
688 | 3 | |||
689 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> | ||
690 | 5 | Everyone is permitted to copy and distribute verbatim copies | ||
691 | 6 | of this license document, but changing it is not allowed. | ||
692 | 7 | |||
693 | 8 | |||
694 | 9 | This version of the GNU Lesser General Public License incorporates | ||
695 | 10 | the terms and conditions of version 3 of the GNU General Public | ||
696 | 11 | License, supplemented by the additional permissions listed below. | ||
697 | 12 | |||
698 | 13 | 0. Additional Definitions. | ||
699 | 14 | |||
700 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser | ||
701 | 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU | ||
702 | 17 | General Public License. | ||
703 | 18 | |||
704 | 19 | "The Library" refers to a covered work governed by this License, | ||
705 | 20 | other than an Application or a Combined Work as defined below. | ||
706 | 21 | |||
707 | 22 | An "Application" is any work that makes use of an interface provided | ||
708 | 23 | by the Library, but which is not otherwise based on the Library. | ||
709 | 24 | Defining a subclass of a class defined by the Library is deemed a mode | ||
710 | 25 | of using an interface provided by the Library. | ||
711 | 26 | |||
712 | 27 | A "Combined Work" is a work produced by combining or linking an | ||
713 | 28 | Application with the Library. The particular version of the Library | ||
714 | 29 | with which the Combined Work was made is also called the "Linked | ||
715 | 30 | Version". | ||
716 | 31 | |||
717 | 32 | The "Minimal Corresponding Source" for a Combined Work means the | ||
718 | 33 | Corresponding Source for the Combined Work, excluding any source code | ||
719 | 34 | for portions of the Combined Work that, considered in isolation, are | ||
720 | 35 | based on the Application, and not on the Linked Version. | ||
721 | 36 | |||
722 | 37 | The "Corresponding Application Code" for a Combined Work means the | ||
723 | 38 | object code and/or source code for the Application, including any data | ||
724 | 39 | and utility programs needed for reproducing the Combined Work from the | ||
725 | 40 | Application, but excluding the System Libraries of the Combined Work. | ||
726 | 41 | |||
727 | 42 | 1. Exception to Section 3 of the GNU GPL. | ||
728 | 43 | |||
729 | 44 | You may convey a covered work under sections 3 and 4 of this License | ||
730 | 45 | without being bound by section 3 of the GNU GPL. | ||
731 | 46 | |||
732 | 47 | 2. Conveying Modified Versions. | ||
733 | 48 | |||
734 | 49 | If you modify a copy of the Library, and, in your modifications, a | ||
735 | 50 | facility refers to a function or data to be supplied by an Application | ||
736 | 51 | that uses the facility (other than as an argument passed when the | ||
737 | 52 | facility is invoked), then you may convey a copy of the modified | ||
738 | 53 | version: | ||
739 | 54 | |||
740 | 55 | a) under this License, provided that you make a good faith effort to | ||
741 | 56 | ensure that, in the event an Application does not supply the | ||
742 | 57 | function or data, the facility still operates, and performs | ||
743 | 58 | whatever part of its purpose remains meaningful, or | ||
744 | 59 | |||
745 | 60 | b) under the GNU GPL, with none of the additional permissions of | ||
746 | 61 | this License applicable to that copy. | ||
747 | 62 | |||
748 | 63 | 3. Object Code Incorporating Material from Library Header Files. | ||
749 | 64 | |||
750 | 65 | The object code form of an Application may incorporate material from | ||
751 | 66 | a header file that is part of the Library. You may convey such object | ||
752 | 67 | code under terms of your choice, provided that, if the incorporated | ||
753 | 68 | material is not limited to numerical parameters, data structure | ||
754 | 69 | layouts and accessors, or small macros, inline functions and templates | ||
755 | 70 | (ten or fewer lines in length), you do both of the following: | ||
756 | 71 | |||
757 | 72 | a) Give prominent notice with each copy of the object code that the | ||
758 | 73 | Library is used in it and that the Library and its use are | ||
759 | 74 | covered by this License. | ||
760 | 75 | |||
761 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license | ||
762 | 77 | document. | ||
763 | 78 | |||
764 | 79 | 4. Combined Works. | ||
765 | 80 | |||
766 | 81 | You may convey a Combined Work under terms of your choice that, | ||
767 | 82 | taken together, effectively do not restrict modification of the | ||
768 | 83 | portions of the Library contained in the Combined Work and reverse | ||
769 | 84 | engineering for debugging such modifications, if you also do each of | ||
770 | 85 | the following: | ||
771 | 86 | |||
772 | 87 | a) Give prominent notice with each copy of the Combined Work that | ||
773 | 88 | the Library is used in it and that the Library and its use are | ||
774 | 89 | covered by this License. | ||
775 | 90 | |||
776 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license | ||
777 | 92 | document. | ||
778 | 93 | |||
779 | 94 | c) For a Combined Work that displays copyright notices during | ||
780 | 95 | execution, include the copyright notice for the Library among | ||
781 | 96 | these notices, as well as a reference directing the user to the | ||
782 | 97 | copies of the GNU GPL and this license document. | ||
783 | 98 | |||
784 | 99 | d) Do one of the following: | ||
785 | 100 | |||
786 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this | ||
787 | 102 | License, and the Corresponding Application Code in a form | ||
788 | 103 | suitable for, and under terms that permit, the user to | ||
789 | 104 | recombine or relink the Application with a modified version of | ||
790 | 105 | the Linked Version to produce a modified Combined Work, in the | ||
791 | 106 | manner specified by section 6 of the GNU GPL for conveying | ||
792 | 107 | Corresponding Source. | ||
793 | 108 | |||
794 | 109 | 1) Use a suitable shared library mechanism for linking with the | ||
795 | 110 | Library. A suitable mechanism is one that (a) uses at run time | ||
796 | 111 | a copy of the Library already present on the user's computer | ||
797 | 112 | system, and (b) will operate properly with a modified version | ||
798 | 113 | of the Library that is interface-compatible with the Linked | ||
799 | 114 | Version. | ||
800 | 115 | |||
801 | 116 | e) Provide Installation Information, but only if you would otherwise | ||
802 | 117 | be required to provide such information under section 6 of the | ||
803 | 118 | GNU GPL, and only to the extent that such information is | ||
804 | 119 | necessary to install and execute a modified version of the | ||
805 | 120 | Combined Work produced by recombining or relinking the | ||
806 | 121 | Application with a modified version of the Linked Version. (If | ||
807 | 122 | you use option 4d0, the Installation Information must accompany | ||
808 | 123 | the Minimal Corresponding Source and Corresponding Application | ||
809 | 124 | Code. If you use option 4d1, you must provide the Installation | ||
810 | 125 | Information in the manner specified by section 6 of the GNU GPL | ||
811 | 126 | for conveying Corresponding Source.) | ||
812 | 127 | |||
813 | 128 | 5. Combined Libraries. | ||
814 | 129 | |||
815 | 130 | You may place library facilities that are a work based on the | ||
816 | 131 | Library side by side in a single library together with other library | ||
817 | 132 | facilities that are not Applications and are not covered by this | ||
818 | 133 | License, and convey such a combined library under terms of your | ||
819 | 134 | choice, if you do both of the following: | ||
820 | 135 | |||
821 | 136 | a) Accompany the combined library with a copy of the same work based | ||
822 | 137 | on the Library, uncombined with any other library facilities, | ||
823 | 138 | conveyed under the terms of this License. | ||
824 | 139 | |||
825 | 140 | b) Give prominent notice with the combined library that part of it | ||
826 | 141 | is a work based on the Library, and explaining where to find the | ||
827 | 142 | accompanying uncombined form of the same work. | ||
828 | 143 | |||
829 | 144 | 6. Revised Versions of the GNU Lesser General Public License. | ||
830 | 145 | |||
831 | 146 | The Free Software Foundation may publish revised and/or new versions | ||
832 | 147 | of the GNU Lesser General Public License from time to time. Such new | ||
833 | 148 | versions will be similar in spirit to the present version, but may | ||
834 | 149 | differ in detail to address new problems or concerns. | ||
835 | 150 | |||
836 | 151 | Each version is given a distinguishing version number. If the | ||
837 | 152 | Library as you received it specifies that a certain numbered version | ||
838 | 153 | of the GNU Lesser General Public License "or any later version" | ||
839 | 154 | applies to it, you have the option of following the terms and | ||
840 | 155 | conditions either of that published version or of any later version | ||
841 | 156 | published by the Free Software Foundation. If the Library as you | ||
842 | 157 | received it does not specify a version number of the GNU Lesser | ||
843 | 158 | General Public License, you may choose any version of the GNU Lesser | ||
844 | 159 | General Public License ever published by the Free Software Foundation. | ||
845 | 160 | |||
846 | 161 | If the Library as you received it specifies that a proxy can decide | ||
847 | 162 | whether future versions of the GNU Lesser General Public License shall | ||
848 | 163 | apply, that proxy's public statement of acceptance of any version is | ||
849 | 164 | permanent authorization for you to choose that version for the | ||
850 | 165 | Library. | ||
851 | 0 | 166 | ||
852 | === added file 'MANIFEST.in' | |||
853 | --- MANIFEST.in 1970-01-01 00:00:00 +0000 | |||
854 | +++ MANIFEST.in 2009-09-11 19:08:53 +0000 | |||
855 | @@ -0,0 +1,14 @@ | |||
856 | 1 | include COPYING COPYING.LESSER README | ||
857 | 2 | recursive-include data *.tmpl | ||
858 | 3 | include desktopcouch-pair.desktop.in | ||
859 | 4 | include po/POTFILES.in | ||
860 | 5 | include start-desktop-couchdb.sh | ||
861 | 6 | include stop-desktop-couchdb.sh | ||
862 | 7 | include desktopcouch/records/doc/records.txt | ||
863 | 8 | include bin/* | ||
864 | 9 | include docs/man/* | ||
865 | 10 | include MANIFEST.in MANIFEST | ||
866 | 11 | include org.desktopcouch.CouchDB.service | ||
867 | 12 | include config/desktop-couch/compulsory-auth.ini | ||
868 | 13 | recursive-include contrib *.py | ||
869 | 14 | |||
870 | 0 | 15 | ||
871 | === added file 'README' | |||
872 | --- README 1970-01-01 00:00:00 +0000 | |||
873 | +++ README 2009-08-07 12:43:11 +0000 | |||
874 | @@ -0,0 +1,35 @@ | |||
875 | 1 | This is Desktop Couch, an infrastructure to place a CouchDB on every desktop | ||
876 | 2 | and provide APIs and management tools for applications to store data within | ||
877 | 3 | it and share that data between computers and to the cloud. | ||
878 | 4 | |||
879 | 5 | = Technical notes = | ||
880 | 6 | |||
881 | 7 | == Creating databases, design documents, and views == | ||
882 | 8 | |||
883 | 9 | Desktop Couch will automatically create databases and design documents for you | ||
884 | 10 | on startup by inspecting the filesystem, meaning that you do not need to check | ||
885 | 11 | in your application whether your views exist. It is of course possible to create | ||
886 | 12 | views directly through the Records API, but having them created via the | ||
887 | 13 | filesystem allows managing of the view definitions more easily (they are | ||
888 | 14 | in separately editable files, which helps with version control) and packaging | ||
889 | 15 | (the files can be separately stored in a distribution package and installed | ||
890 | 16 | into the system-level XDG_DATA_DIR rather than the user-level folder). | ||
891 | 17 | |||
892 | 18 | === To create a database === | ||
893 | 19 | |||
894 | 20 | Create a file $XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/database.cfg | ||
895 | 21 | |||
896 | 22 | This file can currently be empty (in future it may contain database setup and | ||
897 | 23 | configuration information). | ||
898 | 24 | |||
899 | 25 | === To create a design document === | ||
900 | 26 | |||
901 | 27 | Create a file | ||
902 | 28 | $XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/_design/DESIGN_DOC_NAME/views/VIEW_NAME/map.js | ||
903 | 29 | containing the map function from your view. | ||
904 | 30 | If you also require a reduce function for your view, create a file | ||
905 | 31 | $XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/_design/DESIGN_DOC_NAME/views/VIEW_NAME/reduce.js. | ||
906 | 32 | |||
907 | 33 | This is compatible with the filesystem view structure from CouchApp and | ||
908 | 34 | CouchDBKit. | ||
909 | 35 | |||
910 | 0 | 36 | ||
911 | === added directory 'bin' | |||
912 | === added file 'bin/desktopcouch-pair' | |||
913 | --- bin/desktopcouch-pair 1970-01-01 00:00:00 +0000 | |||
914 | +++ bin/desktopcouch-pair 2009-09-14 16:06:52 +0000 | |||
915 | @@ -0,0 +1,905 @@ | |||
916 | 1 | #!/usr/bin/python | ||
917 | 2 | # Copyright 2009 Canonical Ltd. | ||
918 | 3 | # | ||
919 | 4 | # This file is part of desktopcouch. | ||
920 | 5 | # | ||
921 | 6 | # desktopcouch is free software: you can redistribute it and/or modify | ||
922 | 7 | # it under the terms of the GNU Lesser General Public License version 3 | ||
923 | 8 | # as published by the Free Software Foundation. | ||
924 | 9 | # | ||
925 | 10 | # desktopcouch is distributed in the hope that it will be useful, | ||
926 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
927 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
928 | 13 | # GNU Lesser General Public License for more details. | ||
929 | 14 | # | ||
930 | 15 | # You should have received a copy of the GNU Lesser General Public License | ||
931 | 16 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
932 | 17 | # vim: filetype=python expandtab smarttab | ||
933 | 18 | |||
934 | 19 | __doc__ = """\ | ||
935 | 20 | Manage paired machines. | ||
936 | 21 | |||
937 | 22 | A tool to set two local machines to replicate their couchdb instances to each | ||
938 | 23 | other, or to set this machine to replicate to-and-from Ubuntu One (and perhaps | ||
939 | 24 | other cloud services). | ||
940 | 25 | |||
941 | 26 | Local-Pairing Authentication | ||
942 | 27 | ---------------------------- | ||
943 | 28 | |||
944 | 29 | One machine, Alice, sets herself to listen for invitations to pair with another | ||
945 | 30 | machine. In doing so, Alice is discoverable via Zeroconf on local network. | ||
946 | 31 | |||
947 | 32 | Another machine, Bob, sees the advertisement and generates a secret message and | ||
948 | 33 | a public seed. Bob then computes the SHA512 digest of the secret, and sends an | ||
949 | 34 | invitation to Alice, in the form of the hex digest concatenated with the public | ||
950 | 35 | seed. Bob displays the secret to the user, who walks the secret over to Alice. | ||
951 | 36 | |||
952 | 37 | Alice then computes the SHA512 digest of Bob's secret and compares it to be | ||
953 | 38 | sure that the other machine is indeed the user's. Alice then concatenates | ||
954 | 39 | Bob's secret and the public seed, and sends the resulting hex digest back to | ||
955 | 40 | Bob to prove that she received the secret from the user. Alice sets herself to | ||
956 | 41 | replicate to Bob. | ||
957 | 42 | |||
958 | 43 | Bob computes the secret+publicseed digest and compares it with the received hex | ||
959 | 44 | digest from Alice. When it matches, he sets himself to replicate to Alice. | ||
960 | 45 | """ | ||
961 | 46 | |||
962 | 47 | |||
963 | 48 | |||
964 | 49 | import logging | ||
965 | 50 | import getpass | ||
966 | 51 | import gettext | ||
967 | 52 | # gettext implements "_" function. pylint: disable-msg=E0602 | ||
968 | 53 | import random | ||
969 | 54 | import cgi | ||
970 | 55 | |||
971 | 56 | from twisted.internet import gtk2reactor | ||
972 | 57 | gtk2reactor.install() | ||
973 | 58 | from twisted.internet.reactor import run as run_program | ||
974 | 59 | from twisted.internet.reactor import stop as stop_program | ||
975 | 60 | |||
976 | 61 | import pygtk | ||
977 | 62 | pygtk.require('2.0') | ||
978 | 63 | import gtk | ||
979 | 64 | import gobject | ||
980 | 65 | import pango | ||
981 | 66 | |||
982 | 67 | from desktopcouch.pair.couchdb_pairing import couchdb_io | ||
983 | 68 | from desktopcouch.pair.couchdb_pairing import network_io | ||
984 | 69 | from desktopcouch.pair.couchdb_pairing import dbus_io | ||
985 | 70 | from desktopcouch.pair.couchdb_pairing import couchdb_io | ||
986 | 71 | from desktopcouch.pair import pairing_record_type | ||
987 | 72 | from desktopcouch import local_files | ||
988 | 73 | |||
989 | 74 | from desktopcouch.records.record import Record | ||
990 | 75 | |||
991 | 76 | DISCOVERY_TOOL_VERSION = "1" | ||
992 | 77 | |||
993 | 78 | def generate_secret(length=7): | ||
994 | 79 | """Create a secret that is easy to write and read. We hate ambiguity and | ||
995 | 80 | errors.""" | ||
996 | 81 | |||
997 | 82 | pool = "abcdefghijklmnopqrstuvwxyz23456789#$%&*+=-" | ||
998 | 83 | return unmap_easily_mistaken( | ||
999 | 84 | "".join(random.choice(pool) for n in range(length))) | ||
1000 | 85 | |||
1001 | 86 | def unmap_easily_mistaken(user_input): | ||
1002 | 87 | """Returns ASCII-encoded text with visually-ambiguous characters crushed | ||
1003 | 88 | down to some common atom.""" | ||
1004 | 89 | |||
1005 | 90 | import string | ||
1006 | 91 | easily_mistaken = string.maketrans("!10@", "lloo") | ||
1007 | 92 | return string.translate(user_input.lower().encode("ascii"), easily_mistaken) | ||
1008 | 93 | |||
1009 | 94 | def get_host_info(): | ||
1010 | 95 | """Create some text that hopefully identifies this host out of many.""" | ||
1011 | 96 | |||
1012 | 97 | import platform | ||
1013 | 98 | |||
1014 | 99 | try: | ||
1015 | 100 | uptime_seconds = int(float(file("/proc/uptime").read().split()[0])) | ||
1016 | 101 | days, uptime_seconds = divmod(uptime_seconds, 60*60*24) | ||
1017 | 102 | hours, uptime_seconds = divmod(uptime_seconds, 60*60) | ||
1018 | 103 | minutes, seconds = divmod(uptime_seconds, 60) | ||
1019 | 104 | |||
1020 | 105 | # Is ISO8601 too nerdy? ... | ||
1021 | 106 | #uptime_descr = ", up PT%(days)dD%(hours)dH%(minutes)dM" % locals() | ||
1022 | 107 | uptime_descr = ", up %(days)dd %(hours)dh%(minutes)dm" % locals() | ||
1023 | 108 | except OSError: | ||
1024 | 109 | uptime_descr = "" | ||
1025 | 110 | |||
1026 | 111 | try: | ||
1027 | 112 | return " ".join(platform.dist()) + uptime_descr | ||
1028 | 113 | except AttributeError: | ||
1029 | 114 | return platform.platform() + uptime_descr | ||
1030 | 115 | |||
1031 | 116 | |||
1032 | 117 | class Inviting: | ||
1033 | 118 | """We're part of "Bob" in the module's story. | ||
1034 | 119 | |||
1035 | 120 | We see listeners on the network and send invitations to pair with us. | ||
1036 | 121 | We generate a secret message and a public seed. We get the SHA512 hex | ||
1037 | 122 | digest of the secret message, append the public seed and send it to | ||
1038 | 123 | Alice. We also display the cleartext of the secret message to the | ||
1039 | 124 | screen, so that the user can take it to the machine he thinks is Alice. | ||
1040 | 125 | |||
1041 | 126 | Eventually, we receive a message back from Alice. We compute the | ||
1042 | 127 | secret message we started with concatenated with the public seed, and | ||
1043 | 128 | if that matches Alice's message, then Alice must know the secret we | ||
1044 | 129 | displayed to the user. We then set outselves to replicate to Alice.""" | ||
1045 | 130 | |||
1046 | 131 | def delete_event(self, widget, event, data=None): | ||
1047 | 132 | """User requested window be closed. False to propogate event.""" | ||
1048 | 133 | return False | ||
1049 | 134 | |||
1050 | 135 | def destroy(self, widget, data=None): | ||
1051 | 136 | """The window is destroyed.""" | ||
1052 | 137 | self.inviter.close() | ||
1053 | 138 | |||
1054 | 139 | def auth_completed(self, remote_host, remote_id, remote_oauth): | ||
1055 | 140 | """The auth stage is finished. Now pair with the remote host.""" | ||
1056 | 141 | pair_with_host(remote_host, remote_id, remote_oauth) | ||
1057 | 142 | self.window.destroy() | ||
1058 | 143 | |||
1059 | 144 | def on_close(self): | ||
1060 | 145 | """When a socket is closed, we should stop inviting. (?)""" | ||
1061 | 146 | self.window.destroy() | ||
1062 | 147 | |||
1063 | 148 | def __init__(self, service, hostname, port): | ||
1064 | 149 | self.logging = logging.getLogger(self.__class__.__name__) | ||
1065 | 150 | |||
1066 | 151 | self.hostname = hostname | ||
1067 | 152 | self.port = port | ||
1068 | 153 | |||
1069 | 154 | self.window = gtk.Window() | ||
1070 | 155 | self.window.set_border_width(6) | ||
1071 | 156 | self.window.connect("delete_event", self.delete_event) | ||
1072 | 157 | self.window.connect("destroy", self.destroy) | ||
1073 | 158 | self.window.set_destroy_with_parent(True) | ||
1074 | 159 | self.window.set_title( | ||
1075 | 160 | _("Inviting %s to pair for CouchDB Pairing") % hostname) | ||
1076 | 161 | |||
1077 | 162 | secret_message = generate_secret() | ||
1078 | 163 | self.secret_message = secret_message | ||
1079 | 164 | self.public_seed = generate_secret() | ||
1080 | 165 | |||
1081 | 166 | self.inviter = network_io.start_send_invitation(hostname, port, | ||
1082 | 167 | self.auth_completed, self.secret_message, self.public_seed, | ||
1083 | 168 | self.on_close, couchdb_io.get_my_host_unique_id(create=True)[0], | ||
1084 | 169 | local_files.get_oauth_tokens()) | ||
1085 | 170 | |||
1086 | 171 | top_vbox = gtk.VBox() | ||
1087 | 172 | self.window.add(top_vbox) | ||
1088 | 173 | |||
1089 | 174 | text = gtk.Label() | ||
1090 | 175 | text.set_markup(_("""We're inviting %s to pair with\n""" + | ||
1091 | 176 | """us, and to prove veracity of the invitation we\n""" + | ||
1092 | 177 | """sent, it is waiting for you to tell it this secret:\n""" + | ||
1093 | 178 | """<span font-size="xx-large" color="blue" weight="bold">""" + | ||
1094 | 179 | """<tt>%s</tt></span> .""") % | ||
1095 | 180 | (cgi.escape(service), cgi.escape(self.secret_message))) | ||
1096 | 181 | text.set_justify(gtk.JUSTIFY_CENTER) | ||
1097 | 182 | top_vbox.pack_start(text, False, False, 10) | ||
1098 | 183 | text.show() | ||
1099 | 184 | |||
1100 | 185 | cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL) | ||
1101 | 186 | cancel_button.set_border_width(3) | ||
1102 | 187 | cancel_button.connect("clicked", lambda widget: self.window.destroy()) | ||
1103 | 188 | top_vbox.pack_end(cancel_button, False, False, 10) | ||
1104 | 189 | cancel_button.show() | ||
1105 | 190 | |||
1106 | 191 | self.window.show_all() | ||
1107 | 192 | |||
1108 | 193 | |||
1109 | 194 | class AcceptInvitation: | ||
1110 | 195 | """We're part of 'Alice' in this module's story. | ||
1111 | 196 | |||
1112 | 197 | We've received an invitation. We now send the other end a secret key. The | ||
1113 | 198 | secret should make its way back to us via meatspace. We open a dialog | ||
1114 | 199 | asking for that secret here, which we validate. | ||
1115 | 200 | |||
1116 | 201 | When we validate it, we add this end as replicating to the other end. The | ||
1117 | 202 | other end should also set itself to replicate to us.""" | ||
1118 | 203 | |||
1119 | 204 | def delete_event(self, widget, event, data=None): | ||
1120 | 205 | """User requested window be closed. False to propogate event.""" | ||
1121 | 206 | return False | ||
1122 | 207 | |||
1123 | 208 | def destroy(self, widget, data=None): | ||
1124 | 209 | """Window is destroyed.""" | ||
1125 | 210 | pass | ||
1126 | 211 | |||
1127 | 212 | def on_close(self): | ||
1128 | 213 | """Handle communication channel being closed.""" | ||
1129 | 214 | # FIXME this is unimplemented and unused. | ||
1130 | 215 | self.destroy() | ||
1131 | 216 | |||
1132 | 217 | def __init__(self, remote_host, is_secret_valid, send_valid_key, | ||
1133 | 218 | remote_hostid, remote_oauth): | ||
1134 | 219 | self.logging = logging.getLogger(self.__class__.__name__) | ||
1135 | 220 | |||
1136 | 221 | self.is_secret_valid = is_secret_valid | ||
1137 | 222 | self.send_valid_key = send_valid_key | ||
1138 | 223 | |||
1139 | 224 | self.window = gtk.Window() | ||
1140 | 225 | self.window.set_border_width(6) | ||
1141 | 226 | |||
1142 | 227 | self.logging.info("want to listen for invitations.") | ||
1143 | 228 | |||
1144 | 229 | self.window.connect("delete_event", self.delete_event) | ||
1145 | 230 | self.window.connect("destroy", self.destroy) | ||
1146 | 231 | self.window.set_destroy_with_parent(True) | ||
1147 | 232 | self.window.set_title(_("Accepting Invitation")) | ||
1148 | 233 | |||
1149 | 234 | top_vbox = gtk.VBox() | ||
1150 | 235 | top_vbox.show() | ||
1151 | 236 | self.window.add(top_vbox) | ||
1152 | 237 | |||
1153 | 238 | self.remote_hostname = dbus_io.get_remote_hostname(remote_host) | ||
1154 | 239 | self.remote_hostid = remote_hostid | ||
1155 | 240 | self.remote_oauth = remote_oauth | ||
1156 | 241 | |||
1157 | 242 | description = gtk.Label( | ||
1158 | 243 | _("To verify your pairing with %s, enter its secret.") % | ||
1159 | 244 | self.remote_hostname) | ||
1160 | 245 | description.show() | ||
1161 | 246 | top_vbox.pack_start(description, False, False, 0) | ||
1162 | 247 | |||
1163 | 248 | self.entry_box = gtk.Entry(18) | ||
1164 | 249 | self.entry_box.connect("activate", lambda widget: self.verify_secret()) | ||
1165 | 250 | #self.window.connect("activate", lambda widget: self.verify_secret()) | ||
1166 | 251 | self.entry_box.set_activates_default(True) | ||
1167 | 252 | self.entry_box.show() | ||
1168 | 253 | top_vbox.pack_start(self.entry_box, False, False, 0) | ||
1169 | 254 | |||
1170 | 255 | self.result = gtk.Label("") | ||
1171 | 256 | self.result.show() | ||
1172 | 257 | self.entry_box.connect("changed", | ||
1173 | 258 | lambda widget: self.result.set_text("")) | ||
1174 | 259 | top_vbox.pack_start(self.result, False, False, 0) | ||
1175 | 260 | |||
1176 | 261 | button_bar = gtk.HBox(homogeneous=True) | ||
1177 | 262 | button_bar.show() | ||
1178 | 263 | top_vbox.pack_end(button_bar, False, False, 10) | ||
1179 | 264 | |||
1180 | 265 | cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL) | ||
1181 | 266 | cancel_button.set_border_width(3) | ||
1182 | 267 | cancel_button.connect("clicked", lambda widget: self.window.destroy()) | ||
1183 | 268 | button_bar.pack_end(cancel_button, False, False, 10) | ||
1184 | 269 | cancel_button.show() | ||
1185 | 270 | |||
1186 | 271 | connect_button = gtk.Button(_("Verify and connect")) | ||
1187 | 272 | add_image = gtk.Image() | ||
1188 | 273 | add_image.set_from_stock(gtk.STOCK_CONNECT, gtk.ICON_SIZE_BUTTON) | ||
1189 | 274 | connect_button.set_image(add_image) | ||
1190 | 275 | connect_button.set_border_width(3) | ||
1191 | 276 | connect_button.connect("clicked", lambda widget: self.verify_secret()) | ||
1192 | 277 | button_bar.pack_end(connect_button, False, False, 10) | ||
1193 | 278 | connect_button.show() | ||
1194 | 279 | |||
1195 | 280 | self.window.show_all() | ||
1196 | 281 | |||
1197 | 282 | def verify_secret(self): | ||
1198 | 283 | """We got a sumbission from the user's fingers as to what he thinks | ||
1199 | 284 | the secret is. We verify it, and if it's what the other end started | ||
1200 | 285 | with, then it's okay to let us pair with it.""" | ||
1201 | 286 | |||
1202 | 287 | proposed_secret = unmap_easily_mistaken(self.entry_box.get_text()) | ||
1203 | 288 | if self.is_secret_valid(proposed_secret): | ||
1204 | 289 | pair_with_host(self.remote_hostname, self.remote_hostid, | ||
1205 | 290 | self.remote_oauth) | ||
1206 | 291 | self.send_valid_key(proposed_secret) | ||
1207 | 292 | self.window.destroy() | ||
1208 | 293 | else: | ||
1209 | 294 | self.result.set_text("sorry, that is wrong") | ||
1210 | 295 | |||
1211 | 296 | |||
1212 | 297 | class Listening: | ||
1213 | 298 | """We're part of 'Alice' in this module's story. | ||
1214 | 299 | |||
1215 | 300 | Window that starts listening for other machines to pick *us* to pair | ||
1216 | 301 | with. There must be at least one of these on the network for pairing to | ||
1217 | 302 | happen. We listen for a finite amount of time, and then stop.""" | ||
1218 | 303 | |||
1219 | 304 | def delete_event(self, widget, event, data=None): | ||
1220 | 305 | """User requested window be closed. False to propogate event.""" | ||
1221 | 306 | return False | ||
1222 | 307 | |||
1223 | 308 | def destroy(self, widget, data=None): | ||
1224 | 309 | """Window is destroyed.""" | ||
1225 | 310 | self.timeout_counter = None | ||
1226 | 311 | |||
1227 | 312 | if self.advertisement is not None: | ||
1228 | 313 | self.advertisement.die() | ||
1229 | 314 | self.listener.close() | ||
1230 | 315 | |||
1231 | 316 | def receive_invitation_challenge(self, remote_address, is_secret_valid, | ||
1232 | 317 | send_secret, remote_hostid, remote_oauth): | ||
1233 | 318 | """When we receive an invitation, check its validity and if | ||
1234 | 319 | it's what we expected, then continue and accept it.""" | ||
1235 | 320 | |||
1236 | 321 | self.logging.warn("received invitation from %s", remote_address) | ||
1237 | 322 | self.acceptor = AcceptInvitation(remote_address, is_secret_valid, | ||
1238 | 323 | send_secret, remote_hostid, remote_oauth) | ||
1239 | 324 | self.acceptor.window.connect("destroy", | ||
1240 | 325 | lambda *args: setattr(self, "acceptor", None) and False) | ||
1241 | 326 | |||
1242 | 327 | def cancel_acceptor(self): | ||
1243 | 328 | """Destroy window when connection canceled.""" | ||
1244 | 329 | self.acceptor.window.destroy() | ||
1245 | 330 | |||
1246 | 331 | def make_listener(self, couchdb_instance): | ||
1247 | 332 | """Start listening for connections, and start advertising that | ||
1248 | 333 | we're listening.""" | ||
1249 | 334 | |||
1250 | 335 | self.listener = network_io.ListenForInvitations( | ||
1251 | 336 | self.receive_invitation_challenge, | ||
1252 | 337 | lambda: self.window.destroy(), | ||
1253 | 338 | couchdb_io.get_my_host_unique_id(create=True)[0], | ||
1254 | 339 | local_files.get_oauth_tokens()) | ||
1255 | 340 | |||
1256 | 341 | listen_port = self.listener.get_local_port() | ||
1257 | 342 | |||
1258 | 343 | hostname, domainname = dbus_io.get_local_hostname() | ||
1259 | 344 | username = getpass.getuser() | ||
1260 | 345 | self.advertisement = dbus_io.PairAdvertisement(port=listen_port, | ||
1261 | 346 | name="%s-%s-%d" % (hostname, username, listen_port), | ||
1262 | 347 | text=dict(version=str(DISCOVERY_TOOL_VERSION), | ||
1263 | 348 | description=get_host_info())) | ||
1264 | 349 | self.advertisement.publish() | ||
1265 | 350 | return hostname, username, listen_port | ||
1266 | 351 | |||
1267 | 352 | def __init__(self, couchdb_instance): | ||
1268 | 353 | self.logging = logging.getLogger(self.__class__.__name__) | ||
1269 | 354 | |||
1270 | 355 | self.listener = None | ||
1271 | 356 | self.listener_loop = None | ||
1272 | 357 | self.advertisement = None | ||
1273 | 358 | self.acceptor = None | ||
1274 | 359 | |||
1275 | 360 | self.timeout_counter = 180 | ||
1276 | 361 | |||
1277 | 362 | self.window = gtk.Window() | ||
1278 | 363 | self.window.set_border_width(6) | ||
1279 | 364 | |||
1280 | 365 | self.logging.info("want to listen for invitations.") | ||
1281 | 366 | |||
1282 | 367 | self.window.connect("delete_event", self.delete_event) | ||
1283 | 368 | self.window.connect("destroy", self.destroy) | ||
1284 | 369 | self.window.set_destroy_with_parent(True) | ||
1285 | 370 | self.window.set_title(_("Waiting for CouchDB Pairing Invitations")) | ||
1286 | 371 | |||
1287 | 372 | top_vbox = gtk.VBox() | ||
1288 | 373 | top_vbox.show() | ||
1289 | 374 | self.window.add(top_vbox) | ||
1290 | 375 | |||
1291 | 376 | self.counter_text = gtk.Label("#") | ||
1292 | 377 | self.counter_text.show() | ||
1293 | 378 | |||
1294 | 379 | top_vbox.pack_end(self.counter_text, False, False, 5) | ||
1295 | 380 | |||
1296 | 381 | button_bar = gtk.HBox(homogeneous=True) | ||
1297 | 382 | button_bar.show() | ||
1298 | 383 | top_vbox.pack_end(button_bar, False, False, 10) | ||
1299 | 384 | |||
1300 | 385 | cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL) | ||
1301 | 386 | cancel_button.set_border_width(3) | ||
1302 | 387 | cancel_button.connect("clicked", lambda *args: self.window.destroy()) | ||
1303 | 388 | button_bar.pack_end(cancel_button, False, False, 10) | ||
1304 | 389 | cancel_button.show() | ||
1305 | 390 | |||
1306 | 391 | add_minute_button = gtk.Button(_("Add 60 seconds")) | ||
1307 | 392 | add_image = gtk.Image() | ||
1308 | 393 | add_image.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON) | ||
1309 | 394 | add_minute_button.set_image(add_image) | ||
1310 | 395 | add_minute_button.set_border_width(3) | ||
1311 | 396 | add_minute_button.connect("clicked", | ||
1312 | 397 | lambda widget: self.add_to_timeout_counter(60)) | ||
1313 | 398 | button_bar.pack_end(add_minute_button, False, False, 10) | ||
1314 | 399 | add_minute_button.show() | ||
1315 | 400 | |||
1316 | 401 | hostid, userid, listen_port = self.make_listener(couchdb_instance) | ||
1317 | 402 | |||
1318 | 403 | text = gtk.Label() | ||
1319 | 404 | text.set_markup( | ||
1320 | 405 | _("""We're listening for invitations! From another\n""" + | ||
1321 | 406 | """machine on this local network, run this\n""" + | ||
1322 | 407 | """same tool and find the machine called\n""" + | ||
1323 | 408 | """<span font-size="xx-large" weight="bold"><tt>""" + | ||
1324 | 409 | """%s-%s-%d</tt></span> .""") % | ||
1325 | 410 | (cgi.escape(hostid), cgi.escape(userid), | ||
1326 | 411 | listen_port)) | ||
1327 | 412 | text.set_justify(gtk.JUSTIFY_CENTER) | ||
1328 | 413 | top_vbox.pack_start(text, False, False, 10) | ||
1329 | 414 | text.show() | ||
1330 | 415 | |||
1331 | 416 | self.update_counter_view() | ||
1332 | 417 | self.window.show_all() | ||
1333 | 418 | |||
1334 | 419 | gobject.timeout_add(1000, self.decrement_counter) | ||
1335 | 420 | |||
1336 | 421 | def add_to_timeout_counter(self, seconds): | ||
1337 | 422 | """The user wants more time. Add seconds to the clock.""" | ||
1338 | 423 | self.timeout_counter += seconds | ||
1339 | 424 | self.update_counter_view() | ||
1340 | 425 | |||
1341 | 426 | def update_counter_view(self): | ||
1342 | 427 | """Update the counter widget with pretty text.""" | ||
1343 | 428 | self.counter_text.set_text( | ||
1344 | 429 | _("%d seconds remaining") % self.timeout_counter) | ||
1345 | 430 | |||
1346 | 431 | def decrement_counter(self): | ||
1347 | 432 | """Tick! Decrement the counter and update the display.""" | ||
1348 | 433 | if self.timeout_counter is None: | ||
1349 | 434 | return False | ||
1350 | 435 | |||
1351 | 436 | self.timeout_counter -= 1 | ||
1352 | 437 | if self.timeout_counter < 0: | ||
1353 | 438 | self.window.destroy() | ||
1354 | 439 | return False | ||
1355 | 440 | |||
1356 | 441 | self.update_counter_view() | ||
1357 | 442 | return True | ||
1358 | 443 | |||
1359 | 444 | |||
1360 | 445 | class PickOrListen: | ||
1361 | 446 | """Main top-level window that represents the life of the application.""" | ||
1362 | 447 | |||
1363 | 448 | def delete_event(self, widget, event, data=None): | ||
1364 | 449 | """User requested window be closed. False to propogate event.""" | ||
1365 | 450 | return False | ||
1366 | 451 | |||
1367 | 452 | def destroy(self, widget, data=None): | ||
1368 | 453 | """The window was destroyed.""" | ||
1369 | 454 | stop_program() | ||
1370 | 455 | |||
1371 | 456 | def create_pick_pane(self, container): | ||
1372 | 457 | """Set up the pane that contains what's necessary to choose an | ||
1373 | 458 | already-listening tool instance. This sets up a "Bob" in the | ||
1374 | 459 | module's story.""" | ||
1375 | 460 | |||
1376 | 461 | # positions: host id, descr, host, port, cloud_name | ||
1377 | 462 | self.listening_hosts = gtk.TreeStore(str, str, str, int, str) | ||
1378 | 463 | |||
1379 | 464 | import desktopcouch.replication_services as services | ||
1380 | 465 | |||
1381 | 466 | for srv_name in dir(services): | ||
1382 | 467 | if srv_name.startswith("__"): | ||
1383 | 468 | continue | ||
1384 | 469 | srv = getattr(services, srv_name) | ||
1385 | 470 | try: | ||
1386 | 471 | if srv.is_active(): | ||
1387 | 472 | all_paired_cloud_servers = [x.key for x in | ||
1388 | 473 | couchdb_io.get_pairings()] | ||
1389 | 474 | if not srv_name in all_paired_cloud_servers: | ||
1390 | 475 | self.listening_hosts.append(None, | ||
1391 | 476 | [srv.name, srv.description, "", 0, srv_name]) | ||
1392 | 477 | except Exception, e: | ||
1393 | 478 | self.logging.exception("service %r has errors", srv_name) | ||
1394 | 479 | |||
1395 | 480 | self.inviting = None # pylint: disable-msg=W0201 | ||
1396 | 481 | |||
1397 | 482 | hostname_col = gtk.TreeViewColumn(_("hostname")) | ||
1398 | 483 | hostid_col = gtk.TreeViewColumn(_("service name")) | ||
1399 | 484 | description_col = gtk.TreeViewColumn(_("description")) | ||
1400 | 485 | |||
1401 | 486 | pick_box = gtk.VBox() | ||
1402 | 487 | container.pack_start(pick_box, False, False, 10) | ||
1403 | 488 | |||
1404 | 489 | l = gtk.Label(_("Pick a listening host to invite it to pair with us.")) | ||
1405 | 490 | pick_box.pack_start(l, False, False, 0) | ||
1406 | 491 | l.show() | ||
1407 | 492 | |||
1408 | 493 | tv = gtk.TreeView(self.listening_hosts) | ||
1409 | 494 | tv.set_headers_visible(False) | ||
1410 | 495 | tv.set_rules_hint(True) | ||
1411 | 496 | tv.show() | ||
1412 | 497 | |||
1413 | 498 | def clicked(selection): | ||
1414 | 499 | """An item in the list of services was clicked, so now we go | ||
1415 | 500 | about inviting it to pair with us.""" | ||
1416 | 501 | |||
1417 | 502 | model, iter = selection.get_selected() | ||
1418 | 503 | if not iter: | ||
1419 | 504 | return | ||
1420 | 505 | service = model.get_value(iter, 0) | ||
1421 | 506 | description = model.get_value(iter, 1) | ||
1422 | 507 | hostname = model.get_value(iter, 2) | ||
1423 | 508 | port = model.get_value(iter, 3) | ||
1424 | 509 | service_name = model.get_value(iter, 4) | ||
1425 | 510 | |||
1426 | 511 | if service_name: | ||
1427 | 512 | # Pairing with a cloud service, which doesn't do key exchange | ||
1428 | 513 | pair_with_cloud_service(service_name) | ||
1429 | 514 | # remove from listening list | ||
1430 | 515 | self.listening_hosts.remove(iter) | ||
1431 | 516 | # add to already-paired list | ||
1432 | 517 | srv = getattr(services, service_name) | ||
1433 | 518 | self.already_paired_hosts.append(None, | ||
1434 | 519 | [service, _("paired just now"), hostname, port, service_name, None]) | ||
1435 | 520 | return | ||
1436 | 521 | |||
1437 | 522 | self.logging.info("connecting to %s:%s tcp to invite", | ||
1438 | 523 | hostname, port) | ||
1439 | 524 | if self.inviting != None: | ||
1440 | 525 | self.inviting.window.destroy() | ||
1441 | 526 | self.inviting = Inviting(service, hostname, port) | ||
1442 | 527 | self.inviting.window.connect("destroy", | ||
1443 | 528 | lambda *args: setattr(self, "inviting_window", None)) | ||
1444 | 529 | |||
1445 | 530 | tv_selection = tv.get_selection() | ||
1446 | 531 | tv_selection.connect("changed", clicked) | ||
1447 | 532 | |||
1448 | 533 | scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) | ||
1449 | 534 | scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
1450 | 535 | scrolled_window.set_border_width(10) | ||
1451 | 536 | scrolled_window.add_with_viewport(tv) | ||
1452 | 537 | scrolled_window.show() | ||
1453 | 538 | |||
1454 | 539 | pick_box.pack_start(scrolled_window, True, False, 0) | ||
1455 | 540 | |||
1456 | 541 | def add_service_to_list(name, description, host, port, version): | ||
1457 | 542 | """When a zeroconf service appears, this adds it to the | ||
1458 | 543 | listing of choices.""" | ||
1459 | 544 | self.listening_hosts.append(None, [name, description, host, port, None]) | ||
1460 | 545 | |||
1461 | 546 | def remove_service_from_list(name): | ||
1462 | 547 | """When a zeroconf service disappears, this finds it in the | ||
1463 | 548 | listing and removes it as an option for picking.""" | ||
1464 | 549 | |||
1465 | 550 | it = self.listening_hosts.get_iter_first() | ||
1466 | 551 | while it is not None: | ||
1467 | 552 | if self.listening_hosts.get_value(it, 0) == name: | ||
1468 | 553 | self.listening_hosts.remove(it) | ||
1469 | 554 | return True | ||
1470 | 555 | it = self.listening_hosts.iter_next(it) | ||
1471 | 556 | |||
1472 | 557 | dbus_io.discover_services(add_service_to_list, | ||
1473 | 558 | remove_service_from_list, show_local=False) | ||
1474 | 559 | |||
1475 | 560 | cell = gtk.CellRendererText() | ||
1476 | 561 | tv.append_column(hostname_col) | ||
1477 | 562 | hostname_col.pack_start(cell, True) | ||
1478 | 563 | hostname_col.add_attribute(cell, 'text', 2) | ||
1479 | 564 | |||
1480 | 565 | cell = gtk.CellRendererText() | ||
1481 | 566 | cell.set_property("weight", pango.WEIGHT_BOLD) | ||
1482 | 567 | cell.set_property("weight-set", True) | ||
1483 | 568 | tv.append_column(hostid_col) | ||
1484 | 569 | hostid_col.pack_start(cell, True) | ||
1485 | 570 | hostid_col.add_attribute(cell, 'text', 0) | ||
1486 | 571 | |||
1487 | 572 | cell = gtk.CellRendererText() | ||
1488 | 573 | cell.set_property("ellipsize", pango.ELLIPSIZE_END) | ||
1489 | 574 | cell.set_property("ellipsize-set", True) | ||
1490 | 575 | tv.append_column(description_col) | ||
1491 | 576 | description_col.pack_start(cell, True) | ||
1492 | 577 | description_col.add_attribute(cell, 'text', 1) | ||
1493 | 578 | |||
1494 | 579 | return pick_box | ||
1495 | 580 | |||
1496 | 581 | def create_already_paired_pane(self, container): | ||
1497 | 582 | """Set up the pane that shows servers which are already paired.""" | ||
1498 | 583 | |||
1499 | 584 | # positions: host id, descr, host, port, cloud_name, pairingid | ||
1500 | 585 | self.already_paired_hosts = gtk.TreeStore(str, str, str, int, str, str) | ||
1501 | 586 | |||
1502 | 587 | import desktopcouch.replication_services as services | ||
1503 | 588 | for already_paired_record in couchdb_io.get_pairings(): | ||
1504 | 589 | pid = already_paired_record.value["pairing_identifier"] | ||
1505 | 590 | if "service_name" in already_paired_record.value: | ||
1506 | 591 | srv_name = already_paired_record.value["service_name"] | ||
1507 | 592 | if srv_name.startswith("__"): | ||
1508 | 593 | continue | ||
1509 | 594 | srv = getattr(services, srv_name) | ||
1510 | 595 | if not srv.is_active(): | ||
1511 | 596 | continue | ||
1512 | 597 | if already_paired_record.value.get("unpaired", False): | ||
1513 | 598 | continue | ||
1514 | 599 | nice_description = _("paired ") + \ | ||
1515 | 600 | already_paired_record.value.get("ctime", | ||
1516 | 601 | _("unknown date")) | ||
1517 | 602 | try: | ||
1518 | 603 | self.already_paired_hosts.append(None, | ||
1519 | 604 | [srv.name, nice_description, "", 0, srv_name, pid]) | ||
1520 | 605 | except Exception, e: | ||
1521 | 606 | logging.error("Service %s had an error", srv_name, e) | ||
1522 | 607 | elif "server" in already_paired_record.value: | ||
1523 | 608 | hostname = already_paired_record.value["server"] | ||
1524 | 609 | nice_description = _("paired ") + \ | ||
1525 | 610 | already_paired_record.value.get("ctime", | ||
1526 | 611 | _("unknown date")) | ||
1527 | 612 | self.already_paired_hosts.append(None, | ||
1528 | 613 | [hostname, nice_description, None, 0, None, pid]) | ||
1529 | 614 | else: | ||
1530 | 615 | logging.error("unknown pairing record %s", | ||
1531 | 616 | already_paired_record) | ||
1532 | 617 | |||
1533 | 618 | hostid_col = gtk.TreeViewColumn(_("service name")) | ||
1534 | 619 | description_col = gtk.TreeViewColumn(_("service name")) | ||
1535 | 620 | |||
1536 | 621 | pick_box = gtk.VBox() | ||
1537 | 622 | container.pack_start(pick_box, False, False, 10) | ||
1538 | 623 | |||
1539 | 624 | l = gtk.Label(_("You're currently paired with these hosts. Click to unpair.")) | ||
1540 | 625 | pick_box.pack_start(l, False, False, 0) | ||
1541 | 626 | l.show() | ||
1542 | 627 | |||
1543 | 628 | tv = gtk.TreeView(self.already_paired_hosts) | ||
1544 | 629 | tv.set_headers_visible(False) | ||
1545 | 630 | tv.set_rules_hint(True) | ||
1546 | 631 | tv.show() | ||
1547 | 632 | |||
1548 | 633 | def clicked(selection): | ||
1549 | 634 | """An item in the list of services was clicked, so now we go | ||
1550 | 635 | about inviting it to pair with us.""" | ||
1551 | 636 | |||
1552 | 637 | model, iter = selection.get_selected() | ||
1553 | 638 | if not iter: | ||
1554 | 639 | return | ||
1555 | 640 | service = model.get_value(iter, 0) | ||
1556 | 641 | hostname = model.get_value(iter, 2) | ||
1557 | 642 | port = model.get_value(iter, 3) | ||
1558 | 643 | service_name = model.get_value(iter, 4) | ||
1559 | 644 | pid = model.get_value(iter, 5) | ||
1560 | 645 | |||
1561 | 646 | if service_name: | ||
1562 | 647 | # delete record | ||
1563 | 648 | for record in couchdb_io.get_pairings(): | ||
1564 | 649 | couchdb_io.remove_pairing(record.id, True) | ||
1565 | 650 | |||
1566 | 651 | # remove from already-paired list | ||
1567 | 652 | self.already_paired_hosts.remove(iter) | ||
1568 | 653 | # add to listening list | ||
1569 | 654 | srv = getattr(services, service_name) | ||
1570 | 655 | self.listening_hosts.append(None, [service, srv.description, | ||
1571 | 656 | hostname, port, service_name]) | ||
1572 | 657 | return | ||
1573 | 658 | |||
1574 | 659 | # delete (really, mark as "unpaired") | ||
1575 | 660 | for record in couchdb_io.get_pairings(): | ||
1576 | 661 | if record.value["pairing_identifier"] == pid: | ||
1577 | 662 | couchdb_io.remove_pairing(record.id, False) | ||
1578 | 663 | break | ||
1579 | 664 | |||
1580 | 665 | # remove from already-paired list | ||
1581 | 666 | self.already_paired_hosts.remove(iter) | ||
1582 | 667 | # do not add to listening list -- if it's listening then zeroconf | ||
1583 | 668 | # will pick it up | ||
1584 | 669 | return | ||
1585 | 670 | |||
1586 | 671 | tv_selection = tv.get_selection() | ||
1587 | 672 | tv_selection.connect("changed", clicked) | ||
1588 | 673 | |||
1589 | 674 | scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) | ||
1590 | 675 | scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
1591 | 676 | scrolled_window.set_border_width(10) | ||
1592 | 677 | scrolled_window.add_with_viewport(tv) | ||
1593 | 678 | scrolled_window.show() | ||
1594 | 679 | |||
1595 | 680 | pick_box.pack_start(scrolled_window, True, False, 0) | ||
1596 | 681 | |||
1597 | 682 | cell = gtk.CellRendererText() | ||
1598 | 683 | tv.append_column(hostid_col) | ||
1599 | 684 | hostid_col.pack_start(cell, True) | ||
1600 | 685 | hostid_col.add_attribute(cell, 'text', 0) | ||
1601 | 686 | |||
1602 | 687 | cell = gtk.CellRendererText() | ||
1603 | 688 | cell.set_property("ellipsize", pango.ELLIPSIZE_END) | ||
1604 | 689 | cell.set_property("ellipsize-set", True) | ||
1605 | 690 | tv.append_column(description_col) | ||
1606 | 691 | description_col.pack_start(cell, True) | ||
1607 | 692 | description_col.add_attribute(cell, 'text', 1) | ||
1608 | 693 | |||
1609 | 694 | return pick_box | ||
1610 | 695 | |||
1611 | 696 | def create_single_listen_pane(self, container): | ||
1612 | 697 | """This sets up an "Alice" from the module's story. | ||
1613 | 698 | |||
1614 | 699 | This assumes we're pairing a single, known local CouchDB instance, | ||
1615 | 700 | instead of generic instances that we'd need more information to talk | ||
1616 | 701 | about. Instead of using this function, one might use another that | ||
1617 | 702 | lists local DBs as a way of picking one to pair. This function assumes | ||
1618 | 703 | we know the answer to that.""" | ||
1619 | 704 | |||
1620 | 705 | def listen(btn, couchdb_instance=None): | ||
1621 | 706 | """When we decide to listen for invitations, this spawns the | ||
1622 | 707 | advertising/information window, and disables the button that | ||
1623 | 708 | causes/-ed us to get here (to prevent multiple instances).""" | ||
1624 | 709 | |||
1625 | 710 | if couchdb_instance is None: | ||
1626 | 711 | # assume local for this user. FIXME | ||
1627 | 712 | pass | ||
1628 | 713 | |||
1629 | 714 | btn.set_sensitive(False) | ||
1630 | 715 | listening = Listening(couchdb_instance) | ||
1631 | 716 | listening.window.connect("destroy", | ||
1632 | 717 | lambda w: btn.set_sensitive(True)) | ||
1633 | 718 | |||
1634 | 719 | padding = 6 | ||
1635 | 720 | |||
1636 | 721 | listen_box = gtk.HBox() | ||
1637 | 722 | container.pack_start(listen_box, False, False, 2) | ||
1638 | 723 | |||
1639 | 724 | l = gtk.Label(_("Add this host to the list for others to see?")) | ||
1640 | 725 | listen_box.pack_start(l, True, False, padding) | ||
1641 | 726 | l.show() | ||
1642 | 727 | |||
1643 | 728 | listen_button = gtk.Button(_("Listen for invitations")) | ||
1644 | 729 | listen_button.connect("clicked", listen, None) | ||
1645 | 730 | listen_box.pack_start(listen_button, True, False, padding) | ||
1646 | 731 | listen_button.show() | ||
1647 | 732 | |||
1648 | 733 | return listen_box | ||
1649 | 734 | |||
1650 | 735 | def create_any_listen_pane(self, container): | ||
1651 | 736 | """Unused. An example of where to start when we don't already have | ||
1652 | 737 | a couchdb instance in mind to advertise. This should be the generic | ||
1653 | 738 | version.""" | ||
1654 | 739 | |||
1655 | 740 | l = gtk.Label(_("I also know of CouchDB sessions here. Pick one " + | ||
1656 | 741 | "to add it to the invitation list for other computers to see.")) | ||
1657 | 742 | |||
1658 | 743 | local_sharables_list = gtk.TreeStore(str, str) | ||
1659 | 744 | # et c, et c | ||
1660 | 745 | #some_row_in_list.connect("clicked", self.listen, target_db_info) | ||
1661 | 746 | |||
1662 | 747 | def __init__(self): | ||
1663 | 748 | |||
1664 | 749 | |||
1665 | 750 | self.logging = logging.getLogger(self.__class__.__name__) | ||
1666 | 751 | |||
1667 | 752 | self.window = gtk.Window() | ||
1668 | 753 | |||
1669 | 754 | self.window.connect("delete_event", self.delete_event) | ||
1670 | 755 | self.window.connect("destroy", self.destroy) | ||
1671 | 756 | |||
1672 | 757 | self.window.set_border_width(8) | ||
1673 | 758 | self.window.set_title(_("CouchDB Pairing Tool")) | ||
1674 | 759 | |||
1675 | 760 | top_vbox = gtk.VBox() | ||
1676 | 761 | self.window.add(top_vbox) | ||
1677 | 762 | |||
1678 | 763 | self.pick_pane = self.create_pick_pane(top_vbox) | ||
1679 | 764 | self.listen_pane = self.create_single_listen_pane(top_vbox) | ||
1680 | 765 | seperator = gtk.HSeparator() | ||
1681 | 766 | top_vbox.pack_start(seperator, False, False, 10) | ||
1682 | 767 | seperator.show() | ||
1683 | 768 | self.already_paired_pane = self.create_already_paired_pane(top_vbox) | ||
1684 | 769 | |||
1685 | 770 | copyright = gtk.Label(_("Copyright 2009 Canonical")) | ||
1686 | 771 | top_vbox.pack_end(copyright, False, False, 0) | ||
1687 | 772 | copyright.show() | ||
1688 | 773 | |||
1689 | 774 | self.pick_pane.show() | ||
1690 | 775 | self.listen_pane.show() | ||
1691 | 776 | self.already_paired_pane.show() | ||
1692 | 777 | |||
1693 | 778 | top_vbox.show() | ||
1694 | 779 | self.window.show() | ||
1695 | 780 | |||
1696 | 781 | |||
1697 | 782 | def pair_with_host(hostname, hostid, oauth_data): | ||
1698 | 783 | """We've verified all is correct and authorized, so now we pair | ||
1699 | 784 | the databases.""" | ||
1700 | 785 | logging.info("verified host %s/%s. Done!", hostname, hostid) | ||
1701 | 786 | |||
1702 | 787 | try: | ||
1703 | 788 | result = couchdb_io.put_dynamic_paired_host(hostname, hostid, oauth_data) | ||
1704 | 789 | assert result is not None | ||
1705 | 790 | except Exception, e: | ||
1706 | 791 | logging.exception("failure writing record for %s", hostname) | ||
1707 | 792 | fail_note = gtk.MessageDialog( | ||
1708 | 793 | parent=pick_or_listen.window, | ||
1709 | 794 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, | ||
1710 | 795 | buttons=gtk.BUTTONS_OK, | ||
1711 | 796 | type=gtk.MESSAGE_ERROR, | ||
1712 | 797 | message_format =_("Couldn't save pairing details for %s") % hostname) | ||
1713 | 798 | fail_note.run() | ||
1714 | 799 | fail_note.destroy() | ||
1715 | 800 | return | ||
1716 | 801 | |||
1717 | 802 | success_note = gtk.Dialog(title=_("Paired with %(hostname)s") % locals(), | ||
1718 | 803 | parent=pick_or_listen.window, | ||
1719 | 804 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, | ||
1720 | 805 | buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,)) | ||
1721 | 806 | text = gtk.Label( | ||
1722 | 807 | _("Successfully paired with %(hostname)s.") % locals()) | ||
1723 | 808 | text.show() | ||
1724 | 809 | content_box = success_note.get_content_area() | ||
1725 | 810 | content_box.pack_start(text, True, True, 20) | ||
1726 | 811 | success_note.connect("close", | ||
1727 | 812 | lambda *args: pick_or_listen.window.destroy()) | ||
1728 | 813 | success_note.connect("response", | ||
1729 | 814 | lambda *args: pick_or_listen.window.destroy()) | ||
1730 | 815 | success_note.show() | ||
1731 | 816 | |||
1732 | 817 | |||
1733 | 818 | def pair_with_cloud_service(service_name): | ||
1734 | 819 | """Write a paired server record for the selected cloud service.""" | ||
1735 | 820 | try: | ||
1736 | 821 | import desktopcouch.replication_services as services | ||
1737 | 822 | srv = getattr(services, service_name) | ||
1738 | 823 | oauth_data = srv.oauth_data() | ||
1739 | 824 | result = couchdb_io.put_static_paired_service(oauth_data, service_name) | ||
1740 | 825 | assert result != None | ||
1741 | 826 | except Exception, e: | ||
1742 | 827 | logging.exception("failure in module for service %r", service_name) | ||
1743 | 828 | fail_note = gtk.MessageDialog( | ||
1744 | 829 | parent=pick_or_listen.window, | ||
1745 | 830 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, | ||
1746 | 831 | buttons=gtk.BUTTONS_OK, | ||
1747 | 832 | type=gtk.MESSAGE_ERROR, | ||
1748 | 833 | message_format =_("Couldn't save pairing details for %r") % service_name) | ||
1749 | 834 | fail_note.run() | ||
1750 | 835 | fail_note.destroy() | ||
1751 | 836 | return | ||
1752 | 837 | |||
1753 | 838 | success_note = gtk.MessageDialog( | ||
1754 | 839 | parent=pick_or_listen.window, | ||
1755 | 840 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, | ||
1756 | 841 | buttons=gtk.BUTTONS_OK, | ||
1757 | 842 | type=gtk.MESSAGE_INFO, | ||
1758 | 843 | message_format =_("Successfully paired with %s") % service_name) | ||
1759 | 844 | success_note.run() | ||
1760 | 845 | success_note.destroy() | ||
1761 | 846 | |||
1762 | 847 | |||
1763 | 848 | def set_couchdb_bind_address(): | ||
1764 | 849 | from desktopcouch.records.server import CouchDatabase | ||
1765 | 850 | from desktopcouch import local_files | ||
1766 | 851 | bind_address = local_files.get_bind_address() | ||
1767 | 852 | |||
1768 | 853 | if bind_address not in ("127.0.0.1", "0.0.0.0", "::1", None): | ||
1769 | 854 | logging.info("we're not qualified to change explicit address %s", | ||
1770 | 855 | bind_address) | ||
1771 | 856 | return False | ||
1772 | 857 | |||
1773 | 858 | db = CouchDatabase("management", create=True) | ||
1774 | 859 | results = db.get_records(create_view=True) | ||
1775 | 860 | count = 0 | ||
1776 | 861 | for row in results[pairing_record_type]: | ||
1777 | 862 | # Is the record of something that probably connects back to us? | ||
1778 | 863 | if "server" in row.value and row.value["server"] != "": | ||
1779 | 864 | count += 1 | ||
1780 | 865 | logging.debug("paired back-connecting machine count is %d", count) | ||
1781 | 866 | if count > 0: | ||
1782 | 867 | couchdb_io.get_my_host_unique_id(create=True) # ensure self-id record | ||
1783 | 868 | if ":" in bind_address: | ||
1784 | 869 | want_bind_address = "::0" # IPv6 addr any | ||
1785 | 870 | else: | ||
1786 | 871 | want_bind_address = "0.0.0.0" | ||
1787 | 872 | else: | ||
1788 | 873 | if ":" in bind_address: | ||
1789 | 874 | want_bind_address = "::1" # IPv6 loop back | ||
1790 | 875 | else: | ||
1791 | 876 | want_bind_address = "127.0.0.1" | ||
1792 | 877 | |||
1793 | 878 | if bind_address != want_bind_address: | ||
1794 | 879 | local_files.set_bind_address(want_bind_address) | ||
1795 | 880 | logging.warning("changing the desktopcouch bind address from %r to %r", | ||
1796 | 881 | bind_address, want_bind_address) | ||
1797 | 882 | |||
1798 | 883 | def main(args): | ||
1799 | 884 | """Start execution.""" | ||
1800 | 885 | global pick_or_listen # pylint: disable-msg=W0601 | ||
1801 | 886 | |||
1802 | 887 | logging.basicConfig(level=logging.DEBUG, format= | ||
1803 | 888 | "%(asctime)s [%(process)d] %(name)s:%(levelname)s: %(message)s") | ||
1804 | 889 | |||
1805 | 890 | gettext.install("couchdb_pairing") | ||
1806 | 891 | |||
1807 | 892 | try: | ||
1808 | 893 | logging.debug("starting couchdb pairing tool") | ||
1809 | 894 | pick_or_listen = PickOrListen() | ||
1810 | 895 | return run_program() | ||
1811 | 896 | finally: | ||
1812 | 897 | set_couchdb_bind_address() | ||
1813 | 898 | logging.debug("exiting couchdb pairing tool") | ||
1814 | 899 | |||
1815 | 900 | |||
1816 | 901 | if __name__ == "__main__": | ||
1817 | 902 | import sys | ||
1818 | 903 | import desktopcouch | ||
1819 | 904 | desktopcouch_port = desktopcouch.find_port() | ||
1820 | 905 | main(sys.argv) | ||
1821 | 0 | 906 | ||
1822 | === added file 'bin/desktopcouch-service' | |||
1823 | --- bin/desktopcouch-service 1970-01-01 00:00:00 +0000 | |||
1824 | +++ bin/desktopcouch-service 2009-09-11 03:18:00 +0000 | |||
1825 | @@ -0,0 +1,108 @@ | |||
1826 | 1 | #!/usr/bin/python | ||
1827 | 2 | # Copyright 2009 Canonical Ltd. | ||
1828 | 3 | # | ||
1829 | 4 | # This file is part of desktopcouch. | ||
1830 | 5 | # | ||
1831 | 6 | # desktopcouch is free software: you can redistribute it and/or modify | ||
1832 | 7 | # it under the terms of the GNU Lesser General Public License version 3 | ||
1833 | 8 | # as published by the Free Software Foundation. | ||
1834 | 9 | # | ||
1835 | 10 | # desktopcouch is distributed in the hope that it will be useful, | ||
1836 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1837 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1838 | 13 | # GNU Lesser General Public License for more details. | ||
1839 | 14 | # | ||
1840 | 15 | # You should have received a copy of the GNU Lesser General Public License | ||
1841 | 16 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
1842 | 17 | # | ||
1843 | 18 | # Authors: Stuart Langridge <stuart.langridge@canonical.com> | ||
1844 | 19 | # Tim Cole <tim.cole@canonical.com> | ||
1845 | 20 | |||
1846 | 21 | """CouchDB port advertiser. | ||
1847 | 22 | |||
1848 | 23 | A command-line utility which exports a | ||
1849 | 24 | desktopCouch.getPort method on the bus which returns | ||
1850 | 25 | that port, so other apps (specifically, the contacts API) can work out | ||
1851 | 26 | where CouchDB is running so it can be talked to. | ||
1852 | 27 | |||
1853 | 28 | Calculates the port number by looking in the CouchDB log. | ||
1854 | 29 | |||
1855 | 30 | If CouchDB is not running, then run the script to start it and then | ||
1856 | 31 | start advertising the port. | ||
1857 | 32 | |||
1858 | 33 | This file should be started by D-Bus activation. | ||
1859 | 34 | |||
1860 | 35 | """ | ||
1861 | 36 | |||
1862 | 37 | import logging | ||
1863 | 38 | import logging.handlers | ||
1864 | 39 | from errno import ENOENT | ||
1865 | 40 | |||
1866 | 41 | from twisted.internet import glib2reactor | ||
1867 | 42 | glib2reactor.install() | ||
1868 | 43 | from twisted.internet import reactor | ||
1869 | 44 | import dbus.service, gobject, os, errno, time | ||
1870 | 45 | |||
1871 | 46 | import desktopcouch | ||
1872 | 47 | from desktopcouch import local_files | ||
1873 | 48 | from desktopcouch import replication | ||
1874 | 49 | |||
1875 | 50 | |||
1876 | 51 | class PortAdvertiser(dbus.service.Object): | ||
1877 | 52 | "Advertise the discovered port number on the D-Bus Session bus" | ||
1878 | 53 | def __init__(self, death): | ||
1879 | 54 | bus_name = dbus.service.BusName("org.desktopcouch.CouchDB", | ||
1880 | 55 | bus=dbus.SessionBus()) | ||
1881 | 56 | self.death = death | ||
1882 | 57 | dbus.service.Object.__init__(self, object_path="/", bus_name=bus_name) | ||
1883 | 58 | |||
1884 | 59 | @dbus.service.method(dbus_interface='org.desktopcouch.CouchDB', | ||
1885 | 60 | in_signature='', out_signature='i') | ||
1886 | 61 | def getPort(self): | ||
1887 | 62 | "Exported method to return the port" | ||
1888 | 63 | return int(desktopcouch.find_port()) | ||
1889 | 64 | |||
1890 | 65 | @dbus.service.method(dbus_interface='org.desktopcouch.CouchDB', | ||
1891 | 66 | in_signature='', out_signature='') | ||
1892 | 67 | def quit(self): | ||
1893 | 68 | "Exported method to quit the program" | ||
1894 | 69 | self.death() | ||
1895 | 70 | |||
1896 | 71 | if __name__ == "__main__": | ||
1897 | 72 | import xdg.BaseDirectory | ||
1898 | 73 | |||
1899 | 74 | log_directory = os.path.join(xdg.BaseDirectory.xdg_cache_home, | ||
1900 | 75 | "ubuntuone/log") | ||
1901 | 76 | try: | ||
1902 | 77 | os.makedirs(log_directory) | ||
1903 | 78 | except: | ||
1904 | 79 | pass | ||
1905 | 80 | rotating_log = logging.handlers.TimedRotatingFileHandler( | ||
1906 | 81 | os.path.join(log_directory, "desktop-couch-replication.log"), | ||
1907 | 82 | "midnight", 1, 14) | ||
1908 | 83 | rotating_log.setLevel(logging.DEBUG) | ||
1909 | 84 | formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') | ||
1910 | 85 | rotating_log.setFormatter(formatter) | ||
1911 | 86 | logging.getLogger('').addHandler(rotating_log) | ||
1912 | 87 | console_log = logging.StreamHandler() | ||
1913 | 88 | console_log.setLevel(logging.WARNING) | ||
1914 | 89 | console_log.setFormatter(logging.Formatter("%(asctime)s - %(message)s")) | ||
1915 | 90 | logging.getLogger('').addHandler(console_log) | ||
1916 | 91 | logging.getLogger('').setLevel(logging.DEBUG) | ||
1917 | 92 | |||
1918 | 93 | |||
1919 | 94 | # Advertise the port | ||
1920 | 95 | mainloop = reactor | ||
1921 | 96 | |||
1922 | 97 | portAdvertiser = PortAdvertiser(mainloop.stop) | ||
1923 | 98 | |||
1924 | 99 | replication_runtime = replication.set_up(desktopcouch.find_port) | ||
1925 | 100 | try: | ||
1926 | 101 | |||
1927 | 102 | mainloop.run() | ||
1928 | 103 | |||
1929 | 104 | finally: | ||
1930 | 105 | if replication_runtime: | ||
1931 | 106 | print "Shutting down, but may be a moment to finish replication." | ||
1932 | 107 | replication.tear_down(*replication_runtime) | ||
1933 | 108 | |||
1934 | 0 | 109 | ||
1935 | === added file 'bin/desktopcouch-stop' | |||
1936 | --- bin/desktopcouch-stop 1970-01-01 00:00:00 +0000 | |||
1937 | +++ bin/desktopcouch-stop 2009-07-24 19:32:52 +0000 | |||
1938 | @@ -0,0 +1,36 @@ | |||
1939 | 1 | #!/usr/bin/python | ||
1940 | 2 | # Copyright 2009 Canonical Ltd. | ||
1941 | 3 | # | ||
1942 | 4 | # This file is part of desktopcouch. | ||
1943 | 5 | # | ||
1944 | 6 | # desktopcouch is free software: you can redistribute it and/or modify | ||
1945 | 7 | # it under the terms of the GNU Lesser General Public License version 3 | ||
1946 | 8 | # as published by the Free Software Foundation. | ||
1947 | 9 | # | ||
1948 | 10 | # desktopcouch is distributed in the hope that it will be useful, | ||
1949 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1950 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1951 | 13 | # GNU Lesser General Public License for more details. | ||
1952 | 14 | # | ||
1953 | 15 | # You should have received a copy of the GNU Lesser General Public License | ||
1954 | 16 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
1955 | 17 | # | ||
1956 | 18 | # Author: Stuart Langridge <stuart.langridge@canonical.com> | ||
1957 | 19 | """ | ||
1958 | 20 | Stop local CouchDB server. | ||
1959 | 21 | """ | ||
1960 | 22 | import subprocess, sys | ||
1961 | 23 | from desktopcouch import local_files | ||
1962 | 24 | |||
1963 | 25 | local_exec = local_files.COUCH_EXEC_COMMAND + ["-k"] | ||
1964 | 26 | try: | ||
1965 | 27 | retcode = subprocess.call(local_exec, shell=False) | ||
1966 | 28 | if retcode < 0: | ||
1967 | 29 | print >> sys.stderr, "Child was terminated by signal", -retcode | ||
1968 | 30 | elif retcode > 0: | ||
1969 | 31 | print >> sys.stderr, "Child returned", retcode | ||
1970 | 32 | except OSError, e: | ||
1971 | 33 | print >> sys.stderr, "Execution failed: %s: %s" % (e, local_exec) | ||
1972 | 34 | exit(1) | ||
1973 | 35 | |||
1974 | 36 | |||
1975 | 0 | 37 | ||
1976 | === added directory 'config' | |||
1977 | === added directory 'config/desktop-couch' | |||
1978 | === added file 'config/desktop-couch/compulsory-auth.ini' | |||
1979 | --- config/desktop-couch/compulsory-auth.ini 1970-01-01 00:00:00 +0000 | |||
1980 | +++ config/desktop-couch/compulsory-auth.ini 2009-09-11 19:08:53 +0000 | |||
1981 | @@ -0,0 +1,3 @@ | |||
1982 | 1 | [couch_httpd_auth] | ||
1983 | 2 | require_valid_user = true | ||
1984 | 3 | |||
1985 | 0 | 4 | ||
1986 | === added directory 'contrib' | |||
1987 | === added file 'contrib/mocker.py' | |||
1988 | --- contrib/mocker.py 1970-01-01 00:00:00 +0000 | |||
1989 | +++ contrib/mocker.py 2009-08-06 13:57:30 +0000 | |||
1990 | @@ -0,0 +1,2068 @@ | |||
1991 | 1 | """ | ||
1992 | 2 | Copyright (c) 2007 Gustavo Niemeyer <gustavo@niemeyer.net> | ||
1993 | 3 | |||
1994 | 4 | Graceful platform for test doubles in Python (mocks, stubs, fakes, and dummies). | ||
1995 | 5 | """ | ||
1996 | 6 | import __builtin__ | ||
1997 | 7 | import tempfile | ||
1998 | 8 | import unittest | ||
1999 | 9 | import inspect | ||
2000 | 10 | import shutil | ||
2001 | 11 | import types | ||
2002 | 12 | import sys | ||
2003 | 13 | import os | ||
2004 | 14 | import gc | ||
2005 | 15 | |||
2006 | 16 | |||
2007 | 17 | if sys.version_info < (2, 4): | ||
2008 | 18 | from sets import Set as set # pragma: nocover | ||
2009 | 19 | |||
2010 | 20 | |||
2011 | 21 | __all__ = ["Mocker", "expect", "IS", "CONTAINS", "IN", "MATCH", | ||
2012 | 22 | "ANY", "ARGS", "KWARGS"] | ||
2013 | 23 | |||
2014 | 24 | |||
2015 | 25 | __author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" | ||
2016 | 26 | __license__ = "PSF License" | ||
2017 | 27 | __version__ = "0.10.1" | ||
2018 | 28 | |||
2019 | 29 | |||
2020 | 30 | ERROR_PREFIX = "[Mocker] " | ||
2021 | 31 | |||
2022 | 32 | |||
2023 | 33 | # -------------------------------------------------------------------- | ||
2024 | 34 | # Exceptions | ||
2025 | 35 | |||
2026 | 36 | class MatchError(AssertionError): | ||
2027 | 37 | """Raised when an unknown expression is seen in playback mode.""" | ||
2028 | 38 | |||
2029 | 39 | |||
2030 | 40 | # -------------------------------------------------------------------- | ||
2031 | 41 | # Helper for chained-style calling. | ||
2032 | 42 | |||
2033 | 43 | class expect(object): | ||
2034 | 44 | """This is a simple helper that allows a different call-style. | ||
2035 | 45 | |||
2036 | 46 | With this class one can comfortably do chaining of calls to the | ||
2037 | 47 | mocker object responsible by the object being handled. For instance:: | ||
2038 | 48 | |||
2039 | 49 | expect(obj.attr).result(3).count(1, 2) | ||
2040 | 50 | |||
2041 | 51 | Is the same as:: | ||
2042 | 52 | |||
2043 | 53 | obj.attr | ||
2044 | 54 | mocker.result(3) | ||
2045 | 55 | mocker.count(1, 2) | ||
2046 | 56 | |||
2047 | 57 | """ | ||
2048 | 58 | |||
2049 | 59 | def __init__(self, mock, attr=None): | ||
2050 | 60 | self._mock = mock | ||
2051 | 61 | self._attr = attr | ||
2052 | 62 | |||
2053 | 63 | def __getattr__(self, attr): | ||
2054 | 64 | return self.__class__(self._mock, attr) | ||
2055 | 65 | |||
2056 | 66 | def __call__(self, *args, **kwargs): | ||
2057 | 67 | getattr(self._mock.__mocker__, self._attr)(*args, **kwargs) | ||
2058 | 68 | return self | ||
2059 | 69 | |||
2060 | 70 | |||
2061 | 71 | # -------------------------------------------------------------------- | ||
2062 | 72 | # Extensions to Python's unittest. | ||
2063 | 73 | |||
2064 | 74 | class MockerTestCase(unittest.TestCase): | ||
2065 | 75 | """unittest.TestCase subclass with Mocker support. | ||
2066 | 76 | |||
2067 | 77 | @ivar mocker: The mocker instance. | ||
2068 | 78 | |||
2069 | 79 | This is a convenience only. Mocker may easily be used with the | ||
2070 | 80 | standard C{unittest.TestCase} class if wanted. | ||
2071 | 81 | |||
2072 | 82 | Test methods have a Mocker instance available on C{self.mocker}. | ||
2073 | 83 | At the end of each test method, expectations of the mocker will | ||
2074 | 84 | be verified, and any requested changes made to the environment | ||
2075 | 85 | will be restored. | ||
2076 | 86 | |||
2077 | 87 | In addition to the integration with Mocker, this class provides | ||
2078 | 88 | a few additional helper methods. | ||
2079 | 89 | """ | ||
2080 | 90 | |||
2081 | 91 | expect = expect | ||
2082 | 92 | |||
2083 | 93 | def __init__(self, methodName="runTest"): | ||
2084 | 94 | # So here is the trick: we take the real test method, wrap it on | ||
2085 | 95 | # a function that do the job we have to do, and insert it in the | ||
2086 | 96 | # *instance* dictionary, so that getattr() will return our | ||
2087 | 97 | # replacement rather than the class method. | ||
2088 | 98 | test_method = getattr(self, methodName, None) | ||
2089 | 99 | if test_method is not None: | ||
2090 | 100 | def test_method_wrapper(): | ||
2091 | 101 | try: | ||
2092 | 102 | result = test_method() | ||
2093 | 103 | except: | ||
2094 | 104 | raise | ||
2095 | 105 | else: | ||
2096 | 106 | if (self.mocker.is_recording() and | ||
2097 | 107 | self.mocker.get_events()): | ||
2098 | 108 | raise RuntimeError("Mocker must be put in replay " | ||
2099 | 109 | "mode with self.mocker.replay()") | ||
2100 | 110 | if (hasattr(result, "addCallback") and | ||
2101 | 111 | hasattr(result, "addErrback")): | ||
2102 | 112 | def verify(result): | ||
2103 | 113 | self.mocker.verify() | ||
2104 | 114 | return result | ||
2105 | 115 | result.addCallback(verify) | ||
2106 | 116 | else: | ||
2107 | 117 | self.mocker.verify() | ||
2108 | 118 | return result | ||
2109 | 119 | # Copy all attributes from the original method.. | ||
2110 | 120 | for attr in dir(test_method): | ||
2111 | 121 | # .. unless they're present in our wrapper already. | ||
2112 | 122 | if not hasattr(test_method_wrapper, attr) or attr == "__doc__": | ||
2113 | 123 | setattr(test_method_wrapper, attr, | ||
2114 | 124 | getattr(test_method, attr)) | ||
2115 | 125 | setattr(self, methodName, test_method_wrapper) | ||
2116 | 126 | |||
2117 | 127 | # We could overload run() normally, but other well-known testing | ||
2118 | 128 | # frameworks do it as well, and some of them won't call the super, | ||
2119 | 129 | # which might mean that cleanup wouldn't happen. With that in mind, | ||
2120 | 130 | # we make integration easier by using the following trick. | ||
2121 | 131 | run_method = self.run | ||
2122 | 132 | def run_wrapper(*args, **kwargs): | ||
2123 | 133 | try: | ||
2124 | 134 | return run_method(*args, **kwargs) | ||
2125 | 135 | finally: | ||
2126 | 136 | self.__cleanup() | ||
2127 | 137 | self.run = run_wrapper | ||
2128 | 138 | |||
2129 | 139 | self.mocker = Mocker() | ||
2130 | 140 | |||
2131 | 141 | self.__cleanup_funcs = [] | ||
2132 | 142 | self.__cleanup_paths = [] | ||
2133 | 143 | |||
2134 | 144 | super(MockerTestCase, self).__init__(methodName) | ||
2135 | 145 | |||
2136 | 146 | def __cleanup(self): | ||
2137 | 147 | for path in self.__cleanup_paths: | ||
2138 | 148 | if os.path.isfile(path): | ||
2139 | 149 | os.unlink(path) | ||
2140 | 150 | elif os.path.isdir(path): | ||
2141 | 151 | shutil.rmtree(path) | ||
2142 | 152 | self.mocker.restore() | ||
2143 | 153 | for func, args, kwargs in self.__cleanup_funcs: | ||
2144 | 154 | func(*args, **kwargs) | ||
2145 | 155 | |||
2146 | 156 | def addCleanup(self, func, *args, **kwargs): | ||
2147 | 157 | self.__cleanup_funcs.append((func, args, kwargs)) | ||
2148 | 158 | |||
2149 | 159 | def makeFile(self, content=None, suffix="", prefix="tmp", basename=None, | ||
2150 | 160 | dirname=None, path=None): | ||
2151 | 161 | """Create a temporary file and return the path to it. | ||
2152 | 162 | |||
2153 | 163 | @param content: Initial content for the file. | ||
2154 | 164 | @param suffix: Suffix to be given to the file's basename. | ||
2155 | 165 | @param prefix: Prefix to be given to the file's basename. | ||
2156 | 166 | @param basename: Full basename for the file. | ||
2157 | 167 | @param dirname: Put file inside this directory. | ||
2158 | 168 | |||
2159 | 169 | The file is removed after the test runs. | ||
2160 | 170 | """ | ||
2161 | 171 | if path is not None: | ||
2162 | 172 | self.__cleanup_paths.append(path) | ||
2163 | 173 | elif basename is not None: | ||
2164 | 174 | if dirname is None: | ||
2165 | 175 | dirname = tempfile.mkdtemp() | ||
2166 | 176 | self.__cleanup_paths.append(dirname) | ||
2167 | 177 | path = os.path.join(dirname, basename) | ||
2168 | 178 | else: | ||
2169 | 179 | fd, path = tempfile.mkstemp(suffix, prefix, dirname) | ||
2170 | 180 | self.__cleanup_paths.append(path) | ||
2171 | 181 | os.close(fd) | ||
2172 | 182 | if content is None: | ||
2173 | 183 | os.unlink(path) | ||
2174 | 184 | if content is not None: | ||
2175 | 185 | file = open(path, "w") | ||
2176 | 186 | file.write(content) | ||
2177 | 187 | file.close() | ||
2178 | 188 | return path | ||
2179 | 189 | |||
2180 | 190 | def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None): | ||
2181 | 191 | """Create a temporary directory and return the path to it. | ||
2182 | 192 | |||
2183 | 193 | @param suffix: Suffix to be given to the file's basename. | ||
2184 | 194 | @param prefix: Prefix to be given to the file's basename. | ||
2185 | 195 | @param dirname: Put directory inside this parent directory. | ||
2186 | 196 | |||
2187 | 197 | The directory is removed after the test runs. | ||
2188 | 198 | """ | ||
2189 | 199 | if path is not None: | ||
2190 | 200 | os.makedirs(path) | ||
2191 | 201 | else: | ||
2192 | 202 | path = tempfile.mkdtemp(suffix, prefix, dirname) | ||
2193 | 203 | self.__cleanup_paths.append(path) | ||
2194 | 204 | return path | ||
2195 | 205 | |||
2196 | 206 | def failUnlessIs(self, first, second, msg=None): | ||
2197 | 207 | """Assert that C{first} is the same object as C{second}.""" | ||
2198 | 208 | if first is not second: | ||
2199 | 209 | raise self.failureException(msg or "%r is not %r" % (first, second)) | ||
2200 | 210 | |||
2201 | 211 | def failIfIs(self, first, second, msg=None): | ||
2202 | 212 | """Assert that C{first} is not the same object as C{second}.""" | ||
2203 | 213 | if first is second: | ||
2204 | 214 | raise self.failureException(msg or "%r is %r" % (first, second)) | ||
2205 | 215 | |||
2206 | 216 | def failUnlessIn(self, first, second, msg=None): | ||
2207 | 217 | """Assert that C{first} is contained in C{second}.""" | ||
2208 | 218 | if first not in second: | ||
2209 | 219 | raise self.failureException(msg or "%r not in %r" % (first, second)) | ||
2210 | 220 | |||
2211 | 221 | def failUnlessStartsWith(self, first, second, msg=None): | ||
2212 | 222 | """Assert that C{first} starts with C{second}.""" | ||
2213 | 223 | if first[:len(second)] != second: | ||
2214 | 224 | raise self.failureException(msg or "%r doesn't start with %r" % | ||
2215 | 225 | (first, second)) | ||
2216 | 226 | |||
2217 | 227 | def failIfStartsWith(self, first, second, msg=None): | ||
2218 | 228 | """Assert that C{first} doesn't start with C{second}.""" | ||
2219 | 229 | if first[:len(second)] == second: | ||
2220 | 230 | raise self.failureException(msg or "%r starts with %r" % | ||
2221 | 231 | (first, second)) | ||
2222 | 232 | |||
2223 | 233 | def failUnlessEndsWith(self, first, second, msg=None): | ||
2224 | 234 | """Assert that C{first} starts with C{second}.""" | ||
2225 | 235 | if first[len(first)-len(second):] != second: | ||
2226 | 236 | raise self.failureException(msg or "%r doesn't end with %r" % | ||
2227 | 237 | (first, second)) | ||
2228 | 238 | |||
2229 | 239 | def failIfEndsWith(self, first, second, msg=None): | ||
2230 | 240 | """Assert that C{first} doesn't start with C{second}.""" | ||
2231 | 241 | if first[len(first)-len(second):] == second: | ||
2232 | 242 | raise self.failureException(msg or "%r ends with %r" % | ||
2233 | 243 | (first, second)) | ||
2234 | 244 | |||
2235 | 245 | def failIfIn(self, first, second, msg=None): | ||
2236 | 246 | """Assert that C{first} is not contained in C{second}.""" | ||
2237 | 247 | if first in second: | ||
2238 | 248 | raise self.failureException(msg or "%r in %r" % (first, second)) | ||
2239 | 249 | |||
2240 | 250 | def failUnlessApproximates(self, first, second, tolerance, msg=None): | ||
2241 | 251 | """Assert that C{first} is near C{second} by at most C{tolerance}.""" | ||
2242 | 252 | if abs(first - second) > tolerance: | ||
2243 | 253 | raise self.failureException(msg or "abs(%r - %r) > %r" % | ||
2244 | 254 | (first, second, tolerance)) | ||
2245 | 255 | |||
2246 | 256 | def failIfApproximates(self, first, second, tolerance, msg=None): | ||
2247 | 257 | """Assert that C{first} is far from C{second} by at least C{tolerance}. | ||
2248 | 258 | """ | ||
2249 | 259 | if abs(first - second) <= tolerance: | ||
2250 | 260 | raise self.failureException(msg or "abs(%r - %r) <= %r" % | ||
2251 | 261 | (first, second, tolerance)) | ||
2252 | 262 | |||
2253 | 263 | def failUnlessMethodsMatch(self, first, second): | ||
2254 | 264 | """Assert that public methods in C{first} are present in C{second}. | ||
2255 | 265 | |||
2256 | 266 | This method asserts that all public methods found in C{first} are also | ||
2257 | 267 | present in C{second} and accept the same arguments. C{first} may | ||
2258 | 268 | have its own private methods, though, and may not have all methods | ||
2259 | 269 | found in C{second}. Note that if a private method in C{first} matches | ||
2260 | 270 | the name of one in C{second}, their specification is still compared. | ||
2261 | 271 | |||
2262 | 272 | This is useful to verify if a fake or stub class have the same API as | ||
2263 | 273 | the real class being simulated. | ||
2264 | 274 | """ | ||
2265 | 275 | first_methods = dict(inspect.getmembers(first, inspect.ismethod)) | ||
2266 | 276 | second_methods = dict(inspect.getmembers(second, inspect.ismethod)) | ||
2267 | 277 | for name, first_method in first_methods.items(): | ||
2268 | 278 | first_argspec = inspect.getargspec(first_method) | ||
2269 | 279 | first_formatted = inspect.formatargspec(*first_argspec) | ||
2270 | 280 | |||
2271 | 281 | second_method = second_methods.get(name) | ||
2272 | 282 | if second_method is None: | ||
2273 | 283 | if name[:1] == "_": | ||
2274 | 284 | continue # First may have its own private methods. | ||
2275 | 285 | raise self.failureException("%s.%s%s not present in %s" % | ||
2276 | 286 | (first.__name__, name, first_formatted, second.__name__)) | ||
2277 | 287 | |||
2278 | 288 | second_argspec = inspect.getargspec(second_method) | ||
2279 | 289 | if first_argspec != second_argspec: | ||
2280 | 290 | second_formatted = inspect.formatargspec(*second_argspec) | ||
2281 | 291 | raise self.failureException("%s.%s%s != %s.%s%s" % | ||
2282 | 292 | (first.__name__, name, first_formatted, | ||
2283 | 293 | second.__name__, name, second_formatted)) | ||
2284 | 294 | |||
2285 | 295 | |||
2286 | 296 | assertIs = failUnlessIs | ||
2287 | 297 | assertIsNot = failIfIs | ||
2288 | 298 | assertIn = failUnlessIn | ||
2289 | 299 | assertNotIn = failIfIn | ||
2290 | 300 | assertStartsWith = failUnlessStartsWith | ||
2291 | 301 | assertNotStartsWith = failIfStartsWith | ||
2292 | 302 | assertEndsWith = failUnlessEndsWith | ||
2293 | 303 | assertNotEndsWith = failIfEndsWith | ||
2294 | 304 | assertApproximates = failUnlessApproximates | ||
2295 | 305 | assertNotApproximates = failIfApproximates | ||
2296 | 306 | assertMethodsMatch = failUnlessMethodsMatch | ||
2297 | 307 | |||
2298 | 308 | # The following are missing in Python < 2.4. | ||
2299 | 309 | assertTrue = unittest.TestCase.failUnless | ||
2300 | 310 | assertFalse = unittest.TestCase.failIf | ||
2301 | 311 | |||
2302 | 312 | # The following is provided for compatibility with Twisted's trial. | ||
2303 | 313 | assertIdentical = assertIs | ||
2304 | 314 | assertNotIdentical = assertIsNot | ||
2305 | 315 | failUnlessIdentical = failUnlessIs | ||
2306 | 316 | failIfIdentical = failIfIs | ||
2307 | 317 | |||
2308 | 318 | |||
2309 | 319 | # -------------------------------------------------------------------- | ||
2310 | 320 | # Mocker. | ||
2311 | 321 | |||
2312 | 322 | class classinstancemethod(object): | ||
2313 | 323 | |||
2314 | 324 | def __init__(self, method): | ||
2315 | 325 | self.method = method | ||
2316 | 326 | |||
2317 | 327 | def __get__(self, obj, cls=None): | ||
2318 | 328 | def bound_method(*args, **kwargs): | ||
2319 | 329 | return self.method(cls, obj, *args, **kwargs) | ||
2320 | 330 | return bound_method | ||
2321 | 331 | |||
2322 | 332 | |||
2323 | 333 | class MockerBase(object): | ||
2324 | 334 | """Controller of mock objects. | ||
2325 | 335 | |||
2326 | 336 | A mocker instance is used to command recording and replay of | ||
2327 | 337 | expectations on any number of mock objects. | ||
2328 | 338 | |||
2329 | 339 | Expectations should be expressed for the mock object while in | ||
2330 | 340 | record mode (the initial one) by using the mock object itself, | ||
2331 | 341 | and using the mocker (and/or C{expect()} as a helper) to define | ||
2332 | 342 | additional behavior for each event. For instance:: | ||
2333 | 343 | |||
2334 | 344 | mock = mocker.mock() | ||
2335 | 345 | mock.hello() | ||
2336 | 346 | mocker.result("Hi!") | ||
2337 | 347 | mocker.replay() | ||
2338 | 348 | assert mock.hello() == "Hi!" | ||
2339 | 349 | mock.restore() | ||
2340 | 350 | mock.verify() | ||
2341 | 351 | |||
2342 | 352 | In this short excerpt a mock object is being created, then an | ||
2343 | 353 | expectation of a call to the C{hello()} method was recorded, and | ||
2344 | 354 | when called the method should return the value C{10}. Then, the | ||
2345 | 355 | mocker is put in replay mode, and the expectation is satisfied by | ||
2346 | 356 | calling the C{hello()} method, which indeed returns 10. Finally, | ||
2347 | 357 | a call to the L{restore()} method is performed to undo any needed | ||
2348 | 358 | changes made in the environment, and the L{verify()} method is | ||
2349 | 359 | called to ensure that all defined expectations were met. | ||
2350 | 360 | |||
2351 | 361 | The same logic can be expressed more elegantly using the | ||
2352 | 362 | C{with mocker:} statement, as follows:: | ||
2353 | 363 | |||
2354 | 364 | mock = mocker.mock() | ||
2355 | 365 | mock.hello() | ||
2356 | 366 | mocker.result("Hi!") | ||
2357 | 367 | with mocker: | ||
2358 | 368 | assert mock.hello() == "Hi!" | ||
2359 | 369 | |||
2360 | 370 | Also, the MockerTestCase class, which integrates the mocker on | ||
2361 | 371 | a unittest.TestCase subclass, may be used to reduce the overhead | ||
2362 | 372 | of controlling the mocker. A test could be written as follows:: | ||
2363 | 373 | |||
2364 | 374 | class SampleTest(MockerTestCase): | ||
2365 | 375 | |||
2366 | 376 | def test_hello(self): | ||
2367 | 377 | mock = self.mocker.mock() | ||
2368 | 378 | mock.hello() | ||
2369 | 379 | self.mocker.result("Hi!") | ||
2370 | 380 | self.mocker.replay() | ||
2371 | 381 | self.assertEquals(mock.hello(), "Hi!") | ||
2372 | 382 | """ | ||
2373 | 383 | |||
2374 | 384 | _recorders = [] | ||
2375 | 385 | |||
2376 | 386 | # For convenience only. | ||
2377 | 387 | on = expect | ||
2378 | 388 | |||
2379 | 389 | class __metaclass__(type): | ||
2380 | 390 | def __init__(self, name, bases, dict): | ||
2381 | 391 | # Make independent lists on each subclass, inheriting from parent. | ||
2382 | 392 | self._recorders = list(getattr(self, "_recorders", ())) | ||
2383 | 393 | |||
2384 | 394 | def __init__(self): | ||
2385 | 395 | self._recorders = self._recorders[:] | ||
2386 | 396 | self._events = [] | ||
2387 | 397 | self._recording = True | ||
2388 | 398 | self._ordering = False | ||
2389 | 399 | self._last_orderer = None | ||
2390 | 400 | |||
2391 | 401 | def is_recording(self): | ||
2392 | 402 | """Return True if in recording mode, False if in replay mode. | ||
2393 | 403 | |||
2394 | 404 | Recording is the initial state. | ||
2395 | 405 | """ | ||
2396 | 406 | return self._recording | ||
2397 | 407 | |||
2398 | 408 | def replay(self): | ||
2399 | 409 | """Change to replay mode, where recorded events are reproduced. | ||
2400 | 410 | |||
2401 | 411 | If already in replay mode, the mocker will be restored, with all | ||
2402 | 412 | expectations reset, and then put again in replay mode. | ||
2403 | 413 | |||
2404 | 414 | An alternative and more comfortable way to replay changes is | ||
2405 | 415 | using the 'with' statement, as follows:: | ||
2406 | 416 | |||
2407 | 417 | mocker = Mocker() | ||
2408 | 418 | <record events> | ||
2409 | 419 | with mocker: | ||
2410 | 420 | <reproduce events> | ||
2411 | 421 | |||
2412 | 422 | The 'with' statement will automatically put mocker in replay | ||
2413 | 423 | mode, and will also verify if all events were correctly reproduced | ||
2414 | 424 | at the end (using L{verify()}), and also restore any changes done | ||
2415 | 425 | in the environment (with L{restore()}). | ||
2416 | 426 | |||
2417 | 427 | Also check the MockerTestCase class, which integrates the | ||
2418 | 428 | unittest.TestCase class with mocker. | ||
2419 | 429 | """ | ||
2420 | 430 | if not self._recording: | ||
2421 | 431 | for event in self._events: | ||
2422 | 432 | event.restore() | ||
2423 | 433 | else: | ||
2424 | 434 | self._recording = False | ||
2425 | 435 | for event in self._events: | ||
2426 | 436 | event.replay() | ||
2427 | 437 | |||
2428 | 438 | def restore(self): | ||
2429 | 439 | """Restore changes in the environment, and return to recording mode. | ||
2430 | 440 | |||
2431 | 441 | This should always be called after the test is complete (succeeding | ||
2432 | 442 | or not). There are ways to call this method automatically on | ||
2433 | 443 | completion (e.g. using a C{with mocker:} statement, or using the | ||
2434 | 444 | L{MockerTestCase} class. | ||
2435 | 445 | """ | ||
2436 | 446 | if not self._recording: | ||
2437 | 447 | self._recording = True | ||
2438 | 448 | for event in self._events: | ||
2439 | 449 | event.restore() | ||
2440 | 450 | |||
2441 | 451 | def reset(self): | ||
2442 | 452 | """Reset the mocker state. | ||
2443 | 453 | |||
2444 | 454 | This will restore environment changes, if currently in replay | ||
2445 | 455 | mode, and then remove all events previously recorded. | ||
2446 | 456 | """ | ||
2447 | 457 | if not self._recording: | ||
2448 | 458 | self.restore() | ||
2449 | 459 | self.unorder() | ||
2450 | 460 | del self._events[:] | ||
2451 | 461 | |||
2452 | 462 | def get_events(self): | ||
2453 | 463 | """Return all recorded events.""" | ||
2454 | 464 | return self._events[:] | ||
2455 | 465 | |||
2456 | 466 | def add_event(self, event): | ||
2457 | 467 | """Add an event. | ||
2458 | 468 | |||
2459 | 469 | This method is used internally by the implementation, and | ||
2460 | 470 | shouldn't be needed on normal mocker usage. | ||
2461 | 471 | """ | ||
2462 | 472 | self._events.append(event) | ||
2463 | 473 | if self._ordering: | ||
2464 | 474 | orderer = event.add_task(Orderer(event.path)) | ||
2465 | 475 | if self._last_orderer: | ||
2466 | 476 | orderer.add_dependency(self._last_orderer) | ||
2467 | 477 | self._last_orderer = orderer | ||
2468 | 478 | return event | ||
2469 | 479 | |||
2470 | 480 | def verify(self): | ||
2471 | 481 | """Check if all expectations were met, and raise AssertionError if not. | ||
2472 | 482 | |||
2473 | 483 | The exception message will include a nice description of which | ||
2474 | 484 | expectations were not met, and why. | ||
2475 | 485 | """ | ||
2476 | 486 | errors = [] | ||
2477 | 487 | for event in self._events: | ||
2478 | 488 | try: | ||
2479 | 489 | event.verify() | ||
2480 | 490 | except AssertionError, e: | ||
2481 | 491 | error = str(e) | ||
2482 | 492 | if not error: | ||
2483 | 493 | raise RuntimeError("Empty error message from %r" | ||
2484 | 494 | % event) | ||
2485 | 495 | errors.append(error) | ||
2486 | 496 | if errors: | ||
2487 | 497 | message = [ERROR_PREFIX + "Unmet expectations:", ""] | ||
2488 | 498 | for error in errors: | ||
2489 | 499 | lines = error.splitlines() | ||
2490 | 500 | message.append("=> " + lines.pop(0)) | ||
2491 | 501 | message.extend([" " + line for line in lines]) | ||
2492 | 502 | message.append("") | ||
2493 | 503 | raise AssertionError(os.linesep.join(message)) | ||
2494 | 504 | |||
2495 | 505 | def mock(self, spec_and_type=None, spec=None, type=None, | ||
2496 | 506 | name=None, count=True): | ||
2497 | 507 | """Return a new mock object. | ||
2498 | 508 | |||
2499 | 509 | @param spec_and_type: Handy positional argument which sets both | ||
2500 | 510 | spec and type. | ||
2501 | 511 | @param spec: Method calls will be checked for correctness against | ||
2502 | 512 | the given class. | ||
2503 | 513 | @param type: If set, the Mock's __class__ attribute will return | ||
2504 | 514 | the given type. This will make C{isinstance()} calls | ||
2505 | 515 | on the object work. | ||
2506 | 516 | @param name: Name for the mock object, used in the representation of | ||
2507 | 517 | expressions. The name is rarely needed, as it's usually | ||
2508 | 518 | guessed correctly from the variable name used. | ||
2509 | 519 | @param count: If set to false, expressions may be executed any number | ||
2510 | 520 | of times, unless an expectation is explicitly set using | ||
2511 | 521 | the L{count()} method. By default, expressions are | ||
2512 | 522 | expected once. | ||
2513 | 523 | """ | ||
2514 | 524 | if spec_and_type is not None: | ||
2515 | 525 | spec = type = spec_and_type | ||
2516 | 526 | return Mock(self, spec=spec, type=type, name=name, count=count) | ||
2517 | 527 | |||
2518 | 528 | def proxy(self, object, spec=True, type=True, name=None, count=True, | ||
2519 | 529 | passthrough=True): | ||
2520 | 530 | """Return a new mock object which proxies to the given object. | ||
2521 | 531 | |||
2522 | 532 | Proxies are useful when only part of the behavior of an object | ||
2523 | 533 | is to be mocked. Unknown expressions may be passed through to | ||
2524 | 534 | the real implementation implicitly (if the C{passthrough} argument | ||
2525 | 535 | is True), or explicitly (using the L{passthrough()} method | ||
2526 | 536 | on the event). | ||
2527 | 537 | |||
2528 | 538 | @param object: Real object to be proxied, and replaced by the mock | ||
2529 | 539 | on replay mode. It may also be an "import path", | ||
2530 | 540 | such as C{"time.time"}, in which case the object | ||
2531 | 541 | will be the C{time} function from the C{time} module. | ||
2532 | 542 | @param spec: Method calls will be checked for correctness against | ||
2533 | 543 | the given object, which may be a class or an instance | ||
2534 | 544 | where attributes will be looked up. Defaults to the | ||
2535 | 545 | the C{object} parameter. May be set to None explicitly, | ||
2536 | 546 | in which case spec checking is disabled. Checks may | ||
2537 | 547 | also be disabled explicitly on a per-event basis with | ||
2538 | 548 | the L{nospec()} method. | ||
2539 | 549 | @param type: If set, the Mock's __class__ attribute will return | ||
2540 | 550 | the given type. This will make C{isinstance()} calls | ||
2541 | 551 | on the object work. Defaults to the type of the | ||
2542 | 552 | C{object} parameter. May be set to None explicitly. | ||
2543 | 553 | @param name: Name for the mock object, used in the representation of | ||
2544 | 554 | expressions. The name is rarely needed, as it's usually | ||
2545 | 555 | guessed correctly from the variable name used. | ||
2546 | 556 | @param count: If set to false, expressions may be executed any number | ||
2547 | 557 | of times, unless an expectation is explicitly set using | ||
2548 | 558 | the L{count()} method. By default, expressions are | ||
2549 | 559 | expected once. | ||
2550 | 560 | @param passthrough: If set to False, passthrough of actions on the | ||
2551 | 561 | proxy to the real object will only happen when | ||
2552 | 562 | explicitly requested via the L{passthrough()} | ||
2553 | 563 | method. | ||
2554 | 564 | """ | ||
2555 | 565 | if isinstance(object, basestring): | ||
2556 | 566 | if name is None: | ||
2557 | 567 | name = object | ||
2558 | 568 | import_stack = object.split(".") | ||
2559 | 569 | attr_stack = [] | ||
2560 | 570 | while import_stack: | ||
2561 | 571 | module_path = ".".join(import_stack) | ||
2562 | 572 | try: | ||
2563 | 573 | object = __import__(module_path, {}, {}, [""]) | ||
2564 | 574 | except ImportError: | ||
2565 | 575 | attr_stack.insert(0, import_stack.pop()) | ||
2566 | 576 | if not import_stack: | ||
2567 | 577 | raise | ||
2568 | 578 | continue | ||
2569 | 579 | else: | ||
2570 | 580 | for attr in attr_stack: | ||
2571 | 581 | object = getattr(object, attr) | ||
2572 | 582 | break | ||
2573 | 583 | if spec is True: | ||
2574 | 584 | spec = object | ||
2575 | 585 | if type is True: | ||
2576 | 586 | type = __builtin__.type(object) | ||
2577 | 587 | return Mock(self, spec=spec, type=type, object=object, | ||
2578 | 588 | name=name, count=count, passthrough=passthrough) | ||
2579 | 589 | |||
2580 | 590 | def replace(self, object, spec=True, type=True, name=None, count=True, | ||
2581 | 591 | passthrough=True): | ||
2582 | 592 | """Create a proxy, and replace the original object with the mock. | ||
2583 | 593 | |||
2584 | 594 | On replay, the original object will be replaced by the returned | ||
2585 | 595 | proxy in all dictionaries found in the running interpreter via | ||
2586 | 596 | the garbage collecting system. This should cover module | ||
2587 | 597 | namespaces, class namespaces, instance namespaces, and so on. | ||
2588 | 598 | |||
2589 | 599 | @param object: Real object to be proxied, and replaced by the mock | ||
2590 | 600 | on replay mode. It may also be an "import path", | ||
2591 | 601 | such as C{"time.time"}, in which case the object | ||
2592 | 602 | will be the C{time} function from the C{time} module. | ||
2593 | 603 | @param spec: Method calls will be checked for correctness against | ||
2594 | 604 | the given object, which may be a class or an instance | ||
2595 | 605 | where attributes will be looked up. Defaults to the | ||
2596 | 606 | the C{object} parameter. May be set to None explicitly, | ||
2597 | 607 | in which case spec checking is disabled. Checks may | ||
2598 | 608 | also be disabled explicitly on a per-event basis with | ||
2599 | 609 | the L{nospec()} method. | ||
2600 | 610 | @param type: If set, the Mock's __class__ attribute will return | ||
2601 | 611 | the given type. This will make C{isinstance()} calls | ||
2602 | 612 | on the object work. Defaults to the type of the | ||
2603 | 613 | C{object} parameter. May be set to None explicitly. | ||
2604 | 614 | @param name: Name for the mock object, used in the representation of | ||
2605 | 615 | expressions. The name is rarely needed, as it's usually | ||
2606 | 616 | guessed correctly from the variable name used. | ||
2607 | 617 | @param passthrough: If set to False, passthrough of actions on the | ||
2608 | 618 | proxy to the real object will only happen when | ||
2609 | 619 | explicitly requested via the L{passthrough()} | ||
2610 | 620 | method. | ||
2611 | 621 | """ | ||
2612 | 622 | mock = self.proxy(object, spec, type, name, count, passthrough) | ||
2613 | 623 | event = self._get_replay_restore_event() | ||
2614 | 624 | event.add_task(ProxyReplacer(mock)) | ||
2615 | 625 | return mock | ||
2616 | 626 | |||
2617 | 627 | def patch(self, object, spec=True): | ||
2618 | 628 | """Patch an existing object to reproduce recorded events. | ||
2619 | 629 | |||
2620 | 630 | @param object: Class or instance to be patched. | ||
2621 | 631 | @param spec: Method calls will be checked for correctness against | ||
2622 | 632 | the given object, which may be a class or an instance | ||
2623 | 633 | where attributes will be looked up. Defaults to the | ||
2624 | 634 | the C{object} parameter. May be set to None explicitly, | ||
2625 | 635 | in which case spec checking is disabled. Checks may | ||
2626 | 636 | also be disabled explicitly on a per-event basis with | ||
2627 | 637 | the L{nospec()} method. | ||
2628 | 638 | |||
2629 | 639 | The result of this method is still a mock object, which can be | ||
2630 | 640 | used like any other mock object to record events. The difference | ||
2631 | 641 | is that when the mocker is put on replay mode, the *real* object | ||
2632 | 642 | will be modified to behave according to recorded expectations. | ||
2633 | 643 | |||
2634 | 644 | Patching works in individual instances, and also in classes. | ||
2635 | 645 | When an instance is patched, recorded events will only be | ||
2636 | 646 | considered on this specific instance, and other instances should | ||
2637 | 647 | behave normally. When a class is patched, the reproduction of | ||
2638 | 648 | events will be considered on any instance of this class once | ||
2639 | 649 | created (collectively). | ||
2640 | 650 | |||
2641 | 651 | Observe that, unlike with proxies which catch only events done | ||
2642 | 652 | through the mock object, *all* accesses to recorded expectations | ||
2643 | 653 | will be considered; even these coming from the object itself | ||
2644 | 654 | (e.g. C{self.hello()} is considered if this method was patched). | ||
2645 | 655 | While this is a very powerful feature, and many times the reason | ||
2646 | 656 | to use patches in the first place, it's important to keep this | ||
2647 | 657 | behavior in mind. | ||
2648 | 658 | |||
2649 | 659 | Patching of the original object only takes place when the mocker | ||
2650 | 660 | is put on replay mode, and the patched object will be restored | ||
2651 | 661 | to its original state once the L{restore()} method is called | ||
2652 | 662 | (explicitly, or implicitly with alternative conventions, such as | ||
2653 | 663 | a C{with mocker:} block, or a MockerTestCase class). | ||
2654 | 664 | """ | ||
2655 | 665 | if spec is True: | ||
2656 | 666 | spec = object | ||
2657 | 667 | patcher = Patcher() | ||
2658 | 668 | event = self._get_replay_restore_event() | ||
2659 | 669 | event.add_task(patcher) | ||
2660 | 670 | mock = Mock(self, object=object, patcher=patcher, | ||
2661 | 671 | passthrough=True, spec=spec) | ||
2662 | 672 | object.__mocker_mock__ = mock | ||
2663 | 673 | return mock | ||
2664 | 674 | |||
2665 | 675 | def act(self, path): | ||
2666 | 676 | """This is called by mock objects whenever something happens to them. | ||
2667 | 677 | |||
2668 | 678 | This method is part of the implementation between the mocker | ||
2669 | 679 | and mock objects. | ||
2670 | 680 | """ | ||
2671 | 681 | if self._recording: | ||
2672 | 682 | event = self.add_event(Event(path)) | ||
2673 | 683 | for recorder in self._recorders: | ||
2674 | 684 | recorder(self, event) | ||
2675 | 685 | return Mock(self, path) | ||
2676 | 686 | else: | ||
2677 | 687 | # First run events that may run, then run unsatisfied events, then | ||
2678 | 688 | # ones not previously run. We put the index in the ordering tuple | ||
2679 | 689 | # instead of the actual event because we want a stable sort | ||
2680 | 690 | # (ordering between 2 events is undefined). | ||
2681 | 691 | events = self._events | ||
2682 | 692 | order = [(events[i].satisfied()*2 + events[i].has_run(), i) | ||
2683 | 693 | for i in range(len(events))] | ||
2684 | 694 | order.sort() | ||
2685 | 695 | postponed = None | ||
2686 | 696 | for weight, i in order: | ||
2687 | 697 | event = events[i] | ||
2688 | 698 | if event.matches(path): | ||
2689 | 699 | if event.may_run(path): | ||
2690 | 700 | return event.run(path) | ||
2691 | 701 | elif postponed is None: | ||
2692 | 702 | postponed = event | ||
2693 | 703 | if postponed is not None: | ||
2694 | 704 | return postponed.run(path) | ||
2695 | 705 | raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path) | ||
2696 | 706 | |||
2697 | 707 | def get_recorders(cls, self): | ||
2698 | 708 | """Return recorders associated with this mocker class or instance. | ||
2699 | 709 | |||
2700 | 710 | This method may be called on mocker instances and also on mocker | ||
2701 | 711 | classes. See the L{add_recorder()} method for more information. | ||
2702 | 712 | """ | ||
2703 | 713 | return (self or cls)._recorders[:] | ||
2704 | 714 | get_recorders = classinstancemethod(get_recorders) | ||
2705 | 715 | |||
2706 | 716 | def add_recorder(cls, self, recorder): | ||
2707 | 717 | """Add a recorder to this mocker class or instance. | ||
2708 | 718 | |||
2709 | 719 | @param recorder: Callable accepting C{(mocker, event)} as parameters. | ||
2710 | 720 | |||
2711 | 721 | This is part of the implementation of mocker. | ||
2712 | 722 | |||
2713 | 723 | All registered recorders are called for translating events that | ||
2714 | 724 | happen during recording into expectations to be met once the state | ||
2715 | 725 | is switched to replay mode. | ||
2716 | 726 | |||
2717 | 727 | This method may be called on mocker instances and also on mocker | ||
2718 | 728 | classes. When called on a class, the recorder will be used by | ||
2719 | 729 | all instances, and also inherited on subclassing. When called on | ||
2720 | 730 | instances, the recorder is added only to the given instance. | ||
2721 | 731 | """ | ||
2722 | 732 | (self or cls)._recorders.append(recorder) | ||
2723 | 733 | return recorder | ||
2724 | 734 | add_recorder = classinstancemethod(add_recorder) | ||
2725 | 735 | |||
2726 | 736 | def remove_recorder(cls, self, recorder): | ||
2727 | 737 | """Remove the given recorder from this mocker class or instance. | ||
2728 | 738 | |||
2729 | 739 | This method may be called on mocker classes and also on mocker | ||
2730 | 740 | instances. See the L{add_recorder()} method for more information. | ||
2731 | 741 | """ | ||
2732 | 742 | (self or cls)._recorders.remove(recorder) | ||
2733 | 743 | remove_recorder = classinstancemethod(remove_recorder) | ||
2734 | 744 | |||
2735 | 745 | def result(self, value): | ||
2736 | 746 | """Make the last recorded event return the given value on replay. | ||
2737 | 747 | |||
2738 | 748 | @param value: Object to be returned when the event is replayed. | ||
2739 | 749 | """ | ||
2740 | 750 | self.call(lambda *args, **kwargs: value) | ||
2741 | 751 | |||
2742 | 752 | def generate(self, sequence): | ||
2743 | 753 | """Last recorded event will return a generator with the given sequence. | ||
2744 | 754 | |||
2745 | 755 | @param sequence: Sequence of values to be generated. | ||
2746 | 756 | """ | ||
2747 | 757 | def generate(*args, **kwargs): | ||
2748 | 758 | for value in sequence: | ||
2749 | 759 | yield value | ||
2750 | 760 | self.call(generate) | ||
2751 | 761 | |||
2752 | 762 | def throw(self, exception): | ||
2753 | 763 | """Make the last recorded event raise the given exception on replay. | ||
2754 | 764 | |||
2755 | 765 | @param exception: Class or instance of exception to be raised. | ||
2756 | 766 | """ | ||
2757 | 767 | def raise_exception(*args, **kwargs): | ||
2758 | 768 | raise exception | ||
2759 | 769 | self.call(raise_exception) | ||
2760 | 770 | |||
2761 | 771 | def call(self, func): | ||
2762 | 772 | """Make the last recorded event cause the given function to be called. | ||
2763 | 773 | |||
2764 | 774 | @param func: Function to be called. | ||
2765 | 775 | |||
2766 | 776 | The result of the function will be used as the event result. | ||
2767 | 777 | """ | ||
2768 | 778 | self._events[-1].add_task(FunctionRunner(func)) | ||
2769 | 779 | |||
2770 | 780 | def count(self, min, max=False): | ||
2771 | 781 | """Last recorded event must be replayed between min and max times. | ||
2772 | 782 | |||
2773 | 783 | @param min: Minimum number of times that the event must happen. | ||
2774 | 784 | @param max: Maximum number of times that the event must happen. If | ||
2775 | 785 | not given, it defaults to the same value of the C{min} | ||
2776 | 786 | parameter. If set to None, there is no upper limit, and | ||
2777 | 787 | the expectation is met as long as it happens at least | ||
2778 | 788 | C{min} times. | ||
2779 | 789 | """ | ||
2780 | 790 | event = self._events[-1] | ||
2781 | 791 | for task in event.get_tasks(): | ||
2782 | 792 | if isinstance(task, RunCounter): | ||
2783 | 793 | event.remove_task(task) | ||
2784 | 794 | event.add_task(RunCounter(min, max)) | ||
2785 | 795 | |||
2786 | 796 | def is_ordering(self): | ||
2787 | 797 | """Return true if all events are being ordered. | ||
2788 | 798 | |||
2789 | 799 | See the L{order()} method. | ||
2790 | 800 | """ | ||
2791 | 801 | return self._ordering | ||
2792 | 802 | |||
2793 | 803 | def unorder(self): | ||
2794 | 804 | """Disable the ordered mode. | ||
2795 | 805 | |||
2796 | 806 | See the L{order()} method for more information. | ||
2797 | 807 | """ | ||
2798 | 808 | self._ordering = False | ||
2799 | 809 | self._last_orderer = None | ||
2800 | 810 | |||
2801 | 811 | def order(self, *path_holders): | ||
2802 | 812 | """Create an expectation of order between two or more events. | ||
2803 | 813 | |||
2804 | 814 | @param path_holders: Objects returned as the result of recorded events. | ||
2805 | 815 | |||
2806 | 816 | By default, mocker won't force events to happen precisely in | ||
2807 | 817 | the order they were recorded. Calling this method will change | ||
2808 | 818 | this behavior so that events will only match if reproduced in | ||
2809 | 819 | the correct order. | ||
2810 | 820 | |||
2811 | 821 | There are two ways in which this method may be used. Which one | ||
2812 | 822 | is used in a given occasion depends only on convenience. | ||
2813 | 823 | |||
2814 | 824 | If no arguments are passed, the mocker will be put in a mode where | ||
2815 | 825 | all the recorded events following the method call will only be met | ||
2816 | 826 | if they happen in order. When that's used, the mocker may be put | ||
2817 | 827 | back in unordered mode by calling the L{unorder()} method, or by | ||
2818 | 828 | using a 'with' block, like so:: | ||
2819 | 829 | |||
2820 | 830 | with mocker.ordered(): | ||
2821 | 831 | <record events> | ||
2822 | 832 | |||
2823 | 833 | In this case, only expressions in <record events> will be ordered, | ||
2824 | 834 | and the mocker will be back in unordered mode after the 'with' block. | ||
2825 | 835 | |||
2826 | 836 | The second way to use it is by specifying precisely which events | ||
2827 | 837 | should be ordered. As an example:: | ||
2828 | 838 | |||
2829 | 839 | mock = mocker.mock() | ||
2830 | 840 | expr1 = mock.hello() | ||
2831 | 841 | expr2 = mock.world | ||
2832 | 842 | expr3 = mock.x.y.z | ||
2833 | 843 | mocker.order(expr1, expr2, expr3) | ||
2834 | 844 | |||
2835 | 845 | This method of ordering only works when the expression returns | ||
2836 | 846 | another object. | ||
2837 | 847 | |||
2838 | 848 | Also check the L{after()} and L{before()} methods, which are | ||
2839 | 849 | alternative ways to perform this. | ||
2840 | 850 | """ | ||
2841 | 851 | if not path_holders: | ||
2842 | 852 | self._ordering = True | ||
2843 | 853 | return OrderedContext(self) | ||
2844 | 854 | |||
2845 | 855 | last_orderer = None | ||
2846 | 856 | for path_holder in path_holders: | ||
2847 | 857 | if type(path_holder) is Path: | ||
2848 | 858 | path = path_holder | ||
2849 | 859 | else: | ||
2850 | 860 | path = path_holder.__mocker_path__ | ||
2851 | 861 | for event in self._events: | ||
2852 | 862 | if event.path is path: | ||
2853 | 863 | for task in event.get_tasks(): | ||
2854 | 864 | if isinstance(task, Orderer): | ||
2855 | 865 | orderer = task | ||
2856 | 866 | break | ||
2857 | 867 | else: | ||
2858 | 868 | orderer = Orderer(path) | ||
2859 | 869 | event.add_task(orderer) | ||
2860 | 870 | if last_orderer: | ||
2861 | 871 | orderer.add_dependency(last_orderer) | ||
2862 | 872 | last_orderer = orderer | ||
2863 | 873 | break | ||
2864 | 874 | |||
2865 | 875 | def after(self, *path_holders): | ||
2866 | 876 | """Last recorded event must happen after events referred to. | ||
2867 | 877 | |||
2868 | 878 | @param path_holders: Objects returned as the result of recorded events | ||
2869 | 879 | which should happen before the last recorded event | ||
2870 | 880 | |||
2871 | 881 | As an example, the idiom:: | ||
2872 | 882 | |||
2873 | 883 | expect(mock.x).after(mock.y, mock.z) | ||
2874 | 884 | |||
2875 | 885 | is an alternative way to say:: | ||
2876 | 886 | |||
2877 | 887 | expr_x = mock.x | ||
2878 | 888 | expr_y = mock.y | ||
2879 | 889 | expr_z = mock.z | ||
2880 | 890 | mocker.order(expr_y, expr_x) | ||
2881 | 891 | mocker.order(expr_z, expr_x) | ||
2882 | 892 | |||
2883 | 893 | See L{order()} for more information. | ||
2884 | 894 | """ | ||
2885 | 895 | last_path = self._events[-1].path | ||
2886 | 896 | for path_holder in path_holders: | ||
2887 | 897 | self.order(path_holder, last_path) | ||
2888 | 898 | |||
2889 | 899 | def before(self, *path_holders): | ||
2890 | 900 | """Last recorded event must happen before events referred to. | ||
2891 | 901 | |||
2892 | 902 | @param path_holders: Objects returned as the result of recorded events | ||
2893 | 903 | which should happen after the last recorded event | ||
2894 | 904 | |||
2895 | 905 | As an example, the idiom:: | ||
2896 | 906 | |||
2897 | 907 | expect(mock.x).before(mock.y, mock.z) | ||
2898 | 908 | |||
2899 | 909 | is an alternative way to say:: | ||
2900 | 910 | |||
2901 | 911 | expr_x = mock.x | ||
2902 | 912 | expr_y = mock.y | ||
2903 | 913 | expr_z = mock.z | ||
2904 | 914 | mocker.order(expr_x, expr_y) | ||
2905 | 915 | mocker.order(expr_x, expr_z) | ||
2906 | 916 | |||
2907 | 917 | See L{order()} for more information. | ||
2908 | 918 | """ | ||
2909 | 919 | last_path = self._events[-1].path | ||
2910 | 920 | for path_holder in path_holders: | ||
2911 | 921 | self.order(last_path, path_holder) | ||
2912 | 922 | |||
2913 | 923 | def nospec(self): | ||
2914 | 924 | """Don't check method specification of real object on last event. | ||
2915 | 925 | |||
2916 | 926 | By default, when using a mock created as the result of a call to | ||
2917 | 927 | L{proxy()}, L{replace()}, and C{patch()}, or when passing the spec | ||
2918 | 928 | attribute to the L{mock()} method, method calls on the given object | ||
2919 | 929 | are checked for correctness against the specification of the real | ||
2920 | 930 | object (or the explicitly provided spec). | ||
2921 | 931 | |||
2922 | 932 | This method will disable that check specifically for the last | ||
2923 | 933 | recorded event. | ||
2924 | 934 | """ | ||
2925 | 935 | event = self._events[-1] | ||
2926 | 936 | for task in event.get_tasks(): | ||
2927 | 937 | if isinstance(task, SpecChecker): | ||
2928 | 938 | event.remove_task(task) | ||
2929 | 939 | |||
2930 | 940 | def passthrough(self, result_callback=None): | ||
2931 | 941 | """Make the last recorded event run on the real object once seen. | ||
2932 | 942 | |||
2933 | 943 | @param result_callback: If given, this function will be called with | ||
2934 | 944 | the result of the *real* method call as the only argument. | ||
2935 | 945 | |||
2936 | 946 | This can only be used on proxies, as returned by the L{proxy()} | ||
2937 | 947 | and L{replace()} methods, or on mocks representing patched objects, | ||
2938 | 948 | as returned by the L{patch()} method. | ||
2939 | 949 | """ | ||
2940 | 950 | event = self._events[-1] | ||
2941 | 951 | if event.path.root_object is None: | ||
2942 | 952 | raise TypeError("Mock object isn't a proxy") | ||
2943 | 953 | event.add_task(PathExecuter(result_callback)) | ||
2944 | 954 | |||
2945 | 955 | def __enter__(self): | ||
2946 | 956 | """Enter in a 'with' context. This will run replay().""" | ||
2947 | 957 | self.replay() | ||
2948 | 958 | return self | ||
2949 | 959 | |||
2950 | 960 | def __exit__(self, type, value, traceback): | ||
2951 | 961 | """Exit from a 'with' context. | ||
2952 | 962 | |||
2953 | 963 | This will run restore() at all times, but will only run verify() | ||
2954 | 964 | if the 'with' block itself hasn't raised an exception. Exceptions | ||
2955 | 965 | in that block are never swallowed. | ||
2956 | 966 | """ | ||
2957 | 967 | self.restore() | ||
2958 | 968 | if type is None: | ||
2959 | 969 | self.verify() | ||
2960 | 970 | return False | ||
2961 | 971 | |||
2962 | 972 | def _get_replay_restore_event(self): | ||
2963 | 973 | """Return unique L{ReplayRestoreEvent}, creating if needed. | ||
2964 | 974 | |||
2965 | 975 | Some tasks only want to replay/restore. When that's the case, | ||
2966 | 976 | they shouldn't act on other events during replay. Also, they | ||
2967 | 977 | can all be put in a single event when that's the case. Thus, | ||
2968 | 978 | we add a single L{ReplayRestoreEvent} as the first element of | ||
2969 | 979 | the list. | ||
2970 | 980 | """ | ||
2971 | 981 | if not self._events or type(self._events[0]) != ReplayRestoreEvent: | ||
2972 | 982 | self._events.insert(0, ReplayRestoreEvent()) | ||
2973 | 983 | return self._events[0] | ||
2974 | 984 | |||
2975 | 985 | |||
2976 | 986 | class OrderedContext(object): | ||
2977 | 987 | |||
2978 | 988 | def __init__(self, mocker): | ||
2979 | 989 | self._mocker = mocker | ||
2980 | 990 | |||
2981 | 991 | def __enter__(self): | ||
2982 | 992 | return None | ||
2983 | 993 | |||
2984 | 994 | def __exit__(self, type, value, traceback): | ||
2985 | 995 | self._mocker.unorder() | ||
2986 | 996 | |||
2987 | 997 | |||
2988 | 998 | class Mocker(MockerBase): | ||
2989 | 999 | __doc__ = MockerBase.__doc__ | ||
2990 | 1000 | |||
2991 | 1001 | # Decorator to add recorders on the standard Mocker class. | ||
2992 | 1002 | recorder = Mocker.add_recorder | ||
2993 | 1003 | |||
2994 | 1004 | |||
2995 | 1005 | # -------------------------------------------------------------------- | ||
2996 | 1006 | # Mock object. | ||
2997 | 1007 | |||
2998 | 1008 | class Mock(object): | ||
2999 | 1009 | |||
3000 | 1010 | def __init__(self, mocker, path=None, name=None, spec=None, type=None, | ||
3001 | 1011 | object=None, passthrough=False, patcher=None, count=True): | ||
3002 | 1012 | self.__mocker__ = mocker | ||
3003 | 1013 | self.__mocker_path__ = path or Path(self, object) | ||
3004 | 1014 | self.__mocker_name__ = name | ||
3005 | 1015 | self.__mocker_spec__ = spec | ||
3006 | 1016 | self.__mocker_object__ = object | ||
3007 | 1017 | self.__mocker_passthrough__ = passthrough | ||
3008 | 1018 | self.__mocker_patcher__ = patcher | ||
3009 | 1019 | self.__mocker_replace__ = False | ||
3010 | 1020 | self.__mocker_type__ = type | ||
3011 | 1021 | self.__mocker_count__ = count | ||
3012 | 1022 | |||
3013 | 1023 | def __mocker_act__(self, kind, args=(), kwargs={}, object=None): | ||
3014 | 1024 | if self.__mocker_name__ is None: | ||
3015 | 1025 | self.__mocker_name__ = find_object_name(self, 2) | ||
3016 | 1026 | action = Action(kind, args, kwargs, self.__mocker_path__) | ||
3017 | 1027 | path = self.__mocker_path__ + action | ||
3018 | 1028 | if object is not None: | ||
3019 | 1029 | path.root_object = object | ||
3020 | 1030 | try: | ||
3021 | 1031 | return self.__mocker__.act(path) | ||
3022 | 1032 | except MatchError, exception: | ||
3023 | 1033 | root_mock = path.root_mock | ||
3024 | 1034 | if (path.root_object is not None and | ||
3025 | 1035 | root_mock.__mocker_passthrough__): | ||
3026 | 1036 | return path.execute(path.root_object) | ||
3027 | 1037 | # Reinstantiate to show raise statement on traceback, and | ||
3028 | 1038 | # also to make the traceback shown shorter. | ||
3029 | 1039 | raise MatchError(str(exception)) | ||
3030 | 1040 | except AssertionError, e: | ||
3031 | 1041 | lines = str(e).splitlines() | ||
3032 | 1042 | message = [ERROR_PREFIX + "Unmet expectation:", ""] | ||
3033 | 1043 | message.append("=> " + lines.pop(0)) | ||
3034 | 1044 | message.extend([" " + line for line in lines]) | ||
3035 | 1045 | message.append("") | ||
3036 | 1046 | raise AssertionError(os.linesep.join(message)) | ||
3037 | 1047 | |||
3038 | 1048 | def __getattribute__(self, name): | ||
3039 | 1049 | if name.startswith("__mocker_"): | ||
3040 | 1050 | return super(Mock, self).__getattribute__(name) | ||
3041 | 1051 | if name == "__class__": | ||
3042 | 1052 | if self.__mocker__.is_recording() or self.__mocker_type__ is None: | ||
3043 | 1053 | return type(self) | ||
3044 | 1054 | return self.__mocker_type__ | ||
3045 | 1055 | return self.__mocker_act__("getattr", (name,)) | ||
3046 | 1056 | |||
3047 | 1057 | def __setattr__(self, name, value): | ||
3048 | 1058 | if name.startswith("__mocker_"): | ||
3049 | 1059 | return super(Mock, self).__setattr__(name, value) | ||
3050 | 1060 | return self.__mocker_act__("setattr", (name, value)) | ||
3051 | 1061 | |||
3052 | 1062 | def __delattr__(self, name): | ||
3053 | 1063 | return self.__mocker_act__("delattr", (name,)) | ||
3054 | 1064 | |||
3055 | 1065 | def __call__(self, *args, **kwargs): | ||
3056 | 1066 | return self.__mocker_act__("call", args, kwargs) | ||
3057 | 1067 | |||
3058 | 1068 | def __contains__(self, value): | ||
3059 | 1069 | return self.__mocker_act__("contains", (value,)) | ||
3060 | 1070 | |||
3061 | 1071 | def __getitem__(self, key): | ||
3062 | 1072 | return self.__mocker_act__("getitem", (key,)) | ||
3063 | 1073 | |||
3064 | 1074 | def __setitem__(self, key, value): | ||
3065 | 1075 | return self.__mocker_act__("setitem", (key, value)) | ||
3066 | 1076 | |||
3067 | 1077 | def __delitem__(self, key): | ||
3068 | 1078 | return self.__mocker_act__("delitem", (key,)) | ||
3069 | 1079 | |||
3070 | 1080 | def __len__(self): | ||
3071 | 1081 | # MatchError is turned on an AttributeError so that list() and | ||
3072 | 1082 | # friends act properly when trying to get length hints on | ||
3073 | 1083 | # something that doesn't offer them. | ||
3074 | 1084 | try: | ||
3075 | 1085 | result = self.__mocker_act__("len") | ||
3076 | 1086 | except MatchError, e: | ||
3077 | 1087 | raise AttributeError(str(e)) | ||
3078 | 1088 | if type(result) is Mock: | ||
3079 | 1089 | return 0 | ||
3080 | 1090 | return result | ||
3081 | 1091 | |||
3082 | 1092 | def __nonzero__(self): | ||
3083 | 1093 | try: | ||
3084 | 1094 | return self.__mocker_act__("nonzero") | ||
3085 | 1095 | except MatchError, e: | ||
3086 | 1096 | return True | ||
3087 | 1097 | |||
3088 | 1098 | def __iter__(self): | ||
3089 | 1099 | # XXX On py3k, when next() becomes __next__(), we'll be able | ||
3090 | 1100 | # to return the mock itself because it will be considered | ||
3091 | 1101 | # an iterator (we'll be mocking __next__ as well, which we | ||
3092 | 1102 | # can't now). | ||
3093 | 1103 | result = self.__mocker_act__("iter") | ||
3094 | 1104 | if type(result) is Mock: | ||
3095 | 1105 | return iter([]) | ||
3096 | 1106 | return result | ||
3097 | 1107 | |||
3098 | 1108 | # When adding a new action kind here, also add support for it on | ||
3099 | 1109 | # Action.execute() and Path.__str__(). | ||
3100 | 1110 | |||
3101 | 1111 | |||
3102 | 1112 | def find_object_name(obj, depth=0): | ||
3103 | 1113 | """Try to detect how the object is named on a previous scope.""" | ||
3104 | 1114 | try: | ||
3105 | 1115 | frame = sys._getframe(depth+1) | ||
3106 | 1116 | except: | ||
3107 | 1117 | return None | ||
3108 | 1118 | for name, frame_obj in frame.f_locals.iteritems(): | ||
3109 | 1119 | if frame_obj is obj: | ||
3110 | 1120 | return name | ||
3111 | 1121 | self = frame.f_locals.get("self") | ||
3112 | 1122 | if self is not None: | ||
3113 | 1123 | try: | ||
3114 | 1124 | items = list(self.__dict__.iteritems()) | ||
3115 | 1125 | except: | ||
3116 | 1126 | pass | ||
3117 | 1127 | else: | ||
3118 | 1128 | for name, self_obj in items: | ||
3119 | 1129 | if self_obj is obj: | ||
3120 | 1130 | return name | ||
3121 | 1131 | return None | ||
3122 | 1132 | |||
3123 | 1133 | |||
3124 | 1134 | # -------------------------------------------------------------------- | ||
3125 | 1135 | # Action and path. | ||
3126 | 1136 | |||
3127 | 1137 | class Action(object): | ||
3128 | 1138 | |||
3129 | 1139 | def __init__(self, kind, args, kwargs, path=None): | ||
3130 | 1140 | self.kind = kind | ||
3131 | 1141 | self.args = args | ||
3132 | 1142 | self.kwargs = kwargs | ||
3133 | 1143 | self.path = path | ||
3134 | 1144 | self._execute_cache = {} | ||
3135 | 1145 | |||
3136 | 1146 | def __repr__(self): | ||
3137 | 1147 | if self.path is None: | ||
3138 | 1148 | return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs) | ||
3139 | 1149 | return "Action(%r, %r, %r, %r)" % \ | ||
3140 | 1150 | (self.kind, self.args, self.kwargs, self.path) | ||
3141 | 1151 | |||
3142 | 1152 | def __eq__(self, other): | ||
3143 | 1153 | return (self.kind == other.kind and | ||
3144 | 1154 | self.args == other.args and | ||
3145 | 1155 | self.kwargs == other.kwargs) | ||
3146 | 1156 | |||
3147 | 1157 | def __ne__(self, other): | ||
3148 | 1158 | return not self.__eq__(other) | ||
3149 | 1159 | |||
3150 | 1160 | def matches(self, other): | ||
3151 | 1161 | return (self.kind == other.kind and | ||
3152 | 1162 | match_params(self.args, self.kwargs, other.args, other.kwargs)) | ||
3153 | 1163 | |||
3154 | 1164 | def execute(self, object): | ||
3155 | 1165 | # This caching scheme may fail if the object gets deallocated before | ||
3156 | 1166 | # the action, as the id might get reused. It's somewhat easy to fix | ||
3157 | 1167 | # that with a weakref callback. For our uses, though, the object | ||
3158 | 1168 | # should never get deallocated before the action itself, so we'll | ||
3159 | 1169 | # just keep it simple. | ||
3160 | 1170 | if id(object) in self._execute_cache: | ||
3161 | 1171 | return self._execute_cache[id(object)] | ||
3162 | 1172 | execute = getattr(object, "__mocker_execute__", None) | ||
3163 | 1173 | if execute is not None: | ||
3164 | 1174 | result = execute(self, object) | ||
3165 | 1175 | else: | ||
3166 | 1176 | kind = self.kind | ||
3167 | 1177 | if kind == "getattr": | ||
3168 | 1178 | result = getattr(object, self.args[0]) | ||
3169 | 1179 | elif kind == "setattr": | ||
3170 | 1180 | result = setattr(object, self.args[0], self.args[1]) | ||
3171 | 1181 | elif kind == "delattr": | ||
3172 | 1182 | result = delattr(object, self.args[0]) | ||
3173 | 1183 | elif kind == "call": | ||
3174 | 1184 | result = object(*self.args, **self.kwargs) | ||
3175 | 1185 | elif kind == "contains": | ||
3176 | 1186 | result = self.args[0] in object | ||
3177 | 1187 | elif kind == "getitem": | ||
3178 | 1188 | result = object[self.args[0]] | ||
3179 | 1189 | elif kind == "setitem": | ||
3180 | 1190 | result = object[self.args[0]] = self.args[1] | ||
3181 | 1191 | elif kind == "delitem": | ||
3182 | 1192 | del object[self.args[0]] | ||
3183 | 1193 | result = None | ||
3184 | 1194 | elif kind == "len": | ||
3185 | 1195 | result = len(object) | ||
3186 | 1196 | elif kind == "nonzero": | ||
3187 | 1197 | result = bool(object) | ||
3188 | 1198 | elif kind == "iter": | ||
3189 | 1199 | result = iter(object) | ||
3190 | 1200 | else: | ||
3191 | 1201 | raise RuntimeError("Don't know how to execute %r kind." % kind) | ||
3192 | 1202 | self._execute_cache[id(object)] = result | ||
3193 | 1203 | return result | ||
3194 | 1204 | |||
3195 | 1205 | |||
3196 | 1206 | class Path(object): | ||
3197 | 1207 | |||
3198 | 1208 | def __init__(self, root_mock, root_object=None, actions=()): | ||
3199 | 1209 | self.root_mock = root_mock | ||
3200 | 1210 | self.root_object = root_object | ||
3201 | 1211 | self.actions = tuple(actions) | ||
3202 | 1212 | self.__mocker_replace__ = False | ||
3203 | 1213 | |||
3204 | 1214 | def parent_path(self): | ||
3205 | 1215 | if not self.actions: | ||
3206 | 1216 | return None | ||
3207 | 1217 | return self.actions[-1].path | ||
3208 | 1218 | parent_path = property(parent_path) | ||
3209 | 1219 | |||
3210 | 1220 | def __add__(self, action): | ||
3211 | 1221 | """Return a new path which includes the given action at the end.""" | ||
3212 | 1222 | return self.__class__(self.root_mock, self.root_object, | ||
3213 | 1223 | self.actions + (action,)) | ||
3214 | 1224 | |||
3215 | 1225 | def __eq__(self, other): | ||
3216 | 1226 | """Verify if the two paths are equal. | ||
3217 | 1227 | |||
3218 | 1228 | Two paths are equal if they refer to the same mock object, and | ||
3219 | 1229 | have the actions with equal kind, args and kwargs. | ||
3220 | 1230 | """ | ||
3221 | 1231 | if (self.root_mock is not other.root_mock or | ||
3222 | 1232 | self.root_object is not other.root_object or | ||
3223 | 1233 | len(self.actions) != len(other.actions)): | ||
3224 | 1234 | return False | ||
3225 | 1235 | for action, other_action in zip(self.actions, other.actions): | ||
3226 | 1236 | if action != other_action: | ||
3227 | 1237 | return False | ||
3228 | 1238 | return True | ||
3229 | 1239 | |||
3230 | 1240 | def matches(self, other): | ||
3231 | 1241 | """Verify if the two paths are equivalent. | ||
3232 | 1242 | |||
3233 | 1243 | Two paths are equal if they refer to the same mock object, and | ||
3234 | 1244 | have the same actions performed on them. | ||
3235 | 1245 | """ | ||
3236 | 1246 | if (self.root_mock is not other.root_mock or | ||
3237 | 1247 | len(self.actions) != len(other.actions)): | ||
3238 | 1248 | return False | ||
3239 | 1249 | for action, other_action in zip(self.actions, other.actions): | ||
3240 | 1250 | if not action.matches(other_action): | ||
3241 | 1251 | return False | ||
3242 | 1252 | return True | ||
3243 | 1253 | |||
3244 | 1254 | def execute(self, object): | ||
3245 | 1255 | """Execute all actions sequentially on object, and return result. | ||
3246 | 1256 | """ | ||
3247 | 1257 | for action in self.actions: | ||
3248 | 1258 | object = action.execute(object) | ||
3249 | 1259 | return object | ||
3250 | 1260 | |||
3251 | 1261 | def __str__(self): | ||
3252 | 1262 | """Transform the path into a nice string such as obj.x.y('z').""" | ||
3253 | 1263 | result = self.root_mock.__mocker_name__ or "<mock>" | ||
3254 | 1264 | for action in self.actions: | ||
3255 | 1265 | if action.kind == "getattr": | ||
3256 | 1266 | result = "%s.%s" % (result, action.args[0]) | ||
3257 | 1267 | elif action.kind == "setattr": | ||
3258 | 1268 | result = "%s.%s = %r" % (result, action.args[0], action.args[1]) | ||
3259 | 1269 | elif action.kind == "delattr": | ||
3260 | 1270 | result = "del %s.%s" % (result, action.args[0]) | ||
3261 | 1271 | elif action.kind == "call": | ||
3262 | 1272 | args = [repr(x) for x in action.args] | ||
3263 | 1273 | items = list(action.kwargs.iteritems()) | ||
3264 | 1274 | items.sort() | ||
3265 | 1275 | for pair in items: | ||
3266 | 1276 | args.append("%s=%r" % pair) | ||
3267 | 1277 | result = "%s(%s)" % (result, ", ".join(args)) | ||
3268 | 1278 | elif action.kind == "contains": | ||
3269 | 1279 | result = "%r in %s" % (action.args[0], result) | ||
3270 | 1280 | elif action.kind == "getitem": | ||
3271 | 1281 | result = "%s[%r]" % (result, action.args[0]) | ||
3272 | 1282 | elif action.kind == "setitem": | ||
3273 | 1283 | result = "%s[%r] = %r" % (result, action.args[0], | ||
3274 | 1284 | action.args[1]) | ||
3275 | 1285 | elif action.kind == "delitem": | ||
3276 | 1286 | result = "del %s[%r]" % (result, action.args[0]) | ||
3277 | 1287 | elif action.kind == "len": | ||
3278 | 1288 | result = "len(%s)" % result | ||
3279 | 1289 | elif action.kind == "nonzero": | ||
3280 | 1290 | result = "bool(%s)" % result | ||
3281 | 1291 | elif action.kind == "iter": | ||
3282 | 1292 | result = "iter(%s)" % result | ||
3283 | 1293 | else: | ||
3284 | 1294 | raise RuntimeError("Don't know how to format kind %r" % | ||
3285 | 1295 | action.kind) | ||
3286 | 1296 | return result | ||
3287 | 1297 | |||
3288 | 1298 | |||
3289 | 1299 | class SpecialArgument(object): | ||
3290 | 1300 | """Base for special arguments for matching parameters.""" | ||
3291 | 1301 | |||
3292 | 1302 | def __init__(self, object=None): | ||
3293 | 1303 | self.object = object | ||
3294 | 1304 | |||
3295 | 1305 | def __repr__(self): | ||
3296 | 1306 | if self.object is None: | ||
3297 | 1307 | return self.__class__.__name__ | ||
3298 | 1308 | else: | ||
3299 | 1309 | return "%s(%r)" % (self.__class__.__name__, self.object) | ||
3300 | 1310 | |||
3301 | 1311 | def matches(self, other): | ||
3302 | 1312 | return True | ||
3303 | 1313 | |||
3304 | 1314 | def __eq__(self, other): | ||
3305 | 1315 | return type(other) == type(self) and self.object == other.object | ||
3306 | 1316 | |||
3307 | 1317 | |||
3308 | 1318 | class ANY(SpecialArgument): | ||
3309 | 1319 | """Matches any single argument.""" | ||
3310 | 1320 | |||
3311 | 1321 | ANY = ANY() | ||
3312 | 1322 | |||
3313 | 1323 | |||
3314 | 1324 | class ARGS(SpecialArgument): | ||
3315 | 1325 | """Matches zero or more positional arguments.""" | ||
3316 | 1326 | |||
3317 | 1327 | ARGS = ARGS() | ||
3318 | 1328 | |||
3319 | 1329 | |||
3320 | 1330 | class KWARGS(SpecialArgument): | ||
3321 | 1331 | """Matches zero or more keyword arguments.""" | ||
3322 | 1332 | |||
3323 | 1333 | KWARGS = KWARGS() | ||
3324 | 1334 | |||
3325 | 1335 | |||
3326 | 1336 | class IS(SpecialArgument): | ||
3327 | 1337 | |||
3328 | 1338 | def matches(self, other): | ||
3329 | 1339 | return self.object is other | ||
3330 | 1340 | |||
3331 | 1341 | def __eq__(self, other): | ||
3332 | 1342 | return type(other) == type(self) and self.object is other.object | ||
3333 | 1343 | |||
3334 | 1344 | |||
3335 | 1345 | class CONTAINS(SpecialArgument): | ||
3336 | 1346 | |||
3337 | 1347 | def matches(self, other): | ||
3338 | 1348 | try: | ||
3339 | 1349 | other.__contains__ | ||
3340 | 1350 | except AttributeError: | ||
3341 | 1351 | try: | ||
3342 | 1352 | iter(other) | ||
3343 | 1353 | except TypeError: | ||
3344 | 1354 | # If an object can't be iterated, and has no __contains__ | ||
3345 | 1355 | # hook, it'd blow up on the test below. We test this in | ||
3346 | 1356 | # advance to prevent catching more errors than we really | ||
3347 | 1357 | # want. | ||
3348 | 1358 | return False | ||
3349 | 1359 | return self.object in other | ||
3350 | 1360 | |||
3351 | 1361 | |||
3352 | 1362 | class IN(SpecialArgument): | ||
3353 | 1363 | |||
3354 | 1364 | def matches(self, other): | ||
3355 | 1365 | return other in self.object | ||
3356 | 1366 | |||
3357 | 1367 | |||
3358 | 1368 | class MATCH(SpecialArgument): | ||
3359 | 1369 | |||
3360 | 1370 | def matches(self, other): | ||
3361 | 1371 | return bool(self.object(other)) | ||
3362 | 1372 | |||
3363 | 1373 | def __eq__(self, other): | ||
3364 | 1374 | return type(other) == type(self) and self.object is other.object | ||
3365 | 1375 | |||
3366 | 1376 | |||
3367 | 1377 | def match_params(args1, kwargs1, args2, kwargs2): | ||
3368 | 1378 | """Match the two sets of parameters, considering special parameters.""" | ||
3369 | 1379 | |||
3370 | 1380 | has_args = ARGS in args1 | ||
3371 | 1381 | has_kwargs = KWARGS in args1 | ||
3372 | 1382 | |||
3373 | 1383 | if has_kwargs: | ||
3374 | 1384 | args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS] | ||
3375 | 1385 | elif len(kwargs1) != len(kwargs2): | ||
3376 | 1386 | return False | ||
3377 | 1387 | |||
3378 | 1388 | if not has_args and len(args1) != len(args2): | ||
3379 | 1389 | return False | ||
3380 | 1390 | |||
3381 | 1391 | # Either we have the same number of kwargs, or unknown keywords are | ||
3382 | 1392 | # accepted (KWARGS was used), so check just the ones in kwargs1. | ||
3383 | 1393 | for key, arg1 in kwargs1.iteritems(): | ||
3384 | 1394 | if key not in kwargs2: | ||
3385 | 1395 | return False | ||
3386 | 1396 | arg2 = kwargs2[key] | ||
3387 | 1397 | if isinstance(arg1, SpecialArgument): | ||
3388 | 1398 | if not arg1.matches(arg2): | ||
3389 | 1399 | return False | ||
3390 | 1400 | elif arg1 != arg2: | ||
3391 | 1401 | return False | ||
3392 | 1402 | |||
3393 | 1403 | # Keywords match. Now either we have the same number of | ||
3394 | 1404 | # arguments, or ARGS was used. If ARGS wasn't used, arguments | ||
3395 | 1405 | # must match one-on-one necessarily. | ||
3396 | 1406 | if not has_args: | ||
3397 | 1407 | for arg1, arg2 in zip(args1, args2): | ||
3398 | 1408 | if isinstance(arg1, SpecialArgument): | ||
3399 | 1409 | if not arg1.matches(arg2): | ||
3400 | 1410 | return False | ||
3401 | 1411 | elif arg1 != arg2: | ||
3402 | 1412 | return False | ||
3403 | 1413 | return True | ||
3404 | 1414 | |||
3405 | 1415 | # Easy choice. Keywords are matching, and anything on args is accepted. | ||
3406 | 1416 | if (ARGS,) == args1: | ||
3407 | 1417 | return True | ||
3408 | 1418 | |||
3409 | 1419 | # We have something different there. If we don't have positional | ||
3410 | 1420 | # arguments on the original call, it can't match. | ||
3411 | 1421 | if not args2: | ||
3412 | 1422 | # Unless we have just several ARGS (which is bizarre, but..). | ||
3413 | 1423 | for arg1 in args1: | ||
3414 | 1424 | if arg1 is not ARGS: | ||
3415 | 1425 | return False | ||
3416 | 1426 | return True | ||
3417 | 1427 | |||
3418 | 1428 | # Ok, all bets are lost. We have to actually do the more expensive | ||
3419 | 1429 | # matching. This is an algorithm based on the idea of the Levenshtein | ||
3420 | 1430 | # Distance between two strings, but heavily hacked for this purpose. | ||
3421 | 1431 | args2l = len(args2) | ||
3422 | 1432 | if args1[0] is ARGS: | ||
3423 | 1433 | args1 = args1[1:] | ||
3424 | 1434 | array = [0]*args2l | ||
3425 | 1435 | else: | ||
3426 | 1436 | array = [1]*args2l | ||
3427 | 1437 | for i in range(len(args1)): | ||
3428 | 1438 | last = array[0] | ||
3429 | 1439 | if args1[i] is ARGS: | ||
3430 | 1440 | for j in range(1, args2l): | ||
3431 | 1441 | last, array[j] = array[j], min(array[j-1], array[j], last) | ||
3432 | 1442 | else: | ||
3433 | 1443 | array[0] = i or int(args1[i] != args2[0]) | ||
3434 | 1444 | for j in range(1, args2l): | ||
3435 | 1445 | last, array[j] = array[j], last or int(args1[i] != args2[j]) | ||
3436 | 1446 | if 0 not in array: | ||
3437 | 1447 | return False | ||
3438 | 1448 | if array[-1] != 0: | ||
3439 | 1449 | return False | ||
3440 | 1450 | return True | ||
3441 | 1451 | |||
3442 | 1452 | |||
3443 | 1453 | # -------------------------------------------------------------------- | ||
3444 | 1454 | # Event and task base. | ||
3445 | 1455 | |||
3446 | 1456 | class Event(object): | ||
3447 | 1457 | """Aggregation of tasks that keep track of a recorded action. | ||
3448 | 1458 | |||
3449 | 1459 | An event represents something that may or may not happen while the | ||
3450 | 1460 | mocked environment is running, such as an attribute access, or a | ||
3451 | 1461 | method call. The event is composed of several tasks that are | ||
3452 | 1462 | orchestrated together to create a composed meaning for the event, | ||
3453 | 1463 | including for which actions it should be run, what happens when it | ||
3454 | 1464 | runs, and what's the expectations about the actions run. | ||
3455 | 1465 | """ | ||
3456 | 1466 | |||
3457 | 1467 | def __init__(self, path=None): | ||
3458 | 1468 | self.path = path | ||
3459 | 1469 | self._tasks = [] | ||
3460 | 1470 | self._has_run = False | ||
3461 | 1471 | |||
3462 | 1472 | def add_task(self, task): | ||
3463 | 1473 | """Add a new task to this taks.""" | ||
3464 | 1474 | self._tasks.append(task) | ||
3465 | 1475 | return task | ||
3466 | 1476 | |||
3467 | 1477 | def remove_task(self, task): | ||
3468 | 1478 | self._tasks.remove(task) | ||
3469 | 1479 | |||
3470 | 1480 | def get_tasks(self): | ||
3471 | 1481 | return self._tasks[:] | ||
3472 | 1482 | |||
3473 | 1483 | def matches(self, path): | ||
3474 | 1484 | """Return true if *all* tasks match the given path.""" | ||
3475 | 1485 | for task in self._tasks: | ||
3476 | 1486 | if not task.matches(path): | ||
3477 | 1487 | return False | ||
3478 | 1488 | return bool(self._tasks) | ||
3479 | 1489 | |||
3480 | 1490 | def has_run(self): | ||
3481 | 1491 | return self._has_run | ||
3482 | 1492 | |||
3483 | 1493 | def may_run(self, path): | ||
3484 | 1494 | """Verify if any task would certainly raise an error if run. | ||
3485 | 1495 | |||
3486 | 1496 | This will call the C{may_run()} method on each task and return | ||
3487 | 1497 | false if any of them returns false. | ||
3488 | 1498 | """ | ||
3489 | 1499 | for task in self._tasks: | ||
3490 | 1500 | if not task.may_run(path): | ||
3491 | 1501 | return False | ||
3492 | 1502 | return True | ||
3493 | 1503 | |||
3494 | 1504 | def run(self, path): | ||
3495 | 1505 | """Run all tasks with the given action. | ||
3496 | 1506 | |||
3497 | 1507 | @param path: The path of the expression run. | ||
3498 | 1508 | |||
3499 | 1509 | Running an event means running all of its tasks individually and in | ||
3500 | 1510 | order. An event should only ever be run if all of its tasks claim to | ||
3501 | 1511 | match the given action. | ||
3502 | 1512 | |||
3503 | 1513 | The result of this method will be the last result of a task | ||
3504 | 1514 | which isn't None, or None if they're all None. | ||
3505 | 1515 | """ | ||
3506 | 1516 | self._has_run = True | ||
3507 | 1517 | result = None | ||
3508 | 1518 | errors = [] | ||
3509 | 1519 | for task in self._tasks: | ||
3510 | 1520 | try: | ||
3511 | 1521 | task_result = task.run(path) | ||
3512 | 1522 | except AssertionError, e: | ||
3513 | 1523 | error = str(e) | ||
3514 | 1524 | if not error: | ||
3515 | 1525 | raise RuntimeError("Empty error message from %r" % task) | ||
3516 | 1526 | errors.append(error) | ||
3517 | 1527 | else: | ||
3518 | 1528 | if task_result is not None: | ||
3519 | 1529 | result = task_result | ||
3520 | 1530 | if errors: | ||
3521 | 1531 | message = [str(self.path)] | ||
3522 | 1532 | if str(path) != message[0]: | ||
3523 | 1533 | message.append("- Run: %s" % path) | ||
3524 | 1534 | for error in errors: | ||
3525 | 1535 | lines = error.splitlines() | ||
3526 | 1536 | message.append("- " + lines.pop(0)) | ||
3527 | 1537 | message.extend([" " + line for line in lines]) | ||
3528 | 1538 | raise AssertionError(os.linesep.join(message)) | ||
3529 | 1539 | return result | ||
3530 | 1540 | |||
3531 | 1541 | def satisfied(self): | ||
3532 | 1542 | """Return true if all tasks are satisfied. | ||
3533 | 1543 | |||
3534 | 1544 | Being satisfied means that there are no unmet expectations. | ||
3535 | 1545 | """ | ||
3536 | 1546 | for task in self._tasks: | ||
3537 | 1547 | try: | ||
3538 | 1548 | task.verify() | ||
3539 | 1549 | except AssertionError: | ||
3540 | 1550 | return False | ||
3541 | 1551 | return True | ||
3542 | 1552 | |||
3543 | 1553 | def verify(self): | ||
3544 | 1554 | """Run verify on all tasks. | ||
3545 | 1555 | |||
3546 | 1556 | The verify method is supposed to raise an AssertionError if the | ||
3547 | 1557 | task has unmet expectations, with a one-line explanation about | ||
3548 | 1558 | why this item is unmet. This method should be safe to be called | ||
3549 | 1559 | multiple times without side effects. | ||
3550 | 1560 | """ | ||
3551 | 1561 | errors = [] | ||
3552 | 1562 | for task in self._tasks: | ||
3553 | 1563 | try: | ||
3554 | 1564 | task.verify() | ||
3555 | 1565 | except AssertionError, e: | ||
3556 | 1566 | error = str(e) | ||
3557 | 1567 | if not error: | ||
3558 | 1568 | raise RuntimeError("Empty error message from %r" % task) | ||
3559 | 1569 | errors.append(error) | ||
3560 | 1570 | if errors: | ||
3561 | 1571 | message = [str(self.path)] | ||
3562 | 1572 | for error in errors: | ||
3563 | 1573 | lines = error.splitlines() | ||
3564 | 1574 | message.append("- " + lines.pop(0)) | ||
3565 | 1575 | message.extend([" " + line for line in lines]) | ||
3566 | 1576 | raise AssertionError(os.linesep.join(message)) | ||
3567 | 1577 | |||
3568 | 1578 | def replay(self): | ||
3569 | 1579 | """Put all tasks in replay mode.""" | ||
3570 | 1580 | self._has_run = False | ||
3571 | 1581 | for task in self._tasks: | ||
3572 | 1582 | task.replay() | ||
3573 | 1583 | |||
3574 | 1584 | def restore(self): | ||
3575 | 1585 | """Restore the state of all tasks.""" | ||
3576 | 1586 | for task in self._tasks: | ||
3577 | 1587 | task.restore() | ||
3578 | 1588 | |||
3579 | 1589 | |||
3580 | 1590 | class ReplayRestoreEvent(Event): | ||
3581 | 1591 | """Helper event for tasks which need replay/restore but shouldn't match.""" | ||
3582 | 1592 | |||
3583 | 1593 | def matches(self, path): | ||
3584 | 1594 | return False | ||
3585 | 1595 | |||
3586 | 1596 | |||
3587 | 1597 | class Task(object): | ||
3588 | 1598 | """Element used to track one specific aspect on an event. | ||
3589 | 1599 | |||
3590 | 1600 | A task is responsible for adding any kind of logic to an event. | ||
3591 | 1601 | Examples of that are counting the number of times the event was | ||
3592 | 1602 | made, verifying parameters if any, and so on. | ||
3593 | 1603 | """ | ||
3594 | 1604 | |||
3595 | 1605 | def matches(self, path): | ||
3596 | 1606 | """Return true if the task is supposed to be run for the given path. | ||
3597 | 1607 | """ | ||
3598 | 1608 | return True | ||
3599 | 1609 | |||
3600 | 1610 | def may_run(self, path): | ||
3601 | 1611 | """Return false if running this task would certainly raise an error.""" | ||
3602 | 1612 | return True | ||
3603 | 1613 | |||
3604 | 1614 | def run(self, path): | ||
3605 | 1615 | """Perform the task item, considering that the given action happened. | ||
3606 | 1616 | """ | ||
3607 | 1617 | |||
3608 | 1618 | def verify(self): | ||
3609 | 1619 | """Raise AssertionError if expectations for this item are unmet. | ||
3610 | 1620 | |||
3611 | 1621 | The verify method is supposed to raise an AssertionError if the | ||
3612 | 1622 | task has unmet expectations, with a one-line explanation about | ||
3613 | 1623 | why this item is unmet. This method should be safe to be called | ||
3614 | 1624 | multiple times without side effects. | ||
3615 | 1625 | """ | ||
3616 | 1626 | |||
3617 | 1627 | def replay(self): | ||
3618 | 1628 | """Put the task in replay mode. | ||
3619 | 1629 | |||
3620 | 1630 | Any expectations of the task should be reset. | ||
3621 | 1631 | """ | ||
3622 | 1632 | |||
3623 | 1633 | def restore(self): | ||
3624 | 1634 | """Restore any environmental changes made by the task. | ||
3625 | 1635 | |||
3626 | 1636 | Verify should continue to work after this is called. | ||
3627 | 1637 | """ | ||
3628 | 1638 | |||
3629 | 1639 | |||
3630 | 1640 | # -------------------------------------------------------------------- | ||
3631 | 1641 | # Task implementations. | ||
3632 | 1642 | |||
3633 | 1643 | class OnRestoreCaller(Task): | ||
3634 | 1644 | """Call a given callback when restoring.""" | ||
3635 | 1645 | |||
3636 | 1646 | def __init__(self, callback): | ||
3637 | 1647 | self._callback = callback | ||
3638 | 1648 | |||
3639 | 1649 | def restore(self): | ||
3640 | 1650 | self._callback() | ||
3641 | 1651 | |||
3642 | 1652 | |||
3643 | 1653 | class PathMatcher(Task): | ||
3644 | 1654 | """Match the action path against a given path.""" | ||
3645 | 1655 | |||
3646 | 1656 | def __init__(self, path): | ||
3647 | 1657 | self.path = path | ||
3648 | 1658 | |||
3649 | 1659 | def matches(self, path): | ||
3650 | 1660 | return self.path.matches(path) | ||
3651 | 1661 | |||
3652 | 1662 | def path_matcher_recorder(mocker, event): | ||
3653 | 1663 | event.add_task(PathMatcher(event.path)) | ||
3654 | 1664 | |||
3655 | 1665 | Mocker.add_recorder(path_matcher_recorder) | ||
3656 | 1666 | |||
3657 | 1667 | |||
3658 | 1668 | class RunCounter(Task): | ||
3659 | 1669 | """Task which verifies if the number of runs are within given boundaries. | ||
3660 | 1670 | """ | ||
3661 | 1671 | |||
3662 | 1672 | def __init__(self, min, max=False): | ||
3663 | 1673 | self.min = min | ||
3664 | 1674 | if max is None: | ||
3665 | 1675 | self.max = sys.maxint | ||
3666 | 1676 | elif max is False: | ||
3667 | 1677 | self.max = min | ||
3668 | 1678 | else: | ||
3669 | 1679 | self.max = max | ||
3670 | 1680 | self._runs = 0 | ||
3671 | 1681 | |||
3672 | 1682 | def replay(self): | ||
3673 | 1683 | self._runs = 0 | ||
3674 | 1684 | |||
3675 | 1685 | def may_run(self, path): | ||
3676 | 1686 | return self._runs < self.max | ||
3677 | 1687 | |||
3678 | 1688 | def run(self, path): | ||
3679 | 1689 | self._runs += 1 | ||
3680 | 1690 | if self._runs > self.max: | ||
3681 | 1691 | self.verify() | ||
3682 | 1692 | |||
3683 | 1693 | def verify(self): | ||
3684 | 1694 | if not self.min <= self._runs <= self.max: | ||
3685 | 1695 | if self._runs < self.min: | ||
3686 | 1696 | raise AssertionError("Performed fewer times than expected.") | ||
3687 | 1697 | raise AssertionError("Performed more times than expected.") | ||
3688 | 1698 | |||
3689 | 1699 | |||
3690 | 1700 | class ImplicitRunCounter(RunCounter): | ||
3691 | 1701 | """RunCounter inserted by default on any event. | ||
3692 | 1702 | |||
3693 | 1703 | This is a way to differentiate explicitly added counters and | ||
3694 | 1704 | implicit ones. | ||
3695 | 1705 | """ | ||
3696 | 1706 | |||
3697 | 1707 | def run_counter_recorder(mocker, event): | ||
3698 | 1708 | """Any event may be repeated once, unless disabled by default.""" | ||
3699 | 1709 | if event.path.root_mock.__mocker_count__: | ||
3700 | 1710 | event.add_task(ImplicitRunCounter(1)) | ||
3701 | 1711 | |||
3702 | 1712 | Mocker.add_recorder(run_counter_recorder) | ||
3703 | 1713 | |||
3704 | 1714 | def run_counter_removal_recorder(mocker, event): | ||
3705 | 1715 | """ | ||
3706 | 1716 | Events created by getattr actions which lead to other events | ||
3707 | 1717 | may be repeated any number of times. For that, we remove implicit | ||
3708 | 1718 | run counters of any getattr actions leading to the current one. | ||
3709 | 1719 | """ | ||
3710 | 1720 | parent_path = event.path.parent_path | ||
3711 | 1721 | for event in mocker.get_events()[::-1]: | ||
3712 | 1722 | if (event.path is parent_path and | ||
3713 | 1723 | event.path.actions[-1].kind == "getattr"): | ||
3714 | 1724 | for task in event.get_tasks(): | ||
3715 | 1725 | if type(task) is ImplicitRunCounter: | ||
3716 | 1726 | event.remove_task(task) | ||
3717 | 1727 | |||
3718 | 1728 | Mocker.add_recorder(run_counter_removal_recorder) | ||
3719 | 1729 | |||
3720 | 1730 | |||
3721 | 1731 | class MockReturner(Task): | ||
3722 | 1732 | """Return a mock based on the action path.""" | ||
3723 | 1733 | |||
3724 | 1734 | def __init__(self, mocker): | ||
3725 | 1735 | self.mocker = mocker | ||
3726 | 1736 | |||
3727 | 1737 | def run(self, path): | ||
3728 | 1738 | return Mock(self.mocker, path) | ||
3729 | 1739 | |||
3730 | 1740 | def mock_returner_recorder(mocker, event): | ||
3731 | 1741 | """Events that lead to other events must return mock objects.""" | ||
3732 | 1742 | parent_path = event.path.parent_path | ||
3733 | 1743 | for event in mocker.get_events(): | ||
3734 | 1744 | if event.path is parent_path: | ||
3735 | 1745 | for task in event.get_tasks(): | ||
3736 | 1746 | if isinstance(task, MockReturner): | ||
3737 | 1747 | break | ||
3738 | 1748 | else: | ||
3739 | 1749 | event.add_task(MockReturner(mocker)) | ||
3740 | 1750 | break | ||
3741 | 1751 | |||
3742 | 1752 | Mocker.add_recorder(mock_returner_recorder) | ||
3743 | 1753 | |||
3744 | 1754 | |||
3745 | 1755 | class FunctionRunner(Task): | ||
3746 | 1756 | """Task that runs a function everything it's run. | ||
3747 | 1757 | |||
3748 | 1758 | Arguments of the last action in the path are passed to the function, | ||
3749 | 1759 | and the function result is also returned. | ||
3750 | 1760 | """ | ||
3751 | 1761 | |||
3752 | 1762 | def __init__(self, func): | ||
3753 | 1763 | self._func = func | ||
3754 | 1764 | |||
3755 | 1765 | def run(self, path): | ||
3756 | 1766 | action = path.actions[-1] | ||
3757 | 1767 | return self._func(*action.args, **action.kwargs) | ||
3758 | 1768 | |||
3759 | 1769 | |||
3760 | 1770 | class PathExecuter(Task): | ||
3761 | 1771 | """Task that executes a path in the real object, and returns the result.""" | ||
3762 | 1772 | |||
3763 | 1773 | def __init__(self, result_callback=None): | ||
3764 | 1774 | self._result_callback = result_callback | ||
3765 | 1775 | |||
3766 | 1776 | def get_result_callback(self): | ||
3767 | 1777 | return self._result_callback | ||
3768 | 1778 | |||
3769 | 1779 | def run(self, path): | ||
3770 | 1780 | result = path.execute(path.root_object) | ||
3771 | 1781 | if self._result_callback is not None: | ||
3772 | 1782 | self._result_callback(result) | ||
3773 | 1783 | return result | ||
3774 | 1784 | |||
3775 | 1785 | |||
3776 | 1786 | class Orderer(Task): | ||
3777 | 1787 | """Task to establish an order relation between two events. | ||
3778 | 1788 | |||
3779 | 1789 | An orderer task will only match once all its dependencies have | ||
3780 | 1790 | been run. | ||
3781 | 1791 | """ | ||
3782 | 1792 | |||
3783 | 1793 | def __init__(self, path): | ||
3784 | 1794 | self.path = path | ||
3785 | 1795 | self._run = False | ||
3786 | 1796 | self._dependencies = [] | ||
3787 | 1797 | |||
3788 | 1798 | def replay(self): | ||
3789 | 1799 | self._run = False | ||
3790 | 1800 | |||
3791 | 1801 | def has_run(self): | ||
3792 | 1802 | return self._run | ||
3793 | 1803 | |||
3794 | 1804 | def may_run(self, path): | ||
3795 | 1805 | for dependency in self._dependencies: | ||
3796 | 1806 | if not dependency.has_run(): | ||
3797 | 1807 | return False | ||
3798 | 1808 | return True | ||
3799 | 1809 | |||
3800 | 1810 | def run(self, path): | ||
3801 | 1811 | for dependency in self._dependencies: | ||
3802 | 1812 | if not dependency.has_run(): | ||
3803 | 1813 | raise AssertionError("Should be after: %s" % dependency.path) | ||
3804 | 1814 | self._run = True | ||
3805 | 1815 | |||
3806 | 1816 | def add_dependency(self, orderer): | ||
3807 | 1817 | self._dependencies.append(orderer) | ||
3808 | 1818 | |||
3809 | 1819 | def get_dependencies(self): | ||
3810 | 1820 | return self._dependencies | ||
3811 | 1821 | |||
3812 | 1822 | |||
3813 | 1823 | class SpecChecker(Task): | ||
3814 | 1824 | """Task to check if arguments of the last action conform to a real method. | ||
3815 | 1825 | """ | ||
3816 | 1826 | |||
3817 | 1827 | def __init__(self, method): | ||
3818 | 1828 | self._method = method | ||
3819 | 1829 | self._unsupported = False | ||
3820 | 1830 | |||
3821 | 1831 | if method: | ||
3822 | 1832 | try: | ||
3823 | 1833 | self._args, self._varargs, self._varkwargs, self._defaults = \ | ||
3824 | 1834 | inspect.getargspec(method) | ||
3825 | 1835 | except TypeError: | ||
3826 | 1836 | self._unsupported = True | ||
3827 | 1837 | else: | ||
3828 | 1838 | if self._defaults is None: | ||
3829 | 1839 | self._defaults = () | ||
3830 | 1840 | if type(method) is type(self.run): | ||
3831 | 1841 | self._args = self._args[1:] | ||
3832 | 1842 | |||
3833 | 1843 | def get_method(self): | ||
3834 | 1844 | return self._method | ||
3835 | 1845 | |||
3836 | 1846 | def _raise(self, message): | ||
3837 | 1847 | spec = inspect.formatargspec(self._args, self._varargs, | ||
3838 | 1848 | self._varkwargs, self._defaults) | ||
3839 | 1849 | raise AssertionError("Specification is %s%s: %s" % | ||
3840 | 1850 | (self._method.__name__, spec, message)) | ||
3841 | 1851 | |||
3842 | 1852 | def verify(self): | ||
3843 | 1853 | if not self._method: | ||
3844 | 1854 | raise AssertionError("Method not found in real specification") | ||
3845 | 1855 | |||
3846 | 1856 | def may_run(self, path): | ||
3847 | 1857 | try: | ||
3848 | 1858 | self.run(path) | ||
3849 | 1859 | except AssertionError: | ||
3850 | 1860 | return False | ||
3851 | 1861 | return True | ||
3852 | 1862 | |||
3853 | 1863 | def run(self, path): | ||
3854 | 1864 | if not self._method: | ||
3855 | 1865 | raise AssertionError("Method not found in real specification") | ||
3856 | 1866 | if self._unsupported: | ||
3857 | 1867 | return # Can't check it. Happens with builtin functions. :-( | ||
3858 | 1868 | action = path.actions[-1] | ||
3859 | 1869 | obtained_len = len(action.args) | ||
3860 | 1870 | obtained_kwargs = action.kwargs.copy() | ||
3861 | 1871 | nodefaults_len = len(self._args) - len(self._defaults) | ||
3862 | 1872 | for i, name in enumerate(self._args): | ||
3863 | 1873 | if i < obtained_len and name in action.kwargs: | ||
3864 | 1874 | self._raise("%r provided twice" % name) | ||
3865 | 1875 | if (i >= obtained_len and i < nodefaults_len and | ||
3866 | 1876 | name not in action.kwargs): | ||
3867 | 1877 | self._raise("%r not provided" % name) | ||
3868 | 1878 | obtained_kwargs.pop(name, None) | ||
3869 | 1879 | if obtained_len > len(self._args) and not self._varargs: | ||
3870 | 1880 | self._raise("too many args provided") | ||
3871 | 1881 | if obtained_kwargs and not self._varkwargs: | ||
3872 | 1882 | self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs)) | ||
3873 | 1883 | |||
3874 | 1884 | def spec_checker_recorder(mocker, event): | ||
3875 | 1885 | spec = event.path.root_mock.__mocker_spec__ | ||
3876 | 1886 | if spec: | ||
3877 | 1887 | actions = event.path.actions | ||
3878 | 1888 | if len(actions) == 1: | ||
3879 | 1889 | if actions[0].kind == "call": | ||
3880 | 1890 | method = getattr(spec, "__call__", None) | ||
3881 | 1891 | event.add_task(SpecChecker(method)) | ||
3882 | 1892 | elif len(actions) == 2: | ||
3883 | 1893 | if actions[0].kind == "getattr" and actions[1].kind == "call": | ||
3884 | 1894 | method = getattr(spec, actions[0].args[0], None) | ||
3885 | 1895 | event.add_task(SpecChecker(method)) | ||
3886 | 1896 | |||
3887 | 1897 | Mocker.add_recorder(spec_checker_recorder) | ||
3888 | 1898 | |||
3889 | 1899 | |||
3890 | 1900 | class ProxyReplacer(Task): | ||
3891 | 1901 | """Task which installs and deinstalls proxy mocks. | ||
3892 | 1902 | |||
3893 | 1903 | This task will replace a real object by a mock in all dictionaries | ||
3894 | 1904 | found in the running interpreter via the garbage collecting system. | ||
3895 | 1905 | """ | ||
3896 | 1906 | |||
3897 | 1907 | def __init__(self, mock): | ||
3898 | 1908 | self.mock = mock | ||
3899 | 1909 | self.__mocker_replace__ = False | ||
3900 | 1910 | |||
3901 | 1911 | def replay(self): | ||
3902 | 1912 | global_replace(self.mock.__mocker_object__, self.mock) | ||
3903 | 1913 | |||
3904 | 1914 | def restore(self): | ||
3905 | 1915 | global_replace(self.mock, self.mock.__mocker_object__) | ||
3906 | 1916 | |||
3907 | 1917 | |||
3908 | 1918 | def global_replace(remove, install): | ||
3909 | 1919 | """Replace object 'remove' with object 'install' on all dictionaries.""" | ||
3910 | 1920 | for referrer in gc.get_referrers(remove): | ||
3911 | 1921 | if (type(referrer) is dict and | ||
3912 | 1922 | referrer.get("__mocker_replace__", True)): | ||
3913 | 1923 | for key, value in referrer.items(): | ||
3914 | 1924 | if value is remove: | ||
3915 | 1925 | referrer[key] = install | ||
3916 | 1926 | |||
3917 | 1927 | |||
3918 | 1928 | class Undefined(object): | ||
3919 | 1929 | |||
3920 | 1930 | def __repr__(self): | ||
3921 | 1931 | return "Undefined" | ||
3922 | 1932 | |||
3923 | 1933 | Undefined = Undefined() | ||
3924 | 1934 | |||
3925 | 1935 | |||
3926 | 1936 | class Patcher(Task): | ||
3927 | 1937 | |||
3928 | 1938 | def __init__(self): | ||
3929 | 1939 | super(Patcher, self).__init__() | ||
3930 | 1940 | self._monitored = {} # {kind: {id(object): object}} | ||
3931 | 1941 | self._patched = {} | ||
3932 | 1942 | |||
3933 | 1943 | def is_monitoring(self, obj, kind): | ||
3934 | 1944 | monitored = self._monitored.get(kind) | ||
3935 | 1945 | if monitored: | ||
3936 | 1946 | if id(obj) in monitored: | ||
3937 | 1947 | return True | ||
3938 | 1948 | cls = type(obj) | ||
3939 | 1949 | if issubclass(cls, type): | ||
3940 | 1950 | cls = obj | ||
3941 | 1951 | bases = set([id(base) for base in cls.__mro__]) | ||
3942 | 1952 | bases.intersection_update(monitored) | ||
3943 | 1953 | return bool(bases) | ||
3944 | 1954 | return False | ||
3945 | 1955 | |||
3946 | 1956 | def monitor(self, obj, kind): | ||
3947 | 1957 | if kind not in self._monitored: | ||
3948 | 1958 | self._monitored[kind] = {} | ||
3949 | 1959 | self._monitored[kind][id(obj)] = obj | ||
3950 | 1960 | |||
3951 | 1961 | def patch_attr(self, obj, attr, value): | ||
3952 | 1962 | original = obj.__dict__.get(attr, Undefined) | ||
3953 | 1963 | self._patched[id(obj), attr] = obj, attr, original | ||
3954 | 1964 | setattr(obj, attr, value) | ||
3955 | 1965 | |||
3956 | 1966 | def get_unpatched_attr(self, obj, attr): | ||
3957 | 1967 | cls = type(obj) | ||
3958 | 1968 | if issubclass(cls, type): | ||
3959 | 1969 | cls = obj | ||
3960 | 1970 | result = Undefined | ||
3961 | 1971 | for mro_cls in cls.__mro__: | ||
3962 | 1972 | key = (id(mro_cls), attr) | ||
3963 | 1973 | if key in self._patched: | ||
3964 | 1974 | result = self._patched[key][2] | ||
3965 | 1975 | if result is not Undefined: | ||
3966 | 1976 | break | ||
3967 | 1977 | elif attr in mro_cls.__dict__: | ||
3968 | 1978 | result = mro_cls.__dict__.get(attr, Undefined) | ||
3969 | 1979 | break | ||
3970 | 1980 | if isinstance(result, object) and hasattr(type(result), "__get__"): | ||
3971 | 1981 | if cls is obj: | ||
3972 | 1982 | obj = None | ||
3973 | 1983 | return result.__get__(obj, cls) | ||
3974 | 1984 | return result | ||
3975 | 1985 | |||
3976 | 1986 | def _get_kind_attr(self, kind): | ||
3977 | 1987 | if kind == "getattr": | ||
3978 | 1988 | return "__getattribute__" | ||
3979 | 1989 | return "__%s__" % kind | ||
3980 | 1990 | |||
3981 | 1991 | def replay(self): | ||
3982 | 1992 | for kind in self._monitored: | ||
3983 | 1993 | attr = self._get_kind_attr(kind) | ||
3984 | 1994 | seen = set() | ||
3985 | 1995 | for obj in self._monitored[kind].itervalues(): | ||
3986 | 1996 | cls = type(obj) | ||
3987 | 1997 | if issubclass(cls, type): | ||
3988 | 1998 | cls = obj | ||
3989 | 1999 | if cls not in seen: | ||
3990 | 2000 | seen.add(cls) | ||
3991 | 2001 | unpatched = getattr(cls, attr, Undefined) | ||
3992 | 2002 | self.patch_attr(cls, attr, | ||
3993 | 2003 | PatchedMethod(kind, unpatched, | ||
3994 | 2004 | self.is_monitoring)) | ||
3995 | 2005 | self.patch_attr(cls, "__mocker_execute__", | ||
3996 | 2006 | self.execute) | ||
3997 | 2007 | |||
3998 | 2008 | def restore(self): | ||
3999 | 2009 | for obj, attr, original in self._patched.itervalues(): | ||
4000 | 2010 | if original is Undefined: | ||
4001 | 2011 | delattr(obj, attr) | ||
4002 | 2012 | else: | ||
4003 | 2013 | setattr(obj, attr, original) | ||
4004 | 2014 | self._patched.clear() | ||
4005 | 2015 | |||
4006 | 2016 | def execute(self, action, object): | ||
4007 | 2017 | attr = self._get_kind_attr(action.kind) | ||
4008 | 2018 | unpatched = self.get_unpatched_attr(object, attr) | ||
4009 | 2019 | try: | ||
4010 | 2020 | return unpatched(*action.args, **action.kwargs) | ||
4011 | 2021 | except AttributeError: | ||
4012 | 2022 | if action.kind == "getattr": | ||
4013 | 2023 | # The normal behavior of Python is to try __getattribute__, | ||
4014 | 2024 | # and if it raises AttributeError, try __getattr__. We've | ||
4015 | 2025 | # tried the unpatched __getattribute__ above, and we'll now | ||
4016 | 2026 | # try __getattr__. | ||
4017 | 2027 | try: | ||
4018 | 2028 | __getattr__ = unpatched("__getattr__") | ||
4019 | 2029 | except AttributeError: | ||
4020 | 2030 | pass | ||
4021 | 2031 | else: | ||
4022 | 2032 | return __getattr__(*action.args, **action.kwargs) | ||
4023 | 2033 | raise | ||
4024 | 2034 | |||
4025 | 2035 | |||
4026 | 2036 | class PatchedMethod(object): | ||
4027 | 2037 | |||
4028 | 2038 | def __init__(self, kind, unpatched, is_monitoring): | ||
4029 | 2039 | self._kind = kind | ||
4030 | 2040 | self._unpatched = unpatched | ||
4031 | 2041 | self._is_monitoring = is_monitoring | ||
4032 | 2042 | |||
4033 | 2043 | def __get__(self, obj, cls=None): | ||
4034 | 2044 | object = obj or cls | ||
4035 | 2045 | if not self._is_monitoring(object, self._kind): | ||
4036 | 2046 | return self._unpatched.__get__(obj, cls) | ||
4037 | 2047 | def method(*args, **kwargs): | ||
4038 | 2048 | if self._kind == "getattr" and args[0].startswith("__mocker_"): | ||
4039 | 2049 | return self._unpatched.__get__(obj, cls)(args[0]) | ||
4040 | 2050 | mock = object.__mocker_mock__ | ||
4041 | 2051 | return mock.__mocker_act__(self._kind, args, kwargs, object) | ||
4042 | 2052 | return method | ||
4043 | 2053 | |||
4044 | 2054 | def __call__(self, obj, *args, **kwargs): | ||
4045 | 2055 | # At least with __getattribute__, Python seems to use *both* the | ||
4046 | 2056 | # descriptor API and also call the class attribute directly. It | ||
4047 | 2057 | # looks like an interpreter bug, or at least an undocumented | ||
4048 | 2058 | # inconsistency. | ||
4049 | 2059 | return self.__get__(obj)(*args, **kwargs) | ||
4050 | 2060 | |||
4051 | 2061 | |||
4052 | 2062 | def patcher_recorder(mocker, event): | ||
4053 | 2063 | mock = event.path.root_mock | ||
4054 | 2064 | if mock.__mocker_patcher__ and len(event.path.actions) == 1: | ||
4055 | 2065 | patcher = mock.__mocker_patcher__ | ||
4056 | 2066 | patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind) | ||
4057 | 2067 | |||
4058 | 2068 | Mocker.add_recorder(patcher_recorder) | ||
4059 | 0 | 2069 | ||
4060 | === added directory 'data' | |||
4061 | === added file 'data/couchdb.tmpl' | |||
4062 | --- data/couchdb.tmpl 1970-01-01 00:00:00 +0000 | |||
4063 | +++ data/couchdb.tmpl 2009-08-20 12:55:54 +0000 | |||
4064 | @@ -0,0 +1,38 @@ | |||
4065 | 1 | <!doctype html> | ||
4066 | 2 | <html> | ||
4067 | 3 | <head> | ||
4068 | 4 | <title>Your Desktop CouchDB</title> | ||
4069 | 5 | <script> | ||
4070 | 6 | window.onload = function() { | ||
4071 | 7 | if (document.cookie == "yes") { | ||
4072 | 8 | location.href = document.getElementById("there").href; | ||
4073 | 9 | } | ||
4074 | 10 | document.getElementById("there").addEventListener("click", function(e) { | ||
4075 | 11 | document.cookie = "yes; expires=Thu, 31 Dec 2099 21:00:00 UTC"; | ||
4076 | 12 | }, false); | ||
4077 | 13 | var count = 30; | ||
4078 | 14 | setInterval(function() { | ||
4079 | 15 | count -= 1; | ||
4080 | 16 | if (count < 0) { | ||
4081 | 17 | location.href = document.getElementById("there").href; | ||
4082 | 18 | return; | ||
4083 | 19 | } | ||
4084 | 20 | document.getElementsByTagName("span")[0].innerHTML = count; | ||
4085 | 21 | }, 1000); | ||
4086 | 22 | } | ||
4087 | 23 | </script> | ||
4088 | 24 | </head> | ||
4089 | 25 | <body> | ||
4090 | 26 | <h1>Desktop CouchDB</h1> | ||
4091 | 27 | <p>Your desktop CouchDB is the data store for many of your applications. | ||
4092 | 28 | You can browse around it to see which data your applications are storing.</p> | ||
4093 | 29 | <p>You should bookmark this page (by going to <strong>Bookmarks > Bookmark | ||
4094 | 30 | This Page</strong> or pressing <strong>Ctrl-D</strong>) so you can easily | ||
4095 | 31 | come back to browse your CouchDB again.</p> | ||
4096 | 32 | <p>Don't bookmark the CouchDB page itself, because its location may change!</p> | ||
4097 | 33 | <p>Taking you to your Desktop CouchDB in <span>30</span> seconds... | ||
4098 | 34 | <a id="there" href="http://[[COUCHDB_USERNAME]]:[[COUCHDB_PASSWORD]]@localhost:[[COUCHDB_PORT]]/_utils">take me | ||
4099 | 35 | there straight away from now on</a> (remember to bookmark this page first!)</p> | ||
4100 | 36 | </body> | ||
4101 | 37 | </html> | ||
4102 | 38 | |||
4103 | 0 | 39 | ||
4104 | === added directory 'debian' | |||
4105 | === added file 'debian/changelog' | |||
4106 | --- debian/changelog 1970-01-01 00:00:00 +0000 | |||
4107 | +++ debian/changelog 2009-09-14 23:33:14 +0000 | |||
4108 | @@ -0,0 +1,102 @@ | |||
4109 | 1 | desktopcouch (0.4-0ubuntu1) karmic; urgency=low | ||
4110 | 2 | |||
4111 | 3 | * Packaging: desktopcouch-tools installed by default in Karmic (LP: #427421) | ||
4112 | 4 | * Forcing desktopcouch auth on. (LP: #427446) | ||
4113 | 5 | * Requiring new version of couchdb that supports authentication properly. | ||
4114 | 6 | * Pairing updates couchdb replication system. (LP: #397663) | ||
4115 | 7 | * Added pairing of desktop Couches to desktopcouch-tools (LP: #404087) | ||
4116 | 8 | * Admin users in the system couchdb are no longer inherited by desktopcouch | ||
4117 | 9 | couchdbs (LP: #424330) | ||
4118 | 10 | * Fixed failing tests in desktopcouch (LP: #405612) | ||
4119 | 11 | * Creating login details on initial desktopcouch setup (LP: #416413) | ||
4120 | 12 | * Starting replication to paired servers on desktopcouch startup. | ||
4121 | 13 | (LP: #416581) | ||
4122 | 14 | * Unpaired couchdb peers are reconciled with replication. (LP: #424386) | ||
4123 | 15 | * At pairing time, changing couchdb pairing address to public. (LP: #419969) | ||
4124 | 16 | * In replication daemon, verifying local couchdb bind address is not 127/8 . | ||
4125 | 17 | (LP: #419973) | ||
4126 | 18 | * getPort no longer suceeds when desktopcouch isn't running. (LP: #422127) | ||
4127 | 19 | |||
4128 | 20 | -- Chad Miller <chad.miller@canonical.com> Mon, 14 Sep 2009 19:24:08 -0400 | ||
4129 | 21 | |||
4130 | 22 | desktopcouch (0.3.1-0ubuntu1) karmic; urgency=low | ||
4131 | 23 | |||
4132 | 24 | [Ken VanDine] | ||
4133 | 25 | * debian/control | ||
4134 | 26 | - Added depends on python-desktopcouch-records. (LP: #422179) | ||
4135 | 27 | [Chad Miller] | ||
4136 | 28 | * New upstream release. | ||
4137 | 29 | * Changed Vcs-Bzr links in control file. | ||
4138 | 30 | * Changed version dependency on couchdb. | ||
4139 | 31 | * Converting to a full-blown source-package branch. | ||
4140 | 32 | * Fix getPort failure. (LP: #420911, LP: #422127) | ||
4141 | 33 | * Check couchdb bind-port in replicator. (LP: #419973) | ||
4142 | 34 | * Change couchdb bind-port in pairing tool. (LP: #419969) | ||
4143 | 35 | |||
4144 | 36 | -- Chad Miller <chad.miller@canonical.com> Tue, 01 Sep 2009 11:57:25 -0400 | ||
4145 | 37 | |||
4146 | 38 | desktopcouch (0.3-0ubuntu1) karmic; urgency=low | ||
4147 | 39 | |||
4148 | 40 | [ Ken VanDine ] | ||
4149 | 41 | * New upstream release (LP: #416591) | ||
4150 | 42 | - added unit tests for couchwidget, and then fixed bug #412266 | ||
4151 | 43 | - Change to freedesktop URL for record type spec. | ||
4152 | 44 | - First version of the contacts picker, based on CouchWidget | ||
4153 | 45 | - Adding the desktopcouch.contacts module. | ||
4154 | 46 | - Use subprocess.Popen and ourselves to the wait()ing, | ||
4155 | 47 | since subprocess.call() is buggy. There's still an EINTR bug | ||
4156 | 48 | in subprocess, though. | ||
4157 | 49 | - Occasionally stop couchdb in tests, so we exercise the automatic | ||
4158 | 50 | starting code. This will lead to spurious errors because of the | ||
4159 | 51 | aforementioned subprocess bug, but it's the right thing to do. | ||
4160 | 52 | - Abstract away some of the linuxisms and complain if we're run on | ||
4161 | 53 | an unsupported OS. | ||
4162 | 54 | - Fix a race condition in the process-testing code. | ||
4163 | 55 | - Replace the TestCase module with one that doesn't complain of dirty | ||
4164 | 56 | twisted reactors. | ||
4165 | 57 | - Add a means of stopping the desktop couchdb daemon. | ||
4166 | 58 | - Add an additional check that a found PID and process named correctly | ||
4167 | 59 | is indeed a process that this user started, so we don't try to talk | ||
4168 | 60 | to other local users' desktop couchdbs. | ||
4169 | 61 | - Get the port at function-call time, instead of storing a port at | ||
4170 | 62 | start-time and giving that back. The info can be stale, the old | ||
4171 | 63 | way. | ||
4172 | 64 | - Don't create a view per record-type; instead, call the standard | ||
4173 | 65 | return-all-records-keyed-by-record-type and use slice notation on | ||
4174 | 66 | the viewresults to only get back the records with that type, | ||
4175 | 67 | which does the same thing but more elegantly. | ||
4176 | 68 | - Remove the unused/invalid "utils" import from test_server | ||
4177 | 69 | - Change the name of a function tested to be what actually exists in | ||
4178 | 70 | the code. | ||
4179 | 71 | - Refactored server.py by renaming server.get_records_and_type to | ||
4180 | 72 | server.get_records. Also modified the function to take a record | ||
4181 | 73 | type param if desired to only create records of that type and | ||
4182 | 74 | optionally create a view name "get_"+ record_type. (LP: #411475) | ||
4183 | 75 | * debian/control | ||
4184 | 76 | - Make python-desktopcouch depend on python-gtk2 and python-gnomekeyring | ||
4185 | 77 | - Make python-desktopcouch-records depend on python-gtk2, | ||
4186 | 78 | python-gnomekeyring, and python-oauth. | ||
4187 | 79 | - Remove depends for python-distutils-extra | ||
4188 | 80 | - Fixed Vcs-Browser tag | ||
4189 | 81 | |||
4190 | 82 | [Elliot Murphy] | ||
4191 | 83 | * debian/control: added build-dep on python-setuptools | ||
4192 | 84 | |||
4193 | 85 | |||
4194 | 86 | [ Martin Pitt ] | ||
4195 | 87 | * debian/control: Fix Vcs-* links. | ||
4196 | 88 | |||
4197 | 89 | -- Ken VanDine <ken.vandine@canonical.com> Thu, 27 Aug 2009 15:32:11 +0200 | ||
4198 | 90 | |||
4199 | 91 | desktopcouch (0.2-0ubuntu1) karmic; urgency=low | ||
4200 | 92 | |||
4201 | 93 | * New upstream release | ||
4202 | 94 | * Handle the case where the pid file is empty or doesn't exist (LP: #408796) | ||
4203 | 95 | |||
4204 | 96 | -- Ken VanDine <ken.vandine@canonical.com> Wed, 05 Aug 2009 02:17:47 +0100 | ||
4205 | 97 | |||
4206 | 98 | desktopcouch (0.1-0ubuntu1) karmic; urgency=low | ||
4207 | 99 | |||
4208 | 100 | * Initial release (LP: #397662) | ||
4209 | 101 | |||
4210 | 102 | -- Ken VanDine <ken.vandine@canonical.com> Fri, 31 Jul 2009 13:44:45 -0400 | ||
4211 | 0 | 103 | ||
4212 | === added file 'debian/compat' | |||
4213 | --- debian/compat 1970-01-01 00:00:00 +0000 | |||
4214 | +++ debian/compat 2009-09-01 15:46:56 +0000 | |||
4215 | @@ -0,0 +1,1 @@ | |||
4216 | 1 | 6 | ||
4217 | 0 | 2 | ||
4218 | === added file 'debian/control' | |||
4219 | --- debian/control 1970-01-01 00:00:00 +0000 | |||
4220 | +++ debian/control 2009-09-14 23:33:14 +0000 | |||
4221 | @@ -0,0 +1,76 @@ | |||
4222 | 1 | Source: desktopcouch | ||
4223 | 2 | Section: python | ||
4224 | 3 | Priority: optional | ||
4225 | 4 | Build-Depends: cdbs (>= 0.4.43), | ||
4226 | 5 | debhelper (>= 6), | ||
4227 | 6 | python, | ||
4228 | 7 | python-central (>= 0.6.11), | ||
4229 | 8 | python-distutils-extra (>= 2.8), | ||
4230 | 9 | python-setuptools | ||
4231 | 10 | Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> | ||
4232 | 11 | Standards-Version: 3.8.2 | ||
4233 | 12 | XS-Python-Version: current | ||
4234 | 13 | Homepage: http://launchpad.net/desktopcouch | ||
4235 | 14 | Vcs-Bzr: https://code.launchpad.net/~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb | ||
4236 | 15 | Vcs-Browser: http://bazaar.launchpad.net/~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb | ||
4237 | 16 | |||
4238 | 17 | Package: desktopcouch | ||
4239 | 18 | Architecture: all | ||
4240 | 19 | XB-Python-Version: ${python:Versions} | ||
4241 | 20 | Depends: ${misc:Depends}, | ||
4242 | 21 | ${python:Depends}, | ||
4243 | 22 | couchdb (>= 0.10.0~svn8138), | ||
4244 | 23 | python-dbus, | ||
4245 | 24 | python-couchdb (>= 0.6), | ||
4246 | 25 | python-twisted-core, | ||
4247 | 26 | python-gobject, | ||
4248 | 27 | python-avahi, | ||
4249 | 28 | python-desktopcouch, | ||
4250 | 29 | python-desktopcouch-records, | ||
4251 | 30 | python-gtk2 | ||
4252 | 31 | Description: Desktop CouchDB instance, specific to each user | ||
4253 | 32 | Couchdb owned by regular users in their sessions, for management of personal | ||
4254 | 33 | data. | ||
4255 | 34 | |||
4256 | 35 | Package: desktopcouch-tools | ||
4257 | 36 | Architecture: all | ||
4258 | 37 | XB-Python-Version: ${python:Versions} | ||
4259 | 38 | Depends: ${misc:Depends}, | ||
4260 | 39 | ${python:Depends}, | ||
4261 | 40 | python-dbus, | ||
4262 | 41 | python-twisted-core, | ||
4263 | 42 | python-gobject, | ||
4264 | 43 | python-avahi, | ||
4265 | 44 | python-desktopcouch, | ||
4266 | 45 | python-gtk2 | ||
4267 | 46 | Description: Desktop CouchDB tools | ||
4268 | 47 | Tools used to work with DesktopCouch database. | ||
4269 | 48 | |||
4270 | 49 | Package: python-desktopcouch | ||
4271 | 50 | Architecture: all | ||
4272 | 51 | XB-Python-Version: ${python:Versions} | ||
4273 | 52 | Depends: ${misc:Depends}, | ||
4274 | 53 | ${python:Depends}, | ||
4275 | 54 | desktopcouch, | ||
4276 | 55 | python-dbus, | ||
4277 | 56 | python-couchdb, | ||
4278 | 57 | python-xdg, | ||
4279 | 58 | python-twisted-core, | ||
4280 | 59 | python-gtk2, | ||
4281 | 60 | python-gnomekeyring | ||
4282 | 61 | Description: Python Desktop CouchDB | ||
4283 | 62 | A Python library for Desktop CouchDB. | ||
4284 | 63 | |||
4285 | 64 | Package: python-desktopcouch-records | ||
4286 | 65 | Architecture: all | ||
4287 | 66 | XB-Python-Version: ${python:Versions} | ||
4288 | 67 | Depends: ${misc:Depends}, | ||
4289 | 68 | ${python:Depends}, | ||
4290 | 69 | python-dbus, | ||
4291 | 70 | python-couchdb, | ||
4292 | 71 | python-desktopcouch, | ||
4293 | 72 | python-gtk2, | ||
4294 | 73 | python-gnomekeyring, | ||
4295 | 74 | python-oauth | ||
4296 | 75 | Description: Desktop CouchDB Records API | ||
4297 | 76 | A Python library for the Desktop CouchDB Records API. | ||
4298 | 0 | 77 | ||
4299 | === added file 'debian/copyright' | |||
4300 | --- debian/copyright 1970-01-01 00:00:00 +0000 | |||
4301 | +++ debian/copyright 2009-09-01 15:46:56 +0000 | |||
4302 | @@ -0,0 +1,11 @@ | |||
4303 | 1 | Format-Specification: http://wiki.debian.org/Proposals/CopyrightFormat | ||
4304 | 2 | Upstream-Name: desktopcouch | ||
4305 | 3 | Upstream-Maintainer: Stuart Langridge <stuart.langridge@canonical.com> | ||
4306 | 4 | Upstream-Source: https://launchpad.net/desktopcouch | ||
4307 | 5 | |||
4308 | 6 | Files: * | ||
4309 | 7 | Copyright: (C) 2009 Canonical Ltd. | ||
4310 | 8 | |||
4311 | 9 | License: LGPL-3 | ||
4312 | 10 | The full text of the LGPL is distributed in | ||
4313 | 11 | /usr/share/common-licenses/LGPL-3 on Debian systems. | ||
4314 | 0 | 12 | ||
4315 | === added file 'debian/desktopcouch-tools.install' | |||
4316 | --- debian/desktopcouch-tools.install 1970-01-01 00:00:00 +0000 | |||
4317 | +++ debian/desktopcouch-tools.install 2009-09-01 15:46:56 +0000 | |||
4318 | @@ -0,0 +1,4 @@ | |||
4319 | 1 | debian/tmp/usr/share/applications/desktopcouch-pair.desktop | ||
4320 | 2 | debian/tmp/usr/bin/desktopcouch-pair | ||
4321 | 3 | debian/tmp/usr/share/man/man1/desktopcouch-pair.1 | ||
4322 | 4 | #debian/tmp/usr/share/locale/*/LC_MESSAGES/desktopcouch.mo | ||
4323 | 0 | 5 | ||
4324 | === added file 'debian/desktopcouch.install' | |||
4325 | --- debian/desktopcouch.install 1970-01-01 00:00:00 +0000 | |||
4326 | +++ debian/desktopcouch.install 2009-09-01 15:46:56 +0000 | |||
4327 | @@ -0,0 +1,3 @@ | |||
4328 | 1 | debian/tmp/usr/share/desktopcouch | ||
4329 | 2 | debian/tmp/usr/lib/desktopcouch/desktopcouch-{stop,service} | ||
4330 | 3 | debian/tmp/usr/share/dbus-1/services/org.desktopcouch.CouchDB.service | ||
4331 | 0 | 4 | ||
4332 | === added file 'debian/pycompat' | |||
4333 | --- debian/pycompat 1970-01-01 00:00:00 +0000 | |||
4334 | +++ debian/pycompat 2009-09-01 15:46:56 +0000 | |||
4335 | @@ -0,0 +1,1 @@ | |||
4336 | 1 | 2 | ||
4337 | 0 | 2 | ||
4338 | === added file 'debian/python-desktopcouch-records.install' | |||
4339 | --- debian/python-desktopcouch-records.install 1970-01-01 00:00:00 +0000 | |||
4340 | +++ debian/python-desktopcouch-records.install 2009-09-01 15:46:56 +0000 | |||
4341 | @@ -0,0 +1,2 @@ | |||
4342 | 1 | debian/tmp/usr/share/doc/python-desktopcouch-records/api | ||
4343 | 2 | debian/tmp/usr/lib/*/*/desktopcouch/records/* | ||
4344 | 0 | 3 | ||
4345 | === added file 'debian/python-desktopcouch.install' | |||
4346 | --- debian/python-desktopcouch.install 1970-01-01 00:00:00 +0000 | |||
4347 | +++ debian/python-desktopcouch.install 2009-09-15 05:02:20 +0000 | |||
4348 | @@ -0,0 +1,3 @@ | |||
4349 | 1 | debian/tmp/usr/lib/*/*/desktopcouch/*.py | ||
4350 | 2 | debian/tmp/usr/lib/*/*/desktopcouch/replication_services/*.py | ||
4351 | 3 | debian/tmp/usr/lib/*/*/desktopcouch/pair/{couchdb_pairing,__init__.py} | ||
4352 | 0 | 4 | ||
4353 | === added file 'debian/rules' | |||
4354 | --- debian/rules 1970-01-01 00:00:00 +0000 | |||
4355 | +++ debian/rules 2009-09-01 15:46:56 +0000 | |||
4356 | @@ -0,0 +1,7 @@ | |||
4357 | 1 | #!/usr/bin/make -f | ||
4358 | 2 | |||
4359 | 3 | DEB_PYTHON_SYSTEM := pycentral | ||
4360 | 4 | |||
4361 | 5 | include /usr/share/cdbs/1/rules/debhelper.mk | ||
4362 | 6 | include /usr/share/cdbs/1/class/python-distutils.mk | ||
4363 | 7 | include /usr/share/cdbs/1/rules/langpack.mk | ||
4364 | 0 | 8 | ||
4365 | === added file 'debian/watch' | |||
4366 | --- debian/watch 1970-01-01 00:00:00 +0000 | |||
4367 | +++ debian/watch 2009-09-01 15:46:56 +0000 | |||
4368 | @@ -0,0 +1,2 @@ | |||
4369 | 1 | version=3 | ||
4370 | 2 | http://launchpad.net/desktopcouch/+download .*/desktopcouch-([0-9.]+)\.tar\.gz | ||
4371 | 0 | 3 | ||
4372 | === added directory 'desktopcouch' | |||
4373 | === added file 'desktopcouch-pair.desktop.in' | |||
4374 | --- desktopcouch-pair.desktop.in 1970-01-01 00:00:00 +0000 | |||
4375 | +++ desktopcouch-pair.desktop.in 2009-07-27 19:18:55 +0000 | |||
4376 | @@ -0,0 +1,8 @@ | |||
4377 | 1 | [Desktop Entry] | ||
4378 | 2 | _Name=CouchDB Pairing Tool | ||
4379 | 3 | Type=Application | ||
4380 | 4 | _Comment=Utility for pairing Desktop CouchDB | ||
4381 | 5 | Exec=desktopcouch-pair | ||
4382 | 6 | Icon= | ||
4383 | 7 | Categories=Network; | ||
4384 | 8 | _GenericName=CouchDB Pairing Tool | ||
4385 | 0 | 9 | ||
4386 | === added file 'desktopcouch/__init__.py' | |||
4387 | --- desktopcouch/__init__.py 1970-01-01 00:00:00 +0000 | |||
4388 | +++ desktopcouch/__init__.py 2009-09-09 23:24:53 +0000 | |||
4389 | @@ -0,0 +1,98 @@ | |||
4390 | 1 | # Copyright 2009 Canonical Ltd. | ||
4391 | 2 | # | ||
4392 | 3 | # This file is part of desktopcouch. | ||
4393 | 4 | # | ||
4394 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4395 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4396 | 7 | # as published by the Free Software Foundation. | ||
4397 | 8 | # | ||
4398 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4399 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4400 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4401 | 12 | # GNU Lesser General Public License for more details. | ||
4402 | 13 | # | ||
4403 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4404 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4405 | 16 | "Desktop Couch helper files" | ||
4406 | 17 | |||
4407 | 18 | from __future__ import with_statement | ||
4408 | 19 | import os, re | ||
4409 | 20 | from desktopcouch.start_local_couchdb import process_is_couchdb, read_pidfile | ||
4410 | 21 | |||
4411 | 22 | def find_pid(start_if_not_running=True): | ||
4412 | 23 | # Work out whether CouchDB is running by looking at its pid file | ||
4413 | 24 | pid = read_pidfile() | ||
4414 | 25 | if not process_is_couchdb(pid) and start_if_not_running: | ||
4415 | 26 | # start CouchDB by running the startup script | ||
4416 | 27 | print "Desktop CouchDB is not running; starting it.", | ||
4417 | 28 | from desktopcouch import start_local_couchdb | ||
4418 | 29 | pid = start_local_couchdb.start_couchdb() | ||
4419 | 30 | # now load the design documents, because it's started | ||
4420 | 31 | start_local_couchdb.update_design_documents() | ||
4421 | 32 | |||
4422 | 33 | if not process_is_couchdb(pid): | ||
4423 | 34 | raise RuntimeError("desktop-couch not started") | ||
4424 | 35 | |||
4425 | 36 | return pid | ||
4426 | 37 | |||
4427 | 38 | def find_port__linux(pid=None): | ||
4428 | 39 | if pid is None: | ||
4429 | 40 | pid = find_pid() | ||
4430 | 41 | |||
4431 | 42 | proc_dir = "/proc/%s" % (pid,) | ||
4432 | 43 | |||
4433 | 44 | # enumerate the process' file descriptors | ||
4434 | 45 | fd_dir = os.path.join(proc_dir, 'fd') | ||
4435 | 46 | try: | ||
4436 | 47 | fd_paths = [os.readlink(os.path.join(fd_dir, fd)) | ||
4437 | 48 | for fd in os.listdir(fd_dir)] | ||
4438 | 49 | except OSError: | ||
4439 | 50 | raise RuntimeError("Unable to find file descriptors in /proc") | ||
4440 | 51 | |||
4441 | 52 | # identify socket fds | ||
4442 | 53 | socket_matches = [re.match('socket:\\[([0-9]+)\\]', p) for p in fd_paths] | ||
4443 | 54 | # extract their inode numbers | ||
4444 | 55 | socket_inodes = [m.group(1) for m in socket_matches if m is not None] | ||
4445 | 56 | |||
4446 | 57 | # construct a subexpression which matches any one of these inodes | ||
4447 | 58 | inode_subexp = "|".join(map(re.escape, socket_inodes)) | ||
4448 | 59 | # construct regexp to match /proc/net/tcp entries which are listening | ||
4449 | 60 | # sockets having one of the given inode numbers | ||
4450 | 61 | listening_regexp = re.compile(r''' | ||
4451 | 62 | \s*\d+:\s* # sl | ||
4452 | 63 | [0-9A-F]{8}: # local_address part 1 | ||
4453 | 64 | ([0-9A-F]{4})\s+ # local_address part 2 | ||
4454 | 65 | 00000000:0000\s+ # rem_address | ||
4455 | 66 | 0A\s+ # st (0A = listening) | ||
4456 | 67 | [0-9A-F]{8}: # tx_queue | ||
4457 | 68 | [0-9A-F]{8}\s+ # rx_queue | ||
4458 | 69 | [0-9A-F]{2}: # tr | ||
4459 | 70 | [0-9A-F]{8}\s+ # tm->when | ||
4460 | 71 | [0-9A-F]{8}\s* # retrnsmt | ||
4461 | 72 | \d+\s+\d+\s+ # uid, timeout | ||
4462 | 73 | (?:%s)\s+ # inode | ||
4463 | 74 | ''' % (inode_subexp,), re.VERBOSE) | ||
4464 | 75 | |||
4465 | 76 | # extract the TCP port from the first matching line in /proc/$pid/net/tcp | ||
4466 | 77 | port = None | ||
4467 | 78 | with open(os.path.join(proc_dir, 'net', 'tcp')) as tcp_file: | ||
4468 | 79 | for line in tcp_file: | ||
4469 | 80 | match = listening_regexp.match(line) | ||
4470 | 81 | if match is not None: | ||
4471 | 82 | port = str(int(match.group(1), 16)) | ||
4472 | 83 | break | ||
4473 | 84 | if port is None: | ||
4474 | 85 | raise RuntimeError("Unable to find listening port") | ||
4475 | 86 | |||
4476 | 87 | return port | ||
4477 | 88 | |||
4478 | 89 | |||
4479 | 90 | |||
4480 | 91 | import platform | ||
4481 | 92 | os_name = platform.system() | ||
4482 | 93 | try: | ||
4483 | 94 | find_port = { | ||
4484 | 95 | "Linux": find_port__linux | ||
4485 | 96 | } [os_name] | ||
4486 | 97 | except KeyError: | ||
4487 | 98 | raise NotImplementedError("os %r is not yet supported" % (os_name,)) | ||
4488 | 0 | 99 | ||
4489 | === added directory 'desktopcouch/contacts' | |||
4490 | === added file 'desktopcouch/contacts/__init__.py' | |||
4491 | --- desktopcouch/contacts/__init__.py 1970-01-01 00:00:00 +0000 | |||
4492 | +++ desktopcouch/contacts/__init__.py 2009-08-17 21:39:48 +0000 | |||
4493 | @@ -0,0 +1,20 @@ | |||
4494 | 1 | # Copyright 2009 Canonical Ltd. | ||
4495 | 2 | # | ||
4496 | 3 | # This file is part of desktopcouch-contacts. | ||
4497 | 4 | # | ||
4498 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4499 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4500 | 7 | # as published by the Free Software Foundation. | ||
4501 | 8 | # | ||
4502 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4503 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4504 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4505 | 12 | # GNU Lesser General Public License for more details. | ||
4506 | 13 | # | ||
4507 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4508 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4509 | 16 | # | ||
4510 | 17 | # Authors: Eric Casteleijn <eric.casteleijn@canonical.com> | ||
4511 | 18 | # Nicola Larosa <nicola.larosa@canonical.com> | ||
4512 | 19 | |||
4513 | 20 | """UbuntuOne Contacts API""" | ||
4514 | 0 | 21 | ||
4515 | === added file 'desktopcouch/contacts/contactspicker.py' | |||
4516 | --- desktopcouch/contacts/contactspicker.py 1970-01-01 00:00:00 +0000 | |||
4517 | +++ desktopcouch/contacts/contactspicker.py 2009-08-24 20:35:25 +0000 | |||
4518 | @@ -0,0 +1,81 @@ | |||
4519 | 1 | # Copyright 2009 Canonical Ltd. | ||
4520 | 2 | # | ||
4521 | 3 | # This file is part of desktopcouch-contacts. | ||
4522 | 4 | # | ||
4523 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4524 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4525 | 7 | # as published by the Free Software Foundation. | ||
4526 | 8 | # | ||
4527 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4528 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4529 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4530 | 12 | # GNU Lesser General Public License for more details. | ||
4531 | 13 | # | ||
4532 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4533 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4534 | 16 | # | ||
4535 | 17 | # Authors: Rodrigo Moya <rodrigo.moya@canonical.com | ||
4536 | 18 | |||
4537 | 19 | """A widget to allow users to pick contacts""" | ||
4538 | 20 | |||
4539 | 21 | import gtk | ||
4540 | 22 | from desktopcouch.contacts.record import CONTACT_RECORD_TYPE | ||
4541 | 23 | from desktopcouch.records.couchgrid import CouchGrid | ||
4542 | 24 | |||
4543 | 25 | |||
4544 | 26 | class ContactsPicker(gtk.VBox): | ||
4545 | 27 | """A contacts picker""" | ||
4546 | 28 | |||
4547 | 29 | def __init__(self, uri=None): | ||
4548 | 30 | """Create a new ContactsPicker widget.""" | ||
4549 | 31 | |||
4550 | 32 | gtk.VBox.__init__(self) | ||
4551 | 33 | |||
4552 | 34 | # Create search entry and button | ||
4553 | 35 | hbox = gtk.HBox() | ||
4554 | 36 | self.pack_start(hbox, False, False, 3) | ||
4555 | 37 | |||
4556 | 38 | self.search_entry = gtk.Entry() | ||
4557 | 39 | hbox.pack_start(self.search_entry, True, True, 3) | ||
4558 | 40 | |||
4559 | 41 | self.search_button = gtk.Button( | ||
4560 | 42 | stock=gtk.STOCK_FIND, use_underline=True) | ||
4561 | 43 | hbox.pack_start(self.search_button, False, False, 3) | ||
4562 | 44 | |||
4563 | 45 | # Create CouchGrid to contain list of contacts | ||
4564 | 46 | self.contacts_list = CouchGrid('contacts', uri=uri) | ||
4565 | 47 | self.contacts_list.editable = False | ||
4566 | 48 | self.contacts_list.keys = [ "first_name", "last_name" ] | ||
4567 | 49 | self.contacts_list.record_type = CONTACT_RECORD_TYPE | ||
4568 | 50 | |||
4569 | 51 | #Pimp out the columns with some nicer titles | ||
4570 | 52 | #TODO: this should be set up for translatability | ||
4571 | 53 | columns = self.contacts_list.get_columns() | ||
4572 | 54 | columns[0].set_title("First Name") | ||
4573 | 55 | columns[1].set_title("Last Name") | ||
4574 | 56 | |||
4575 | 57 | self.contacts_list.show() | ||
4576 | 58 | self.pack_start(self.contacts_list, True, True, 3) | ||
4577 | 59 | |||
4578 | 60 | def get_contacts_list(self): | ||
4579 | 61 | """get_contacts_list - gtk.Widget value | ||
4580 | 62 | Gets the CouchGrid inside the ContactsPicker widget | ||
4581 | 63 | """ | ||
4582 | 64 | return self.contacts_list | ||
4583 | 65 | |||
4584 | 66 | if __name__ == "__main__": | ||
4585 | 67 | """creates a test ContactsPicker if called directly""" | ||
4586 | 68 | |||
4587 | 69 | # Create and show a test window | ||
4588 | 70 | win = gtk.Dialog("ContactsPicker widget test", None, 0, | ||
4589 | 71 | (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)) | ||
4590 | 72 | win.connect("response", gtk.main_quit) | ||
4591 | 73 | win.resize(300, 450) | ||
4592 | 74 | |||
4593 | 75 | # Create the contacts picker widget | ||
4594 | 76 | picker = ContactsPicker() | ||
4595 | 77 | win.get_content_area().pack_start(picker, True, True, 3) | ||
4596 | 78 | |||
4597 | 79 | # Run the test application | ||
4598 | 80 | win.show_all() | ||
4599 | 81 | gtk.main() | ||
4600 | 0 | 82 | ||
4601 | === added file 'desktopcouch/contacts/record.py' | |||
4602 | --- desktopcouch/contacts/record.py 1970-01-01 00:00:00 +0000 | |||
4603 | +++ desktopcouch/contacts/record.py 2009-08-19 10:07:54 +0000 | |||
4604 | @@ -0,0 +1,34 @@ | |||
4605 | 1 | # Copyright 2009 Canonical Ltd. | ||
4606 | 2 | # | ||
4607 | 3 | # This file is part of desktopcouch-contacts. | ||
4608 | 4 | # | ||
4609 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4610 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4611 | 7 | # as published by the Free Software Foundation. | ||
4612 | 8 | # | ||
4613 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4614 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4615 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4616 | 12 | # GNU Lesser General Public License for more details. | ||
4617 | 13 | # | ||
4618 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4619 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4620 | 16 | # | ||
4621 | 17 | # Authors: Eric Casteleijn <eric.casteleijn@canonical.com> | ||
4622 | 18 | # Nicola Larosa <nicola.larosa@canonical.com> | ||
4623 | 19 | # Mark G. Saye <mark.saye@canonical.com> | ||
4624 | 20 | |||
4625 | 21 | |||
4626 | 22 | """A dictionary based contact record representation.""" | ||
4627 | 23 | |||
4628 | 24 | from desktopcouch.records.record import Record | ||
4629 | 25 | |||
4630 | 26 | CONTACT_RECORD_TYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact' | ||
4631 | 27 | |||
4632 | 28 | |||
4633 | 29 | class Contact(Record): | ||
4634 | 30 | """An Ubuntuone Contact Record.""" | ||
4635 | 31 | |||
4636 | 32 | def __init__(self, data=None, record_id=None): | ||
4637 | 33 | super(Contact, self).__init__( | ||
4638 | 34 | record_id=record_id, data=data, record_type=CONTACT_RECORD_TYPE) | ||
4639 | 0 | 35 | ||
4640 | === added file 'desktopcouch/contacts/schema.txt' | |||
4641 | --- desktopcouch/contacts/schema.txt 1970-01-01 00:00:00 +0000 | |||
4642 | +++ desktopcouch/contacts/schema.txt 2009-08-19 10:07:54 +0000 | |||
4643 | @@ -0,0 +1,50 @@ | |||
4644 | 1 | # Copyright 2009 Canonical Ltd. | ||
4645 | 2 | # | ||
4646 | 3 | # This file is part of desktopcouch-contacts. | ||
4647 | 4 | # | ||
4648 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4649 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4650 | 7 | # as published by the Free Software Foundation. | ||
4651 | 8 | # | ||
4652 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4653 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4654 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4655 | 12 | # GNU Lesser General Public License for more details. | ||
4656 | 13 | # | ||
4657 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4658 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4659 | 16 | |||
4660 | 17 | Schema | ||
4661 | 18 | |||
4662 | 19 | The proposed CouchDB contact schema is as follows: | ||
4663 | 20 | |||
4664 | 21 | Core fields | ||
4665 | 22 | |||
4666 | 23 | * record_type 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact' | ||
4667 | 24 | * first_name (string) | ||
4668 | 25 | * last_name (string) | ||
4669 | 26 | * birth_date (string, "YYYY-MM-DD") | ||
4670 | 27 | * addresses (MergeableList of "address" dictionaries) | ||
4671 | 28 | o city (string) | ||
4672 | 29 | o address1 (string) | ||
4673 | 30 | o address2 (string) | ||
4674 | 31 | o pobox (string) | ||
4675 | 32 | o state (string) | ||
4676 | 33 | o country (string) | ||
4677 | 34 | o postalcode (string) | ||
4678 | 35 | o description (string, e.g., "Home") | ||
4679 | 36 | * email_addresses (MergeableList of "emailaddress" dictionaries) | ||
4680 | 37 | o address (string), | ||
4681 | 38 | o description (string) | ||
4682 | 39 | * phone_numbers (MergeableList of "phone number" dictionaries) | ||
4683 | 40 | o number (string) | ||
4684 | 41 | o description (string) | ||
4685 | 42 | * application_annotations Everything else, organized per application. | ||
4686 | 43 | |||
4687 | 44 | Note: None of the core fields are mandatory, but applications should | ||
4688 | 45 | not add any other fields at the top level of the record. Any fields | ||
4689 | 46 | needed not defined here should be put under application_annotations in | ||
4690 | 47 | the namespace of the application there. So for Ubuntu One: | ||
4691 | 48 | |||
4692 | 49 | "application_annotations": { | ||
4693 | 50 | "Ubuntu One": {<Ubuntu One specific fields here>}} | ||
4694 | 0 | 51 | ||
4695 | === added directory 'desktopcouch/contacts/testing' | |||
4696 | === added file 'desktopcouch/contacts/testing/__init__.py' | |||
4697 | --- desktopcouch/contacts/testing/__init__.py 1970-01-01 00:00:00 +0000 | |||
4698 | +++ desktopcouch/contacts/testing/__init__.py 2009-08-17 21:39:48 +0000 | |||
4699 | @@ -0,0 +1,20 @@ | |||
4700 | 1 | # Copyright 2009 Canonical Ltd. | ||
4701 | 2 | # | ||
4702 | 3 | # This file is part of desktopcouch-contacts. | ||
4703 | 4 | # | ||
4704 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4705 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4706 | 7 | # as published by the Free Software Foundation. | ||
4707 | 8 | # | ||
4708 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4709 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4710 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4711 | 12 | # GNU Lesser General Public License for more details. | ||
4712 | 13 | # | ||
4713 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4714 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4715 | 16 | # | ||
4716 | 17 | # Authors: Stuart Langridge <stuart.langridge@canonical.com> | ||
4717 | 18 | # Nicola Larosa <nicola.larosa@canonical.com> | ||
4718 | 19 | |||
4719 | 20 | """Support code for tests""" | ||
4720 | 0 | 21 | ||
4721 | === added file 'desktopcouch/contacts/testing/create.py' | |||
4722 | --- desktopcouch/contacts/testing/create.py 1970-01-01 00:00:00 +0000 | |||
4723 | +++ desktopcouch/contacts/testing/create.py 2009-09-09 23:24:53 +0000 | |||
4724 | @@ -0,0 +1,178 @@ | |||
4725 | 1 | # Copyright 2009 Canonical Ltd. | ||
4726 | 2 | # | ||
4727 | 3 | # This file is part of desktopcouch-contacts. | ||
4728 | 4 | # | ||
4729 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4730 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4731 | 7 | # as published by the Free Software Foundation. | ||
4732 | 8 | # | ||
4733 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4734 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4735 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4736 | 12 | # GNU Lesser General Public License for more details. | ||
4737 | 13 | # | ||
4738 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4739 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4740 | 16 | # | ||
4741 | 17 | # Authors: Stuart Langridge <stuart.langridge@canonical.com> | ||
4742 | 18 | # Nicola Larosa <nicola.larosa@canonical.com> | ||
4743 | 19 | |||
4744 | 20 | """Creating CouchDb-stored contacts for testing""" | ||
4745 | 21 | |||
4746 | 22 | import random, string, uuid | ||
4747 | 23 | |||
4748 | 24 | from couchdb import Server | ||
4749 | 25 | import desktopcouch | ||
4750 | 26 | |||
4751 | 27 | CONTACT_DOCTYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact' | ||
4752 | 28 | COUCHDB_SYS_PORT = 5984 | ||
4753 | 29 | |||
4754 | 30 | FIRST_NAMES = ('Jack', 'Thomas', 'Oliver', 'Joshua', 'Harry', 'Charlie', | ||
4755 | 31 | 'Daniel', 'William', 'James', 'Alfie', 'Grace', 'Ruby', 'Olivia', | ||
4756 | 32 | 'Emily', 'Jessica', 'Sophie', 'Chloe', 'Lily', 'Ella', 'Amelia') | ||
4757 | 33 | LAST_NAMES = ('Dalglish', 'Grobbelaar', 'Lawrenson', 'Beglin', 'Nicol', | ||
4758 | 34 | 'Whelan', 'Hansen', 'Johnston', 'Rush', 'Molby', 'MacDonald', 'McMahon', | ||
4759 | 35 | 'Penny', 'Leicester', 'Langley', 'Commodore', 'Touchstone', 'Fielding') | ||
4760 | 36 | COMPANIES = ('Gostram', 'Grulthing', 'Nasform', 'Smarfish', 'Builsank') | ||
4761 | 37 | STREET_TYPES = ('Street', 'Road', 'Lane', 'Avenue', 'Square', 'Park', 'Mall') | ||
4762 | 38 | CITIES = ('Scunthorpe', 'Birmingham', 'Cambridge', 'Durham', 'Bedford') | ||
4763 | 39 | COUNTRIES = ('England', 'Ireland', 'Scotland', 'Wales') | ||
4764 | 40 | |||
4765 | 41 | def head_or_tails(): | ||
4766 | 42 | """Randomly return True or False""" | ||
4767 | 43 | return random.choice((False, True)) | ||
4768 | 44 | |||
4769 | 45 | def random_bools(num_bools, at_least_one_true=True): | ||
4770 | 46 | """ | ||
4771 | 47 | Return a sequence of random booleans. If at_least_one_true, | ||
4772 | 48 | guarantee at list one True value. | ||
4773 | 49 | """ | ||
4774 | 50 | if num_bools < 2: | ||
4775 | 51 | raise RuntimeError('Cannot build a sequence smaller than two elements') | ||
4776 | 52 | bools = [head_or_tails() for __ in range(num_bools)] | ||
4777 | 53 | if at_least_one_true and not any(bools): | ||
4778 | 54 | bools[random.randrange(num_bools)] = True | ||
4779 | 55 | return bools | ||
4780 | 56 | |||
4781 | 57 | def random_string(length=10, upper=False): | ||
4782 | 58 | """ | ||
4783 | 59 | Return a string, of specified length, of random lower or uppercase letters. | ||
4784 | 60 | """ | ||
4785 | 61 | charset = string.uppercase if upper else string.lowercase | ||
4786 | 62 | return ''.join(random.sample(charset, length)) | ||
4787 | 63 | |||
4788 | 64 | def random_postal_address(): | ||
4789 | 65 | """Return something that looks like a postal address""" | ||
4790 | 66 | return '%d %s %s' % (random.randint(1, 100), random.choice(LAST_NAMES), | ||
4791 | 67 | random.choice(STREET_TYPES)) | ||
4792 | 68 | |||
4793 | 69 | def random_email_address( | ||
4794 | 70 | first_name='', last_name='', company='', address_type='other'): | ||
4795 | 71 | """ | ||
4796 | 72 | Return something that looks like an email address. | ||
4797 | 73 | address_type values: 'personal', 'work', 'other'. | ||
4798 | 74 | """ | ||
4799 | 75 | # avoid including the dot if one or both names are missing | ||
4800 | 76 | pers_name = '.'.join([name for name in (first_name, last_name) if name]) | ||
4801 | 77 | return {'personal': pers_name, 'work': company, | ||
4802 | 78 | }.get(address_type, random_string(5)) + '@example.com' | ||
4803 | 79 | |||
4804 | 80 | def random_postal_code(): | ||
4805 | 81 | """Return something that looks like a postal code""" | ||
4806 | 82 | return '%s12 3%s' % (random_string(2, True), random_string(2, True)) | ||
4807 | 83 | |||
4808 | 84 | def random_phone_number(): | ||
4809 | 85 | """Return something that looks like a phone number""" | ||
4810 | 86 | return '+%s %s %s %s' % (random.randint(10, 99), random.randint(10, 99), | ||
4811 | 87 | random.randint(1000, 9999), random.randint(1000, 9999)) | ||
4812 | 88 | |||
4813 | 89 | def random_birth_date(): | ||
4814 | 90 | """Return something that looks like a birth date""" | ||
4815 | 91 | return '%04d-%02d-%02d' % (random.randint(1900, 2006), | ||
4816 | 92 | random.randint(1, 12), random.randint(1, 28)) | ||
4817 | 93 | |||
4818 | 94 | def make_one_contact(maincount, doctype, app_annots): | ||
4819 | 95 | """Make up one contact randomly""" | ||
4820 | 96 | # Record schema | ||
4821 | 97 | fielddict = {'record_type': doctype, 'record_type_version': '1.0'} | ||
4822 | 98 | # Names | ||
4823 | 99 | # at least one of the three will be present | ||
4824 | 100 | has_first_name, has_last_name, has_company_name = random_bools(3) | ||
4825 | 101 | first_name = (random.choice(FIRST_NAMES) + str(maincount) | ||
4826 | 102 | ) if has_first_name else '' | ||
4827 | 103 | last_name = random.choice(LAST_NAMES) if has_last_name else '' | ||
4828 | 104 | company = (random.choice(COMPANIES) + str(maincount) | ||
4829 | 105 | ) if has_company_name else '' | ||
4830 | 106 | # Address places and types | ||
4831 | 107 | address_places, email_types, phone_places = [], [], [] | ||
4832 | 108 | if has_first_name or has_last_name: | ||
4833 | 109 | for (has_it, seq, val) in zip( | ||
4834 | 110 | # at least one of the three will be present | ||
4835 | 111 | random_bools(3), | ||
4836 | 112 | (address_places, email_types, phone_places), | ||
4837 | 113 | ('home', 'personal', 'home')): | ||
4838 | 114 | if has_it: | ||
4839 | 115 | seq.append(val) | ||
4840 | 116 | if has_company_name: | ||
4841 | 117 | for (has_it, seq) in zip( | ||
4842 | 118 | # at least one of the three will be present | ||
4843 | 119 | random_bools(3), | ||
4844 | 120 | (address_places, email_types, phone_places)): | ||
4845 | 121 | if has_it: | ||
4846 | 122 | seq.append('work') | ||
4847 | 123 | for (has_it, seq) in zip( | ||
4848 | 124 | # none of the three may be present | ||
4849 | 125 | random_bools(3, at_least_one_true=False), | ||
4850 | 126 | (address_places, email_types, phone_places)): | ||
4851 | 127 | if has_it: | ||
4852 | 128 | seq.append('other') | ||
4853 | 129 | # Addresses | ||
4854 | 130 | addresses = {} | ||
4855 | 131 | for address_type in address_places: | ||
4856 | 132 | addresses[str(uuid.uuid4())] = { | ||
4857 | 133 | 'address1': random_postal_address(), 'address2': '', | ||
4858 | 134 | 'pobox': '', 'city': random.choice(CITIES), | ||
4859 | 135 | 'state': '', 'postalcode': random_postal_code(), | ||
4860 | 136 | 'country': random.choice(COUNTRIES), 'description': address_type} | ||
4861 | 137 | # Email addresses | ||
4862 | 138 | email_addresses = {} | ||
4863 | 139 | for email_address_type in email_types: | ||
4864 | 140 | email_addresses[str(uuid.uuid4())] = { | ||
4865 | 141 | 'address': random_email_address( | ||
4866 | 142 | first_name, last_name, company, email_address_type), | ||
4867 | 143 | 'description': email_address_type} | ||
4868 | 144 | # Phone numbers | ||
4869 | 145 | phone_numbers = {} | ||
4870 | 146 | for phone_number_type in phone_places: | ||
4871 | 147 | phone_numbers[str(uuid.uuid4())] = { | ||
4872 | 148 | 'priority': 0, 'number': random_phone_number(), | ||
4873 | 149 | 'description': phone_number_type} | ||
4874 | 150 | # Store data in fielddict | ||
4875 | 151 | if (has_first_name or has_last_name) and head_or_tails(): | ||
4876 | 152 | fielddict['birth_date'] = random_birth_date() | ||
4877 | 153 | fielddict.update({'first_name': first_name, 'last_name': last_name, | ||
4878 | 154 | 'addresses': addresses, 'email_addresses': email_addresses, | ||
4879 | 155 | 'phone_numbers': phone_numbers, 'company': company}) | ||
4880 | 156 | # Possibly add example application annotations | ||
4881 | 157 | if app_annots: | ||
4882 | 158 | fielddict['application_annotations'] = app_annots | ||
4883 | 159 | return fielddict | ||
4884 | 160 | |||
4885 | 161 | def create_many_contacts( | ||
4886 | 162 | num_contacts=10, host='localhost', port=None, | ||
4887 | 163 | db_name='contacts', doctype=CONTACT_DOCTYPE, app_annots=None): | ||
4888 | 164 | """Make many contacts and create their records""" | ||
4889 | 165 | if port is None: | ||
4890 | 166 | desktopcouch.find_pid() | ||
4891 | 167 | port = desktopcouch.find_port() | ||
4892 | 168 | server_url = 'http://%s:%s/' % (host, port) | ||
4893 | 169 | server = Server(server_url) | ||
4894 | 170 | db = server[db_name] if db_name in server else server.create(db_name) | ||
4895 | 171 | record_ids = [] | ||
4896 | 172 | for maincount in range(1, num_contacts + 1): | ||
4897 | 173 | # Make the contact | ||
4898 | 174 | fielddict = make_one_contact(maincount, doctype, app_annots) | ||
4899 | 175 | # Store data in CouchDB | ||
4900 | 176 | record_id = db.create(fielddict) | ||
4901 | 177 | record_ids.append(record_id) | ||
4902 | 178 | return record_ids | ||
4903 | 0 | 179 | ||
4904 | === added directory 'desktopcouch/contacts/tests' | |||
4905 | === added file 'desktopcouch/contacts/tests/__init__.py' | |||
4906 | --- desktopcouch/contacts/tests/__init__.py 1970-01-01 00:00:00 +0000 | |||
4907 | +++ desktopcouch/contacts/tests/__init__.py 2009-09-07 09:23:36 +0000 | |||
4908 | @@ -0,0 +1,18 @@ | |||
4909 | 1 | # Copyright 2009 Canonical Ltd. | ||
4910 | 2 | # | ||
4911 | 3 | # This file is part of desktopcouch-contacts. | ||
4912 | 4 | # | ||
4913 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4914 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4915 | 7 | # as published by the Free Software Foundation. | ||
4916 | 8 | # | ||
4917 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4918 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4919 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4920 | 12 | # GNU Lesser General Public License for more details. | ||
4921 | 13 | # | ||
4922 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4923 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4924 | 16 | |||
4925 | 17 | """Tests for Contacts API""" | ||
4926 | 18 | |||
4927 | 0 | 19 | ||
4928 | === added file 'desktopcouch/contacts/tests/test_contactspicker.py' | |||
4929 | --- desktopcouch/contacts/tests/test_contactspicker.py 1970-01-01 00:00:00 +0000 | |||
4930 | +++ desktopcouch/contacts/tests/test_contactspicker.py 2009-09-01 22:29:01 +0000 | |||
4931 | @@ -0,0 +1,54 @@ | |||
4932 | 1 | # Copyright 2009 Canonical Ltd. | ||
4933 | 2 | # | ||
4934 | 3 | # This file is part of desktopcouch-contacts. | ||
4935 | 4 | # | ||
4936 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4937 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4938 | 7 | # as published by the Free Software Foundation. | ||
4939 | 8 | # | ||
4940 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
4941 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4942 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4943 | 12 | # GNU Lesser General Public License for more details. | ||
4944 | 13 | # | ||
4945 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4946 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | ||
4947 | 16 | # | ||
4948 | 17 | # Authors: Rick Spencer <rick.spencer@canonical.com> | ||
4949 | 18 | |||
4950 | 19 | """Tests for the ContactsPicker class""" | ||
4951 | 20 | |||
4952 | 21 | import testtools | ||
4953 | 22 | import gtk | ||
4954 | 23 | |||
4955 | 24 | from desktopcouch.tests import xdg_cache | ||
4956 | 25 | |||
4957 | 26 | from desktopcouch.contacts.contactspicker import ContactsPicker | ||
4958 | 27 | from desktopcouch.records.server import CouchDatabase | ||
4959 | 28 | |||
4960 | 29 | class TestContactsPicker(testtools.TestCase): | ||
4961 | 30 | """Test the Contact Picker Window.""" | ||
4962 | 31 | |||
4963 | 32 | def setUp(self): | ||
4964 | 33 | """setup each test""" | ||
4965 | 34 | # Connect to CouchDB server | ||
4966 | 35 | self.assert_(xdg_cache) | ||
4967 | 36 | self.dbname = 'contacts' | ||
4968 | 37 | self.database = CouchDatabase(self.dbname, create=True) | ||
4969 | 38 | |||
4970 | 39 | def tearDown(self): | ||
4971 | 40 | """tear down each test""" | ||
4972 | 41 | del self.database._server[self.dbname] | ||
4973 | 42 | |||
4974 | 43 | def test_can_contruct_contactspicker(self): | ||
4975 | 44 | """Test that we can build the window""" | ||
4976 | 45 | # Create and show a test window | ||
4977 | 46 | win = gtk.Dialog("ContactsPicker widget test", None, 0, | ||
4978 | 47 | (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)) | ||
4979 | 48 | win.connect("response", gtk.main_quit) | ||
4980 | 49 | win.resize(300, 450) | ||
4981 | 50 | |||
4982 | 51 | # Create the contacts picker widget | ||
4983 | 52 | picker = ContactsPicker() | ||
4984 | 53 | win.get_content_area().pack_start(picker, True, True, 3) | ||
4985 | 54 | self.assert_(picker.get_contacts_list()) | ||
4986 | 0 | 55 | ||
4987 | === added file 'desktopcouch/contacts/tests/test_create.py' | |||
4988 | --- desktopcouch/contacts/tests/test_create.py 1970-01-01 00:00:00 +0000 | |||
4989 | +++ desktopcouch/contacts/tests/test_create.py 2009-09-14 19:02:58 +0000 | |||
4990 | @@ -0,0 +1,67 @@ | |||
4991 | 1 | # Copyright 2009 Canonical Ltd. | ||
4992 | 2 | # | ||
4993 | 3 | # This file is part of desktopcouch-contacts. | ||
4994 | 4 | # | ||
4995 | 5 | # desktopcouch is free software: you can redistribute it and/or modify | ||
4996 | 6 | # it under the terms of the GNU Lesser General Public License version 3 | ||
4997 | 7 | # as published by the Free Software Foundation. | ||
4998 | 8 | # | ||
4999 | 9 | # desktopcouch is distributed in the hope that it will be useful, | ||
5000 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
This should go in after the alpha I think. Pushing it in now
may require a respin, so we only want to do that if it is
critical to have it for the alpha.
Thanks,
James