Salome HOME
Merge branch 'master' into V9_dev
[tools/configuration.git] / copyright / insert_copyright
1 #!/usr/bin/env python
2 #  -*- coding: utf8 -*-
3
4 # Copyright (C) 2017  CEA/DEN, EDF R&D
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License Version 3 as
8 # published by the Free Software Foundation.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # 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 this program; if not, you may download a copy of license
17 # from https://www.gnu.org/licenses/gpl-3.0.
18
19 """
20 Command line tool to insert copyright notice to a file.
21 Usage: type "insert_copyright --help" to learn how to use tool.
22 """
23
24 import argparse
25 import os
26 import re
27 import sys
28 import time
29
30 # pragma pylint: disable=redefined-builtin
31
32 # -----------------------------------------------------------------------------
33 _COMMENTS = {
34     'cpp': '//',
35     'shell': '#',
36     'python': '#',
37     'auto': None,
38     }
39
40 _OWNERS = {
41     'cea': 'CEA/DEN',
42     'edf': 'EDF R&D',
43     'occ': 'OPEN CASCADE'
44     }
45
46
47 # -----------------------------------------------------------------------------
48 def error_exit(msg):
49     """
50     Print error message to stderr and exit.
51
52     Arguments:
53         msg (str): Error message.
54     """
55     sys.stderr.write("ERROR: {}\n".format(msg))
56     sys.exit(-1)
57
58
59 # -----------------------------------------------------------------------------
60 def warning(msg):
61     """
62     Print wating message to stderr.
63
64     Arguments:
65         msg (str): Warning message.
66     """
67     sys.stderr.write("WARNING: {}\n".format(msg))
68
69
70 # -----------------------------------------------------------------------------
71 def formats():
72     """
73     Get supported formats of comments.
74
75     Returns:
76         list[str]: List of formats.
77     """
78     return list(_COMMENTS)
79
80
81 # -----------------------------------------------------------------------------
82 def search_line(lines, rex, depth=1):
83     """
84     Search regexp in given lines.
85
86     Arguments:
87         lines (list[str]): List of strings.
88         regex (str): Regular expression.
89         depth (Optional[int]): Depth of search. Defaults to 1 line.
90
91     Returns:
92         int: Index of first matched line.
93     """
94     for i in range(depth if depth >= 0 else len(lines)):
95         if i < len(lines):
96             if re.search(rex, lines[i]):
97                 return i
98     return -1
99
100
101 # -----------------------------------------------------------------------------
102 def get_owner(owner):
103     """
104     Get owner's title
105
106     Arguments:
107         owner (str): Owner's name of alias or list of owners separated
108             by comma.
109
110     Returns:
111         str: Owner's title.
112     """
113     if owner.lower() in 'all':
114         return get_owner('cea,edf,occ')
115
116     owners = [i.strip() for i in owner.split(',')]
117     result = []
118     for i in owners:
119         i = _OWNERS.get(i.lower(), i)
120         if i not in result:
121             result.append(i)
122     return ', '.join(result)
123
124
125 # -----------------------------------------------------------------------------
126 def get_comment(file_format):
127     """
128     Get comment for given format.
129
130     Arguments:
131         format (str): Format of comments.
132
133     Returns:
134         str: Comment signature for given format; *None* for unsupported
135         format.
136     """
137     return _COMMENTS.get(file_format) if file_format else None
138
139
140 # -----------------------------------------------------------------------------
141 def get_copyright(comment, owner, year):
142     """
143     Generate copyright from template.
144
145     Arguments:
146         comment (str): Comment signature.
147         owner (str): Copyright owner.
148         year (str): Copyright year(s).
149
150     Returns:
151         list[str]: List of strings with copyright data.
152     """
153     template = os.path.join(os.path.dirname(sys.argv[0]), 'copyright.template')
154     the_copyright = []
155     try:
156         with open(template) as fid:
157             the_copyright = fid.readlines()
158     except IOError:
159         error_exit("cannot find copyright template")
160     the_copyright = [i.replace('@year@', year) for i in the_copyright]
161     the_copyright = [i.replace('@owner@', owner) for i in the_copyright]
162     the_copyright = [comment + ' ' + i if i.strip() else comment + '\n'
163                      for i in the_copyright]
164     return the_copyright
165
166
167 # -----------------------------------------------------------------------------
168 def get_module_owner(module):
169     """
170     Get owner of given module.
171
172     Arguments:
173         module (str): Module name.
174
175     Returns:
176         str: Module's owner.
177     """
178     modules_info = os.path.join(os.path.dirname(sys.argv[0]), 'modules.info')
179     owner = None
180     try:
181         with open(modules_info) as fid:
182             lines = fid.readlines()
183             index = search_line(lines, r'^{}:'.format(module), -1)
184             if index >= 0:
185                 return get_owner(lines[index].split(":")[1].strip())
186     except IOError:
187         warning("cannot find modules info file")
188     return owner
189
190
191 # -----------------------------------------------------------------------------
192 def autodetect_owner(filename):
193     """
194     Auto-detect owner from file path.
195
196     Arguments:
197         filename (str): File path.
198
199     Returns:
200         str: Owner; *None* if owner isn't detected.
201     """
202     filename = os.path.realpath(filename)
203     if os.path.exists(filename):
204         directory = os.path.dirname(filename)
205         while directory != '/':
206             config_file = os.path.join(directory, '.git', 'config')
207             if os.path.exists(config_file):
208                 try:
209                     from ConfigParser import ConfigParser
210                 except ImportError:
211                     from configparser import ConfigParser
212                 try:
213                     from StringIO import StringIO
214                 except ImportError:
215                     from io import StringIO
216                 with open(config_file) as fid:
217                     gitcfg = fid.readlines()
218                     cfg = ConfigParser()
219                     data = StringIO(''.join([l.lstrip() for l in gitcfg]))
220                     cfg.readfp(data)
221                     url = cfg.get('remote "origin"', 'url')
222                     module = os.path.split(url)[-1]
223                     if module.endswith('.git'):
224                         module = module[:-4]
225                     return get_module_owner(module)
226                 break
227             directory = os.path.dirname(directory)
228     return None
229
230
231 # -----------------------------------------------------------------------------
232 def autodetect_format(filename):
233     """
234     Auto-detect format from filename.
235
236     Arguments:
237         filename (str): File path.
238
239     Returns:
240         str: Format of comments; *None* if format isn't detected.
241     """
242     extensions = {
243         'cpp': ('c', 'cpp', 'cxx', 'cc', 'c++',
244                 'h', 'hxx', 'hpp', 'hh', 'h++',
245                 'idl', 'i'),
246         'shell': ('sh', 'bash', 'csh', 'cmake', 'txt', 'cfg', 'ini', 'm4'),
247         'python': ('py',),
248         }
249     rev_extensions = {e: k for k, exts in extensions.items() for e in exts}
250     if filename and os.path.isfile(filename):
251         extension = os.path.splitext(filename)[1][1:].lower()
252         if extension in ('in',):
253             name = os.path.splitext(filename)[0]
254             extension = os.path.splitext(name)[1][1:].lower()
255         if extension in rev_extensions:
256             return rev_extensions[extension]
257
258     try:
259         import magic
260         m = magic.open(magic.MAGIC_MIME_TYPE)
261         m.load()
262         file_formats = {
263             'cpp': ('text/x-c', 'text/x-c++'),
264             'shell': ('text/x-shellscript',),
265             'python': ('text/x-python',),
266             }
267         rev_file_formats = {f: k for k, ff in file_formats.items() for f in ff}
268         file_format = m.file(filename)
269         if file_format in rev_file_formats:
270             return rev_file_formats[file_format]
271     except ImportError:
272         pass
273
274     return None
275
276
277 # -----------------------------------------------------------------------------
278 def insert_copyright(filename, owner, year, file_format):
279     """
280     Insert copyright note to a file.
281
282     Arguments:
283         filename (str): File path.
284         owner (str): Copyright owner.
285         year (str): Copyright year(s).
286         file_format (str): Format of comments.
287     """
288     try:
289         with open(filename) as fid:
290             lines = fid.readlines()
291     except IOError:
292         warning("cannot read file: {}".format(filename))
293         return
294
295     if file_format in ('auto',):
296         file_format = autodetect_format(filename)
297
298     if owner.lower() in ('auto',):
299         owner = autodetect_owner(filename) or get_owner('all')
300     else:
301         owner = get_owner(owner)
302
303     comment = get_comment(file_format)
304     if comment is None:
305         warning("cannot detect format")
306         return
307
308     shell_row = search_line(lines, r'^#!') \
309         if file_format in ('sh', 'bash', 'csh', 'py', 'python') else -1
310     coding_row = search_line(lines, r'coding:', 3) \
311         if file_format in ('py', 'python') else -1
312     insert_point = max(0, shell_row + 1, coding_row + 1)
313
314     the_copyright = get_copyright(comment, owner, year)
315     if the_copyright:
316         lines = lines[:insert_point] + the_copyright + ['\n'] \
317             + lines[insert_point:]
318         try:
319             with open(filename, 'w') as fid:
320                 for line in lines:
321                     fid.write(line)
322         except IOError:
323             warning("cannot write file: {}".format(filename))
324             return
325
326
327 # -----------------------------------------------------------------------------
328 def main():
329     """Main function."""
330
331     # Parse command line.
332     description = "Command line tool to insert copyright notice to a file."
333     parser = argparse.ArgumentParser(description=description)
334
335     help_string = "copyright owner; if not specified, tool tries to " \
336         "autodetect an owner from the file path; if auto-detection fails, " \
337         "an owner is set to '{owner}'"
338     owner = 'auto'
339     parser.add_argument("-o", "--owner", action="store",
340                         dest="owner", default=owner,
341                         help=help_string.format(owner=get_owner('all')))
342     help_string = "copyright year(s); default: current year ({year})"
343     year = str(time.localtime().tm_year)
344     parser.add_argument("-y", "--year", action="store",
345                         dest="year", default=year,
346                         help=help_string.format(year=year))
347     help_string = "format of comments ({choices}); default: {file_format}"
348     file_format = 'auto'
349     parser.add_argument("-f", "--format", action="store", choices=formats(),
350                         dest="format", default=file_format,
351                         help=help_string.format(file_format=file_format,
352                                                 choices="|".join(formats())))
353     parser.add_argument('files', nargs='+', metavar='FILE')
354
355     args = parser.parse_args(sys.argv[1:])
356
357     owner = args.owner
358     year = args.year
359     file_format = args.format
360     files = args.files
361
362     for filename in files:
363         insert_copyright(filename, owner, year, file_format)
364
365     return 0
366
367 # -----------------------------------------------------------------------------
368 if __name__ == "__main__":
369     sys.exit(main())