Improve unicode handling and increase the default textwrap width.
[update-copyright.git] / update_copyright / vcs / utils.py
1 # Copyright (C) 2012 W. Trevor King
2 #
3 # This file is part of update-copyright.
4 #
5 # update-copyright is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # update-copyright 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 GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with update-copyright.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Useful utilities for backend classes."""
20
21 import email.utils as _email_utils
22 import os.path as _os_path
23 import subprocess as _subprocess
24 import sys as _sys
25
26 from ..utils import ENCODING as _ENCODING
27
28
29 _MSWINDOWS = _sys.platform == 'win32'
30 _POSIX = not _MSWINDOWS
31
32
33 def invoke(args, stdin=None, stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
34            expect=(0,), unicode_output=False, encoding=None):
35     """Invoke an external program and return the results
36
37     ``expect`` should be a tuple of allowed exit codes.
38
39     When ``unicode_output`` is ``True``, convert stdout and stdin
40     strings to unicode before returing them.
41     """
42     try :
43         if _POSIX:
44             q = _subprocess.Popen(args, stdin=_subprocess.PIPE,
45                                   stdout=stdout, stderr=stderr)
46         else:
47             assert _MSWINDOWS == True, 'invalid platform'
48             # win32 don't have os.execvp() so run the command in a shell
49             q = _subprocess.Popen(args, stdin=_subprocess.PIPE,
50                                   stdout=stdout, stderr=stderr, shell=True)
51     except OSError, e:
52         raise ValueError([args, e])
53     stdout,stderr = q.communicate(input=stdin)
54     status = q.wait()
55     if unicode_output == True:
56         if encoding is None:
57             encoding = _ENCODING
58         if stdout is not None:
59             stdout = unicode(stdout, encoding)
60         if stderr is not None:
61             stderr = unicode(stderr, encoding)
62     if status not in expect:
63         raise ValueError([args, status, stdout, stderr])
64     return status, stdout, stderr
65
66 def splitpath(path):
67     """Recursively split a path into elements.
68
69     Examples
70     --------
71
72     >>> import os.path
73     >>> splitpath(os.path.join('a', 'b', 'c'))
74     ('a', 'b', 'c')
75     >>> splitpath(os.path.join('.', 'a', 'b', 'c'))
76     ('a', 'b', 'c')
77     """
78     path = _os_path.normpath(path)
79     elements = []
80     while True:
81         dirname,basename = _os_path.split(path)
82         elements.insert(0,basename)
83         if dirname in ['', '.']:
84             break
85         path = dirname
86     return tuple(elements)
87
88 def strip_email(*args):
89     """Remove email addresses from a series of names.
90
91     Examples
92     --------
93
94     >>> strip_email('J Doe')
95     ['J Doe']
96     >>> strip_email('J Doe <jdoe@a.com>')
97     ['J Doe']
98     >>> strip_email('J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>')
99     ['J Doe', 'JJJ Smith']
100     """
101     args = list(args)
102     for i,arg in enumerate(args):
103         if arg == None:
104             continue
105         author,addr = _email_utils.parseaddr(arg)
106         if author == '':
107             author = arg
108         args[i] = author
109     return args
110
111 def reverse_aliases(aliases):
112     """Reverse an `aliases` dict.
113
114     Input:   key: canonical name,  value: list of aliases
115     Output:  key: alias,           value: canonical name
116
117     Examples
118     --------
119
120     >>> aliases = {
121     ...     'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>', 'J'],
122     ...     'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
123     ...     None:['Anonymous <a@a.com>'],
124     ...     }
125     >>> r = reverse_aliases(aliases)
126     >>> for item in sorted(r.items()):
127     ...     print item
128     ('Anonymous <a@a.com>', None)
129     ('J', 'J Doe <jdoe@a.com>')
130     ('Jingly <jjjs@b.edu>', 'JJJ Smith <jjjs@a.com>')
131     ('Johnny <jdoe@b.edu>', 'J Doe <jdoe@a.com>')
132     """
133     output = {}
134     for canonical_name,_aliases in aliases.items():
135         for alias in _aliases:
136             output[alias] = canonical_name
137     return output
138
139 def replace_aliases(authors, with_email=True, aliases=None):
140     """Consolidate and sort `authors`.
141
142     Make the replacements listed in the `aliases` dict (key: canonical
143     name, value: list of aliases).  If `aliases` is ``None``, default
144     to ``ALIASES``.
145
146     >>> aliases = {
147     ...     'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>'],
148     ...     'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
149     ...     None:['Anonymous <a@a.com>'],
150     ...     }
151     >>> authors = [
152     ...     'JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
153     ...     'Jingly <jjjs@b.edu>', 'J Doe <jdoe@a.com>', 'Anonymous <a@a.com>']
154     >>> replace_aliases(authors, with_email=True, aliases=aliases)
155     ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
156     >>> replace_aliases(authors, with_email=False, aliases=aliases)
157     ['J Doe', 'JJJ Smith']
158     """
159     if aliases == None:
160         aliases = ALIASES
161     rev_aliases = reverse_aliases(aliases)
162     for i,author in enumerate(authors):
163         if author in rev_aliases:
164             authors[i] = rev_aliases[author]
165     authors = sorted(list(set(authors)))
166     if None in authors:
167         authors.remove(None)
168     if with_email == False:
169         authors = strip_email(*authors)
170     return authors