Coverage for src/backend/InvenTree/InvenTree/format.py: 84%
49 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Custom string formatting functions and helpers."""
3import re
4import string
6from django.utils.translation import gettext_lazy as _
9def parse_format_string(fmt_string: str) -> dict:
10 """Extract formatting information from the provided format string.
12 Returns a dict object which contains structured information about the format groups
13 """
14 groups = string.Formatter().parse(fmt_string)
16 info = {}
18 seen_groups = set()
20 for group in groups:
21 # Skip any group which does not have a named value
22 if not group[1]: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 continue
25 name = group[1]
27 # Check for duplicate named groups
28 if name in seen_groups: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 raise ValueError(f"Duplicate group '{name}'")
30 else:
31 seen_groups.add(name)
33 info[group[1]] = {'format': group[1], 'prefix': group[0]}
35 return info
38def construct_format_regex(fmt_string: str) -> str:
39 r"""Construct a regular expression based on a provided format string.
41 This function turns a python format string into a regular expression,
42 which can be used for two purposes:
44 - Ensure that a particular string matches the specified format
45 - Extract named variables from a matching string
47 This function also provides support for wildcard characters:
49 - '?' provides single character matching; is converted to a '.' (period) for regex
50 - '#' provides single digit matching; is converted to '\d'
52 Args:
53 fmt_string: A typical format string e.g. "PO-???-{ref:04d}"
55 Returns:
56 str: A regular expression pattern e.g. ^PO\-...\-(?P<ref>.*)$
58 Raises:
59 ValueError: Format string is invalid
60 """
61 pattern = '^'
63 for group in string.Formatter().parse(fmt_string):
64 prefix = group[0] # Prefix (literal text appearing before this group)
65 name = group[1] # Name of this format variable
66 _fmt = group[2] # Format specifier e.g :04d
68 rep = [
69 '+',
70 '-',
71 '.',
72 '{',
73 '}',
74 '(',
75 ')',
76 '^',
77 '$',
78 '~',
79 '!',
80 '@',
81 ':',
82 ';',
83 '|',
84 "'",
85 '"',
86 ]
88 # Escape any special regex characters
89 for ch in rep:
90 prefix = prefix.replace(ch, '\\' + ch)
92 # Replace ? with single-character match
93 prefix = prefix.replace('?', '.')
95 # Replace # with single-digit match
96 prefix = prefix.replace('#', r'\d')
98 pattern += prefix
100 # Add a named capture group for the format entry
101 if name: 101 ↛ 63line 101 didn't jump to line 63 because the condition on line 101 was always true
102 # Check if integer values are required
103 c = '\\d' if _fmt and _fmt.endswith('d') else '.'
105 # Specify width
106 # TODO: Introspect required width
107 w = '+'
109 # replace invalid regex group name '?' with a valid name
110 if name == '?': 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 name = 'wild'
113 pattern += f'(?P<{name}>{c}{w})'
115 pattern += '$'
117 return pattern
120def validate_string(value: str, fmt_string: str) -> bool:
121 """Validate that the provided string matches the specified format.
123 Args:
124 value: The string to be tested e.g. 'SO-1234-ABC',
125 fmt_string: The required format e.g. 'SO-{ref}-???',
127 Returns:
128 bool: True if the value matches the required format, else False
130 Raises:
131 ValueError: The provided format string is invalid
132 """
133 pattern = construct_format_regex(fmt_string)
135 result = re.match(pattern, value)
137 return result is not None
140def extract_named_group(name: str, value: str, fmt_string: str) -> str:
141 """Extract a named value from the provided string, given the provided format string.
143 Args:
144 name: Name of group to extract e.g. 'ref'
145 value: Raw string e.g. 'PO-ABC-1234'
146 fmt_string: Format pattern e.g. 'PO-???-{ref}
148 Returns:
149 str: String value of the named group
151 Raises:
152 ValueError: format string is incorrectly specified, or provided value does not match format string
153 NameError: named value does not exist in the format string
154 IndexError: named value could not be found in the provided entry
155 """
156 info = parse_format_string(fmt_string)
158 if name not in info: 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true
159 raise NameError(_(f"Value '{name}' does not appear in pattern format"))
161 # Construct a regular expression for matching against the provided format string
162 # Note: This will raise a ValueError if 'fmt_string' is incorrectly specified
163 pattern = construct_format_regex(fmt_string)
165 # Run the regex matcher against the raw string
166 result = re.match(pattern, value)
168 if not result: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 raise ValueError(
170 _('Provided value does not match required pattern: ') + fmt_string
171 )
173 # And return the value we are interested in
174 # Note: This will raise an IndexError if the named group was not matched
175 return result.group(name)