Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pkg_resources/_vendor/jaraco/text/__init__.py: 4%
156 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1import re
2import itertools
3import textwrap
4import functools
6try:
7 from importlib.resources import files # type: ignore
8except ImportError: # pragma: nocover
9 from pkg_resources.extern.importlib_resources import files # type: ignore
11from pkg_resources.extern.jaraco.functools import compose, method_cache
12from pkg_resources.extern.jaraco.context import ExceptionTrap
15def substitution(old, new):
16 """
17 Return a function that will perform a substitution on a string
18 """
19 return lambda s: s.replace(old, new)
22def multi_substitution(*substitutions):
23 """
24 Take a sequence of pairs specifying substitutions, and create
25 a function that performs those substitutions.
27 >>> multi_substitution(('foo', 'bar'), ('bar', 'baz'))('foo')
28 'baz'
29 """
30 substitutions = itertools.starmap(substitution, substitutions)
31 # compose function applies last function first, so reverse the
32 # substitutions to get the expected order.
33 substitutions = reversed(tuple(substitutions))
34 return compose(*substitutions)
37class FoldedCase(str):
38 """
39 A case insensitive string class; behaves just like str
40 except compares equal when the only variation is case.
42 >>> s = FoldedCase('hello world')
44 >>> s == 'Hello World'
45 True
47 >>> 'Hello World' == s
48 True
50 >>> s != 'Hello World'
51 False
53 >>> s.index('O')
54 4
56 >>> s.split('O')
57 ['hell', ' w', 'rld']
59 >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
60 ['alpha', 'Beta', 'GAMMA']
62 Sequence membership is straightforward.
64 >>> "Hello World" in [s]
65 True
66 >>> s in ["Hello World"]
67 True
69 You may test for set inclusion, but candidate and elements
70 must both be folded.
72 >>> FoldedCase("Hello World") in {s}
73 True
74 >>> s in {FoldedCase("Hello World")}
75 True
77 String inclusion works as long as the FoldedCase object
78 is on the right.
80 >>> "hello" in FoldedCase("Hello World")
81 True
83 But not if the FoldedCase object is on the left:
85 >>> FoldedCase('hello') in 'Hello World'
86 False
88 In that case, use ``in_``:
90 >>> FoldedCase('hello').in_('Hello World')
91 True
93 >>> FoldedCase('hello') > FoldedCase('Hello')
94 False
95 """
97 def __lt__(self, other):
98 return self.lower() < other.lower()
100 def __gt__(self, other):
101 return self.lower() > other.lower()
103 def __eq__(self, other):
104 return self.lower() == other.lower()
106 def __ne__(self, other):
107 return self.lower() != other.lower()
109 def __hash__(self):
110 return hash(self.lower())
112 def __contains__(self, other):
113 return super().lower().__contains__(other.lower())
115 def in_(self, other):
116 "Does self appear in other?"
117 return self in FoldedCase(other)
119 # cache lower since it's likely to be called frequently.
120 @method_cache
121 def lower(self):
122 return super().lower()
124 def index(self, sub):
125 return self.lower().index(sub.lower())
127 def split(self, splitter=' ', maxsplit=0):
128 pattern = re.compile(re.escape(splitter), re.I)
129 return pattern.split(self, maxsplit)
132# Python 3.8 compatibility
133_unicode_trap = ExceptionTrap(UnicodeDecodeError)
136@_unicode_trap.passes
137def is_decodable(value):
138 r"""
139 Return True if the supplied value is decodable (using the default
140 encoding).
142 >>> is_decodable(b'\xff')
143 False
144 >>> is_decodable(b'\x32')
145 True
146 """
147 value.decode()
150def is_binary(value):
151 r"""
152 Return True if the value appears to be binary (that is, it's a byte
153 string and isn't decodable).
155 >>> is_binary(b'\xff')
156 True
157 >>> is_binary('\xff')
158 False
159 """
160 return isinstance(value, bytes) and not is_decodable(value)
163def trim(s):
164 r"""
165 Trim something like a docstring to remove the whitespace that
166 is common due to indentation and formatting.
168 >>> trim("\n\tfoo = bar\n\t\tbar = baz\n")
169 'foo = bar\n\tbar = baz'
170 """
171 return textwrap.dedent(s).strip()
174def wrap(s):
175 """
176 Wrap lines of text, retaining existing newlines as
177 paragraph markers.
179 >>> print(wrap(lorem_ipsum))
180 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
181 eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
182 minim veniam, quis nostrud exercitation ullamco laboris nisi ut
183 aliquip ex ea commodo consequat. Duis aute irure dolor in
184 reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
185 pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
186 culpa qui officia deserunt mollit anim id est laborum.
187 <BLANKLINE>
188 Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam
189 varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus
190 magna felis sollicitudin mauris. Integer in mauris eu nibh euismod
191 gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis
192 risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue,
193 eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas
194 fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla
195 a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis,
196 neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing
197 sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque
198 nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus
199 quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis,
200 molestie eu, feugiat in, orci. In hac habitasse platea dictumst.
201 """
202 paragraphs = s.splitlines()
203 wrapped = ('\n'.join(textwrap.wrap(para)) for para in paragraphs)
204 return '\n\n'.join(wrapped)
207def unwrap(s):
208 r"""
209 Given a multi-line string, return an unwrapped version.
211 >>> wrapped = wrap(lorem_ipsum)
212 >>> wrapped.count('\n')
213 20
214 >>> unwrapped = unwrap(wrapped)
215 >>> unwrapped.count('\n')
216 1
217 >>> print(unwrapped)
218 Lorem ipsum dolor sit amet, consectetur adipiscing ...
219 Curabitur pretium tincidunt lacus. Nulla gravida orci ...
221 """
222 paragraphs = re.split(r'\n\n+', s)
223 cleaned = (para.replace('\n', ' ') for para in paragraphs)
224 return '\n'.join(cleaned)
229class Splitter(object):
230 """object that will split a string with the given arguments for each call
232 >>> s = Splitter(',')
233 >>> s('hello, world, this is your, master calling')
234 ['hello', ' world', ' this is your', ' master calling']
235 """
237 def __init__(self, *args):
238 self.args = args
240 def __call__(self, s):
241 return s.split(*self.args)
244def indent(string, prefix=' ' * 4):
245 """
246 >>> indent('foo')
247 ' foo'
248 """
249 return prefix + string
252class WordSet(tuple):
253 """
254 Given an identifier, return the words that identifier represents,
255 whether in camel case, underscore-separated, etc.
257 >>> WordSet.parse("camelCase")
258 ('camel', 'Case')
260 >>> WordSet.parse("under_sep")
261 ('under', 'sep')
263 Acronyms should be retained
265 >>> WordSet.parse("firstSNL")
266 ('first', 'SNL')
268 >>> WordSet.parse("you_and_I")
269 ('you', 'and', 'I')
271 >>> WordSet.parse("A simple test")
272 ('A', 'simple', 'test')
274 Multiple caps should not interfere with the first cap of another word.
276 >>> WordSet.parse("myABCClass")
277 ('my', 'ABC', 'Class')
279 The result is a WordSet, so you can get the form you need.
281 >>> WordSet.parse("myABCClass").underscore_separated()
282 'my_ABC_Class'
284 >>> WordSet.parse('a-command').camel_case()
285 'ACommand'
287 >>> WordSet.parse('someIdentifier').lowered().space_separated()
288 'some identifier'
290 Slices of the result should return another WordSet.
292 >>> WordSet.parse('taken-out-of-context')[1:].underscore_separated()
293 'out_of_context'
295 >>> WordSet.from_class_name(WordSet()).lowered().space_separated()
296 'word set'
298 >>> example = WordSet.parse('figured it out')
299 >>> example.headless_camel_case()
300 'figuredItOut'
301 >>> example.dash_separated()
302 'figured-it-out'
304 """
306 _pattern = re.compile('([A-Z]?[a-z]+)|([A-Z]+(?![a-z]))')
308 def capitalized(self):
309 return WordSet(word.capitalize() for word in self)
311 def lowered(self):
312 return WordSet(word.lower() for word in self)
314 def camel_case(self):
315 return ''.join(self.capitalized())
317 def headless_camel_case(self):
318 words = iter(self)
319 first = next(words).lower()
320 new_words = itertools.chain((first,), WordSet(words).camel_case())
321 return ''.join(new_words)
323 def underscore_separated(self):
324 return '_'.join(self)
326 def dash_separated(self):
327 return '-'.join(self)
329 def space_separated(self):
330 return ' '.join(self)
332 def trim_right(self, item):
333 """
334 Remove the item from the end of the set.
336 >>> WordSet.parse('foo bar').trim_right('foo')
337 ('foo', 'bar')
338 >>> WordSet.parse('foo bar').trim_right('bar')
339 ('foo',)
340 >>> WordSet.parse('').trim_right('bar')
341 ()
342 """
343 return self[:-1] if self and self[-1] == item else self
345 def trim_left(self, item):
346 """
347 Remove the item from the beginning of the set.
349 >>> WordSet.parse('foo bar').trim_left('foo')
350 ('bar',)
351 >>> WordSet.parse('foo bar').trim_left('bar')
352 ('foo', 'bar')
353 >>> WordSet.parse('').trim_left('bar')
354 ()
355 """
356 return self[1:] if self and self[0] == item else self
358 def trim(self, item):
359 """
360 >>> WordSet.parse('foo bar').trim('foo')
361 ('bar',)
362 """
363 return self.trim_left(item).trim_right(item)
365 def __getitem__(self, item):
366 result = super(WordSet, self).__getitem__(item)
367 if isinstance(item, slice):
368 result = WordSet(result)
369 return result
371 @classmethod
372 def parse(cls, identifier):
373 matches = cls._pattern.finditer(identifier)
374 return WordSet(match.group(0) for match in matches)
376 @classmethod
377 def from_class_name(cls, subject):
378 return cls.parse(subject.__class__.__name__)
381# for backward compatibility
382words = WordSet.parse
385def simple_html_strip(s):
386 r"""
387 Remove HTML from the string `s`.
389 >>> str(simple_html_strip(''))
390 ''
392 >>> print(simple_html_strip('A <bold>stormy</bold> day in paradise'))
393 A stormy day in paradise
395 >>> print(simple_html_strip('Somebody <!-- do not --> tell the truth.'))
396 Somebody tell the truth.
398 >>> print(simple_html_strip('What about<br/>\nmultiple lines?'))
399 What about
400 multiple lines?
401 """
402 html_stripper = re.compile('(<!--.*?-->)|(<[^>]*>)|([^<]+)', re.DOTALL)
403 texts = (match.group(3) or '' for match in html_stripper.finditer(s))
404 return ''.join(texts)
407class SeparatedValues(str):
408 """
409 A string separated by a separator. Overrides __iter__ for getting
410 the values.
412 >>> list(SeparatedValues('a,b,c'))
413 ['a', 'b', 'c']
415 Whitespace is stripped and empty values are discarded.
417 >>> list(SeparatedValues(' a, b , c, '))
418 ['a', 'b', 'c']
419 """
421 separator = ','
423 def __iter__(self):
424 parts = self.split(self.separator)
425 return filter(None, (part.strip() for part in parts))
428class Stripper:
429 r"""
430 Given a series of lines, find the common prefix and strip it from them.
432 >>> lines = [
433 ... 'abcdefg\n',
434 ... 'abc\n',
435 ... 'abcde\n',
436 ... ]
437 >>> res = Stripper.strip_prefix(lines)
438 >>> res.prefix
439 'abc'
440 >>> list(res.lines)
441 ['defg\n', '\n', 'de\n']
443 If no prefix is common, nothing should be stripped.
445 >>> lines = [
446 ... 'abcd\n',
447 ... '1234\n',
448 ... ]
449 >>> res = Stripper.strip_prefix(lines)
450 >>> res.prefix = ''
451 >>> list(res.lines)
452 ['abcd\n', '1234\n']
453 """
455 def __init__(self, prefix, lines):
456 self.prefix = prefix
457 self.lines = map(self, lines)
459 @classmethod
460 def strip_prefix(cls, lines):
461 prefix_lines, lines = itertools.tee(lines)
462 prefix = functools.reduce(cls.common_prefix, prefix_lines)
463 return cls(prefix, lines)
465 def __call__(self, line):
466 if not self.prefix:
467 return line
468 null, prefix, rest = line.partition(self.prefix)
469 return rest
471 @staticmethod
472 def common_prefix(s1, s2):
473 """
474 Return the common prefix of two lines.
475 """
476 index = min(len(s1), len(s2))
477 while s1[:index] != s2[:index]:
478 index -= 1
479 return s1[:index]
482def remove_prefix(text, prefix):
483 """
484 Remove the prefix from the text if it exists.
486 >>> remove_prefix('underwhelming performance', 'underwhelming ')
487 'performance'
489 >>> remove_prefix('something special', 'sample')
490 'something special'
491 """
492 null, prefix, rest = text.rpartition(prefix)
493 return rest
496def remove_suffix(text, suffix):
497 """
498 Remove the suffix from the text if it exists.
500 >>> remove_suffix('name.git', '.git')
501 'name'
503 >>> remove_suffix('something special', 'sample')
504 'something special'
505 """
506 rest, suffix, null = text.partition(suffix)
507 return rest
510def normalize_newlines(text):
511 r"""
512 Replace alternate newlines with the canonical newline.
514 >>> normalize_newlines('Lorem Ipsum\u2029')
515 'Lorem Ipsum\n'
516 >>> normalize_newlines('Lorem Ipsum\r\n')
517 'Lorem Ipsum\n'
518 >>> normalize_newlines('Lorem Ipsum\x85')
519 'Lorem Ipsum\n'
520 """
521 newlines = ['\r\n', '\r', '\n', '\u0085', '\u2028', '\u2029']
522 pattern = '|'.join(newlines)
523 return re.sub(pattern, '\n', text)
526def _nonblank(str):
527 return str and not str.startswith('#')
530@functools.singledispatch
531def yield_lines(iterable):
532 r"""
533 Yield valid lines of a string or iterable.
535 >>> list(yield_lines(''))
536 []
537 >>> list(yield_lines(['foo', 'bar']))
538 ['foo', 'bar']
539 >>> list(yield_lines('foo\nbar'))
540 ['foo', 'bar']
541 >>> list(yield_lines('\nfoo\n#bar\nbaz #comment'))
542 ['foo', 'baz #comment']
543 >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n']))
544 ['foo', 'bar', 'baz', 'bing']
545 """
546 return itertools.chain.from_iterable(map(yield_lines, iterable))
549@yield_lines.register(str)
550def _(text):
551 return filter(_nonblank, map(str.strip, text.splitlines()))
554def drop_comment(line):
555 """
556 Drop comments.
558 >>> drop_comment('foo # bar')
559 'foo'
561 A hash without a space may be in a URL.
563 >>> drop_comment('http://example.com/foo#bar')
564 'http://example.com/foo#bar'
565 """
566 return line.partition(' #')[0]
569def join_continuation(lines):
570 r"""
571 Join lines continued by a trailing backslash.
573 >>> list(join_continuation(['foo \\', 'bar', 'baz']))
574 ['foobar', 'baz']
575 >>> list(join_continuation(['foo \\', 'bar', 'baz']))
576 ['foobar', 'baz']
577 >>> list(join_continuation(['foo \\', 'bar \\', 'baz']))
578 ['foobarbaz']
580 Not sure why, but...
581 The character preceeding the backslash is also elided.
583 >>> list(join_continuation(['goo\\', 'dly']))
584 ['godly']
586 A terrible idea, but...
587 If no line is available to continue, suppress the lines.
589 >>> list(join_continuation(['foo', 'bar\\', 'baz\\']))
590 ['foo']
591 """
592 lines = iter(lines)
593 for item in lines:
594 while item.endswith('\\'):
595 try:
596 item = item[:-2].strip() + next(lines)
597 except StopIteration:
598 return
599 yield item