comparison planemo/lib/python3.7/site-packages/docutils/utils/__init__.py @ 0:d30785e31577 draft

"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author guerler
date Fri, 31 Jul 2020 00:18:57 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:d30785e31577
1 # coding: utf-8
2 # $Id: __init__.py 8376 2019-08-27 19:49:29Z milde $
3 # Author: David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
5
6 """
7 Miscellaneous utilities for the documentation utilities.
8 """
9
10 __docformat__ = 'reStructuredText'
11
12 import sys
13 import os
14 import os.path
15 import re
16 import itertools
17 import warnings
18 import unicodedata
19 from docutils import ApplicationError, DataError, __version_info__
20 from docutils import nodes
21 from docutils.nodes import unescape
22 import docutils.io
23 from docutils.utils.error_reporting import ErrorOutput, SafeString
24
25 if sys.version_info >= (3, 0):
26 unicode = str
27
28
29 class SystemMessage(ApplicationError):
30
31 def __init__(self, system_message, level):
32 Exception.__init__(self, system_message.astext())
33 self.level = level
34
35
36 class SystemMessagePropagation(ApplicationError): pass
37
38
39 class Reporter(object):
40
41 """
42 Info/warning/error reporter and ``system_message`` element generator.
43
44 Five levels of system messages are defined, along with corresponding
45 methods: `debug()`, `info()`, `warning()`, `error()`, and `severe()`.
46
47 There is typically one Reporter object per process. A Reporter object is
48 instantiated with thresholds for reporting (generating warnings) and
49 halting processing (raising exceptions), a switch to turn debug output on
50 or off, and an I/O stream for warnings. These are stored as instance
51 attributes.
52
53 When a system message is generated, its level is compared to the stored
54 thresholds, and a warning or error is generated as appropriate. Debug
55 messages are produced if the stored debug switch is on, independently of
56 other thresholds. Message output is sent to the stored warning stream if
57 not set to ''.
58
59 The Reporter class also employs a modified form of the "Observer" pattern
60 [GoF95]_ to track system messages generated. The `attach_observer` method
61 should be called before parsing, with a bound method or function which
62 accepts system messages. The observer can be removed with
63 `detach_observer`, and another added in its place.
64
65 .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
66 Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
67 1995.
68 """
69
70 levels = 'DEBUG INFO WARNING ERROR SEVERE'.split()
71 """List of names for system message levels, indexed by level."""
72
73 # system message level constants:
74 (DEBUG_LEVEL,
75 INFO_LEVEL,
76 WARNING_LEVEL,
77 ERROR_LEVEL,
78 SEVERE_LEVEL) = range(5)
79
80 def __init__(self, source, report_level, halt_level, stream=None,
81 debug=False, encoding=None, error_handler='backslashreplace'):
82 """
83 :Parameters:
84 - `source`: The path to or description of the source data.
85 - `report_level`: The level at or above which warning output will
86 be sent to `stream`.
87 - `halt_level`: The level at or above which `SystemMessage`
88 exceptions will be raised, halting execution.
89 - `debug`: Show debug (level=0) system messages?
90 - `stream`: Where warning output is sent. Can be file-like (has a
91 ``.write`` method), a string (file name, opened for writing),
92 '' (empty string) or `False` (for discarding all stream messages)
93 or `None` (implies `sys.stderr`; default).
94 - `encoding`: The output encoding.
95 - `error_handler`: The error handler for stderr output encoding.
96 """
97
98 self.source = source
99 """The path to or description of the source data."""
100
101 self.error_handler = error_handler
102 """The character encoding error handler."""
103
104 self.debug_flag = debug
105 """Show debug (level=0) system messages?"""
106
107 self.report_level = report_level
108 """The level at or above which warning output will be sent
109 to `self.stream`."""
110
111 self.halt_level = halt_level
112 """The level at or above which `SystemMessage` exceptions
113 will be raised, halting execution."""
114
115 if not isinstance(stream, ErrorOutput):
116 stream = ErrorOutput(stream, encoding, error_handler)
117
118 self.stream = stream
119 """Where warning output is sent."""
120
121 self.encoding = encoding or getattr(stream, 'encoding', 'ascii')
122 """The output character encoding."""
123
124 self.observers = []
125 """List of bound methods or functions to call with each system_message
126 created."""
127
128 self.max_level = -1
129 """The highest level system message generated so far."""
130
131 def set_conditions(self, category, report_level, halt_level,
132 stream=None, debug=False):
133 warnings.warn('docutils.utils.Reporter.set_conditions deprecated; '
134 'set attributes via configuration settings or directly',
135 DeprecationWarning, stacklevel=2)
136 self.report_level = report_level
137 self.halt_level = halt_level
138 if not isinstance(stream, ErrorOutput):
139 stream = ErrorOutput(stream, self.encoding, self.error_handler)
140 self.stream = stream
141 self.debug_flag = debug
142
143 def attach_observer(self, observer):
144 """
145 The `observer` parameter is a function or bound method which takes one
146 argument, a `nodes.system_message` instance.
147 """
148 self.observers.append(observer)
149
150 def detach_observer(self, observer):
151 self.observers.remove(observer)
152
153 def notify_observers(self, message):
154 for observer in self.observers:
155 observer(message)
156
157 def system_message(self, level, message, *children, **kwargs):
158 """
159 Return a system_message object.
160
161 Raise an exception or generate a warning if appropriate.
162 """
163 # `message` can be a `string`, `unicode`, or `Exception` instance.
164 if isinstance(message, Exception):
165 message = SafeString(message)
166
167 attributes = kwargs.copy()
168 if 'base_node' in kwargs:
169 source, line = get_source_line(kwargs['base_node'])
170 del attributes['base_node']
171 if source is not None:
172 attributes.setdefault('source', source)
173 if line is not None:
174 attributes.setdefault('line', line)
175 # assert source is not None, "node has line- but no source-argument"
176 if not 'source' in attributes: # 'line' is absolute line number
177 try: # look up (source, line-in-source)
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)
188
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
202
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)
211
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)
218
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)
225
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)
232
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)
240
241
242 class ExtensionOptionError(DataError): pass
243 class BadOptionError(ExtensionOptionError): pass
244 class BadOptionDataError(ExtensionOptionError): pass
245 class DuplicateOptionError(ExtensionOptionError): pass
246
247
248 def extract_extension_options(field_list, options_spec):
249 """
250 Return a dictionary mapping extension option names to converted values.
251
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`.
257
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 option_dict = assemble_option_dict(option_list, options_spec)
271 return option_dict
272
273 def extract_options(field_list):
274 """
275 Return a list of option (name, value) pairs from field names & bodies.
276
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.
280
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 or not isinstance(body[0], nodes.paragraph) \
296 or len(body[0]) != 1 or not isinstance(body[0][0], nodes.Text):
297 raise BadOptionDataError(
298 'extension option field body may contain\n'
299 'a single paragraph only (option "%s")' % name)
300 else:
301 data = body[0][0].astext()
302 option_list.append((name, data))
303 return option_list
304
305 def assemble_option_dict(option_list, options_spec):
306 """
307 Return a mapping of option names to values.
308
309 :Parameters:
310 - `option_list`: A list of (name, value) pairs (the output of
311 `extract_options()`).
312 - `options_spec`: Dictionary mapping known option names to a
313 conversion function such as `int` or `float`.
314
315 :Exceptions:
316 - `KeyError` for unknown option names.
317 - `DuplicateOptionError` for duplicate options.
318 - `ValueError` for invalid option values (raised by conversion
319 function).
320 - `TypeError` for invalid option value types (raised by conversion
321 function).
322 """
323 options = {}
324 for name, value in option_list:
325 convertor = options_spec[name] # raises KeyError if unknown
326 if convertor is None:
327 raise KeyError(name) # or if explicitly disabled
328 if name in options:
329 raise DuplicateOptionError('duplicate option "%s"' % name)
330 try:
331 options[name] = convertor(value)
332 except (ValueError, TypeError) as detail:
333 raise detail.__class__('(option: "%s"; value: %r)\n%s'
334 % (name, value, ' '.join(detail.args)))
335 return options
336
337
338 class NameValueError(DataError): pass
339
340
341 def decode_path(path):
342 """
343 Ensure `path` is Unicode. Return `nodes.reprunicode` object.
344
345 Decode file/path string in a failsave manner if not already done.
346 """
347 # see also http://article.gmane.org/gmane.text.docutils.user/2905
348 if isinstance(path, unicode):
349 return path
350 try:
351 path = path.decode(sys.getfilesystemencoding(), 'strict')
352 except AttributeError: # default value None has no decode method
353 return nodes.reprunicode(path)
354 except UnicodeDecodeError:
355 try:
356 path = path.decode('utf-8', 'strict')
357 except UnicodeDecodeError:
358 path = path.decode('ascii', 'replace')
359 return nodes.reprunicode(path)
360
361
362 def extract_name_value(line):
363 """
364 Return a list of (name, value) from a line of the form "name=value ...".
365
366 :Exception:
367 `NameValueError` for invalid input (missing name, missing data, bad
368 quotes, etc.).
369 """
370 attlist = []
371 while line:
372 equals = line.find('=')
373 if equals == -1:
374 raise NameValueError('missing "="')
375 attname = line[:equals].strip()
376 if equals == 0 or not attname:
377 raise NameValueError(
378 'missing attribute name before "="')
379 line = line[equals+1:].lstrip()
380 if not line:
381 raise NameValueError(
382 'missing value after "%s="' % attname)
383 if line[0] in '\'"':
384 endquote = line.find(line[0], 1)
385 if endquote == -1:
386 raise NameValueError(
387 'attribute "%s" missing end quote (%s)'
388 % (attname, line[0]))
389 if len(line) > endquote + 1 and line[endquote + 1].strip():
390 raise NameValueError(
391 'attribute "%s" end quote (%s) not followed by '
392 'whitespace' % (attname, line[0]))
393 data = line[1:endquote]
394 line = line[endquote+1:].lstrip()
395 else:
396 space = line.find(' ')
397 if space == -1:
398 data = line
399 line = ''
400 else:
401 data = line[:space]
402 line = line[space+1:].lstrip()
403 attlist.append((attname.lower(), data))
404 return attlist
405
406 def new_reporter(source_path, settings):
407 """
408 Return a new Reporter object.
409
410 :Parameters:
411 `source` : string
412 The path to or description of the source text of the document.
413 `settings` : optparse.Values object
414 Runtime settings.
415 """
416 reporter = Reporter(
417 source_path, settings.report_level, settings.halt_level,
418 stream=settings.warning_stream, debug=settings.debug,
419 encoding=settings.error_encoding,
420 error_handler=settings.error_encoding_error_handler)
421 return reporter
422
423 def new_document(source_path, settings=None):
424 """
425 Return a new empty document object.
426
427 :Parameters:
428 `source_path` : string
429 The path to or description of the source text of the document.
430 `settings` : optparse.Values object
431 Runtime settings. If none are provided, a default core set will
432 be used. If you will use the document object with any Docutils
433 components, you must provide their default settings as well. For
434 example, if parsing, at least provide the parser settings,
435 obtainable as follows::
436
437 settings = docutils.frontend.OptionParser(
438 components=(docutils.parsers.rst.Parser,)
439 ).get_default_values()
440 """
441 from docutils import frontend
442 if settings is None:
443 settings = frontend.OptionParser().get_default_values()
444 source_path = decode_path(source_path)
445 reporter = new_reporter(source_path, settings)
446 document = nodes.document(settings, reporter, source=source_path)
447 document.note_source(source_path, -1)
448 return document
449
450 def clean_rcs_keywords(paragraph, keyword_substitutions):
451 if len(paragraph) == 1 and isinstance(paragraph[0], nodes.Text):
452 textnode = paragraph[0]
453 for pattern, substitution in keyword_substitutions:
454 match = pattern.search(textnode)
455 if match:
456 paragraph[0] = nodes.Text(pattern.sub(substitution, textnode))
457 return
458
459 def relative_path(source, target):
460 """
461 Build and return a path to `target`, relative to `source` (both files).
462
463 If there is no common prefix, return the absolute path to `target`.
464 """
465 source_parts = os.path.abspath(source or type(target)('dummy_file')
466 ).split(os.sep)
467 target_parts = os.path.abspath(target).split(os.sep)
468 # Check first 2 parts because '/dir'.split('/') == ['', 'dir']:
469 if source_parts[:2] != target_parts[:2]:
470 # Nothing in common between paths.
471 # Return absolute path, using '/' for URLs:
472 return '/'.join(target_parts)
473 source_parts.reverse()
474 target_parts.reverse()
475 while (source_parts and target_parts
476 and source_parts[-1] == target_parts[-1]):
477 # Remove path components in common:
478 source_parts.pop()
479 target_parts.pop()
480 target_parts.reverse()
481 parts = ['..'] * (len(source_parts) - 1) + target_parts
482 return '/'.join(parts)
483
484 def get_stylesheet_reference(settings, relative_to=None):
485 """
486 Retrieve a stylesheet reference from the settings object.
487
488 Deprecated. Use get_stylesheet_list() instead to
489 enable specification of multiple stylesheets as a comma-separated
490 list.
491 """
492 if settings.stylesheet_path:
493 assert not settings.stylesheet, (
494 'stylesheet and stylesheet_path are mutually exclusive.')
495 if relative_to == None:
496 relative_to = settings._destination
497 return relative_path(relative_to, settings.stylesheet_path)
498 else:
499 return settings.stylesheet
500
501 # Return 'stylesheet' or 'stylesheet_path' arguments as list.
502 #
503 # The original settings arguments are kept unchanged: you can test
504 # with e.g. ``if settings.stylesheet_path:``
505 #
506 # Differences to ``get_stylesheet_reference``:
507 # * return value is a list
508 # * no re-writing of the path (and therefore no optional argument)
509 # (if required, use ``utils.relative_path(source, target)``
510 # in the calling script)
511 def get_stylesheet_list(settings):
512 """
513 Retrieve list of stylesheet references from the settings object.
514 """
515 assert not (settings.stylesheet and settings.stylesheet_path), (
516 'stylesheet and stylesheet_path are mutually exclusive.')
517 stylesheets = settings.stylesheet_path or settings.stylesheet or []
518 # programmatically set default can be string or unicode:
519 if not isinstance(stylesheets, list):
520 stylesheets = [path.strip() for path in stylesheets.split(',')]
521 # expand relative paths if found in stylesheet-dirs:
522 return [find_file_in_dirs(path, settings.stylesheet_dirs)
523 for path in stylesheets]
524
525 def find_file_in_dirs(path, dirs):
526 """
527 Search for `path` in the list of directories `dirs`.
528
529 Return the first expansion that matches an existing file.
530 """
531 if os.path.isabs(path):
532 return path
533 for d in dirs:
534 if d == '.':
535 f = path
536 else:
537 d = os.path.expanduser(d)
538 f = os.path.join(d, path)
539 if os.path.exists(f):
540 return f
541 return path
542
543 def get_trim_footnote_ref_space(settings):
544 """
545 Return whether or not to trim footnote space.
546
547 If trim_footnote_reference_space is not None, return it.
548
549 If trim_footnote_reference_space is None, return False unless the
550 footnote reference style is 'superscript'.
551 """
552 if settings.trim_footnote_reference_space is None:
553 return hasattr(settings, 'footnote_references') and \
554 settings.footnote_references == 'superscript'
555 else:
556 return settings.trim_footnote_reference_space
557
558 def get_source_line(node):
559 """
560 Return the "source" and "line" attributes from the `node` given or from
561 its closest ancestor.
562 """
563 while node:
564 if node.source or node.line:
565 return node.source, node.line
566 node = node.parent
567 return None, None
568
569 def escape2null(text):
570 """Return a string with escape-backslashes converted to nulls."""
571 parts = []
572 start = 0
573 while True:
574 found = text.find('\\', start)
575 if found == -1:
576 parts.append(text[start:])
577 return ''.join(parts)
578 parts.append(text[start:found])
579 parts.append('\x00' + text[found+1:found+2])
580 start = found + 2 # skip character after escape
581
582 # `unescape` definition moved to `nodes` to avoid circular import dependency.
583
584 def split_escaped_whitespace(text):
585 """
586 Split `text` on escaped whitespace (null+space or null+newline).
587 Return a list of strings.
588 """
589 strings = text.split('\x00 ')
590 strings = [string.split('\x00\n') for string in strings]
591 # flatten list of lists of strings to list of strings:
592 return list(itertools.chain(*strings))
593
594 def strip_combining_chars(text):
595 if isinstance(text, str) and sys.version_info < (3, 0):
596 return text
597 return u''.join([c for c in text if not unicodedata.combining(c)])
598
599 def find_combining_chars(text):
600 """Return indices of all combining chars in Unicode string `text`.
601
602 >>> from docutils.utils import find_combining_chars
603 >>> find_combining_chars(u'A t̆ab̆lĕ')
604 [3, 6, 9]
605
606 """
607 if isinstance(text, str) and sys.version_info < (3, 0):
608 return []
609 return [i for i,c in enumerate(text) if unicodedata.combining(c)]
610
611 def column_indices(text):
612 """Indices of Unicode string `text` when skipping combining characters.
613
614 >>> from docutils.utils import column_indices
615 >>> column_indices(u'A t̆ab̆lĕ')
616 [0, 1, 2, 4, 5, 7, 8]
617
618 """
619 # TODO: account for asian wide chars here instead of using dummy
620 # replacements in the tableparser?
621 string_indices = list(range(len(text)))
622 for index in find_combining_chars(text):
623 string_indices[index] = None
624 return [i for i in string_indices if i is not None]
625
626 east_asian_widths = {'W': 2, # Wide
627 'F': 2, # Full-width (wide)
628 'Na': 1, # Narrow
629 'H': 1, # Half-width (narrow)
630 'N': 1, # Neutral (not East Asian, treated as narrow)
631 'A': 1} # Ambiguous (s/b wide in East Asian context,
632 # narrow otherwise, but that doesn't work)
633 """Mapping of result codes from `unicodedata.east_asian_widt()` to character
634 column widths."""
635
636 def column_width(text):
637 """Return the column width of text.
638
639 Correct ``len(text)`` for wide East Asian and combining Unicode chars.
640 """
641 if isinstance(text, str) and sys.version_info < (3, 0):
642 return len(text)
643 width = sum([east_asian_widths[unicodedata.east_asian_width(c)]
644 for c in text])
645 # correction for combining chars:
646 width -= len(find_combining_chars(text))
647 return width
648
649 def uniq(L):
650 r = []
651 for item in L:
652 if not item in r:
653 r.append(item)
654 return r
655
656 def unique_combinations(items, n):
657 """Return `itertools.combinations`."""
658 warnings.warn('docutils.utils.unique_combinations is deprecated; '
659 'use itertools.combinations directly.',
660 DeprecationWarning, stacklevel=2)
661 return itertools.combinations(items, n)
662
663 def normalize_language_tag(tag):
664 """Return a list of normalized combinations for a `BCP 47` language tag.
665
666 Example:
667
668 >>> from docutils.utils import normalize_language_tag
669 >>> normalize_language_tag('de_AT-1901')
670 ['de-at-1901', 'de-at', 'de-1901', 'de']
671 >>> normalize_language_tag('de-CH-x_altquot')
672 ['de-ch-x-altquot', 'de-ch', 'de-x-altquot', 'de']
673
674 """
675 # normalize:
676 tag = tag.lower().replace('-', '_')
677 # split (except singletons, which mark the following tag as non-standard):
678 tag = re.sub(r'_([a-zA-Z0-9])_', r'_\1-', tag)
679 subtags = [subtag for subtag in tag.split('_')]
680 base_tag = (subtags.pop(0),)
681 # find all combinations of subtags
682 taglist = []
683 for n in range(len(subtags), 0, -1):
684 # for tags in unique_combinations(subtags, n):
685 for tags in itertools.combinations(subtags, n):
686 taglist.append('-'.join(base_tag+tags))
687 taglist += base_tag
688 return taglist
689
690
691 class DependencyList(object):
692
693 """
694 List of dependencies, with file recording support.
695
696 Note that the output file is not automatically closed. You have
697 to explicitly call the close() method.
698 """
699
700 def __init__(self, output_file=None, dependencies=[]):
701 """
702 Initialize the dependency list, automatically setting the
703 output file to `output_file` (see `set_output()`) and adding
704 all supplied dependencies.
705 """
706 self.set_output(output_file)
707 for i in dependencies:
708 self.add(i)
709
710 def set_output(self, output_file):
711 """
712 Set the output file and clear the list of already added
713 dependencies.
714
715 `output_file` must be a string. The specified file is
716 immediately overwritten.
717
718 If output_file is '-', the output will be written to stdout.
719 If it is None, no file output is done when calling add().
720 """
721 self.list = []
722 if output_file:
723 if output_file == '-':
724 of = None
725 else:
726 of = output_file
727 self.file = docutils.io.FileOutput(destination_path=of,
728 encoding='utf8', autoclose=False)
729 else:
730 self.file = None
731
732 def add(self, *filenames):
733 """
734 If the dependency `filename` has not already been added,
735 append it to self.list and print it to self.file if self.file
736 is not None.
737 """
738 for filename in filenames:
739 if not filename in self.list:
740 self.list.append(filename)
741 if self.file is not None:
742 self.file.write(filename+'\n')
743
744 def close(self):
745 """
746 Close the output file.
747 """
748 self.file.close()
749 self.file = None
750
751 def __repr__(self):
752 try:
753 output_file = self.file.name
754 except AttributeError:
755 output_file = None
756 return '%s(%r, %s)' % (self.__class__.__name__, output_file, self.list)
757
758
759 release_level_abbreviations = {
760 'alpha': 'a',
761 'beta': 'b',
762 'candidate': 'rc',
763 'final': '',}
764
765 def version_identifier(version_info=None):
766 """
767 Return a version identifier string built from `version_info`, a
768 `docutils.VersionInfo` namedtuple instance or compatible tuple. If
769 `version_info` is not provided, by default return a version identifier
770 string based on `docutils.__version_info__` (i.e. the current Docutils
771 version).
772 """
773 if version_info is None:
774 version_info = __version_info__
775 if version_info.micro:
776 micro = '.%s' % version_info.micro
777 else:
778 # 0 is omitted:
779 micro = ''
780 releaselevel = release_level_abbreviations[version_info.releaselevel]
781 if version_info.serial:
782 serial = version_info.serial
783 else:
784 # 0 is omitted:
785 serial = ''
786 if version_info.release:
787 dev = ''
788 else:
789 dev = '.dev'
790 version = '%s.%s%s%s%s%s' % (
791 version_info.major,
792 version_info.minor,
793 micro,
794 releaselevel,
795 serial,
796 dev)
797 return version