build_manpage.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # -*- coding: utf-8 -*-
  2. # This file is distributed under the same License of Python
  3. # Copyright (c) 2014 Oz Nahum Tiram <nahumoz@gmail.com>
  4. """
  5. build_manpage.py
  6. Add a `build_manpage` command to your setup.py.
  7. To use this Command class import the class to your setup.py,
  8. and add a command to call this class::
  9. from build_manpage import BuildManPage
  10. ...
  11. ...
  12. setup(
  13. ...
  14. ...
  15. cmdclass={
  16. 'build_manpage': BuildManPage,
  17. )
  18. You can then use the following setup command to produce a man page::
  19. $ python setup.py build_manpage --output=prog.1 --parser=yourmodule:argparser
  20. Alternatively, set the variable AUTO_BUILD to True, and just invoke::
  21. $ python setup.py build
  22. If automatically want to build the man page every time you invoke your build,
  23. add to your ```setup.cfg``` the following::
  24. [build_manpage]
  25. output = <appname>.1
  26. parser = <path_to_your_parser>
  27. """
  28. import datetime
  29. from distutils.core import Command
  30. from distutils.errors import DistutilsOptionError
  31. from distutils.command.build import build
  32. import argparse
  33. import re as _re
  34. AUTO_BUILD = False
  35. class BuildManPage(Command):
  36. description = 'Generate man page from an ArgumentParser instance.'
  37. user_options = [
  38. ('output=', 'O', 'output file'),
  39. ('parser=', None, 'module path to an ArgumentParser instance'
  40. '(e.g. mymod:func, where func is a method or function which return'
  41. 'an arparse.ArgumentParser instance.'),
  42. ]
  43. def initialize_options(self):
  44. self.output = None
  45. self.parser = None
  46. def finalize_options(self):
  47. if self.output is None:
  48. raise DistutilsOptionError('\'output\' option is required')
  49. if self.parser is None:
  50. raise DistutilsOptionError('\'parser\' option is required')
  51. mod_name, func_name = self.parser.split(':')
  52. fromlist = mod_name.split('.')
  53. try:
  54. mod = __import__(mod_name, fromlist=fromlist)
  55. self._parser = getattr(mod, func_name)(formatter_class=ManPageFormatter)
  56. except ImportError as err:
  57. raise err
  58. self.announce('Writing man page %s' % self.output)
  59. self._today = datetime.date.today()
  60. def _markup(self, txt):
  61. return txt.replace('-', '\\-')
  62. def _write_header(self):
  63. appname = self.distribution.get_name()
  64. ret = []
  65. ret.append(self._parser.formatter_class._mk_title(self._parser._get_formatter(),
  66. appname))
  67. description = self.distribution.get_description()
  68. ret.append(self._parser.formatter_class._make_name(self._parser._get_formatter(),
  69. self._parser))
  70. self._parser._prog = appname
  71. ret.append(self._parser.formatter_class._mk_synopsis(self._parser._get_formatter(),
  72. self._parser))
  73. ret.append(self._parser.formatter_class._mk_description(self._parser._get_formatter(),
  74. self.distribution))
  75. return ''.join(ret)
  76. def _write_options(self):
  77. return self._parser.formatter_class.format_options(self._parser)
  78. def _write_footer(self):
  79. """
  80. Writing the footer allows one to add a lot of extra information.
  81. Sections and and their content can be specified in the dictionary
  82. sections which is passed to the formater method
  83. """
  84. appname = self.distribution.get_name()
  85. homepage = self.distribution.get_url()
  86. sections = {'authors': ("pwman3 was originally written by Ivan Kelly "
  87. "<ivan@ivankelly.net>. pwman3 is now maintained "
  88. "by Oz Nahum <nahumoz@gmail.com>."),
  89. 'distribution': ("The latest version of {} may be "
  90. "downloaded from {}".format(appname,
  91. homepage))
  92. }
  93. return self._parser.formatter_class._mk_footer(self._parser._get_formatter(),
  94. sections)
  95. def run(self):
  96. manpage = []
  97. manpage.append(self._write_header())
  98. manpage.append(self._write_options())
  99. manpage.append(self._write_footer())
  100. stream = open(self.output, 'w')
  101. stream.write(''.join(manpage))
  102. stream.close()
  103. class ManPageFormatter(argparse.HelpFormatter):
  104. """
  105. Formatter class to create man pages.
  106. Ideally, this class should rely only on the parser, and not distutils.
  107. The following shows a scenario for usage::
  108. from pwman import parser_options
  109. from build_manpage import ManPageFormatter
  110. p = parser_options(ManPageFormatter)
  111. p.format_help()
  112. The last line would print all the options and help infomation wrapped with
  113. man page macros where needed.
  114. """
  115. def __init__(self,
  116. prog,
  117. indent_increment=2,
  118. max_help_position=24,
  119. width=None,
  120. section=1,
  121. desc = None,
  122. long_desc = None,
  123. authors=None,
  124. distribution=None):
  125. super(ManPageFormatter, self).__init__(prog)
  126. self._prog = prog
  127. self._section = 1
  128. self._today = datetime.date.today().strftime('%Y\\-%m\\-%d')
  129. def _markup(self, txt):
  130. return txt.replace('-', '\\-')
  131. def _underline(self, string):
  132. return "\\fI\\s-1" + string + "\\s0\\fR"
  133. def _bold(self, string):
  134. if not string.strip().startswith('\\fB'):
  135. string = '\\fB' + string
  136. if not string.strip().endswith('\\fR'):
  137. string = string + '\\fR'
  138. return string
  139. def _mk_synopsis(self, parser):
  140. self.add_usage(parser.usage, parser._actions,
  141. parser._mutually_exclusive_groups, prefix='')
  142. usage = self._format_usage(None, parser._actions,
  143. parser._mutually_exclusive_groups, '')
  144. usage = usage.replace('%s ' % parser._prog, '')
  145. usage = '.SH SYNOPSIS\n \\fB%s\\fR %s\n' % (self._markup(parser._prog),
  146. usage)
  147. return usage
  148. def _mk_title(self, prog):
  149. return '.TH {0} {1} {2}\n'.format(prog, self._section,
  150. self._today)
  151. def _make_name(self, parser):
  152. """
  153. this method is in consitent with others ... it relies on
  154. distribution
  155. """
  156. return '.SH NAME\n%s \\- %s\n' % (parser.prog,
  157. parser.description)
  158. def _mk_name(self, distribution):
  159. """
  160. obsolete, replaced by _make_name
  161. """
  162. return '.SH NAME\n%s \\- %s\n' % (distribution.get_name(),
  163. distribution.get_description())
  164. def _mk_description(self, distribution):
  165. long_desc = distribution.get_long_description()
  166. if long_desc:
  167. long_desc = long_desc.replace('\n', '\n.br\n')
  168. return '.SH DESCRIPTION\n%s\n' % self._markup(long_desc)
  169. else:
  170. return ''
  171. def _mk_footer(self, sections):
  172. footer = []
  173. for section, value in sections.iteritems():
  174. part = ".SH {}\n {}".format(section.upper(), value)
  175. footer.append(part)
  176. return '\n'.join(footer)
  177. @staticmethod
  178. def format_options(parser):
  179. formatter = parser._get_formatter()
  180. # positionals, optionals and user-defined groups
  181. for action_group in parser._action_groups:
  182. formatter.start_section(None)
  183. formatter.add_text(None)
  184. formatter.add_arguments(action_group._group_actions)
  185. formatter.end_section()
  186. # epilog
  187. formatter.add_text(parser.epilog)
  188. # determine help from format above
  189. return '.SH OPTIONS\n' + formatter.format_help()
  190. def _format_action_invocation(self, action):
  191. if not action.option_strings:
  192. metavar, = self._metavar_formatter(action, action.dest)(1)
  193. return metavar
  194. else:
  195. parts = []
  196. # if the Optional doesn't take a value, format is:
  197. # -s, --long
  198. if action.nargs == 0:
  199. parts.extend([self._bold(action_str) for action_str in action.option_strings])
  200. # if the Optional takes a value, format is:
  201. # -s ARGS, --long ARGS
  202. else:
  203. default = self._underline(action.dest.upper())
  204. args_string = self._format_args(action, default)
  205. for option_string in action.option_strings:
  206. parts.append('%s %s' % (self._bold(option_string), args_string))
  207. return ', '.join(parts)
  208. class ManPageCreator(object):
  209. """
  210. This class takes a little different approach. Instead of relying on
  211. information from ArgumentParser, it relies on information retrieved
  212. from distutils.
  213. This class makes it easy for package maintainer to create man pages in cases,
  214. that there is no ArgumentParser.
  215. """
  216. pass
  217. def _mk_name(self, distribution):
  218. """
  219. """
  220. return '.SH NAME\n%s \\- %s\n' % (distribution.get_name(),
  221. distribution.get_description())
  222. if AUTO_BUILD:
  223. build.sub_commands.append(('build_manpage', None))