-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
bpo-40503: Add compile-time configuration to PEP 615 #20034
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
35dfa40
93c03aa
45146f1
ea7afea
b5de778
6802178
9292b3c
0cad151
c12439b
7fa32f2
3e4d81d
87d8c51
2324a30
3c8427b
4f12629
88ae102
9cc8073
3d30dcb
8159b90
507f5c2
874dca0
44b3135
d490586
d9fc16f
07fc66d
d55872c
d7996a6
ddb8244
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,7 @@ jobs: | |
./python -m venv .venv | ||
source ./.venv/bin/activate | ||
python -m pip install -U coverage | ||
python -m pip install -r Misc/requirements-test.txt | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also as discussed, coverage is the one exception, but it's also not a core part of the PR workflow. We already switch CI providers for PRs every time Victor gets annoyed at network instability, so let's try and keep PR clean. We can leave it in the post-merge workflow if you really want. |
||
python -m test.pythoninfo | ||
- name: 'Tests with coverage' | ||
run: > | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -546,6 +546,7 @@ def get_config_vars(*args): | |
|
||
if os.name == 'nt': | ||
_init_non_posix(_CONFIG_VARS) | ||
_CONFIG_VARS['TZPATH'] = '' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should be guaranteeing that it's set, and it should be an error condition otherwise. The "reasonable default" varies by platform, and can be configured at build time. On Windows, the reasonable default is actually an empty string (no tzdata), but also I'd like to add it as a flag at build time. |
||
if os.name == 'posix': | ||
_init_posix(_CONFIG_VARS) | ||
# For backward compatibility, see issue19555 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .test_zoneinfo import * |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import unittest | ||
|
||
unittest.main('test.test_zoneinfo') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import contextlib | ||
import functools | ||
import sys | ||
import threading | ||
import unittest | ||
from test.support import import_fresh_module | ||
|
||
OS_ENV_LOCK = threading.Lock() | ||
TZPATH_LOCK = threading.Lock() | ||
TZPATH_TEST_LOCK = threading.Lock() | ||
|
||
|
||
def call_once(f): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
"""Decorator that ensures a function is only ever called once.""" | ||
lock = threading.Lock() | ||
cached = functools.lru_cache(None)(f) | ||
|
||
@functools.wraps(f) | ||
def inner(): | ||
with lock: | ||
return cached() | ||
|
||
return inner | ||
|
||
|
||
@call_once | ||
def get_modules(): | ||
"""Retrieve two copies of zoneinfo: pure Python and C accelerated. | ||
|
||
Because this function manipulates the import system in a way that might | ||
be fragile or do unexpected things if it is run many times, it uses a | ||
`call_once` decorator to ensure that this is only ever called exactly | ||
one time — in other words, when using this function you will only ever | ||
get one copy of each module rather than a fresh import each time. | ||
""" | ||
import zoneinfo as c_module | ||
|
||
py_module = import_fresh_module("zoneinfo", blocked=["_czoneinfo"]) | ||
|
||
return py_module, c_module | ||
|
||
|
||
@contextlib.contextmanager | ||
def set_zoneinfo_module(module): | ||
"""Make sure sys.modules["zoneinfo"] refers to `module`. | ||
|
||
This is necessary because `pickle` will refuse to serialize | ||
an type calling itself `zoneinfo.ZoneInfo` unless `zoneinfo.ZoneInfo` | ||
refers to the same object. | ||
""" | ||
|
||
NOT_PRESENT = object() | ||
old_zoneinfo = sys.modules.get("zoneinfo", NOT_PRESENT) | ||
sys.modules["zoneinfo"] = module | ||
yield | ||
if old_zoneinfo is not NOT_PRESENT: | ||
sys.modules["zoneinfo"] = old_zoneinfo | ||
else: # pragma: nocover | ||
sys.modules.pop("zoneinfo") | ||
|
||
|
||
class ZoneInfoTestBase(unittest.TestCase): | ||
@classmethod | ||
def setUpClass(cls): | ||
cls.klass = cls.module.ZoneInfo | ||
super().setUpClass() | ||
|
||
@contextlib.contextmanager | ||
def tzpath_context(self, tzpath, lock=TZPATH_LOCK): | ||
with lock: | ||
old_path = self.module.TZPATH | ||
try: | ||
self.module.reset_tzpath(tzpath) | ||
yield | ||
finally: | ||
self.module.reset_tzpath(old_path) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
""" | ||
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed, I don't like relying on pip to install into the source tree before we've run the test suite.
In general, system-wide dependencies are installed using the system package manager, or downloaded using specialised scripts and an existing Python installation (see
PCbuild/get-externals.py
). This would be a new network dependency, and I'd rather keep us to things that are checked in or directly grabbed from GitHub.