# -*- coding: utf-8 -*- # This file is distributed under the same License of Python # Copyright (c) 2014 Oz Nahum Tiram """ build_manpage.py Add a `build_manpage` command to your setup.py. To use this Command class import the class to your setup.py, and add a command to call this class:: from build_manpage import BuildManPage ... ... setup( ... ... cmdclass={ 'build_manpage': BuildManPage, ) You can then use the following setup command to produce a man page:: $ python setup.py build_manpage --output=prog.1 --parser=yourmodule:argparser """ import datetime from distutils.core import Command from distutils.errors import DistutilsOptionError import argparse class BuildManPage(Command): """-O pwman.1 --parser=pwman:parser_options""" description = 'Generate man page from setup().' user_options = [ ('output=', 'O', 'output file'), ('parser=', None, 'module path to optparser (e.g. mymod:func'), ] def initialize_options(self): self.output = None self.parser = None def finalize_options(self): if self.output is None: raise DistutilsOptionError('\'output\' option is required') if self.parser is None: raise DistutilsOptionError('\'parser\' option is required') mod_name, func_name = self.parser.split(':') fromlist = mod_name.split('.') try: mod = __import__(mod_name, fromlist=fromlist) self._parser = getattr(mod, func_name)(formatter_class=ManPageFormatter) except ImportError as err: raise err self._parser.formatter_class = ManPageFormatter self.announce('Writing man page %s' % self.output) self._today = datetime.date.today() def _markup(self, txt): return txt.replace('-', '\\-') def _write_header(self): appname = self.distribution.get_name() ret = [] ret.append('.TH %s 1 %s\n' % (self._markup(appname), self._today.strftime('%Y\\-%m\\-%d'))) description = self.distribution.get_description() if description: name = self._markup('%s - %s' % (self._markup(appname), description.splitlines()[0])) else: name = self._markup(appname) ret.append('.SH NAME\n%s\n' % name) if isinstance(self._parser, argparse.ArgumentParser): self._parser.prog = self.distribution.get_name() synopsis = self._parser.format_usage().split(':', 1)[1] else: synopsis = self._parser.get_usage() if synopsis: synopsis = synopsis.replace('%s ' % appname, '') ret.append('.SH SYNOPSIS\n \\fB%s\\fR %s\n' % (self._markup(appname), synopsis)) long_desc = self.distribution.get_long_description() if long_desc: long_desc = long_desc.replace('\n', '\n.br\n') ret.append('.SH DESCRIPTION\n%s\n' % self._markup(long_desc)) return ''.join(ret) def _write_options(self): ret = ['.SH OPTIONS\n'] ret.extend(self._parser.formatter_class.format_options(self._parser)) return ''.join(ret) def _write_footer(self): ret = [] appname = self.distribution.get_name() author = '%s <%s>' % (self.distribution.get_author(), self.distribution.get_author_email()) ret.append(('.SH AUTHORS\n.B %s\nwas written by %s.\n' % (self._markup(appname), self._markup(author)))) homepage = self.distribution.get_url() ret.append(('.SH DISTRIBUTION\nThe latest version of %s may ' 'be downloaded from\n' '%s\n\n' % (self._markup(appname), self._markup(homepage),))) return ''.join(ret) def run(self): manpage = [] manpage.append(self._write_header()) manpage.append(self._write_options()) manpage.append(self._write_footer()) stream = open(self.output, 'w') stream.write(''.join(manpage)) stream.close() class ManPageFormatter(argparse.HelpFormatter): def _underline(self, string): return "\\fI\\s-1" + string + "\\s0\\fR" def _bold(self, string): if not string.strip().startswith('\\fB'): string = '\\fB' + string if not string.strip().endswith('\\fR'): string = string + '\\fR' return string @staticmethod def format_options(parser): formatter = parser._get_formatter() # positionals, optionals and user-defined groups for action_group in parser._action_groups: formatter.start_section(None) formatter.add_text(None) formatter.add_arguments(action_group._group_actions) formatter.end_section() # epilog formatter.add_text(parser.epilog) # determine help from format above return formatter.format_help() def _format_action_invocation(self, action): if not action.option_strings: metavar, = self._metavar_formatter(action, action.dest)(1) return metavar else: parts = [] # if the Optional doesn't take a value, format is: # -s, --long if action.nargs == 0: parts.extend([self._bold(action_str) for action_str in action.option_strings]) # if the Optional takes a value, format is: # -s ARGS, --long ARGS else: default = self._underline(action.dest.upper()) args_string = self._format_args(action, default) for option_string in action.option_strings: parts.append('%s %s' % (self._bold(option_string), args_string)) return ', '.join(parts) # build.sub_commands.append(('build_manpage', None))