formatters.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
  2. #
  3. # See COPYING file distributed along with the DataLad package for the
  4. # copyright and license terms.
  5. #
  6. # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
  7. import argparse
  8. import datetime
  9. import re
  10. class ManPageFormatter(argparse.HelpFormatter):
  11. # This code was originally distributed
  12. # under the same License of Python
  13. # Copyright (c) 2014 Oz Nahum Tiram <nahumoz@gmail.com>
  14. def __init__(self,
  15. prog,
  16. indent_increment=2,
  17. max_help_position=4,
  18. width=1000000,
  19. section=1,
  20. ext_sections=None,
  21. authors=None,
  22. version=None
  23. ):
  24. super(ManPageFormatter, self).__init__(
  25. prog,
  26. indent_increment=indent_increment,
  27. max_help_position=max_help_position,
  28. width=width)
  29. self._prog = prog
  30. self._section = 1
  31. self._today = datetime.date.today().strftime('%Y\\-%m\\-%d')
  32. self._ext_sections = ext_sections
  33. self._version = version
  34. def _get_formatter(self, **kwargs):
  35. return self.formatter_class(prog=self.prog, **kwargs)
  36. def _markup(self, txt):
  37. return txt.replace('-', '\\-')
  38. def _underline(self, string):
  39. return "\\fI\\s-1" + string + "\\s0\\fR"
  40. def _bold(self, string):
  41. if not string.strip().startswith('\\fB'):
  42. string = '\\fB' + string
  43. if not string.strip().endswith('\\fR'):
  44. string = string + '\\fR'
  45. return string
  46. def _mk_synopsis(self, parser):
  47. self.add_usage(parser.usage, parser._actions,
  48. parser._mutually_exclusive_groups, prefix='')
  49. usage = self._format_usage(None, parser._actions,
  50. parser._mutually_exclusive_groups, '')
  51. # replace too long list of commands with a single placeholder
  52. usage = re.sub(r'{[^]]*?create,.*?}', ' COMMAND ', usage, flags=re.MULTILINE)
  53. # take care of proper wrapping
  54. usage = re.sub(r'\[([-a-zA-Z0-9]*)\s([a-zA-Z0-9{}|_]*)\]', r'[\1\~\2]', usage)
  55. usage = usage.replace('%s ' % self._prog, '')
  56. usage = '.SH SYNOPSIS\n.nh\n.HP\n\\fB%s\\fR %s\n.hy\n' % (self._markup(self._prog),
  57. usage)
  58. return usage
  59. def _mk_title(self, prog):
  60. name_version = "{0} {1}".format(prog, self._version)
  61. return '.TH "{0}" "{1}" "{2}" "{3}"\n'.format(
  62. prog, self._section, self._today, name_version)
  63. def _mk_name(self, prog, desc):
  64. """
  65. this method is in consitent with others ... it relies on
  66. distribution
  67. """
  68. desc = desc.splitlines()[0] if desc else 'it is in the name'
  69. # ensure starting lower case
  70. desc = desc[0].lower() + desc[1:]
  71. return '.SH NAME\n%s \\- %s\n' % (self._bold(prog), desc)
  72. def _mk_description(self, parser):
  73. desc = parser.description
  74. desc = '\n'.join(desc.splitlines()[1:])
  75. if not desc:
  76. return ''
  77. desc = desc.replace('\n\n', '\n.PP\n')
  78. # sub-section headings
  79. desc = re.sub(r'^\*(.*)\*$', r'.SS \1', desc, flags=re.MULTILINE)
  80. # italic commands
  81. desc = re.sub(r'^ ([-a-z]*)$', r'.TP\n\\fI\1\\fR', desc, flags=re.MULTILINE)
  82. # deindent body text, leave to troff viewer
  83. desc = re.sub(r'^ (\S.*)\n', '\\1\n', desc, flags=re.MULTILINE)
  84. # format NOTEs as indented paragraphs
  85. desc = re.sub(r'^NOTE\n', '.TP\nNOTE\n', desc, flags=re.MULTILINE)
  86. # deindent indented paragraphs after heading setup
  87. desc = re.sub(r'^ (.*)$', '\\1', desc, flags=re.MULTILINE)
  88. return '.SH DESCRIPTION\n%s\n' % self._markup(desc)
  89. def _mk_footer(self, sections):
  90. if not hasattr(sections, '__iter__'):
  91. return ''
  92. footer = []
  93. for section, value in sections.items():
  94. part = ".SH {}\n {}".format(section.upper(), value)
  95. footer.append(part)
  96. return '\n'.join(footer)
  97. def format_man_page(self, parser):
  98. page = []
  99. page.append(self._mk_title(self._prog))
  100. page.append(self._mk_name(self._prog, parser.description))
  101. page.append(self._mk_synopsis(parser))
  102. page.append(self._mk_description(parser))
  103. page.append(self._mk_options(parser))
  104. page.append(self._mk_footer(self._ext_sections))
  105. return ''.join(page)
  106. def _mk_options(self, parser):
  107. formatter = parser._get_formatter()
  108. # positionals, optionals and user-defined groups
  109. for action_group in parser._action_groups:
  110. formatter.start_section(None)
  111. formatter.add_text(None)
  112. formatter.add_arguments(action_group._group_actions)
  113. formatter.end_section()
  114. # epilog
  115. formatter.add_text(parser.epilog)
  116. # determine help from format above
  117. help = formatter.format_help()
  118. # add spaces after comma delimiters for easier reformatting
  119. help = re.sub(r'([a-z]),([a-z])', '\\1, \\2', help)
  120. # get proper indentation for argument items
  121. help = re.sub(r'^ (\S.*)\n', '.TP\n\\1\n', help, flags=re.MULTILINE)
  122. # deindent body text, leave to troff viewer
  123. help = re.sub(r'^ (\S.*)\n', '\\1\n', help, flags=re.MULTILINE)
  124. return '.SH OPTIONS\n' + help
  125. def _format_action_invocation(self, action, doubledash='--'):
  126. if not action.option_strings:
  127. metavar, = self._metavar_formatter(action, action.dest)(1)
  128. return metavar
  129. else:
  130. parts = []
  131. # if the Optional doesn't take a value, format is:
  132. # -s, --long
  133. if action.nargs == 0:
  134. parts.extend([self._bold(action_str) for action_str in
  135. action.option_strings])
  136. # if the Optional takes a value, format is:
  137. # -s ARGS, --long ARGS
  138. else:
  139. default = self._underline(action.dest.upper())
  140. args_string = self._format_args(action, default)
  141. for option_string in action.option_strings:
  142. parts.append('%s %s' % (self._bold(option_string),
  143. args_string))
  144. return ', '.join(p.replace('--', doubledash) for p in parts)
  145. class RSTManPageFormatter(ManPageFormatter):
  146. def _get_formatter(self, **kwargs):
  147. return self.formatter_class(prog=self.prog, **kwargs)
  148. def _markup(self, txt):
  149. # put general tune-ups here
  150. return txt
  151. def _underline(self, string):
  152. return "*{0}*".format(string)
  153. def _bold(self, string):
  154. return "**{0}**".format(string)
  155. def _mk_synopsis(self, parser):
  156. self.add_usage(parser.usage, parser._actions,
  157. parser._mutually_exclusive_groups, prefix='')
  158. usage = self._format_usage(None, parser._actions,
  159. parser._mutually_exclusive_groups, '')
  160. usage = usage.replace('%s ' % self._prog, '')
  161. usage = 'Synopsis\n--------\n::\n\n %s %s\n' \
  162. % (self._markup(self._prog), usage)
  163. return usage
  164. def _mk_title(self, prog):
  165. # and an easy to use reference point
  166. title = ".. _man_%s:\n\n" % prog.replace(' ', '-')
  167. title += "{0}".format(prog)
  168. title += '\n{0}\n\n'.format('=' * len(prog))
  169. return title
  170. def _mk_name(self, prog, desc):
  171. return ''
  172. def _mk_description(self, parser):
  173. desc = parser.description
  174. if not desc:
  175. return ''
  176. return 'Description\n-----------\n%s\n' % self._markup(desc)
  177. def _mk_footer(self, sections):
  178. if not hasattr(sections, '__iter__'):
  179. return ''
  180. footer = []
  181. for section, value in sections.items():
  182. part = "\n{0}\n{1}\n{2}\n".format(
  183. section,
  184. '-' * len(section),
  185. value)
  186. footer.append(part)
  187. return '\n'.join(footer)
  188. def _mk_options(self, parser):
  189. # this non-obvious maneuver is really necessary!
  190. formatter = self.__class__(self._prog)
  191. # positionals, optionals and user-defined groups
  192. for action_group in parser._action_groups:
  193. formatter.start_section(None)
  194. formatter.add_text(None)
  195. formatter.add_arguments(action_group._group_actions)
  196. formatter.end_section()
  197. # epilog
  198. formatter.add_text(parser.epilog)
  199. # determine help from format above
  200. option_sec = formatter.format_help()
  201. return '\n\nOptions\n-------\n{0}'.format(option_sec)
  202. def _format_action(self, action):
  203. # determine the required width and the entry label
  204. action_header = self._format_action_invocation(action)
  205. if action.help:
  206. help_text = self._expand_help(action)
  207. help_lines = self._split_lines(help_text, 80)
  208. help = ' '.join(help_lines)
  209. else:
  210. help = ''
  211. # return a single string
  212. return '{0}\n{1}\n{2}\n\n'.format(
  213. action_header,
  214. '~' * len(action_header),
  215. help)
  216. def cmdline_example_to_rst(src, out=None, ref=None):
  217. if out is None:
  218. from io import StringIO
  219. out = StringIO()
  220. # place header
  221. out.write('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n')
  222. if ref:
  223. # place cross-ref target
  224. out.write('.. {0}:\n\n'.format(ref))
  225. # parser status vars
  226. inexample = False
  227. incodeblock = False
  228. for line in src:
  229. if line.startswith('#% EXAMPLE START'):
  230. inexample = True
  231. incodeblock = False
  232. continue
  233. if not inexample:
  234. continue
  235. if line.startswith('#% EXAMPLE END'):
  236. break
  237. if not inexample:
  238. continue
  239. if line.startswith('#%'):
  240. incodeblock = not incodeblock
  241. if incodeblock:
  242. out.write('\n.. code-block:: sh\n\n')
  243. continue
  244. if not incodeblock and line.startswith('#'):
  245. out.write(line[(min(2, len(line) - 1)):])
  246. continue
  247. if incodeblock:
  248. if not line.rstrip().endswith('#% SKIP'):
  249. out.write(' %s' % line)
  250. continue
  251. if not len(line.strip()):
  252. continue
  253. else:
  254. raise RuntimeError("this should not happen")
  255. return out