Merge lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb into lp:ubuntu/karmic/desktopcouch

Proposed by Chad Miller
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
Reviewer Review Type Date Requested Status
James Westby (community) Disapprove
Review via email: mp+11778@code.launchpad.net
To post a comment you must log in.
Revision history for this message
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.

Thanks,

James

Revision history for this message
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?

Revision history for this message
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

Revision history for this message
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

review: Disapprove
Revision history for this message
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://launchpad.net/~statik/

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'COPYING'
--- COPYING 1970-01-01 00:00:00 +0000
+++ COPYING 2009-07-09 20:17:15 +0000
@@ -0,0 +1,676 @@
1
2 GNU GENERAL PUBLIC LICENSE
3 Version 3, 29 June 2007
4
5 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
6 Everyone is permitted to copy and distribute verbatim copies
7 of this license document, but changing it is not allowed.
8
9 Preamble
10
11 The GNU General Public License is a free, copyleft license for
12software and other kinds of works.
13
14 The licenses for most software and other practical works are designed
15to take away your freedom to share and change the works. By contrast,
16the GNU General Public License is intended to guarantee your freedom to
17share and change all versions of a program--to make sure it remains free
18software for all its users. We, the Free Software Foundation, use the
19GNU General Public License for most of our software; it applies also to
20any other work released this way by its authors. You can apply it to
21your programs, too.
22
23 When we speak of free software, we are referring to freedom, not
24price. Our General Public Licenses are designed to make sure that you
25have the freedom to distribute copies of free software (and charge for
26them if you wish), that you receive source code or can get it if you
27want it, that you can change the software or use pieces of it in new
28free programs, and that you know you can do these things.
29
30 To protect your rights, we need to prevent others from denying you
31these rights or asking you to surrender the rights. Therefore, you have
32certain responsibilities if you distribute copies of the software, or if
33you modify it: responsibilities to respect the freedom of others.
34
35 For example, if you distribute copies of such a program, whether
36gratis or for a fee, you must pass on to the recipients the same
37freedoms that you received. You must make sure that they, too, receive
38or can get the source code. And you must show them these terms so they
39know their rights.
40
41 Developers that use the GNU GPL protect your rights with two steps:
42(1) assert copyright on the software, and (2) offer you this License
43giving you legal permission to copy, distribute and/or modify it.
44
45 For the developers' and authors' protection, the GPL clearly explains
46that there is no warranty for this free software. For both users' and
47authors' sake, the GPL requires that modified versions be marked as
48changed, so that their problems will not be attributed erroneously to
49authors of previous versions.
50
51 Some devices are designed to deny users access to install or run
52modified versions of the software inside them, although the manufacturer
53can do so. This is fundamentally incompatible with the aim of
54protecting users' freedom to change the software. The systematic
55pattern of such abuse occurs in the area of products for individuals to
56use, which is precisely where it is most unacceptable. Therefore, we
57have designed this version of the GPL to prohibit the practice for those
58products. If such problems arise substantially in other domains, we
59stand ready to extend this provision to those domains in future versions
60of the GPL, as needed to protect the freedom of users.
61
62 Finally, every program is threatened constantly by software patents.
63States should not allow patents to restrict development and use of
64software on general-purpose computers, but in those that do, we wish to
65avoid the special danger that patents applied to a free program could
66make it effectively proprietary. To prevent this, the GPL assures that
67patents cannot be used to render the program non-free.
68
69 The precise terms and conditions for copying, distribution and
70modification follow.
71
72 TERMS AND CONDITIONS
73
74 0. Definitions.
75
76 "This License" refers to version 3 of the GNU General Public License.
77
78 "Copyright" also means copyright-like laws that apply to other kinds of
79works, such as semiconductor masks.
80
81 "The Program" refers to any copyrightable work licensed under this
82License. Each licensee is addressed as "you". "Licensees" and
83"recipients" may be individuals or organizations.
84
85 To "modify" a work means to copy from or adapt all or part of the work
86in a fashion requiring copyright permission, other than the making of an
87exact copy. The resulting work is called a "modified version" of the
88earlier work or a work "based on" the earlier work.
89
90 A "covered work" means either the unmodified Program or a work based
91on the Program.
92
93 To "propagate" a work means to do anything with it that, without
94permission, would make you directly or secondarily liable for
95infringement under applicable copyright law, except executing it on a
96computer or modifying a private copy. Propagation includes copying,
97distribution (with or without modification), making available to the
98public, and in some countries other activities as well.
99
100 To "convey" a work means any kind of propagation that enables other
101parties to make or receive copies. Mere interaction with a user through
102a computer network, with no transfer of a copy, is not conveying.
103
104 An interactive user interface displays "Appropriate Legal Notices"
105to the extent that it includes a convenient and prominently visible
106feature that (1) displays an appropriate copyright notice, and (2)
107tells the user that there is no warranty for the work (except to the
108extent that warranties are provided), that licensees may convey the
109work under this License, and how to view a copy of this License. If
110the interface presents a list of user commands or options, such as a
111menu, a prominent item in the list meets this criterion.
112
113 1. Source Code.
114
115 The "source code" for a work means the preferred form of the work
116for making modifications to it. "Object code" means any non-source
117form of a work.
118
119 A "Standard Interface" means an interface that either is an official
120standard defined by a recognized standards body, or, in the case of
121interfaces specified for a particular programming language, one that
122is widely used among developers working in that language.
123
124 The "System Libraries" of an executable work include anything, other
125than the work as a whole, that (a) is included in the normal form of
126packaging a Major Component, but which is not part of that Major
127Component, and (b) serves only to enable use of the work with that
128Major Component, or to implement a Standard Interface for which an
129implementation is available to the public in source code form. A
130"Major Component", in this context, means a major essential component
131(kernel, window system, and so on) of the specific operating system
132(if any) on which the executable work runs, or a compiler used to
133produce the work, or an object code interpreter used to run it.
134
135 The "Corresponding Source" for a work in object code form means all
136the source code needed to generate, install, and (for an executable
137work) run the object code and to modify the work, including scripts to
138control those activities. However, it does not include the work's
139System Libraries, or general-purpose tools or generally available free
140programs which are used unmodified in performing those activities but
141which are not part of the work. For example, Corresponding Source
142includes interface definition files associated with source files for
143the work, and the source code for shared libraries and dynamically
144linked subprograms that the work is specifically designed to require,
145such as by intimate data communication or control flow between those
146subprograms and other parts of the work.
147
148 The Corresponding Source need not include anything that users
149can regenerate automatically from other parts of the Corresponding
150Source.
151
152 The Corresponding Source for a work in source code form is that
153same work.
154
155 2. Basic Permissions.
156
157 All rights granted under this License are granted for the term of
158copyright on the Program, and are irrevocable provided the stated
159conditions are met. This License explicitly affirms your unlimited
160permission to run the unmodified Program. The output from running a
161covered work is covered by this License only if the output, given its
162content, constitutes a covered work. This License acknowledges your
163rights of fair use or other equivalent, as provided by copyright law.
164
165 You may make, run and propagate covered works that you do not
166convey, without conditions so long as your license otherwise remains
167in force. You may convey covered works to others for the sole purpose
168of having them make modifications exclusively for you, or provide you
169with facilities for running those works, provided that you comply with
170the terms of this License in conveying all material for which you do
171not control copyright. Those thus making or running the covered works
172for you must do so exclusively on your behalf, under your direction
173and control, on terms that prohibit them from making any copies of
174your copyrighted material outside their relationship with you.
175
176 Conveying under any other circumstances is permitted solely under
177the conditions stated below. Sublicensing is not allowed; section 10
178makes it unnecessary.
179
180 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
181
182 No covered work shall be deemed part of an effective technological
183measure under any applicable law fulfilling obligations under article
18411 of the WIPO copyright treaty adopted on 20 December 1996, or
185similar laws prohibiting or restricting circumvention of such
186measures.
187
188 When you convey a covered work, you waive any legal power to forbid
189circumvention of technological measures to the extent such circumvention
190is effected by exercising rights under this License with respect to
191the covered work, and you disclaim any intention to limit operation or
192modification of the work as a means of enforcing, against the work's
193users, your or third parties' legal rights to forbid circumvention of
194technological measures.
195
196 4. Conveying Verbatim Copies.
197
198 You may convey verbatim copies of the Program's source code as you
199receive it, in any medium, provided that you conspicuously and
200appropriately publish on each copy an appropriate copyright notice;
201keep intact all notices stating that this License and any
202non-permissive terms added in accord with section 7 apply to the code;
203keep intact all notices of the absence of any warranty; and give all
204recipients a copy of this License along with the Program.
205
206 You may charge any price or no price for each copy that you convey,
207and you may offer support or warranty protection for a fee.
208
209 5. Conveying Modified Source Versions.
210
211 You may convey a work based on the Program, or the modifications to
212produce it from the Program, in the form of source code under the
213terms of section 4, provided that you also meet all of these conditions:
214
215 a) The work must carry prominent notices stating that you modified
216 it, and giving a relevant date.
217
218 b) The work must carry prominent notices stating that it is
219 released under this License and any conditions added under section
220 7. This requirement modifies the requirement in section 4 to
221 "keep intact all notices".
222
223 c) You must license the entire work, as a whole, under this
224 License to anyone who comes into possession of a copy. This
225 License will therefore apply, along with any applicable section 7
226 additional terms, to the whole of the work, and all its parts,
227 regardless of how they are packaged. This License gives no
228 permission to license the work in any other way, but it does not
229 invalidate such permission if you have separately received it.
230
231 d) If the work has interactive user interfaces, each must display
232 Appropriate Legal Notices; however, if the Program has interactive
233 interfaces that do not display Appropriate Legal Notices, your
234 work need not make them do so.
235
236 A compilation of a covered work with other separate and independent
237works, which are not by their nature extensions of the covered work,
238and which are not combined with it such as to form a larger program,
239in or on a volume of a storage or distribution medium, is called an
240"aggregate" if the compilation and its resulting copyright are not
241used to limit the access or legal rights of the compilation's users
242beyond what the individual works permit. Inclusion of a covered work
243in an aggregate does not cause this License to apply to the other
244parts of the aggregate.
245
246 6. Conveying Non-Source Forms.
247
248 You may convey a covered work in object code form under the terms
249of sections 4 and 5, provided that you also convey the
250machine-readable Corresponding Source under the terms of this License,
251in one of these ways:
252
253 a) Convey the object code in, or embodied in, a physical product
254 (including a physical distribution medium), accompanied by the
255 Corresponding Source fixed on a durable physical medium
256 customarily used for software interchange.
257
258 b) Convey the object code in, or embodied in, a physical product
259 (including a physical distribution medium), accompanied by a
260 written offer, valid for at least three years and valid for as
261 long as you offer spare parts or customer support for that product
262 model, to give anyone who possesses the object code either (1) a
263 copy of the Corresponding Source for all the software in the
264 product that is covered by this License, on a durable physical
265 medium customarily used for software interchange, for a price no
266 more than your reasonable cost of physically performing this
267 conveying of source, or (2) access to copy the
268 Corresponding Source from a network server at no charge.
269
270 c) Convey individual copies of the object code with a copy of the
271 written offer to provide the Corresponding Source. This
272 alternative is allowed only occasionally and noncommercially, and
273 only if you received the object code with such an offer, in accord
274 with subsection 6b.
275
276 d) Convey the object code by offering access from a designated
277 place (gratis or for a charge), and offer equivalent access to the
278 Corresponding Source in the same way through the same place at no
279 further charge. You need not require recipients to copy the
280 Corresponding Source along with the object code. If the place to
281 copy the object code is a network server, the Corresponding Source
282 may be on a different server (operated by you or a third party)
283 that supports equivalent copying facilities, provided you maintain
284 clear directions next to the object code saying where to find the
285 Corresponding Source. Regardless of what server hosts the
286 Corresponding Source, you remain obligated to ensure that it is
287 available for as long as needed to satisfy these requirements.
288
289 e) Convey the object code using peer-to-peer transmission, provided
290 you inform other peers where the object code and Corresponding
291 Source of the work are being offered to the general public at no
292 charge under subsection 6d.
293
294 A separable portion of the object code, whose source code is excluded
295from the Corresponding Source as a System Library, need not be
296included in conveying the object code work.
297
298 A "User Product" is either (1) a "consumer product", which means any
299tangible personal property which is normally used for personal, family,
300or household purposes, or (2) anything designed or sold for incorporation
301into a dwelling. In determining whether a product is a consumer product,
302doubtful cases shall be resolved in favor of coverage. For a particular
303product received by a particular user, "normally used" refers to a
304typical or common use of that class of product, regardless of the status
305of the particular user or of the way in which the particular user
306actually uses, or expects or is expected to use, the product. A product
307is a consumer product regardless of whether the product has substantial
308commercial, industrial or non-consumer uses, unless such uses represent
309the only significant mode of use of the product.
310
311 "Installation Information" for a User Product means any methods,
312procedures, authorization keys, or other information required to install
313and execute modified versions of a covered work in that User Product from
314a modified version of its Corresponding Source. The information must
315suffice to ensure that the continued functioning of the modified object
316code is in no case prevented or interfered with solely because
317modification has been made.
318
319 If you convey an object code work under this section in, or with, or
320specifically for use in, a User Product, and the conveying occurs as
321part of a transaction in which the right of possession and use of the
322User Product is transferred to the recipient in perpetuity or for a
323fixed term (regardless of how the transaction is characterized), the
324Corresponding Source conveyed under this section must be accompanied
325by the Installation Information. But this requirement does not apply
326if neither you nor any third party retains the ability to install
327modified object code on the User Product (for example, the work has
328been installed in ROM).
329
330 The requirement to provide Installation Information does not include a
331requirement to continue to provide support service, warranty, or updates
332for a work that has been modified or installed by the recipient, or for
333the User Product in which it has been modified or installed. Access to a
334network may be denied when the modification itself materially and
335adversely affects the operation of the network or violates the rules and
336protocols for communication across the network.
337
338 Corresponding Source conveyed, and Installation Information provided,
339in accord with this section must be in a format that is publicly
340documented (and with an implementation available to the public in
341source code form), and must require no special password or key for
342unpacking, reading or copying.
343
344 7. Additional Terms.
345
346 "Additional permissions" are terms that supplement the terms of this
347License by making exceptions from one or more of its conditions.
348Additional permissions that are applicable to the entire Program shall
349be treated as though they were included in this License, to the extent
350that they are valid under applicable law. If additional permissions
351apply only to part of the Program, that part may be used separately
352under those permissions, but the entire Program remains governed by
353this License without regard to the additional permissions.
354
355 When you convey a copy of a covered work, you may at your option
356remove any additional permissions from that copy, or from any part of
357it. (Additional permissions may be written to require their own
358removal in certain cases when you modify the work.) You may place
359additional permissions on material, added by you to a covered work,
360for which you have or can give appropriate copyright permission.
361
362 Notwithstanding any other provision of this License, for material you
363add to a covered work, you may (if authorized by the copyright holders of
364that material) supplement the terms of this License with terms:
365
366 a) Disclaiming warranty or limiting liability differently from the
367 terms of sections 15 and 16 of this License; or
368
369 b) Requiring preservation of specified reasonable legal notices or
370 author attributions in that material or in the Appropriate Legal
371 Notices displayed by works containing it; or
372
373 c) Prohibiting misrepresentation of the origin of that material, or
374 requiring that modified versions of such material be marked in
375 reasonable ways as different from the original version; or
376
377 d) Limiting the use for publicity purposes of names of licensors or
378 authors of the material; or
379
380 e) Declining to grant rights under trademark law for use of some
381 trade names, trademarks, or service marks; or
382
383 f) Requiring indemnification of licensors and authors of that
384 material by anyone who conveys the material (or modified versions of
385 it) with contractual assumptions of liability to the recipient, for
386 any liability that these contractual assumptions directly impose on
387 those licensors and authors.
388
389 All other non-permissive additional terms are considered "further
390restrictions" within the meaning of section 10. If the Program as you
391received it, or any part of it, contains a notice stating that it is
392governed by this License along with a term that is a further
393restriction, you may remove that term. If a license document contains
394a further restriction but permits relicensing or conveying under this
395License, you may add to a covered work material governed by the terms
396of that license document, provided that the further restriction does
397not survive such relicensing or conveying.
398
399 If you add terms to a covered work in accord with this section, you
400must place, in the relevant source files, a statement of the
401additional terms that apply to those files, or a notice indicating
402where to find the applicable terms.
403
404 Additional terms, permissive or non-permissive, may be stated in the
405form of a separately written license, or stated as exceptions;
406the above requirements apply either way.
407
408 8. Termination.
409
410 You may not propagate or modify a covered work except as expressly
411provided under this License. Any attempt otherwise to propagate or
412modify it is void, and will automatically terminate your rights under
413this License (including any patent licenses granted under the third
414paragraph of section 11).
415
416 However, if you cease all violation of this License, then your
417license from a particular copyright holder is reinstated (a)
418provisionally, unless and until the copyright holder explicitly and
419finally terminates your license, and (b) permanently, if the copyright
420holder fails to notify you of the violation by some reasonable means
421prior to 60 days after the cessation.
422
423 Moreover, your license from a particular copyright holder is
424reinstated permanently if the copyright holder notifies you of the
425violation by some reasonable means, this is the first time you have
426received notice of violation of this License (for any work) from that
427copyright holder, and you cure the violation prior to 30 days after
428your receipt of the notice.
429
430 Termination of your rights under this section does not terminate the
431licenses of parties who have received copies or rights from you under
432this License. If your rights have been terminated and not permanently
433reinstated, you do not qualify to receive new licenses for the same
434material under section 10.
435
436 9. Acceptance Not Required for Having Copies.
437
438 You are not required to accept this License in order to receive or
439run a copy of the Program. Ancillary propagation of a covered work
440occurring solely as a consequence of using peer-to-peer transmission
441to receive a copy likewise does not require acceptance. However,
442nothing other than this License grants you permission to propagate or
443modify any covered work. These actions infringe copyright if you do
444not accept this License. Therefore, by modifying or propagating a
445covered work, you indicate your acceptance of this License to do so.
446
447 10. Automatic Licensing of Downstream Recipients.
448
449 Each time you convey a covered work, the recipient automatically
450receives a license from the original licensors, to run, modify and
451propagate that work, subject to this License. You are not responsible
452for enforcing compliance by third parties with this License.
453
454 An "entity transaction" is a transaction transferring control of an
455organization, or substantially all assets of one, or subdividing an
456organization, or merging organizations. If propagation of a covered
457work results from an entity transaction, each party to that
458transaction who receives a copy of the work also receives whatever
459licenses to the work the party's predecessor in interest had or could
460give under the previous paragraph, plus a right to possession of the
461Corresponding Source of the work from the predecessor in interest, if
462the predecessor has it or can get it with reasonable efforts.
463
464 You may not impose any further restrictions on the exercise of the
465rights granted or affirmed under this License. For example, you may
466not impose a license fee, royalty, or other charge for exercise of
467rights granted under this License, and you may not initiate litigation
468(including a cross-claim or counterclaim in a lawsuit) alleging that
469any patent claim is infringed by making, using, selling, offering for
470sale, or importing the Program or any portion of it.
471
472 11. Patents.
473
474 A "contributor" is a copyright holder who authorizes use under this
475License of the Program or a work on which the Program is based. The
476work thus licensed is called the contributor's "contributor version".
477
478 A contributor's "essential patent claims" are all patent claims
479owned or controlled by the contributor, whether already acquired or
480hereafter acquired, that would be infringed by some manner, permitted
481by this License, of making, using, or selling its contributor version,
482but do not include claims that would be infringed only as a
483consequence of further modification of the contributor version. For
484purposes of this definition, "control" includes the right to grant
485patent sublicenses in a manner consistent with the requirements of
486this License.
487
488 Each contributor grants you a non-exclusive, worldwide, royalty-free
489patent license under the contributor's essential patent claims, to
490make, use, sell, offer for sale, import and otherwise run, modify and
491propagate the contents of its contributor version.
492
493 In the following three paragraphs, a "patent license" is any express
494agreement or commitment, however denominated, not to enforce a patent
495(such as an express permission to practice a patent or covenant not to
496sue for patent infringement). To "grant" such a patent license to a
497party means to make such an agreement or commitment not to enforce a
498patent against the party.
499
500 If you convey a covered work, knowingly relying on a patent license,
501and the Corresponding Source of the work is not available for anyone
502to copy, free of charge and under the terms of this License, through a
503publicly available network server or other readily accessible means,
504then you must either (1) cause the Corresponding Source to be so
505available, or (2) arrange to deprive yourself of the benefit of the
506patent license for this particular work, or (3) arrange, in a manner
507consistent with the requirements of this License, to extend the patent
508license to downstream recipients. "Knowingly relying" means you have
509actual knowledge that, but for the patent license, your conveying the
510covered work in a country, or your recipient's use of the covered work
511in a country, would infringe one or more identifiable patents in that
512country that you have reason to believe are valid.
513
514 If, pursuant to or in connection with a single transaction or
515arrangement, you convey, or propagate by procuring conveyance of, a
516covered work, and grant a patent license to some of the parties
517receiving the covered work authorizing them to use, propagate, modify
518or convey a specific copy of the covered work, then the patent license
519you grant is automatically extended to all recipients of the covered
520work and works based on it.
521
522 A patent license is "discriminatory" if it does not include within
523the scope of its coverage, prohibits the exercise of, or is
524conditioned on the non-exercise of one or more of the rights that are
525specifically granted under this License. You may not convey a covered
526work if you are a party to an arrangement with a third party that is
527in the business of distributing software, under which you make payment
528to the third party based on the extent of your activity of conveying
529the work, and under which the third party grants, to any of the
530parties who would receive the covered work from you, a discriminatory
531patent license (a) in connection with copies of the covered work
532conveyed by you (or copies made from those copies), or (b) primarily
533for and in connection with specific products or compilations that
534contain the covered work, unless you entered into that arrangement,
535or that patent license was granted, prior to 28 March 2007.
536
537 Nothing in this License shall be construed as excluding or limiting
538any implied license or other defenses to infringement that may
539otherwise be available to you under applicable patent law.
540
541 12. No Surrender of Others' Freedom.
542
543 If conditions are imposed on you (whether by court order, agreement or
544otherwise) that contradict the conditions of this License, they do not
545excuse you from the conditions of this License. If you cannot convey a
546covered work so as to satisfy simultaneously your obligations under this
547License and any other pertinent obligations, then as a consequence you may
548not convey it at all. For example, if you agree to terms that obligate you
549to collect a royalty for further conveying from those to whom you convey
550the Program, the only way you could satisfy both those terms and this
551License would be to refrain entirely from conveying the Program.
552
553 13. Use with the GNU Affero General Public License.
554
555 Notwithstanding any other provision of this License, you have
556permission to link or combine any covered work with a work licensed
557under version 3 of the GNU Affero General Public License into a single
558combined work, and to convey the resulting work. The terms of this
559License will continue to apply to the part which is the covered work,
560but the special requirements of the GNU Affero General Public License,
561section 13, concerning interaction through a network will apply to the
562combination as such.
563
564 14. Revised Versions of this License.
565
566 The Free Software Foundation may publish revised and/or new versions of
567the GNU General Public License from time to time. Such new versions will
568be similar in spirit to the present version, but may differ in detail to
569address new problems or concerns.
570
571 Each version is given a distinguishing version number. If the
572Program specifies that a certain numbered version of the GNU General
573Public License "or any later version" applies to it, you have the
574option of following the terms and conditions either of that numbered
575version or of any later version published by the Free Software
576Foundation. If the Program does not specify a version number of the
577GNU General Public License, you may choose any version ever published
578by the Free Software Foundation.
579
580 If the Program specifies that a proxy can decide which future
581versions of the GNU General Public License can be used, that proxy's
582public statement of acceptance of a version permanently authorizes you
583to choose that version for the Program.
584
585 Later license versions may give you additional or different
586permissions. However, no additional obligations are imposed on any
587author or copyright holder as a result of your choosing to follow a
588later version.
589
590 15. Disclaimer of Warranty.
591
592 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
593APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
594HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
595OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
596THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
597PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
598IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
599ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
600
601 16. Limitation of Liability.
602
603 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
604WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
605THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
606GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
607USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
608DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
609PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
610EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
611SUCH DAMAGES.
612
613 17. Interpretation of Sections 15 and 16.
614
615 If the disclaimer of warranty and limitation of liability provided
616above cannot be given local legal effect according to their terms,
617reviewing courts shall apply local law that most closely approximates
618an absolute waiver of all civil liability in connection with the
619Program, unless a warranty or assumption of liability accompanies a
620copy of the Program in return for a fee.
621
622 END OF TERMS AND CONDITIONS
623
624 How to Apply These Terms to Your New Programs
625
626 If you develop a new program, and you want it to be of the greatest
627possible use to the public, the best way to achieve this is to make it
628free software which everyone can redistribute and change under these terms.
629
630 To do so, attach the following notices to the program. It is safest
631to attach them to the start of each source file to most effectively
632state the exclusion of warranty; and each file should have at least
633the "copyright" line and a pointer to where the full notice is found.
634
635 <one line to give the program's name and a brief idea of what it does.>
636 Copyright (C) <year> <name of author>
637
638 This program is free software: you can redistribute it and/or modify
639 it under the terms of the GNU General Public License as published by
640 the Free Software Foundation, either version 3 of the License, or
641 (at your option) any later version.
642
643 This program is distributed in the hope that it will be useful,
644 but WITHOUT ANY WARRANTY; without even the implied warranty of
645 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
646 GNU General Public License for more details.
647
648 You should have received a copy of the GNU General Public License
649 along with this program. If not, see <http://www.gnu.org/licenses/>.
650
651Also add information on how to contact you by electronic and paper mail.
652
653 If the program does terminal interaction, make it output a short
654notice like this when it starts in an interactive mode:
655
656 <program> Copyright (C) <year> <name of author>
657 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
658 This is free software, and you are welcome to redistribute it
659 under certain conditions; type `show c' for details.
660
661The hypothetical commands `show w' and `show c' should show the appropriate
662parts of the General Public License. Of course, your program's commands
663might be different; for a GUI interface, you would use an "about box".
664
665 You should also get your employer (if you work as a programmer) or school,
666if any, to sign a "copyright disclaimer" for the program, if necessary.
667For more information on this, and how to apply and follow the GNU GPL, see
668<http://www.gnu.org/licenses/>.
669
670 The GNU General Public License does not permit incorporating your program
671into proprietary programs. If your program is a subroutine library, you
672may consider it more useful to permit linking proprietary applications with
673the library. If this is what you want to do, use the GNU Lesser General
674Public License instead of this License. But first, please read
675<http://www.gnu.org/philosophy/why-not-lgpl.html>.
676
0677
=== added file 'COPYING.LESSER'
--- COPYING.LESSER 1970-01-01 00:00:00 +0000
+++ COPYING.LESSER 2009-07-13 15:53:45 +0000
@@ -0,0 +1,165 @@
1 GNU LESSER GENERAL PUBLIC LICENSE
2 Version 3, 29 June 2007
3
4 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5 Everyone is permitted to copy and distribute verbatim copies
6 of this license document, but changing it is not allowed.
7
8
9 This version of the GNU Lesser General Public License incorporates
10the terms and conditions of version 3 of the GNU General Public
11License, supplemented by the additional permissions listed below.
12
13 0. Additional Definitions.
14
15 As used herein, "this License" refers to version 3 of the GNU Lesser
16General Public License, and the "GNU GPL" refers to version 3 of the GNU
17General Public License.
18
19 "The Library" refers to a covered work governed by this License,
20other than an Application or a Combined Work as defined below.
21
22 An "Application" is any work that makes use of an interface provided
23by the Library, but which is not otherwise based on the Library.
24Defining a subclass of a class defined by the Library is deemed a mode
25of using an interface provided by the Library.
26
27 A "Combined Work" is a work produced by combining or linking an
28Application with the Library. The particular version of the Library
29with which the Combined Work was made is also called the "Linked
30Version".
31
32 The "Minimal Corresponding Source" for a Combined Work means the
33Corresponding Source for the Combined Work, excluding any source code
34for portions of the Combined Work that, considered in isolation, are
35based on the Application, and not on the Linked Version.
36
37 The "Corresponding Application Code" for a Combined Work means the
38object code and/or source code for the Application, including any data
39and utility programs needed for reproducing the Combined Work from the
40Application, but excluding the System Libraries of the Combined Work.
41
42 1. Exception to Section 3 of the GNU GPL.
43
44 You may convey a covered work under sections 3 and 4 of this License
45without being bound by section 3 of the GNU GPL.
46
47 2. Conveying Modified Versions.
48
49 If you modify a copy of the Library, and, in your modifications, a
50facility refers to a function or data to be supplied by an Application
51that uses the facility (other than as an argument passed when the
52facility is invoked), then you may convey a copy of the modified
53version:
54
55 a) under this License, provided that you make a good faith effort to
56 ensure that, in the event an Application does not supply the
57 function or data, the facility still operates, and performs
58 whatever part of its purpose remains meaningful, or
59
60 b) under the GNU GPL, with none of the additional permissions of
61 this License applicable to that copy.
62
63 3. Object Code Incorporating Material from Library Header Files.
64
65 The object code form of an Application may incorporate material from
66a header file that is part of the Library. You may convey such object
67code under terms of your choice, provided that, if the incorporated
68material is not limited to numerical parameters, data structure
69layouts and accessors, or small macros, inline functions and templates
70(ten or fewer lines in length), you do both of the following:
71
72 a) Give prominent notice with each copy of the object code that the
73 Library is used in it and that the Library and its use are
74 covered by this License.
75
76 b) Accompany the object code with a copy of the GNU GPL and this license
77 document.
78
79 4. Combined Works.
80
81 You may convey a Combined Work under terms of your choice that,
82taken together, effectively do not restrict modification of the
83portions of the Library contained in the Combined Work and reverse
84engineering for debugging such modifications, if you also do each of
85the following:
86
87 a) Give prominent notice with each copy of the Combined Work that
88 the Library is used in it and that the Library and its use are
89 covered by this License.
90
91 b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 document.
93
94 c) For a Combined Work that displays copyright notices during
95 execution, include the copyright notice for the Library among
96 these notices, as well as a reference directing the user to the
97 copies of the GNU GPL and this license document.
98
99 d) Do one of the following:
100
101 0) Convey the Minimal Corresponding Source under the terms of this
102 License, and the Corresponding Application Code in a form
103 suitable for, and under terms that permit, the user to
104 recombine or relink the Application with a modified version of
105 the Linked Version to produce a modified Combined Work, in the
106 manner specified by section 6 of the GNU GPL for conveying
107 Corresponding Source.
108
109 1) Use a suitable shared library mechanism for linking with the
110 Library. A suitable mechanism is one that (a) uses at run time
111 a copy of the Library already present on the user's computer
112 system, and (b) will operate properly with a modified version
113 of the Library that is interface-compatible with the Linked
114 Version.
115
116 e) Provide Installation Information, but only if you would otherwise
117 be required to provide such information under section 6 of the
118 GNU GPL, and only to the extent that such information is
119 necessary to install and execute a modified version of the
120 Combined Work produced by recombining or relinking the
121 Application with a modified version of the Linked Version. (If
122 you use option 4d0, the Installation Information must accompany
123 the Minimal Corresponding Source and Corresponding Application
124 Code. If you use option 4d1, you must provide the Installation
125 Information in the manner specified by section 6 of the GNU GPL
126 for conveying Corresponding Source.)
127
128 5. Combined Libraries.
129
130 You may place library facilities that are a work based on the
131Library side by side in a single library together with other library
132facilities that are not Applications and are not covered by this
133License, and convey such a combined library under terms of your
134choice, if you do both of the following:
135
136 a) Accompany the combined library with a copy of the same work based
137 on the Library, uncombined with any other library facilities,
138 conveyed under the terms of this License.
139
140 b) Give prominent notice with the combined library that part of it
141 is a work based on the Library, and explaining where to find the
142 accompanying uncombined form of the same work.
143
144 6. Revised Versions of the GNU Lesser General Public License.
145
146 The Free Software Foundation may publish revised and/or new versions
147of the GNU Lesser General Public License from time to time. Such new
148versions will be similar in spirit to the present version, but may
149differ in detail to address new problems or concerns.
150
151 Each version is given a distinguishing version number. If the
152Library as you received it specifies that a certain numbered version
153of the GNU Lesser General Public License "or any later version"
154applies to it, you have the option of following the terms and
155conditions either of that published version or of any later version
156published by the Free Software Foundation. If the Library as you
157received it does not specify a version number of the GNU Lesser
158General Public License, you may choose any version of the GNU Lesser
159General Public License ever published by the Free Software Foundation.
160
161 If the Library as you received it specifies that a proxy can decide
162whether future versions of the GNU Lesser General Public License shall
163apply, that proxy's public statement of acceptance of any version is
164permanent authorization for you to choose that version for the
165Library.
0166
=== added file 'MANIFEST.in'
--- MANIFEST.in 1970-01-01 00:00:00 +0000
+++ MANIFEST.in 2009-09-11 19:08:53 +0000
@@ -0,0 +1,14 @@
1include COPYING COPYING.LESSER README
2recursive-include data *.tmpl
3include desktopcouch-pair.desktop.in
4include po/POTFILES.in
5include start-desktop-couchdb.sh
6include stop-desktop-couchdb.sh
7include desktopcouch/records/doc/records.txt
8include bin/*
9include docs/man/*
10include MANIFEST.in MANIFEST
11include org.desktopcouch.CouchDB.service
12include config/desktop-couch/compulsory-auth.ini
13recursive-include contrib *.py
14
015
=== added file 'README'
--- README 1970-01-01 00:00:00 +0000
+++ README 2009-08-07 12:43:11 +0000
@@ -0,0 +1,35 @@
1This is Desktop Couch, an infrastructure to place a CouchDB on every desktop
2and provide APIs and management tools for applications to store data within
3it and share that data between computers and to the cloud.
4
5= Technical notes =
6
7== Creating databases, design documents, and views ==
8
9Desktop Couch will automatically create databases and design documents for you
10on startup by inspecting the filesystem, meaning that you do not need to check
11in your application whether your views exist. It is of course possible to create
12views directly through the Records API, but having them created via the
13filesystem allows managing of the view definitions more easily (they are
14in separately editable files, which helps with version control) and packaging
15(the files can be separately stored in a distribution package and installed
16into the system-level XDG_DATA_DIR rather than the user-level folder).
17
18=== To create a database ===
19
20Create a file $XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/database.cfg
21
22This file can currently be empty (in future it may contain database setup and
23configuration information).
24
25=== To create a design document ===
26
27Create a file
28$XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/_design/DESIGN_DOC_NAME/views/VIEW_NAME/map.js
29containing the map function from your view.
30If you also require a reduce function for your view, create a file
31$XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/_design/DESIGN_DOC_NAME/views/VIEW_NAME/reduce.js.
32
33This is compatible with the filesystem view structure from CouchApp and
34CouchDBKit.
35
036
=== added directory 'bin'
=== added file 'bin/desktopcouch-pair'
--- bin/desktopcouch-pair 1970-01-01 00:00:00 +0000
+++ bin/desktopcouch-pair 2009-09-14 16:06:52 +0000
@@ -0,0 +1,905 @@
1#!/usr/bin/python
2# Copyright 2009 Canonical Ltd.
3#
4# This file is part of desktopcouch.
5#
6# desktopcouch is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Lesser General Public License version 3
8# as published by the Free Software Foundation.
9#
10# desktopcouch is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
17# vim: filetype=python expandtab smarttab
18
19__doc__ = """\
20Manage paired machines.
21
22A tool to set two local machines to replicate their couchdb instances to each
23other, or to set this machine to replicate to-and-from Ubuntu One (and perhaps
24other cloud services).
25
26Local-Pairing Authentication
27----------------------------
28
29One machine, Alice, sets herself to listen for invitations to pair with another
30machine. In doing so, Alice is discoverable via Zeroconf on local network.
31
32Another machine, Bob, sees the advertisement and generates a secret message and
33a public seed. Bob then computes the SHA512 digest of the secret, and sends an
34invitation to Alice, in the form of the hex digest concatenated with the public
35seed. Bob displays the secret to the user, who walks the secret over to Alice.
36
37Alice then computes the SHA512 digest of Bob's secret and compares it to be
38sure that the other machine is indeed the user's. Alice then concatenates
39Bob's secret and the public seed, and sends the resulting hex digest back to
40Bob to prove that she received the secret from the user. Alice sets herself to
41replicate to Bob.
42
43Bob computes the secret+publicseed digest and compares it with the received hex
44digest from Alice. When it matches, he sets himself to replicate to Alice.
45"""
46
47
48
49import logging
50import getpass
51import gettext
52# gettext implements "_" function. pylint: disable-msg=E0602
53import random
54import cgi
55
56from twisted.internet import gtk2reactor
57gtk2reactor.install()
58from twisted.internet.reactor import run as run_program
59from twisted.internet.reactor import stop as stop_program
60
61import pygtk
62pygtk.require('2.0')
63import gtk
64import gobject
65import pango
66
67from desktopcouch.pair.couchdb_pairing import couchdb_io
68from desktopcouch.pair.couchdb_pairing import network_io
69from desktopcouch.pair.couchdb_pairing import dbus_io
70from desktopcouch.pair.couchdb_pairing import couchdb_io
71from desktopcouch.pair import pairing_record_type
72from desktopcouch import local_files
73
74from desktopcouch.records.record import Record
75
76DISCOVERY_TOOL_VERSION = "1"
77
78def generate_secret(length=7):
79 """Create a secret that is easy to write and read. We hate ambiguity and
80 errors."""
81
82 pool = "abcdefghijklmnopqrstuvwxyz23456789#$%&*+=-"
83 return unmap_easily_mistaken(
84 "".join(random.choice(pool) for n in range(length)))
85
86def unmap_easily_mistaken(user_input):
87 """Returns ASCII-encoded text with visually-ambiguous characters crushed
88 down to some common atom."""
89
90 import string
91 easily_mistaken = string.maketrans("!10@", "lloo")
92 return string.translate(user_input.lower().encode("ascii"), easily_mistaken)
93
94def get_host_info():
95 """Create some text that hopefully identifies this host out of many."""
96
97 import platform
98
99 try:
100 uptime_seconds = int(float(file("/proc/uptime").read().split()[0]))
101 days, uptime_seconds = divmod(uptime_seconds, 60*60*24)
102 hours, uptime_seconds = divmod(uptime_seconds, 60*60)
103 minutes, seconds = divmod(uptime_seconds, 60)
104
105 # Is ISO8601 too nerdy? ...
106 #uptime_descr = ", up PT%(days)dD%(hours)dH%(minutes)dM" % locals()
107 uptime_descr = ", up %(days)dd %(hours)dh%(minutes)dm" % locals()
108 except OSError:
109 uptime_descr = ""
110
111 try:
112 return " ".join(platform.dist()) + uptime_descr
113 except AttributeError:
114 return platform.platform() + uptime_descr
115
116
117class Inviting:
118 """We're part of "Bob" in the module's story.
119
120 We see listeners on the network and send invitations to pair with us.
121 We generate a secret message and a public seed. We get the SHA512 hex
122 digest of the secret message, append the public seed and send it to
123 Alice. We also display the cleartext of the secret message to the
124 screen, so that the user can take it to the machine he thinks is Alice.
125
126 Eventually, we receive a message back from Alice. We compute the
127 secret message we started with concatenated with the public seed, and
128 if that matches Alice's message, then Alice must know the secret we
129 displayed to the user. We then set outselves to replicate to Alice."""
130
131 def delete_event(self, widget, event, data=None):
132 """User requested window be closed. False to propogate event."""
133 return False
134
135 def destroy(self, widget, data=None):
136 """The window is destroyed."""
137 self.inviter.close()
138
139 def auth_completed(self, remote_host, remote_id, remote_oauth):
140 """The auth stage is finished. Now pair with the remote host."""
141 pair_with_host(remote_host, remote_id, remote_oauth)
142 self.window.destroy()
143
144 def on_close(self):
145 """When a socket is closed, we should stop inviting. (?)"""
146 self.window.destroy()
147
148 def __init__(self, service, hostname, port):
149 self.logging = logging.getLogger(self.__class__.__name__)
150
151 self.hostname = hostname
152 self.port = port
153
154 self.window = gtk.Window()
155 self.window.set_border_width(6)
156 self.window.connect("delete_event", self.delete_event)
157 self.window.connect("destroy", self.destroy)
158 self.window.set_destroy_with_parent(True)
159 self.window.set_title(
160 _("Inviting %s to pair for CouchDB Pairing") % hostname)
161
162 secret_message = generate_secret()
163 self.secret_message = secret_message
164 self.public_seed = generate_secret()
165
166 self.inviter = network_io.start_send_invitation(hostname, port,
167 self.auth_completed, self.secret_message, self.public_seed,
168 self.on_close, couchdb_io.get_my_host_unique_id(create=True)[0],
169 local_files.get_oauth_tokens())
170
171 top_vbox = gtk.VBox()
172 self.window.add(top_vbox)
173
174 text = gtk.Label()
175 text.set_markup(_("""We're inviting %s to pair with\n""" +
176 """us, and to prove veracity of the invitation we\n""" +
177 """sent, it is waiting for you to tell it this secret:\n""" +
178 """<span font-size="xx-large" color="blue" weight="bold">""" +
179 """<tt>%s</tt></span> .""") %
180 (cgi.escape(service), cgi.escape(self.secret_message)))
181 text.set_justify(gtk.JUSTIFY_CENTER)
182 top_vbox.pack_start(text, False, False, 10)
183 text.show()
184
185 cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
186 cancel_button.set_border_width(3)
187 cancel_button.connect("clicked", lambda widget: self.window.destroy())
188 top_vbox.pack_end(cancel_button, False, False, 10)
189 cancel_button.show()
190
191 self.window.show_all()
192
193
194class AcceptInvitation:
195 """We're part of 'Alice' in this module's story.
196
197 We've received an invitation. We now send the other end a secret key. The
198 secret should make its way back to us via meatspace. We open a dialog
199 asking for that secret here, which we validate.
200
201 When we validate it, we add this end as replicating to the other end. The
202 other end should also set itself to replicate to us."""
203
204 def delete_event(self, widget, event, data=None):
205 """User requested window be closed. False to propogate event."""
206 return False
207
208 def destroy(self, widget, data=None):
209 """Window is destroyed."""
210 pass
211
212 def on_close(self):
213 """Handle communication channel being closed."""
214 # FIXME this is unimplemented and unused.
215 self.destroy()
216
217 def __init__(self, remote_host, is_secret_valid, send_valid_key,
218 remote_hostid, remote_oauth):
219 self.logging = logging.getLogger(self.__class__.__name__)
220
221 self.is_secret_valid = is_secret_valid
222 self.send_valid_key = send_valid_key
223
224 self.window = gtk.Window()
225 self.window.set_border_width(6)
226
227 self.logging.info("want to listen for invitations.")
228
229 self.window.connect("delete_event", self.delete_event)
230 self.window.connect("destroy", self.destroy)
231 self.window.set_destroy_with_parent(True)
232 self.window.set_title(_("Accepting Invitation"))
233
234 top_vbox = gtk.VBox()
235 top_vbox.show()
236 self.window.add(top_vbox)
237
238 self.remote_hostname = dbus_io.get_remote_hostname(remote_host)
239 self.remote_hostid = remote_hostid
240 self.remote_oauth = remote_oauth
241
242 description = gtk.Label(
243 _("To verify your pairing with %s, enter its secret.") %
244 self.remote_hostname)
245 description.show()
246 top_vbox.pack_start(description, False, False, 0)
247
248 self.entry_box = gtk.Entry(18)
249 self.entry_box.connect("activate", lambda widget: self.verify_secret())
250 #self.window.connect("activate", lambda widget: self.verify_secret())
251 self.entry_box.set_activates_default(True)
252 self.entry_box.show()
253 top_vbox.pack_start(self.entry_box, False, False, 0)
254
255 self.result = gtk.Label("")
256 self.result.show()
257 self.entry_box.connect("changed",
258 lambda widget: self.result.set_text(""))
259 top_vbox.pack_start(self.result, False, False, 0)
260
261 button_bar = gtk.HBox(homogeneous=True)
262 button_bar.show()
263 top_vbox.pack_end(button_bar, False, False, 10)
264
265 cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
266 cancel_button.set_border_width(3)
267 cancel_button.connect("clicked", lambda widget: self.window.destroy())
268 button_bar.pack_end(cancel_button, False, False, 10)
269 cancel_button.show()
270
271 connect_button = gtk.Button(_("Verify and connect"))
272 add_image = gtk.Image()
273 add_image.set_from_stock(gtk.STOCK_CONNECT, gtk.ICON_SIZE_BUTTON)
274 connect_button.set_image(add_image)
275 connect_button.set_border_width(3)
276 connect_button.connect("clicked", lambda widget: self.verify_secret())
277 button_bar.pack_end(connect_button, False, False, 10)
278 connect_button.show()
279
280 self.window.show_all()
281
282 def verify_secret(self):
283 """We got a sumbission from the user's fingers as to what he thinks
284 the secret is. We verify it, and if it's what the other end started
285 with, then it's okay to let us pair with it."""
286
287 proposed_secret = unmap_easily_mistaken(self.entry_box.get_text())
288 if self.is_secret_valid(proposed_secret):
289 pair_with_host(self.remote_hostname, self.remote_hostid,
290 self.remote_oauth)
291 self.send_valid_key(proposed_secret)
292 self.window.destroy()
293 else:
294 self.result.set_text("sorry, that is wrong")
295
296
297class Listening:
298 """We're part of 'Alice' in this module's story.
299
300 Window that starts listening for other machines to pick *us* to pair
301 with. There must be at least one of these on the network for pairing to
302 happen. We listen for a finite amount of time, and then stop."""
303
304 def delete_event(self, widget, event, data=None):
305 """User requested window be closed. False to propogate event."""
306 return False
307
308 def destroy(self, widget, data=None):
309 """Window is destroyed."""
310 self.timeout_counter = None
311
312 if self.advertisement is not None:
313 self.advertisement.die()
314 self.listener.close()
315
316 def receive_invitation_challenge(self, remote_address, is_secret_valid,
317 send_secret, remote_hostid, remote_oauth):
318 """When we receive an invitation, check its validity and if
319 it's what we expected, then continue and accept it."""
320
321 self.logging.warn("received invitation from %s", remote_address)
322 self.acceptor = AcceptInvitation(remote_address, is_secret_valid,
323 send_secret, remote_hostid, remote_oauth)
324 self.acceptor.window.connect("destroy",
325 lambda *args: setattr(self, "acceptor", None) and False)
326
327 def cancel_acceptor(self):
328 """Destroy window when connection canceled."""
329 self.acceptor.window.destroy()
330
331 def make_listener(self, couchdb_instance):
332 """Start listening for connections, and start advertising that
333 we're listening."""
334
335 self.listener = network_io.ListenForInvitations(
336 self.receive_invitation_challenge,
337 lambda: self.window.destroy(),
338 couchdb_io.get_my_host_unique_id(create=True)[0],
339 local_files.get_oauth_tokens())
340
341 listen_port = self.listener.get_local_port()
342
343 hostname, domainname = dbus_io.get_local_hostname()
344 username = getpass.getuser()
345 self.advertisement = dbus_io.PairAdvertisement(port=listen_port,
346 name="%s-%s-%d" % (hostname, username, listen_port),
347 text=dict(version=str(DISCOVERY_TOOL_VERSION),
348 description=get_host_info()))
349 self.advertisement.publish()
350 return hostname, username, listen_port
351
352 def __init__(self, couchdb_instance):
353 self.logging = logging.getLogger(self.__class__.__name__)
354
355 self.listener = None
356 self.listener_loop = None
357 self.advertisement = None
358 self.acceptor = None
359
360 self.timeout_counter = 180
361
362 self.window = gtk.Window()
363 self.window.set_border_width(6)
364
365 self.logging.info("want to listen for invitations.")
366
367 self.window.connect("delete_event", self.delete_event)
368 self.window.connect("destroy", self.destroy)
369 self.window.set_destroy_with_parent(True)
370 self.window.set_title(_("Waiting for CouchDB Pairing Invitations"))
371
372 top_vbox = gtk.VBox()
373 top_vbox.show()
374 self.window.add(top_vbox)
375
376 self.counter_text = gtk.Label("#")
377 self.counter_text.show()
378
379 top_vbox.pack_end(self.counter_text, False, False, 5)
380
381 button_bar = gtk.HBox(homogeneous=True)
382 button_bar.show()
383 top_vbox.pack_end(button_bar, False, False, 10)
384
385 cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
386 cancel_button.set_border_width(3)
387 cancel_button.connect("clicked", lambda *args: self.window.destroy())
388 button_bar.pack_end(cancel_button, False, False, 10)
389 cancel_button.show()
390
391 add_minute_button = gtk.Button(_("Add 60 seconds"))
392 add_image = gtk.Image()
393 add_image.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
394 add_minute_button.set_image(add_image)
395 add_minute_button.set_border_width(3)
396 add_minute_button.connect("clicked",
397 lambda widget: self.add_to_timeout_counter(60))
398 button_bar.pack_end(add_minute_button, False, False, 10)
399 add_minute_button.show()
400
401 hostid, userid, listen_port = self.make_listener(couchdb_instance)
402
403 text = gtk.Label()
404 text.set_markup(
405 _("""We're listening for invitations! From another\n""" +
406 """machine on this local network, run this\n""" +
407 """same tool and find the machine called\n""" +
408 """<span font-size="xx-large" weight="bold"><tt>""" +
409 """%s-%s-%d</tt></span> .""") %
410 (cgi.escape(hostid), cgi.escape(userid),
411 listen_port))
412 text.set_justify(gtk.JUSTIFY_CENTER)
413 top_vbox.pack_start(text, False, False, 10)
414 text.show()
415
416 self.update_counter_view()
417 self.window.show_all()
418
419 gobject.timeout_add(1000, self.decrement_counter)
420
421 def add_to_timeout_counter(self, seconds):
422 """The user wants more time. Add seconds to the clock."""
423 self.timeout_counter += seconds
424 self.update_counter_view()
425
426 def update_counter_view(self):
427 """Update the counter widget with pretty text."""
428 self.counter_text.set_text(
429 _("%d seconds remaining") % self.timeout_counter)
430
431 def decrement_counter(self):
432 """Tick! Decrement the counter and update the display."""
433 if self.timeout_counter is None:
434 return False
435
436 self.timeout_counter -= 1
437 if self.timeout_counter < 0:
438 self.window.destroy()
439 return False
440
441 self.update_counter_view()
442 return True
443
444
445class PickOrListen:
446 """Main top-level window that represents the life of the application."""
447
448 def delete_event(self, widget, event, data=None):
449 """User requested window be closed. False to propogate event."""
450 return False
451
452 def destroy(self, widget, data=None):
453 """The window was destroyed."""
454 stop_program()
455
456 def create_pick_pane(self, container):
457 """Set up the pane that contains what's necessary to choose an
458 already-listening tool instance. This sets up a "Bob" in the
459 module's story."""
460
461 # positions: host id, descr, host, port, cloud_name
462 self.listening_hosts = gtk.TreeStore(str, str, str, int, str)
463
464 import desktopcouch.replication_services as services
465
466 for srv_name in dir(services):
467 if srv_name.startswith("__"):
468 continue
469 srv = getattr(services, srv_name)
470 try:
471 if srv.is_active():
472 all_paired_cloud_servers = [x.key for x in
473 couchdb_io.get_pairings()]
474 if not srv_name in all_paired_cloud_servers:
475 self.listening_hosts.append(None,
476 [srv.name, srv.description, "", 0, srv_name])
477 except Exception, e:
478 self.logging.exception("service %r has errors", srv_name)
479
480 self.inviting = None # pylint: disable-msg=W0201
481
482 hostname_col = gtk.TreeViewColumn(_("hostname"))
483 hostid_col = gtk.TreeViewColumn(_("service name"))
484 description_col = gtk.TreeViewColumn(_("description"))
485
486 pick_box = gtk.VBox()
487 container.pack_start(pick_box, False, False, 10)
488
489 l = gtk.Label(_("Pick a listening host to invite it to pair with us."))
490 pick_box.pack_start(l, False, False, 0)
491 l.show()
492
493 tv = gtk.TreeView(self.listening_hosts)
494 tv.set_headers_visible(False)
495 tv.set_rules_hint(True)
496 tv.show()
497
498 def clicked(selection):
499 """An item in the list of services was clicked, so now we go
500 about inviting it to pair with us."""
501
502 model, iter = selection.get_selected()
503 if not iter:
504 return
505 service = model.get_value(iter, 0)
506 description = model.get_value(iter, 1)
507 hostname = model.get_value(iter, 2)
508 port = model.get_value(iter, 3)
509 service_name = model.get_value(iter, 4)
510
511 if service_name:
512 # Pairing with a cloud service, which doesn't do key exchange
513 pair_with_cloud_service(service_name)
514 # remove from listening list
515 self.listening_hosts.remove(iter)
516 # add to already-paired list
517 srv = getattr(services, service_name)
518 self.already_paired_hosts.append(None,
519 [service, _("paired just now"), hostname, port, service_name, None])
520 return
521
522 self.logging.info("connecting to %s:%s tcp to invite",
523 hostname, port)
524 if self.inviting != None:
525 self.inviting.window.destroy()
526 self.inviting = Inviting(service, hostname, port)
527 self.inviting.window.connect("destroy",
528 lambda *args: setattr(self, "inviting_window", None))
529
530 tv_selection = tv.get_selection()
531 tv_selection.connect("changed", clicked)
532
533 scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
534 scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
535 scrolled_window.set_border_width(10)
536 scrolled_window.add_with_viewport(tv)
537 scrolled_window.show()
538
539 pick_box.pack_start(scrolled_window, True, False, 0)
540
541 def add_service_to_list(name, description, host, port, version):
542 """When a zeroconf service appears, this adds it to the
543 listing of choices."""
544 self.listening_hosts.append(None, [name, description, host, port, None])
545
546 def remove_service_from_list(name):
547 """When a zeroconf service disappears, this finds it in the
548 listing and removes it as an option for picking."""
549
550 it = self.listening_hosts.get_iter_first()
551 while it is not None:
552 if self.listening_hosts.get_value(it, 0) == name:
553 self.listening_hosts.remove(it)
554 return True
555 it = self.listening_hosts.iter_next(it)
556
557 dbus_io.discover_services(add_service_to_list,
558 remove_service_from_list, show_local=False)
559
560 cell = gtk.CellRendererText()
561 tv.append_column(hostname_col)
562 hostname_col.pack_start(cell, True)
563 hostname_col.add_attribute(cell, 'text', 2)
564
565 cell = gtk.CellRendererText()
566 cell.set_property("weight", pango.WEIGHT_BOLD)
567 cell.set_property("weight-set", True)
568 tv.append_column(hostid_col)
569 hostid_col.pack_start(cell, True)
570 hostid_col.add_attribute(cell, 'text', 0)
571
572 cell = gtk.CellRendererText()
573 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
574 cell.set_property("ellipsize-set", True)
575 tv.append_column(description_col)
576 description_col.pack_start(cell, True)
577 description_col.add_attribute(cell, 'text', 1)
578
579 return pick_box
580
581 def create_already_paired_pane(self, container):
582 """Set up the pane that shows servers which are already paired."""
583
584 # positions: host id, descr, host, port, cloud_name, pairingid
585 self.already_paired_hosts = gtk.TreeStore(str, str, str, int, str, str)
586
587 import desktopcouch.replication_services as services
588 for already_paired_record in couchdb_io.get_pairings():
589 pid = already_paired_record.value["pairing_identifier"]
590 if "service_name" in already_paired_record.value:
591 srv_name = already_paired_record.value["service_name"]
592 if srv_name.startswith("__"):
593 continue
594 srv = getattr(services, srv_name)
595 if not srv.is_active():
596 continue
597 if already_paired_record.value.get("unpaired", False):
598 continue
599 nice_description = _("paired ") + \
600 already_paired_record.value.get("ctime",
601 _("unknown date"))
602 try:
603 self.already_paired_hosts.append(None,
604 [srv.name, nice_description, "", 0, srv_name, pid])
605 except Exception, e:
606 logging.error("Service %s had an error", srv_name, e)
607 elif "server" in already_paired_record.value:
608 hostname = already_paired_record.value["server"]
609 nice_description = _("paired ") + \
610 already_paired_record.value.get("ctime",
611 _("unknown date"))
612 self.already_paired_hosts.append(None,
613 [hostname, nice_description, None, 0, None, pid])
614 else:
615 logging.error("unknown pairing record %s",
616 already_paired_record)
617
618 hostid_col = gtk.TreeViewColumn(_("service name"))
619 description_col = gtk.TreeViewColumn(_("service name"))
620
621 pick_box = gtk.VBox()
622 container.pack_start(pick_box, False, False, 10)
623
624 l = gtk.Label(_("You're currently paired with these hosts. Click to unpair."))
625 pick_box.pack_start(l, False, False, 0)
626 l.show()
627
628 tv = gtk.TreeView(self.already_paired_hosts)
629 tv.set_headers_visible(False)
630 tv.set_rules_hint(True)
631 tv.show()
632
633 def clicked(selection):
634 """An item in the list of services was clicked, so now we go
635 about inviting it to pair with us."""
636
637 model, iter = selection.get_selected()
638 if not iter:
639 return
640 service = model.get_value(iter, 0)
641 hostname = model.get_value(iter, 2)
642 port = model.get_value(iter, 3)
643 service_name = model.get_value(iter, 4)
644 pid = model.get_value(iter, 5)
645
646 if service_name:
647 # delete record
648 for record in couchdb_io.get_pairings():
649 couchdb_io.remove_pairing(record.id, True)
650
651 # remove from already-paired list
652 self.already_paired_hosts.remove(iter)
653 # add to listening list
654 srv = getattr(services, service_name)
655 self.listening_hosts.append(None, [service, srv.description,
656 hostname, port, service_name])
657 return
658
659 # delete (really, mark as "unpaired")
660 for record in couchdb_io.get_pairings():
661 if record.value["pairing_identifier"] == pid:
662 couchdb_io.remove_pairing(record.id, False)
663 break
664
665 # remove from already-paired list
666 self.already_paired_hosts.remove(iter)
667 # do not add to listening list -- if it's listening then zeroconf
668 # will pick it up
669 return
670
671 tv_selection = tv.get_selection()
672 tv_selection.connect("changed", clicked)
673
674 scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
675 scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
676 scrolled_window.set_border_width(10)
677 scrolled_window.add_with_viewport(tv)
678 scrolled_window.show()
679
680 pick_box.pack_start(scrolled_window, True, False, 0)
681
682 cell = gtk.CellRendererText()
683 tv.append_column(hostid_col)
684 hostid_col.pack_start(cell, True)
685 hostid_col.add_attribute(cell, 'text', 0)
686
687 cell = gtk.CellRendererText()
688 cell.set_property("ellipsize", pango.ELLIPSIZE_END)
689 cell.set_property("ellipsize-set", True)
690 tv.append_column(description_col)
691 description_col.pack_start(cell, True)
692 description_col.add_attribute(cell, 'text', 1)
693
694 return pick_box
695
696 def create_single_listen_pane(self, container):
697 """This sets up an "Alice" from the module's story.
698
699 This assumes we're pairing a single, known local CouchDB instance,
700 instead of generic instances that we'd need more information to talk
701 about. Instead of using this function, one might use another that
702 lists local DBs as a way of picking one to pair. This function assumes
703 we know the answer to that."""
704
705 def listen(btn, couchdb_instance=None):
706 """When we decide to listen for invitations, this spawns the
707 advertising/information window, and disables the button that
708 causes/-ed us to get here (to prevent multiple instances)."""
709
710 if couchdb_instance is None:
711 # assume local for this user. FIXME
712 pass
713
714 btn.set_sensitive(False)
715 listening = Listening(couchdb_instance)
716 listening.window.connect("destroy",
717 lambda w: btn.set_sensitive(True))
718
719 padding = 6
720
721 listen_box = gtk.HBox()
722 container.pack_start(listen_box, False, False, 2)
723
724 l = gtk.Label(_("Add this host to the list for others to see?"))
725 listen_box.pack_start(l, True, False, padding)
726 l.show()
727
728 listen_button = gtk.Button(_("Listen for invitations"))
729 listen_button.connect("clicked", listen, None)
730 listen_box.pack_start(listen_button, True, False, padding)
731 listen_button.show()
732
733 return listen_box
734
735 def create_any_listen_pane(self, container):
736 """Unused. An example of where to start when we don't already have
737 a couchdb instance in mind to advertise. This should be the generic
738 version."""
739
740 l = gtk.Label(_("I also know of CouchDB sessions here. Pick one " +
741 "to add it to the invitation list for other computers to see."))
742
743 local_sharables_list = gtk.TreeStore(str, str)
744 # et c, et c
745 #some_row_in_list.connect("clicked", self.listen, target_db_info)
746
747 def __init__(self):
748
749
750 self.logging = logging.getLogger(self.__class__.__name__)
751
752 self.window = gtk.Window()
753
754 self.window.connect("delete_event", self.delete_event)
755 self.window.connect("destroy", self.destroy)
756
757 self.window.set_border_width(8)
758 self.window.set_title(_("CouchDB Pairing Tool"))
759
760 top_vbox = gtk.VBox()
761 self.window.add(top_vbox)
762
763 self.pick_pane = self.create_pick_pane(top_vbox)
764 self.listen_pane = self.create_single_listen_pane(top_vbox)
765 seperator = gtk.HSeparator()
766 top_vbox.pack_start(seperator, False, False, 10)
767 seperator.show()
768 self.already_paired_pane = self.create_already_paired_pane(top_vbox)
769
770 copyright = gtk.Label(_("Copyright 2009 Canonical"))
771 top_vbox.pack_end(copyright, False, False, 0)
772 copyright.show()
773
774 self.pick_pane.show()
775 self.listen_pane.show()
776 self.already_paired_pane.show()
777
778 top_vbox.show()
779 self.window.show()
780
781
782def pair_with_host(hostname, hostid, oauth_data):
783 """We've verified all is correct and authorized, so now we pair
784 the databases."""
785 logging.info("verified host %s/%s. Done!", hostname, hostid)
786
787 try:
788 result = couchdb_io.put_dynamic_paired_host(hostname, hostid, oauth_data)
789 assert result is not None
790 except Exception, e:
791 logging.exception("failure writing record for %s", hostname)
792 fail_note = gtk.MessageDialog(
793 parent=pick_or_listen.window,
794 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
795 buttons=gtk.BUTTONS_OK,
796 type=gtk.MESSAGE_ERROR,
797 message_format =_("Couldn't save pairing details for %s") % hostname)
798 fail_note.run()
799 fail_note.destroy()
800 return
801
802 success_note = gtk.Dialog(title=_("Paired with %(hostname)s") % locals(),
803 parent=pick_or_listen.window,
804 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
805 buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,))
806 text = gtk.Label(
807 _("Successfully paired with %(hostname)s.") % locals())
808 text.show()
809 content_box = success_note.get_content_area()
810 content_box.pack_start(text, True, True, 20)
811 success_note.connect("close",
812 lambda *args: pick_or_listen.window.destroy())
813 success_note.connect("response",
814 lambda *args: pick_or_listen.window.destroy())
815 success_note.show()
816
817
818def pair_with_cloud_service(service_name):
819 """Write a paired server record for the selected cloud service."""
820 try:
821 import desktopcouch.replication_services as services
822 srv = getattr(services, service_name)
823 oauth_data = srv.oauth_data()
824 result = couchdb_io.put_static_paired_service(oauth_data, service_name)
825 assert result != None
826 except Exception, e:
827 logging.exception("failure in module for service %r", service_name)
828 fail_note = gtk.MessageDialog(
829 parent=pick_or_listen.window,
830 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
831 buttons=gtk.BUTTONS_OK,
832 type=gtk.MESSAGE_ERROR,
833 message_format =_("Couldn't save pairing details for %r") % service_name)
834 fail_note.run()
835 fail_note.destroy()
836 return
837
838 success_note = gtk.MessageDialog(
839 parent=pick_or_listen.window,
840 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
841 buttons=gtk.BUTTONS_OK,
842 type=gtk.MESSAGE_INFO,
843 message_format =_("Successfully paired with %s") % service_name)
844 success_note.run()
845 success_note.destroy()
846
847
848def set_couchdb_bind_address():
849 from desktopcouch.records.server import CouchDatabase
850 from desktopcouch import local_files
851 bind_address = local_files.get_bind_address()
852
853 if bind_address not in ("127.0.0.1", "0.0.0.0", "::1", None):
854 logging.info("we're not qualified to change explicit address %s",
855 bind_address)
856 return False
857
858 db = CouchDatabase("management", create=True)
859 results = db.get_records(create_view=True)
860 count = 0
861 for row in results[pairing_record_type]:
862 # Is the record of something that probably connects back to us?
863 if "server" in row.value and row.value["server"] != "":
864 count += 1
865 logging.debug("paired back-connecting machine count is %d", count)
866 if count > 0:
867 couchdb_io.get_my_host_unique_id(create=True) # ensure self-id record
868 if ":" in bind_address:
869 want_bind_address = "::0" # IPv6 addr any
870 else:
871 want_bind_address = "0.0.0.0"
872 else:
873 if ":" in bind_address:
874 want_bind_address = "::1" # IPv6 loop back
875 else:
876 want_bind_address = "127.0.0.1"
877
878 if bind_address != want_bind_address:
879 local_files.set_bind_address(want_bind_address)
880 logging.warning("changing the desktopcouch bind address from %r to %r",
881 bind_address, want_bind_address)
882
883def main(args):
884 """Start execution."""
885 global pick_or_listen # pylint: disable-msg=W0601
886
887 logging.basicConfig(level=logging.DEBUG, format=
888 "%(asctime)s [%(process)d] %(name)s:%(levelname)s: %(message)s")
889
890 gettext.install("couchdb_pairing")
891
892 try:
893 logging.debug("starting couchdb pairing tool")
894 pick_or_listen = PickOrListen()
895 return run_program()
896 finally:
897 set_couchdb_bind_address()
898 logging.debug("exiting couchdb pairing tool")
899
900
901if __name__ == "__main__":
902 import sys
903 import desktopcouch
904 desktopcouch_port = desktopcouch.find_port()
905 main(sys.argv)
0906
=== added file 'bin/desktopcouch-service'
--- bin/desktopcouch-service 1970-01-01 00:00:00 +0000
+++ bin/desktopcouch-service 2009-09-11 03:18:00 +0000
@@ -0,0 +1,108 @@
1#!/usr/bin/python
2# Copyright 2009 Canonical Ltd.
3#
4# This file is part of desktopcouch.
5#
6# desktopcouch is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Lesser General Public License version 3
8# as published by the Free Software Foundation.
9#
10# desktopcouch is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
17#
18# Authors: Stuart Langridge <stuart.langridge@canonical.com>
19# Tim Cole <tim.cole@canonical.com>
20
21"""CouchDB port advertiser.
22
23A command-line utility which exports a
24desktopCouch.getPort method on the bus which returns
25that port, so other apps (specifically, the contacts API) can work out
26where CouchDB is running so it can be talked to.
27
28Calculates the port number by looking in the CouchDB log.
29
30If CouchDB is not running, then run the script to start it and then
31start advertising the port.
32
33This file should be started by D-Bus activation.
34
35"""
36
37import logging
38import logging.handlers
39from errno import ENOENT
40
41from twisted.internet import glib2reactor
42glib2reactor.install()
43from twisted.internet import reactor
44import dbus.service, gobject, os, errno, time
45
46import desktopcouch
47from desktopcouch import local_files
48from desktopcouch import replication
49
50
51class PortAdvertiser(dbus.service.Object):
52 "Advertise the discovered port number on the D-Bus Session bus"
53 def __init__(self, death):
54 bus_name = dbus.service.BusName("org.desktopcouch.CouchDB",
55 bus=dbus.SessionBus())
56 self.death = death
57 dbus.service.Object.__init__(self, object_path="/", bus_name=bus_name)
58
59 @dbus.service.method(dbus_interface='org.desktopcouch.CouchDB',
60 in_signature='', out_signature='i')
61 def getPort(self):
62 "Exported method to return the port"
63 return int(desktopcouch.find_port())
64
65 @dbus.service.method(dbus_interface='org.desktopcouch.CouchDB',
66 in_signature='', out_signature='')
67 def quit(self):
68 "Exported method to quit the program"
69 self.death()
70
71if __name__ == "__main__":
72 import xdg.BaseDirectory
73
74 log_directory = os.path.join(xdg.BaseDirectory.xdg_cache_home,
75 "ubuntuone/log")
76 try:
77 os.makedirs(log_directory)
78 except:
79 pass
80 rotating_log = logging.handlers.TimedRotatingFileHandler(
81 os.path.join(log_directory, "desktop-couch-replication.log"),
82 "midnight", 1, 14)
83 rotating_log.setLevel(logging.DEBUG)
84 formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
85 rotating_log.setFormatter(formatter)
86 logging.getLogger('').addHandler(rotating_log)
87 console_log = logging.StreamHandler()
88 console_log.setLevel(logging.WARNING)
89 console_log.setFormatter(logging.Formatter("%(asctime)s - %(message)s"))
90 logging.getLogger('').addHandler(console_log)
91 logging.getLogger('').setLevel(logging.DEBUG)
92
93
94 # Advertise the port
95 mainloop = reactor
96
97 portAdvertiser = PortAdvertiser(mainloop.stop)
98
99 replication_runtime = replication.set_up(desktopcouch.find_port)
100 try:
101
102 mainloop.run()
103
104 finally:
105 if replication_runtime:
106 print "Shutting down, but may be a moment to finish replication."
107 replication.tear_down(*replication_runtime)
108
0109
=== added file 'bin/desktopcouch-stop'
--- bin/desktopcouch-stop 1970-01-01 00:00:00 +0000
+++ bin/desktopcouch-stop 2009-07-24 19:32:52 +0000
@@ -0,0 +1,36 @@
1#!/usr/bin/python
2# Copyright 2009 Canonical Ltd.
3#
4# This file is part of desktopcouch.
5#
6# desktopcouch is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Lesser General Public License version 3
8# as published by the Free Software Foundation.
9#
10# desktopcouch is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
17#
18# Author: Stuart Langridge <stuart.langridge@canonical.com>
19"""
20Stop local CouchDB server.
21"""
22import subprocess, sys
23from desktopcouch import local_files
24
25local_exec = local_files.COUCH_EXEC_COMMAND + ["-k"]
26try:
27 retcode = subprocess.call(local_exec, shell=False)
28 if retcode < 0:
29 print >> sys.stderr, "Child was terminated by signal", -retcode
30 elif retcode > 0:
31 print >> sys.stderr, "Child returned", retcode
32except OSError, e:
33 print >> sys.stderr, "Execution failed: %s: %s" % (e, local_exec)
34 exit(1)
35
36
037
=== added directory 'config'
=== added directory 'config/desktop-couch'
=== added file 'config/desktop-couch/compulsory-auth.ini'
--- config/desktop-couch/compulsory-auth.ini 1970-01-01 00:00:00 +0000
+++ config/desktop-couch/compulsory-auth.ini 2009-09-11 19:08:53 +0000
@@ -0,0 +1,3 @@
1[couch_httpd_auth]
2require_valid_user = true
3
04
=== added directory 'contrib'
=== added file 'contrib/mocker.py'
--- contrib/mocker.py 1970-01-01 00:00:00 +0000
+++ contrib/mocker.py 2009-08-06 13:57:30 +0000
@@ -0,0 +1,2068 @@
1"""
2Copyright (c) 2007 Gustavo Niemeyer <gustavo@niemeyer.net>
3
4Graceful platform for test doubles in Python (mocks, stubs, fakes, and dummies).
5"""
6import __builtin__
7import tempfile
8import unittest
9import inspect
10import shutil
11import types
12import sys
13import os
14import gc
15
16
17if sys.version_info < (2, 4):
18 from sets import Set as set # pragma: nocover
19
20
21__all__ = ["Mocker", "expect", "IS", "CONTAINS", "IN", "MATCH",
22 "ANY", "ARGS", "KWARGS"]
23
24
25__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
26__license__ = "PSF License"
27__version__ = "0.10.1"
28
29
30ERROR_PREFIX = "[Mocker] "
31
32
33# --------------------------------------------------------------------
34# Exceptions
35
36class MatchError(AssertionError):
37 """Raised when an unknown expression is seen in playback mode."""
38
39
40# --------------------------------------------------------------------
41# Helper for chained-style calling.
42
43class expect(object):
44 """This is a simple helper that allows a different call-style.
45
46 With this class one can comfortably do chaining of calls to the
47 mocker object responsible by the object being handled. For instance::
48
49 expect(obj.attr).result(3).count(1, 2)
50
51 Is the same as::
52
53 obj.attr
54 mocker.result(3)
55 mocker.count(1, 2)
56
57 """
58
59 def __init__(self, mock, attr=None):
60 self._mock = mock
61 self._attr = attr
62
63 def __getattr__(self, attr):
64 return self.__class__(self._mock, attr)
65
66 def __call__(self, *args, **kwargs):
67 getattr(self._mock.__mocker__, self._attr)(*args, **kwargs)
68 return self
69
70
71# --------------------------------------------------------------------
72# Extensions to Python's unittest.
73
74class MockerTestCase(unittest.TestCase):
75 """unittest.TestCase subclass with Mocker support.
76
77 @ivar mocker: The mocker instance.
78
79 This is a convenience only. Mocker may easily be used with the
80 standard C{unittest.TestCase} class if wanted.
81
82 Test methods have a Mocker instance available on C{self.mocker}.
83 At the end of each test method, expectations of the mocker will
84 be verified, and any requested changes made to the environment
85 will be restored.
86
87 In addition to the integration with Mocker, this class provides
88 a few additional helper methods.
89 """
90
91 expect = expect
92
93 def __init__(self, methodName="runTest"):
94 # So here is the trick: we take the real test method, wrap it on
95 # a function that do the job we have to do, and insert it in the
96 # *instance* dictionary, so that getattr() will return our
97 # replacement rather than the class method.
98 test_method = getattr(self, methodName, None)
99 if test_method is not None:
100 def test_method_wrapper():
101 try:
102 result = test_method()
103 except:
104 raise
105 else:
106 if (self.mocker.is_recording() and
107 self.mocker.get_events()):
108 raise RuntimeError("Mocker must be put in replay "
109 "mode with self.mocker.replay()")
110 if (hasattr(result, "addCallback") and
111 hasattr(result, "addErrback")):
112 def verify(result):
113 self.mocker.verify()
114 return result
115 result.addCallback(verify)
116 else:
117 self.mocker.verify()
118 return result
119 # Copy all attributes from the original method..
120 for attr in dir(test_method):
121 # .. unless they're present in our wrapper already.
122 if not hasattr(test_method_wrapper, attr) or attr == "__doc__":
123 setattr(test_method_wrapper, attr,
124 getattr(test_method, attr))
125 setattr(self, methodName, test_method_wrapper)
126
127 # We could overload run() normally, but other well-known testing
128 # frameworks do it as well, and some of them won't call the super,
129 # which might mean that cleanup wouldn't happen. With that in mind,
130 # we make integration easier by using the following trick.
131 run_method = self.run
132 def run_wrapper(*args, **kwargs):
133 try:
134 return run_method(*args, **kwargs)
135 finally:
136 self.__cleanup()
137 self.run = run_wrapper
138
139 self.mocker = Mocker()
140
141 self.__cleanup_funcs = []
142 self.__cleanup_paths = []
143
144 super(MockerTestCase, self).__init__(methodName)
145
146 def __cleanup(self):
147 for path in self.__cleanup_paths:
148 if os.path.isfile(path):
149 os.unlink(path)
150 elif os.path.isdir(path):
151 shutil.rmtree(path)
152 self.mocker.restore()
153 for func, args, kwargs in self.__cleanup_funcs:
154 func(*args, **kwargs)
155
156 def addCleanup(self, func, *args, **kwargs):
157 self.__cleanup_funcs.append((func, args, kwargs))
158
159 def makeFile(self, content=None, suffix="", prefix="tmp", basename=None,
160 dirname=None, path=None):
161 """Create a temporary file and return the path to it.
162
163 @param content: Initial content for the file.
164 @param suffix: Suffix to be given to the file's basename.
165 @param prefix: Prefix to be given to the file's basename.
166 @param basename: Full basename for the file.
167 @param dirname: Put file inside this directory.
168
169 The file is removed after the test runs.
170 """
171 if path is not None:
172 self.__cleanup_paths.append(path)
173 elif basename is not None:
174 if dirname is None:
175 dirname = tempfile.mkdtemp()
176 self.__cleanup_paths.append(dirname)
177 path = os.path.join(dirname, basename)
178 else:
179 fd, path = tempfile.mkstemp(suffix, prefix, dirname)
180 self.__cleanup_paths.append(path)
181 os.close(fd)
182 if content is None:
183 os.unlink(path)
184 if content is not None:
185 file = open(path, "w")
186 file.write(content)
187 file.close()
188 return path
189
190 def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None):
191 """Create a temporary directory and return the path to it.
192
193 @param suffix: Suffix to be given to the file's basename.
194 @param prefix: Prefix to be given to the file's basename.
195 @param dirname: Put directory inside this parent directory.
196
197 The directory is removed after the test runs.
198 """
199 if path is not None:
200 os.makedirs(path)
201 else:
202 path = tempfile.mkdtemp(suffix, prefix, dirname)
203 self.__cleanup_paths.append(path)
204 return path
205
206 def failUnlessIs(self, first, second, msg=None):
207 """Assert that C{first} is the same object as C{second}."""
208 if first is not second:
209 raise self.failureException(msg or "%r is not %r" % (first, second))
210
211 def failIfIs(self, first, second, msg=None):
212 """Assert that C{first} is not the same object as C{second}."""
213 if first is second:
214 raise self.failureException(msg or "%r is %r" % (first, second))
215
216 def failUnlessIn(self, first, second, msg=None):
217 """Assert that C{first} is contained in C{second}."""
218 if first not in second:
219 raise self.failureException(msg or "%r not in %r" % (first, second))
220
221 def failUnlessStartsWith(self, first, second, msg=None):
222 """Assert that C{first} starts with C{second}."""
223 if first[:len(second)] != second:
224 raise self.failureException(msg or "%r doesn't start with %r" %
225 (first, second))
226
227 def failIfStartsWith(self, first, second, msg=None):
228 """Assert that C{first} doesn't start with C{second}."""
229 if first[:len(second)] == second:
230 raise self.failureException(msg or "%r starts with %r" %
231 (first, second))
232
233 def failUnlessEndsWith(self, first, second, msg=None):
234 """Assert that C{first} starts with C{second}."""
235 if first[len(first)-len(second):] != second:
236 raise self.failureException(msg or "%r doesn't end with %r" %
237 (first, second))
238
239 def failIfEndsWith(self, first, second, msg=None):
240 """Assert that C{first} doesn't start with C{second}."""
241 if first[len(first)-len(second):] == second:
242 raise self.failureException(msg or "%r ends with %r" %
243 (first, second))
244
245 def failIfIn(self, first, second, msg=None):
246 """Assert that C{first} is not contained in C{second}."""
247 if first in second:
248 raise self.failureException(msg or "%r in %r" % (first, second))
249
250 def failUnlessApproximates(self, first, second, tolerance, msg=None):
251 """Assert that C{first} is near C{second} by at most C{tolerance}."""
252 if abs(first - second) > tolerance:
253 raise self.failureException(msg or "abs(%r - %r) > %r" %
254 (first, second, tolerance))
255
256 def failIfApproximates(self, first, second, tolerance, msg=None):
257 """Assert that C{first} is far from C{second} by at least C{tolerance}.
258 """
259 if abs(first - second) <= tolerance:
260 raise self.failureException(msg or "abs(%r - %r) <= %r" %
261 (first, second, tolerance))
262
263 def failUnlessMethodsMatch(self, first, second):
264 """Assert that public methods in C{first} are present in C{second}.
265
266 This method asserts that all public methods found in C{first} are also
267 present in C{second} and accept the same arguments. C{first} may
268 have its own private methods, though, and may not have all methods
269 found in C{second}. Note that if a private method in C{first} matches
270 the name of one in C{second}, their specification is still compared.
271
272 This is useful to verify if a fake or stub class have the same API as
273 the real class being simulated.
274 """
275 first_methods = dict(inspect.getmembers(first, inspect.ismethod))
276 second_methods = dict(inspect.getmembers(second, inspect.ismethod))
277 for name, first_method in first_methods.items():
278 first_argspec = inspect.getargspec(first_method)
279 first_formatted = inspect.formatargspec(*first_argspec)
280
281 second_method = second_methods.get(name)
282 if second_method is None:
283 if name[:1] == "_":
284 continue # First may have its own private methods.
285 raise self.failureException("%s.%s%s not present in %s" %
286 (first.__name__, name, first_formatted, second.__name__))
287
288 second_argspec = inspect.getargspec(second_method)
289 if first_argspec != second_argspec:
290 second_formatted = inspect.formatargspec(*second_argspec)
291 raise self.failureException("%s.%s%s != %s.%s%s" %
292 (first.__name__, name, first_formatted,
293 second.__name__, name, second_formatted))
294
295
296 assertIs = failUnlessIs
297 assertIsNot = failIfIs
298 assertIn = failUnlessIn
299 assertNotIn = failIfIn
300 assertStartsWith = failUnlessStartsWith
301 assertNotStartsWith = failIfStartsWith
302 assertEndsWith = failUnlessEndsWith
303 assertNotEndsWith = failIfEndsWith
304 assertApproximates = failUnlessApproximates
305 assertNotApproximates = failIfApproximates
306 assertMethodsMatch = failUnlessMethodsMatch
307
308 # The following are missing in Python < 2.4.
309 assertTrue = unittest.TestCase.failUnless
310 assertFalse = unittest.TestCase.failIf
311
312 # The following is provided for compatibility with Twisted's trial.
313 assertIdentical = assertIs
314 assertNotIdentical = assertIsNot
315 failUnlessIdentical = failUnlessIs
316 failIfIdentical = failIfIs
317
318
319# --------------------------------------------------------------------
320# Mocker.
321
322class classinstancemethod(object):
323
324 def __init__(self, method):
325 self.method = method
326
327 def __get__(self, obj, cls=None):
328 def bound_method(*args, **kwargs):
329 return self.method(cls, obj, *args, **kwargs)
330 return bound_method
331
332
333class MockerBase(object):
334 """Controller of mock objects.
335
336 A mocker instance is used to command recording and replay of
337 expectations on any number of mock objects.
338
339 Expectations should be expressed for the mock object while in
340 record mode (the initial one) by using the mock object itself,
341 and using the mocker (and/or C{expect()} as a helper) to define
342 additional behavior for each event. For instance::
343
344 mock = mocker.mock()
345 mock.hello()
346 mocker.result("Hi!")
347 mocker.replay()
348 assert mock.hello() == "Hi!"
349 mock.restore()
350 mock.verify()
351
352 In this short excerpt a mock object is being created, then an
353 expectation of a call to the C{hello()} method was recorded, and
354 when called the method should return the value C{10}. Then, the
355 mocker is put in replay mode, and the expectation is satisfied by
356 calling the C{hello()} method, which indeed returns 10. Finally,
357 a call to the L{restore()} method is performed to undo any needed
358 changes made in the environment, and the L{verify()} method is
359 called to ensure that all defined expectations were met.
360
361 The same logic can be expressed more elegantly using the
362 C{with mocker:} statement, as follows::
363
364 mock = mocker.mock()
365 mock.hello()
366 mocker.result("Hi!")
367 with mocker:
368 assert mock.hello() == "Hi!"
369
370 Also, the MockerTestCase class, which integrates the mocker on
371 a unittest.TestCase subclass, may be used to reduce the overhead
372 of controlling the mocker. A test could be written as follows::
373
374 class SampleTest(MockerTestCase):
375
376 def test_hello(self):
377 mock = self.mocker.mock()
378 mock.hello()
379 self.mocker.result("Hi!")
380 self.mocker.replay()
381 self.assertEquals(mock.hello(), "Hi!")
382 """
383
384 _recorders = []
385
386 # For convenience only.
387 on = expect
388
389 class __metaclass__(type):
390 def __init__(self, name, bases, dict):
391 # Make independent lists on each subclass, inheriting from parent.
392 self._recorders = list(getattr(self, "_recorders", ()))
393
394 def __init__(self):
395 self._recorders = self._recorders[:]
396 self._events = []
397 self._recording = True
398 self._ordering = False
399 self._last_orderer = None
400
401 def is_recording(self):
402 """Return True if in recording mode, False if in replay mode.
403
404 Recording is the initial state.
405 """
406 return self._recording
407
408 def replay(self):
409 """Change to replay mode, where recorded events are reproduced.
410
411 If already in replay mode, the mocker will be restored, with all
412 expectations reset, and then put again in replay mode.
413
414 An alternative and more comfortable way to replay changes is
415 using the 'with' statement, as follows::
416
417 mocker = Mocker()
418 <record events>
419 with mocker:
420 <reproduce events>
421
422 The 'with' statement will automatically put mocker in replay
423 mode, and will also verify if all events were correctly reproduced
424 at the end (using L{verify()}), and also restore any changes done
425 in the environment (with L{restore()}).
426
427 Also check the MockerTestCase class, which integrates the
428 unittest.TestCase class with mocker.
429 """
430 if not self._recording:
431 for event in self._events:
432 event.restore()
433 else:
434 self._recording = False
435 for event in self._events:
436 event.replay()
437
438 def restore(self):
439 """Restore changes in the environment, and return to recording mode.
440
441 This should always be called after the test is complete (succeeding
442 or not). There are ways to call this method automatically on
443 completion (e.g. using a C{with mocker:} statement, or using the
444 L{MockerTestCase} class.
445 """
446 if not self._recording:
447 self._recording = True
448 for event in self._events:
449 event.restore()
450
451 def reset(self):
452 """Reset the mocker state.
453
454 This will restore environment changes, if currently in replay
455 mode, and then remove all events previously recorded.
456 """
457 if not self._recording:
458 self.restore()
459 self.unorder()
460 del self._events[:]
461
462 def get_events(self):
463 """Return all recorded events."""
464 return self._events[:]
465
466 def add_event(self, event):
467 """Add an event.
468
469 This method is used internally by the implementation, and
470 shouldn't be needed on normal mocker usage.
471 """
472 self._events.append(event)
473 if self._ordering:
474 orderer = event.add_task(Orderer(event.path))
475 if self._last_orderer:
476 orderer.add_dependency(self._last_orderer)
477 self._last_orderer = orderer
478 return event
479
480 def verify(self):
481 """Check if all expectations were met, and raise AssertionError if not.
482
483 The exception message will include a nice description of which
484 expectations were not met, and why.
485 """
486 errors = []
487 for event in self._events:
488 try:
489 event.verify()
490 except AssertionError, e:
491 error = str(e)
492 if not error:
493 raise RuntimeError("Empty error message from %r"
494 % event)
495 errors.append(error)
496 if errors:
497 message = [ERROR_PREFIX + "Unmet expectations:", ""]
498 for error in errors:
499 lines = error.splitlines()
500 message.append("=> " + lines.pop(0))
501 message.extend([" " + line for line in lines])
502 message.append("")
503 raise AssertionError(os.linesep.join(message))
504
505 def mock(self, spec_and_type=None, spec=None, type=None,
506 name=None, count=True):
507 """Return a new mock object.
508
509 @param spec_and_type: Handy positional argument which sets both
510 spec and type.
511 @param spec: Method calls will be checked for correctness against
512 the given class.
513 @param type: If set, the Mock's __class__ attribute will return
514 the given type. This will make C{isinstance()} calls
515 on the object work.
516 @param name: Name for the mock object, used in the representation of
517 expressions. The name is rarely needed, as it's usually
518 guessed correctly from the variable name used.
519 @param count: If set to false, expressions may be executed any number
520 of times, unless an expectation is explicitly set using
521 the L{count()} method. By default, expressions are
522 expected once.
523 """
524 if spec_and_type is not None:
525 spec = type = spec_and_type
526 return Mock(self, spec=spec, type=type, name=name, count=count)
527
528 def proxy(self, object, spec=True, type=True, name=None, count=True,
529 passthrough=True):
530 """Return a new mock object which proxies to the given object.
531
532 Proxies are useful when only part of the behavior of an object
533 is to be mocked. Unknown expressions may be passed through to
534 the real implementation implicitly (if the C{passthrough} argument
535 is True), or explicitly (using the L{passthrough()} method
536 on the event).
537
538 @param object: Real object to be proxied, and replaced by the mock
539 on replay mode. It may also be an "import path",
540 such as C{"time.time"}, in which case the object
541 will be the C{time} function from the C{time} module.
542 @param spec: Method calls will be checked for correctness against
543 the given object, which may be a class or an instance
544 where attributes will be looked up. Defaults to the
545 the C{object} parameter. May be set to None explicitly,
546 in which case spec checking is disabled. Checks may
547 also be disabled explicitly on a per-event basis with
548 the L{nospec()} method.
549 @param type: If set, the Mock's __class__ attribute will return
550 the given type. This will make C{isinstance()} calls
551 on the object work. Defaults to the type of the
552 C{object} parameter. May be set to None explicitly.
553 @param name: Name for the mock object, used in the representation of
554 expressions. The name is rarely needed, as it's usually
555 guessed correctly from the variable name used.
556 @param count: If set to false, expressions may be executed any number
557 of times, unless an expectation is explicitly set using
558 the L{count()} method. By default, expressions are
559 expected once.
560 @param passthrough: If set to False, passthrough of actions on the
561 proxy to the real object will only happen when
562 explicitly requested via the L{passthrough()}
563 method.
564 """
565 if isinstance(object, basestring):
566 if name is None:
567 name = object
568 import_stack = object.split(".")
569 attr_stack = []
570 while import_stack:
571 module_path = ".".join(import_stack)
572 try:
573 object = __import__(module_path, {}, {}, [""])
574 except ImportError:
575 attr_stack.insert(0, import_stack.pop())
576 if not import_stack:
577 raise
578 continue
579 else:
580 for attr in attr_stack:
581 object = getattr(object, attr)
582 break
583 if spec is True:
584 spec = object
585 if type is True:
586 type = __builtin__.type(object)
587 return Mock(self, spec=spec, type=type, object=object,
588 name=name, count=count, passthrough=passthrough)
589
590 def replace(self, object, spec=True, type=True, name=None, count=True,
591 passthrough=True):
592 """Create a proxy, and replace the original object with the mock.
593
594 On replay, the original object will be replaced by the returned
595 proxy in all dictionaries found in the running interpreter via
596 the garbage collecting system. This should cover module
597 namespaces, class namespaces, instance namespaces, and so on.
598
599 @param object: Real object to be proxied, and replaced by the mock
600 on replay mode. It may also be an "import path",
601 such as C{"time.time"}, in which case the object
602 will be the C{time} function from the C{time} module.
603 @param spec: Method calls will be checked for correctness against
604 the given object, which may be a class or an instance
605 where attributes will be looked up. Defaults to the
606 the C{object} parameter. May be set to None explicitly,
607 in which case spec checking is disabled. Checks may
608 also be disabled explicitly on a per-event basis with
609 the L{nospec()} method.
610 @param type: If set, the Mock's __class__ attribute will return
611 the given type. This will make C{isinstance()} calls
612 on the object work. Defaults to the type of the
613 C{object} parameter. May be set to None explicitly.
614 @param name: Name for the mock object, used in the representation of
615 expressions. The name is rarely needed, as it's usually
616 guessed correctly from the variable name used.
617 @param passthrough: If set to False, passthrough of actions on the
618 proxy to the real object will only happen when
619 explicitly requested via the L{passthrough()}
620 method.
621 """
622 mock = self.proxy(object, spec, type, name, count, passthrough)
623 event = self._get_replay_restore_event()
624 event.add_task(ProxyReplacer(mock))
625 return mock
626
627 def patch(self, object, spec=True):
628 """Patch an existing object to reproduce recorded events.
629
630 @param object: Class or instance to be patched.
631 @param spec: Method calls will be checked for correctness against
632 the given object, which may be a class or an instance
633 where attributes will be looked up. Defaults to the
634 the C{object} parameter. May be set to None explicitly,
635 in which case spec checking is disabled. Checks may
636 also be disabled explicitly on a per-event basis with
637 the L{nospec()} method.
638
639 The result of this method is still a mock object, which can be
640 used like any other mock object to record events. The difference
641 is that when the mocker is put on replay mode, the *real* object
642 will be modified to behave according to recorded expectations.
643
644 Patching works in individual instances, and also in classes.
645 When an instance is patched, recorded events will only be
646 considered on this specific instance, and other instances should
647 behave normally. When a class is patched, the reproduction of
648 events will be considered on any instance of this class once
649 created (collectively).
650
651 Observe that, unlike with proxies which catch only events done
652 through the mock object, *all* accesses to recorded expectations
653 will be considered; even these coming from the object itself
654 (e.g. C{self.hello()} is considered if this method was patched).
655 While this is a very powerful feature, and many times the reason
656 to use patches in the first place, it's important to keep this
657 behavior in mind.
658
659 Patching of the original object only takes place when the mocker
660 is put on replay mode, and the patched object will be restored
661 to its original state once the L{restore()} method is called
662 (explicitly, or implicitly with alternative conventions, such as
663 a C{with mocker:} block, or a MockerTestCase class).
664 """
665 if spec is True:
666 spec = object
667 patcher = Patcher()
668 event = self._get_replay_restore_event()
669 event.add_task(patcher)
670 mock = Mock(self, object=object, patcher=patcher,
671 passthrough=True, spec=spec)
672 object.__mocker_mock__ = mock
673 return mock
674
675 def act(self, path):
676 """This is called by mock objects whenever something happens to them.
677
678 This method is part of the implementation between the mocker
679 and mock objects.
680 """
681 if self._recording:
682 event = self.add_event(Event(path))
683 for recorder in self._recorders:
684 recorder(self, event)
685 return Mock(self, path)
686 else:
687 # First run events that may run, then run unsatisfied events, then
688 # ones not previously run. We put the index in the ordering tuple
689 # instead of the actual event because we want a stable sort
690 # (ordering between 2 events is undefined).
691 events = self._events
692 order = [(events[i].satisfied()*2 + events[i].has_run(), i)
693 for i in range(len(events))]
694 order.sort()
695 postponed = None
696 for weight, i in order:
697 event = events[i]
698 if event.matches(path):
699 if event.may_run(path):
700 return event.run(path)
701 elif postponed is None:
702 postponed = event
703 if postponed is not None:
704 return postponed.run(path)
705 raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path)
706
707 def get_recorders(cls, self):
708 """Return recorders associated with this mocker class or instance.
709
710 This method may be called on mocker instances and also on mocker
711 classes. See the L{add_recorder()} method for more information.
712 """
713 return (self or cls)._recorders[:]
714 get_recorders = classinstancemethod(get_recorders)
715
716 def add_recorder(cls, self, recorder):
717 """Add a recorder to this mocker class or instance.
718
719 @param recorder: Callable accepting C{(mocker, event)} as parameters.
720
721 This is part of the implementation of mocker.
722
723 All registered recorders are called for translating events that
724 happen during recording into expectations to be met once the state
725 is switched to replay mode.
726
727 This method may be called on mocker instances and also on mocker
728 classes. When called on a class, the recorder will be used by
729 all instances, and also inherited on subclassing. When called on
730 instances, the recorder is added only to the given instance.
731 """
732 (self or cls)._recorders.append(recorder)
733 return recorder
734 add_recorder = classinstancemethod(add_recorder)
735
736 def remove_recorder(cls, self, recorder):
737 """Remove the given recorder from this mocker class or instance.
738
739 This method may be called on mocker classes and also on mocker
740 instances. See the L{add_recorder()} method for more information.
741 """
742 (self or cls)._recorders.remove(recorder)
743 remove_recorder = classinstancemethod(remove_recorder)
744
745 def result(self, value):
746 """Make the last recorded event return the given value on replay.
747
748 @param value: Object to be returned when the event is replayed.
749 """
750 self.call(lambda *args, **kwargs: value)
751
752 def generate(self, sequence):
753 """Last recorded event will return a generator with the given sequence.
754
755 @param sequence: Sequence of values to be generated.
756 """
757 def generate(*args, **kwargs):
758 for value in sequence:
759 yield value
760 self.call(generate)
761
762 def throw(self, exception):
763 """Make the last recorded event raise the given exception on replay.
764
765 @param exception: Class or instance of exception to be raised.
766 """
767 def raise_exception(*args, **kwargs):
768 raise exception
769 self.call(raise_exception)
770
771 def call(self, func):
772 """Make the last recorded event cause the given function to be called.
773
774 @param func: Function to be called.
775
776 The result of the function will be used as the event result.
777 """
778 self._events[-1].add_task(FunctionRunner(func))
779
780 def count(self, min, max=False):
781 """Last recorded event must be replayed between min and max times.
782
783 @param min: Minimum number of times that the event must happen.
784 @param max: Maximum number of times that the event must happen. If
785 not given, it defaults to the same value of the C{min}
786 parameter. If set to None, there is no upper limit, and
787 the expectation is met as long as it happens at least
788 C{min} times.
789 """
790 event = self._events[-1]
791 for task in event.get_tasks():
792 if isinstance(task, RunCounter):
793 event.remove_task(task)
794 event.add_task(RunCounter(min, max))
795
796 def is_ordering(self):
797 """Return true if all events are being ordered.
798
799 See the L{order()} method.
800 """
801 return self._ordering
802
803 def unorder(self):
804 """Disable the ordered mode.
805
806 See the L{order()} method for more information.
807 """
808 self._ordering = False
809 self._last_orderer = None
810
811 def order(self, *path_holders):
812 """Create an expectation of order between two or more events.
813
814 @param path_holders: Objects returned as the result of recorded events.
815
816 By default, mocker won't force events to happen precisely in
817 the order they were recorded. Calling this method will change
818 this behavior so that events will only match if reproduced in
819 the correct order.
820
821 There are two ways in which this method may be used. Which one
822 is used in a given occasion depends only on convenience.
823
824 If no arguments are passed, the mocker will be put in a mode where
825 all the recorded events following the method call will only be met
826 if they happen in order. When that's used, the mocker may be put
827 back in unordered mode by calling the L{unorder()} method, or by
828 using a 'with' block, like so::
829
830 with mocker.ordered():
831 <record events>
832
833 In this case, only expressions in <record events> will be ordered,
834 and the mocker will be back in unordered mode after the 'with' block.
835
836 The second way to use it is by specifying precisely which events
837 should be ordered. As an example::
838
839 mock = mocker.mock()
840 expr1 = mock.hello()
841 expr2 = mock.world
842 expr3 = mock.x.y.z
843 mocker.order(expr1, expr2, expr3)
844
845 This method of ordering only works when the expression returns
846 another object.
847
848 Also check the L{after()} and L{before()} methods, which are
849 alternative ways to perform this.
850 """
851 if not path_holders:
852 self._ordering = True
853 return OrderedContext(self)
854
855 last_orderer = None
856 for path_holder in path_holders:
857 if type(path_holder) is Path:
858 path = path_holder
859 else:
860 path = path_holder.__mocker_path__
861 for event in self._events:
862 if event.path is path:
863 for task in event.get_tasks():
864 if isinstance(task, Orderer):
865 orderer = task
866 break
867 else:
868 orderer = Orderer(path)
869 event.add_task(orderer)
870 if last_orderer:
871 orderer.add_dependency(last_orderer)
872 last_orderer = orderer
873 break
874
875 def after(self, *path_holders):
876 """Last recorded event must happen after events referred to.
877
878 @param path_holders: Objects returned as the result of recorded events
879 which should happen before the last recorded event
880
881 As an example, the idiom::
882
883 expect(mock.x).after(mock.y, mock.z)
884
885 is an alternative way to say::
886
887 expr_x = mock.x
888 expr_y = mock.y
889 expr_z = mock.z
890 mocker.order(expr_y, expr_x)
891 mocker.order(expr_z, expr_x)
892
893 See L{order()} for more information.
894 """
895 last_path = self._events[-1].path
896 for path_holder in path_holders:
897 self.order(path_holder, last_path)
898
899 def before(self, *path_holders):
900 """Last recorded event must happen before events referred to.
901
902 @param path_holders: Objects returned as the result of recorded events
903 which should happen after the last recorded event
904
905 As an example, the idiom::
906
907 expect(mock.x).before(mock.y, mock.z)
908
909 is an alternative way to say::
910
911 expr_x = mock.x
912 expr_y = mock.y
913 expr_z = mock.z
914 mocker.order(expr_x, expr_y)
915 mocker.order(expr_x, expr_z)
916
917 See L{order()} for more information.
918 """
919 last_path = self._events[-1].path
920 for path_holder in path_holders:
921 self.order(last_path, path_holder)
922
923 def nospec(self):
924 """Don't check method specification of real object on last event.
925
926 By default, when using a mock created as the result of a call to
927 L{proxy()}, L{replace()}, and C{patch()}, or when passing the spec
928 attribute to the L{mock()} method, method calls on the given object
929 are checked for correctness against the specification of the real
930 object (or the explicitly provided spec).
931
932 This method will disable that check specifically for the last
933 recorded event.
934 """
935 event = self._events[-1]
936 for task in event.get_tasks():
937 if isinstance(task, SpecChecker):
938 event.remove_task(task)
939
940 def passthrough(self, result_callback=None):
941 """Make the last recorded event run on the real object once seen.
942
943 @param result_callback: If given, this function will be called with
944 the result of the *real* method call as the only argument.
945
946 This can only be used on proxies, as returned by the L{proxy()}
947 and L{replace()} methods, or on mocks representing patched objects,
948 as returned by the L{patch()} method.
949 """
950 event = self._events[-1]
951 if event.path.root_object is None:
952 raise TypeError("Mock object isn't a proxy")
953 event.add_task(PathExecuter(result_callback))
954
955 def __enter__(self):
956 """Enter in a 'with' context. This will run replay()."""
957 self.replay()
958 return self
959
960 def __exit__(self, type, value, traceback):
961 """Exit from a 'with' context.
962
963 This will run restore() at all times, but will only run verify()
964 if the 'with' block itself hasn't raised an exception. Exceptions
965 in that block are never swallowed.
966 """
967 self.restore()
968 if type is None:
969 self.verify()
970 return False
971
972 def _get_replay_restore_event(self):
973 """Return unique L{ReplayRestoreEvent}, creating if needed.
974
975 Some tasks only want to replay/restore. When that's the case,
976 they shouldn't act on other events during replay. Also, they
977 can all be put in a single event when that's the case. Thus,
978 we add a single L{ReplayRestoreEvent} as the first element of
979 the list.
980 """
981 if not self._events or type(self._events[0]) != ReplayRestoreEvent:
982 self._events.insert(0, ReplayRestoreEvent())
983 return self._events[0]
984
985
986class OrderedContext(object):
987
988 def __init__(self, mocker):
989 self._mocker = mocker
990
991 def __enter__(self):
992 return None
993
994 def __exit__(self, type, value, traceback):
995 self._mocker.unorder()
996
997
998class Mocker(MockerBase):
999 __doc__ = MockerBase.__doc__
1000
1001# Decorator to add recorders on the standard Mocker class.
1002recorder = Mocker.add_recorder
1003
1004
1005# --------------------------------------------------------------------
1006# Mock object.
1007
1008class Mock(object):
1009
1010 def __init__(self, mocker, path=None, name=None, spec=None, type=None,
1011 object=None, passthrough=False, patcher=None, count=True):
1012 self.__mocker__ = mocker
1013 self.__mocker_path__ = path or Path(self, object)
1014 self.__mocker_name__ = name
1015 self.__mocker_spec__ = spec
1016 self.__mocker_object__ = object
1017 self.__mocker_passthrough__ = passthrough
1018 self.__mocker_patcher__ = patcher
1019 self.__mocker_replace__ = False
1020 self.__mocker_type__ = type
1021 self.__mocker_count__ = count
1022
1023 def __mocker_act__(self, kind, args=(), kwargs={}, object=None):
1024 if self.__mocker_name__ is None:
1025 self.__mocker_name__ = find_object_name(self, 2)
1026 action = Action(kind, args, kwargs, self.__mocker_path__)
1027 path = self.__mocker_path__ + action
1028 if object is not None:
1029 path.root_object = object
1030 try:
1031 return self.__mocker__.act(path)
1032 except MatchError, exception:
1033 root_mock = path.root_mock
1034 if (path.root_object is not None and
1035 root_mock.__mocker_passthrough__):
1036 return path.execute(path.root_object)
1037 # Reinstantiate to show raise statement on traceback, and
1038 # also to make the traceback shown shorter.
1039 raise MatchError(str(exception))
1040 except AssertionError, e:
1041 lines = str(e).splitlines()
1042 message = [ERROR_PREFIX + "Unmet expectation:", ""]
1043 message.append("=> " + lines.pop(0))
1044 message.extend([" " + line for line in lines])
1045 message.append("")
1046 raise AssertionError(os.linesep.join(message))
1047
1048 def __getattribute__(self, name):
1049 if name.startswith("__mocker_"):
1050 return super(Mock, self).__getattribute__(name)
1051 if name == "__class__":
1052 if self.__mocker__.is_recording() or self.__mocker_type__ is None:
1053 return type(self)
1054 return self.__mocker_type__
1055 return self.__mocker_act__("getattr", (name,))
1056
1057 def __setattr__(self, name, value):
1058 if name.startswith("__mocker_"):
1059 return super(Mock, self).__setattr__(name, value)
1060 return self.__mocker_act__("setattr", (name, value))
1061
1062 def __delattr__(self, name):
1063 return self.__mocker_act__("delattr", (name,))
1064
1065 def __call__(self, *args, **kwargs):
1066 return self.__mocker_act__("call", args, kwargs)
1067
1068 def __contains__(self, value):
1069 return self.__mocker_act__("contains", (value,))
1070
1071 def __getitem__(self, key):
1072 return self.__mocker_act__("getitem", (key,))
1073
1074 def __setitem__(self, key, value):
1075 return self.__mocker_act__("setitem", (key, value))
1076
1077 def __delitem__(self, key):
1078 return self.__mocker_act__("delitem", (key,))
1079
1080 def __len__(self):
1081 # MatchError is turned on an AttributeError so that list() and
1082 # friends act properly when trying to get length hints on
1083 # something that doesn't offer them.
1084 try:
1085 result = self.__mocker_act__("len")
1086 except MatchError, e:
1087 raise AttributeError(str(e))
1088 if type(result) is Mock:
1089 return 0
1090 return result
1091
1092 def __nonzero__(self):
1093 try:
1094 return self.__mocker_act__("nonzero")
1095 except MatchError, e:
1096 return True
1097
1098 def __iter__(self):
1099 # XXX On py3k, when next() becomes __next__(), we'll be able
1100 # to return the mock itself because it will be considered
1101 # an iterator (we'll be mocking __next__ as well, which we
1102 # can't now).
1103 result = self.__mocker_act__("iter")
1104 if type(result) is Mock:
1105 return iter([])
1106 return result
1107
1108 # When adding a new action kind here, also add support for it on
1109 # Action.execute() and Path.__str__().
1110
1111
1112def find_object_name(obj, depth=0):
1113 """Try to detect how the object is named on a previous scope."""
1114 try:
1115 frame = sys._getframe(depth+1)
1116 except:
1117 return None
1118 for name, frame_obj in frame.f_locals.iteritems():
1119 if frame_obj is obj:
1120 return name
1121 self = frame.f_locals.get("self")
1122 if self is not None:
1123 try:
1124 items = list(self.__dict__.iteritems())
1125 except:
1126 pass
1127 else:
1128 for name, self_obj in items:
1129 if self_obj is obj:
1130 return name
1131 return None
1132
1133
1134# --------------------------------------------------------------------
1135# Action and path.
1136
1137class Action(object):
1138
1139 def __init__(self, kind, args, kwargs, path=None):
1140 self.kind = kind
1141 self.args = args
1142 self.kwargs = kwargs
1143 self.path = path
1144 self._execute_cache = {}
1145
1146 def __repr__(self):
1147 if self.path is None:
1148 return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs)
1149 return "Action(%r, %r, %r, %r)" % \
1150 (self.kind, self.args, self.kwargs, self.path)
1151
1152 def __eq__(self, other):
1153 return (self.kind == other.kind and
1154 self.args == other.args and
1155 self.kwargs == other.kwargs)
1156
1157 def __ne__(self, other):
1158 return not self.__eq__(other)
1159
1160 def matches(self, other):
1161 return (self.kind == other.kind and
1162 match_params(self.args, self.kwargs, other.args, other.kwargs))
1163
1164 def execute(self, object):
1165 # This caching scheme may fail if the object gets deallocated before
1166 # the action, as the id might get reused. It's somewhat easy to fix
1167 # that with a weakref callback. For our uses, though, the object
1168 # should never get deallocated before the action itself, so we'll
1169 # just keep it simple.
1170 if id(object) in self._execute_cache:
1171 return self._execute_cache[id(object)]
1172 execute = getattr(object, "__mocker_execute__", None)
1173 if execute is not None:
1174 result = execute(self, object)
1175 else:
1176 kind = self.kind
1177 if kind == "getattr":
1178 result = getattr(object, self.args[0])
1179 elif kind == "setattr":
1180 result = setattr(object, self.args[0], self.args[1])
1181 elif kind == "delattr":
1182 result = delattr(object, self.args[0])
1183 elif kind == "call":
1184 result = object(*self.args, **self.kwargs)
1185 elif kind == "contains":
1186 result = self.args[0] in object
1187 elif kind == "getitem":
1188 result = object[self.args[0]]
1189 elif kind == "setitem":
1190 result = object[self.args[0]] = self.args[1]
1191 elif kind == "delitem":
1192 del object[self.args[0]]
1193 result = None
1194 elif kind == "len":
1195 result = len(object)
1196 elif kind == "nonzero":
1197 result = bool(object)
1198 elif kind == "iter":
1199 result = iter(object)
1200 else:
1201 raise RuntimeError("Don't know how to execute %r kind." % kind)
1202 self._execute_cache[id(object)] = result
1203 return result
1204
1205
1206class Path(object):
1207
1208 def __init__(self, root_mock, root_object=None, actions=()):
1209 self.root_mock = root_mock
1210 self.root_object = root_object
1211 self.actions = tuple(actions)
1212 self.__mocker_replace__ = False
1213
1214 def parent_path(self):
1215 if not self.actions:
1216 return None
1217 return self.actions[-1].path
1218 parent_path = property(parent_path)
1219
1220 def __add__(self, action):
1221 """Return a new path which includes the given action at the end."""
1222 return self.__class__(self.root_mock, self.root_object,
1223 self.actions + (action,))
1224
1225 def __eq__(self, other):
1226 """Verify if the two paths are equal.
1227
1228 Two paths are equal if they refer to the same mock object, and
1229 have the actions with equal kind, args and kwargs.
1230 """
1231 if (self.root_mock is not other.root_mock or
1232 self.root_object is not other.root_object or
1233 len(self.actions) != len(other.actions)):
1234 return False
1235 for action, other_action in zip(self.actions, other.actions):
1236 if action != other_action:
1237 return False
1238 return True
1239
1240 def matches(self, other):
1241 """Verify if the two paths are equivalent.
1242
1243 Two paths are equal if they refer to the same mock object, and
1244 have the same actions performed on them.
1245 """
1246 if (self.root_mock is not other.root_mock or
1247 len(self.actions) != len(other.actions)):
1248 return False
1249 for action, other_action in zip(self.actions, other.actions):
1250 if not action.matches(other_action):
1251 return False
1252 return True
1253
1254 def execute(self, object):
1255 """Execute all actions sequentially on object, and return result.
1256 """
1257 for action in self.actions:
1258 object = action.execute(object)
1259 return object
1260
1261 def __str__(self):
1262 """Transform the path into a nice string such as obj.x.y('z')."""
1263 result = self.root_mock.__mocker_name__ or "<mock>"
1264 for action in self.actions:
1265 if action.kind == "getattr":
1266 result = "%s.%s" % (result, action.args[0])
1267 elif action.kind == "setattr":
1268 result = "%s.%s = %r" % (result, action.args[0], action.args[1])
1269 elif action.kind == "delattr":
1270 result = "del %s.%s" % (result, action.args[0])
1271 elif action.kind == "call":
1272 args = [repr(x) for x in action.args]
1273 items = list(action.kwargs.iteritems())
1274 items.sort()
1275 for pair in items:
1276 args.append("%s=%r" % pair)
1277 result = "%s(%s)" % (result, ", ".join(args))
1278 elif action.kind == "contains":
1279 result = "%r in %s" % (action.args[0], result)
1280 elif action.kind == "getitem":
1281 result = "%s[%r]" % (result, action.args[0])
1282 elif action.kind == "setitem":
1283 result = "%s[%r] = %r" % (result, action.args[0],
1284 action.args[1])
1285 elif action.kind == "delitem":
1286 result = "del %s[%r]" % (result, action.args[0])
1287 elif action.kind == "len":
1288 result = "len(%s)" % result
1289 elif action.kind == "nonzero":
1290 result = "bool(%s)" % result
1291 elif action.kind == "iter":
1292 result = "iter(%s)" % result
1293 else:
1294 raise RuntimeError("Don't know how to format kind %r" %
1295 action.kind)
1296 return result
1297
1298
1299class SpecialArgument(object):
1300 """Base for special arguments for matching parameters."""
1301
1302 def __init__(self, object=None):
1303 self.object = object
1304
1305 def __repr__(self):
1306 if self.object is None:
1307 return self.__class__.__name__
1308 else:
1309 return "%s(%r)" % (self.__class__.__name__, self.object)
1310
1311 def matches(self, other):
1312 return True
1313
1314 def __eq__(self, other):
1315 return type(other) == type(self) and self.object == other.object
1316
1317
1318class ANY(SpecialArgument):
1319 """Matches any single argument."""
1320
1321ANY = ANY()
1322
1323
1324class ARGS(SpecialArgument):
1325 """Matches zero or more positional arguments."""
1326
1327ARGS = ARGS()
1328
1329
1330class KWARGS(SpecialArgument):
1331 """Matches zero or more keyword arguments."""
1332
1333KWARGS = KWARGS()
1334
1335
1336class IS(SpecialArgument):
1337
1338 def matches(self, other):
1339 return self.object is other
1340
1341 def __eq__(self, other):
1342 return type(other) == type(self) and self.object is other.object
1343
1344
1345class CONTAINS(SpecialArgument):
1346
1347 def matches(self, other):
1348 try:
1349 other.__contains__
1350 except AttributeError:
1351 try:
1352 iter(other)
1353 except TypeError:
1354 # If an object can't be iterated, and has no __contains__
1355 # hook, it'd blow up on the test below. We test this in
1356 # advance to prevent catching more errors than we really
1357 # want.
1358 return False
1359 return self.object in other
1360
1361
1362class IN(SpecialArgument):
1363
1364 def matches(self, other):
1365 return other in self.object
1366
1367
1368class MATCH(SpecialArgument):
1369
1370 def matches(self, other):
1371 return bool(self.object(other))
1372
1373 def __eq__(self, other):
1374 return type(other) == type(self) and self.object is other.object
1375
1376
1377def match_params(args1, kwargs1, args2, kwargs2):
1378 """Match the two sets of parameters, considering special parameters."""
1379
1380 has_args = ARGS in args1
1381 has_kwargs = KWARGS in args1
1382
1383 if has_kwargs:
1384 args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS]
1385 elif len(kwargs1) != len(kwargs2):
1386 return False
1387
1388 if not has_args and len(args1) != len(args2):
1389 return False
1390
1391 # Either we have the same number of kwargs, or unknown keywords are
1392 # accepted (KWARGS was used), so check just the ones in kwargs1.
1393 for key, arg1 in kwargs1.iteritems():
1394 if key not in kwargs2:
1395 return False
1396 arg2 = kwargs2[key]
1397 if isinstance(arg1, SpecialArgument):
1398 if not arg1.matches(arg2):
1399 return False
1400 elif arg1 != arg2:
1401 return False
1402
1403 # Keywords match. Now either we have the same number of
1404 # arguments, or ARGS was used. If ARGS wasn't used, arguments
1405 # must match one-on-one necessarily.
1406 if not has_args:
1407 for arg1, arg2 in zip(args1, args2):
1408 if isinstance(arg1, SpecialArgument):
1409 if not arg1.matches(arg2):
1410 return False
1411 elif arg1 != arg2:
1412 return False
1413 return True
1414
1415 # Easy choice. Keywords are matching, and anything on args is accepted.
1416 if (ARGS,) == args1:
1417 return True
1418
1419 # We have something different there. If we don't have positional
1420 # arguments on the original call, it can't match.
1421 if not args2:
1422 # Unless we have just several ARGS (which is bizarre, but..).
1423 for arg1 in args1:
1424 if arg1 is not ARGS:
1425 return False
1426 return True
1427
1428 # Ok, all bets are lost. We have to actually do the more expensive
1429 # matching. This is an algorithm based on the idea of the Levenshtein
1430 # Distance between two strings, but heavily hacked for this purpose.
1431 args2l = len(args2)
1432 if args1[0] is ARGS:
1433 args1 = args1[1:]
1434 array = [0]*args2l
1435 else:
1436 array = [1]*args2l
1437 for i in range(len(args1)):
1438 last = array[0]
1439 if args1[i] is ARGS:
1440 for j in range(1, args2l):
1441 last, array[j] = array[j], min(array[j-1], array[j], last)
1442 else:
1443 array[0] = i or int(args1[i] != args2[0])
1444 for j in range(1, args2l):
1445 last, array[j] = array[j], last or int(args1[i] != args2[j])
1446 if 0 not in array:
1447 return False
1448 if array[-1] != 0:
1449 return False
1450 return True
1451
1452
1453# --------------------------------------------------------------------
1454# Event and task base.
1455
1456class Event(object):
1457 """Aggregation of tasks that keep track of a recorded action.
1458
1459 An event represents something that may or may not happen while the
1460 mocked environment is running, such as an attribute access, or a
1461 method call. The event is composed of several tasks that are
1462 orchestrated together to create a composed meaning for the event,
1463 including for which actions it should be run, what happens when it
1464 runs, and what's the expectations about the actions run.
1465 """
1466
1467 def __init__(self, path=None):
1468 self.path = path
1469 self._tasks = []
1470 self._has_run = False
1471
1472 def add_task(self, task):
1473 """Add a new task to this taks."""
1474 self._tasks.append(task)
1475 return task
1476
1477 def remove_task(self, task):
1478 self._tasks.remove(task)
1479
1480 def get_tasks(self):
1481 return self._tasks[:]
1482
1483 def matches(self, path):
1484 """Return true if *all* tasks match the given path."""
1485 for task in self._tasks:
1486 if not task.matches(path):
1487 return False
1488 return bool(self._tasks)
1489
1490 def has_run(self):
1491 return self._has_run
1492
1493 def may_run(self, path):
1494 """Verify if any task would certainly raise an error if run.
1495
1496 This will call the C{may_run()} method on each task and return
1497 false if any of them returns false.
1498 """
1499 for task in self._tasks:
1500 if not task.may_run(path):
1501 return False
1502 return True
1503
1504 def run(self, path):
1505 """Run all tasks with the given action.
1506
1507 @param path: The path of the expression run.
1508
1509 Running an event means running all of its tasks individually and in
1510 order. An event should only ever be run if all of its tasks claim to
1511 match the given action.
1512
1513 The result of this method will be the last result of a task
1514 which isn't None, or None if they're all None.
1515 """
1516 self._has_run = True
1517 result = None
1518 errors = []
1519 for task in self._tasks:
1520 try:
1521 task_result = task.run(path)
1522 except AssertionError, e:
1523 error = str(e)
1524 if not error:
1525 raise RuntimeError("Empty error message from %r" % task)
1526 errors.append(error)
1527 else:
1528 if task_result is not None:
1529 result = task_result
1530 if errors:
1531 message = [str(self.path)]
1532 if str(path) != message[0]:
1533 message.append("- Run: %s" % path)
1534 for error in errors:
1535 lines = error.splitlines()
1536 message.append("- " + lines.pop(0))
1537 message.extend([" " + line for line in lines])
1538 raise AssertionError(os.linesep.join(message))
1539 return result
1540
1541 def satisfied(self):
1542 """Return true if all tasks are satisfied.
1543
1544 Being satisfied means that there are no unmet expectations.
1545 """
1546 for task in self._tasks:
1547 try:
1548 task.verify()
1549 except AssertionError:
1550 return False
1551 return True
1552
1553 def verify(self):
1554 """Run verify on all tasks.
1555
1556 The verify method is supposed to raise an AssertionError if the
1557 task has unmet expectations, with a one-line explanation about
1558 why this item is unmet. This method should be safe to be called
1559 multiple times without side effects.
1560 """
1561 errors = []
1562 for task in self._tasks:
1563 try:
1564 task.verify()
1565 except AssertionError, e:
1566 error = str(e)
1567 if not error:
1568 raise RuntimeError("Empty error message from %r" % task)
1569 errors.append(error)
1570 if errors:
1571 message = [str(self.path)]
1572 for error in errors:
1573 lines = error.splitlines()
1574 message.append("- " + lines.pop(0))
1575 message.extend([" " + line for line in lines])
1576 raise AssertionError(os.linesep.join(message))
1577
1578 def replay(self):
1579 """Put all tasks in replay mode."""
1580 self._has_run = False
1581 for task in self._tasks:
1582 task.replay()
1583
1584 def restore(self):
1585 """Restore the state of all tasks."""
1586 for task in self._tasks:
1587 task.restore()
1588
1589
1590class ReplayRestoreEvent(Event):
1591 """Helper event for tasks which need replay/restore but shouldn't match."""
1592
1593 def matches(self, path):
1594 return False
1595
1596
1597class Task(object):
1598 """Element used to track one specific aspect on an event.
1599
1600 A task is responsible for adding any kind of logic to an event.
1601 Examples of that are counting the number of times the event was
1602 made, verifying parameters if any, and so on.
1603 """
1604
1605 def matches(self, path):
1606 """Return true if the task is supposed to be run for the given path.
1607 """
1608 return True
1609
1610 def may_run(self, path):
1611 """Return false if running this task would certainly raise an error."""
1612 return True
1613
1614 def run(self, path):
1615 """Perform the task item, considering that the given action happened.
1616 """
1617
1618 def verify(self):
1619 """Raise AssertionError if expectations for this item are unmet.
1620
1621 The verify method is supposed to raise an AssertionError if the
1622 task has unmet expectations, with a one-line explanation about
1623 why this item is unmet. This method should be safe to be called
1624 multiple times without side effects.
1625 """
1626
1627 def replay(self):
1628 """Put the task in replay mode.
1629
1630 Any expectations of the task should be reset.
1631 """
1632
1633 def restore(self):
1634 """Restore any environmental changes made by the task.
1635
1636 Verify should continue to work after this is called.
1637 """
1638
1639
1640# --------------------------------------------------------------------
1641# Task implementations.
1642
1643class OnRestoreCaller(Task):
1644 """Call a given callback when restoring."""
1645
1646 def __init__(self, callback):
1647 self._callback = callback
1648
1649 def restore(self):
1650 self._callback()
1651
1652
1653class PathMatcher(Task):
1654 """Match the action path against a given path."""
1655
1656 def __init__(self, path):
1657 self.path = path
1658
1659 def matches(self, path):
1660 return self.path.matches(path)
1661
1662def path_matcher_recorder(mocker, event):
1663 event.add_task(PathMatcher(event.path))
1664
1665Mocker.add_recorder(path_matcher_recorder)
1666
1667
1668class RunCounter(Task):
1669 """Task which verifies if the number of runs are within given boundaries.
1670 """
1671
1672 def __init__(self, min, max=False):
1673 self.min = min
1674 if max is None:
1675 self.max = sys.maxint
1676 elif max is False:
1677 self.max = min
1678 else:
1679 self.max = max
1680 self._runs = 0
1681
1682 def replay(self):
1683 self._runs = 0
1684
1685 def may_run(self, path):
1686 return self._runs < self.max
1687
1688 def run(self, path):
1689 self._runs += 1
1690 if self._runs > self.max:
1691 self.verify()
1692
1693 def verify(self):
1694 if not self.min <= self._runs <= self.max:
1695 if self._runs < self.min:
1696 raise AssertionError("Performed fewer times than expected.")
1697 raise AssertionError("Performed more times than expected.")
1698
1699
1700class ImplicitRunCounter(RunCounter):
1701 """RunCounter inserted by default on any event.
1702
1703 This is a way to differentiate explicitly added counters and
1704 implicit ones.
1705 """
1706
1707def run_counter_recorder(mocker, event):
1708 """Any event may be repeated once, unless disabled by default."""
1709 if event.path.root_mock.__mocker_count__:
1710 event.add_task(ImplicitRunCounter(1))
1711
1712Mocker.add_recorder(run_counter_recorder)
1713
1714def run_counter_removal_recorder(mocker, event):
1715 """
1716 Events created by getattr actions which lead to other events
1717 may be repeated any number of times. For that, we remove implicit
1718 run counters of any getattr actions leading to the current one.
1719 """
1720 parent_path = event.path.parent_path
1721 for event in mocker.get_events()[::-1]:
1722 if (event.path is parent_path and
1723 event.path.actions[-1].kind == "getattr"):
1724 for task in event.get_tasks():
1725 if type(task) is ImplicitRunCounter:
1726 event.remove_task(task)
1727
1728Mocker.add_recorder(run_counter_removal_recorder)
1729
1730
1731class MockReturner(Task):
1732 """Return a mock based on the action path."""
1733
1734 def __init__(self, mocker):
1735 self.mocker = mocker
1736
1737 def run(self, path):
1738 return Mock(self.mocker, path)
1739
1740def mock_returner_recorder(mocker, event):
1741 """Events that lead to other events must return mock objects."""
1742 parent_path = event.path.parent_path
1743 for event in mocker.get_events():
1744 if event.path is parent_path:
1745 for task in event.get_tasks():
1746 if isinstance(task, MockReturner):
1747 break
1748 else:
1749 event.add_task(MockReturner(mocker))
1750 break
1751
1752Mocker.add_recorder(mock_returner_recorder)
1753
1754
1755class FunctionRunner(Task):
1756 """Task that runs a function everything it's run.
1757
1758 Arguments of the last action in the path are passed to the function,
1759 and the function result is also returned.
1760 """
1761
1762 def __init__(self, func):
1763 self._func = func
1764
1765 def run(self, path):
1766 action = path.actions[-1]
1767 return self._func(*action.args, **action.kwargs)
1768
1769
1770class PathExecuter(Task):
1771 """Task that executes a path in the real object, and returns the result."""
1772
1773 def __init__(self, result_callback=None):
1774 self._result_callback = result_callback
1775
1776 def get_result_callback(self):
1777 return self._result_callback
1778
1779 def run(self, path):
1780 result = path.execute(path.root_object)
1781 if self._result_callback is not None:
1782 self._result_callback(result)
1783 return result
1784
1785
1786class Orderer(Task):
1787 """Task to establish an order relation between two events.
1788
1789 An orderer task will only match once all its dependencies have
1790 been run.
1791 """
1792
1793 def __init__(self, path):
1794 self.path = path
1795 self._run = False
1796 self._dependencies = []
1797
1798 def replay(self):
1799 self._run = False
1800
1801 def has_run(self):
1802 return self._run
1803
1804 def may_run(self, path):
1805 for dependency in self._dependencies:
1806 if not dependency.has_run():
1807 return False
1808 return True
1809
1810 def run(self, path):
1811 for dependency in self._dependencies:
1812 if not dependency.has_run():
1813 raise AssertionError("Should be after: %s" % dependency.path)
1814 self._run = True
1815
1816 def add_dependency(self, orderer):
1817 self._dependencies.append(orderer)
1818
1819 def get_dependencies(self):
1820 return self._dependencies
1821
1822
1823class SpecChecker(Task):
1824 """Task to check if arguments of the last action conform to a real method.
1825 """
1826
1827 def __init__(self, method):
1828 self._method = method
1829 self._unsupported = False
1830
1831 if method:
1832 try:
1833 self._args, self._varargs, self._varkwargs, self._defaults = \
1834 inspect.getargspec(method)
1835 except TypeError:
1836 self._unsupported = True
1837 else:
1838 if self._defaults is None:
1839 self._defaults = ()
1840 if type(method) is type(self.run):
1841 self._args = self._args[1:]
1842
1843 def get_method(self):
1844 return self._method
1845
1846 def _raise(self, message):
1847 spec = inspect.formatargspec(self._args, self._varargs,
1848 self._varkwargs, self._defaults)
1849 raise AssertionError("Specification is %s%s: %s" %
1850 (self._method.__name__, spec, message))
1851
1852 def verify(self):
1853 if not self._method:
1854 raise AssertionError("Method not found in real specification")
1855
1856 def may_run(self, path):
1857 try:
1858 self.run(path)
1859 except AssertionError:
1860 return False
1861 return True
1862
1863 def run(self, path):
1864 if not self._method:
1865 raise AssertionError("Method not found in real specification")
1866 if self._unsupported:
1867 return # Can't check it. Happens with builtin functions. :-(
1868 action = path.actions[-1]
1869 obtained_len = len(action.args)
1870 obtained_kwargs = action.kwargs.copy()
1871 nodefaults_len = len(self._args) - len(self._defaults)
1872 for i, name in enumerate(self._args):
1873 if i < obtained_len and name in action.kwargs:
1874 self._raise("%r provided twice" % name)
1875 if (i >= obtained_len and i < nodefaults_len and
1876 name not in action.kwargs):
1877 self._raise("%r not provided" % name)
1878 obtained_kwargs.pop(name, None)
1879 if obtained_len > len(self._args) and not self._varargs:
1880 self._raise("too many args provided")
1881 if obtained_kwargs and not self._varkwargs:
1882 self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs))
1883
1884def spec_checker_recorder(mocker, event):
1885 spec = event.path.root_mock.__mocker_spec__
1886 if spec:
1887 actions = event.path.actions
1888 if len(actions) == 1:
1889 if actions[0].kind == "call":
1890 method = getattr(spec, "__call__", None)
1891 event.add_task(SpecChecker(method))
1892 elif len(actions) == 2:
1893 if actions[0].kind == "getattr" and actions[1].kind == "call":
1894 method = getattr(spec, actions[0].args[0], None)
1895 event.add_task(SpecChecker(method))
1896
1897Mocker.add_recorder(spec_checker_recorder)
1898
1899
1900class ProxyReplacer(Task):
1901 """Task which installs and deinstalls proxy mocks.
1902
1903 This task will replace a real object by a mock in all dictionaries
1904 found in the running interpreter via the garbage collecting system.
1905 """
1906
1907 def __init__(self, mock):
1908 self.mock = mock
1909 self.__mocker_replace__ = False
1910
1911 def replay(self):
1912 global_replace(self.mock.__mocker_object__, self.mock)
1913
1914 def restore(self):
1915 global_replace(self.mock, self.mock.__mocker_object__)
1916
1917
1918def global_replace(remove, install):
1919 """Replace object 'remove' with object 'install' on all dictionaries."""
1920 for referrer in gc.get_referrers(remove):
1921 if (type(referrer) is dict and
1922 referrer.get("__mocker_replace__", True)):
1923 for key, value in referrer.items():
1924 if value is remove:
1925 referrer[key] = install
1926
1927
1928class Undefined(object):
1929
1930 def __repr__(self):
1931 return "Undefined"
1932
1933Undefined = Undefined()
1934
1935
1936class Patcher(Task):
1937
1938 def __init__(self):
1939 super(Patcher, self).__init__()
1940 self._monitored = {} # {kind: {id(object): object}}
1941 self._patched = {}
1942
1943 def is_monitoring(self, obj, kind):
1944 monitored = self._monitored.get(kind)
1945 if monitored:
1946 if id(obj) in monitored:
1947 return True
1948 cls = type(obj)
1949 if issubclass(cls, type):
1950 cls = obj
1951 bases = set([id(base) for base in cls.__mro__])
1952 bases.intersection_update(monitored)
1953 return bool(bases)
1954 return False
1955
1956 def monitor(self, obj, kind):
1957 if kind not in self._monitored:
1958 self._monitored[kind] = {}
1959 self._monitored[kind][id(obj)] = obj
1960
1961 def patch_attr(self, obj, attr, value):
1962 original = obj.__dict__.get(attr, Undefined)
1963 self._patched[id(obj), attr] = obj, attr, original
1964 setattr(obj, attr, value)
1965
1966 def get_unpatched_attr(self, obj, attr):
1967 cls = type(obj)
1968 if issubclass(cls, type):
1969 cls = obj
1970 result = Undefined
1971 for mro_cls in cls.__mro__:
1972 key = (id(mro_cls), attr)
1973 if key in self._patched:
1974 result = self._patched[key][2]
1975 if result is not Undefined:
1976 break
1977 elif attr in mro_cls.__dict__:
1978 result = mro_cls.__dict__.get(attr, Undefined)
1979 break
1980 if isinstance(result, object) and hasattr(type(result), "__get__"):
1981 if cls is obj:
1982 obj = None
1983 return result.__get__(obj, cls)
1984 return result
1985
1986 def _get_kind_attr(self, kind):
1987 if kind == "getattr":
1988 return "__getattribute__"
1989 return "__%s__" % kind
1990
1991 def replay(self):
1992 for kind in self._monitored:
1993 attr = self._get_kind_attr(kind)
1994 seen = set()
1995 for obj in self._monitored[kind].itervalues():
1996 cls = type(obj)
1997 if issubclass(cls, type):
1998 cls = obj
1999 if cls not in seen:
2000 seen.add(cls)
2001 unpatched = getattr(cls, attr, Undefined)
2002 self.patch_attr(cls, attr,
2003 PatchedMethod(kind, unpatched,
2004 self.is_monitoring))
2005 self.patch_attr(cls, "__mocker_execute__",
2006 self.execute)
2007
2008 def restore(self):
2009 for obj, attr, original in self._patched.itervalues():
2010 if original is Undefined:
2011 delattr(obj, attr)
2012 else:
2013 setattr(obj, attr, original)
2014 self._patched.clear()
2015
2016 def execute(self, action, object):
2017 attr = self._get_kind_attr(action.kind)
2018 unpatched = self.get_unpatched_attr(object, attr)
2019 try:
2020 return unpatched(*action.args, **action.kwargs)
2021 except AttributeError:
2022 if action.kind == "getattr":
2023 # The normal behavior of Python is to try __getattribute__,
2024 # and if it raises AttributeError, try __getattr__. We've
2025 # tried the unpatched __getattribute__ above, and we'll now
2026 # try __getattr__.
2027 try:
2028 __getattr__ = unpatched("__getattr__")
2029 except AttributeError:
2030 pass
2031 else:
2032 return __getattr__(*action.args, **action.kwargs)
2033 raise
2034
2035
2036class PatchedMethod(object):
2037
2038 def __init__(self, kind, unpatched, is_monitoring):
2039 self._kind = kind
2040 self._unpatched = unpatched
2041 self._is_monitoring = is_monitoring
2042
2043 def __get__(self, obj, cls=None):
2044 object = obj or cls
2045 if not self._is_monitoring(object, self._kind):
2046 return self._unpatched.__get__(obj, cls)
2047 def method(*args, **kwargs):
2048 if self._kind == "getattr" and args[0].startswith("__mocker_"):
2049 return self._unpatched.__get__(obj, cls)(args[0])
2050 mock = object.__mocker_mock__
2051 return mock.__mocker_act__(self._kind, args, kwargs, object)
2052 return method
2053
2054 def __call__(self, obj, *args, **kwargs):
2055 # At least with __getattribute__, Python seems to use *both* the
2056 # descriptor API and also call the class attribute directly. It
2057 # looks like an interpreter bug, or at least an undocumented
2058 # inconsistency.
2059 return self.__get__(obj)(*args, **kwargs)
2060
2061
2062def patcher_recorder(mocker, event):
2063 mock = event.path.root_mock
2064 if mock.__mocker_patcher__ and len(event.path.actions) == 1:
2065 patcher = mock.__mocker_patcher__
2066 patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind)
2067
2068Mocker.add_recorder(patcher_recorder)
02069
=== added directory 'data'
=== added file 'data/couchdb.tmpl'
--- data/couchdb.tmpl 1970-01-01 00:00:00 +0000
+++ data/couchdb.tmpl 2009-08-20 12:55:54 +0000
@@ -0,0 +1,38 @@
1<!doctype html>
2<html>
3<head>
4<title>Your Desktop CouchDB</title>
5<script>
6window.onload = function() {
7 if (document.cookie == "yes") {
8 location.href = document.getElementById("there").href;
9 }
10 document.getElementById("there").addEventListener("click", function(e) {
11 document.cookie = "yes; expires=Thu, 31 Dec 2099 21:00:00 UTC";
12 }, false);
13 var count = 30;
14 setInterval(function() {
15 count -= 1;
16 if (count < 0) {
17 location.href = document.getElementById("there").href;
18 return;
19 }
20 document.getElementsByTagName("span")[0].innerHTML = count;
21 }, 1000);
22}
23</script>
24</head>
25<body>
26<h1>Desktop CouchDB</h1>
27<p>Your desktop CouchDB is the data store for many of your applications.
28You can browse around it to see which data your applications are storing.</p>
29<p>You should bookmark this page (by going to <strong>Bookmarks > Bookmark
30This Page</strong> or pressing <strong>Ctrl-D</strong>) so you can easily
31come back to browse your CouchDB again.</p>
32<p>Don't bookmark the CouchDB page itself, because its location may change!</p>
33<p>Taking you to your Desktop CouchDB in <span>30</span> seconds...
34<a id="there" href="http://[[COUCHDB_USERNAME]]:[[COUCHDB_PASSWORD]]@localhost:[[COUCHDB_PORT]]/_utils">take me
35there straight away from now on</a> (remember to bookmark this page first!)</p>
36</body>
37</html>
38
039
=== added directory 'debian'
=== added file 'debian/changelog'
--- debian/changelog 1970-01-01 00:00:00 +0000
+++ debian/changelog 2009-09-14 23:33:14 +0000
@@ -0,0 +1,102 @@
1desktopcouch (0.4-0ubuntu1) karmic; urgency=low
2
3 * Packaging: desktopcouch-tools installed by default in Karmic (LP: #427421)
4 * Forcing desktopcouch auth on. (LP: #427446)
5 * Requiring new version of couchdb that supports authentication properly.
6 * Pairing updates couchdb replication system. (LP: #397663)
7 * Added pairing of desktop Couches to desktopcouch-tools (LP: #404087)
8 * Admin users in the system couchdb are no longer inherited by desktopcouch
9 couchdbs (LP: #424330)
10 * Fixed failing tests in desktopcouch (LP: #405612)
11 * Creating login details on initial desktopcouch setup (LP: #416413)
12 * Starting replication to paired servers on desktopcouch startup.
13 (LP: #416581)
14 * Unpaired couchdb peers are reconciled with replication. (LP: #424386)
15 * At pairing time, changing couchdb pairing address to public. (LP: #419969)
16 * In replication daemon, verifying local couchdb bind address is not 127/8 .
17 (LP: #419973)
18 * getPort no longer suceeds when desktopcouch isn't running. (LP: #422127)
19
20 -- Chad Miller <chad.miller@canonical.com> Mon, 14 Sep 2009 19:24:08 -0400
21
22desktopcouch (0.3.1-0ubuntu1) karmic; urgency=low
23
24 [Ken VanDine]
25 * debian/control
26 - Added depends on python-desktopcouch-records. (LP: #422179)
27 [Chad Miller]
28 * New upstream release.
29 * Changed Vcs-Bzr links in control file.
30 * Changed version dependency on couchdb.
31 * Converting to a full-blown source-package branch.
32 * Fix getPort failure. (LP: #420911, LP: #422127)
33 * Check couchdb bind-port in replicator. (LP: #419973)
34 * Change couchdb bind-port in pairing tool. (LP: #419969)
35
36 -- Chad Miller <chad.miller@canonical.com> Tue, 01 Sep 2009 11:57:25 -0400
37
38desktopcouch (0.3-0ubuntu1) karmic; urgency=low
39
40 [ Ken VanDine ]
41 * New upstream release (LP: #416591)
42 - added unit tests for couchwidget, and then fixed bug #412266
43 - Change to freedesktop URL for record type spec.
44 - First version of the contacts picker, based on CouchWidget
45 - Adding the desktopcouch.contacts module.
46 - Use subprocess.Popen and ourselves to the wait()ing,
47 since subprocess.call() is buggy. There's still an EINTR bug
48 in subprocess, though.
49 - Occasionally stop couchdb in tests, so we exercise the automatic
50 starting code. This will lead to spurious errors because of the
51 aforementioned subprocess bug, but it's the right thing to do.
52 - Abstract away some of the linuxisms and complain if we're run on
53 an unsupported OS.
54 - Fix a race condition in the process-testing code.
55 - Replace the TestCase module with one that doesn't complain of dirty
56 twisted reactors.
57 - Add a means of stopping the desktop couchdb daemon.
58 - Add an additional check that a found PID and process named correctly
59 is indeed a process that this user started, so we don't try to talk
60 to other local users' desktop couchdbs.
61 - Get the port at function-call time, instead of storing a port at
62 start-time and giving that back. The info can be stale, the old
63 way.
64 - Don't create a view per record-type; instead, call the standard
65 return-all-records-keyed-by-record-type and use slice notation on
66 the viewresults to only get back the records with that type,
67 which does the same thing but more elegantly.
68 - Remove the unused/invalid "utils" import from test_server
69 - Change the name of a function tested to be what actually exists in
70 the code.
71 - Refactored server.py by renaming server.get_records_and_type to
72 server.get_records. Also modified the function to take a record
73 type param if desired to only create records of that type and
74 optionally create a view name "get_"+ record_type. (LP: #411475)
75 * debian/control
76 - Make python-desktopcouch depend on python-gtk2 and python-gnomekeyring
77 - Make python-desktopcouch-records depend on python-gtk2,
78 python-gnomekeyring, and python-oauth.
79 - Remove depends for python-distutils-extra
80 - Fixed Vcs-Browser tag
81
82 [Elliot Murphy]
83 * debian/control: added build-dep on python-setuptools
84
85
86 [ Martin Pitt ]
87 * debian/control: Fix Vcs-* links.
88
89 -- Ken VanDine <ken.vandine@canonical.com> Thu, 27 Aug 2009 15:32:11 +0200
90
91desktopcouch (0.2-0ubuntu1) karmic; urgency=low
92
93 * New upstream release
94 * Handle the case where the pid file is empty or doesn't exist (LP: #408796)
95
96 -- Ken VanDine <ken.vandine@canonical.com> Wed, 05 Aug 2009 02:17:47 +0100
97
98desktopcouch (0.1-0ubuntu1) karmic; urgency=low
99
100 * Initial release (LP: #397662)
101
102 -- Ken VanDine <ken.vandine@canonical.com> Fri, 31 Jul 2009 13:44:45 -0400
0103
=== added file 'debian/compat'
--- debian/compat 1970-01-01 00:00:00 +0000
+++ debian/compat 2009-09-01 15:46:56 +0000
@@ -0,0 +1,1 @@
16
02
=== added file 'debian/control'
--- debian/control 1970-01-01 00:00:00 +0000
+++ debian/control 2009-09-14 23:33:14 +0000
@@ -0,0 +1,76 @@
1Source: desktopcouch
2Section: python
3Priority: optional
4Build-Depends: cdbs (>= 0.4.43),
5 debhelper (>= 6),
6 python,
7 python-central (>= 0.6.11),
8 python-distutils-extra (>= 2.8),
9 python-setuptools
10Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
11Standards-Version: 3.8.2
12XS-Python-Version: current
13Homepage: http://launchpad.net/desktopcouch
14Vcs-Bzr: https://code.launchpad.net/~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb
15Vcs-Browser: http://bazaar.launchpad.net/~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/spb
16
17Package: desktopcouch
18Architecture: all
19XB-Python-Version: ${python:Versions}
20Depends: ${misc:Depends},
21 ${python:Depends},
22 couchdb (>= 0.10.0~svn8138),
23 python-dbus,
24 python-couchdb (>= 0.6),
25 python-twisted-core,
26 python-gobject,
27 python-avahi,
28 python-desktopcouch,
29 python-desktopcouch-records,
30 python-gtk2
31Description: Desktop CouchDB instance, specific to each user
32 Couchdb owned by regular users in their sessions, for management of personal
33 data.
34
35Package: desktopcouch-tools
36Architecture: all
37XB-Python-Version: ${python:Versions}
38Depends: ${misc:Depends},
39 ${python:Depends},
40 python-dbus,
41 python-twisted-core,
42 python-gobject,
43 python-avahi,
44 python-desktopcouch,
45 python-gtk2
46Description: Desktop CouchDB tools
47 Tools used to work with DesktopCouch database.
48
49Package: python-desktopcouch
50Architecture: all
51XB-Python-Version: ${python:Versions}
52Depends: ${misc:Depends},
53 ${python:Depends},
54 desktopcouch,
55 python-dbus,
56 python-couchdb,
57 python-xdg,
58 python-twisted-core,
59 python-gtk2,
60 python-gnomekeyring
61Description: Python Desktop CouchDB
62 A Python library for Desktop CouchDB.
63
64Package: python-desktopcouch-records
65Architecture: all
66XB-Python-Version: ${python:Versions}
67Depends: ${misc:Depends},
68 ${python:Depends},
69 python-dbus,
70 python-couchdb,
71 python-desktopcouch,
72 python-gtk2,
73 python-gnomekeyring,
74 python-oauth
75Description: Desktop CouchDB Records API
76 A Python library for the Desktop CouchDB Records API.
077
=== added file 'debian/copyright'
--- debian/copyright 1970-01-01 00:00:00 +0000
+++ debian/copyright 2009-09-01 15:46:56 +0000
@@ -0,0 +1,11 @@
1Format-Specification: http://wiki.debian.org/Proposals/CopyrightFormat
2Upstream-Name: desktopcouch
3Upstream-Maintainer: Stuart Langridge <stuart.langridge@canonical.com>
4Upstream-Source: https://launchpad.net/desktopcouch
5
6Files: *
7Copyright: (C) 2009 Canonical Ltd.
8
9License: LGPL-3
10 The full text of the LGPL is distributed in
11 /usr/share/common-licenses/LGPL-3 on Debian systems.
012
=== added file 'debian/desktopcouch-tools.install'
--- debian/desktopcouch-tools.install 1970-01-01 00:00:00 +0000
+++ debian/desktopcouch-tools.install 2009-09-01 15:46:56 +0000
@@ -0,0 +1,4 @@
1debian/tmp/usr/share/applications/desktopcouch-pair.desktop
2debian/tmp/usr/bin/desktopcouch-pair
3debian/tmp/usr/share/man/man1/desktopcouch-pair.1
4#debian/tmp/usr/share/locale/*/LC_MESSAGES/desktopcouch.mo
05
=== added file 'debian/desktopcouch.install'
--- debian/desktopcouch.install 1970-01-01 00:00:00 +0000
+++ debian/desktopcouch.install 2009-09-01 15:46:56 +0000
@@ -0,0 +1,3 @@
1debian/tmp/usr/share/desktopcouch
2debian/tmp/usr/lib/desktopcouch/desktopcouch-{stop,service}
3debian/tmp/usr/share/dbus-1/services/org.desktopcouch.CouchDB.service
04
=== added file 'debian/pycompat'
--- debian/pycompat 1970-01-01 00:00:00 +0000
+++ debian/pycompat 2009-09-01 15:46:56 +0000
@@ -0,0 +1,1 @@
12
02
=== added file 'debian/python-desktopcouch-records.install'
--- debian/python-desktopcouch-records.install 1970-01-01 00:00:00 +0000
+++ debian/python-desktopcouch-records.install 2009-09-01 15:46:56 +0000
@@ -0,0 +1,2 @@
1debian/tmp/usr/share/doc/python-desktopcouch-records/api
2debian/tmp/usr/lib/*/*/desktopcouch/records/*
03
=== added file 'debian/python-desktopcouch.install'
--- debian/python-desktopcouch.install 1970-01-01 00:00:00 +0000
+++ debian/python-desktopcouch.install 2009-09-15 05:02:20 +0000
@@ -0,0 +1,3 @@
1debian/tmp/usr/lib/*/*/desktopcouch/*.py
2debian/tmp/usr/lib/*/*/desktopcouch/replication_services/*.py
3debian/tmp/usr/lib/*/*/desktopcouch/pair/{couchdb_pairing,__init__.py}
04
=== added file 'debian/rules'
--- debian/rules 1970-01-01 00:00:00 +0000
+++ debian/rules 2009-09-01 15:46:56 +0000
@@ -0,0 +1,7 @@
1#!/usr/bin/make -f
2
3DEB_PYTHON_SYSTEM := pycentral
4
5include /usr/share/cdbs/1/rules/debhelper.mk
6include /usr/share/cdbs/1/class/python-distutils.mk
7include /usr/share/cdbs/1/rules/langpack.mk
08
=== added file 'debian/watch'
--- debian/watch 1970-01-01 00:00:00 +0000
+++ debian/watch 2009-09-01 15:46:56 +0000
@@ -0,0 +1,2 @@
1version=3
2http://launchpad.net/desktopcouch/+download .*/desktopcouch-([0-9.]+)\.tar\.gz
03
=== added directory 'desktopcouch'
=== added file 'desktopcouch-pair.desktop.in'
--- desktopcouch-pair.desktop.in 1970-01-01 00:00:00 +0000
+++ desktopcouch-pair.desktop.in 2009-07-27 19:18:55 +0000
@@ -0,0 +1,8 @@
1[Desktop Entry]
2_Name=CouchDB Pairing Tool
3Type=Application
4_Comment=Utility for pairing Desktop CouchDB
5Exec=desktopcouch-pair
6Icon=
7Categories=Network;
8_GenericName=CouchDB Pairing Tool
09
=== added file 'desktopcouch/__init__.py'
--- desktopcouch/__init__.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/__init__.py 2009-09-09 23:24:53 +0000
@@ -0,0 +1,98 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16"Desktop Couch helper files"
17
18from __future__ import with_statement
19import os, re
20from desktopcouch.start_local_couchdb import process_is_couchdb, read_pidfile
21
22def find_pid(start_if_not_running=True):
23 # Work out whether CouchDB is running by looking at its pid file
24 pid = read_pidfile()
25 if not process_is_couchdb(pid) and start_if_not_running:
26 # start CouchDB by running the startup script
27 print "Desktop CouchDB is not running; starting it.",
28 from desktopcouch import start_local_couchdb
29 pid = start_local_couchdb.start_couchdb()
30 # now load the design documents, because it's started
31 start_local_couchdb.update_design_documents()
32
33 if not process_is_couchdb(pid):
34 raise RuntimeError("desktop-couch not started")
35
36 return pid
37
38def find_port__linux(pid=None):
39 if pid is None:
40 pid = find_pid()
41
42 proc_dir = "/proc/%s" % (pid,)
43
44 # enumerate the process' file descriptors
45 fd_dir = os.path.join(proc_dir, 'fd')
46 try:
47 fd_paths = [os.readlink(os.path.join(fd_dir, fd))
48 for fd in os.listdir(fd_dir)]
49 except OSError:
50 raise RuntimeError("Unable to find file descriptors in /proc")
51
52 # identify socket fds
53 socket_matches = [re.match('socket:\\[([0-9]+)\\]', p) for p in fd_paths]
54 # extract their inode numbers
55 socket_inodes = [m.group(1) for m in socket_matches if m is not None]
56
57 # construct a subexpression which matches any one of these inodes
58 inode_subexp = "|".join(map(re.escape, socket_inodes))
59 # construct regexp to match /proc/net/tcp entries which are listening
60 # sockets having one of the given inode numbers
61 listening_regexp = re.compile(r'''
62 \s*\d+:\s* # sl
63 [0-9A-F]{8}: # local_address part 1
64 ([0-9A-F]{4})\s+ # local_address part 2
65 00000000:0000\s+ # rem_address
66 0A\s+ # st (0A = listening)
67 [0-9A-F]{8}: # tx_queue
68 [0-9A-F]{8}\s+ # rx_queue
69 [0-9A-F]{2}: # tr
70 [0-9A-F]{8}\s+ # tm->when
71 [0-9A-F]{8}\s* # retrnsmt
72 \d+\s+\d+\s+ # uid, timeout
73 (?:%s)\s+ # inode
74 ''' % (inode_subexp,), re.VERBOSE)
75
76 # extract the TCP port from the first matching line in /proc/$pid/net/tcp
77 port = None
78 with open(os.path.join(proc_dir, 'net', 'tcp')) as tcp_file:
79 for line in tcp_file:
80 match = listening_regexp.match(line)
81 if match is not None:
82 port = str(int(match.group(1), 16))
83 break
84 if port is None:
85 raise RuntimeError("Unable to find listening port")
86
87 return port
88
89
90
91import platform
92os_name = platform.system()
93try:
94 find_port = {
95 "Linux": find_port__linux
96 } [os_name]
97except KeyError:
98 raise NotImplementedError("os %r is not yet supported" % (os_name,))
099
=== added directory 'desktopcouch/contacts'
=== added file 'desktopcouch/contacts/__init__.py'
--- desktopcouch/contacts/__init__.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/__init__.py 2009-08-17 21:39:48 +0000
@@ -0,0 +1,20 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
18# Nicola Larosa <nicola.larosa@canonical.com>
19
20"""UbuntuOne Contacts API"""
021
=== added file 'desktopcouch/contacts/contactspicker.py'
--- desktopcouch/contacts/contactspicker.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/contactspicker.py 2009-08-24 20:35:25 +0000
@@ -0,0 +1,81 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Rodrigo Moya <rodrigo.moya@canonical.com
18
19"""A widget to allow users to pick contacts"""
20
21import gtk
22from desktopcouch.contacts.record import CONTACT_RECORD_TYPE
23from desktopcouch.records.couchgrid import CouchGrid
24
25
26class ContactsPicker(gtk.VBox):
27 """A contacts picker"""
28
29 def __init__(self, uri=None):
30 """Create a new ContactsPicker widget."""
31
32 gtk.VBox.__init__(self)
33
34 # Create search entry and button
35 hbox = gtk.HBox()
36 self.pack_start(hbox, False, False, 3)
37
38 self.search_entry = gtk.Entry()
39 hbox.pack_start(self.search_entry, True, True, 3)
40
41 self.search_button = gtk.Button(
42 stock=gtk.STOCK_FIND, use_underline=True)
43 hbox.pack_start(self.search_button, False, False, 3)
44
45 # Create CouchGrid to contain list of contacts
46 self.contacts_list = CouchGrid('contacts', uri=uri)
47 self.contacts_list.editable = False
48 self.contacts_list.keys = [ "first_name", "last_name" ]
49 self.contacts_list.record_type = CONTACT_RECORD_TYPE
50
51 #Pimp out the columns with some nicer titles
52 #TODO: this should be set up for translatability
53 columns = self.contacts_list.get_columns()
54 columns[0].set_title("First Name")
55 columns[1].set_title("Last Name")
56
57 self.contacts_list.show()
58 self.pack_start(self.contacts_list, True, True, 3)
59
60 def get_contacts_list(self):
61 """get_contacts_list - gtk.Widget value
62 Gets the CouchGrid inside the ContactsPicker widget
63 """
64 return self.contacts_list
65
66if __name__ == "__main__":
67 """creates a test ContactsPicker if called directly"""
68
69 # Create and show a test window
70 win = gtk.Dialog("ContactsPicker widget test", None, 0,
71 (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
72 win.connect("response", gtk.main_quit)
73 win.resize(300, 450)
74
75 # Create the contacts picker widget
76 picker = ContactsPicker()
77 win.get_content_area().pack_start(picker, True, True, 3)
78
79 # Run the test application
80 win.show_all()
81 gtk.main()
082
=== added file 'desktopcouch/contacts/record.py'
--- desktopcouch/contacts/record.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/record.py 2009-08-19 10:07:54 +0000
@@ -0,0 +1,34 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
18# Nicola Larosa <nicola.larosa@canonical.com>
19# Mark G. Saye <mark.saye@canonical.com>
20
21
22"""A dictionary based contact record representation."""
23
24from desktopcouch.records.record import Record
25
26CONTACT_RECORD_TYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact'
27
28
29class Contact(Record):
30 """An Ubuntuone Contact Record."""
31
32 def __init__(self, data=None, record_id=None):
33 super(Contact, self).__init__(
34 record_id=record_id, data=data, record_type=CONTACT_RECORD_TYPE)
035
=== added file 'desktopcouch/contacts/schema.txt'
--- desktopcouch/contacts/schema.txt 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/schema.txt 2009-08-19 10:07:54 +0000
@@ -0,0 +1,50 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16
17Schema
18
19The proposed CouchDB contact schema is as follows:
20
21Core fields
22
23 * record_type 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact'
24 * first_name (string)
25 * last_name (string)
26 * birth_date (string, "YYYY-MM-DD")
27 * addresses (MergeableList of "address" dictionaries)
28 o city (string)
29 o address1 (string)
30 o address2 (string)
31 o pobox (string)
32 o state (string)
33 o country (string)
34 o postalcode (string)
35 o description (string, e.g., "Home")
36 * email_addresses (MergeableList of "emailaddress" dictionaries)
37 o address (string),
38 o description (string)
39 * phone_numbers (MergeableList of "phone number" dictionaries)
40 o number (string)
41 o description (string)
42 * application_annotations Everything else, organized per application.
43
44Note: None of the core fields are mandatory, but applications should
45not add any other fields at the top level of the record. Any fields
46needed not defined here should be put under application_annotations in
47the namespace of the application there. So for Ubuntu One:
48
49 "application_annotations": {
50 "Ubuntu One": {<Ubuntu One specific fields here>}}
051
=== added directory 'desktopcouch/contacts/testing'
=== added file 'desktopcouch/contacts/testing/__init__.py'
--- desktopcouch/contacts/testing/__init__.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/testing/__init__.py 2009-08-17 21:39:48 +0000
@@ -0,0 +1,20 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Stuart Langridge <stuart.langridge@canonical.com>
18# Nicola Larosa <nicola.larosa@canonical.com>
19
20"""Support code for tests"""
021
=== added file 'desktopcouch/contacts/testing/create.py'
--- desktopcouch/contacts/testing/create.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/testing/create.py 2009-09-09 23:24:53 +0000
@@ -0,0 +1,178 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Stuart Langridge <stuart.langridge@canonical.com>
18# Nicola Larosa <nicola.larosa@canonical.com>
19
20"""Creating CouchDb-stored contacts for testing"""
21
22import random, string, uuid
23
24from couchdb import Server
25import desktopcouch
26
27CONTACT_DOCTYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact'
28COUCHDB_SYS_PORT = 5984
29
30FIRST_NAMES = ('Jack', 'Thomas', 'Oliver', 'Joshua', 'Harry', 'Charlie',
31 'Daniel', 'William', 'James', 'Alfie', 'Grace', 'Ruby', 'Olivia',
32 'Emily', 'Jessica', 'Sophie', 'Chloe', 'Lily', 'Ella', 'Amelia')
33LAST_NAMES = ('Dalglish', 'Grobbelaar', 'Lawrenson', 'Beglin', 'Nicol',
34 'Whelan', 'Hansen', 'Johnston', 'Rush', 'Molby', 'MacDonald', 'McMahon',
35 'Penny', 'Leicester', 'Langley', 'Commodore', 'Touchstone', 'Fielding')
36COMPANIES = ('Gostram', 'Grulthing', 'Nasform', 'Smarfish', 'Builsank')
37STREET_TYPES = ('Street', 'Road', 'Lane', 'Avenue', 'Square', 'Park', 'Mall')
38CITIES = ('Scunthorpe', 'Birmingham', 'Cambridge', 'Durham', 'Bedford')
39COUNTRIES = ('England', 'Ireland', 'Scotland', 'Wales')
40
41def head_or_tails():
42 """Randomly return True or False"""
43 return random.choice((False, True))
44
45def random_bools(num_bools, at_least_one_true=True):
46 """
47 Return a sequence of random booleans. If at_least_one_true,
48 guarantee at list one True value.
49 """
50 if num_bools < 2:
51 raise RuntimeError('Cannot build a sequence smaller than two elements')
52 bools = [head_or_tails() for __ in range(num_bools)]
53 if at_least_one_true and not any(bools):
54 bools[random.randrange(num_bools)] = True
55 return bools
56
57def random_string(length=10, upper=False):
58 """
59 Return a string, of specified length, of random lower or uppercase letters.
60 """
61 charset = string.uppercase if upper else string.lowercase
62 return ''.join(random.sample(charset, length))
63
64def random_postal_address():
65 """Return something that looks like a postal address"""
66 return '%d %s %s' % (random.randint(1, 100), random.choice(LAST_NAMES),
67 random.choice(STREET_TYPES))
68
69def random_email_address(
70 first_name='', last_name='', company='', address_type='other'):
71 """
72 Return something that looks like an email address.
73 address_type values: 'personal', 'work', 'other'.
74 """
75 # avoid including the dot if one or both names are missing
76 pers_name = '.'.join([name for name in (first_name, last_name) if name])
77 return {'personal': pers_name, 'work': company,
78 }.get(address_type, random_string(5)) + '@example.com'
79
80def random_postal_code():
81 """Return something that looks like a postal code"""
82 return '%s12 3%s' % (random_string(2, True), random_string(2, True))
83
84def random_phone_number():
85 """Return something that looks like a phone number"""
86 return '+%s %s %s %s' % (random.randint(10, 99), random.randint(10, 99),
87 random.randint(1000, 9999), random.randint(1000, 9999))
88
89def random_birth_date():
90 """Return something that looks like a birth date"""
91 return '%04d-%02d-%02d' % (random.randint(1900, 2006),
92 random.randint(1, 12), random.randint(1, 28))
93
94def make_one_contact(maincount, doctype, app_annots):
95 """Make up one contact randomly"""
96 # Record schema
97 fielddict = {'record_type': doctype, 'record_type_version': '1.0'}
98 # Names
99 # at least one of the three will be present
100 has_first_name, has_last_name, has_company_name = random_bools(3)
101 first_name = (random.choice(FIRST_NAMES) + str(maincount)
102 ) if has_first_name else ''
103 last_name = random.choice(LAST_NAMES) if has_last_name else ''
104 company = (random.choice(COMPANIES) + str(maincount)
105 ) if has_company_name else ''
106 # Address places and types
107 address_places, email_types, phone_places = [], [], []
108 if has_first_name or has_last_name:
109 for (has_it, seq, val) in zip(
110 # at least one of the three will be present
111 random_bools(3),
112 (address_places, email_types, phone_places),
113 ('home', 'personal', 'home')):
114 if has_it:
115 seq.append(val)
116 if has_company_name:
117 for (has_it, seq) in zip(
118 # at least one of the three will be present
119 random_bools(3),
120 (address_places, email_types, phone_places)):
121 if has_it:
122 seq.append('work')
123 for (has_it, seq) in zip(
124 # none of the three may be present
125 random_bools(3, at_least_one_true=False),
126 (address_places, email_types, phone_places)):
127 if has_it:
128 seq.append('other')
129 # Addresses
130 addresses = {}
131 for address_type in address_places:
132 addresses[str(uuid.uuid4())] = {
133 'address1': random_postal_address(), 'address2': '',
134 'pobox': '', 'city': random.choice(CITIES),
135 'state': '', 'postalcode': random_postal_code(),
136 'country': random.choice(COUNTRIES), 'description': address_type}
137 # Email addresses
138 email_addresses = {}
139 for email_address_type in email_types:
140 email_addresses[str(uuid.uuid4())] = {
141 'address': random_email_address(
142 first_name, last_name, company, email_address_type),
143 'description': email_address_type}
144 # Phone numbers
145 phone_numbers = {}
146 for phone_number_type in phone_places:
147 phone_numbers[str(uuid.uuid4())] = {
148 'priority': 0, 'number': random_phone_number(),
149 'description': phone_number_type}
150 # Store data in fielddict
151 if (has_first_name or has_last_name) and head_or_tails():
152 fielddict['birth_date'] = random_birth_date()
153 fielddict.update({'first_name': first_name, 'last_name': last_name,
154 'addresses': addresses, 'email_addresses': email_addresses,
155 'phone_numbers': phone_numbers, 'company': company})
156 # Possibly add example application annotations
157 if app_annots:
158 fielddict['application_annotations'] = app_annots
159 return fielddict
160
161def create_many_contacts(
162 num_contacts=10, host='localhost', port=None,
163 db_name='contacts', doctype=CONTACT_DOCTYPE, app_annots=None):
164 """Make many contacts and create their records"""
165 if port is None:
166 desktopcouch.find_pid()
167 port = desktopcouch.find_port()
168 server_url = 'http://%s:%s/' % (host, port)
169 server = Server(server_url)
170 db = server[db_name] if db_name in server else server.create(db_name)
171 record_ids = []
172 for maincount in range(1, num_contacts + 1):
173 # Make the contact
174 fielddict = make_one_contact(maincount, doctype, app_annots)
175 # Store data in CouchDB
176 record_id = db.create(fielddict)
177 record_ids.append(record_id)
178 return record_ids
0179
=== added directory 'desktopcouch/contacts/tests'
=== added file 'desktopcouch/contacts/tests/__init__.py'
--- desktopcouch/contacts/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/tests/__init__.py 2009-09-07 09:23:36 +0000
@@ -0,0 +1,18 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for Contacts API"""
18
019
=== added file 'desktopcouch/contacts/tests/test_contactspicker.py'
--- desktopcouch/contacts/tests/test_contactspicker.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/tests/test_contactspicker.py 2009-09-01 22:29:01 +0000
@@ -0,0 +1,54 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Rick Spencer <rick.spencer@canonical.com>
18
19"""Tests for the ContactsPicker class"""
20
21import testtools
22import gtk
23
24from desktopcouch.tests import xdg_cache
25
26from desktopcouch.contacts.contactspicker import ContactsPicker
27from desktopcouch.records.server import CouchDatabase
28
29class TestContactsPicker(testtools.TestCase):
30 """Test the Contact Picker Window."""
31
32 def setUp(self):
33 """setup each test"""
34 # Connect to CouchDB server
35 self.assert_(xdg_cache)
36 self.dbname = 'contacts'
37 self.database = CouchDatabase(self.dbname, create=True)
38
39 def tearDown(self):
40 """tear down each test"""
41 del self.database._server[self.dbname]
42
43 def test_can_contruct_contactspicker(self):
44 """Test that we can build the window"""
45 # Create and show a test window
46 win = gtk.Dialog("ContactsPicker widget test", None, 0,
47 (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
48 win.connect("response", gtk.main_quit)
49 win.resize(300, 450)
50
51 # Create the contacts picker widget
52 picker = ContactsPicker()
53 win.get_content_area().pack_start(picker, True, True, 3)
54 self.assert_(picker.get_contacts_list())
055
=== added file 'desktopcouch/contacts/tests/test_create.py'
--- desktopcouch/contacts/tests/test_create.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/tests/test_create.py 2009-09-14 19:02:58 +0000
@@ -0,0 +1,67 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches