mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 05:31:20 +00:00 
			
		
		
		
	 62972d9d73
			
		
	
	
		62972d9d73
		
			
		
	
	
	
	
		
			
			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()
 |