Skip to content

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

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
35dfa40
Add tests and implementation for ZoneInfo
pganssle May 4, 2020
93c03aa
Add tzdata to Travis requirements
pganssle May 8, 2020
45146f1
Deliberately break TZData tests to see who is skipping them
pganssle May 8, 2020
ea7afea
Add test requirements to Github Actions builds
pganssle May 8, 2020
b5de778
Add test dependencies to windows AP builds
pganssle May 8, 2020
6802178
Revert "Add test dependencies to windows AP builds"
pganssle May 8, 2020
9292b3c
Revert "Deliberately break TZData tests to see who is skipping them"
pganssle May 8, 2020
0cad151
fixup! Add tests and implementation for ZoneInfo
pganssle May 9, 2020
c12439b
fixup! Add tests and implementation for ZoneInfo
pganssle May 9, 2020
7fa32f2
fixup! Add tests and implementation for ZoneInfo
pganssle May 9, 2020
3e4d81d
fixup! Add tests and implementation for ZoneInfo
pganssle May 9, 2020
87d8c51
fixup! fixup! Add tests and implementation for ZoneInfo
pganssle May 9, 2020
2324a30
Fix member def for key
pganssle May 11, 2020
3c8427b
Fix refleak in error code
pganssle May 11, 2020
4f12629
Fix refleak on cache miss
pganssle May 11, 2020
88ae102
fixup! Add tests and implementation for ZoneInfo
pganssle May 11, 2020
9cc8073
Fix memory leak in zoneinfo_dealloc
pganssle May 11, 2020
3d30dcb
Fix refleak in fromutc
pganssle May 11, 2020
8159b90
Add smoke test that C and Python modules built
pganssle May 12, 2020
507f5c2
Add Windows build support
pganssle May 12, 2020
874dca0
fixup! Add Windows build support
pganssle May 12, 2020
44b3135
Revert "fixup! Add Windows build support"
pganssle May 12, 2020
d490586
fixup! Add Windows build support
pganssle May 12, 2020
d9fc16f
Remove __version__ from __dir__
pganssle May 12, 2020
07fc66d
Include all globals in __dir__
pganssle May 12, 2020
d55872c
fixup! Add Windows build support
pganssle May 12, 2020
d7996a6
Reduce indentation in _parse_python_tzpath
pganssle May 12, 2020
ddb8244
Add --with-tzpath to autoconf
pganssle May 11, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
run: .\PCbuild\build.bat -e -p Win32
- name: Display build info
run: .\python.bat -m test.pythoninfo
- name: Install test dependencies
run: |
.\python.bat -m ensurepip --user
.\python.bat -m pip install --user -r Misc/requirements-test.txt
Copy link
Member

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.

- name: Tests
run: .\PCbuild\rt.bat -q -uall -u-cpu -rwW --slowest --timeout=1200 -j0

Expand All @@ -37,6 +41,10 @@ jobs:
run: .\PCbuild\build.bat -e -p x64
- name: Display build info
run: .\python.bat -m test.pythoninfo
- name: Install test dependencies
run: |
.\python.bat -m ensurepip --user
.\python.bat -m pip install --user -r Misc/requirements-test.txt
- name: Tests
run: .\PCbuild\rt.bat -x64 -q -uall -u-cpu -rwW --slowest --timeout=1200 -j0

Expand All @@ -51,6 +59,10 @@ jobs:
run: make -j4
- name: Display build info
run: make pythoninfo
- name: Install test dependencies
run: |
./python.exe -m ensurepip --user
./python.exe -m pip install --user -r Misc/requirements-test.txt
- name: Tests
run: make buildbottest TESTOPTS="-j4 -uall,-cpu"

Expand Down Expand Up @@ -78,5 +90,9 @@ jobs:
run: make -j4
- name: Display build info
run: make pythoninfo
- name: Install test dependencies
run: |
./python -m ensurepip --user
./python -m pip install --user -r Misc/requirements-test.txt
- name: Tests
run: xvfb-run make buildbottest TESTOPTS="-j4 -uall,-cpu"
1 change: 1 addition & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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: >
Expand Down
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ matrix:
# Need a venv that can parse covered code.
- ./python -m venv venv
- ./venv/bin/python -m pip install -U coverage
- ./venv/bin/python -m pip install -r Misc/requirements-test.txt
- ./venv/bin/python -m test.pythoninfo
script:
# Skip tests that re-run the entire test suite.
Expand Down Expand Up @@ -171,6 +172,8 @@ before_script:
fi
- make -j4
- make pythoninfo
- ./python -m ensurepip --user
- ./python -m pip install --user -r Misc/requirements-test.txt

script:
# Using the built Python as patchcheck.py is built around the idea of using
Expand Down
1 change: 1 addition & 0 deletions Lib/sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ def get_config_vars(*args):

if os.name == 'nt':
_init_non_posix(_CONFIG_VARS)
_CONFIG_VARS['TZPATH'] = ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just .get() it rather than assuming it'll be set?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_zoneinfo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .test_zoneinfo import *
3 changes: 3 additions & 0 deletions Lib/test/test_zoneinfo/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import unittest

unittest.main('test.test_zoneinfo')
76 changes: 76 additions & 0 deletions Lib/test/test_zoneinfo/_support.py
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:trollface:

"""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)
122 changes: 122 additions & 0 deletions Lib/test/test_zoneinfo/data/update_test_data.py
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()
Loading