use tox (afterfinding _ctypes)
[docutils.git] / docutils / tools / buildhtml.py
blobffd930097c1c647f47c95cad024a4b0392a80a74
1 #!/usr/bin/env python
3 # $Id$
4 # Author: David Goodger <goodger@python.org>
5 # Copyright: This module has been placed in the public domain.
7 """
8 Generates .html from all the .txt files in a directory.
10 Ordinary .txt files are understood to be standalone reStructuredText.
11 Files named ``pep-*.txt`` are interpreted as reStructuredText PEPs.
12 """
13 # Once PySource is here, build .html from .py as well.
15 __docformat__ = 'reStructuredText'
18 try:
19 import locale
20 locale.setlocale(locale.LC_ALL, '')
21 except:
22 pass
24 import sys
25 import os
26 import os.path
27 import copy
28 from fnmatch import fnmatch
29 import docutils
30 from docutils import ApplicationError
31 from docutils import core, frontend, utils
32 from docutils.utils.error_reporting import ErrorOutput, ErrorString
33 from docutils.parsers import rst
34 from docutils.readers import standalone, pep
35 from docutils.writers import html4css1, html5_polyglot, pep_html
38 usage = '%prog [options] [<directory> ...]'
39 description = ('Generates .html from all the reStructuredText .txt files '
40 '(including PEPs) in each <directory> '
41 '(default is the current directory).')
44 class SettingsSpec(docutils.SettingsSpec):
46 """
47 Runtime settings & command-line options for the front end.
48 """
50 prune_default = ['.hg', '.bzr', '.git', '.svn', 'CVS']
52 # Can't be included in OptionParser below because we don't want to
53 # override the base class.
54 settings_spec = (
55 'Build-HTML Options',
56 None,
57 (('Recursively scan subdirectories for files to process. This is '
58 'the default.',
59 ['--recurse'],
60 {'action': 'store_true', 'default': 1,
61 'validator': frontend.validate_boolean}),
62 ('Do not scan subdirectories for files to process.',
63 ['--local'], {'dest': 'recurse', 'action': 'store_false'}),
64 ('Do not process files in <directory> (shell globbing patterns, '
65 'separated by colons). This option may be used '
66 'more than once to specify multiple directories. Default: "%s".'
67 % ':'.join(prune_default),
68 ['--prune'],
69 {'metavar': '<directory>', 'action': 'append',
70 'validator': frontend.validate_colon_separated_string_list,
71 'default': prune_default,}),
72 ('Recursively ignore files matching any of the given '
73 'wildcard (shell globbing) patterns (separated by colons).',
74 ['--ignore'],
75 {'metavar': '<patterns>', 'action': 'append',
76 'default': [],
77 'validator': frontend.validate_colon_separated_string_list}),
78 ('Docutils writer, one of "html", "html4", "html5". '
79 'Default: "html" (use Docutils\' default HTML writer).',
80 ['--writer'],
81 {'metavar': '<writer>',
82 'choices': ['html', 'html4', 'html5'],
83 'default': 'html'}),
84 ('Obsoleted by "--writer".',
85 ['--html-writer'],
86 {'dest': 'writer',
87 'metavar': '<writer>',
88 'choices': ['html', 'html4', 'html5'],}),
89 ('Work silently (no progress messages). Independent of "--quiet".',
90 ['--silent'],
91 {'action': 'store_true', 'validator': frontend.validate_boolean}),
92 ('Do not process files, show files that would be processed.',
93 ['--dry-run'],
94 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
96 relative_path_settings = ('prune',)
97 config_section = 'buildhtml application'
98 config_section_dependencies = ('applications',)
101 class OptionParser(frontend.OptionParser):
104 Command-line option processing for the ``buildhtml.py`` front end.
107 def check_values(self, values, args):
108 frontend.OptionParser.check_values(self, values, args)
109 values._source = None
110 return values
112 def check_args(self, args):
113 source = destination = None
114 if args:
115 self.values._directories = args
116 else:
117 self.values._directories = [os.getcwd()]
118 return source, destination
121 class Struct(object):
123 """Stores data attributes for dotted-attribute access."""
125 def __init__(self, **keywordargs):
126 self.__dict__.update(keywordargs)
129 class Builder(object):
131 def __init__(self):
132 self.publishers = {
133 '': Struct(components=(pep.Reader, rst.Parser, pep_html.Writer,
134 SettingsSpec)),
135 'html4': Struct(components=(rst.Parser, standalone.Reader,
136 html4css1.Writer, SettingsSpec),
137 reader_name='standalone',
138 writer_name='html4'),
139 'html5': Struct(components=(rst.Parser, standalone.Reader,
140 html5_polyglot.Writer, SettingsSpec),
141 reader_name='standalone',
142 writer_name='html5'),
143 'PEPs': Struct(components=(rst.Parser, pep.Reader,
144 pep_html.Writer, SettingsSpec),
145 reader_name='pep',
146 writer_name='pep_html')}
147 """Publisher-specific settings. Key '' is for the front-end script
148 itself. ``self.publishers[''].components`` must contain a superset of
149 all components used by individual publishers."""
151 self.setup_publishers()
152 # default html writer (may change to html5 some time):
153 self.publishers['html'] = self.publishers['html4']
155 def setup_publishers(self):
157 Manage configurations for individual publishers.
159 Each publisher (combination of parser, reader, and writer) may have
160 its own configuration defaults, which must be kept separate from those
161 of the other publishers. Setting defaults are combined with the
162 config file settings and command-line options by
163 `self.get_settings()`.
165 for name, publisher in self.publishers.items():
166 option_parser = OptionParser(
167 components=publisher.components, read_config_files=1,
168 usage=usage, description=description)
169 publisher.option_parser = option_parser
170 publisher.setting_defaults = option_parser.get_default_values()
171 frontend.make_paths_absolute(publisher.setting_defaults.__dict__,
172 option_parser.relative_path_settings)
173 publisher.config_settings = (
174 option_parser.get_standard_config_settings())
175 self.settings_spec = self.publishers[''].option_parser.parse_args(
176 values=frontend.Values()) # no defaults; just the cmdline opts
177 self.initial_settings = self.get_settings('')
179 def get_settings(self, publisher_name, directory=None):
181 Return a settings object, from multiple sources.
183 Copy the setting defaults, overlay the startup config file settings,
184 then the local config file settings, then the command-line options.
185 Assumes the current directory has been set.
187 publisher = self.publishers[publisher_name]
188 settings = frontend.Values(publisher.setting_defaults.__dict__)
189 settings.update(publisher.config_settings, publisher.option_parser)
190 if directory:
191 local_config = publisher.option_parser.get_config_file_settings(
192 os.path.join(directory, 'docutils.conf'))
193 frontend.make_paths_absolute(
194 local_config, publisher.option_parser.relative_path_settings,
195 directory)
196 settings.update(local_config, publisher.option_parser)
197 settings.update(self.settings_spec.__dict__, publisher.option_parser)
198 return settings
200 def run(self, directory=None, recurse=1):
201 recurse = recurse and self.initial_settings.recurse
202 if directory:
203 self.directories = [directory]
204 elif self.settings_spec._directories:
205 self.directories = self.settings_spec._directories
206 else:
207 self.directories = [os.getcwd()]
208 for directory in self.directories:
209 for root, dirs, files in os.walk(directory):
210 # os.walk by default this recurses down the tree,
211 # influence by modifying dirs.
212 if not recurse:
213 del dirs[:]
214 self.visit(root, files, dirs)
216 def visit(self, directory, names, subdirectories):
217 settings = self.get_settings('', directory)
218 errout = ErrorOutput(encoding=settings.error_encoding)
219 if settings.prune and (os.path.abspath(directory) in settings.prune):
220 errout.write('/// ...Skipping directory (pruned): %s\n' %
221 directory)
222 sys.stderr.flush()
223 del subdirectories[:]
224 return
225 if not self.initial_settings.silent:
226 errout.write('/// Processing directory: %s\n' % directory)
227 sys.stderr.flush()
228 # settings.ignore grows many duplicate entries as we recurse
229 # if we add patterns in config files or on the command line.
230 for pattern in utils.uniq(settings.ignore):
231 for i in range(len(names) - 1, -1, -1):
232 if fnmatch(names[i], pattern):
233 # Modify in place!
234 del names[i]
235 for name in names:
236 if name.endswith('.txt'):
237 self.process_txt(directory, name)
239 def process_txt(self, directory, name):
240 if name.startswith('pep-'):
241 publisher = 'PEPs'
242 else:
243 publisher = self.initial_settings.writer
244 settings = self.get_settings(publisher, directory)
245 errout = ErrorOutput(encoding=settings.error_encoding)
246 pub_struct = self.publishers[publisher]
247 settings._source = os.path.normpath(os.path.join(directory, name))
248 settings._destination = settings._source[:-4]+'.html'
249 if not self.initial_settings.silent:
250 errout.write(' ::: Processing: %s\n' % name)
251 sys.stderr.flush()
252 try:
253 if not settings.dry_run:
254 core.publish_file(source_path=settings._source,
255 destination_path=settings._destination,
256 reader_name=pub_struct.reader_name,
257 parser_name='restructuredtext',
258 writer_name=pub_struct.writer_name,
259 settings=settings)
260 except ApplicationError:
261 error = sys.exc_info()[1] # get exception in Python 3.x
262 errout.write(' %s\n' % ErrorString(error))
265 if __name__ == "__main__":
266 Builder().run()