cls._tzif_header = bytes(out)
- def zone_from_tzstr(self, tzstr):
+ def zone_from_tzstr(self, tzstr, encoding="ascii"):
"""Creates a zoneinfo file following a POSIX rule."""
zonefile = io.BytesIO(self._tzif_header)
zonefile.seek(0, 2)
# Write the footer
zonefile.write(b"\x0A")
- zonefile.write(tzstr.encode("ascii"))
+ zonefile.write(tzstr.encode(encoding))
zonefile.write(b"\x0A")
zonefile.seek(0)
"+11", # Unquoted alphanumeric
"GMT,M3.2.0/2,M11.1.0/3", # Transition rule but no DST
"GMT0+11,M3.2.0/2,M11.1.0/3", # Unquoted alphanumeric in DST
+ # Unquoted abbreviation with embedded or leading whitespace
+ "AB C3",
+ " A B 3",
+ "AAA4BB B,J60/2,J300/2", # Embedded whitespace in DST
+ # Empty quoted abbreviation
+ "<>5",
+ "AAA4<>,M3.2.0/2,M11.1.0/3",
"PST8PDT,M3.2.0/2", # Only one transition rule
# Invalid offset hours
"AAA168",
with self.assertRaisesRegex(ValueError, tzstr_regex):
self.zone_from_tzstr(invalid_tzstr)
+ def test_invalid_tzstr_non_ascii_abbr(self):
+ tzstr = "ABÀC3"
+ if self.module is py_zoneinfo:
+ expected = re.escape(tzstr)
+ else:
+ expected = re.escape(repr(tzstr.encode("utf-8")))
+ with self.assertRaisesRegex(ValueError, expected):
+ self.zone_from_tzstr(tzstr, encoding="utf-8")
+
@classmethod
def _populate_test_cases(cls):
# This method uses a somewhat unusual style in that it populates the
parser_re = re.compile(
r"""
- (?P<std>[^<0-9:.+-]+|<[a-zA-Z0-9+-]+>)
+ (?P<std>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)
(?:
(?P<stdoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)
(?:
- (?P<dst>[^0-9:.+-]+|<[a-zA-Z0-9+-]+>)
+ (?P<dst>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)
(?P<dstoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)?
)? # dst
)? # stdoff