Skip to content

Commit c6e80fc

Browse files
authored
Various fixes by @mundanevision20 (#288)
- Ensure custom selectors or namespace dictionaries reject non-string keys - Fix handling of `:in-range` and `:out-of-range` with end of year weeks - Fix a potential infinite loop in pretty printing of compiled selector objects
1 parent f899797 commit c6e80fc

8 files changed

Lines changed: 59 additions & 23 deletions

File tree

docs/src/markdown/about/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ icon: lucide/scroll-text
33
---
44
# Changelog
55

6+
## 2.8.2
7+
8+
- **FIX**: Ensure custom selectors or namespace dictionaries reject non-string keys (@mundanevision20).
9+
- **FIX**: Fix handling of `:in-range` and `:out-of-range` with end of year weeks (@mundanevision20).
10+
- **FIX**: Fix a potential infinite loop in pretty printing of compiled selector objects (@mundanevision20).
11+
612
## 2.8.1
713

814
- **FIX**: Changes in tests to accommodate latest Python HTML parser changes.

soupsieve/__meta__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
193193
return Version(major, minor, micro, release, pre, post, dev)
194194

195195

196-
__version_info__ = Version(2, 8, 1, "final")
196+
__version_info__ = Version(2, 8, 2, "final")
197197
__version__ = __version_info__._get_canonical()

soupsieve/css_match.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,18 @@ def validate_day(year: int, month: int, day: int) -> bool:
447447
def validate_week(year: int, week: int) -> bool:
448448
"""Validate week."""
449449

450-
max_week = datetime.strptime(f"{12}-{31}-{year}", "%m-%d-%Y").isocalendar()[1]
451-
if max_week == 1:
452-
max_week = 53
450+
# Validate an ISO week number for `year`.
451+
#
452+
# Per ISO 8601 rules, the last ISO week of a year is the week
453+
# containing Dec 28. Using Dec 28 guarantees we obtain the
454+
# correct ISO week-number for the final week of `year`, even in
455+
# years where Dec 31 falls in ISO week 01 of the following year.
456+
#
457+
# Example: if Dec 31 is a Thursday the year's last ISO week will
458+
# be week 53; if Dec 31 is a Monday and that week is counted as
459+
# week 1 of the next year, Dec 28 still belongs to the final
460+
# week of the current ISO year and yields the correct max week.
461+
max_week = datetime(year, 12, 28).isocalendar()[1]
453462
return 1 <= week <= max_week
454463

455464
@staticmethod

soupsieve/css_types.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ def __init__(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None:
155155
def _validate(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None:
156156
"""Validate arguments."""
157157

158-
if isinstance(arg, dict):
159-
if not all(isinstance(v, str) for v in arg.values()):
160-
raise TypeError(f'{self.__class__.__name__} values must be hashable')
161-
elif not all(isinstance(k, str) and isinstance(v, str) for k, v in arg):
162-
raise TypeError(f'{self.__class__.__name__} keys and values must be Unicode strings')
158+
if not all(
159+
isinstance(k, str) and isinstance(v, str)
160+
for k, v in (arg.items() if isinstance(arg, dict) else arg)
161+
):
162+
raise TypeError(f'{self.__class__.__name__} values must be hashable')
163163

164164

165165
class CustomSelectors(ImmutableDict):
@@ -173,11 +173,11 @@ def __init__(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None:
173173
def _validate(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None:
174174
"""Validate arguments."""
175175

176-
if isinstance(arg, dict):
177-
if not all(isinstance(v, str) for v in arg.values()):
178-
raise TypeError(f'{self.__class__.__name__} values must be hashable')
179-
elif not all(isinstance(k, str) and isinstance(v, str) for k, v in arg):
180-
raise TypeError(f'{self.__class__.__name__} keys and values must be Unicode strings')
176+
if not all(
177+
isinstance(k, str) and isinstance(v, str)
178+
for k, v in (arg.items() if isinstance(arg, dict) else arg)
179+
):
180+
raise TypeError(f'{self.__class__.__name__} values must be hashable')
181181

182182

183183
class Selector(Immutable):

soupsieve/pretty.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
import re
7171
from typing import Any
7272

73-
RE_CLASS = re.compile(r'(?i)[a-z_][_a-z\d\.]+\(')
73+
RE_CLASS = re.compile(r'(?i)[a-z_][_a-z\d.]+\(')
7474
RE_PARAM = re.compile(r'(?i)[_a-z][_a-z\d]+=')
7575
RE_EMPTY = re.compile(r'\(\)|\[\]|\{\}')
7676
RE_LSTRT = re.compile(r'\[')
@@ -80,11 +80,12 @@
8080
RE_DEND = re.compile(r'\}')
8181
RE_TEND = re.compile(r'\)')
8282
RE_INT = re.compile(r'\d+')
83-
RE_KWORD = re.compile(r'(?i)[_a-z][_a-z\d]+')
83+
RE_KWORD = re.compile(r'(?i)[_a-z][_a-z\d.]+')
8484
RE_DQSTR = re.compile(r'"(?:\\.|[^"\\])*"')
8585
RE_SQSTR = re.compile(r"'(?:\\.|[^'\\])*'")
8686
RE_SEP = re.compile(r'\s*(,)\s*')
8787
RE_DSEP = re.compile(r'\s*(:)\s*')
88+
RE_PSEP = re.compile(r'\s*(\|)\s*')
8889

8990
TOKENS = {
9091
'class': RE_CLASS,
@@ -99,6 +100,7 @@
99100
'sqstr': RE_SQSTR,
100101
'sep': RE_SEP,
101102
'dsep': RE_DSEP,
103+
'psep': RE_PSEP,
102104
'int': RE_INT,
103105
'kword': RE_KWORD,
104106
'dqstr': RE_DQSTR
@@ -134,6 +136,13 @@ def pretty(obj: Any) -> str: # pragma: no cover
134136
output.append(f'{m.group(1)}\n{" " * indent}')
135137
elif name in ('dsep',):
136138
output.append(f'{m.group(1)} ')
139+
elif name in ('psep'):
140+
output.append(f' {m.group(1)} ')
137141
break
138142

143+
# We shouldn't hit this, but if we do, store unrecognized character
144+
if m is None: # pragma: no cover
145+
output.append(sel[index])
146+
index += 1
147+
139148
return ''.join(output)

tests/test_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ def test_invalid_namespace_hashable_key(self):
538538
with self.assertRaises(TypeError):
539539
sv.ct.Namespaces({{}: 'string'})
540540

541+
def test_invalid_namespace_key(self):
542+
"""Test custom selector key is hashable."""
543+
544+
with self.assertRaises(TypeError):
545+
sv.ct.Namespaces({3: 'string'})
546+
541547
def test_invalid_custom_type(self):
542548
"""Test invalid custom selector type."""
543549

@@ -556,6 +562,12 @@ def test_invalid_custom_hashable_key(self):
556562
with self.assertRaises(TypeError):
557563
sv.ct.CustomSelectors({{}: 'string'})
558564

565+
def test_invalid_custom_key(self):
566+
"""Test custom selector key is hashable."""
567+
568+
with self.assertRaises(TypeError):
569+
sv.ct.CustomSelectors({3: 'string'})
570+
559571
def test_invalid_type_input_match(self):
560572
"""Test bad input into the match API."""
561573

tests/test_level4/test_in_range.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ def test_in_range_week(self):
107107
<input id="0" type="week" min="1980-W53" max="2004-W20" value="1999-W05">
108108
<input id="1" type="week" min="1980-W53" max="2004-W20" value="1980-W53">
109109
<input id="2" type="week" min="1980-W53" max="2004-W20" value="2004-W20">
110-
<input id="3" type="week" min="1980-W53" value="1999-W05">
111110
<input id="4" type="week" max="2004-W20" value="1999-W05">
112111
<input id="5" type="week" min="1980-W53" max="2004-W20" value="2005-W53">
113112
<input id="6" type="week" min="1980-W53" max="2004-W20" value="2005-w52">
114113
<input id="7" type="week" min="1980-W53" max="2004-W20">
115-
116-
<!-- These should not match -->
117114
<input id="8" type="week" min="1980-W53" max="2004-W20" value="1979-W53">
118115
<input id="9" type="week" min="1980-W53" max="2004-W20" value="1980-W52">
116+
117+
<!-- These should not match -->
118+
<input id="3" type="week" min="1980-W53" value="1999-W05">
119119
<input id="10" type="week" min="1980-W53" max="2004-W20" value="2005-W20">
120120
<input id="11" type="week" min="1980-W53" max="2004-W20" value="2004-W21">
121121
@@ -127,7 +127,7 @@ def test_in_range_week(self):
127127
self.assert_selector(
128128
markup,
129129
":in-range",
130-
['0', '1', '2', '3', '4', '5', '6', '7'],
130+
['0', '1', '2', '4', '5', '6', '7', '8', '9'],
131131
flags=util.HTML
132132
)
133133

tests/test_level4/test_out_of_range.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ def test_out_of_range_week(self):
112112
<input id="5" type="week" min="1980-W53" max="2004-W20" value="2005-W53">
113113
<input id="6" type="week" min="1980-W53" max="2004-W20" value="2005-w52">
114114
<input id="7" type="week" min="1980-W53" max="2004-W20">
115-
116-
<!-- These should match -->
117115
<input id="8" type="week" min="1980-W53" max="2004-W20" value="1979-W53">
118116
<input id="9" type="week" min="1980-W53" max="2004-W20" value="1980-W52">
117+
118+
<!-- These should match -->
119119
<input id="10" type="week" min="1980-W53" max="2004-W20" value="2005-W20">
120120
<input id="11" type="week" min="1980-W53" max="2004-W20" value="2004-W21">
121121
@@ -127,7 +127,7 @@ def test_out_of_range_week(self):
127127
self.assert_selector(
128128
markup,
129129
":out-of-range",
130-
['8', '9', '10', '11'],
130+
['10', '11'],
131131
flags=util.HTML
132132
)
133133

0 commit comments

Comments
 (0)