Mercurial > repos > guerler > springsuite
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 |