1# $Id: core.py 8367 2019-08-27 12:09:56Z milde $
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
4
5"""
6Calling the ``publish_*`` convenience functions (or instantiating a
7`Publisher` object) with component names will result in default
8behavior.  For custom behavior (setting component options), create
9custom component objects first, and pass *them* to
10``publish_*``/`Publisher`.  See `The Docutils Publisher`_.
11
12.. _The Docutils Publisher: http://docutils.sf.net/docs/api/publisher.html
13"""
14from __future__ import print_function
15
16__docformat__ = 'reStructuredText'
17
18import sys
19import pprint
20from docutils import __version__, __version_details__, SettingsSpec
21from docutils import frontend, io, utils, readers, writers
22from docutils.frontend import OptionParser
23from docutils.transforms import Transformer
24from docutils.utils.error_reporting import ErrorOutput, ErrorString
25import docutils.readers.doctree
26
27class Publisher(object):
28
29    """
30    A facade encapsulating the high-level logic of a Docutils system.
31    """
32
33    def __init__(self, reader=None, parser=None, writer=None,
34                 source=None, source_class=io.FileInput,
35                 destination=None, destination_class=io.FileOutput,
36                 settings=None):
37        """
38        Initial setup.  If any of `reader`, `parser`, or `writer` are not
39        specified, the corresponding ``set_...`` method should be called with
40        a component name (`set_reader` sets the parser as well).
41        """
42
43        self.document = None
44        """The document tree (`docutils.nodes` objects)."""
45
46        self.reader = reader
47        """A `docutils.readers.Reader` instance."""
48
49        self.parser = parser
50        """A `docutils.parsers.Parser` instance."""
51
52        self.writer = writer
53        """A `docutils.writers.Writer` instance."""
54
55        for component in 'reader', 'parser', 'writer':
56            assert not isinstance(getattr(self, component), str), (
57                'passed string "%s" as "%s" parameter; pass an instance, '
58                'or use the "%s_name" parameter instead (in '
59                'docutils.core.publish_* convenience functions).'
60                % (getattr(self, component), component, component))
61
62        self.source = source
63        """The source of input data, a `docutils.io.Input` instance."""
64
65        self.source_class = source_class
66        """The class for dynamically created source objects."""
67
68        self.destination = destination
69        """The destination for docutils output, a `docutils.io.Output`
70        instance."""
71
72        self.destination_class = destination_class
73        """The class for dynamically created destination objects."""
74
75        self.settings = settings
76        """An object containing Docutils settings as instance attributes.
77        Set by `self.process_command_line()` or `self.get_settings()`."""
78
79        self._stderr = ErrorOutput()
80
81    def set_reader(self, reader_name, parser, parser_name):
82        """Set `self.reader` by name."""
83        reader_class = readers.get_reader_class(reader_name)
84        self.reader = reader_class(parser, parser_name)
85        self.parser = self.reader.parser
86
87    def set_writer(self, writer_name):
88        """Set `self.writer` by name."""
89        writer_class = writers.get_writer_class(writer_name)
90        self.writer = writer_class()
91
92    def set_components(self, reader_name, parser_name, writer_name):
93        if self.reader is None:
94            self.set_reader(reader_name, self.parser, parser_name)
95        if self.parser is None:
96            if self.reader.parser is None:
97                self.reader.set_parser(parser_name)
98            self.parser = self.reader.parser
99        if self.writer is None:
100            self.set_writer(writer_name)
101
102    def setup_option_parser(self, usage=None, description=None,
103                            settings_spec=None, config_section=None,
104                            **defaults):
105        if config_section:
106            if not settings_spec:
107                settings_spec = SettingsSpec()
108            settings_spec.config_section = config_section
109            parts = config_section.split()
110            if len(parts) > 1 and parts[-1] == 'application':
111                settings_spec.config_section_dependencies = ['applications']
112        #@@@ Add self.source & self.destination to components in future?
113        option_parser = OptionParser(
114            components=(self.parser, self.reader, self.writer, settings_spec),
115            defaults=defaults, read_config_files=True,
116            usage=usage, description=description)
117        return option_parser
118
119    def get_settings(self, usage=None, description=None,
120                     settings_spec=None, config_section=None, **defaults):
121        """
122        Set and return default settings (overrides in `defaults` dict).
123
124        Set components first (`self.set_reader` & `self.set_writer`).
125        Explicitly setting `self.settings` disables command line option
126        processing from `self.publish()`.
127        """
128        option_parser = self.setup_option_parser(
129            usage, description, settings_spec, config_section, **defaults)
130        self.settings = option_parser.get_default_values()
131        return self.settings
132
133    def process_programmatic_settings(self, settings_spec,
134                                      settings_overrides,
135                                      config_section):
136        if self.settings is None:
137            defaults = (settings_overrides or {}).copy()
138            # Propagate exceptions by default when used programmatically:
139            defaults.setdefault('traceback', True)
140            self.get_settings(settings_spec=settings_spec,
141                              config_section=config_section,
142                              **defaults)
143
144    def process_command_line(self, argv=None, usage=None, description=None,
145                             settings_spec=None, config_section=None,
146                             **defaults):
147        """
148        Pass an empty list to `argv` to avoid reading `sys.argv` (the
149        default).
150
151        Set components first (`self.set_reader` & `self.set_writer`).
152        """
153        option_parser = self.setup_option_parser(
154            usage, description, settings_spec, config_section, **defaults)
155        if argv is None:
156            argv = sys.argv[1:]
157            # converting to Unicode (Python 3 does this automatically):
158            if sys.version_info < (3, 0):
159                # TODO: make this failsafe and reversible?
160                argv_encoding = (frontend.locale_encoding or 'ascii')
161                argv = [a.decode(argv_encoding) for a in argv]
162        self.settings = option_parser.parse_args(argv)
163
164    def set_io(self, source_path=None, destination_path=None):
165        if self.source is None:
166            self.set_source(source_path=source_path)
167        if self.destination is None:
168            self.set_destination(destination_path=destination_path)
169
170    def set_source(self, source=None, source_path=None):
171        if source_path is None:
172            source_path = self.settings._source
173        else:
174            self.settings._source = source_path
175        # Raise IOError instead of system exit with `tracback == True`
176        # TODO: change io.FileInput's default behaviour and remove this hack
177        try:
178            self.source = self.source_class(
179                source=source, source_path=source_path,
180                encoding=self.settings.input_encoding)
181        except TypeError:
182            self.source = self.source_class(
183                source=source, source_path=source_path,
184                encoding=self.settings.input_encoding)
185
186    def set_destination(self, destination=None, destination_path=None):
187        if destination_path is None:
188            destination_path = self.settings._destination
189        else:
190            self.settings._destination = destination_path
191        self.destination = self.destination_class(
192            destination=destination, destination_path=destination_path,
193            encoding=self.settings.output_encoding,
194            error_handler=self.settings.output_encoding_error_handler)
195
196    def apply_transforms(self):
197        self.document.transformer.populate_from_components(
198            (self.source, self.reader, self.reader.parser, self.writer,
199             self.destination))
200        self.document.transformer.apply_transforms()
201
202    def publish(self, argv=None, usage=None, description=None,
203                settings_spec=None, settings_overrides=None,
204                config_section=None, enable_exit_status=False):
205        """
206        Process command line options and arguments (if `self.settings` not
207        already set), run `self.reader` and then `self.writer`.  Return
208        `self.writer`'s output.
209        """
210        exit = None
211        try:
212            if self.settings is None:
213                self.process_command_line(
214                    argv, usage, description, settings_spec, config_section,
215                    **(settings_overrides or {}))
216            self.set_io()
217            self.document = self.reader.read(self.source, self.parser,
218                                             self.settings)
219            self.apply_transforms()
220            output = self.writer.write(self.document, self.destination)
221            self.writer.assemble_parts()
222        except SystemExit as error:
223            exit = 1
224            exit_status = error.code
225        except Exception as error:
226            if not self.settings:       # exception too early to report nicely
227                raise
228            if self.settings.traceback: # Propagate exceptions?
229                self.debugging_dumps()
230                raise
231            self.report_Exception(error)
232            exit = True
233            exit_status = 1
234        self.debugging_dumps()
235        if (enable_exit_status and self.document
236            and (self.document.reporter.max_level
237                 >= self.settings.exit_status_level)):
238            sys.exit(self.document.reporter.max_level + 10)
239        elif exit:
240            sys.exit(exit_status)
241        return output
242
243    def debugging_dumps(self):
244        if not self.document:
245            return
246        if self.settings.dump_settings:
247            print('\n::: Runtime settings:', file=self._stderr)
248            print(pprint.pformat(self.settings.__dict__), file=self._stderr)
249        if self.settings.dump_internals:
250            print('\n::: Document internals:', file=self._stderr)
251            print(pprint.pformat(self.document.__dict__), file=self._stderr)
252        if self.settings.dump_transforms:
253            print('\n::: Transforms applied:', file=self._stderr)
254            print(' (priority, transform class, pending node details, '
255                  'keyword args)', file=self._stderr)
256            print(pprint.pformat(
257                [(priority, '%s.%s' % (xclass.__module__, xclass.__name__),
258                  pending and pending.details, kwargs)
259                 for priority, xclass, pending, kwargs
260                 in self.document.transformer.applied]), file=self._stderr)
261        if self.settings.dump_pseudo_xml:
262            print('\n::: Pseudo-XML:', file=self._stderr)
263            print(self.document.pformat().encode(
264                'raw_unicode_escape'), file=self._stderr)
265
266    def report_Exception(self, error):
267        if isinstance(error, utils.SystemMessage):
268            self.report_SystemMessage(error)
269        elif isinstance(error, UnicodeEncodeError):
270            self.report_UnicodeError(error)
271        elif isinstance(error, io.InputError):
272            self._stderr.write(u'Unable to open source file for reading:\n'
273                               u'  %s\n' % ErrorString(error))
274        elif isinstance(error, io.OutputError):
275            self._stderr.write(
276                u'Unable to open destination file for writing:\n'
277                u'  %s\n' % ErrorString(error))
278        else:
279            print(u'%s' % ErrorString(error), file=self._stderr)
280            print(("""\
281Exiting due to error.  Use "--traceback" to diagnose.
282Please report errors to <docutils-users@lists.sf.net>.
283Include "--traceback" output, Docutils version (%s%s),
284Python version (%s), your OS type & version, and the
285command line used.""" % (__version__,
286                         docutils.__version_details__ and
287                         ' [%s]'%docutils.__version_details__ or '',
288                         sys.version.split()[0])), file=self._stderr)
289
290    def report_SystemMessage(self, error):
291        print('Exiting due to level-%s (%s) system message.' % (
292            error.level, utils.Reporter.levels[error.level]),
293              file=self._stderr)
294
295    def report_UnicodeError(self, error):
296        data = error.object[error.start:error.end]
297        self._stderr.write(
298            '%s\n'
299            '\n'
300            'The specified output encoding (%s) cannot\n'
301            'handle all of the output.\n'
302            'Try setting "--output-encoding-error-handler" to\n'
303            '\n'
304            '* "xmlcharrefreplace" (for HTML & XML output);\n'
305            '  the output will contain "%s" and should be usable.\n'
306            '* "backslashreplace" (for other output formats);\n'
307            '  look for "%s" in the output.\n'
308            '* "replace"; look for "?" in the output.\n'
309            '\n'
310            '"--output-encoding-error-handler" is currently set to "%s".\n'
311            '\n'
312            'Exiting due to error.  Use "--traceback" to diagnose.\n'
313            'If the advice above doesn\'t eliminate the error,\n'
314            'please report it to <docutils-users@lists.sf.net>.\n'
315            'Include "--traceback" output, Docutils version (%s),\n'
316            'Python version (%s), your OS type & version, and the\n'
317            'command line used.\n'
318            % (ErrorString(error),
319               self.settings.output_encoding,
320               data.encode('ascii', 'xmlcharrefreplace'),
321               data.encode('ascii', 'backslashreplace'),
322               self.settings.output_encoding_error_handler,
323               __version__, sys.version.split()[0]))
324
325default_usage = '%prog [options] [<source> [<destination>]]'
326default_description = ('Reads from <source> (default is stdin) and writes to '
327                       '<destination> (default is stdout).  See '
328                       '<http://docutils.sf.net/docs/user/config.html> for '
329                       'the full reference.')
330
331def publish_cmdline(reader=None, reader_name='standalone',
332                    parser=None, parser_name='restructuredtext',
333                    writer=None, writer_name='pseudoxml',
334                    settings=None, settings_spec=None,
335                    settings_overrides=None, config_section=None,
336                    enable_exit_status=True, argv=None,
337                    usage=default_usage, description=default_description):
338    """
339    Set up & run a `Publisher` for command-line-based file I/O (input and
340    output file paths taken automatically from the command line).  Return the
341    encoded string output also.
342
343    Parameters: see `publish_programmatically` for the remainder.
344
345    - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``.
346    - `usage`: Usage string, output if there's a problem parsing the command
347      line.
348    - `description`: Program description, output for the "--help" option
349      (along with command-line option descriptions).
350    """
351    pub = Publisher(reader, parser, writer, settings=settings)
352    pub.set_components(reader_name, parser_name, writer_name)
353    output = pub.publish(
354        argv, usage, description, settings_spec, settings_overrides,
355        config_section=config_section, enable_exit_status=enable_exit_status)
356    return output
357
358def publish_file(source=None, source_path=None,
359                 destination=None, destination_path=None,
360                 reader=None, reader_name='standalone',
361                 parser=None, parser_name='restructuredtext',
362                 writer=None, writer_name='pseudoxml',
363                 settings=None, settings_spec=None, settings_overrides=None,
364                 config_section=None, enable_exit_status=False):
365    """
366    Set up & run a `Publisher` for programmatic use with file-like I/O.
367    Return the encoded string output also.
368
369    Parameters: see `publish_programmatically`.
370    """
371    output, pub = publish_programmatically(
372        source_class=io.FileInput, source=source, source_path=source_path,
373        destination_class=io.FileOutput,
374        destination=destination, destination_path=destination_path,
375        reader=reader, reader_name=reader_name,
376        parser=parser, parser_name=parser_name,
377        writer=writer, writer_name=writer_name,
378        settings=settings, settings_spec=settings_spec,
379        settings_overrides=settings_overrides,
380        config_section=config_section,
381        enable_exit_status=enable_exit_status)
382    return output
383
384def publish_string(source, source_path=None, destination_path=None,
385                   reader=None, reader_name='standalone',
386                   parser=None, parser_name='restructuredtext',
387                   writer=None, writer_name='pseudoxml',
388                   settings=None, settings_spec=None,
389                   settings_overrides=None, config_section=None,
390                   enable_exit_status=False):
391    """
392    Set up & run a `Publisher` for programmatic use with string I/O.  Return
393    the encoded string or Unicode string output.
394
395    For encoded string output, be sure to set the 'output_encoding' setting to
396    the desired encoding.  Set it to 'unicode' for unencoded Unicode string
397    output.  Here's one way::
398
399        publish_string(..., settings_overrides={'output_encoding': 'unicode'})
400
401    Similarly for Unicode string input (`source`)::
402
403        publish_string(..., settings_overrides={'input_encoding': 'unicode'})
404
405    Parameters: see `publish_programmatically`.
406    """
407    output, pub = publish_programmatically(
408        source_class=io.StringInput, source=source, source_path=source_path,
409        destination_class=io.StringOutput,
410        destination=None, destination_path=destination_path,
411        reader=reader, reader_name=reader_name,
412        parser=parser, parser_name=parser_name,
413        writer=writer, writer_name=writer_name,
414        settings=settings, settings_spec=settings_spec,
415        settings_overrides=settings_overrides,
416        config_section=config_section,
417        enable_exit_status=enable_exit_status)
418    return output
419
420def publish_parts(source, source_path=None, source_class=io.StringInput,
421                  destination_path=None,
422                  reader=None, reader_name='standalone',
423                  parser=None, parser_name='restructuredtext',
424                  writer=None, writer_name='pseudoxml',
425                  settings=None, settings_spec=None,
426                  settings_overrides=None, config_section=None,
427                  enable_exit_status=False):
428    """
429    Set up & run a `Publisher`, and return a dictionary of document parts.
430    Dictionary keys are the names of parts, and values are Unicode strings;
431    encoding is up to the client.  For programmatic use with string I/O.
432
433    For encoded string input, be sure to set the 'input_encoding' setting to
434    the desired encoding.  Set it to 'unicode' for unencoded Unicode string
435    input.  Here's how::
436
437        publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
438
439    Parameters: see `publish_programmatically`.
440    """
441    output, pub = publish_programmatically(
442        source=source, source_path=source_path, source_class=source_class,
443        destination_class=io.StringOutput,
444        destination=None, destination_path=destination_path,
445        reader=reader, reader_name=reader_name,
446        parser=parser, parser_name=parser_name,
447        writer=writer, writer_name=writer_name,
448        settings=settings, settings_spec=settings_spec,
449        settings_overrides=settings_overrides,
450        config_section=config_section,
451        enable_exit_status=enable_exit_status)
452    return pub.writer.parts
453
454def publish_doctree(source, source_path=None,
455                    source_class=io.StringInput,
456                    reader=None, reader_name='standalone',
457                    parser=None, parser_name='restructuredtext',
458                    settings=None, settings_spec=None,
459                    settings_overrides=None, config_section=None,
460                    enable_exit_status=False):
461    """
462    Set up & run a `Publisher` for programmatic use with string I/O.
463    Return the document tree.
464
465    For encoded string input, be sure to set the 'input_encoding' setting to
466    the desired encoding.  Set it to 'unicode' for unencoded Unicode string
467    input.  Here's one way::
468
469        publish_doctree(..., settings_overrides={'input_encoding': 'unicode'})
470
471    Parameters: see `publish_programmatically`.
472    """
473    pub = Publisher(reader=reader, parser=parser, writer=None,
474                    settings=settings,
475                    source_class=source_class,
476                    destination_class=io.NullOutput)
477    pub.set_components(reader_name, parser_name, 'null')
478    pub.process_programmatic_settings(
479        settings_spec, settings_overrides, config_section)
480    pub.set_source(source, source_path)
481    pub.set_destination(None, None)
482    output = pub.publish(enable_exit_status=enable_exit_status)
483    return pub.document
484
485def publish_from_doctree(document, destination_path=None,
486                         writer=None, writer_name='pseudoxml',
487                         settings=None, settings_spec=None,
488                         settings_overrides=None, config_section=None,
489                         enable_exit_status=False):
490    """
491    Set up & run a `Publisher` to render from an existing document
492    tree data structure, for programmatic use with string I/O.  Return
493    the encoded string output.
494
495    Note that document.settings is overridden; if you want to use the settings
496    of the original `document`, pass settings=document.settings.
497
498    Also, new document.transformer and document.reporter objects are
499    generated.
500
501    For encoded string output, be sure to set the 'output_encoding' setting to
502    the desired encoding.  Set it to 'unicode' for unencoded Unicode string
503    output.  Here's one way::
504
505        publish_from_doctree(
506            ..., settings_overrides={'output_encoding': 'unicode'})
507
508    Parameters: `document` is a `docutils.nodes.document` object, an existing
509    document tree.
510
511    Other parameters: see `publish_programmatically`.
512    """
513    reader = docutils.readers.doctree.Reader(parser_name='null')
514    pub = Publisher(reader, None, writer,
515                    source=io.DocTreeInput(document),
516                    destination_class=io.StringOutput, settings=settings)
517    if not writer and writer_name:
518        pub.set_writer(writer_name)
519    pub.process_programmatic_settings(
520        settings_spec, settings_overrides, config_section)
521    pub.set_destination(None, destination_path)
522    return pub.publish(enable_exit_status=enable_exit_status)
523
524def publish_cmdline_to_binary(reader=None, reader_name='standalone',
525                    parser=None, parser_name='restructuredtext',
526                    writer=None, writer_name='pseudoxml',
527                    settings=None, settings_spec=None,
528                    settings_overrides=None, config_section=None,
529                    enable_exit_status=True, argv=None,
530                    usage=default_usage, description=default_description,
531                    destination=None, destination_class=io.BinaryFileOutput
532                    ):
533    """
534    Set up & run a `Publisher` for command-line-based file I/O (input and
535    output file paths taken automatically from the command line).  Return the
536    encoded string output also.
537
538    This is just like publish_cmdline, except that it uses
539    io.BinaryFileOutput instead of io.FileOutput.
540
541    Parameters: see `publish_programmatically` for the remainder.
542
543    - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``.
544    - `usage`: Usage string, output if there's a problem parsing the command
545      line.
546    - `description`: Program description, output for the "--help" option
547      (along with command-line option descriptions).
548    """
549    pub = Publisher(reader, parser, writer, settings=settings,
550        destination_class=destination_class)
551    pub.set_components(reader_name, parser_name, writer_name)
552    output = pub.publish(
553        argv, usage, description, settings_spec, settings_overrides,
554        config_section=config_section, enable_exit_status=enable_exit_status)
555    return output
556
557def publish_programmatically(source_class, source, source_path,
558                             destination_class, destination, destination_path,
559                             reader, reader_name,
560                             parser, parser_name,
561                             writer, writer_name,
562                             settings, settings_spec,
563                             settings_overrides, config_section,
564                             enable_exit_status):
565    """
566    Set up & run a `Publisher` for custom programmatic use.  Return the
567    encoded string output and the Publisher object.
568
569    Applications should not need to call this function directly.  If it does
570    seem to be necessary to call this function directly, please write to the
571    Docutils-develop mailing list
572    <http://docutils.sf.net/docs/user/mailing-lists.html#docutils-develop>.
573
574    Parameters:
575
576    * `source_class` **required**: The class for dynamically created source
577      objects.  Typically `io.FileInput` or `io.StringInput`.
578
579    * `source`: Type depends on `source_class`:
580
581      - If `source_class` is `io.FileInput`: Either a file-like object
582        (must have 'read' and 'close' methods), or ``None``
583        (`source_path` is opened).  If neither `source` nor
584        `source_path` are supplied, `sys.stdin` is used.
585
586      - If `source_class` is `io.StringInput` **required**: The input
587        string, either an encoded 8-bit string (set the
588        'input_encoding' setting to the correct encoding) or a Unicode
589        string (set the 'input_encoding' setting to 'unicode').
590
591    * `source_path`: Type depends on `source_class`:
592
593      - `io.FileInput`: Path to the input file, opened if no `source`
594        supplied.
595
596      - `io.StringInput`: Optional.  Path to the file or object that produced
597        `source`.  Only used for diagnostic output.
598
599    * `destination_class` **required**: The class for dynamically created
600      destination objects.  Typically `io.FileOutput` or `io.StringOutput`.
601
602    * `destination`: Type depends on `destination_class`:
603
604      - `io.FileOutput`: Either a file-like object (must have 'write' and
605        'close' methods), or ``None`` (`destination_path` is opened).  If
606        neither `destination` nor `destination_path` are supplied,
607        `sys.stdout` is used.
608
609      - `io.StringOutput`: Not used; pass ``None``.
610
611    * `destination_path`: Type depends on `destination_class`:
612
613      - `io.FileOutput`: Path to the output file.  Opened if no `destination`
614        supplied.
615
616      - `io.StringOutput`: Path to the file or object which will receive the
617        output; optional.  Used for determining relative paths (stylesheets,
618        source links, etc.).
619
620    * `reader`: A `docutils.readers.Reader` object.
621
622    * `reader_name`: Name or alias of the Reader class to be instantiated if
623      no `reader` supplied.
624
625    * `parser`: A `docutils.parsers.Parser` object.
626
627    * `parser_name`: Name or alias of the Parser class to be instantiated if
628      no `parser` supplied.
629
630    * `writer`: A `docutils.writers.Writer` object.
631
632    * `writer_name`: Name or alias of the Writer class to be instantiated if
633      no `writer` supplied.
634
635    * `settings`: A runtime settings (`docutils.frontend.Values`) object, for
636      dotted-attribute access to runtime settings.  It's the end result of the
637      `SettingsSpec`, config file, and option processing.  If `settings` is
638      passed, it's assumed to be complete and no further setting/config/option
639      processing is done.
640
641    * `settings_spec`: A `docutils.SettingsSpec` subclass or object.  Provides
642      extra application-specific settings definitions independently of
643      components.  In other words, the application becomes a component, and
644      its settings data is processed along with that of the other components.
645      Used only if no `settings` specified.
646
647    * `settings_overrides`: A dictionary containing application-specific
648      settings defaults that override the defaults of other components.
649      Used only if no `settings` specified.
650
651    * `config_section`: A string, the name of the configuration file section
652      for this application.  Overrides the ``config_section`` attribute
653      defined by `settings_spec`.  Used only if no `settings` specified.
654
655    * `enable_exit_status`: Boolean; enable exit status at end of processing?
656    """
657    pub = Publisher(reader, parser, writer, settings=settings,
658                    source_class=source_class,
659                    destination_class=destination_class)
660    pub.set_components(reader_name, parser_name, writer_name)
661    pub.process_programmatic_settings(
662        settings_spec, settings_overrides, config_section)
663    pub.set_source(source, source_path)
664    pub.set_destination(destination, destination_path)
665    output = pub.publish(enable_exit_status=enable_exit_status)
666    return output, pub
667