Skip to content

Commit abf15f2

Browse files
committed
update 1.8.4
1 parent d905da6 commit abf15f2

4 files changed

Lines changed: 210 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
# <br><b>Changelog</b><br>
1616

17+
<span id="v1-8-4" />
18+
19+
## ... `v1.8.4`
20+
* adjusted `Regex.hsla_str()` to not include optional degree (`°`) and percent (`%`) symbols in the captured groups
21+
* fixed that `Regex.hexa_str()` couldn't match HEXA colors anywhere inside a string, but only if the whole string was just the HEXA color
22+
1723
<span id="v1-8-3" />
1824

1925
## 08.10.2025 `v1.8.3`

src/xulbux/regex.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ def hsla_str(fix_sep: str = ",", allow_alpha: bool = True) -> str:
135135
fix_sep = r"[^0-9A-Z]"
136136
else:
137137
fix_sep = _re.escape(fix_sep)
138-
hsl_part = rf"""((?:0*(?:360|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9]))(?:\s*°)?)
139-
(?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9]))(?:\s*%)?)
140-
(?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9]))(?:\s*%)?)"""
138+
hsl_part = rf"""((?:0*(?:360|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])))(?:\s*°)?
139+
(?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))(?:\s*%)?
140+
(?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))(?:\s*%)?"""
141141
return (
142142
rf"""(?ix)
143143
(?:hsl|hsla)?\s*(?:\(?\s*{hsl_part}
@@ -157,6 +157,6 @@ def hexa_str(allow_alpha: bool = True) -> str:
157157
#### Valid ranges:
158158
every channel from 0-9 and A-F (case insensitive)"""
159159
return (
160-
r"(?i)^(?:#|0x)?[0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{4}|[0-9A-F]{3}$"
161-
if allow_alpha else r"(?i)^(?:#|0x)?[0-9A-F]{6}|[0-9A-F]{3}$"
160+
r"(?i)(?:#|0x)?([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{4}|[0-9A-F]{3})"
161+
if allow_alpha else r"(?i)(?:#|0x)?([0-9A-F]{6}|[0-9A-F]{3})"
162162
)

tests/test_regex.py

Lines changed: 183 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,11 @@ def test_regex_brackets_empty_brackets():
146146
def test_regex_brackets_with_strip_spaces():
147147
"""Test brackets pattern with strip_spaces option"""
148148
text = "Function ( spaced content ) and (normal)"
149-
pattern = Regex.brackets(strip_spaces=True)
149+
pattern = Regex.brackets(strip_spaces=True, is_group=True)
150150
matches = rx.findall(pattern, text)
151-
assert "( spaced content )" in matches
152-
assert "(normal)" in matches
153-
pattern = Regex.brackets(strip_spaces=False)
154-
matches = rx.findall(pattern, text)
155-
assert "( spaced content )" in matches
156-
assert "(normal)" in matches
151+
assert len(matches) == 2
152+
assert any("spaced content" in m for m in matches)
153+
assert "normal" in matches
157154

158155

159156
def test_regex_brackets_as_group():
@@ -165,6 +162,21 @@ def test_regex_brackets_as_group():
165162
assert match.group(1) == "content"
166163

167164

165+
def test_regex_brackets_ignore_in_strings():
166+
"""Test brackets pattern with ignore_in_strings option"""
167+
text = 'func(param = "f(x)")'
168+
pattern = Regex.brackets(ignore_in_strings=True)
169+
matches = rx.findall(pattern, text)
170+
assert len(matches) == 1
171+
assert 'param = "f(x)"' in matches[0]
172+
173+
# TEST THAT IT CORRECTLY HANDLES BRACKETS INSIDE STRINGS
174+
text2 = 'outer("inner(test)")'
175+
matches2 = rx.findall(pattern, text2)
176+
assert len(matches2) == 1
177+
assert 'inner(test)' in matches2[0]
178+
179+
168180
def test_regex_outside_strings_pattern():
169181
"""Test outside_strings method returns correct pattern"""
170182
pattern = Regex.outside_strings()
@@ -178,8 +190,25 @@ def test_regex_outside_strings_custom_pattern():
178190
text = 'Number 123 and "string 456" and 789'
179191
matches = re.findall(pattern, text)
180192
assert "123" in matches
181-
assert "789" in matches
182193
assert "456" not in matches
194+
assert "789" in matches
195+
196+
197+
def test_regex_outside_strings_with_special_chars():
198+
"""Test outside_strings with special characters"""
199+
pattern = Regex.outside_strings(r"\$")
200+
text = 'Price $100 and "cost $50" and $200'
201+
matches = re.findall(pattern, text)
202+
assert len(matches) >= 2
203+
204+
205+
def test_regex_outside_strings_complex_pattern():
206+
"""Test outside_strings with complex pattern"""
207+
pattern = Regex.outside_strings(r"[a-z]+")
208+
text = 'word1 "word2" word3 \'word4\' word5'
209+
matches = re.findall(pattern, text)
210+
assert len(matches) >= 3
211+
assert any("word" in match for match in matches)
183212

184213

185214
def test_regex_all_except_pattern():
@@ -188,16 +217,32 @@ def test_regex_all_except_pattern():
188217
assert isinstance(pattern, str)
189218

190219

220+
def test_regex_all_except_basic():
221+
"""Test all_except with basic pattern"""
222+
pattern = Regex.all_except(">")
223+
text = "Hello > World"
224+
match = re.match(pattern, text)
225+
assert match is not None
226+
assert "Hello" in match.group(0)
227+
assert ">" not in match.group(0)
228+
229+
191230
def test_regex_all_except_with_ignore():
192231
"""Test all_except with ignore pattern"""
193232
pattern = Regex.all_except(">", "->")
194-
assert isinstance(pattern, str)
233+
text = "Arrow -> here"
234+
match = re.match(pattern, text)
235+
assert match is not None
236+
assert len(match.group(0)) > 0
195237

196238

197239
def test_regex_all_except_as_group():
198240
"""Test all_except with is_group option"""
199241
pattern = Regex.all_except(">", is_group=True)
200-
assert isinstance(pattern, str)
242+
text = "Content > more"
243+
match = re.match(pattern, text)
244+
assert match is not None
245+
assert match.group(1) is not None
201246

202247

203248
def test_regex_func_call_pattern():
@@ -232,8 +277,27 @@ def test_regex_rgba_str_default_separator():
232277
"""Test rgba_str pattern with default comma separator"""
233278
text = "Color rgba(255, 128, 0) and (100, 200, 50, 0.5)"
234279
pattern = Regex.rgba_str()
235-
matches = re.findall(pattern, text, re.IGNORECASE | re.VERBOSE)
280+
matches = re.findall(pattern, text)
236281
assert len(matches) > 0
282+
assert len(matches) >= 2
283+
284+
285+
def test_regex_rgba_str_valid_values():
286+
"""Test rgba_str pattern validates correct ranges"""
287+
pattern = Regex.rgba_str()
288+
# VALID RGB VALUES IN A STRING
289+
text = "Colors: rgba(255, 255, 255, 1.0) and rgb(0, 0, 0) and (128, 128, 128) and plain 255, 128, 0"
290+
matches = re.findall(pattern, text)
291+
assert len(matches) >= 4, f"Should match all valid colors, got: {matches}"
292+
293+
# INVALID RGB VALUES (OUT OF RANGE) SHOULD NOT MATCH OR MATCH PARTIALLY
294+
text_invalid = "Invalid: rgba(256, 128, 0) and rgb(300, 0, 0)"
295+
matches_invalid = re.findall(pattern, text_invalid)
296+
# SHOULD EITHER NOT MATCH OR NOT INCLUDE THE INVALID VALUES
297+
for match in matches_invalid:
298+
match_str = str(match)
299+
assert "256" not in match_str
300+
assert "300" not in match_str
237301

238302

239303
def test_regex_rgba_str_no_alpha():
@@ -259,10 +323,36 @@ def test_regex_hsla_str_default_separator():
259323
"""Test hsla_str pattern with default comma separator"""
260324
text = "Color hsla(240, 100%, 50%) and (120, 80%, 60%, 0.8)"
261325
pattern = Regex.hsla_str()
262-
matches = re.findall(pattern, text, re.IGNORECASE | re.VERBOSE)
326+
matches = re.findall(pattern, text)
263327
assert len(matches) > 0
264328

265329

330+
def test_regex_hsla_str_valid_values():
331+
"""Test hsla_str pattern validates correct ranges"""
332+
pattern = Regex.hsla_str()
333+
# VALID HSL VALUES IN A STRING
334+
text = "Colors: hsla(360, 100%, 50%, 1.0) and hsl(0, 0%, 0%) and (180, 50%, 50%) and plain 240, 100%, 50% and with degree 120°, 80%, 60%"
335+
matches = re.findall(pattern, text)
336+
assert len(matches) >= 5, f"Should match all valid colors, got: {matches}"
337+
338+
# VERIFY THAT % AND ° SYMBOLS ARE NOT IN THE CAPTURED GROUPS
339+
for match in matches:
340+
groups = match if isinstance(match, tuple) else (match, )
341+
for group in groups:
342+
if group: # Skip empty groups
343+
assert "%" not in group, f"Percent sign should not be in captured group: {group}"
344+
assert "°" not in group, f"Degree sign should not be in captured group: {group}"
345+
346+
# INVALID HSL VALUES (OUT OF RANGE)
347+
text_invalid = "Invalid: hsla(361, 100%, 50%) and hsl(240, 101%, 50%)"
348+
matches_invalid = re.findall(pattern, text_invalid)
349+
# SHOULD EITHER NOT MATCH OR NOT INCLUDE THE INVALID VALUES
350+
for match in matches_invalid:
351+
match_str = str(match)
352+
assert "361" not in match_str
353+
assert "101" not in match_str
354+
355+
266356
def test_regex_hsla_str_no_alpha():
267357
"""Test hsla_str pattern with alpha disabled"""
268358
pattern = Regex.hsla_str(allow_alpha=False)
@@ -284,23 +374,89 @@ def test_regex_hexa_str_pattern():
284374
def test_regex_hexa_str_with_alpha():
285375
"""Test hexa_str pattern with alpha channel"""
286376
pattern = Regex.hexa_str(allow_alpha=True)
287-
test_colors = ["FF0000", "FF0000FF", "F00", "F00F"]
288-
for color in test_colors:
289-
assert re.match(pattern, color) is not None
377+
text = "Colors: FF0000 and FF0000FF and F00 and F00F and #ABCDEF and 0xF0F in text"
378+
matches = re.findall(pattern, text)
379+
assert len(matches) == 6, f"Should match all 6 colors, got: {matches}"
380+
# VERIFY ALL EXPECTED COLORS ARE CAPTURED (GROUP 1 CONTAINS THE HEX VALUE)
381+
expected = ["FF0000", "FF0000FF", "F00", "F00F", "ABCDEF", "F0F"]
382+
for exp in expected:
383+
assert any(exp.upper() == match.upper() for match in matches), f"Should match {exp}"
290384

291385

292386
def test_regex_hexa_str_no_alpha():
293387
"""Test hexa_str pattern without alpha channel"""
294388
pattern = Regex.hexa_str(allow_alpha=False)
295-
valid_colors = ["FF0000", "F00"]
296-
invalid_colors = ["FF0000FF", "F00F"]
297-
298-
for color in valid_colors:
299-
match = re.match(pattern, color)
300-
assert match is not None
301-
assert match.group() == color
302-
303-
for color in invalid_colors:
304-
match = re.match(pattern, color)
305-
if match:
306-
assert match.group() != color
389+
390+
# TEST VALID COLORS (3 AND 6 DIGIT FORMATS)
391+
text = "Valid colors: FF0000 and F00 and #ABCDEF and 0xABC in the text"
392+
matches = re.findall(pattern, text)
393+
assert len(matches) == 4, f"Should match all 4 valid colors, got: {matches}"
394+
# THE CAPTURED GROUPS SHOULD NOT INCLUDE PREFIX
395+
for hex_value in matches:
396+
assert "#" not in hex_value
397+
assert "0x" not in hex_value.lower()
398+
399+
# TEST THAT 4-DIGIT AND 8-DIGIT FORMATS ONLY PARTIALLY MATCH (FIRST 3 OR 6 CHARS)
400+
text_with_alpha = "With alpha: FF0000FF and F00F should only match the non-alpha part"
401+
matches_alpha = re.findall(pattern, text_with_alpha)
402+
# SHOULD MATCH FF0000 AND F00 (WITHOUT THE ALPHA CHANNEL)
403+
assert len(matches_alpha) == 2
404+
for hex_value in matches_alpha:
405+
assert len(hex_value) in [3, 6], f"Should only match 3 or 6 digit formats, got: {hex_value}"
406+
407+
408+
def test_regex_hexa_str_with_prefix():
409+
"""Test hexa_str pattern with optional prefixes"""
410+
pattern = Regex.hexa_str(allow_alpha=True)
411+
text = "Mixed: #FF0000 and 0xABCDEF and F00 and #F00F in text"
412+
matches = re.findall(pattern, text)
413+
assert len(matches) == 4, f"Should match all 4 colors, got: {matches}"
414+
415+
# VERIFY THE CAPTURED HEX VALUES (WITHOUT PREFIX)
416+
expected = ["FF0000", "ABCDEF", "F00", "F00F"]
417+
for exp in expected:
418+
assert any(exp.upper() == match.upper() for match in matches), f"Should capture {exp}"
419+
420+
421+
def test_regex_func_call_nested():
422+
"""Test func_call pattern with nested function calls"""
423+
text = "outer(inner(arg1, arg2), arg3)"
424+
pattern = Regex.func_call()
425+
matches = rx.findall(pattern, text)
426+
assert len(matches) >= 1
427+
func_names = [m[0] for m in matches]
428+
assert "outer" in func_names
429+
430+
431+
def test_regex_quotes_with_escapes():
432+
"""Test quotes pattern handles escaped characters properly"""
433+
text = r'He said "She said \"Hello\" to me"'
434+
pattern = Regex.quotes()
435+
matches = rx.findall(pattern, text)
436+
assert len(matches) >= 1
437+
438+
439+
def test_regex_rgba_str_without_prefix():
440+
"""Test rgba_str matches plain number format"""
441+
pattern = Regex.rgba_str()
442+
text = "255, 128, 0"
443+
match = re.search(pattern, text)
444+
assert match is not None
445+
446+
447+
def test_regex_hsla_str_without_prefix():
448+
"""Test hsla_str matches plain number format"""
449+
pattern = Regex.hsla_str()
450+
text = "240, 100%, 50%"
451+
match = re.search(pattern, text)
452+
assert match is not None
453+
454+
455+
def test_regex_brackets_deeply_nested():
456+
"""Test brackets pattern with deeply nested brackets"""
457+
text = "Level1(Level2(Level3(deepest)))"
458+
pattern = Regex.brackets()
459+
matches = rx.findall(pattern, text)
460+
assert len(matches) >= 1
461+
assert "deepest" in matches[0]
462+
assert "Level2" in matches[0]

tests/test_system.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,6 @@ def test_check_libs_decline_install(mock_input, mock_subprocess):
4242
mock_subprocess.assert_not_called()
4343

4444

45-
@pytest.mark.skipif(os.name != 'nt', reason="Windows-specific test")
46-
def test_elevate_windows_already_elevated():
47-
"""Test elevate on Windows when already elevated"""
48-
with patch.object(System, 'is_elevated', True):
49-
result = System.elevate()
50-
assert result is True
51-
52-
53-
@pytest.mark.skipif(os.name == 'nt', reason="POSIX-specific test")
54-
def test_elevate_posix_already_elevated():
55-
"""Test elevate on POSIX when already elevated"""
56-
with patch.object(System, 'is_elevated', True):
57-
result = System.elevate()
58-
assert result is True
59-
60-
6145
@patch('xulbux.system._platform.system')
6246
@patch('xulbux.system._subprocess.check_output')
6347
@patch('xulbux.system._os.system')
@@ -87,3 +71,19 @@ def test_restart_unsupported_system(mock_subprocess, mock_platform):
8771
mock_subprocess.return_value = b"some output"
8872
with pytest.raises(NotImplementedError, match="Restart not implemented for `unknown`"):
8973
System.restart()
74+
75+
76+
@pytest.mark.skipif(os.name != 'nt', reason="Windows-specific test")
77+
def test_elevate_windows_already_elevated():
78+
"""Test elevate on Windows when already elevated"""
79+
with patch.object(System, 'is_elevated', True):
80+
result = System.elevate()
81+
assert result is True
82+
83+
84+
@pytest.mark.skipif(os.name == 'nt', reason="POSIX-specific test")
85+
def test_elevate_posix_already_elevated():
86+
"""Test elevate on POSIX when already elevated"""
87+
with patch.object(System, 'is_elevated', True):
88+
result = System.elevate()
89+
assert result is True

0 commit comments

Comments
 (0)