mirror of
https://github.com/python/cpython.git
synced 2025-07-23 19:25:40 +00:00

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.
122 lines
3.1 KiB
Python
122 lines
3.1 KiB
Python
"""
|
|
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()
|