• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

manoss96 / pregex / 4356417821

pending completion
4356417821

push

github

manoss96
add logo and funding

1625 of 1625 relevant lines covered (100.0%)

3.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/pregex/core/pre.py
1
__doc__ = """
3✔
2
This module a single class, namely :class:`Pregex`, which
3
constitutes the base class for every other class within `pregex`.
4

5
Classes & methods
6
-------------------------------------------
7

8
Below are listed all classes within :py:mod:`pregex.core.pre`
9
along with any possible methods they may possess.
10
"""
11

12

13
import re as _re
3✔
14
import enum as _enum
3✔
15
import pregex.core.exceptions as _ex
3✔
16
from typing import Union as _Union
3✔
17
from typing import Optional as _Optional
3✔
18
from typing import Iterator as _Iterator
3✔
19

20

21
class _Type(_enum.Enum):
3✔
22
    '''
3✔
23
    This enum represents all possible types of a RegEx pattern.
24
    '''
25
    Alternation = 0
3✔
26
    Assertion = 1
3✔
27
    Class = 2
3✔
28
    Empty = 3
3✔
29
    Group = 4
3✔
30
    Other = 5
3✔
31
    Quantifier = 6
3✔
32
    Token = 7
3✔
33

34

35
class Pregex():
3✔
36
    '''
3✔
37
    Wraps the provided pattern within an instance of this class.
38

39
    :param str pattern: The pattern that is to be wrapped within an instance \
40
        of this class. Defaults to the empty string ``''``.
41
    :param bool escape: Determines whether to escape the provided pattern or not. \
42
        Defaults to ``True``.
43

44
    :raises InvalidArgumentTypeException: Parameter ``pattern`` is not a string.
45

46
    :note: This class constitutes the base class for every other class within the `pregex` package.
47
    '''
48

49
    '''
2✔
50
    Determines the groupping rules of each Pregex instance type:
51

52
    :schema: __groupping_rules[type] => (on_concat, on_quantify, on_assertion)
53
    '''
54
    __groupping_rules: dict[_Type, str] = {
3✔
55
        _Type.Alternation: (True, True, True),
56
        _Type.Assertion : (False, True, False),
57
        _Type.Class: (False, False, False),
58
        _Type.Empty: (False, False, False),
59
        _Type.Group: (False, False, False),
60
        _Type.Other: (False, True, False),
61
        _Type.Quantifier: (False, True, False),
62
        _Type.Token: (False, False, False),
63
    }
64

65

66
    '''
2✔
67
    The totality of active RegEx flags.
68
    '''
69
    __flags: _re.RegexFlag = _re.MULTILINE | _re.DOTALL
3✔
70

71

72
    def __init__(self, pattern: str = '', escape: bool = True) -> 'Pregex':
3✔
73
        '''
74
        Wraps the provided pattern within an instance of this class.
75

76
        :param str pattern: The pattern that is to be wrapped within an instance \
77
            of this class. Defaults to the empty string ``''``.
78
        :param bool escape: Determines whether to escape the provided pattern or not. \
79
            Defaults to ``True``.
80

81
        :raises InvalidArgumentTypeException: Parameter ``pattern`` is not a string.
82

83
        :note: This class constitutes the base class for every other class within the `pregex` package.
84
        '''
85
        if not isinstance(pattern, str):
3✔
86
            message = "Provided argument \"pattern\" is not a string."
3✔
87
            raise _ex.InvalidArgumentTypeException(message)
3✔
88
        if escape:
3✔
89
            self.__pattern = __class__.__escape(pattern)
3✔
90
        else:
91
            self.__pattern = pattern
3✔
92
        self.__type, self.__repeatable = __class__.__infer_type(self.__pattern)
3✔
93
        self.__compiled: _re.Pattern = None
3✔
94

95

96
    '''
2✔
97
    Public Methods
98
    '''
99
    def print_pattern(self, include_flags: bool = False) -> None:
3✔
100
        '''
101
        Prints this instance's underlying RegEx pattern.
102

103
        :param bool include_flags: Determines whether to display the \
104
            used RegEx flags along with the pattern. Defaults to ``False``.
105
        '''
106
        print(self.get_pattern(include_flags))
3✔
107

108

109
    def get_pattern(self, include_flags: bool = False) -> str:
3✔
110
        '''
111
        Returns this instance's underlying RegEx pattern as a string.
112

113
        :param bool include_flags: Determines whether to display the \
114
            used RegEx flags along with the pattern. Defaults to ``False``.
115

116
        :note: This method is to be preferred over str() when one needs \
117
            to display this instance's underlying Regex pattern.
118
        '''
119
        pattern = repr(self)
3✔
120
        return f"/{pattern}/gmsu" if include_flags else pattern
3✔
121

122

123
    def get_compiled_pattern(self, discard_after: bool = True) -> _re.Pattern:
3✔
124
        '''
125
        Returns this instance's underlying RegEx pattern as a ``re.Pattern`` instance.
126
        
127
        :param bool discard_after: Determines whether the compiled pattern is to be \
128
            discarded after the program has exited from this method, or to be retained \
129
            so that any further attempt at matching a string will use the compiled pattern \
130
            instead of the regular one. Defaults to ``True``.
131
        '''
132
        if self.__compiled is None:
3✔
133
            self.compile()
3✔
134
        compiled = self.__compiled
3✔
135
        if discard_after:
3✔
136
            self.__compiled = None
3✔
137
        return compiled
3✔
138

139

140
    def compile(self) -> None:
3✔
141
        '''
142
        Compiles the underlying RegEx pattern. After invoking this method, \
143
        any further attempt at matching a string will be making use of the \
144
        compiled RegEx pattern.
145
        '''
146
        self.__compiled = _re.compile(self.get_pattern(), flags=self.__flags)
3✔
147

148

149
    @staticmethod
3✔
150
    def purge() -> None:
3✔
151
        '''
152
        Clears the regular expression caches.
153
        '''
154
        _re.purge()
3✔
155

156

157
    def has_match(self, source: str, is_path: bool = False) -> bool:
3✔
158
        '''
159
        Returns ``True`` if at least one match is found within the provided text.
160

161
        :param str source: The text that is to be examined.
162
        :param bool is_path: If set to ``True``, then parameter ``source`` \
163
            is considered to be a local path pointing to the file from which \
164
            the text is to be read. Defaults to ``False``.
165
        '''
166
        if is_path:
3✔
167
            source = self.__extract_text(source)
3✔
168
        return bool(_re.search(self.__pattern, source, flags=self.__flags) \
3✔
169
            if self.__compiled is None else self.__compiled.search(source))
170

171

172
    def is_exact_match(self, source: str, is_path: bool = False) -> bool:
3✔
173
        '''
174
        Returns ``True`` only if the provided text matches this pattern exactly.
175

176
        :param str source: The text that is to be examined.
177
        :param bool is_path: If set to ``True``, then parameter ``source`` \
178
            is considered to be a local path pointing to the file from which \
179
            the text is to be read. Defaults to ``False``.
180
        '''
181
        if is_path:
3✔
182
            source = self.__extract_text(source)
3✔
183
        return bool(_re.fullmatch(self.__pattern, source, flags=self.__flags) \
3✔
184
            if self.__compiled is None else self.__compiled.fullmatch(source))
185

186

187
    def iterate_matches(self, source: str, is_path: bool = False) -> _Iterator[str]:
3✔
188
        '''
189
        Generates any possible matches found within the provided text.
190

191
        :param str source: The text that is to be examined.
192
        :param bool is_path: If set to ``True``, then parameter ``source`` \
193
            is considered to be a local path pointing to the file from which \
194
            the text is to be read. Defaults to ``False``.
195
        '''
196
        for match in self.__iterate_match_objects(source, is_path):
3✔
197
            yield match.group(0)
3✔
198

199

200
    def iterate_matches_and_pos(self, source: str, is_path: bool = False) -> _Iterator[tuple[str, int, int]]:
3✔
201
        '''
202
        Generates any possible matches found within the provided text \
203
        along with their exact position.
204

205
        :param str source: The text that is to be examined.
206
        :param bool is_path: If set to ``True``, then parameter ``source`` \
207
            is considered to be a local path pointing to the file from which \
208
            the text is to be read. Defaults to ``False``.
209
        '''
210
        for match in self.__iterate_match_objects(source, is_path):
3✔
211
            yield (match.group(0), *match.span())
3✔
212

213

214
    def iterate_matches_with_context(self, source: str, n_left: int = 5,
3✔
215
        n_right: int = 5, is_path: bool = False) -> _Iterator[str]:
216
        '''
217
        Generates any possible matches found within the provided text, \
218
        along with any of its surrounding context, the exact length of \
219
        which can be configured through this method's parameters.
220

221
        :param str source: The text that is to be examined.
222
        :param int n_left: The number of characters representing the context \
223
            on the left side of the match. Defaults to ``5``.
224
        :param int n_right: The number of characters representing the context \
225
            on the right side of the match. Defaults to ``5``.
226
        :param bool is_path: If set to ``True``, then parameter ``source`` \
227
            is considered to be a local path pointing to the file from which \
228
            the text is to be read. Defaults to ``False``.
229

230
        :raises InvalidArgumentTypeException: Either parameter ``n_left`` or \
231
            ``n_right`` is not an integer. 
232
        :raises InvalidArgumentValueException: Either parameter ``n_left`` or \
233
            ``n_right`` has a value of less than zero.
234
        '''
235
        if not isinstance(n_left, int) or isinstance(n_left, bool):
3✔
236
            message = "Provided argument \"n_left\" is not an integer."
3✔
237
            raise _ex.InvalidArgumentTypeException(message)
3✔
238
        if not isinstance(n_right, int) or isinstance(n_right, bool):
3✔
239
            message = "Provided argument \"n_right\" is not an integer."
3✔
240
            raise _ex.InvalidArgumentTypeException(message)
3✔
241
        if n_left < 0:
3✔
242
            message = "Parameter \"n_left\" can't be negative."
3✔
243
            raise _ex.InvalidArgumentValueException(message)
3✔
244
        if n_right < 0:
3✔
245
            message = "Parameter \"n_right\" can't be negative."
3✔
246
            raise _ex.InvalidArgumentValueException(message)
3✔
247

248
        for _, start, end in self.iterate_matches_and_pos(source, is_path):
3✔
249
            yield source[max(start - n_left, 0):min(end + n_right, len(source))]
3✔
250

251

252
    def iterate_captures(self, source: str, include_empty: bool = True,
3✔
253
        is_path: bool = False) -> _Iterator[tuple[str]]:
254
        '''
255
        Generates tuples, one tuple per match, where each tuple contains \
256
        all of its corresponding match's captured groups.
257

258
        :param str source: The text that is to be examined.
259
        :param bool include_empty: Determines whether to include empty captures \
260
            into the results. Defaults to ``True``.
261
        :param bool is_path: If set to ``True``, then parameter ``source`` \
262
            is considered to be a local path pointing to the file from which \
263
            the text is to be read. Defaults to ``False``.
264

265
        :note: In case there exists an optional capturing group within the pattern, \
266
            that has not been captured by a match, then that capture's corresponding \
267
            value will be ``None``.
268
        '''
269
        for match in self.__iterate_match_objects(source, is_path):
3✔
270
            yield match.groups() if include_empty else \
3✔
271
                tuple(group for group in match.groups() if group != '')
272

273

274
    def iterate_captures_and_pos(self, source: str, include_empty: bool = True,
3✔
275
        relative_to_match : bool = False, is_path: bool = False) -> _Iterator[list[tuple[str, int, int]]]:
276
        '''
277
        Generates lists of tuples, one list per match, where each tuple contains one \
278
        of its corresponding match's captured groups along with its exact position \
279
        within the text.
280

281
        :param str source: The text that is to be examined.
282
        :param bool include_empty: Determines whether to include empty captures into the \
283
            results. Defaults to ``True``.
284
        :param bool relative_to_match: If ``True``, then each group's position-indices \
285
            are calculated relative to the group's corresponding match, not to the whole \
286
            string. Defaults to ``False``.
287
        :param bool is_path: If set to ``True``, then parameter ``source`` \
288
            is considered to be a local path pointing to the file from which \
289
            the text is to be read. Defaults to ``False``.
290

291
        :note: In case there exists an optional capturing group within the pattern, \
292
            that has not been captured by a match, then that capture's corresponding \
293
            tuple will be ``(None, -1, -1)``.
294
        '''
295
        for match in self.__iterate_match_objects(source, is_path):
3✔
296
            groups, counter = list(), 0
3✔
297
            for group in match.groups():
3✔
298
                counter += 1
3✔
299
                if include_empty or (group != ''):
3✔
300
                    start, end = match.span(counter)
3✔
301
                    if relative_to_match and start > -1:
3✔
302
                        start, end = start - match.start(0), end - match.start(0)
3✔
303
                    groups.append((group, start, end))
3✔
304
            yield groups
3✔
305

306

307
    def iterate_named_captures(self, source: str, include_empty: bool = True,
3✔
308
        is_path: bool = False) -> _Iterator[dict[str, str]]:
309
        '''
310
        Generates dictionaries, one dictionary per match, where each dictionary \
311
        contains key-value pairs of any named captured groups that belong to its \
312
        corresponding match, with each key being the name of the captured group, \
313
        whereas its corresponding value will be the actual captured text.
314

315
        :param str source: The text that is to be examined.
316
        :param bool include_empty: Determines whether to include empty captures \
317
            into the results. Defaults to ``True``.
318
        :param bool is_path: If set to ``True``, then parameter ``source`` \
319
            is considered to be a local path pointing to the file from which \
320
            the text is to be read. Defaults to ``False``.
321

322
        :note: In case there exists an optional capturing group within the pattern, \
323
            that has not been captured by a match, then that capture's corresponding \
324
            key-value pair will be ``name --> None``.
325
        '''
326
        for match in self.__iterate_match_objects(source, is_path):
3✔
327
            yield match.groupdict() if include_empty else \
3✔
328
                {k : v for k, v in match.groupdict().items() if v != ''}
329

330

331
    def iterate_named_captures_and_pos(self, source: str, include_empty: bool = True,
3✔
332
        relative_to_match : bool = False, is_path: bool = False) -> _Iterator[dict[str, tuple[str, int, int]]]:
333
        '''
334
        Generates dictionaries, one dictionary per match, where each dictionary \
335
        contains key-value pairs of any named captured groups that belong to its\
336
        corresponding match, with each key being the name of the captured group, \
337
        whereas its corresponding value will be a tuple containing the actual \
338
        captured group along with its exact position within the text.
339

340
        :param str source: The text that is to be examined.
341
        :param bool include_empty: Determines whether to include empty captures into the \
342
            results. Defaults to ``True``.
343
        :param bool relative_to_match: If ``True``, then each group's position-indices \
344
            are calculated relative to the group's corresponding match, not to the whole \
345
            string. Defaults to ``False``.
346
        :param bool is_path: If set to ``True``, then parameter ``source`` \
347
            is considered to be a local path pointing to the file from which \
348
            the text is to be read. Defaults to ``False``.
349

350
        :note: In case there exists an optional capturing group within the pattern, \
351
            that has not been captured by a match, then that capture's corresponding \
352
            key-value pair will be ``name --> (None, -1, -1)``.
353
        '''
354
        for match in self.__iterate_match_objects(source, is_path):
3✔
355
            groups, counter = dict(), 0
3✔
356
            for k, v in match.groupdict().items():
3✔
357
                counter += 1
3✔
358
                if include_empty or (v != ''):
3✔
359
                    start, end = match.span(counter)
3✔
360
                    if relative_to_match and start > -1:
3✔
361
                        start, end = start - match.start(0), end - match.start(0)
3✔
362
                    groups.update({k: (v, start, end)})
3✔
363
            yield groups
3✔
364

365

366
    def get_matches(self, source: str, is_path: bool = False) -> list[str]:
3✔
367
        '''
368
        Returns a list containing any possible matches found within \
369
        the provided text.
370

371
        :param str source: The text that is to be examined.
372
        :param bool is_path: If set to ``True``, then parameter ``source`` \
373
            is considered to be a local path pointing to the file from which \
374
            the text is to be read. Defaults to ``False``.
375
        '''
376
        return list(match for match in self.iterate_matches(source, is_path))
3✔
377

378

379
    def get_matches_and_pos(self, source: str, is_path: bool = False) -> list[tuple[str, int, int]]:
3✔
380
        '''
381
        Returns a list containing any possible matches found within the \
382
        provided text along with their exact position.
383

384
        :param str source: The text that is to be examined.
385
        :param bool is_path: If set to ``True``, then parameter ``source`` \
386
            is considered to be a local path pointing to the file from which \
387
            the text is to be read. Defaults to ``False``.
388
        '''
389
        return list(match for match in self.iterate_matches_and_pos(source, is_path))
3✔
390

391

392
    def get_matches_with_context(self, source: str, n_left: int = 5, n_right: int = 5,
3✔
393
        is_path: bool = False) -> list[str]:
394
        '''
395
        Returns a list containing any possible matches found within the \
396
        provided text, along with any of its surrounding context, the exact \
397
        length of which can be configured through this method's parameters.
398

399
        :param str source: The text that is to be examined.
400
        :param int n_left: The number of characters representing the context \
401
            on the left side of the match. Defaults to ``5``.
402
        :param int n_right: The number of characters representing the context \
403
            on the right side of the match. Defaults to ``5``.
404
        :param bool is_path: If set to ``True``, then parameter ``source`` \
405
            is considered to be a local path pointing to the file from which \
406
            the text is to be read. Defaults to ``False``.
407

408
        :raises InvalidArgumentTypeException: Either parameter ``n_left`` or \
409
            ``n_right`` is not an integer. 
410
        :raises InvalidArgumentValueException: Either parameter ``n_left`` or \
411
            ``n_right`` has a value of less than zero.
412
        '''
413
        return list(match for match in self.iterate_matches_with_context(
3✔
414
            source, n_left, n_right, is_path))
415

416

417
    def get_captures(self, source: str, include_empty: bool = True, is_path: bool = False) -> list[tuple[str]]:
3✔
418
        '''
419
        Returns a list of tuples, one tuple per match, where each tuple contains \
420
        all of its corresponding match's captured groups.
421

422
        :param str source: The text that is to be examined.
423
        :param bool include_empty: Determines whether to include empty captures \
424
            into the results. Defaults to ``True``.
425
        :param bool is_path: If set to ``True``, then parameter ``source`` \
426
            is considered to be a local path pointing to the file from which \
427
            the text is to be read. Defaults to ``False``.
428

429
        :note: In case there exists an optional capturing group within the pattern, \
430
            that has not been captured by a match, then that capture's corresponding \
431
            value will be ``None``.
432
        '''
433
        return list(group for group in self.iterate_captures(source, include_empty, is_path))
3✔
434

435

436
    def get_captures_and_pos(self, source: str, include_empty: bool = True,
3✔
437
        relative_to_match: bool = False, is_path: bool = False) -> list[list[tuple[str, int, int]]]:
438
        '''
439
        Returns a list containing lists of tuples, one list per match, where each \
440
        tuple contains one of its corresponding match's captured groups along with \
441
        its exact position within the text.
442

443
        :param str source: The text that is to be examined.
444
        :param bool include_empty: Determines whether to include empty captures into the \
445
            results. Defaults to ``True``.
446
        :param bool relative_to_match: If ``True``, then each group's position-indices \
447
            are calculated relative to the group's corresponding match, not to the whole \
448
            string. Defaults to ``False``.
449
        :param bool is_path: If set to ``True``, then parameter ``source`` \
450
            is considered to be a local path pointing to the file from which \
451
            the text is to be read. Defaults to ``False``.
452

453
        :note: In case there exists an optional capturing group within the pattern, \
454
            that has not been captured by a match, then that capture's corresponding \
455
            tuple will be ``(None, -1, -1)``.
456
        '''
457
        return list(tup for tup in self.iterate_captures_and_pos(
3✔
458
            source, include_empty, relative_to_match, is_path))
459

460

461
    def get_named_captures(self, source: str,
3✔
462
        include_empty: bool = True, is_path: bool = False) -> list[dict[str, str]]:
463
        '''
464
        Returns a dictionary of tuples, one dictionary per match, where each \
465
        dictionary contains key-value pairs of any named captured groups that \
466
        belong to its corresponding match, with each key being the name of the \
467
        captured group, whereas its corresponding value will be the actual \
468
        captured text.
469

470
        :param str source: The text that is to be examined.
471
        :param bool include_empty: Determines whether to include empty captures \
472
            into the results. Defaults to ``True``.
473
        :param bool is_path: If set to ``True``, then parameter ``source`` \
474
            is considered to be a local path pointing to the file from which \
475
            the text is to be read. Defaults to ``False``.
476

477
        :note: In case there exists an optional capturing group within the pattern, \
478
            that has not been captured by a match, then that capture's corresponding \
479
            key-value pair will be ``name --> None``.
480
        '''
481
        return list(group for group in self.iterate_named_captures(source, include_empty, is_path))
3✔
482

483

484
    def get_named_captures_and_pos(self, source: str, include_empty: bool = True,
3✔
485
        relative_to_match: bool = False, is_path: bool = False) -> list[dict[str, tuple[str, int, int]]]:
486
        '''
487
        Returns a dictionary of tuples, one dictionary per match, where each \
488
        dictionary contains key-value pairs of any named captured groups that \
489
        belong to its corresponding match, with each key being the name of the \
490
        captured group, whereas its corresponding value will be a tuple containing \
491
        the actual captured group along with its exact position within the text.
492

493
        :param str source: The text that is to be examined.
494
        :param bool include_empty: Determines whether to include empty captures into the \
495
            results. Defaults to ``True``.
496
        :param bool relative_to_match: If ``True``, then each group's position-indices \
497
            are calculated relative to the group's corresponding match, not to the whole \
498
            string. Defaults to ``False``.
499
        :param bool is_path: If set to ``True``, then parameter ``source`` \
500
            is considered to be a local path pointing to the file from which \
501
            the text is to be read. Defaults to ``False``.
502

503
        :note: In case there exists an optional capturing group within the pattern, \
504
            that has not been captured by a match, then that capture's corresponding \
505
            key-value pair will be ``name --> (None, -1, -1)``.
506
        '''
507
        return list(group for group in self.iterate_named_captures_and_pos(
3✔
508
            source, include_empty, relative_to_match, is_path))
509

510

511
    def replace(self, source: str, repl: str, count: int = 0, is_path: bool = False) -> str:
3✔
512
        '''
513
        Replaces all or some of the occuring matches with ``repl`` and \
514
        returns the resulting string. If there are no matches, then this \
515
        method will return the provided text without modifying it.
516

517
        :param str source: The text that is to be matched and modified.
518
        :param str repl: The string that is to replace any matches.
519
        :param int count: The number of matches that are to be replaced, \
520
            starting from left to right. A value of ``0`` indicates that \
521
            all matches must be replaced. Defaults to ``0``.
522
        :param bool is_path: If set to ``True``, then parameter ``source`` \
523
            is considered to be a local path pointing to the file from which \
524
            the text is to be read. Defaults to ``False``.
525

526
        :raises InvalidArgumentValueException: Parameter ``count`` has a value of \
527
            less than zero.
528
        '''
529
        if count < 0:
3✔
530
            message = "Parameter \"count\" can't be negative."
3✔
531
            raise _ex.InvalidArgumentValueException(message)
3✔
532
        if is_path:
3✔
533
            source = self.__extract_text(source)
3✔
534
        return _re.sub(str(self), repl, source, count, flags=self.__flags)
3✔
535

536

537
    def split_by_match(self, source: str, is_path: bool = False) -> list[str]:
3✔
538
        '''
539
        Splits the provided text based on any occuring matches and returns \
540
        the result as a list containing each individual part of the text \
541
        after the split.
542

543
        :param str source: The text that is to be matched and split.
544
        :param bool is_path: If set to ``True``, then parameter ``source`` \
545
            is considered to be a local path pointing to the file from which \
546
            the text is to be read. Defaults to ``False``.
547
        '''
548
        if is_path:
3✔
549
            source = self.__extract_text(source)
3✔
550
        split_list, index = list(), 0
3✔
551
        for _, start, end in self.iterate_matches_and_pos(source):
3✔
552
            split_list.append(source[index:start])
3✔
553
            index = end
3✔
554
        split_list.append(source[index:])
3✔
555
        return split_list
3✔
556

557

558
    def split_by_capture(self, source: str, include_empty: bool = True, is_path: bool = False) -> list[str]:
3✔
559
        '''
560
        Splits the provided text based on any occuring captures and returns \
561
        the result as alist containing each individual part of the text \
562
        after the split.
563

564
        :param str source: The piece of text that is to be matched and split.
565
        :param bool include_empty: Determines whether to include empty groups into the results. \
566
            Defaults to ``True``.
567
        :param bool is_path: If set to ``True``, then parameter ``source`` \
568
            is considered to be a local path pointing to the file from which \
569
            the text is to be read. Defaults to ``False``.
570
        '''
571
        if is_path:
3✔
572
            source = self.__extract_text(source)
3✔
573
        split_list, index = list(), 0
3✔
574
        for groups in self.iterate_captures_and_pos(source, include_empty):
3✔
575
            for group, start, end in groups:
3✔
576
                if group is None:
3✔
577
                    continue
3✔
578
                split_list.append(source[index:start])
3✔
579
                index = end
3✔
580
        split_list.append(source[index:])
3✔
581
        return split_list
3✔
582

583

584
    '''
2✔
585
    Quantifiers
586
    '''
587
    def optional(self, is_greedy: bool = True)-> 'Pregex':
3✔
588
        '''
589
        Applies quantifier ``?`` to this instance's underlying pattern \
590
        and returns the result as a ``Pregex`` instance.
591

592
        :param bool is_greedy: Determines whether to declare this quantifier as greedy. \
593
            When declared as such, the regex engine will try to match \
594
            the expression as many times as possible. Defaults to ``True``.
595
        '''
596
        if self._get_type() == _Type.Empty:
3✔
597
            return self
3✔
598
        return __class__(
3✔
599
            f"{self._quantify_conditional_group()}?{'' if is_greedy else '?'}",
600
            escape=False)
601

602

603
    def indefinite(self, is_greedy: bool = True) -> 'Pregex':
3✔
604
        '''
605
        Applies quantifier ``*`` to this instance's underlying pattern \
606
        and returns the result as a ``Pregex`` instance.
607

608
        :param bool is_greedy: Determines whether to declare this quantifier as greedy. \
609
            When declared as such, the regex engine will try to match \
610
            the expression as many times as possible. Defaults to ``True``.
611

612
        :raises CannotBeRepeatedException: This instance represents a non-repeatable pattern.
613
        '''
614
        if self._get_type() == _Type.Empty:
3✔
615
            return self
3✔
616
        if not self._is_repeatable():
3✔
617
            raise _ex.CannotBeRepeatedException(self)
3✔
618
        return __class__(
3✔
619
            f"{self._quantify_conditional_group()}*{'' if is_greedy else '?'}",
620
            escape=False)
621

622

623
    def one_or_more(self, is_greedy: bool = True) -> 'Pregex':
3✔
624
        '''
625
        Applies quantifier ``+`` to this instance's underlying pattern \
626
        and returns the result as a ``Pregex`` instance.
627

628
        :param bool is_greedy: Determines whether to declare this quantifier as greedy. \
629
            When declared as such, the regex engine will try to match \
630
            the expression as many times as possible. Defaults to ``True``.
631

632
        :raises CannotBeRepeatedException: This instance represents a non-repeatable pattern.
633
        '''
634
        if self._get_type() == _Type.Empty:
3✔
635
            return self
3✔
636
        if not self._is_repeatable():
3✔
637
            raise _ex.CannotBeRepeatedException(self)
3✔
638
        return __class__(
3✔
639
            f"{self._quantify_conditional_group()}+{'' if is_greedy else '?'}",
640
            escape=False)
641

642

643
    def exactly(self, n: int) -> 'Pregex':
3✔
644
        '''
645
        Applies quantifier ``{n}`` to this instance's underlying pattern \
646
        and returns the result as a ``Pregex`` instance.
647

648
        :param int n: The exact number of times that the patterns is to be matched.
649

650
        :raises InvalidArgumentTypeException: Parameter ``n`` is not an integer.
651
        :raises InvalidArgumentValueException: Parameter ``n`` has a value of less \
652
            than zero.
653
        :raises CannotBeRepeatedException: Parameter ``n`` has a value of greater \
654
            than one, while this instance represents a non-repeatable pattern.
655
        '''
656
        if not isinstance(n, int) or isinstance(n, bool):
3✔
657
            message = "Provided argument \"n\" is not an integer."
3✔
658
            raise _ex.InvalidArgumentTypeException(message)
3✔
659
        if n == 0:
3✔
660
            return Pregex()
3✔
661
        if n == 1:
3✔
662
            return self
3✔
663
        else:
664
            if n < 0:
3✔
665
                message = "Parameter \"n\" can't be negative."
3✔
666
                raise _ex.InvalidArgumentValueException(message)
3✔
667
            if self._get_type() == _Type.Empty:
3✔
668
                return self
3✔
669
            if not self._is_repeatable():
3✔
670
                raise _ex.CannotBeRepeatedException(self)
3✔
671
            return __class__(
3✔
672
                f"{self._quantify_conditional_group()}{{{n}}}",
673
                escape=False)
674

675

676
    def at_least(self, n: int, is_greedy: bool = True)-> 'Pregex':
3✔
677
        '''
678
        Applies quantifier ``{n,}`` to this instance's underlying pattern \
679
        and returns the result as a ``Pregex`` instance.
680

681
        :param int n: The minimum number of times that the pattern is to be matched.
682
        :param bool is_greedy: Determines whether to declare this quantifier as greedy. \
683
            When declared as such, the regex engine will try to match \
684
            the expression as many times as possible. Defaults to ``True``.`
685

686
        :raises InvalidArgumentTypeException: Parameter ``n`` is not an integer.
687
        :raises InvalidArgumentValueException: Parameter ``n`` has a value of \
688
            less than zero.
689
        :raises CannotBeRepeatedException: This instance represents a \
690
            non-repeatable pattern.
691
        '''
692
        if not isinstance(n, int) or isinstance(n, bool):
3✔
693
            message = "Provided argument \"n\" is not an integer."
3✔
694
            raise _ex.InvalidArgumentTypeException(message)
3✔
695
        if n == 0:
3✔
696
            return self.indefinite(is_greedy)
3✔
697
        elif n == 1:
3✔
698
            return self.one_or_more(is_greedy)
3✔
699
        else:
700
            if n < 0:
3✔
701
                message = "Parameter \"n\" can't be negative."
3✔
702
                raise _ex.InvalidArgumentValueException(message)
3✔
703
            if self._get_type() == _Type.Empty:
3✔
704
                return self
3✔
705
            if not self._is_repeatable():
3✔
706
                raise _ex.CannotBeRepeatedException(self)
3✔
707
            return __class__(
3✔
708
                f"{self._quantify_conditional_group()}{{{n},}}{'' if is_greedy else '?'}",
709
                escape=False)
710

711

712
    def at_most(self, n: _Optional[int], is_greedy: bool = True) -> 'Pregex':
3✔
713
        '''
714
        Applies quantifier ``{,n}`` to this instance's underlying pattern \
715
        and returns the result as a ``Pregex`` instance.
716

717
        :param int n: The maximum number of times that the pattern is to be matched.
718
        :param bool is_greedy: Determines whether to declare this quantifier as greedy. \
719
            When declared as such, the regex engine will try to match \
720
            the expression as many times as possible. Defaults to ``True``.
721

722
        :raises InvalidArgumentTypeException: Parameter ``n`` is neither an \
723
            integer nor ``None``.
724
        :raises InvalidArgumentValueException: Parameter ``n`` has a value of \
725
            less than zero.
726
        :raises CannotBeRepeatedException: Parameter ``n`` has a value of \
727
            greater than one, while this instance represents a non-repeatable \
728
            pattern.
729

730
        :note: Setting ``n`` equal to ``None`` indicates that there is no upper limit to \
731
            the number of times the pattern is to be repeated.
732
        '''
733
        if not isinstance(n, int) or isinstance(n, bool):
3✔
734
            if n == None:
3✔
735
                return self.indefinite(is_greedy)
3✔
736
            message = "Provided argument \"n\" is neither an integer nor None."
3✔
737
            raise _ex.InvalidArgumentTypeException(message)
3✔
738
        elif n == 0:
3✔
739
            return self.exactly(n)
3✔
740
        elif n == 1:
3✔
741
            return self.optional(is_greedy)
3✔
742
        else:
743
            if n < 0:
3✔
744
                message = "Parameter \"n\" can't be negative."
3✔
745
                raise _ex.InvalidArgumentValueException(message)
3✔
746
            if self._get_type() == _Type.Empty:
3✔
747
                return self
3✔
748
            if not self._is_repeatable():
3✔
749
                raise _ex.CannotBeRepeatedException(self)
3✔
750
            return __class__(
3✔
751
                f"{self._quantify_conditional_group()}{{,{n}}}{'' if is_greedy else '?'}",
752
                escape=False)
753

754

755
    def at_least_at_most(self, n: int, m: _Optional[int], is_greedy: bool = True) -> 'Pregex':
3✔
756
        '''
757
        Applies quantifier ``{n,m}`` to this instance's underlying pattern \
758
        and returns the result as a ``Pregex`` instance.
759

760
        :param int n: The minimum number of times that the pattern is to be matched.
761
        :param int m: The minimum number of times that the pattern is to be matched.
762
        :param bool is_greedy: Determines whether to declare this quantifier as greedy. \
763
            When declared as such, the regex engine will try to match \
764
            the expression as many times as possible. Defaults to ``True``.`
765

766
        :raises InvalidArgumentTypeException: 
767
            - Parameter ``pre`` is neither a ``Pregex`` instance nor a string.
768
            - Parameter ``n`` is not an integer.
769
            - Parameter ``m`` is neither an integer nor ``None``.
770
        :raises InvalidArgumentValueException:
771
            - Either parameter ``n`` or ``m`` has a value of less than zero.
772
            - Parameter ``n`` has a greater value than that of parameter ``m``.
773
        :raises CannotBeRepeatedException: Parameter ``m`` has a value of greater \
774
            than one, while this instance represents a non-repeatable pattern.
775

776
        :note: 
777
            - Parameter ``is_greedy`` has no effect in the case that ``n`` equals ``m``.
778
            - Setting ``m`` equal to ``None`` indicates that there is no upper limit to the \
779
                number of times the pattern is to be repeated.
780
        '''
781
        if not isinstance(n, int) or isinstance(n, bool):
3✔
782
            message = "Provided argument \"n\" is not an integer."
3✔
783
            raise _ex.InvalidArgumentTypeException(message)
3✔
784
        elif not isinstance(m, int) or isinstance(m, bool):
3✔
785
            if m is not None:
3✔
786
                message = "Provided argument \"m\" is neither an integer nor \"None\"."
3✔
787
                raise _ex.InvalidArgumentTypeException(message)
3✔
788
        elif n < 0:
3✔
789
            message = "Parameter \"n\" can't be negative."
3✔
790
            raise _ex.InvalidArgumentValueException(message)
3✔
791
        elif m < 0:
3✔
792
            message = "Parameter \"m\" can't be negative."
3✔
793
            raise _ex.InvalidArgumentValueException(message)
3✔
794
        elif m < n:
3✔
795
            message = "The value of parameter \"m\" can't be"
3✔
796
            message += " less than the value of parameter \"n\"."
3✔
797
            raise _ex.InvalidArgumentValueException(message)
3✔
798
        if n == m:
3✔
799
            return self.exactly(n)
3✔
800
        elif n == 0:
3✔
801
            return self.at_most(m, is_greedy)
3✔
802
        elif m is None:
3✔
803
            return self.at_least(n, is_greedy)
3✔
804
        else:
805
            if self._get_type() == _Type.Empty:
3✔
806
                return self
3✔
807
            if not self._is_repeatable():
3✔
808
                raise _ex.CannotBeRepeatedException(self)
3✔
809
            return __class__(
3✔
810
                    f"{self._quantify_conditional_group()}{{{n},{m}}}{'' if is_greedy else '?'}",
811
                    escape=False)
812

813

814
    '''
2✔
815
    Operators
816
    '''
817
    def concat(self, pre: _Union['Pregex', str], on_right: bool = True) -> 'Pregex':
3✔
818
        '''
819
        Concatenates the provided pattern to this instance's underlying pattern \
820
        and returns the resulting pattern as a ``Pregex`` instance.
821

822
        :param Pregex | str pre: Either a string or a ``Pregex`` instance \
823
            representing the pattern that is to take part in the concatenation.
824
        :param bool on_right: If ``True``, then places the provided pattern on the \
825
            right side of the concatenation, else on the left. Defaults to ``True``.
826

827
        :raises InvalidArgumentTypeException: Parameter ``pre`` is neither \
828
            a ``Pregex`` instance nor a string.
829
        '''
830
        pre = __class__._to_pregex(pre)
3✔
831

832
        if pre._get_type() == _Type.Empty:
3✔
833
            return self
3✔
834

835
        pattern = self._concat_conditional_group()
3✔
836
        pre = pre._concat_conditional_group()
3✔
837

838
        pattern = pattern + pre if on_right else pre + pattern
3✔
839

840
        return __class__(pattern, escape=False)
3✔
841

842

843
    def either(self, pre: _Union['Pregex', str], on_right: bool = True) -> 'Pregex':
3✔
844
        '''
845
        Applies the alternation operator ``|`` between the provided pattern \
846
        and this instance's underlying pattern, and returns the resulting pattern \
847
        as a ``Pregex`` instance.
848

849
        :param Pregex | str pre: Either a string or a ``Pregex`` instance \
850
            representing the pattern that is to take part in the alternation.
851
        :param bool on_right: If ``True``, then places the provided pattern on the \
852
            right side of the alternation, else on the left. Defaults to ``True``.
853

854
        :raises InvalidArgumentTypeException: Parameter ``pre`` is neither \
855
            a ``Pregex`` instance nor a string.
856
        '''
857
        pre = __class__._to_pregex(pre)
3✔
858

859
        if pre._get_type() == _Type.Empty:
3✔
860
            pattern = str(self)
3✔
861
        else:
862
            pattern = f"{self}|{pre}" if on_right else f"{pre}|{self}"
3✔
863

864
        return __class__(pattern, escape=False)
3✔
865

866

867
    def enclose(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
868
        '''
869
        Concatenates the provided pattern to both sides of this instance's \
870
        underlying pattern, and returns the resulting pattern as a ``Pregex`` \
871
        instance.
872

873
        :param Pregex | str pre: Either a string or a ``Pregex`` instance \
874
            representing the "enclosing" pattern.
875

876
        :raises InvalidArgumentTypeException: Parameter `pre` is neither a \
877
            ``Pregex`` instance nor a string.
878
        '''
879
        pre = __class__._to_pregex(pre)._concat_conditional_group()
3✔
880
        pattern = f"{pre}{self._concat_conditional_group()}{pre}"
3✔
881
        return __class__(pattern, escape=False)
3✔
882
        
883

884
    '''
2✔
885
    Groups
886
    '''
887
    def capture(self, name: _Optional[str] = None) -> 'Pregex':
3✔
888
        '''
889
        Creates a capturing group out of this instance's underlying \
890
        pattern and returns the result as a ``Pregex`` instance.
891

892
        :param Pregex | str pre: The pattern out of which the capturing group is created.
893
        :param str name: The name that is assigned to the captured group for backreference \
894
            purposes. A value of ``None`` indicates that no name is to be assigned to the \
895
            group. Defaults to ``None``.
896

897
        :raises InvalidArgumentTypeException: Parameter ``name`` is neither a string \
898
            nor ``None``.
899
        :raises InvalidCapturingGroupNameException: Parameter ``name`` is not a valid \
900
            capturing group name. Such name must contain word characters only and start \
901
            with a non-digit character.
902

903
        :note:
904
            - Creating a capturing group out of a capturing group does nothing.
905
            - Creating a capturing group out of a non-capturing group converts it \
906
              into a capturing group, except if any flags have been applied to it, \
907
              in which case, the non-capturing group is wrapped within a capturing \
908
              group as a whole.
909
            - Creating a named capturing group out of an unnamed capturing group, \
910
              assigns a name to it.
911
            - Creating a named capturing group out of a named capturing group, \
912
              changes the group's name.
913
        '''
914
        if name is not None:
3✔
915
            if not isinstance(name, str):
3✔
916
                message = "Provided argument \"name\" is not a string."
3✔
917
                raise _ex.InvalidArgumentTypeException(message)
3✔
918
            if _re.fullmatch("[A-Za-z_]\w*", name) is None:
3✔
919
                raise _ex.InvalidCapturingGroupNameException(name)
3✔
920
        if self.__type == _Type.Empty:
3✔
921
            return self
3✔
922
        elif self.__type == _Type.Group:
3✔
923
            if self.__pattern.startswith('(?:'):
3✔
924
                # non-capturing group.
925
                pattern = self.__pattern.replace('?:', '', 1)
3✔
926
            elif _re.match('\(\?[i].+', self.__pattern):
3✔
927
                # non-capturing group with flag.
928
                pattern = f'({str(self)})'
3✔
929
            else:
930
                # capturing group.
931
                pattern = self.__pattern
3✔
932
            if name is not None:
3✔
933
                if pattern.startswith('(?P'):
3✔
934
                    pattern = _re.sub('\(\?P<[^>]*>', f'(?P<{name}>', pattern)
3✔
935
                else:
936
                    pattern = f"(?P<{name}>{pattern[1:-1]})"
3✔
937
        else:
938
            pattern = f"({f'?P<{name}>' if name != None else ''}{self})"
3✔
939
        return __class__(pattern, escape=False)
3✔
940

941

942
    def group(self, is_case_insensitive: bool = False) -> 'Pregex':
3✔
943
        '''
944
        Creates a non-capturing group out of this instance's underlying \
945
        pattern and returns the result as a ``Pregex`` instance.
946

947
        :param bool is_case_insensitive: If ``True``, then the "case insensitive" \
948
            flag is applied to the group so that the pattern within it ignores case \
949
            when it comes to matching. Defaults to ``False``.
950

951
        :raises InvalidArgumentTypeException: Parameter ``pre`` is neither \
952
            a ``Pregex`` instance nor a string.
953

954
        :note:
955
            - Creating a non-capturing group out of a non-capturing group does nothing, \
956
              except for reset its flags, e.g. ``is_case_insensitive``, if it has any.
957
            - Creating a non-capturing group out of a capturing group converts it into \
958
              a non-capturing group.
959
        '''
960
        if self.__type == _Type.Empty:
3✔
961
            return self
3✔
962
        elif self.__type == _Type.Group:
3✔
963
            if self.__pattern.startswith('(?P'):
3✔
964
                # Remove name from named capturing group.
965
                pattern = _re.sub('\(\?P<[^>]*>', f'(?:', str(self))
3✔
966
            elif self.__pattern.startswith('(?'):
3✔
967
                # Remove any possible flags from non-capturing group.
968
                pattern = _re.sub(
3✔
969
                    r'\(\?[i]*:', f"(?{'i' if is_case_insensitive else ''}:",
970
                    self.__pattern,
971
                    count=1)
972
            else:
973
                # Else convert capturing group to non-capturing group.
974
                pattern = self.__pattern.replace('(', '(?:', 1)
3✔
975
        else:
976
            pattern = f"(?{'i' if is_case_insensitive else ''}:{self})"
3✔
977
        return __class__(pattern, escape=False)
3✔
978

979

980
    '''
2✔
981
    Assertions
982
    '''
983
    def match_at_start(self) -> 'Pregex':
3✔
984
        '''
985
        Applies assertion ``\\A`` to this instance's underlying pattern \
986
        so that it only matches if it is found at the start of a string, \
987
        and returns the resulting pattern as a ``Pregex`` instance.
988

989
        :note: The resulting pattern cannot have a repeating quantifier \
990
            applied to it.
991
        '''
992
        return __class__(f"\\A{self._assert_conditional_group()}", escape=False)
3✔
993

994

995
    def match_at_end(self) -> 'Pregex':
3✔
996
        '''
997
        Applies assertion ``\\Z`` to this instance's underlying pattern \
998
        so that it only matches if it is found at the end of a string, \
999
        and returns the resulting pattern as a ``Pregex`` instance.
1000

1001
        :note: The resulting pattern cannot have a repeating quantifier \
1002
            applied to it.
1003
        '''
1004
        return __class__(f"{self._assert_conditional_group()}\\Z", escape=False)
3✔
1005

1006

1007
    def match_at_line_start(self) -> 'Pregex':
3✔
1008
        '''
1009
        Applies assertion ``^`` to this instance's underlying pattern \
1010
        so that it only matches if it is found at the start of a line, \
1011
        and returns the resulting pattern as a ``Pregex`` instance.
1012

1013
        :note:
1014
            - The resulting pattern cannot have a repeating quantifier \
1015
                applied to it.
1016
            - Uses meta character ``^`` since the `MULTILINE` flag is \
1017
                considered on.
1018
        '''
1019
        return __class__(f"^{self._assert_conditional_group()}", escape=False)
3✔
1020

1021

1022
    def match_at_line_end(self) -> 'Pregex':
3✔
1023
        '''
1024
        Applies assertion ``$`` to this instance's underlying pattern \
1025
        so that it only matches if it is found at the end of a line, \
1026
        and returns the resulting pattern as a ``Pregex`` instance.
1027

1028
        :note:
1029
            - The resulting pattern cannot have a repeating quantifier\
1030
                applied to it.
1031
            - Uses meta character ``$`` since the `MULTILINE` flag is \
1032
                considered on.
1033
        '''
1034
        return __class__(f"{self._assert_conditional_group()}$", escape=False)
3✔
1035

1036

1037
    def followed_by(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1038
        '''
1039
        Applies positive lookahead assertion ``(?=<PRE>)``, where \
1040
        ``<PRE>`` corresponds to the provided pattern, to this \
1041
        instance's underlying pattern and returns the resulting pattern \
1042
        as a ``Pregex`` instance.
1043

1044
        :param str | Pregex pre: A Pregex instance or string \
1045
            representing the "assertion" pattern.
1046

1047
        :raises InvalidArgumentTypeException: The provided argument \
1048
            is neither a ``Pregex`` instance nor a string.
1049

1050
        :note: The resulting pattern cannot have a repeating quantifier \
1051
            applied to it.
1052
        '''
1053
        pre = __class__._to_pregex(pre)
3✔
1054
        if pre._get_type() == _Type.Empty:
3✔
1055
            return self
3✔
1056
        return __class__(
3✔
1057
            f"{self._assert_conditional_group()}(?={pre})",
1058
            escape=False)
1059

1060

1061
    def preceded_by(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1062
        '''
1063
        Applies positive lookbehind assertion ``(?<=<PRE>)``, where \
1064
        ``<PRE>`` corresponds to the provided pattern, to this \
1065
        instance's underlying pattern and returns the resulting pattern \
1066
        as a ``Pregex`` instance.
1067

1068
        :param str | Pregex pre: A Pregex instance or string \
1069
            representing the "assertion" pattern.
1070

1071
        :raises InvalidArgumentTypeException: The provided argument \
1072
            is neither a ``Pregex`` instance nor a string.
1073
        :raises NonFixedWidthPatternException: A non-fixed-width pattern \
1074
            is provided in place of parameter ``assertion``.
1075

1076
        :note: The resulting pattern cannot have a repeating quantifier \
1077
            applied to it.
1078
        '''
1079
        pre = __class__._to_pregex(pre)
3✔
1080
        if pre._get_type() == _Type.Empty:
3✔
1081
            return self
3✔
1082
        if _re.search(_re.sub(r"\s", "", r"""
3✔
1083
            (?<!\\)(?:\\\\)*(?<!\()(?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})|
1084
            (?<!\\)(?:\\\\)*\\\((?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})
1085
        """), str(pre)) is not None:
1086
            raise _ex.NonFixedWidthPatternException(pre)
3✔
1087
        return __class__(
3✔
1088
            f"(?<={pre}){self._assert_conditional_group()}",
1089
            escape=False)
1090

1091

1092
    def enclosed_by(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1093
        '''
1094
        Applies both positive lookahead assertion ``(?=<PRE>)`` and positive \
1095
        lookbehind assertion ``(?<=<PRE>)``, where ``<PRE>`` corresponds to \
1096
        the provided pattern, to this instance's underlying pattern and \
1097
        returns the resulting pattern as a ``Pregex`` instance.
1098

1099
        :param str | Pregex pre: A Pregex instance or string \
1100
            representing the "assertion" pattern.
1101

1102
        :raises InvalidArgumentTypeException: The provided argument \
1103
            is neither a ``Pregex`` instance nor a string.
1104
        :raises NonFixedWidthPatternException: A non-fixed-width pattern \
1105
            is provided in place of parameter ``assertion``.
1106

1107
        :note: The resulting pattern cannot have a repeating quantifier \
1108
            applied to it.
1109
        '''
1110
        pre = __class__._to_pregex(pre)
3✔
1111
        if pre._get_type() == _Type.Empty:
3✔
1112
            return self
3✔
1113
        if _re.search(_re.sub(r"\s", "", r"""
3✔
1114
            (?<!\\)(?:\\\\)*(?<!\()(?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})|
1115
            (?<!\\)(?:\\\\)*\\\((?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})
1116
        """), str(pre)) is not None:
1117
            raise _ex.NonFixedWidthPatternException(pre)
3✔
1118
        return __class__(
3✔
1119
            f"(?<={pre}){self._assert_conditional_group()}(?={pre})",
1120
            escape=False)
1121

1122

1123
    def not_followed_by(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1124
        '''
1125
        Applies negative lookahead assertion ``(?!<PRE>)``, where ``<PRE>`` \
1126
        corresponds to the provided pattern, to this instance's underlying \
1127
        pattern and returns the resulting pattern as a ``Pregex`` instance.
1128

1129
        :param Pregex | str pre: Either a string or a ``Pregex`` instance \
1130
            representing the "assertion" pattern.
1131

1132
        :raises InvalidArgumentTypeException: The provided argument is neither \
1133
            a ``Pregex`` instance nor a string.
1134
        :raises EmptyNegativeAssertionException: The provided assertion pattern \
1135
            is the empty-string pattern.
1136
        '''
1137
        pre = __class__._to_pregex(pre)
3✔
1138
        if pre._get_type() == _Type.Empty:
3✔
1139
            raise _ex.EmptyNegativeAssertionException()
3✔
1140
        pattern = f"{self._assert_conditional_group()}(?!{pre})"
3✔
1141
        return __class__(pattern, escape=False)
3✔
1142

1143

1144
    def not_preceded_by(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1145
        '''
1146
        Applies negative lookbehind assertion ``(?<!<PRE>)``, where ``<PRE>`` \
1147
        corresponds to the provided pattern, to this instance's underlying \
1148
        pattern and returns the resulting pattern as a ``Pregex`` instance.
1149

1150
        :param Pregex | str pre: Either a string or a ``Pregex`` instance \
1151
            representing the "assertion" pattern.
1152

1153
        :raises InvalidArgumentTypeException: The provided argument is neither \
1154
            a ``Pregex`` instance nor a string.
1155
        :raises EmptyNegativeAssertionException: The provided assertion pattern \
1156
            is the empty-string pattern.
1157
        :raises NonFixedWidthPatternException: The provided assertion pattern \
1158
            does not have a fixed width.
1159
        '''
1160
        pre = __class__._to_pregex(pre)
3✔
1161
        if pre._get_type() == _Type.Empty:
3✔
1162
            raise _ex.EmptyNegativeAssertionException()
3✔
1163
        if _re.search(_re.sub(r"\s", "", r"""
3✔
1164
            (?<!\\)(?:\\\\)*(?<!\()(?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})|
1165
            (?<!\\)(?:\\\\)*\\\((?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})
1166
        """), str(pre)) is not None:
1167
            raise _ex.NonFixedWidthPatternException(pre)
3✔
1168
        pattern = f"(?<!{pre}){self._assert_conditional_group()}"
3✔
1169
        return __class__(pattern, escape=False)
3✔
1170

1171

1172
    def not_enclosed_by(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1173
        '''
1174
        Applies both negative lookahead assertion ``(?=<PRE>)``` and \
1175
        negative lookbehind assertion ``(?<!<PRE>)``, where ``<PRE>`` \
1176
        corresponds to the provided pattern, to this instance's underlying \
1177
        pattern and returns the resulting pattern as a ``Pregex`` instance.
1178

1179
        :param Pregex | str pre: Either a string or a ``Pregex`` instance \
1180
            representing the "assertion" pattern.
1181

1182
        :raises InvalidArgumentTypeException: The provided argument is neither \
1183
            a ``Pregex`` instance nor a string.
1184
        :raises EmptyNegativeAssertionException: The provided assertion pattern \
1185
            is the empty-string pattern.
1186
        :raises NonFixedWidthPatternException: The provided assertion pattern \
1187
            does not have a fixed width.
1188
        '''
1189
        pre = __class__._to_pregex(pre)
3✔
1190
        if pre._get_type() == _Type.Empty:
3✔
1191
            raise _ex.EmptyNegativeAssertionException()
3✔
1192
        if _re.search(_re.sub(r"\s", "", r"""
3✔
1193
            (?<!\\)(?:\\\\)*(?<!\()(?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})|
1194
            (?<!\\)(?:\\\\)*\\\((?:\?|\*|\+|\{,\d+\}|\{\d+,\}|\{\d+,\d+\})
1195
        """), str(pre)) is not None:
1196
            raise _ex.NonFixedWidthPatternException(pre)
3✔
1197
        pattern = f"(?<!{pre}){self._assert_conditional_group()}(?!{pre})"
3✔
1198
        return __class__(pattern, escape=False)
3✔
1199

1200

1201
    '''
2✔
1202
    Protected Methods
1203
    '''
1204
    def _get_type(self) -> _Type:
3✔
1205
        '''
1206
        Returns the type of this instance's underlying pattern.
1207
        '''
1208
        return self.__type
3✔
1209

1210

1211
    def _is_repeatable(self) -> bool:
3✔
1212
        '''
1213
        Returns ``True`` if this pattern can be quantified, \
1214
        else returns ``False``.
1215
        '''
1216
        return self.__repeatable
3✔
1217

1218

1219
    def _concat_conditional_group(self) -> str:
3✔
1220
        '''
1221
        Returns this instance's underlying pattern wrapped within a \
1222
        non-capturing group only if the instance's "group-on-concat" \
1223
        rule is set to ``True``, else returns it as it is.
1224
        '''
1225
        return str(self.group()) if self.__get_group_on_concat_rule() else str(self)
3✔
1226

1227

1228
    def _quantify_conditional_group(self) -> str:
3✔
1229
        '''
1230
        Returns this instance's underlying pattern wrapped within a \
1231
        non-capturing group only if the instance's "group-on-quantify" \
1232
        rule is set to ``True``, else returns it as it is.
1233
        '''
1234
        return str(self.group()) if self.__get_group_on_quantify_rule() else str(self)
3✔
1235

1236

1237
    def _assert_conditional_group(self) -> str:
3✔
1238
        '''
1239
        Returns this instance's underlying pattern wrapped within a \
1240
        non-capturing group only if the instance's "group-on-assertion" \
1241
        rule is set to ``True``, else returns it as it is.
1242
        '''
1243
        return str(self.group()) if self.__get_group_on_assert_rule() else str(self)
3✔
1244

1245

1246
    @staticmethod
3✔
1247
    def _to_pregex(pre: 'Pregex' or str) -> 'Pregex':
3✔
1248
        '''
1249
        Returns ``pre`` exactly as provided if it is a ``Pregex`` instance, \
1250
        else if it is a string, this method returns it wrapped within a ``Pregex`` \
1251
        instance for which parameter ``escape`` has been set to ``True``.
1252

1253
        :param Pregex | str: Either a string or a ``Pregex`` instance.
1254

1255
        :raises InvalidArgumentTypeException: Argument ``pre`` is neither a string nor a \
1256
            ``Pregex`` class instance.
1257
        '''
1258
        if isinstance(pre, str):
3✔
1259
            return Pregex(pre, escape=True)
3✔
1260
        elif issubclass(pre.__class__, __class__):
3✔
1261
            return pre
3✔
1262
        else:
1263
            message = "Parameter \"pre\" must either be a string or an instance of \"Pregex\"."
3✔
1264
            raise _ex.InvalidArgumentTypeException(message)
3✔
1265

1266

1267
    ''' 
2✔
1268
    Private Methods
1269
    '''
1270
    def __str__(self) -> str:
3✔
1271
        '''
1272
        Returns the string representation of this instance's \
1273
        underlying pattern.
1274

1275
        :note: Not to be used for pattern-display purposes.
1276
        '''
1277
        return self.__pattern
3✔
1278

1279

1280
    def __repr__(self) -> str:
3✔
1281
        '''
1282
        Returns the string representation of this instance's \
1283
        underlying pattern in a printable format.
1284
        '''
1285
        # Replace any quadraple backslashes.
1286
        return _re.sub(r"\\\\", r"\\", repr(self.__pattern)[1:-1])
3✔
1287
        
1288

1289
    def __add__(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1290
        '''
1291
        Concatenates this instance's underlying pattern with the provided \
1292
        pattern and returns the resulting ``Pregex`` instance.
1293

1294
        :param pre: Either a string or ``Pregex`` class instance that is to \
1295
            be concatenated to this instance's underlying pattern. 
1296
        '''
1297
        return __class__(str(self.concat(__class__._to_pregex(pre))), escape=False)
3✔
1298

1299

1300
    def __radd__(self, pre: _Union['Pregex', str]) -> 'Pregex':
3✔
1301
        '''
1302
        Concatenates this instance's underlying pattern with the provided \
1303
        pattern and returns the resulting ``Pregex`` instance.
1304

1305
        :param pre: Either a string or ``Pregex`` class instance that is to \
1306
            be concatenated to this instance's underlying pattern. 
1307
        '''
1308
        return __class__(str(__class__._to_pregex(pre).concat(self)), escape=False)
3✔
1309

1310

1311
    def __mul__(self, n: int) -> 'Pregex':
3✔
1312
        '''
1313
        Applies quantifier ``{n}`` to this instance's underlying pattern \
1314
        and returns the result as a ``Pregex`` instance.
1315

1316
        :param int n: The exact number of times that the patterns is to be matched.
1317

1318
        :raises InvalidArgumentTypeException: Parameter ``n`` is not an integer.
1319
        :raises InvalidArgumentValueException: Parameter ``n`` has a value of less \
1320
            than zero.
1321
        :raises CannotBeRepeatedException: Parameter ``n`` has a value of greater \
1322
            than one, while this instance represents a non-repeatable pattern.
1323
        '''
1324
        if not self._is_repeatable():
3✔
1325
            raise _ex.CannotBeRepeatedException(self)
3✔
1326
        if not isinstance(n, int) or isinstance(n, bool):
3✔
1327
            message = "Provided argument \"n\" is not an integer."
3✔
1328
            raise _ex.InvalidArgumentTypeException(message)
3✔
1329
        if n < 0:
3✔
1330
            message = "Using multiplication operator with a negative integer is not allowed."
3✔
1331
            raise _ex.InvalidArgumentValueException(message)
3✔
1332
        if self._get_type() == _Type.Empty:
3✔
1333
            return self
3✔
1334
        return __class__(str(self.exactly(n)), escape=False)
3✔
1335

1336

1337
    def __rmul__(self, n: int) -> 'Pregex':
3✔
1338
        '''
1339
        Applies quantifier ``{n}`` to this instance's underlying pattern \
1340
        and returns the result as a ``Pregex`` instance.
1341

1342
        :param int n: The exact number of times that the patterns is to be matched.
1343

1344
        :raises InvalidArgumentTypeException: Parameter ``n`` is not an integer.
1345
        :raises InvalidArgumentValueException: Parameter ``n`` has a value of less \
1346
            than zero.
1347
        :raises CannotBeRepeatedException: Parameter ``n`` has a value of greater \
1348
            than one, while this instance represents a non-repeatable pattern.
1349
        '''
1350
        if not self._is_repeatable():
3✔
1351
            raise _ex.CannotBeRepeatedException(self)
3✔
1352
        if not isinstance(n, int) or isinstance(n, bool):
3✔
1353
            message = "Provided argument \"n\" is not an integer."
3✔
1354
            raise _ex.InvalidArgumentTypeException(message)
3✔
1355
        if n < 0:
3✔
1356
            message = "Using multiplication operator with a negative integer is not allowed."
3✔
1357
            raise _ex.InvalidArgumentValueException(message)
3✔
1358
        if self._get_type() == _Type.Empty:
3✔
1359
            return self
3✔
1360
        return __class__(str(self.exactly(n)), escape=False)
3✔
1361

1362

1363
    def __get_group_on_concat_rule(self) -> bool:
3✔
1364
        '''
1365
        Returns the value of this instance's "group-on-concat" rule.
1366
        '''
1367
        return __class__.__groupping_rules[self.__type][0]
3✔
1368

1369

1370
    def __get_group_on_quantify_rule(self) -> bool:
3✔
1371
        '''
1372
        Returns the value of this instance's "group-on-quantify" rule.
1373
        '''
1374
        return __class__.__groupping_rules[self.__type][1]
3✔
1375

1376

1377
    def __get_group_on_assert_rule(self) -> bool:
3✔
1378
        '''
1379
        Returns the value of this instance's "group-on-assertion" rule.
1380
        '''
1381
        return __class__.__groupping_rules[self.__type][2]
3✔
1382

1383

1384
    def __iterate_match_objects(self, source: str, is_path: bool) -> _Iterator[_re.Match]:
3✔
1385
        '''
1386
        Invokes ``re.finditer`` in order to iterate over all matches of this \
1387
        instance's underlying pattern with the provided text as instances of \
1388
        type ``re.Match``.
1389

1390
        :param str source: The text that is to be examined.
1391
        :param bool is_path: If set to ``True``, then parameter ``source`` \
1392
            is considered to be a local path pointing to the file from which \
1393
            the text is to be read.
1394
        '''
1395
        if is_path:
3✔
1396
            source = self.__extract_text(source)
3✔
1397
        return _re.finditer(self.__pattern, source, flags=self.__flags) \
3✔
1398
            if self.__compiled is None else self.__compiled.finditer(source)
1399

1400

1401
    @staticmethod
3✔
1402
    def __escape(pattern: str) -> str:
3✔
1403
        '''
1404
        Scans this instance's underlying pattern for any characters that need to \
1405
        be escaped, escapes them if there are any, and returns the resulting \
1406
        pattern as a string.
1407
        '''
1408
        pattern = pattern.replace("\\", "\\\\")
3✔
1409
        for c in {'^', '$', '(', ')', '[', ']', '{', '}', '?', '+', '*', '.', '|', '/'}:
3✔
1410
            pattern = pattern.replace(c, f"\\{c}")
3✔
1411
        return pattern   
3✔
1412

1413

1414
    @staticmethod
3✔
1415
    def __infer_type(pattern: str) -> tuple[_Type, bool]:
3✔
1416
        '''
1417
        Examines the provided RegEx pattern and returns its type, \
1418
        as well as a boolean indicating whether said pattern can be \
1419
        quantified or not.
1420

1421
        :param str pattern: The RegEx pattern that is to be examined.
1422
        '''
1423
        def remove_groups(pattern: str, repl: str = ''):
3✔
1424
            '''
1425
            Removes all groups from the provided pattern, and replaces them with ``repl``.
1426

1427
            :param str pattern: The pattern whose groups are to be removed.
1428
            :param str repl: The string that replaces all groups within the pattern. \
1429
                Defaults to ``''``.
1430
            '''
1431
            left_par, right_par = r"(?:(?<!\\)\()", r"(?:(?<!\\)\))"
3✔
1432
            if len(_re.findall(left_par, pattern)) == 0:
3✔
1433
                return pattern
3✔
1434
            temp = _re.sub(pattern=left_par + r"(?:[^\(\)]|\\(?:\(|\)))+" + right_par,
3✔
1435
                repl=repl, string=pattern)
1436
            return temp if temp == repl else remove_groups(temp, repl)
3✔
1437

1438
        def __is_group(pattern: str) -> bool:
3✔
1439
            '''
1440
            Looks at the underlying pattern of this instance, and returns either \
1441
            ``True`` or ``False``, depending on whether the provided RegEx pattern \
1442
            represents a group or not.
1443

1444
            :param str pattern: The pattern that is to be examined.
1445
            '''
1446
            if pattern.startswith('(') and pattern.endswith(')'):
3✔
1447
                n_open = 0
3✔
1448
                for i in range(1, len(pattern) - 1):
3✔
1449
                    prev_char, curr_char = pattern[i-1], pattern[i]
3✔
1450
                    if prev_char != "\\": 
3✔
1451
                        if curr_char == ')':
3✔
1452
                            if n_open == 0:
3✔
1453
                                return False
3✔
1454
                            else:
1455
                                n_open -= 1
3✔
1456
                        if curr_char == '(':
3✔
1457
                            n_open += 1
3✔
1458
                return n_open == 0
3✔
1459
            return False
3✔
1460

1461
        # Replace escaped backslashes with some other character.
1462
        pattern = _re.sub(r"\\{2}", "a", pattern)
3✔
1463

1464
        if pattern == "":
3✔
1465
            return _Type.Empty, True
3✔
1466
        elif _re.fullmatch(r"\\?.", pattern, flags=__class__.__flags) is not None:
3✔
1467
            if _re.fullmatch(r"\.|\\(?:w|d|s)", pattern,
3✔
1468
                flags=__class__.__flags | _re.IGNORECASE) is not None:
1469
                return _Type.Class, True
3✔
1470
            elif _re.fullmatch(r"\\b", pattern,
3✔
1471
                flags=__class__.__flags | _re.IGNORECASE) is not None:
1472
                return _Type.Assertion, True
3✔
1473
            else:
1474
                return _Type.Token, True
3✔
1475

1476
        # Simplify classes by removing extra characters.
1477
        pattern = _re.sub(r"\[.+?(?<!\\)\]", "[a]", pattern)
3✔
1478

1479
        if pattern == "[a]":
3✔
1480
            return _Type.Class, True
3✔
1481
        elif __is_group(pattern):
3✔
1482
            return _Type.Group, True
3✔
1483

1484
        # Replace every group with a simple character.
1485
        temp = remove_groups(pattern, repl="G")
3✔
1486

1487
        if len(_re.split(pattern=r"(?<!\\)\|", string=temp)) > 1:
3✔
1488
                return _Type.Alternation, True
3✔
1489
        elif _re.fullmatch(r"(?:\^|\\A|\(\?<=.+\)).+|.+(?:\$|\\Z|\(\?=.+\))",
3✔
1490
            pattern, flags=__class__.__flags) is not None:
1491
            return _Type.Assertion, False
3✔
1492
        elif _re.fullmatch(r"(?:\\b|\\B|\(\?<!.+\)).+|.+(?:\\b|\\B|\(\?!.+\))",
3✔
1493
            pattern, flags=__class__.__flags) is not None:
1494
            return _Type.Assertion, True
3✔
1495
        elif _re.fullmatch(r"(?:\\.|[^\\])?(?:\?|\*|\+|\{(?:\d+|\d+,|,\d+|\d+,\d+)\})",
3✔
1496
            temp, flags=__class__.__flags) is not None:
1497
            return _Type.Quantifier, True
3✔
1498
        return _Type.Other, True
3✔
1499

1500

1501
    @staticmethod
3✔
1502
    def __extract_text(source: str) -> str:
3✔
1503
        '''
1504
        Reads and returns the text that is contained within the file \
1505
        to which the provided path points.
1506

1507
        :param str source: The path pointing to the file from which the text \
1508
            is to be extracted.
1509
        '''
1510
        with open(file=source, mode='r', encoding='utf-8') as f:
3✔
1511
            text = f.read()
3✔
1512
        return text
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc