1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Utilities
19 =========
20
21 This module contains some utility functions and classes used in several
22 places of the svnmailer. These functions have a quite general character
23 and can be used easily outside of the svnmailer as well.
24 """
25 __author__ = "André Malo"
26 __docformat__ = "epytext en"
27 __all__ = [
28 'TempFile',
29 'getPipe4',
30 'getSuitableCommandLine',
31 'splitCommand',
32 'filename',
33 'extractX509User',
34 'substitute',
35 'filterForXml',
36 'getParentDirList',
37 'getGlobValue',
38 'parseQuery',
39 'modifyQuery',
40 'inherit',
41 'commonPaths',
42 'ReadOnlyDict',
43 'SafeDict',
44 ]
45
46
47 import locale, os, sys
48
49
51 """ Tempfile container class
52
53 The class contains a destructor that removes the created
54 file. This differs from the stuff in tempfile, which removes
55 the file, when it's closed.
56
57 The mode is fixed to C{w+}; a C{b} is added if the C{text}
58 argument is false (see C{__init__})
59
60 @cvar name: C{None}
61 @ivar name: The full name of the file
62 @type name: C{str}
63
64 @cvar fp: C{None}
65 @ivar fp: The file descriptor
66 @type fp: file like object
67
68 @cvar _unlink: C{None}
69 @ivar _unlink: C{os.unlink}
70 @type _unlink: callable
71 """
72 name = None
73 fp = None
74 _unlink = None
75
76 - def __init__(self, tempdir = None, text = False):
77 """ Initialization
78
79 @param tempdir: The temporary directory
80 @type tempdir: C{str}
81
82 @param text: want to write text?
83 @type text: C{bool}
84 """
85 import tempfile
86
87 self._unlink = os.unlink
88
89 fd, self.name = tempfile.mkstemp(dir = tempdir, text = text)
90 self.fp = os.fdopen(fd, "w+%s" % ["b", ""][bool(text)])
91
92
94 """ Unlink the file name """
95 if self.fp:
96 try:
97 self.fp.close()
98 except ValueError:
99
100 pass
101
102 if self.name and self._unlink:
103 try:
104 self._unlink(self.name)
105 except OSError:
106
107 pass
108
109
111 """ Close the file (but don't delete it)
112
113 @exception ValueError: The file was already closed
114 """
115 if self.fp:
116 self.fp.close()
117
118
120 """ Returns a pipe object (C{Popen3} or C{_DummyPopen3} on win32)
121
122 @param command: The command list (the first item is the command
123 itself, the rest represents the arguments)
124 @type command: C{list}
125
126 @return: The pipe object
127 @rtype: C{popen2.Popen3} or C{_DummyPopen3}
128 """
129 import popen2
130
131 try:
132 cls = popen2.Popen3
133 except AttributeError:
134 cls = _DummyPopen3
135
136 return cls(getSuitableCommandLine(command))
137
138
140 """ Returns a pipe object (C{Popen4} or C{_DummyPopen4} on win32)
141
142 @param command: The command list (the first item is the command
143 itself, the rest represents the arguments)
144 @type command: C{list}
145
146 @return: The pipe object
147 @rtype: C{popen2.Popen4} or C{_DummyPopen4}
148 """
149 import popen2
150
151 try:
152 cls = popen2.Popen4
153 except AttributeError:
154 cls = _DummyPopen4
155
156 return cls(getSuitableCommandLine(command))
157
158
160 """ Dummy Popen4 class for platforms which don't provide one in popen2 """
161
163 """ Initialization """
164 bufsize = -1
165 self.tochild, self.fromchild = os.popen4(cmd, 'b', bufsize)
166
167
169 """ Dummy wait """
170 return 0
171
172
174 """ Dummy Popen3 class for platforms which don't provide one in popen2 """
175
176 - def __init__(self, cmd, capturestderr = False, bufsize = -1):
177 """ Initialization """
178 bufsize = -1
179 capturestderr = False
180 self.tochild, self.fromchild = os.popen2(cmd, 'b', bufsize)
181 self.childerr = None
182
183
185 """ Dummy wait """
186 return 0
187
188
190 """ Return the revised command suitable for being exec'd
191
192 Currently this means, it's escaped and converted to a string
193 only for Win32, because on this system the shell is called.
194 For other systems the list is just returned.
195
196 @note: This is more or less the same as the stuff in
197 svn.fs._escape_msvcrt_shell_command/arg. But it
198 belongs somewhere else - e.g. into a util module...
199
200 Perhaps once a day the whole package goes directly
201 into the subversion distribution and then it's all
202 cool.
203
204 @param command: The command to escape
205 @type command: C{list}
206
207 @param _platform: A platform string (for testing purposes only)
208 @type _platform: C{str}
209
210 @return: The escaped command string or the original list
211 @rtype: C{str} or C{list}
212 """
213 platform = _platform or sys.platform
214 if platform != "win32":
215 return command
216
217 try:
218 slashre = getSuitableCommandLine._slashre
219 except AttributeError:
220 import re
221 slashre = getSuitableCommandLine._slashre = re.compile(r'(\\+)("|$)')
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245 return '"%s"' % " ".join([
246 '"%s"' % slashre.sub(r'\1\1\2', arg).replace('"', '"^""')
247 for arg in command
248 ])
249
250
252 r"""Split a command string with respect to quotes and such
253
254 The command string consists of several tokens:
255 - whitespace: Those are separators except inside quoted items
256 - unquoted items: every token that doesn't start with
257 a double quote (")
258 - quoted items: every token that starts with a double quote (").
259 Those items must be closed with a double quote and may contain
260 whitespaces. The enclosing quotes are stripped. To put a double
261 quote character inside such a token, it has to be escaped with
262 a backslash (\). Therefore - backslashes themselves have to be
263 escaped as well. The escapes are also stripped from the result.
264
265 Here's an example: C{r'foo bar "baz" "zo\"" "\\nk"'} resolves
266 to C{['foo', 'bar', 'baz', 'zo"', r'\nk']}
267
268 @param command: The command string
269 @type command: C{str}
270
271 @return: The splitted command
272 @rtype: C{list}
273
274 @exception ValueError: The command string is not valid
275 (unclosed quote or the like)
276 """
277 try:
278 argre, checkre, subre = splitCommand._regexps
279 except AttributeError:
280 import re
281 argre = r'[^"\s]\S*|"[^\\"]*(?:\\[\\"][^\\"]*)*"'
282 checkre = r'\s*(?:%(arg)s)(?:\s+(?:%(arg)s))*\s*$' % {'arg': argre}
283 subre = r'\\([\\"])'
284
285 argre, checkre, subre = splitCommand._regexps = (
286 re.compile(argre), re.compile(checkre), re.compile(subre)
287 )
288
289 if not checkre.match(command or ''):
290 raise ValueError("Command string %r is not valid" % command)
291
292 return [
293 (arg.startswith('"') and [subre.sub(r'\1', arg[1:-1])] or [arg])[0]
294 for arg in argre.findall(command or '')
295 ]
296
297
299 """ Transform filenames according to locale """
300 - def __init__(self, _locale = locale, _os = os, _sys = sys):
301 """ Initialization """
302 self.unicode_system = _os.path.supports_unicode_filenames
303 self.from_enc = _locale.getpreferredencoding(False) or "us-ascii"
304 self.to_enc = _sys.getfilesystemencoding() or "us-ascii"
305
306
307 - def toLocale(self, name, name_enc = None, locale_enc = None):
308 """ Transforms a file name to the locale representation
309
310 @param name: The name to consider
311 @type name: C{str} / C{unicode}
312
313 @param name_enc: The source encoding of C{name}, if it's
314 not unicode already
315 @type name_enc: C{str}
316
317 @param locale_enc: The file system encoding (used only
318 if it's not a unicode supporting OS)
319 @type locale_enc: C{str}
320
321 @return: The name in locale representation
322 @rtype: C{str}/C{unicode}
323
324 @exception UnicodeError: An error happened while recoding
325 """
326 if locale_enc is None:
327 locale_enc = self.to_enc
328 if name_enc is None:
329 name_enc = self.from_enc
330
331 if self.unicode_system:
332 if isinstance(name, unicode):
333 return name
334 else:
335 return name.decode(name_enc, "strict")
336
337 if locale_enc.lower() == "none":
338 if isinstance(name, unicode):
339 raise RuntimeError("Illegal call")
340 else:
341 return name
342
343 if not isinstance(name, unicode):
344 name = name.decode(name_enc, "strict")
345
346 return name.encode(locale_enc, "strict")
347
348
350 """ Transform a file name from locale repr to unicode (hopefully)
351
352 @param name: The name to decode
353 @type name: C{str}/C{unicode}
354
355 @param locale_enc: The locale encoding
356 @type locale_enc: C{str}
357
358 @return: The decoded name
359 @rtype: C{unicode}/C{str}
360
361 @exception UnicodeError: An error happend while recoding
362 """
363 if isinstance(name, unicode):
364 return name
365
366 if locale_enc is None:
367 locale_enc = self.from_enc
368
369 if locale_enc.lower() == "none":
370 return name
371
372 return name.decode(locale_enc, "strict")
373
374 filename = _LocaleFile()
375
376
378 """ Returns user data extracted from x509 subject string
379
380 @param author: The author string
381 @type author: C{str}
382
383 @return: user name, mail address (user name maybe C{None})
384 @rtype: C{tuple} or C{None}
385 """
386 if author:
387 try:
388 cnre, eare = extractX509User._regexps
389 except AttributeError:
390 import re
391 cnre = re.compile(ur'/CN=([^/]+)', re.I)
392 eare = re.compile(ur'/emailAddress=([^/]+)', re.I)
393 extractX509User._regexps = (cnre, eare)
394
395 author = author.decode('utf-8', 'replace')
396 ea_match = eare.search(author)
397 if ea_match:
398 cn_match = cnre.search(author)
399 return (cn_match and cn_match.group(1), ea_match.group(1))
400
401 return None
402
403
405 """ Returns a filled template
406
407 If the L{template} is C{None}, this function returns C{None}
408 as well.
409
410 @param template: The temlate to fill
411 @type template: C{unicode}
412
413 @param subst: The substitution parameters
414 @type subst: C{dict}
415
416 @return: The filled template (The return type depends on the
417 template and the parameters)
418 @rtype: C{str} or C{unicode}
419 """
420 if template is None:
421 return None
422
423 return template % SafeDict(subst.items())
424
425
427 """ Replaces control characters with replace characters
428
429 @param value: The value to filter
430 @type value: C{unicode}
431
432 @return: The filtered value
433 @rtype: C{unicode}
434 """
435 try:
436 regex = filterForXml._regex
437 except AttributeError:
438 import re
439 chars = u''.join([chr(num) for num in range(32)
440 if num not in (9, 10, 13)
441 ])
442 regex = filterForXml._regex = re.compile("[%s]" % chars)
443
444 return regex.sub(u'\ufffd', value)
445
446
448 """ Returns the directories up to a (posix) path
449
450 @param path: The path to process
451 @type path: C{str}
452
453 @return: The directory list
454 @rtype: C{list}
455 """
456 import posixpath
457
458 path = posixpath.normpath("/%s" % path)
459 if path[:2] == '//':
460 path = path[1:]
461
462 dirs = []
463 path = posixpath.dirname(path)
464 while path != '/':
465 dirs.append(path)
466 path = posixpath.dirname(path)
467 dirs.append('/')
468
469 return dirs
470
471
473 """ Returns the value of the glob, where path matches
474
475 @param globs: The glob list (C{[(glob, associated value)]})
476 @type globs: C{list} of C{tuple}
477
478 @param path: The path to match
479 @type path: C{str}
480
481 @return: The matched value or C{None}
482 @rtype: any
483 """
484 import fnmatch
485
486 result = None
487 for glob in globs:
488 if fnmatch.fnmatchcase(path, glob[0]):
489 result = glob[1]
490 break
491
492 return result
493
494
495 -def modifyQuery(query, rem = None, add = None, set = None, delim = '&'):
496 """ Returns a modified query string
497
498 @note: set is a convenience parameter, it's actually a combination of
499 C{rem} and C{add}. The order of processing is:
500 1. append the set parameters to C{rem} and C{add}
501 2. apply C{rem}
502 3. apply C{add}
503
504 @warning: query parameters containing no C{=} character are silently
505 dropped.
506
507 @param query: The query string to modify
508 @type query: C{str} or C{dict}
509
510 @param rem: parameters to remove (if present)
511 @type rem: C{list} of C{str}
512
513 @param add: parameters to add
514 @type add: C{list} of C{tuple}
515
516 @param set: parameters to override
517 @type set: C{list} of C{tuple}
518
519 @param delim: Delimiter to use when rebuilding the query string
520 @type delim: C{str}
521 """
522 rem = list(rem or [])
523 add = list(add or [])
524 set = list(set or [])
525
526
527 query_dict = (isinstance(query, dict) and
528 [query.copy()] or [parseQuery(query)]
529 )[0]
530
531
532 rem.extend([tup[0] for tup in set])
533 add.extend(set)
534
535
536 for key in rem:
537 try:
538 del query_dict[key]
539 except KeyError:
540
541 pass
542
543
544 for key, val in add:
545 query_dict.setdefault(key, []).append(val)
546
547
548 return delim.join([
549 delim.join(["%s=%s" % (key, str(val)) for val in vals])
550 for key, vals in query_dict.items()
551 ])
552
553
555 """ Parses a query string
556
557 @warning: query parameters containing no C{=} character are silently
558 dropped.
559
560 @param query: The query string to parse
561 @type query: C{str}
562
563 @return: The parsed query (C{{key: [values]}})
564 @rtype: C{dict}
565 """
566 try:
567 queryre = parseQuery._regex
568 except AttributeError:
569 import re
570 parseQuery._regex = queryre = re.compile(r'[&;]')
571
572 query_dict = {}
573 for key, val in [pair.split('=', 1)
574 for pair in queryre.split(query) if '=' in pair]:
575 query_dict.setdefault(key, []).append(val)
576
577 return query_dict
578
579
581 """ Returns the common component and the stripped paths
582
583 It expects that directories do always end with a trailing slash and
584 paths never begin with a slash (except root).
585
586 @param paths: The list of paths (C{[str, str, ...]})
587 @type paths: C{list}
588
589 @return: The common component (always a directory) and the stripped
590 paths (C{(str, [str, str, ...])})
591 @rtype: C{tuple}
592 """
593 import posixpath
594
595 common = ''
596 if len(paths) > 1 and "/" not in paths:
597 common = posixpath.commonprefix(paths)
598 if common[-1:] != "/":
599 common = common[:common.rfind("/") + 1]
600
601 idx = len(common)
602 if idx > 0:
603 paths = [path[idx:] or "./" for path in paths]
604 common = common[:-1]
605
606 return (common, paths)
607
608
610 """ Inherits class cls from *bases
611
612 @note: cls needs a __dict__, so __slots__ is tabu
613
614 @param cls: The class to inherit from *bases
615 @type cls: C{class}
616
617 @param bases: The base class(es)
618 @type bases: C{list}
619 """
620 newdict = dict([(key, value)
621 for key, value in cls.__dict__.items()
622 if key != '__module__'
623 ])
624 cls = type(cls.__name__, tuple(bases), newdict)
625 setattr(cls, "_%s__decorator_class" % cls.__name__, cls)
626
627 return cls
628
629
631 """ Parses a content type
632
633 (the email module unfortunately doesn't provide a public
634 interface for this)
635
636 @warning: comments are not recognized yet
637
638 @param value: The value to parse - must be ascii compatible
639 @type value: C{basestring}
640
641 @return: The parsed header (C{(value, {key, [value, value, ...]})})
642 or C{None}
643 @rtype: C{tuple}
644 """
645 try:
646 if isinstance(value, unicode):
647 value.encode('us-ascii')
648 else:
649 value.decode('us-ascii')
650 except (AttributeError, UnicodeError):
651 return None
652
653 try:
654 typere, pairre, stripre = parseContentType._regexps
655 except AttributeError:
656 import re
657
658 tokenres = r'[^\000-\040()<>@,;:\\"/[\]?=]+'
659 qcontent = r'[^\000\\"]'
660 qsres = r'"%(qc)s*(?:\\"%(qc)s*)*"' % {'qc': qcontent}
661 valueres = r'(?:%(token)s|%(quoted-string)s)' % {
662 'token': tokenres, 'quoted-string': qsres,
663 }
664
665 typere = re.compile(
666 r'\s*([^;/\s]+/[^;/\s]+)((?:\s*;\s*%(key)s\s*=\s*%(val)s)*)\s*$' %
667 {'key': tokenres, 'val': valueres,}
668 )
669 pairre = re.compile(r'\s*;\s*(%(key)s)\s*=\s*(%(val)s)' % {
670 'key': tokenres, 'val': valueres
671 })
672 stripre = re.compile(r'\r?\n')
673 parseContentType._regexps = (typere, pairre, stripre)
674
675 match = typere.match(value)
676 if not match:
677 return None
678
679 parsed = (match.group(1).lower(), {})
680 match = match.group(2)
681 if match:
682 for key, val in pairre.findall(match):
683 if val[:1] == '"':
684 val = stripre.sub(r'', val[1:-1]).replace(r'\"', '"')
685 parsed[1].setdefault(key.lower(), []).append(val)
686
687 return parsed
688
689
691 """ Read only dictionary """
692 __msg = "The dictionary is read-only"
693
695 """ modifiying is not allowed """
696 raise TypeError(self.__msg)
697
698
700 """ deleting is not allowed """
701 raise TypeError(self.__msg)
702
703
705 """ clearing is not allowed """
706 raise TypeError(self.__msg)
707
708
710 """ Chokes by default, so work around it """
711 return cls(dict.fromkeys(seq, value))
712 fromkeys = classmethod(fromkeys)
713
714
715 - def pop(self, key, default = None):
716 """ popping is not allowed """
717 raise TypeError(self.__msg)
718
719
721 """ popping is not allowed """
722 raise TypeError(self.__msg)
723
724
726 """ modifying is not allowed """
727 raise TypeError(self.__msg)
728
729
731 """ updating is not allowed """
732 raise TypeError(self.__msg)
733
734
736 """ A dict, which returns '' on unknown keys or false values """
737
739 """ Returns an empty string on false values or unknown keys """
740 return dict.get(self, key) or ''
741