Skip to content

Commit

Permalink
Switch to more recent 'files' API from importlib.resources from Pytho…
Browse files Browse the repository at this point in the history
…n 3.9 and importlib_metadata 1.1. Add explicit temporary file cleanup behavior using atexit. Fixes certifi#131.
  • Loading branch information
jaraco committed Jan 24, 2021
1 parent 45a6465 commit 1c47d26
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 47 deletions.
17 changes: 17 additions & 0 deletions certifi/compat.py
@@ -0,0 +1,17 @@
"""
Fallback for Python prior to 3.9 where importlib_resources
is not available.
Does not support modules where __file__ is not defined on
the module.
"""

import os


def read_text(_module, _path):
with open(where(), "r", encoding="ascii") as data:
return data.read()


def where():
return os.path.join(os.path.dirname(__file__), "cacert.pem")
52 changes: 5 additions & 47 deletions certifi/core.py
Expand Up @@ -6,55 +6,13 @@
This module returns the installation location of cacert.pem or its contents.
"""
import os

try:
from importlib.resources import path as get_path, read_text

_CACERT_CTX = None
_CACERT_PATH = None

def where():
# This is slightly terrible, but we want to delay extracting the file
# in cases where we're inside of a zipimport situation until someone
# actually calls where(), but we don't want to re-extract the file
# on every call of where(), so we'll do it once then store it in a
# global variable.
global _CACERT_CTX
global _CACERT_PATH
if _CACERT_PATH is None:
# This is slightly janky, the importlib.resources API wants you to
# manage the cleanup of this file, so it doesn't actually return a
# path, it returns a context manager that will give you the path
# when you enter it and will do any cleanup when you leave it. In
# the common case of not needing a temporary file, it will just
# return the file system location and the __exit__() is a no-op.
#
# We also have to hold onto the actual context manager, because
# it will do the cleanup whenever it gets garbage collected, so
# we will also store that at the global level as well.
_CACERT_CTX = get_path("certifi", "cacert.pem")
_CACERT_PATH = str(_CACERT_CTX.__enter__())

return _CACERT_PATH


except ImportError:
# This fallback will work for Python versions prior to 3.7 that lack the
# importlib.resources module but relies on the existing `where` function
# so won't address issues with environments like PyOxidizer that don't set
# __file__ on modules.
def read_text(_module, _path, encoding="ascii"):
with open(where(), "r", encoding=encoding) as data:
return data.read()

# If we don't have importlib.resources, then we will just do the old logic
# of assuming we're on the filesystem and munge the path directly.
def where():
f = os.path.dirname(__file__)

return os.path.join(f, "cacert.pem")
try:
from .resources import where, read_text
except Exception:
from .compat import where, read_text # noqa: F401


def contents():
return read_text("certifi", "cacert.pem", encoding="ascii")
return read_text("certifi", "cacert.pem")
28 changes: 28 additions & 0 deletions certifi/resources.py
@@ -0,0 +1,28 @@
import atexit
import functools

try:
from importlib import resources
except ImportError:
import importlib_resources as resources


# ensure 'files' API is present
resources.files
read_text = resources.read_text


def as_file(path):
"""
Ensure the path is a file on the file system for the duration
of the interpreter run.
"""
ctx = resources.as_file(path)
tmp_copy = ctx.__enter__()
atexit.register(tmp_copy.__exit__, None, None, None)
return tmp_copy


@functools.lru_cache()
def where():
return as_file(resources.files('certifi') / 'cacert.pem')

0 comments on commit 1c47d26

Please sign in to comment.