mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +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()
 |