Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/docutils/utils/__init__.py: 51%
333 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:06 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:06 +0000
1# $Id$
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
5"""
6Miscellaneous utilities for the documentation utilities.
7"""
9__docformat__ = 'reStructuredText'
11import sys
12import os
13import os.path
14from pathlib import PurePath, Path
15import re
16import itertools
17import warnings
18import unicodedata
20from docutils import ApplicationError, DataError, __version_info__
21from docutils import io, nodes
22# for backwards compatibility
23from docutils.nodes import unescape # noqa: F401
26class SystemMessage(ApplicationError):
28 def __init__(self, system_message, level):
29 Exception.__init__(self, system_message.astext())
30 self.level = level
33class SystemMessagePropagation(ApplicationError):
34 pass
37class Reporter:
39 """
40 Info/warning/error reporter and ``system_message`` element generator.
42 Five levels of system messages are defined, along with corresponding
43 methods: `debug()`, `info()`, `warning()`, `error()`, and `severe()`.
45 There is typically one Reporter object per process. A Reporter object is
46 instantiated with thresholds for reporting (generating warnings) and
47 halting processing (raising exceptions), a switch to turn debug output on
48 or off, and an I/O stream for warnings. These are stored as instance
49 attributes.
51 When a system message is generated, its level is compared to the stored
52 thresholds, and a warning or error is generated as appropriate. Debug
53 messages are produced if the stored debug switch is on, independently of
54 other thresholds. Message output is sent to the stored warning stream if
55 not set to ''.
57 The Reporter class also employs a modified form of the "Observer" pattern
58 [GoF95]_ to track system messages generated. The `attach_observer` method
59 should be called before parsing, with a bound method or function which
60 accepts system messages. The observer can be removed with
61 `detach_observer`, and another added in its place.
63 .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
64 Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
65 1995.
66 """
68 levels = 'DEBUG INFO WARNING ERROR SEVERE'.split()
69 """List of names for system message levels, indexed by level."""
71 # system message level constants:
72 (DEBUG_LEVEL,
73 INFO_LEVEL,
74 WARNING_LEVEL,
75 ERROR_LEVEL,
76 SEVERE_LEVEL) = range(5)
78 def __init__(self, source, report_level, halt_level, stream=None,
79 debug=False, encoding=None, error_handler='backslashreplace'):
80 """
81 :Parameters:
82 - `source`: The path to or description of the source data.
83 - `report_level`: The level at or above which warning output will
84 be sent to `stream`.
85 - `halt_level`: The level at or above which `SystemMessage`
86 exceptions will be raised, halting execution.
87 - `debug`: Show debug (level=0) system messages?
88 - `stream`: Where warning output is sent. Can be file-like (has a
89 ``.write`` method), a string (file name, opened for writing),
90 '' (empty string) or `False` (for discarding all stream messages)
91 or `None` (implies `sys.stderr`; default).
92 - `encoding`: The output encoding.
93 - `error_handler`: The error handler for stderr output encoding.
94 """
96 self.source = source
97 """The path to or description of the source data."""
99 self.error_handler = error_handler
100 """The character encoding error handler."""
102 self.debug_flag = debug
103 """Show debug (level=0) system messages?"""
105 self.report_level = report_level
106 """The level at or above which warning output will be sent
107 to `self.stream`."""
109 self.halt_level = halt_level
110 """The level at or above which `SystemMessage` exceptions
111 will be raised, halting execution."""
113 if not isinstance(stream, io.ErrorOutput):
114 stream = io.ErrorOutput(stream, encoding, error_handler)
116 self.stream = stream
117 """Where warning output is sent."""
119 self.encoding = encoding or getattr(stream, 'encoding', 'ascii')
120 """The output character encoding."""
122 self.observers = []
123 """List of bound methods or functions to call with each system_message
124 created."""
126 self.max_level = -1
127 """The highest level system message generated so far."""
129 def set_conditions(self, category, report_level, halt_level,
130 stream=None, debug=False):
131 warnings.warn('docutils.utils.Reporter.set_conditions() deprecated; '
132 'Will be removed in Docutils 0.21 or later. '
133 'Set attributes via configuration settings or directly.',
134 DeprecationWarning, stacklevel=2)
135 self.report_level = report_level
136 self.halt_level = halt_level
137 if not isinstance(stream, io.ErrorOutput):
138 stream = io.ErrorOutput(stream, self.encoding, self.error_handler)
139 self.stream = stream
140 self.debug_flag = debug
142 def attach_observer(self, observer):
143 """
144 The `observer` parameter is a function or bound method which takes one
145 argument, a `nodes.system_message` instance.
146 """
147 self.observers.append(observer)
149 def detach_observer(self, observer):
150 self.observers.remove(observer)
152 def notify_observers(self, message):
153 for observer in self.observers:
154 observer(message)
156 def system_message(self, level, message, *children, **kwargs):
157 """
158 Return a system_message object.
160 Raise an exception or generate a warning if appropriate.
161 """
162 # `message` can be a `str` or `Exception` instance.
163 if isinstance(message, Exception):
164 message = str(message)
166 attributes = kwargs.copy()
167 if 'base_node' in kwargs:
168 source, line = get_source_line(kwargs['base_node'])
169 del attributes['base_node']
170 if source is not None:
171 attributes.setdefault('source', source)
172 if line is not None:
173 attributes.setdefault('line', line)
174 # assert source is not None, "line- but no source-argument"
175 if 'source' not in attributes:
176 # 'line' is absolute line number
177 try:
178 source, line = self.get_source_and_line(attributes.get('line'))
179 except AttributeError:
180 source, line = None, None
181 if source is not None:
182 attributes['source'] = source
183 if line is not None:
184 attributes['line'] = line
185 # assert attributes['line'] is not None, (message, kwargs)
186 # assert attributes['source'] is not None, (message, kwargs)
187 attributes.setdefault('source', self.source)
189 msg = nodes.system_message(message, level=level,
190 type=self.levels[level],
191 *children, **attributes)
192 if self.stream and (level >= self.report_level
193 or self.debug_flag and level == self.DEBUG_LEVEL
194 or level >= self.halt_level):
195 self.stream.write(msg.astext() + '\n')
196 if level >= self.halt_level:
197 raise SystemMessage(msg, level)
198 if level > self.DEBUG_LEVEL or self.debug_flag:
199 self.notify_observers(msg)
200 self.max_level = max(level, self.max_level)
201 return msg
203 def debug(self, *args, **kwargs):
204 """
205 Level-0, "DEBUG": an internal reporting issue. Typically, there is no
206 effect on the processing. Level-0 system messages are handled
207 separately from the others.
208 """
209 if self.debug_flag:
210 return self.system_message(self.DEBUG_LEVEL, *args, **kwargs)
212 def info(self, *args, **kwargs):
213 """
214 Level-1, "INFO": a minor issue that can be ignored. Typically there is
215 no effect on processing, and level-1 system messages are not reported.
216 """
217 return self.system_message(self.INFO_LEVEL, *args, **kwargs)
219 def warning(self, *args, **kwargs):
220 """
221 Level-2, "WARNING": an issue that should be addressed. If ignored,
222 there may be unpredictable problems with the output.
223 """
224 return self.system_message(self.WARNING_LEVEL, *args, **kwargs)
226 def error(self, *args, **kwargs):
227 """
228 Level-3, "ERROR": an error that should be addressed. If ignored, the
229 output will contain errors.
230 """
231 return self.system_message(self.ERROR_LEVEL, *args, **kwargs)
233 def severe(self, *args, **kwargs):
234 """
235 Level-4, "SEVERE": a severe error that must be addressed. If ignored,
236 the output will contain severe errors. Typically level-4 system
237 messages are turned into exceptions which halt processing.
238 """
239 return self.system_message(self.SEVERE_LEVEL, *args, **kwargs)
242class ExtensionOptionError(DataError): pass
243class BadOptionError(ExtensionOptionError): pass
244class BadOptionDataError(ExtensionOptionError): pass
245class DuplicateOptionError(ExtensionOptionError): pass
248def extract_extension_options(field_list, options_spec):
249 """
250 Return a dictionary mapping extension option names to converted values.
252 :Parameters:
253 - `field_list`: A flat field list without field arguments, where each
254 field body consists of a single paragraph only.
255 - `options_spec`: Dictionary mapping known option names to a
256 conversion function such as `int` or `float`.
258 :Exceptions:
259 - `KeyError` for unknown option names.
260 - `ValueError` for invalid option values (raised by the conversion
261 function).
262 - `TypeError` for invalid option value types (raised by conversion
263 function).
264 - `DuplicateOptionError` for duplicate options.
265 - `BadOptionError` for invalid fields.
266 - `BadOptionDataError` for invalid option data (missing name,
267 missing data, bad quotes, etc.).
268 """
269 option_list = extract_options(field_list)
270 return assemble_option_dict(option_list, options_spec)
273def extract_options(field_list):
274 """
275 Return a list of option (name, value) pairs from field names & bodies.
277 :Parameter:
278 `field_list`: A flat field list, where each field name is a single
279 word and each field body consists of a single paragraph only.
281 :Exceptions:
282 - `BadOptionError` for invalid fields.
283 - `BadOptionDataError` for invalid option data (missing name,
284 missing data, bad quotes, etc.).
285 """
286 option_list = []
287 for field in field_list:
288 if len(field[0].astext().split()) != 1:
289 raise BadOptionError(
290 'extension option field name may not contain multiple words')
291 name = str(field[0].astext().lower())
292 body = field[1]
293 if len(body) == 0:
294 data = None
295 elif (len(body) > 1
296 or not isinstance(body[0], nodes.paragraph)
297 or len(body[0]) != 1
298 or not isinstance(body[0][0], nodes.Text)):
299 raise BadOptionDataError(
300 'extension option field body may contain\n'
301 'a single paragraph only (option "%s")' % name)
302 else:
303 data = body[0][0].astext()
304 option_list.append((name, data))
305 return option_list
308def assemble_option_dict(option_list, options_spec):
309 """
310 Return a mapping of option names to values.
312 :Parameters:
313 - `option_list`: A list of (name, value) pairs (the output of
314 `extract_options()`).
315 - `options_spec`: Dictionary mapping known option names to a
316 conversion function such as `int` or `float`.
318 :Exceptions:
319 - `KeyError` for unknown option names.
320 - `DuplicateOptionError` for duplicate options.
321 - `ValueError` for invalid option values (raised by conversion
322 function).
323 - `TypeError` for invalid option value types (raised by conversion
324 function).
325 """
326 options = {}
327 for name, value in option_list:
328 convertor = options_spec[name] # raises KeyError if unknown
329 if convertor is None:
330 raise KeyError(name) # or if explicitly disabled
331 if name in options:
332 raise DuplicateOptionError('duplicate option "%s"' % name)
333 try:
334 options[name] = convertor(value)
335 except (ValueError, TypeError) as detail:
336 raise detail.__class__('(option: "%s"; value: %r)\n%s'
337 % (name, value, ' '.join(detail.args)))
338 return options
341class NameValueError(DataError): pass
344def decode_path(path):
345 """
346 Ensure `path` is Unicode. Return `str` instance.
348 Decode file/path string in a failsafe manner if not already done.
349 """
350 # TODO: is this still required with Python 3?
351 if isinstance(path, str):
352 return path
353 try:
354 path = path.decode(sys.getfilesystemencoding(), 'strict')
355 except AttributeError: # default value None has no decode method
356 if not path:
357 return ''
358 raise ValueError('`path` value must be a String or ``None``, '
359 f'not {path!r}')
360 except UnicodeDecodeError:
361 try:
362 path = path.decode('utf-8', 'strict')
363 except UnicodeDecodeError:
364 path = path.decode('ascii', 'replace')
365 return path
368def extract_name_value(line):
369 """
370 Return a list of (name, value) from a line of the form "name=value ...".
372 :Exception:
373 `NameValueError` for invalid input (missing name, missing data, bad
374 quotes, etc.).
375 """
376 attlist = []
377 while line:
378 equals = line.find('=')
379 if equals == -1:
380 raise NameValueError('missing "="')
381 attname = line[:equals].strip()
382 if equals == 0 or not attname:
383 raise NameValueError(
384 'missing attribute name before "="')
385 line = line[equals+1:].lstrip()
386 if not line:
387 raise NameValueError(
388 'missing value after "%s="' % attname)
389 if line[0] in '\'"':
390 endquote = line.find(line[0], 1)
391 if endquote == -1:
392 raise NameValueError(
393 'attribute "%s" missing end quote (%s)'
394 % (attname, line[0]))
395 if len(line) > endquote + 1 and line[endquote + 1].strip():
396 raise NameValueError(
397 'attribute "%s" end quote (%s) not followed by '
398 'whitespace' % (attname, line[0]))
399 data = line[1:endquote]
400 line = line[endquote+1:].lstrip()
401 else:
402 space = line.find(' ')
403 if space == -1:
404 data = line
405 line = ''
406 else:
407 data = line[:space]
408 line = line[space+1:].lstrip()
409 attlist.append((attname.lower(), data))
410 return attlist
413def new_reporter(source_path, settings):
414 """
415 Return a new Reporter object.
417 :Parameters:
418 `source` : string
419 The path to or description of the source text of the document.
420 `settings` : optparse.Values object
421 Runtime settings.
422 """
423 reporter = Reporter(
424 source_path, settings.report_level, settings.halt_level,
425 stream=settings.warning_stream, debug=settings.debug,
426 encoding=settings.error_encoding,
427 error_handler=settings.error_encoding_error_handler)
428 return reporter
431def new_document(source_path, settings=None):
432 """
433 Return a new empty document object.
435 :Parameters:
436 `source_path` : string
437 The path to or description of the source text of the document.
438 `settings` : optparse.Values object
439 Runtime settings. If none are provided, a default core set will
440 be used. If you will use the document object with any Docutils
441 components, you must provide their default settings as well.
443 For example, if parsing rST, at least provide the rst-parser
444 settings, obtainable as follows:
446 Defaults for parser component::
448 settings = docutils.frontend.get_default_settings(
449 docutils.parsers.rst.Parser)
451 Defaults and configuration file customizations::
453 settings = docutils.core.Publisher(
454 parser=docutils.parsers.rst.Parser).get_settings()
456 """
457 # Import at top of module would lead to circular dependency!
458 from docutils import frontend
459 if settings is None:
460 settings = frontend.get_default_settings()
461 source_path = decode_path(source_path)
462 reporter = new_reporter(source_path, settings)
463 document = nodes.document(settings, reporter, source=source_path)
464 document.note_source(source_path, -1)
465 return document
468def clean_rcs_keywords(paragraph, keyword_substitutions):
469 if len(paragraph) == 1 and isinstance(paragraph[0], nodes.Text):
470 textnode = paragraph[0]
471 for pattern, substitution in keyword_substitutions:
472 match = pattern.search(textnode)
473 if match:
474 paragraph[0] = nodes.Text(pattern.sub(substitution, textnode))
475 return
478def relative_path(source, target):
479 """
480 Build and return a path to `target`, relative to `source` (both files).
482 Differences to `os.relpath()`:
484 * Inverse argument order.
485 * `source` expects path to a FILE (while os.relpath expects a dir)!
486 (Add a "dummy" file name if `source` points to a directory.)
487 * Always use Posix path separator ("/") for the output.
488 * Use `os.sep` for parsing the input (ignored by `os.relpath()`).
489 * If there is no common prefix, return the absolute path to `target`.
491 """
492 source_parts = os.path.abspath(source or type(target)('dummy_file')
493 ).split(os.sep)
494 target_parts = os.path.abspath(target).split(os.sep)
495 # Check first 2 parts because '/dir'.split('/') == ['', 'dir']:
496 if source_parts[:2] != target_parts[:2]:
497 # Nothing in common between paths.
498 # Return absolute path, using '/' for URLs:
499 return '/'.join(target_parts)
500 source_parts.reverse()
501 target_parts.reverse()
502 while (source_parts and target_parts
503 and source_parts[-1] == target_parts[-1]):
504 # Remove path components in common:
505 source_parts.pop()
506 target_parts.pop()
507 target_parts.reverse()
508 parts = ['..'] * (len(source_parts) - 1) + target_parts
509 return '/'.join(parts)
512def get_stylesheet_reference(settings, relative_to=None):
513 """
514 Retrieve a stylesheet reference from the settings object.
516 Deprecated. Use get_stylesheet_list() instead to
517 enable specification of multiple stylesheets as a comma-separated
518 list.
519 """
520 warnings.warn('utils.get_stylesheet_reference()'
521 ' is obsoleted by utils.get_stylesheet_list()'
522 ' and will be removed in Docutils 2.0.',
523 DeprecationWarning, stacklevel=2)
524 if settings.stylesheet_path:
525 assert not settings.stylesheet, (
526 'stylesheet and stylesheet_path are mutually exclusive.')
527 if relative_to is None:
528 relative_to = settings._destination
529 return relative_path(relative_to, settings.stylesheet_path)
530 else:
531 return settings.stylesheet
534# Return 'stylesheet' or 'stylesheet_path' arguments as list.
535#
536# The original settings arguments are kept unchanged: you can test
537# with e.g. ``if settings.stylesheet_path: ...``.
538#
539# Differences to the depracated `get_stylesheet_reference()`:
540# * return value is a list
541# * no re-writing of the path (and therefore no optional argument)
542# (if required, use ``utils.relative_path(source, target)``
543# in the calling script)
544def get_stylesheet_list(settings):
545 """
546 Retrieve list of stylesheet references from the settings object.
547 """
548 assert not (settings.stylesheet and settings.stylesheet_path), (
549 'stylesheet and stylesheet_path are mutually exclusive.')
550 stylesheets = settings.stylesheet_path or settings.stylesheet or []
551 # programmatically set default may be string with comma separated list:
552 if not isinstance(stylesheets, list):
553 stylesheets = [path.strip() for path in stylesheets.split(',')]
554 if settings.stylesheet_path:
555 # expand relative paths if found in stylesheet-dirs:
556 stylesheets = [find_file_in_dirs(path, settings.stylesheet_dirs)
557 for path in stylesheets]
558 return stylesheets
561def find_file_in_dirs(path, dirs):
562 """
563 Search for `path` in the list of directories `dirs`.
565 Return the first expansion that matches an existing file.
566 """
567 path = Path(path)
568 if path.is_absolute():
569 return path.as_posix()
570 for d in dirs:
571 f = Path(d).expanduser() / path
572 if f.exists():
573 return f.as_posix()
574 return path.as_posix()
577def get_trim_footnote_ref_space(settings):
578 """
579 Return whether or not to trim footnote space.
581 If trim_footnote_reference_space is not None, return it.
583 If trim_footnote_reference_space is None, return False unless the
584 footnote reference style is 'superscript'.
585 """
586 if settings.setdefault('trim_footnote_reference_space', None) is None:
587 return getattr(settings, 'footnote_references', None) == 'superscript'
588 else:
589 return settings.trim_footnote_reference_space
592def get_source_line(node):
593 """
594 Return the "source" and "line" attributes from the `node` given or from
595 its closest ancestor.
596 """
597 while node:
598 if node.source or node.line:
599 return node.source, node.line
600 node = node.parent
601 return None, None
604def escape2null(text):
605 """Return a string with escape-backslashes converted to nulls."""
606 parts = []
607 start = 0
608 while True:
609 found = text.find('\\', start)
610 if found == -1:
611 parts.append(text[start:])
612 return ''.join(parts)
613 parts.append(text[start:found])
614 parts.append('\x00' + text[found+1:found+2])
615 start = found + 2 # skip character after escape
618def split_escaped_whitespace(text):
619 """
620 Split `text` on escaped whitespace (null+space or null+newline).
621 Return a list of strings.
622 """
623 strings = text.split('\x00 ')
624 strings = [string.split('\x00\n') for string in strings]
625 # flatten list of lists of strings to list of strings:
626 return list(itertools.chain(*strings))
629def strip_combining_chars(text):
630 return ''.join(c for c in text if not unicodedata.combining(c))
633def find_combining_chars(text):
634 """Return indices of all combining chars in Unicode string `text`.
636 >>> from docutils.utils import find_combining_chars
637 >>> find_combining_chars('A t̆ab̆lĕ')
638 [3, 6, 9]
640 """
641 return [i for i, c in enumerate(text) if unicodedata.combining(c)]
644def column_indices(text):
645 """Indices of Unicode string `text` when skipping combining characters.
647 >>> from docutils.utils import column_indices
648 >>> column_indices('A t̆ab̆lĕ')
649 [0, 1, 2, 4, 5, 7, 8]
651 """
652 # TODO: account for asian wide chars here instead of using dummy
653 # replacements in the tableparser?
654 string_indices = list(range(len(text)))
655 for index in find_combining_chars(text):
656 string_indices[index] = None
657 return [i for i in string_indices if i is not None]
660east_asian_widths = {'W': 2, # Wide
661 'F': 2, # Full-width (wide)
662 'Na': 1, # Narrow
663 'H': 1, # Half-width (narrow)
664 'N': 1, # Neutral (not East Asian, treated as narrow)
665 'A': 1, # Ambiguous (s/b wide in East Asian context,
666 } # narrow otherwise, but that doesn't work)
667"""Mapping of result codes from `unicodedata.east_asian_widt()` to character
668column widths."""
671def column_width(text):
672 """Return the column width of text.
674 Correct ``len(text)`` for wide East Asian and combining Unicode chars.
675 """
676 width = sum(east_asian_widths[unicodedata.east_asian_width(c)]
677 for c in text)
678 # correction for combining chars:
679 width -= len(find_combining_chars(text))
680 return width
683def uniq(L):
684 r = []
685 for item in L:
686 if item not in r:
687 r.append(item)
688 return r
691def normalize_language_tag(tag):
692 """Return a list of normalized combinations for a `BCP 47` language tag.
694 Example:
696 >>> from docutils.utils import normalize_language_tag
697 >>> normalize_language_tag('de_AT-1901')
698 ['de-at-1901', 'de-at', 'de-1901', 'de']
699 >>> normalize_language_tag('de-CH-x_altquot')
700 ['de-ch-x-altquot', 'de-ch', 'de-x-altquot', 'de']
702 """
703 # normalize:
704 tag = tag.lower().replace('-', '_')
705 # split (except singletons, which mark the following tag as non-standard):
706 tag = re.sub(r'_([a-zA-Z0-9])_', r'_\1-', tag)
707 subtags = [subtag for subtag in tag.split('_')]
708 base_tag = (subtags.pop(0),)
709 # find all combinations of subtags
710 taglist = []
711 for n in range(len(subtags), 0, -1):
712 for tags in itertools.combinations(subtags, n):
713 taglist.append('-'.join(base_tag+tags))
714 taglist += base_tag
715 return taglist
718def xml_declaration(encoding=None):
719 """Return an XML text declaration.
721 Include an encoding declaration, if `encoding`
722 is not 'unicode', '', or None.
723 """
724 if encoding and encoding.lower() != 'unicode':
725 encoding_declaration = f' encoding="{encoding}"'
726 else:
727 encoding_declaration = ''
728 return f'<?xml version="1.0"{encoding_declaration}?>\n'
731class DependencyList:
733 """
734 List of dependencies, with file recording support.
736 Note that the output file is not automatically closed. You have
737 to explicitly call the close() method.
738 """
740 def __init__(self, output_file=None, dependencies=()):
741 """
742 Initialize the dependency list, automatically setting the
743 output file to `output_file` (see `set_output()`) and adding
744 all supplied dependencies.
746 If output_file is None, no file output is done when calling add().
747 """
748 self.list = []
749 self.file = None
750 if output_file:
751 self.set_output(output_file)
752 self.add(*dependencies)
754 def set_output(self, output_file):
755 """
756 Set the output file and clear the list of already added
757 dependencies.
759 `output_file` must be a string. The specified file is
760 immediately overwritten.
762 If output_file is '-', the output will be written to stdout.
763 """
764 if output_file:
765 if output_file == '-':
766 self.file = sys.stdout
767 else:
768 self.file = open(output_file, 'w', encoding='utf-8')
770 def add(self, *paths):
771 """
772 Append `path` to `self.list` unless it is already there.
774 Also append to `self.file` unless it is already there
775 or `self.file is `None`.
776 """
777 for path in paths:
778 if isinstance(path, PurePath):
779 path = path.as_posix() # use '/' as separator
780 if path not in self.list:
781 self.list.append(path)
782 if self.file is not None:
783 self.file.write(path+'\n')
785 def close(self):
786 """
787 Close the output file.
788 """
789 if self.file is not sys.stdout:
790 self.file.close()
791 self.file = None
793 def __repr__(self):
794 try:
795 output_file = self.file.name
796 except AttributeError:
797 output_file = None
798 return '%s(%r, %s)' % (self.__class__.__name__, output_file, self.list)
801release_level_abbreviations = {
802 'alpha': 'a',
803 'beta': 'b',
804 'candidate': 'rc',
805 'final': ''}
808def version_identifier(version_info=None):
809 """
810 Return a version identifier string built from `version_info`, a
811 `docutils.VersionInfo` namedtuple instance or compatible tuple. If
812 `version_info` is not provided, by default return a version identifier
813 string based on `docutils.__version_info__` (i.e. the current Docutils
814 version).
815 """
816 if version_info is None:
817 version_info = __version_info__
818 if version_info.micro:
819 micro = '.%s' % version_info.micro
820 else:
821 # 0 is omitted:
822 micro = ''
823 releaselevel = release_level_abbreviations[version_info.releaselevel]
824 if version_info.serial:
825 serial = version_info.serial
826 else:
827 # 0 is omitted:
828 serial = ''
829 if version_info.release:
830 dev = ''
831 else:
832 dev = '.dev'
833 version = '%s.%s%s%s%s%s' % (
834 version_info.major,
835 version_info.minor,
836 micro,
837 releaselevel,
838 serial,
839 dev)
840 return version