setup.py 9.1 KB

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