1 | # |
---|
2 | # common.py |
---|
3 | # |
---|
4 | # Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com> |
---|
5 | # |
---|
6 | # Deluge is free software. |
---|
7 | # |
---|
8 | # You may redistribute it and/or modify it under the terms of the |
---|
9 | # GNU General Public License, as published by the Free Software |
---|
10 | # Foundation; either version 3 of the License, or (at your option) |
---|
11 | # any later version. |
---|
12 | # |
---|
13 | # deluge is distributed in the hope that it will be useful, |
---|
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
---|
16 | # See the GNU General Public License for more details. |
---|
17 | # |
---|
18 | # You should have received a copy of the GNU General Public License |
---|
19 | # along with deluge. If not, write to: |
---|
20 | # The Free Software Foundation, Inc., |
---|
21 | # 51 Franklin Street, Fifth Floor |
---|
22 | # Boston, MA 02110-1301, USA. |
---|
23 | # |
---|
24 | # In addition, as a special exception, the copyright holders give |
---|
25 | # permission to link the code of portions of this program with the OpenSSL |
---|
26 | # library. |
---|
27 | # You must obey the GNU General Public License in all respects for all of |
---|
28 | # the code used other than OpenSSL. If you modify file(s) with this |
---|
29 | # exception, you may extend this exception to your version of the file(s), |
---|
30 | # but you are not obligated to do so. If you do not wish to do so, delete |
---|
31 | # this exception statement from your version. If you delete this exception |
---|
32 | # statement from all source files in the program, then also delete it here. |
---|
33 | # |
---|
34 | # |
---|
35 | |
---|
36 | |
---|
37 | |
---|
38 | """Common functions for various parts of Deluge to use.""" |
---|
39 | |
---|
40 | import os |
---|
41 | import time |
---|
42 | import subprocess |
---|
43 | import platform |
---|
44 | import sys |
---|
45 | import chardet |
---|
46 | |
---|
47 | try: |
---|
48 | import json |
---|
49 | except ImportError: |
---|
50 | import simplejson as json |
---|
51 | |
---|
52 | # Do a little hack here just in case the user has json-py installed since it |
---|
53 | # has a different api |
---|
54 | if not hasattr(json, "dumps"): |
---|
55 | json.dumps = json.write |
---|
56 | json.loads = json.read |
---|
57 | |
---|
58 | def dump(obj, fp, **kw): |
---|
59 | fp.write(json.dumps(obj)) |
---|
60 | |
---|
61 | def load(fp, **kw): |
---|
62 | return json.loads(fp.read()) |
---|
63 | |
---|
64 | json.dump = dump |
---|
65 | json.load = load |
---|
66 | |
---|
67 | import pkg_resources |
---|
68 | import gettext |
---|
69 | import locale |
---|
70 | |
---|
71 | # Initialize gettext |
---|
72 | try: |
---|
73 | if hasattr(locale, "bindtextdomain"): |
---|
74 | locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n")) |
---|
75 | if hasattr(locale, "textdomain"): |
---|
76 | locale.textdomain("deluge") |
---|
77 | gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n")) |
---|
78 | gettext.textdomain("deluge") |
---|
79 | gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n")) |
---|
80 | except Exception, e: |
---|
81 | from deluge.log import LOG as log |
---|
82 | log.error("Unable to initialize gettext/locale!") |
---|
83 | log.exception(e) |
---|
84 | import __builtin__ |
---|
85 | __builtin__.__dict__["_"] = lambda x: x |
---|
86 | |
---|
87 | from deluge.error import * |
---|
88 | |
---|
89 | LT_TORRENT_STATE = { |
---|
90 | "Queued": 0, |
---|
91 | "Checking": 1, |
---|
92 | "Downloading Metadata": 2, |
---|
93 | "Downloading": 3, |
---|
94 | "Finished": 4, |
---|
95 | "Seeding": 5, |
---|
96 | "Allocating": 6, |
---|
97 | "Checking Resume Data": 7, |
---|
98 | 0: "Queued", |
---|
99 | 1: "Checking", |
---|
100 | 2: "Downloading Metadata", |
---|
101 | 3: "Downloading", |
---|
102 | 4: "Finished", |
---|
103 | 5: "Seeding", |
---|
104 | 6: "Allocating", |
---|
105 | 7: "Checking Resume Data" |
---|
106 | } |
---|
107 | |
---|
108 | |
---|
109 | TORRENT_STATE = [ |
---|
110 | "Allocating", |
---|
111 | "Checking", |
---|
112 | "Downloading", |
---|
113 | "Seeding", |
---|
114 | "Paused", |
---|
115 | "Error", |
---|
116 | "Queued" |
---|
117 | ] |
---|
118 | |
---|
119 | FILE_PRIORITY = { |
---|
120 | 0: "Do Not Download", |
---|
121 | 1: "Normal Priority", |
---|
122 | 2: "High Priority", |
---|
123 | 5: "Highest Priority", |
---|
124 | "Do Not Download": 0, |
---|
125 | "Normal Priority": 1, |
---|
126 | "High Priority": 2, |
---|
127 | "Highest Priority": 5 |
---|
128 | } |
---|
129 | |
---|
130 | def get_version(): |
---|
131 | """ |
---|
132 | Returns the program version from the egg metadata |
---|
133 | |
---|
134 | :returns: the version of Deluge |
---|
135 | :rtype: string |
---|
136 | |
---|
137 | """ |
---|
138 | return pkg_resources.require("Deluge")[0].version |
---|
139 | |
---|
140 | def get_default_config_dir(filename=None): |
---|
141 | """ |
---|
142 | :param filename: if None, only the config path is returned, if provided, a path including the filename will be returned |
---|
143 | :type filename: string |
---|
144 | :returns: a file path to the config directory and optional filename |
---|
145 | :rtype: string |
---|
146 | |
---|
147 | """ |
---|
148 | if windows_check(): |
---|
149 | |
---|
150 | appDataPath = os.environ.get("APPDATA") |
---|
151 | if not appDataPath: |
---|
152 | import _winreg |
---|
153 | hkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") |
---|
154 | appDataReg = _winreg.QueryValueEx(hkey, "AppData") |
---|
155 | appDataPath = appDataReg[0] |
---|
156 | _winreg.CloseKey(hkey) |
---|
157 | |
---|
158 | if filename: |
---|
159 | return os.path.join(appDataPath, "deluge", filename) |
---|
160 | else: |
---|
161 | return os.path.join(appDataPath, "deluge") |
---|
162 | else: |
---|
163 | import xdg.BaseDirectory |
---|
164 | if filename: |
---|
165 | return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename) |
---|
166 | else: |
---|
167 | return xdg.BaseDirectory.save_config_path("deluge") |
---|
168 | |
---|
169 | def get_default_download_dir(): |
---|
170 | """ |
---|
171 | :returns: the default download directory |
---|
172 | :rtype: string |
---|
173 | |
---|
174 | """ |
---|
175 | if windows_check(): |
---|
176 | return os.path.expanduser("~") |
---|
177 | else: |
---|
178 | return os.environ.get("HOME") |
---|
179 | |
---|
180 | def windows_check(): |
---|
181 | """ |
---|
182 | Checks if the current platform is Windows |
---|
183 | |
---|
184 | :returns: True or False |
---|
185 | :rtype: bool |
---|
186 | |
---|
187 | """ |
---|
188 | return platform.system() in ('Windows', 'Microsoft') |
---|
189 | |
---|
190 | def vista_check(): |
---|
191 | """ |
---|
192 | Checks if the current platform is Windows Vista |
---|
193 | |
---|
194 | :returns: True or False |
---|
195 | :rtype: bool |
---|
196 | |
---|
197 | """ |
---|
198 | return platform.release() == "Vista" |
---|
199 | |
---|
200 | def osx_check(): |
---|
201 | """ |
---|
202 | Checks if the current platform is Mac OS X |
---|
203 | |
---|
204 | :returns: True or False |
---|
205 | :rtype: bool |
---|
206 | |
---|
207 | """ |
---|
208 | return platform.system() == "Darwin" |
---|
209 | |
---|
210 | def get_pixmap(fname): |
---|
211 | """ |
---|
212 | Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg |
---|
213 | |
---|
214 | :param fname: the filename to look for |
---|
215 | :type fname: string |
---|
216 | :returns: a path to a pixmap file included with Deluge |
---|
217 | :rtype: string |
---|
218 | |
---|
219 | """ |
---|
220 | return pkg_resources.resource_filename("deluge", os.path.join("data", \ |
---|
221 | "pixmaps", fname)) |
---|
222 | |
---|
223 | def open_file(path): |
---|
224 | """ |
---|
225 | Opens a file or folder using the system configured program |
---|
226 | |
---|
227 | :param path: the path to the file or folder to open |
---|
228 | :type path: string |
---|
229 | |
---|
230 | """ |
---|
231 | if windows_check(): |
---|
232 | os.startfile("%s" % path) |
---|
233 | else: |
---|
234 | subprocess.Popen(["xdg-open", "%s" % path]) |
---|
235 | |
---|
236 | def open_url_in_browser(url): |
---|
237 | """ |
---|
238 | Opens a url in the desktop's default browser |
---|
239 | |
---|
240 | :param url: the url to open |
---|
241 | :type url: string |
---|
242 | |
---|
243 | """ |
---|
244 | import webbrowser |
---|
245 | webbrowser.open(url) |
---|
246 | |
---|
247 | ## Formatting text functions |
---|
248 | |
---|
249 | def fsize(fsize_b): |
---|
250 | """ |
---|
251 | Formats the bytes value into a string with KiB, MiB or GiB units |
---|
252 | |
---|
253 | :param fsize_b: the filesize in bytes |
---|
254 | :type fsize_b: int |
---|
255 | :returns: formatted string in KiB, MiB or GiB units |
---|
256 | :rtype: string |
---|
257 | |
---|
258 | **Usage** |
---|
259 | |
---|
260 | >>> fsize(112245) |
---|
261 | '109.6 KiB' |
---|
262 | |
---|
263 | """ |
---|
264 | fsize_kb = fsize_b / 1024.0 |
---|
265 | if fsize_kb < 1024: |
---|
266 | return "%.1f %s" % (fsize_kb, _("KiB")) |
---|
267 | fsize_mb = fsize_kb / 1024.0 |
---|
268 | if fsize_mb < 1024: |
---|
269 | return "%.1f %s" % (fsize_mb, _("MiB")) |
---|
270 | fsize_gb = fsize_mb / 1024.0 |
---|
271 | return "%.1f %s" % (fsize_gb, _("GiB")) |
---|
272 | |
---|
273 | def fpcnt(dec): |
---|
274 | """ |
---|
275 | Formats a string to display a percentage with two decimal places |
---|
276 | |
---|
277 | :param dec: the ratio in the range [0.0, 1.0] |
---|
278 | :type dec: float |
---|
279 | :returns: a formatted string representing a percentage |
---|
280 | :rtype: string |
---|
281 | |
---|
282 | **Usage** |
---|
283 | |
---|
284 | >>> fpcnt(0.9311) |
---|
285 | '93.11%' |
---|
286 | |
---|
287 | """ |
---|
288 | return '%.2f%%' % (dec * 100) |
---|
289 | |
---|
290 | def fspeed(bps): |
---|
291 | """ |
---|
292 | Formats a string to display a transfer speed utilizing :func:`fsize` |
---|
293 | |
---|
294 | :param bps: bytes per second |
---|
295 | :type bps: int |
---|
296 | :returns: a formatted string representing transfer speed |
---|
297 | :rtype: string |
---|
298 | |
---|
299 | **Usage** |
---|
300 | |
---|
301 | >>> fspeed(43134) |
---|
302 | '42.1 KiB/s' |
---|
303 | |
---|
304 | """ |
---|
305 | return '%s/s' % (fsize(bps)) |
---|
306 | |
---|
307 | def fpeer(num_peers, total_peers): |
---|
308 | """ |
---|
309 | Formats a string to show 'num_peers' ('total_peers') |
---|
310 | |
---|
311 | :param num_peers: the number of connected peers |
---|
312 | :type num_peers: int |
---|
313 | :param total_peers: the total number of peers |
---|
314 | :type total_peers: int |
---|
315 | :returns: a formatted string: num_peers (total_peers), if total_peers < 0, then it will not be shown |
---|
316 | :rtype: string |
---|
317 | |
---|
318 | **Usage** |
---|
319 | |
---|
320 | >>> fpeer(10, 20) |
---|
321 | '10 (20)' |
---|
322 | >>> fpeer(10, -1) |
---|
323 | '10' |
---|
324 | |
---|
325 | """ |
---|
326 | if total_peers > -1: |
---|
327 | return "%d (%d)" % (num_peers, total_peers) |
---|
328 | else: |
---|
329 | return "%d" % num_peers |
---|
330 | |
---|
331 | def ftime(seconds): |
---|
332 | """ |
---|
333 | Formats a string to show time in a human readable form |
---|
334 | |
---|
335 | :param seconds: the number of seconds |
---|
336 | :type seconds: int |
---|
337 | :returns: a formatted time string, will return '' if seconds == 0 |
---|
338 | :rtype: string |
---|
339 | |
---|
340 | **Usage** |
---|
341 | |
---|
342 | >>> ftime(23011) |
---|
343 | '6h 23m' |
---|
344 | |
---|
345 | """ |
---|
346 | if seconds == 0: |
---|
347 | return "" |
---|
348 | if seconds < 60: |
---|
349 | return '%ds' % (seconds) |
---|
350 | minutes = seconds / 60 |
---|
351 | if minutes < 60: |
---|
352 | seconds = seconds % 60 |
---|
353 | return '%dm %ds' % (minutes, seconds) |
---|
354 | hours = minutes / 60 |
---|
355 | if hours < 24: |
---|
356 | minutes = minutes % 60 |
---|
357 | return '%dh %dm' % (hours, minutes) |
---|
358 | days = hours / 24 |
---|
359 | if days < 7: |
---|
360 | hours = hours % 24 |
---|
361 | return '%dd %dh' % (days, hours) |
---|
362 | weeks = days / 7 |
---|
363 | if weeks < 52: |
---|
364 | days = days % 7 |
---|
365 | return '%dw %dd' % (weeks, days) |
---|
366 | years = weeks / 52 |
---|
367 | weeks = weeks % 52 |
---|
368 | return '%dy %dw' % (years, weeks) |
---|
369 | |
---|
370 | def fdate(seconds): |
---|
371 | """ |
---|
372 | Formats a date time string in the locale's date representation based on the systems timezone |
---|
373 | |
---|
374 | :param seconds: time in seconds since the Epoch |
---|
375 | :type seconds: float |
---|
376 | :returns: a string in the locale's datetime representation or "" if seconds < 0 |
---|
377 | :rtype: string |
---|
378 | |
---|
379 | """ |
---|
380 | if seconds < 0: |
---|
381 | return "" |
---|
382 | return time.strftime("%x %X", time.localtime(seconds)) |
---|
383 | |
---|
384 | def is_url(url): |
---|
385 | """ |
---|
386 | A simple test to check if the URL is valid |
---|
387 | |
---|
388 | :param url: the url to test |
---|
389 | :type url: string |
---|
390 | :returns: True or False |
---|
391 | :rtype: bool |
---|
392 | |
---|
393 | **Usage** |
---|
394 | |
---|
395 | >>> is_url("http://deluge-torrent.org") |
---|
396 | True |
---|
397 | |
---|
398 | """ |
---|
399 | return url.partition('://')[0] in ("http", "https", "ftp", "udp") |
---|
400 | |
---|
401 | def is_magnet(uri): |
---|
402 | """ |
---|
403 | A check to determine if a uri is a valid bittorrent magnet uri |
---|
404 | |
---|
405 | :param uri: the uri to check |
---|
406 | :type uri: string |
---|
407 | :returns: True or False |
---|
408 | :rtype: bool |
---|
409 | |
---|
410 | **Usage** |
---|
411 | |
---|
412 | >>> is_magnet("magnet:?xt=urn:btih:SU5225URMTUEQLDXQWRB2EQWN6KLTYKN") |
---|
413 | True |
---|
414 | |
---|
415 | """ |
---|
416 | if uri[:20] == "magnet:?xt=urn:btih:": |
---|
417 | return True |
---|
418 | return False |
---|
419 | |
---|
420 | def create_magnet_uri(infohash, name=None, trackers=[]): |
---|
421 | """ |
---|
422 | Creates a magnet uri |
---|
423 | |
---|
424 | :param infohash: the info-hash of the torrent |
---|
425 | :type infohash: string |
---|
426 | :param name: the name of the torrent (optional) |
---|
427 | :type name: string |
---|
428 | :param trackers: the trackers to announce to (optional) |
---|
429 | :type trackers: list of strings |
---|
430 | |
---|
431 | :returns: a magnet uri string |
---|
432 | :rtype: string |
---|
433 | |
---|
434 | """ |
---|
435 | from base64 import b32encode |
---|
436 | uri = "magnet:?xt=urn:btih:" + b32encode(infohash.decode("hex")) |
---|
437 | if name: |
---|
438 | uri = uri + "&dn=" + name |
---|
439 | if trackers: |
---|
440 | for t in trackers: |
---|
441 | uri = uri + "&tr=" + t |
---|
442 | |
---|
443 | return uri |
---|
444 | |
---|
445 | def get_path_size(path): |
---|
446 | """ |
---|
447 | Gets the size in bytes of 'path' |
---|
448 | |
---|
449 | :param path: the path to check for size |
---|
450 | :type path: string |
---|
451 | :returns: the size in bytes of the path or -1 if the path does not exist |
---|
452 | :rtype: int |
---|
453 | |
---|
454 | """ |
---|
455 | if not os.path.exists(path): |
---|
456 | return -1 |
---|
457 | |
---|
458 | if os.path.isfile(path): |
---|
459 | return os.path.getsize(path) |
---|
460 | |
---|
461 | dir_size = 0 |
---|
462 | for (p, dirs, files) in os.walk(path): |
---|
463 | for file in files: |
---|
464 | filename = os.path.join(p, file) |
---|
465 | dir_size += os.path.getsize(filename) |
---|
466 | return dir_size |
---|
467 | |
---|
468 | def free_space(path): |
---|
469 | """ |
---|
470 | Gets the free space available at 'path' |
---|
471 | |
---|
472 | :param path: the path to check |
---|
473 | :type path: string |
---|
474 | :returns: the free space at path in bytes |
---|
475 | :rtype: int |
---|
476 | |
---|
477 | :raises InvalidPathError: if the path is not valid |
---|
478 | |
---|
479 | """ |
---|
480 | if not os.path.exists(path): |
---|
481 | raise InvalidPathError("%s is not a valid path" % path) |
---|
482 | |
---|
483 | if windows_check(): |
---|
484 | import win32file |
---|
485 | sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path)) |
---|
486 | return (free * sectors * bytes) |
---|
487 | else: |
---|
488 | disk_data = os.statvfs(path.encode("utf8")) |
---|
489 | block_size = disk_data.f_bsize |
---|
490 | return disk_data.f_bavail * block_size |
---|
491 | |
---|
492 | def is_ip(ip): |
---|
493 | """ |
---|
494 | A simple test to see if 'ip' is valid |
---|
495 | |
---|
496 | :param ip: the ip to check |
---|
497 | :type ip: string |
---|
498 | :returns: True or False |
---|
499 | :rtype: bool |
---|
500 | |
---|
501 | ** Usage ** |
---|
502 | |
---|
503 | >>> is_ip("127.0.0.1") |
---|
504 | True |
---|
505 | |
---|
506 | """ |
---|
507 | import socket |
---|
508 | #first we test ipv4 |
---|
509 | try: |
---|
510 | if socket.inet_pton(socket.AF_INET, "%s" % (ip)): |
---|
511 | return True |
---|
512 | except socket.error: |
---|
513 | if not socket.has_ipv6: |
---|
514 | return False |
---|
515 | #now test ipv6 |
---|
516 | try: |
---|
517 | if socket.inet_pton(socket.AF_INET6, "%s" % (ip)): |
---|
518 | return True |
---|
519 | except socket.error: |
---|
520 | return False |
---|
521 | |
---|
522 | def path_join(*parts): |
---|
523 | """ |
---|
524 | An implementation of os.path.join that always uses / for the separator |
---|
525 | to ensure that the correct paths are produced when working with internal |
---|
526 | paths on Windows. |
---|
527 | """ |
---|
528 | path = '' |
---|
529 | for part in parts: |
---|
530 | if not part: |
---|
531 | continue |
---|
532 | elif part[0] == '/': |
---|
533 | path = part |
---|
534 | elif not path: |
---|
535 | path = part |
---|
536 | else: |
---|
537 | path += '/' + part |
---|
538 | return path |
---|
539 | |
---|
540 | XML_ESCAPES = ( |
---|
541 | ('&', '&'), |
---|
542 | ('<', '<'), |
---|
543 | ('>', '>'), |
---|
544 | ('"', '"'), |
---|
545 | ("'", ''') |
---|
546 | ) |
---|
547 | |
---|
548 | def xml_decode(string): |
---|
549 | """ |
---|
550 | Unescape a string that was previously encoded for use within xml. |
---|
551 | |
---|
552 | :param string: The string to escape |
---|
553 | :type string: string |
---|
554 | :returns: The unescaped version of the string. |
---|
555 | :rtype: string |
---|
556 | """ |
---|
557 | for char, escape in XML_ESCAPES: |
---|
558 | string = string.replace(escape, char) |
---|
559 | return string |
---|
560 | |
---|
561 | def xml_encode(string): |
---|
562 | """ |
---|
563 | Escape a string for use within an xml element or attribute. |
---|
564 | |
---|
565 | :param string: The string to escape |
---|
566 | :type string: string |
---|
567 | :returns: An escaped version of the string. |
---|
568 | :rtype: string |
---|
569 | """ |
---|
570 | for char, escape in XML_ESCAPES: |
---|
571 | string = string.replace(char, escape) |
---|
572 | return string |
---|
573 | |
---|
574 | def decode_string(s, encoding="utf8"): |
---|
575 | """ |
---|
576 | Decodes a string and re-encodes it in utf8. If it cannot decode using |
---|
577 | `:param:encoding` then it will try to detect the string encoding and |
---|
578 | decode it. |
---|
579 | |
---|
580 | :param s: string to decode |
---|
581 | :type s: string |
---|
582 | :keyword encoding: the encoding to use in the decoding |
---|
583 | :type encoding: string |
---|
584 | |
---|
585 | """ |
---|
586 | |
---|
587 | try: |
---|
588 | s = s.decode(encoding).encode("utf8", "ignore") |
---|
589 | except UnicodeDecodeError: |
---|
590 | s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore") |
---|
591 | return s |
---|
592 | |
---|
593 | def utf8_encoded(s): |
---|
594 | """ |
---|
595 | Returns a utf8 encoded string of s |
---|
596 | |
---|
597 | :param s: (unicode) string to (re-)encode |
---|
598 | :type s: basestring |
---|
599 | :returns: a utf8 encoded string of s |
---|
600 | :rtype: str |
---|
601 | |
---|
602 | """ |
---|
603 | if isinstance(s, str): |
---|
604 | s = decode_string(s, locale.getpreferredencoding()) |
---|
605 | elif isinstance(s, unicode): |
---|
606 | s = s.encode("utf8", "ignore") |
---|
607 | return s |
---|
608 | |
---|
609 | class VersionSplit(object): |
---|
610 | """ |
---|
611 | Used for comparing version numbers. |
---|
612 | |
---|
613 | :param ver: the version |
---|
614 | :type ver: string |
---|
615 | |
---|
616 | """ |
---|
617 | def __init__(self, ver): |
---|
618 | ver = ver.lower() |
---|
619 | vs = ver.replace("_", "-").split("-") |
---|
620 | self.version = [int(x) for x in vs[0].split(".")] |
---|
621 | self.suffix = None |
---|
622 | self.dev = False |
---|
623 | if len(vs) > 1: |
---|
624 | if vs[1].startswith(("rc", "alpha", "beta")): |
---|
625 | self.suffix = vs[1] |
---|
626 | if vs[-1] == 'dev': |
---|
627 | self.dev = True |
---|
628 | |
---|
629 | def __cmp__(self, ver): |
---|
630 | """ |
---|
631 | The comparison method. |
---|
632 | |
---|
633 | :param ver: the version to compare with |
---|
634 | :type ver: VersionSplit |
---|
635 | |
---|
636 | """ |
---|
637 | |
---|
638 | # If there is no suffix we use z because we want final |
---|
639 | # to appear after alpha, beta, and rc alphabetically. |
---|
640 | v1 = [self.version, self.suffix or 'z', self.dev] |
---|
641 | v2 = [ver.version, ver.suffix or 'z', ver.dev] |
---|
642 | return cmp(v1, v2) |
---|