Merge lp:~stefanor/ibid/exchange-825217 into lp:ibid
- exchange-825217
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Stefano Rivera | ||||
Approved revision: | 1068 | ||||
Merged at revision: | 1033 | ||||
Proposed branch: | lp:~stefanor/ibid/exchange-825217 | ||||
Merge into: | lp:ibid | ||||
Diff against target: |
549 lines (+258/-99) 8 files modified
docs/api/ibid.utils.rst (+8/-1) ibid/plugins/conversions.py (+140/-68) ibid/test/__init__.py (+26/-8) ibid/test/plugins/test_conversions.py (+52/-4) ibid/test/plugins/test_core.py (+6/-11) ibid/test/plugins/test_url.py (+3/-3) ibid/test/test_utils.py (+19/-0) ibid/utils/__init__.py (+4/-4) |
||||
To merge this branch: | bzr merge lp:~stefanor/ibid/exchange-825217 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Max Rabkin | Approve | ||
Review via email: mp+71367@code.launchpad.net |
Commit message
XE.com no longer has a nice helpful country to currency list, so we build our own, based on ISO-4127. This means many heuristics, so we include a reasonable test suite.
Description of the change
- 1033. By Stefano Rivera
-
Load country_codes in _load_currencies
- 1034. By Stefano Rivera
-
Comment data-structures, remove XE.com bracketed place name search heuristics, and return to pre r586 (exchange-343775) heuristics
- 1035. By Stefano Rivera
-
Plugin name in cacheable_download location
- 1036. By Stefano Rivera
-
Add test suite for resolve_currency (and rename to a public name)
- 1037. By Stefano Rivera
-
Filter non-exchangeable currencies
- 1038. By Stefano Rivera
-
Add ibid.test.TestCase, less full-stack than PluginTestCase
- 1039. By Stefano Rivera
-
use ibid.test.TestCase for conversions tests
- 1040. By Stefano Rivera
-
Use ibid.test.TestCase instead of unittest.TestCase for plugin tests
- 1041. By Stefano Rivera
-
More fund codes
Stefano Rivera (stefanor) wrote : | # |
A.2 is of course a .doc file. I've just used it to hard-code some more fund codes.
The fund codes don't exactly hurt, they just cause confusion (and one of them matched euro before the euro currency :P )
I implemented mkdtemp, but throwing away the cache after every test is a bit silly, and seeing as our test.ini contains "cachedir = cache", I thought I'd just leave it using that.
- 1042. By Stefano Rivera
-
Missing super()
- 1043. By Stefano Rivera
-
Typos
- 1044. By Stefano Rivera
-
Add a full-stack CurrencyConvers
ionTest - 1045. By Stefano Rivera
-
Group 'currencies for country' output by country
- 1046. By Stefano Rivera
-
Omit 'No universal currency' entries
- 1047. By Stefano Rivera
-
Don't assume we have country_currencies for every country_code
- 1048. By Stefano Rivera
-
Try to determine the country code through country_codes if the currency does not begin with a country code
- 1049. By Stefano Rivera
-
Identify all countries into country_currencies or warn. Detect non_currencies early
- 1050. By Stefano Rivera
-
Do exact search before 'in' searches, fill more entries in country_currencies (including Euro contries)
- 1051. By Stefano Rivera
-
Strip whitespace from re-ordered names
- 1052. By Stefano Rivera
-
Make sure we get countries that primarily use another country's currency in country_currencies
- 1053. By Stefano Rivera
-
Snarky comment for same-currency conversions
Max Rabkin (max-rabkin) wrote : | # |
<Taejo> tibid: currencies for Saint Helena
<tibid> Taejo: Ascension And Tristan Da Cunha Saint Helena uses Saint Helena Pound (SHP)
<Taejo> tumbleweed: ^^
<Taejo> should be called "Saint Helena, Ascension and Tristan da Cunha"
<Taejo> but your code to invert "foo, the republic of"
<Taejo> also "Taiwan, Province of China"
<Taejo> looking through the list, one solution is to check that it ends with " of" before inverting
- 1054. By Stefano Rivera
-
Tweak country comma-reordering to not reorder compound names.
- 1055. By Stefano Rivera
-
Document ibid.utils.
get_country_ codes - 1056. By Stefano Rivera
-
Tweak country comma-reordering to not reorder compound names. in ibid.utils, add tests.
Stefano Rivera (stefanor) wrote : | # |
Found a minutely better solution and added a test case. Also some missing documentation and stuff. Sorry the commits are rather unorganised.
- 1057. By Stefano Rivera
-
etree's iter() function is only in 2.7 (even if it makes lound deprecation noises)
- 1058. By Stefano Rivera
-
Ag screw it, let's stop rotating country names with commas in them. Too messy
- 1059. By Stefano Rivera
-
Country: Currency name
- 1060. By Stefano Rivera
-
More test cases for Currency Lookup
- 1061. By Stefano Rivera
-
Don't let currencies that don't start with country-codes fall through the gaps
- 1062. By Stefano Rivera
-
s/Sint/Saint/ only works if you do it everywhere
- 1063. By Stefano Rivera
-
Drop country names, the currency table we are using disambiguates currency names sufficiently
- 1064. By Stefano Rivera
-
Don't pre-compile regexes unecessarily
- 1065. By Stefano Rivera
-
Merge nested if pair
Max Rabkin (max-rabkin) : | # |
- 1066. By Stefano Rivera
-
Only do the inexact currency name search if rough
- 1067. By Stefano Rivera
-
Add fallthrough test case
- 1068. By Stefano Rivera
-
Handle reversed format conversion questions, such as 'convert GBP 1 to ZAR'. Otherwise units will grab them
Preview Diff
1 | === modified file 'docs/api/ibid.utils.rst' | |||
2 | --- docs/api/ibid.utils.rst 2011-01-21 21:20:22 +0000 | |||
3 | +++ docs/api/ibid.utils.rst 2011-10-23 20:17:24 +0000 | |||
4 | @@ -125,9 +125,16 @@ | |||
5 | 125 | 125 | ||
6 | 126 | Returns the filename to the resource. | 126 | Returns the filename to the resource. |
7 | 127 | 127 | ||
8 | 128 | .. function:: get_country_codes() | ||
9 | 129 | |||
10 | 130 | Retrieve and decode a list of ISO-3166-1 country codes. | ||
11 | 131 | |||
12 | 132 | Returns a dict of code -> country_name. | ||
13 | 133 | The codes are capitalised. | ||
14 | 134 | |||
15 | 128 | .. function:: identity_name(event, identity) | 135 | .. function:: identity_name(event, identity) |
16 | 129 | 136 | ||
18 | 130 | Refer to *identity* naturally in response to *event*. | 137 | Refer to *identity* naturally in response to *event*. |
19 | 131 | 138 | ||
20 | 132 | URL Functions | 139 | URL Functions |
21 | 133 | ------------- | 140 | ------------- |
22 | 134 | 141 | ||
23 | === modified file 'ibid/plugins/conversions.py' | |||
24 | --- ibid/plugins/conversions.py 2010-09-13 09:22:29 +0000 | |||
25 | +++ ibid/plugins/conversions.py 2011-10-23 20:17:24 +0000 | |||
26 | @@ -9,10 +9,10 @@ | |||
27 | 9 | 9 | ||
28 | 10 | import ibid | 10 | import ibid |
29 | 11 | from ibid.plugins import Processor, handler, match | 11 | from ibid.plugins import Processor, handler, match |
31 | 12 | from ibid.compat import any, defaultdict | 12 | from ibid.compat import any, defaultdict, ElementTree |
32 | 13 | from ibid.config import Option | 13 | from ibid.config import Option |
35 | 14 | from ibid.utils import file_in_path, get_country_codes, human_join, \ | 14 | from ibid.utils import (cacheable_download, file_in_path, get_country_codes, |
36 | 15 | unicode_output, generic_webservice | 15 | human_join, unicode_output, generic_webservice) |
37 | 16 | from ibid.utils.html import get_html_parse_tree | 16 | from ibid.utils.html import get_html_parse_tree |
38 | 17 | 17 | ||
39 | 18 | features = {} | 18 | features = {} |
40 | @@ -312,86 +312,153 @@ | |||
41 | 312 | country_codes = {} | 312 | country_codes = {} |
42 | 313 | 313 | ||
43 | 314 | def _load_currencies(self): | 314 | def _load_currencies(self): |
52 | 315 | etree = get_html_parse_tree( | 315 | iso4127_file = cacheable_download( |
53 | 316 | 'http://www.xe.com/iso4217.php', headers = { | 316 | 'http://www.currency-iso.org/dl_iso_table_a1.xml', |
54 | 317 | 'User-Agent': 'Mozilla/5.0', | 317 | 'conversions/iso4217.xml') |
55 | 318 | 'Referer': 'http://www.xe.com/', | 318 | document = ElementTree.parse(iso4127_file) |
56 | 319 | }, treetype='etree') | 319 | # Code -> [Countries..., Currency Name] |
49 | 320 | |||
50 | 321 | tbl_main = [x for x in etree.getiterator('table') if x.get('class') == 'tbl_main'][0] | ||
51 | 322 | |||
57 | 323 | self.currencies = {} | 320 | self.currencies = {} |
79 | 324 | for tbl_sub in tbl_main.getiterator('table'): | 321 | # Country -> Code |
80 | 325 | if tbl_sub.get('class') == 'tbl_sub': | 322 | self.country_currencies = {} |
81 | 326 | for tr in tbl_sub.getiterator('tr'): | 323 | self.country_codes = get_country_codes() |
82 | 327 | code, place = [x.text for x in tr.getchildren()] | 324 | # Non-currencies: |
83 | 328 | name = u'' | 325 | non_currencies = set(('BOV CLF COU MXV ' |
84 | 329 | if not place: | 326 | 'UYI XSU XUA ' # Various Fund codes |
85 | 330 | place = u'' | 327 | 'CHE CHW ' # Swiss WIR currencies |
86 | 331 | if u',' in place[1:-1]: | 328 | 'USN USS ' # US Dollar fund codes |
87 | 332 | place, name = place.split(u',', 1) | 329 | 'XAG XAU XPD XPT ' # Metals |
88 | 333 | place = place.strip() | 330 | 'XBA XBB XBC XBD ' # Euro Bond Market |
89 | 334 | if code in self.currencies: | 331 | 'XDR XTS XXX ' # Other specials |
90 | 335 | currency = self.currencies[code] | 332 | ).split()) |
91 | 336 | # Are we using another country's currency? | 333 | no_country_codes = set(('Saint Martin', |
92 | 337 | if place != u'' and name != u'' and (currency[1] == u'' or currency[1].rsplit(None, 1)[0] in place | 334 | 'Virgin Islands (Us)', |
93 | 338 | or (u'(also called' in currency[1] and currency[1].split(u'(', 1)[0].rsplit(None, 1)[0] in place)): | 335 | 'Virgin Islands (British)',)) |
94 | 339 | currency[0].insert(0, place) | 336 | accociated_all_countries = True |
95 | 340 | currency[1] = name.strip() | 337 | for currency in document.getiterator('ISO_CURRENCY'): |
96 | 341 | else: | 338 | code = currency.findtext('ALPHABETIC_CODE').strip() |
97 | 342 | currency[0].append(place) | 339 | name = currency.findtext('CURRENCY').strip() |
98 | 343 | else: | 340 | place = currency.findtext('ENTITY').strip().title() |
99 | 344 | self.currencies[code] = [[place], name.strip()] | 341 | if code == '' or code in non_currencies: |
100 | 342 | continue | ||
101 | 343 | # Fund codes | ||
102 | 344 | if re.match(r'^Zz[0-9]{2}', place, re.UNICODE): | ||
103 | 345 | continue | ||
104 | 346 | if code in self.currencies: | ||
105 | 347 | self.currencies[code][0].append(place) | ||
106 | 348 | else: | ||
107 | 349 | self.currencies[code] = [[place], name] | ||
108 | 350 | if place in no_country_codes: | ||
109 | 351 | continue | ||
110 | 352 | if (code[:2] in self.country_codes | ||
111 | 353 | and code[:2] not in self.country_currencies): | ||
112 | 354 | self.country_currencies[code[:2]] = code | ||
113 | 355 | continue | ||
114 | 356 | ascii_place = (unicodedata.normalize('NFD', unicode(place)) | ||
115 | 357 | .encode('ASCII', 'ignore') | ||
116 | 358 | .replace('-', ' ') | ||
117 | 359 | .replace('Sint', 'Saint')) | ||
118 | 360 | |||
119 | 361 | # Countries with (alternative names) | ||
120 | 362 | swapped_place = None | ||
121 | 363 | m = re.match(r'^(.+?)\s+\((.+)\)$', ascii_place) | ||
122 | 364 | if m is not None: | ||
123 | 365 | swapped_place = '%s (%s)' % (m.group(2), m.group(1)) | ||
124 | 366 | |||
125 | 367 | for ccode, country in self.country_codes.iteritems(): | ||
126 | 368 | country = country.title() | ||
127 | 369 | ascii_country = (unicodedata.normalize('NFD', country) | ||
128 | 370 | .encode('ASCII', 'ignore') | ||
129 | 371 | .replace('-', ' ') | ||
130 | 372 | .replace('Sint', 'Saint')) | ||
131 | 373 | if ascii_country in (ascii_place, swapped_place): | ||
132 | 374 | if ccode not in self.country_currencies: | ||
133 | 375 | self.country_currencies[ccode] = code | ||
134 | 376 | break | ||
135 | 377 | else: | ||
136 | 378 | log.info(u"ISO4127 parsing: Can't identify %s as a known " | ||
137 | 379 | u"country", place) | ||
138 | 380 | accociated_all_countries = False | ||
139 | 345 | 381 | ||
140 | 346 | # Special cases for shared currencies: | 382 | # Special cases for shared currencies: |
144 | 347 | self.currencies['EUR'][0].insert(0, u'Euro Member Countries') | 383 | self.currencies['EUR'][0].append(u'Euro Member Countries') |
145 | 348 | self.currencies['XOF'][0].insert(0, u'Communaut\xe9 Financi\xe8re Africaine') | 384 | self.currencies['XAF'][0].append(u"Communaut\xe9 financi\xe8re d'Afrique") |
146 | 349 | self.currencies['XOF'][1] = u'Francs' | 385 | self.currencies['XCD'][0].append(u'Organisation of Eastern Caribbean States') |
147 | 386 | self.currencies['XOF'][0].append(u'Coop\xe9ration financi\xe8re en Afrique centrale') | ||
148 | 387 | self.currencies['XPF'][0].append(u'Comptoirs Fran\xe7ais du Pacifique') | ||
149 | 388 | return accociated_all_countries | ||
150 | 350 | 389 | ||
152 | 351 | def _resolve_currency(self, name, rough=True): | 390 | def resolve_currency(self, name, rough=True, plural_recursion=False): |
153 | 352 | "Return the canonical name for a currency" | 391 | "Return the canonical name for a currency" |
154 | 353 | 392 | ||
155 | 393 | if not self.currencies: | ||
156 | 394 | self._load_currencies() | ||
157 | 395 | |||
158 | 354 | if name.upper() in self.currencies: | 396 | if name.upper() in self.currencies: |
159 | 355 | return name.upper() | 397 | return name.upper() |
160 | 356 | 398 | ||
164 | 357 | strip_currency_re = re.compile(r'^[\.\s]*([\w\s]+?)s?$', re.UNICODE) | 399 | # Strip leading dots (.TLD) |
165 | 358 | m = strip_currency_re.match(name) | 400 | m = re.match(r'^[\.\s]*(.+)$', name, re.UNICODE) |
163 | 359 | |||
166 | 360 | if m is None: | 401 | if m is None: |
167 | 361 | return False | 402 | return False |
168 | 362 | |||
169 | 363 | name = m.group(1).lower() | 403 | name = m.group(1).lower() |
170 | 364 | 404 | ||
174 | 365 | # TLD -> country name | 405 | # TLD: |
175 | 366 | if rough and len(name) == 2 and name.upper() in self.country_codes: | 406 | if rough and len(name) == 2 and name.upper() in self.country_currencies: |
176 | 367 | name = self.country_codes[name.upper()].lower() | 407 | return self.country_currencies[name.upper()] |
177 | 368 | 408 | ||
178 | 369 | # Currency Name | 409 | # Currency Name |
179 | 370 | if name == u'dollar': | 410 | if name == u'dollar': |
180 | 371 | return "USD" | 411 | return "USD" |
183 | 372 | 412 | if name == u'pound': | |
184 | 373 | name_re = re.compile(r'^(.+\s+)?\(?%ss?\)?(\s+.+)?$' % name, re.I | re.UNICODE) | 413 | return "GBP" |
185 | 374 | for code, (places, currency) in self.currencies.iteritems(): | 414 | for code, (places, currency) in self.currencies.iteritems(): |
189 | 375 | if name_re.match(currency) or [True for place in places if name_re.match(place)]: | 415 | if name == currency.lower(): |
190 | 376 | return code | 416 | return code |
191 | 377 | 417 | if name.title() in places: | |
192 | 418 | return code | ||
193 | 419 | |||
194 | 420 | # There are also country names in country_codes: | ||
195 | 421 | for code, place in self.country_codes.iteritems(): | ||
196 | 422 | if name == place.lower() and code in self.country_currencies: | ||
197 | 423 | return self.country_currencies[code] | ||
198 | 424 | |||
199 | 425 | # Second pass, not requiring exact match: | ||
200 | 426 | if rough: | ||
201 | 427 | for code, (places, currency) in self.currencies.iteritems(): | ||
202 | 428 | if name in currency.lower(): | ||
203 | 429 | return code | ||
204 | 430 | if any(name in place.lower() for place in places): | ||
205 | 431 | return code | ||
206 | 432 | |||
207 | 433 | for code, place in self.country_codes.iteritems(): | ||
208 | 434 | if name in place.lower() and code in self.country_currencies: | ||
209 | 435 | return self.country_currencies[code] | ||
210 | 436 | |||
211 | 437 | # Maybe it's a plural? | ||
212 | 438 | if name.endswith('s') and not plural_recursion: | ||
213 | 439 | return self.resolve_currency(name[:-1], rough, True) | ||
214 | 378 | return False | 440 | return False |
215 | 379 | 441 | ||
216 | 380 | @match(r'^(exchange|convert)\s+([0-9.]+)\s+(.+)\s+(?:for|to|into)\s+(.+)$') | 442 | @match(r'^(exchange|convert)\s+([0-9.]+)\s+(.+)\s+(?:for|to|into)\s+(.+)$') |
217 | 381 | def exchange(self, event, command, amount, frm, to): | 443 | def exchange(self, event, command, amount, frm, to): |
218 | 382 | if not self.currencies: | ||
219 | 383 | self._load_currencies() | ||
220 | 384 | |||
221 | 385 | if not self.country_codes: | ||
222 | 386 | self.country_codes = get_country_codes() | ||
223 | 387 | |||
224 | 388 | rough = command.lower() == 'exchange' | 444 | rough = command.lower() == 'exchange' |
225 | 389 | 445 | ||
228 | 390 | canonical_frm = self._resolve_currency(frm, rough) | 446 | canonical_frm = self.resolve_currency(frm, rough) |
229 | 391 | canonical_to = self._resolve_currency(to, rough) | 447 | canonical_to = self.resolve_currency(to, rough) |
230 | 392 | if not canonical_frm or not canonical_to: | 448 | if not canonical_frm or not canonical_to: |
231 | 393 | if rough: | 449 | if rough: |
233 | 394 | event.addresponse(u"Sorry, I don't know about a currency for %s", (not canonical_frm and frm or to)) | 450 | event.addresponse( |
234 | 451 | u"Sorry, I don't know about a currency for %s", | ||
235 | 452 | (not canonical_frm and frm or to)) | ||
236 | 453 | return | ||
237 | 454 | if canonical_frm == canonical_to: | ||
238 | 455 | event.addresponse( | ||
239 | 456 | u"Um, that's the same currency. Tell you what, " | ||
240 | 457 | u"I can offer you my special rate of 0.5 %(currency)s for " | ||
241 | 458 | u"each %(code)s you sell me.", { | ||
242 | 459 | 'currency': self.currencies[canonical_frm][1], | ||
243 | 460 | 'code': canonical_frm, | ||
244 | 461 | }) | ||
245 | 395 | return | 462 | return |
246 | 396 | 463 | ||
247 | 397 | data = generic_webservice( | 464 | data = generic_webservice( |
248 | @@ -407,14 +474,12 @@ | |||
249 | 407 | return | 474 | return |
250 | 408 | 475 | ||
251 | 409 | event.addresponse( | 476 | event.addresponse( |
254 | 410 | u'%(fresult)s %(fcode)s (%(fcountry)s %(fcurrency)s) = ' | 477 | u'%(fresult)s %(fcode)s (%(fcurrency)s) = ' |
255 | 411 | u'%(tresult)0.2f %(tcode)s (%(tcountry)s %(tcurrency)s) ' | 478 | u'%(tresult)0.2f %(tcode)s (%(tcurrency)s) ' |
256 | 412 | u'(Last trade rate: %(rate)s, Bid: %(bid)s, Ask: %(ask)s)', { | 479 | u'(Last trade rate: %(rate)s, Bid: %(bid)s, Ask: %(ask)s)', { |
257 | 413 | 'fresult': amount, | 480 | 'fresult': amount, |
258 | 414 | 'tresult': float(amount) * float(last_trade_rate), | 481 | 'tresult': float(amount) * float(last_trade_rate), |
259 | 415 | 'fcountry': self.currencies[canonical_frm][0][0], | ||
260 | 416 | 'fcurrency': self.currencies[canonical_frm][1], | 482 | 'fcurrency': self.currencies[canonical_frm][1], |
261 | 417 | 'tcountry': self.currencies[canonical_to][0][0], | ||
262 | 418 | 'tcurrency': self.currencies[canonical_to][1], | 483 | 'tcurrency': self.currencies[canonical_to][1], |
263 | 419 | 'fcode': canonical_frm, | 484 | 'fcode': canonical_frm, |
264 | 420 | 'tcode': canonical_to, | 485 | 'tcode': canonical_to, |
265 | @@ -423,21 +488,28 @@ | |||
266 | 423 | 'ask': ask, | 488 | 'ask': ask, |
267 | 424 | }) | 489 | }) |
268 | 425 | 490 | ||
269 | 491 | @match(r'^(exchange|convert)\s+(.+)\s+([0-9.]+)\s+(?:for|to|into)\s+(.+)$') | ||
270 | 492 | def exchange_reversed(self, event, command, amount, frm, to): | ||
271 | 493 | self.exchange(event, command, frm, amount, to) | ||
272 | 494 | |||
273 | 495 | |||
274 | 426 | @match(r'^(?:currency|currencies)\s+for\s+(?:the\s+)?(.+)$') | 496 | @match(r'^(?:currency|currencies)\s+for\s+(?:the\s+)?(.+)$') |
275 | 427 | def currency(self, event, place): | 497 | def currency(self, event, place): |
276 | 428 | if not self.currencies: | 498 | if not self.currencies: |
277 | 429 | self._load_currencies() | 499 | self._load_currencies() |
278 | 430 | 500 | ||
285 | 431 | search = re.compile(place, re.I) | 501 | results = defaultdict(list) |
286 | 432 | results = [] | 502 | for code, (c_places, name) in self.currencies.iteritems(): |
287 | 433 | for code, (places, name) in self.currencies.iteritems(): | 503 | for c_place in c_places: |
288 | 434 | for place in places: | 504 | if re.search(place, c_place, re.I): |
289 | 435 | if search.search(place): | 505 | results[c_place].append(u'%s (%s)' % (name, code)) |
284 | 436 | results.append(u'%s uses %s (%s)' % (place, name, code)) | ||
290 | 437 | break | 506 | break |
291 | 438 | 507 | ||
292 | 439 | if results: | 508 | if results: |
294 | 440 | event.addresponse(human_join(results)) | 509 | event.addresponse(human_join( |
295 | 510 | u'%s uses %s' % (place, human_join(currencies)) | ||
296 | 511 | for place, currencies in results.iteritems() | ||
297 | 512 | )) | ||
298 | 441 | else: | 513 | else: |
299 | 442 | event.addresponse(u'No currencies found') | 514 | event.addresponse(u'No currencies found') |
300 | 443 | 515 | ||
301 | 444 | 516 | ||
302 | === modified file 'ibid/test/__init__.py' | |||
303 | --- ibid/test/__init__.py 2011-06-20 20:59:56 +0000 | |||
304 | +++ ibid/test/__init__.py 2011-10-23 20:17:24 +0000 | |||
305 | @@ -6,7 +6,7 @@ | |||
306 | 6 | import os | 6 | import os |
307 | 7 | from traceback import format_exception | 7 | from traceback import format_exception |
308 | 8 | import re | 8 | import re |
310 | 9 | from shutil import copyfile | 9 | import shutil |
311 | 10 | import sys | 10 | import sys |
312 | 11 | import tempfile | 11 | import tempfile |
313 | 12 | 12 | ||
314 | @@ -72,21 +72,37 @@ | |||
315 | 72 | return None | 72 | return None |
316 | 73 | 73 | ||
317 | 74 | 74 | ||
319 | 75 | class PluginTestCase(unittest.TestCase): | 75 | class TestCase(unittest.TestCase): |
320 | 76 | """TestCase subclass, implementing: | ||
321 | 77 | * detection for tests using network resources | ||
322 | 78 | * basic Ibid configuration | ||
323 | 79 | """ | ||
324 | 80 | network = False | ||
325 | 81 | |||
326 | 82 | def setUp(self): | ||
327 | 83 | super(TestCase, self).setUp() | ||
328 | 84 | if self.network and os.getenv('IBID_NETWORKLESS_TEST') is not None: | ||
329 | 85 | raise unittest.SkipTest('test uses network') | ||
330 | 86 | ibid.config = FileConfig(locate_resource('ibid.test', 'test.ini')) | ||
331 | 87 | |||
332 | 88 | |||
333 | 89 | class PluginTestCase(TestCase): | ||
334 | 90 | """A full-stack plugin test, implementing: | ||
335 | 91 | * Loading of the specified plugins before running the tests, and cleanup | ||
336 | 92 | afterwards | ||
337 | 93 | * DB support (clean DB for each TestCase) | ||
338 | 94 | * Test events passed through the standard Ibid event dispatcher | ||
339 | 95 | """ | ||
340 | 76 | load = [] | 96 | load = [] |
341 | 77 | noload = [] | 97 | noload = [] |
342 | 78 | load_base = True | 98 | load_base = True |
343 | 79 | load_configured = None | 99 | load_configured = None |
344 | 80 | username = u'user' | 100 | username = u'user' |
345 | 81 | public = False | 101 | public = False |
346 | 82 | network = False | ||
347 | 83 | empty_dbfile = None | 102 | empty_dbfile = None |
348 | 84 | 103 | ||
349 | 85 | def setUp(self): | 104 | def setUp(self): |
354 | 86 | if self.network and os.getenv('IBID_NETWORKLESS_TEST') is not None: | 105 | super(PluginTestCase, self).setUp() |
351 | 87 | raise unittest.SkipTest('test uses network') | ||
352 | 88 | |||
353 | 89 | ibid.config = FileConfig(locate_resource('ibid.test', 'test.ini')) | ||
355 | 90 | 106 | ||
356 | 91 | if self.load_configured is None: | 107 | if self.load_configured is None: |
357 | 92 | self.load_configured = not self.load | 108 | self.load_configured = not self.load |
358 | @@ -145,7 +161,7 @@ | |||
359 | 145 | if self.empty_dbfile is None: | 161 | if self.empty_dbfile is None: |
360 | 146 | self._create_empty_database() | 162 | self._create_empty_database() |
361 | 147 | self.dbfile = self.mktemp() | 163 | self.dbfile = self.mktemp() |
363 | 148 | copyfile(self.empty_dbfile, self.dbfile) | 164 | shutil.copyfile(self.empty_dbfile, self.dbfile) |
364 | 149 | ibid.config['databases']['ibid'] = 'sqlite:///' + self.dbfile | 165 | ibid.config['databases']['ibid'] = 'sqlite:///' + self.dbfile |
365 | 150 | 166 | ||
366 | 151 | def make_event(self, message=None, type=u'message'): | 167 | def make_event(self, message=None, type=u'message'): |
367 | @@ -216,6 +232,8 @@ | |||
368 | 216 | self.fail("Event was expected to fail", event) | 232 | self.fail("Event was expected to fail", event) |
369 | 217 | 233 | ||
370 | 218 | def tearDown(self): | 234 | def tearDown(self): |
371 | 235 | super(PluginTestCase, self).tearDown() | ||
372 | 236 | |||
373 | 219 | for processor in ibid.processors: | 237 | for processor in ibid.processors: |
374 | 220 | processor.shutdown() | 238 | processor.shutdown() |
375 | 221 | del ibid.processors[:] | 239 | del ibid.processors[:] |
376 | 222 | 240 | ||
377 | === modified file 'ibid/test/plugins/test_conversions.py' | |||
378 | --- ibid/test/plugins/test_conversions.py 2011-01-26 12:20:59 +0000 | |||
379 | +++ ibid/test/plugins/test_conversions.py 2011-10-23 20:17:24 +0000 | |||
380 | @@ -1,9 +1,11 @@ | |||
382 | 1 | # Copyright (c) 2010-2011, Max Rabkin | 1 | # Copyright (c) 2010-2011, Max Rabkin, Stefano Rivera |
383 | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. |
384 | 3 | 3 | ||
388 | 4 | from ibid.test import PluginTestCase | 4 | import logging |
389 | 5 | 5 | ||
390 | 6 | class UnihanTest(PluginTestCase): | 6 | import ibid.test |
391 | 7 | |||
392 | 8 | class UnihanTest(ibid.test.PluginTestCase): | ||
393 | 7 | load = ['conversions'] | 9 | load = ['conversions'] |
394 | 8 | network = True | 10 | network = True |
395 | 9 | 11 | ||
396 | @@ -12,3 +14,49 @@ | |||
397 | 12 | u'.*the traditional form is \u99AC') | 14 | u'.*the traditional form is \u99AC') |
398 | 13 | self.assertResponseMatches(u'unihan \u99AC', | 15 | self.assertResponseMatches(u'unihan \u99AC', |
399 | 14 | u'.*the simplified form is \u9A6C') | 16 | u'.*the simplified form is \u9A6C') |
400 | 17 | |||
401 | 18 | class CurrencyLookupTest(ibid.test.TestCase): | ||
402 | 19 | network = True | ||
403 | 20 | |||
404 | 21 | def setUp(self): | ||
405 | 22 | super(CurrencyLookupTest, self).setUp() | ||
406 | 23 | from ibid.plugins import conversions | ||
407 | 24 | self.processor = conversions.Currency(u'testplugin') | ||
408 | 25 | |||
409 | 26 | def test_country_association(self): | ||
410 | 27 | self.assertTrue(self.processor._load_currencies()) | ||
411 | 28 | |||
412 | 29 | def test_common_currencies(self): | ||
413 | 30 | self.assertEqual(self.processor.resolve_currency('pound', True), 'GBP') | ||
414 | 31 | self.assertEqual(self.processor.resolve_currency('dollar', True), 'USD') | ||
415 | 32 | self.assertEqual(self.processor.resolve_currency('euro', True), 'EUR') | ||
416 | 33 | self.assertEqual(self.processor.resolve_currency('rand', True), 'ZAR') | ||
417 | 34 | |||
418 | 35 | def test_tld(self): | ||
419 | 36 | self.assertEqual(self.processor.resolve_currency('.za', True), 'ZAR') | ||
420 | 37 | self.assertEqual(self.processor.resolve_currency('.na', True), 'NAD') | ||
421 | 38 | self.assertEqual(self.processor.resolve_currency('.ch', True), 'CHF') | ||
422 | 39 | # Currency from a former country | ||
423 | 40 | self.assertEqual(self.processor.resolve_currency('.sx', True), 'ANG') | ||
424 | 41 | # X- Currency | ||
425 | 42 | self.assertEqual(self.processor.resolve_currency('.cm', True), 'XAF') | ||
426 | 43 | |||
427 | 44 | def test_country(self): | ||
428 | 45 | self.assertEqual(self.processor.resolve_currency('united kingdom', True), 'GBP') | ||
429 | 46 | self.assertEqual(self.processor.resolve_currency('south africa', True), 'ZAR') | ||
430 | 47 | self.assertEqual(self.processor.resolve_currency('bosnia', True), 'BAM') | ||
431 | 48 | |||
432 | 49 | def test_fallthrough(self): | ||
433 | 50 | self.assertFalse(self.processor.resolve_currency('oz', False)) | ||
434 | 51 | |||
435 | 52 | class CurrencyConversionTest(ibid.test.PluginTestCase): | ||
436 | 53 | load = ['conversions'] | ||
437 | 54 | network = True | ||
438 | 55 | |||
439 | 56 | def test_conversion(self): | ||
440 | 57 | self.assertResponseMatches(u'exchange 1 Pound for ZAR', | ||
441 | 58 | r'1 GBP \(.+\) = [0-9.]+ ZAR \(.+\) .* Bid: [0-9.]+') | ||
442 | 59 | self.assertResponseMatches(u'exchange 1 France for Egypt', | ||
443 | 60 | r'1 EUR \(.+\) = [0-9.]+ EGP \(.+\) .* Bid: [0-9.]+') | ||
444 | 61 | self.assertResponseMatches(u'exchange 1 Virgin Islands for .tv', | ||
445 | 62 | r'1 USD \(.+\) = [0-9.]+ AUD \(.+\) .* Bid: [0-9.]+') | ||
446 | 15 | 63 | ||
447 | === modified file 'ibid/test/plugins/test_core.py' | |||
448 | --- ibid/test/plugins/test_core.py 2010-05-01 10:02:44 +0000 | |||
449 | +++ ibid/test/plugins/test_core.py 2011-10-23 20:17:24 +0000 | |||
450 | @@ -1,22 +1,17 @@ | |||
451 | 1 | # Copyright (c) 2009-2010, Jeremy Thurgood and Max Rabkin | 1 | # Copyright (c) 2009-2010, Jeremy Thurgood and Max Rabkin |
452 | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. |
453 | 3 | 3 | ||
456 | 4 | from twisted.trial import unittest | 4 | import ibid |
455 | 5 | |||
457 | 6 | import ibid.test | 5 | import ibid.test |
458 | 7 | from ibid.event import Event | 6 | from ibid.event import Event |
459 | 8 | 7 | ||
461 | 9 | class TestAddressed(unittest.TestCase): | 8 | class TestAddressed(ibid.test.TestCase): |
462 | 10 | 9 | ||
463 | 11 | def setUp(self): | 10 | def setUp(self): |
472 | 12 | ibid.test.set_config({ | 11 | super(TestAddressed, self).setUp() |
473 | 13 | u'botname': u'test_ibid', | 12 | ibid.config.botname = u'test_ibid' |
474 | 14 | u'plugins': { | 13 | ibid.config.plugins['testplugin'] = ibid.test.FakeConfig( |
475 | 15 | u'testplugin': { | 14 | {'names': [u'test_ibid', u'bot', u'ant']}) |
468 | 16 | u'names': [u'test_ibid', u'bot', u'ant'] | ||
469 | 17 | }, | ||
470 | 18 | }, | ||
471 | 19 | }) | ||
476 | 20 | 15 | ||
477 | 21 | from ibid.plugins import core | 16 | from ibid.plugins import core |
478 | 22 | self.processor = core.Addressed(u'testplugin') | 17 | self.processor = core.Addressed(u'testplugin') |
479 | 23 | 18 | ||
480 | === modified file 'ibid/test/plugins/test_url.py' | |||
481 | --- ibid/test/plugins/test_url.py 2010-01-18 22:45:15 +0000 | |||
482 | +++ ibid/test/plugins/test_url.py 2011-10-23 20:17:24 +0000 | |||
483 | @@ -1,13 +1,13 @@ | |||
484 | 1 | # Copyright (c) 2009-2010, Stefano Rivera | 1 | # Copyright (c) 2009-2010, Stefano Rivera |
485 | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. |
486 | 3 | 3 | ||
489 | 4 | from twisted.trial import unittest | 4 | import ibid.test |
488 | 5 | |||
490 | 6 | from ibid.plugins import urlgrab | 5 | from ibid.plugins import urlgrab |
491 | 7 | 6 | ||
493 | 8 | class TestURLGrabber(unittest.TestCase): | 7 | class TestURLGrabber(ibid.test.TestCase): |
494 | 9 | 8 | ||
495 | 10 | def setUp(self): | 9 | def setUp(self): |
496 | 10 | super(TestURLGrabber, self).setUp() | ||
497 | 11 | self.grab = urlgrab.Grab(u'testplugin') | 11 | self.grab = urlgrab.Grab(u'testplugin') |
498 | 12 | 12 | ||
499 | 13 | good_grabs = [ | 13 | good_grabs = [ |
500 | 14 | 14 | ||
501 | === added file 'ibid/test/test_utils.py' | |||
502 | --- ibid/test/test_utils.py 1970-01-01 00:00:00 +0000 | |||
503 | +++ ibid/test/test_utils.py 2011-10-23 20:17:24 +0000 | |||
504 | @@ -0,0 +1,19 @@ | |||
505 | 1 | # Copyright (c) 2011, Stefano Rivera | ||
506 | 2 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. | ||
507 | 3 | |||
508 | 4 | import datetime | ||
509 | 5 | |||
510 | 6 | import ibid.test | ||
511 | 7 | import ibid.utils | ||
512 | 8 | |||
513 | 9 | class TestUtils(ibid.test.TestCase): | ||
514 | 10 | def test_ago(self): | ||
515 | 11 | self.assertEqual(ibid.utils.ago(datetime.timedelta(seconds=60)), u'1 minute') | ||
516 | 12 | self.assertEqual(ibid.utils.ago(datetime.timedelta(seconds=60000), 1), u'16 hours') | ||
517 | 13 | |||
518 | 14 | class TestUtilsNetwork(ibid.test.TestCase): | ||
519 | 15 | network = True | ||
520 | 16 | |||
521 | 17 | def test_get_country_codes(self): | ||
522 | 18 | codes = ibid.utils.get_country_codes() | ||
523 | 19 | self.assertIn('ZA', codes) | ||
524 | 0 | 20 | ||
525 | === modified file 'ibid/utils/__init__.py' | |||
526 | --- ibid/utils/__init__.py 2011-01-22 23:04:00 +0000 | |||
527 | +++ ibid/utils/__init__.py 2011-10-23 20:17:24 +0000 | |||
528 | @@ -376,9 +376,9 @@ | |||
529 | 376 | line = line.strip() | 376 | line = line.strip() |
530 | 377 | if started and ';' in line: | 377 | if started and ';' in line: |
531 | 378 | country, code = line.split(u';') | 378 | country, code = line.split(u';') |
535 | 379 | if u',' in country: | 379 | country = country.lower() |
536 | 380 | country = u' '.join(reversed(country.split(u',', 1))) | 380 | # Hack around http://bugs.python.org/issue7008 |
537 | 381 | country = country.title() | 381 | country = country.title().replace(u"'S", u"'s") |
538 | 382 | countries[code] = country | 382 | countries[code] = country |
539 | 383 | elif line == u'': | 383 | elif line == u'': |
540 | 384 | started = True | 384 | started = True |
541 | @@ -387,7 +387,7 @@ | |||
542 | 387 | 387 | ||
543 | 388 | return countries | 388 | return countries |
544 | 389 | 389 | ||
546 | 390 | def identity_name (event, identity): | 390 | def identity_name(event, identity): |
547 | 391 | if event.identity == identity.id: | 391 | if event.identity == identity.id: |
548 | 392 | return u'you' | 392 | return u'you' |
549 | 393 | elif event.source == identity.source: | 393 | elif event.source == identity.source: |
Some non-currencies are missing (search http:// en.wikipedia. org/wiki/ ISO_4217 for "funds code" for some more -- the funds codes are in table A.2 of the standard, we may be able to use this instead of hard coding).
You should use trial's tempdir functions instead of hardcoding /tmp.
The tests are networking tesst but it don't check for IBID_NETWORKLES S_TEST.