bpo-40503: PEP 615: Tests and implementation for zoneinfo (GH-19909)
This is the initial implementation of PEP 615, the zoneinfo module,
ported from the standalone reference implementation (see
https://www.python.org/dev/peps/pep-0615/#reference-implementation for a
link, which has a more detailed commit history).
This includes (hopefully) all functional elements described in the PEP,
but documentation is found in a separate PR. This includes:
1. A pure python implementation of the ZoneInfo class
2. A C accelerated implementation of the ZoneInfo class
3. Tests with 100% branch coverage for the Python code (though C code
coverage is less than 100%).
4. A compile-time configuration option on Linux (though not on Windows)
Differences from the reference implementation:
- The module is arranged slightly differently: the accelerated module is
`_zoneinfo` rather than `zoneinfo._czoneinfo`, which also necessitates
some changes in the test support function. (Suggested by Victor
Stinner and Steve Dower.)
- The tests are arranged slightly differently and do not include the
property tests. The tests live at test/test_zoneinfo/test_zoneinfo.py
rather than test/test_zoneinfo.py or test/test_zoneinfo/__init__.py
because we may do some refactoring in the future that would likely
require this separation anyway; we may:
- include the property tests
- automatically run all the tests against both pure Python and C,
rather than manually constructing C and Python test classes (similar
to the way this works with test_datetime.py, which generates C
and Python test cases from datetimetester.py).
- This includes a compile-time configuration option on Linux (though not
on Windows); added with much help from Thomas Wouters.
- Integration into the CPython build system is obviously different from
building a standalone zoneinfo module wheel.
- This includes configuration to install the tzdata package as part of
CI, though only on the coverage jobs. Introducing a PyPI dependency as
part of the CI build was controversial, and this is seen as less of a
major change, since the coverage jobs already depend on pip and PyPI.
Additional changes that were introduced as part of this PR, most / all of
which were backported to the reference implementation:
- Fixed reference and memory leaks
With much debugging help from Pablo Galindo
- Added smoke tests ensuring that the C and Python modules are built
The import machinery can be somewhat fragile, and the "seamlessly falls
back to pure Python" nature of this module makes it so that a problem
building the C extension or a failure to import the pure Python version
might easily go unnoticed.
- Adjustments to zoneinfo.__dir__
Suggested by Petr Viktorin.
- Slight refactorings as suggested by Steve Dower.
- Removed unnecessary if check on std_abbr
Discovered this because of a missing line in branch coverage.
2020-05-16 04:20:06 -04:00
|
|
|
"""
|
|
|
|
|
Script to automatically generate a JSON file containing time zone information.
|
|
|
|
|
|
|
|
|
|
This is done to allow "pinning" a small subset of the tzdata in the tests,
|
|
|
|
|
since we are testing properties of a file that may be subject to change. For
|
|
|
|
|
example, the behavior in the far future of any given zone is likely to change,
|
|
|
|
|
but "does this give the right answer for this file in 2040" is still an
|
|
|
|
|
important property to test.
|
|
|
|
|
|
|
|
|
|
This must be run from a computer with zoneinfo data installed.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import base64
|
|
|
|
|
import functools
|
|
|
|
|
import json
|
|
|
|
|
import lzma
|
|
|
|
|
import pathlib
|
|
|
|
|
import textwrap
|
|
|
|
|
import typing
|
|
|
|
|
|
|
|
|
|
import zoneinfo
|
|
|
|
|
|
|
|
|
|
KEYS = [
|
|
|
|
|
"Africa/Abidjan",
|
|
|
|
|
"Africa/Casablanca",
|
|
|
|
|
"America/Los_Angeles",
|
|
|
|
|
"America/Santiago",
|
|
|
|
|
"Asia/Tokyo",
|
|
|
|
|
"Australia/Sydney",
|
|
|
|
|
"Europe/Dublin",
|
|
|
|
|
"Europe/Lisbon",
|
|
|
|
|
"Europe/London",
|
|
|
|
|
"Pacific/Kiritimati",
|
|
|
|
|
"UTC",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
TEST_DATA_LOC = pathlib.Path(__file__).parent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
|
|
|
def get_zoneinfo_path() -> pathlib.Path:
|
|
|
|
|
"""Get the first zoneinfo directory on TZPATH containing the "UTC" zone."""
|
|
|
|
|
key = "UTC"
|
|
|
|
|
for path in map(pathlib.Path, zoneinfo.TZPATH):
|
|
|
|
|
if (path / key).exists():
|
|
|
|
|
return path
|
|
|
|
|
else:
|
|
|
|
|
raise OSError("Cannot find time zone data.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_zoneinfo_metadata() -> typing.Dict[str, str]:
|
|
|
|
|
path = get_zoneinfo_path()
|
|
|
|
|
|
|
|
|
|
tzdata_zi = path / "tzdata.zi"
|
|
|
|
|
if not tzdata_zi.exists():
|
|
|
|
|
# tzdata.zi is necessary to get the version information
|
|
|
|
|
raise OSError("Time zone data does not include tzdata.zi.")
|
|
|
|
|
|
|
|
|
|
with open(tzdata_zi, "r") as f:
|
|
|
|
|
version_line = next(f)
|
|
|
|
|
|
|
|
|
|
_, version = version_line.strip().rsplit(" ", 1)
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
not version[0:4].isdigit()
|
|
|
|
|
or len(version) < 5
|
|
|
|
|
or not version[4:].isalpha()
|
|
|
|
|
):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Version string should be YYYYx, "
|
|
|
|
|
+ "where YYYY is the year and x is a letter; "
|
|
|
|
|
+ f"found: {version}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {"version": version}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_zoneinfo(key: str) -> bytes:
|
|
|
|
|
path = get_zoneinfo_path()
|
|
|
|
|
|
|
|
|
|
with open(path / key, "rb") as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_compressed(data: bytes) -> typing.List[str]:
|
|
|
|
|
compressed_zone = lzma.compress(data)
|
|
|
|
|
raw = base64.b85encode(compressed_zone)
|
|
|
|
|
|
|
|
|
|
raw_data_str = raw.decode("utf-8")
|
|
|
|
|
|
|
|
|
|
data_str = textwrap.wrap(raw_data_str, width=70)
|
|
|
|
|
return data_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_compressed_keys() -> typing.Dict[str, typing.List[str]]:
|
|
|
|
|
output = {key: encode_compressed(get_zoneinfo(key)) for key in KEYS}
|
|
|
|
|
|
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_test_data(fname: str = "zoneinfo_data.json") -> None:
|
|
|
|
|
TEST_DATA_LOC.mkdir(exist_ok=True, parents=True)
|
|
|
|
|
|
|
|
|
|
# Annotation required: https://github.com/python/mypy/issues/8772
|
|
|
|
|
json_kwargs: typing.Dict[str, typing.Any] = dict(
|
|
|
|
|
indent=2, sort_keys=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
compressed_keys = load_compressed_keys()
|
|
|
|
|
metadata = get_zoneinfo_metadata()
|
|
|
|
|
output = {
|
|
|
|
|
"metadata": metadata,
|
|
|
|
|
"data": compressed_keys,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with open(TEST_DATA_LOC / fname, "w") as f:
|
|
|
|
|
json.dump(output, f, **json_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
update_test_data()
|
2025-12-13 18:13:55 +00:00
|
|
|
|
|
|
|
|
print("Remember to update the HAS_TZDATA_PKG version requirement in "
|
|
|
|
|
"test_zoneinfo.py!")
|