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