| 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, block_tzdata=True, lock=TZPATH_LOCK): |
| def pop_tzdata_modules(): |
| tzdata_modules = {} |
| for modname in list(sys.modules): |
| if modname.split(".", 1)[0] != "tzdata": # pragma: nocover |
| continue |
| |
| tzdata_modules[modname] = sys.modules.pop(modname) |
| |
| return tzdata_modules |
| |
| with lock: |
| if block_tzdata: |
| # In order to fully exclude tzdata from the path, we need to |
| # clear the sys.modules cache of all its contents — setting the |
| # root package to None is not enough to block direct access of |
| # already-imported submodules (though it will prevent new |
| # imports of submodules). |
| tzdata_modules = pop_tzdata_modules() |
| sys.modules["tzdata"] = None |
| |
| old_path = self.module.TZPATH |
| try: |
| self.module.reset_tzpath(tzpath) |
| yield |
| finally: |
| if block_tzdata: |
| sys.modules.pop("tzdata") |
| for modname, module in tzdata_modules.items(): |
| sys.modules[modname] = module |
| |
| self.module.reset_tzpath(old_path) |