bpo-40503: PEP 615: Tests and implementation for zoneinfo (GH-19909)

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.
This commit is contained in:
Paul Ganssle 2020-05-16 04:20:06 -04:00 committed by GitHub
parent 6e8cda91d9
commit 62972d9d73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 6383 additions and 2 deletions

View file

@ -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):
"""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=["_zoneinfo"])
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)