| Server IP : 172.67.201.108 / Your IP : 216.73.216.55 Web Server : Apache/2.4.68 (Amazon Linux) OpenSSL/3.5.7 System : Linux ip-172-31-69-123.ec2.internal 6.1.177-224.371.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Mon Jul 27 20:28:29 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.24 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /lib/python3.9/site-packages/elementpath/regex/ |
Upload File : |
#
# Copyright (c), 2016-2020, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <[email protected]>
#
"""
Parse and translate XML Schema regular expressions to Python regex syntax.
"""
import re
from sys import maxunicode
from .unicode_subsets import RegexError, UnicodeSubset, unicode_subset
from .character_classes import I_SHORTCUT_REPLACE, C_SHORTCUT_REPLACE, CharacterClass
HYPHENS_PATTERN = re.compile(r'(?<!\\)--')
INVALID_HYPHEN_PATTERN = re.compile(r'[^\\]-[^\\]-[^\\]')
DIGITS_PATTERN = re.compile(r'\d+')
QUANTIFIER_PATTERN = re.compile(r'{\d+(,(\d+)?)?}')
FORBIDDEN_ESCAPES_NOREF_PATTERN = re.compile(
r'(?<!\\)\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|o{\d+}|\d+|A|Z|z|B|b|o)'
)
FORBIDDEN_ESCAPES_REF_PATTERN = re.compile(
r'(?<!\\)\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|o{\d+}|A|Z|z|B|b|o)'
)
def translate_pattern(pattern: str, flags: int = 0, xsd_version: str = '1.0',
back_references: bool = True, lazy_quantifiers: bool = True,
anchors: bool = True) -> str:
"""
Translates a pattern regex expression to a Python regex pattern. With default
options the translator processes XPath 2.0/XQuery 1.0 regex patterns. For XML
Schema patterns set all boolean options to `False`.
:param pattern: the source XML Schema regular expression.
:param flags: regex flags as represented by Python's re module.
:param xsd_version: apply regex rules of a specific XSD version, '1.0' for default.
:param back_references: if `True` supports back-references and capturing groups.
:param lazy_quantifiers: if `True` supports lazy quantifiers (\\*?, +?).
:param anchors: if `True` supports ^ and $ anchors, otherwise the translated \
pattern is anchored to its boundaries and anchors are treated as normal characters.
"""
def parse_character_class():
nonlocal pos
nonlocal msg
pos += 1
if pattern[pos] == '^':
pos += 1
negative = True
else:
negative = False
char_class_pos = pos
while True:
if pattern[pos] == '[':
msg = "invalid character '[' at position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
elif pattern[pos] == '\\':
if pattern[pos + 1].isdigit():
msg = "illegal back-reference in character class at position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
pos += 2
elif pattern[pos] == ']' or pattern[pos:pos + 2] == '-[':
if pos == char_class_pos:
msg = "empty character class at position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
char_class_pattern = pattern[char_class_pos:pos]
if HYPHENS_PATTERN.search(char_class_pattern) and pos - char_class_pos > 2:
msg = "invalid character range '--' at position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
if xsd_version == '1.0':
hyphen_match = INVALID_HYPHEN_PATTERN.search(char_class_pattern)
if hyphen_match is not None:
hyphen_pos = char_class_pos + hyphen_match.span()[1] - 2
msg = "unescaped character '-' at position {}: {!r}"
raise RegexError(msg.format(hyphen_pos, pattern))
char_class = CharacterClass(char_class_pattern, xsd_version)
if negative:
char_class.complement()
break # pragma: no cover
else:
pos += 1
if pattern[pos] != ']':
# Parse a group subtraction
pos += 1
subtracted_class = parse_character_class()
pos += 1
char_class -= subtracted_class
return char_class
group_open_char = '(' if back_references else '(?:'
regex = [] if anchors else ['^%s' % group_open_char]
pos = 0
pattern_len = len(pattern)
total_groups = 0
nested_groups = 0
dot_all = flags & re.DOTALL
if back_references:
match = FORBIDDEN_ESCAPES_REF_PATTERN.search(pattern)
else:
match = FORBIDDEN_ESCAPES_NOREF_PATTERN.search(pattern)
if match:
msg = "not allowed escape sequence {!r} at position {}: {!r}"
raise RegexError(msg.format(match.group(), match.span()[0], pattern))
while pos < pattern_len:
ch = pattern[pos]
if ch == '.':
regex.append(ch if dot_all else '[^\r\n]')
elif ch in ('^', '$'):
if not anchors:
regex.append(r'\%s' % ch)
elif ch == '^':
regex.append(r'(?<!\n\Z)^' if flags & re.MULTILINE else '^')
else:
regex.append('$' if flags & re.MULTILINE else r'$(?!\n\Z)')
elif ch == '[':
try:
char_class_repr = str(parse_character_class())
except IndexError:
msg = "unterminated character class at position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
else:
if char_class_repr == '[]':
regex.append(r'[^\w\W]')
else:
regex.append(char_class_repr)
elif ch == '{':
if pos == 0:
msg = "unexpected quantifier {!r} at position {}: {!r}"
raise RegexError(msg.format(ch, pos, pattern))
match = QUANTIFIER_PATTERN.match(pattern[pos:])
if match is None:
msg = "invalid quantifier {!r} at position {}: {!r}"
raise RegexError(msg.format(ch, pos, pattern))
regex.append(match.group())
pos += len(match.group())
if not lazy_quantifiers and pos < pattern_len and pattern[pos] in ('?', '+', '*'):
msg = "unexpected meta character {!r} at position {}: {!r}"
raise RegexError(msg.format(pattern[pos], pos, pattern))
continue # pragma: no cover
elif ch == '(':
if pattern[pos:pos + 2] == '(?':
msg = "invalid '(?...)' extension notation ad position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
total_groups += 1
nested_groups += 1
regex.append(group_open_char)
elif ch == ']':
msg = "unexpected meta character {!r} at position {}: {!r}"
raise RegexError(msg.format(ch, pos, pattern))
elif ch == ')':
if nested_groups == 0:
msg = "unbalanced parenthesis ')' at position {}: {!r}"
raise RegexError(msg.format(pos, pattern))
nested_groups -= 1
regex.append(ch)
elif ch in ('?', '+', '*'):
if pos == 0:
msg = "unexpected quantifier {!r} at position {}: {!r}"
raise RegexError(msg.format(ch, pos, pattern))
elif lazy_quantifiers:
pass
elif pos < pattern_len - 1 and pattern[pos + 1] in ('?', '+', '*', '{'):
msg = "unexpected meta character {!r} at position {}: {!r}"
raise RegexError(msg.format(pattern[pos + 1], pos + 1, pattern))
regex.append(ch)
elif ch == '\\':
pos += 1
if flags & re.VERBOSE:
while pos < pattern_len and pattern[pos] == ' ':
pos += 1
if pos >= pattern_len:
regex.append('\\')
elif pattern[pos].isdigit():
regex.append('\\%s' % pattern[pos])
reference = DIGITS_PATTERN.match(pattern[pos:]).group() # type: ignore[union-attr]
if len(reference) > 1:
k = 0
for k in range(1, len(reference)):
if total_groups < int(reference[:k + 1]):
regex.append('[%s]' % pattern[pos + k])
break
else:
regex.append(pattern[pos + k])
pos += k # pragma: no cover
elif pattern[pos] == 'i':
regex.append('[%s]' % I_SHORTCUT_REPLACE)
elif pattern[pos] == 'I':
regex.append('[^%s]' % I_SHORTCUT_REPLACE)
elif pattern[pos] == 'c':
regex.append('[%s]' % C_SHORTCUT_REPLACE)
elif pattern[pos] == 'C':
regex.append('[^%s]' % C_SHORTCUT_REPLACE)
elif pattern[pos] in 'pP':
block_pos = pos - 1
try:
if pattern[pos + 1] != '{':
raise RegexError("a '{' expected, found %r." % pattern[pos + 1])
while pattern[pos] != '}':
pos += 1
except (IndexError, ValueError):
msg = "truncated unicode block escape at position {}: {!r}"
raise RegexError(msg.format(block_pos, pattern))
block_name = pattern[block_pos + 3:pos]
if flags & re.VERBOSE:
# spaces are completely collapsed in verbose regex patterns
block_name = block_name.replace(' ', '')
try:
p_shortcut_set = unicode_subset(block_name)
except RegexError:
# XSD 1.1 supports Is prefix to match Unicode blocks
if xsd_version == '1.0' or not block_name.startswith('Is'):
raise
p_shortcut_group = '[%s]' % UnicodeSubset([(0, maxunicode)])
else:
if pattern[block_pos + 1] == 'p':
p_shortcut_group = '[%s]' % p_shortcut_set
else:
p_shortcut_group = '[^%s]' % p_shortcut_set
if flags & re.IGNORECASE:
regex.append('(?-i:%s)' % p_shortcut_group)
else:
regex.append(p_shortcut_group)
else:
regex.append('\\%s' % pattern[pos])
else:
regex.append(ch)
pos += 1
if nested_groups > 0:
raise RegexError("unterminated subpattern in expression: %r" % pattern)
if not anchors:
regex.append(r')$(?!\n\Z)')
return ''.join(regex)