A year limit inherited from datetime: range matching for week inputs in soupsieve
In soupsieve, the CSS selector library behind Beautiful Soup, :in-range and :out-of-range raised ValueError for a week input with a week-year of 10000 or more. I fixed it in PR #322, merged about 25 minutes after opening.
When it happens
import soupsieve as sv
from bs4 import BeautifulSoup
soup = BeautifulSoup('<input id="a" type="week" min="10000-W01" max="10000-W52" value="10000-W20">', 'html.parser')
sv.select(':in-range', soup)
It raised ValueError: year 10000 is out of range instead of returning the input. The HTML spec says a week string’s year is “Four or more ASCII digits”, so 10000-W20 is valid. Every release from 1.7, where these selectors were added, through 3.0.3 raises. Such years are rare; one extra digit, as in 20255-W01, is enough.
How the last week is found
A week-year has 52 or 53 weeks, and W53 is valid only in a 53-week year. validate_week() in soupsieve/css_match.py took the last week from the ISO week of datetime(year, 12, 28), a day always in the last week.
Why the gap existed
The week pattern has taken four or more digits since :in-range arrived in #61 in 2019. That code used strptime, which also fails past 9999. #288, released in 2.8.2, replaced it with datetime(year, 12, 28) to fix end-of-year weeks, with the same limit. No test used a week-year above 9999.
The fix
The soupsieve/css_match.py diff, without context lines:
@@ -450 +450,5 @@ class Inputs:
- max_week = datetime(year, 12, 28).isocalendar()[1]
+ #
+ # `datetime` stops at year 9999, but the calendar repeats every 400
+ # years (a whole number of weeks), so use the matching year from 2000
+ # to 2399, as `calendar.weekday()` does.
+ max_week = datetime(2000 + year % 400, 12, 28).isocalendar()[1]
400 years are 146097 days, or 20871 whole weeks, so a year and the year 400 later share weekdays and week count. Old and new validate_week() agree for every year from 1 to 9999 and weeks 0 to 60.
The test
Two rows joined each selector’s week test. 10000-W53 does not parse, since 10000 has 52 weeks, so it is never out of range and :in-range matches it. 10004-W53 parses, since 10004 has 53 weeks, and is above the max 10004-W52, so :out-of-range matches it. With -k week, 2 failed before the fix and 2 passed after.
What the maintainer did
@facelessuser approved and merged it with no change requested, and the code and tests merged as I opened them.
The lesson
- When a type cannot hold your input, check whether the answer repeats. A year’s week count repeats every 400 years, and
calendar.weekday()folds years the same way. - A modulo fold needs a base when zero is invalid:
10000 % 400is 0 anddatetimehas no year 0, so the fix adds 2000. Test a value whose remainder is 0;10001would pass without it.
If your parser takes more digits than the type behind it, reply on X.