setup.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
  2. #
  3. # See COPYING file distributed along with the DataLad package for the
  4. # copyright and license terms.
  5. #
  6. # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
  7. import datetime
  8. import os
  9. from os.path import (
  10. dirname,
  11. join as opj,
  12. )
  13. from setuptools import Command, DistutilsOptionError
  14. from setuptools.config import read_configuration
  15. import versioneer
  16. from . import formatters as fmt
  17. class BuildManPage(Command):
  18. # The BuildManPage code was originally distributed
  19. # under the same License of Python
  20. # Copyright (c) 2014 Oz Nahum Tiram <nahumoz@gmail.com>
  21. description = 'Generate man page from an ArgumentParser instance.'
  22. user_options = [
  23. ('manpath=', None,
  24. 'output path for manpages (relative paths are relative to the '
  25. 'datalad package)'),
  26. ('rstpath=', None,
  27. 'output path for RST files (relative paths are relative to the '
  28. 'datalad package)'),
  29. ('parser=', None, 'module path to an ArgumentParser instance'
  30. '(e.g. mymod:func, where func is a method or function which return'
  31. 'a dict with one or more arparse.ArgumentParser instances.'),
  32. ('cmdsuite=', None, 'module path to an extension command suite '
  33. '(e.g. mymod:command_suite) to limit the build to the contained '
  34. 'commands.'),
  35. ]
  36. def initialize_options(self):
  37. self.manpath = opj('build', 'man')
  38. self.rstpath = opj('docs', 'source', 'generated', 'man')
  39. self.parser = 'datalad.cmdline.main:setup_parser'
  40. self.cmdsuite = None
  41. def finalize_options(self):
  42. if self.manpath is None:
  43. raise DistutilsOptionError('\'manpath\' option is required')
  44. if self.rstpath is None:
  45. raise DistutilsOptionError('\'rstpath\' option is required')
  46. if self.parser is None:
  47. raise DistutilsOptionError('\'parser\' option is required')
  48. mod_name, func_name = self.parser.split(':')
  49. fromlist = mod_name.split('.')
  50. try:
  51. mod = __import__(mod_name, fromlist=fromlist)
  52. self._parser = getattr(mod, func_name)(
  53. ['datalad'],
  54. formatter_class=fmt.ManPageFormatter,
  55. return_subparsers=True,
  56. # ignore extensions only for the main package to avoid pollution
  57. # with all extension commands that happen to be installed
  58. help_ignore_extensions=self.distribution.get_name() == 'datalad')
  59. except ImportError as err:
  60. raise err
  61. if self.cmdsuite:
  62. mod_name, suite_name = self.cmdsuite.split(':')
  63. mod = __import__(mod_name, fromlist=mod_name.split('.'))
  64. suite = getattr(mod, suite_name)
  65. self.cmdlist = [c[2] if len(c) > 2 else c[1].replace('_', '-').lower()
  66. for c in suite[1]]
  67. self.announce('Writing man page(s) to %s' % self.manpath)
  68. self._today = datetime.date.today()
  69. @classmethod
  70. def handle_module(cls, mod_name, **kwargs):
  71. """Module specific handling.
  72. This particular one does
  73. 1. Memorize (at class level) the module name of interest here
  74. 2. Check if 'datalad.extensions' are specified for the module,
  75. and then analyzes them to obtain command names it provides
  76. If cmdline commands are found, its entries are to be used instead of
  77. the ones in datalad's _parser.
  78. Parameters
  79. ----------
  80. **kwargs:
  81. all the kwargs which might be provided to setuptools.setup
  82. """
  83. cls.mod_name = mod_name
  84. exts = kwargs.get('entry_points', {}).get('datalad.extensions', [])
  85. for ext in exts:
  86. assert '=' in ext # should be label=module:obj
  87. ext_label, mod_obj = ext.split('=', 1)
  88. assert ':' in mod_obj # should be module:obj
  89. mod, obj = mod_obj.split(':', 1)
  90. assert mod_name == mod # AFAIK should be identical
  91. mod = __import__(mod_name)
  92. if hasattr(mod, obj):
  93. command_suite = getattr(mod, obj)
  94. assert len(command_suite) == 2 # as far as I see it
  95. if not hasattr(cls, 'cmdline_names'):
  96. cls.cmdline_names = []
  97. cls.cmdline_names += [
  98. cmd
  99. for _, _, cmd, _ in command_suite[1]
  100. ]
  101. def run(self):
  102. dist = self.distribution
  103. #homepage = dist.get_url()
  104. #appname = self._parser.prog
  105. appname = 'datalad'
  106. cfg = read_configuration(
  107. opj(dirname(dirname(__file__)), 'setup.cfg'))['metadata']
  108. sections = {
  109. 'Authors': """{0} is developed by {1} <{2}>.""".format(
  110. appname, cfg['author'], cfg['author_email']),
  111. }
  112. for cls, opath, ext in ((fmt.ManPageFormatter, self.manpath, '1'),
  113. (fmt.RSTManPageFormatter, self.rstpath, 'rst')):
  114. if not os.path.exists(opath):
  115. os.makedirs(opath)
  116. for cmdname in getattr(self, 'cmdline_names', list(self._parser)):
  117. if hasattr(self, 'cmdlist') and cmdname not in self.cmdlist:
  118. continue
  119. p = self._parser[cmdname]
  120. cmdname = "{0}{1}".format(
  121. 'datalad ' if cmdname != 'datalad' else '',
  122. cmdname)
  123. format = cls(
  124. cmdname,
  125. ext_sections=sections,
  126. version=versioneer.get_version())
  127. formatted = format.format_man_page(p)
  128. with open(opj(opath, '{0}.{1}'.format(
  129. cmdname.replace(' ', '-'),
  130. ext)),
  131. 'w') as f:
  132. f.write(formatted)
  133. class BuildConfigInfo(Command):
  134. description = 'Generate RST documentation for all config items.'
  135. user_options = [
  136. ('rstpath=', None, 'output path for RST file'),
  137. ]
  138. def initialize_options(self):
  139. self.rstpath = opj('docs', 'source', 'generated', 'cfginfo')
  140. def finalize_options(self):
  141. if self.rstpath is None:
  142. raise DistutilsOptionError('\'rstpath\' option is required')
  143. self.announce('Generating configuration documentation')
  144. def run(self):
  145. opath = self.rstpath
  146. if not os.path.exists(opath):
  147. os.makedirs(opath)
  148. from datalad.interface.common_cfg import definitions as cfgdefs
  149. from datalad.dochelpers import _indent
  150. categories = {
  151. 'global': {},
  152. 'local': {},
  153. 'dataset': {},
  154. 'misc': {}
  155. }
  156. for term, v in cfgdefs.items():
  157. categories[v.get('destination', 'misc')][term] = v
  158. for cat in categories:
  159. with open(opj(opath, '{}.rst.in'.format(cat)), 'w') as rst:
  160. rst.write('.. glossary::\n')
  161. for term, v in sorted(categories[cat].items(), key=lambda x: x[0]):
  162. rst.write(_indent(term, '\n '))
  163. qtype, docs = v.get('ui', (None, {}))
  164. desc_tmpl = '\n'
  165. if 'title' in docs:
  166. desc_tmpl += '{title}:\n'
  167. if 'text' in docs:
  168. desc_tmpl += '{text}\n'
  169. if 'default' in v:
  170. default = v['default']
  171. if hasattr(default, 'replace'):
  172. # protect against leaking specific home dirs
  173. v['default'] = default.replace(os.path.expanduser('~'), '~')
  174. desc_tmpl += 'Default: {default}\n'
  175. if 'type' in v:
  176. type_ = v['type']
  177. if hasattr(type_, 'long_description'):
  178. type_ = type_.long_description()
  179. else:
  180. type_ = type_.__name__
  181. desc_tmpl += '\n[{type}]\n'
  182. v['type'] = type_
  183. if desc_tmpl == '\n':
  184. # we need something to avoid joining terms
  185. desc_tmpl += 'undocumented\n'
  186. v.update(docs)
  187. rst.write(_indent(desc_tmpl.format(**v), ' '))