_tzpath.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import os
  2. import sysconfig
  3. def _reset_tzpath(to=None, stacklevel=4):
  4. global TZPATH
  5. tzpaths = to
  6. if tzpaths is not None:
  7. if isinstance(tzpaths, (str, bytes)):
  8. raise TypeError(
  9. f"tzpaths must be a list or tuple, "
  10. + f"not {type(tzpaths)}: {tzpaths!r}"
  11. )
  12. if not all(map(os.path.isabs, tzpaths)):
  13. raise ValueError(_get_invalid_paths_message(tzpaths))
  14. base_tzpath = tzpaths
  15. else:
  16. env_var = os.environ.get("PYTHONTZPATH", None)
  17. if env_var is None:
  18. env_var = sysconfig.get_config_var("TZPATH")
  19. base_tzpath = _parse_python_tzpath(env_var, stacklevel)
  20. TZPATH = tuple(base_tzpath)
  21. def reset_tzpath(to=None):
  22. """Reset global TZPATH."""
  23. # We need `_reset_tzpath` helper function because it produces a warning,
  24. # it is used as both a module-level call and a public API.
  25. # This is how we equalize the stacklevel for both calls.
  26. _reset_tzpath(to)
  27. def _parse_python_tzpath(env_var, stacklevel):
  28. if not env_var:
  29. return ()
  30. raw_tzpath = env_var.split(os.pathsep)
  31. new_tzpath = tuple(filter(os.path.isabs, raw_tzpath))
  32. # If anything has been filtered out, we will warn about it
  33. if len(new_tzpath) != len(raw_tzpath):
  34. import warnings
  35. msg = _get_invalid_paths_message(raw_tzpath)
  36. warnings.warn(
  37. "Invalid paths specified in PYTHONTZPATH environment variable. "
  38. + msg,
  39. InvalidTZPathWarning,
  40. stacklevel=stacklevel,
  41. )
  42. return new_tzpath
  43. def _get_invalid_paths_message(tzpaths):
  44. invalid_paths = (path for path in tzpaths if not os.path.isabs(path))
  45. prefix = "\n "
  46. indented_str = prefix + prefix.join(invalid_paths)
  47. return (
  48. "Paths should be absolute but found the following relative paths:"
  49. + indented_str
  50. )
  51. def find_tzfile(key):
  52. """Retrieve the path to a TZif file from a key."""
  53. _validate_tzfile_path(key)
  54. for search_path in TZPATH:
  55. filepath = os.path.join(search_path, key)
  56. if os.path.isfile(filepath):
  57. return filepath
  58. return None
  59. _TEST_PATH = os.path.normpath(os.path.join("_", "_"))[:-1]
  60. def _validate_tzfile_path(path, _base=_TEST_PATH):
  61. if os.path.isabs(path):
  62. raise ValueError(
  63. f"ZoneInfo keys may not be absolute paths, got: {path}"
  64. )
  65. # We only care about the kinds of path normalizations that would change the
  66. # length of the key - e.g. a/../b -> a/b, or a/b/ -> a/b. On Windows,
  67. # normpath will also change from a/b to a\b, but that would still preserve
  68. # the length.
  69. new_path = os.path.normpath(path)
  70. if len(new_path) != len(path):
  71. raise ValueError(
  72. f"ZoneInfo keys must be normalized relative paths, got: {path}"
  73. )
  74. resolved = os.path.normpath(os.path.join(_base, new_path))
  75. if not resolved.startswith(_base):
  76. raise ValueError(
  77. f"ZoneInfo keys must refer to subdirectories of TZPATH, got: {path}"
  78. )
  79. del _TEST_PATH
  80. def available_timezones():
  81. """Returns a set containing all available time zones.
  82. .. caution::
  83. This may attempt to open a large number of files, since the best way to
  84. determine if a given file on the time zone search path is to open it
  85. and check for the "magic string" at the beginning.
  86. """
  87. from importlib import resources
  88. valid_zones = set()
  89. # Start with loading from the tzdata package if it exists: this has a
  90. # pre-assembled list of zones that only requires opening one file.
  91. try:
  92. with resources.files("tzdata").joinpath("zones").open("r") as f:
  93. for zone in f:
  94. zone = zone.strip()
  95. if zone:
  96. valid_zones.add(zone)
  97. except (ImportError, FileNotFoundError):
  98. pass
  99. def valid_key(fpath):
  100. try:
  101. with open(fpath, "rb") as f:
  102. return f.read(4) == b"TZif"
  103. except Exception: # pragma: nocover
  104. return False
  105. for tz_root in TZPATH:
  106. if not os.path.exists(tz_root):
  107. continue
  108. for root, dirnames, files in os.walk(tz_root):
  109. if root == tz_root:
  110. # right/ and posix/ are special directories and shouldn't be
  111. # included in the output of available zones
  112. if "right" in dirnames:
  113. dirnames.remove("right")
  114. if "posix" in dirnames:
  115. dirnames.remove("posix")
  116. for file in files:
  117. fpath = os.path.join(root, file)
  118. key = os.path.relpath(fpath, start=tz_root)
  119. if os.sep != "/": # pragma: nocover
  120. key = key.replace(os.sep, "/")
  121. if not key or key in valid_zones:
  122. continue
  123. if valid_key(fpath):
  124. valid_zones.add(key)
  125. if "posixrules" in valid_zones:
  126. # posixrules is a special symlink-only time zone where it exists, it
  127. # should not be included in the output
  128. valid_zones.remove("posixrules")
  129. return valid_zones
  130. class InvalidTZPathWarning(RuntimeWarning):
  131. """Warning raised if an invalid path is specified in PYTHONTZPATH."""
  132. TZPATH = ()
  133. _reset_tzpath(stacklevel=5)