Replace pkg_resources with importlib for Python >= 3.9

Fixes #1184
This commit is contained in:
Unrud 2022-04-04 18:17:01 +02:00
parent a97093d001
commit 2b8f4b9419
6 changed files with 88 additions and 45 deletions

View File

@ -29,13 +29,11 @@ import os
import threading
from typing import Iterable, Optional, cast
import pkg_resources
from radicale import config, log, types
from radicale import config, log, types, utils
from radicale.app import Application
from radicale.log import logger
VERSION: str = pkg_resources.get_distribution("radicale").version
VERSION: str = utils.package_version("radicale")
_application_instance: Optional[Application] = None
_application_config_path: Optional[str] = None

View File

@ -24,13 +24,25 @@ Helper functions for HTTP.
import contextlib
import os
import pathlib
import sys
import time
from http import client
from typing import List, Mapping, cast
from typing import List, Mapping, Union, cast
from radicale import config, pathutils, types
from radicale.log import logger
if sys.version_info < (3, 9):
import pkg_resources
_TRAVERSABLE_LIKE_TYPE = pathlib.Path
else:
import importlib.abc
from importlib import resources
_TRAVERSABLE_LIKE_TYPE = Union[importlib.abc.Traversable, pathlib.Path]
NOT_ALLOWED: types.WSGIResponse = (
client.FORBIDDEN, (("Content-Type", "text/plain"),),
"Access to the requested resource forbidden.")
@ -140,36 +152,63 @@ def redirect(location: str, status: int = client.FOUND) -> types.WSGIResponse:
"Redirected to %s" % location)
def serve_folder(folder: str, base_prefix: str, path: str,
path_prefix: str = "/.web", index_file: str = "index.html",
mimetypes: Mapping[str, str] = MIMETYPES,
fallback_mimetype: str = FALLBACK_MIMETYPE,
) -> types.WSGIResponse:
def _serve_traversable(
traversable: _TRAVERSABLE_LIKE_TYPE, base_prefix: str, path: str,
path_prefix: str, index_file: str, mimetypes: Mapping[str, str],
fallback_mimetype: str) -> types.WSGIResponse:
if path != path_prefix and not path.startswith(path_prefix):
raise ValueError("path must start with path_prefix: %r --> %r" %
(path_prefix, path))
assert pathutils.sanitize_path(path) == path
try:
filesystem_path = pathutils.path_to_filesystem(
folder, path[len(path_prefix):].strip("/"))
except ValueError as e:
logger.debug("Web content with unsafe path %r requested: %s",
path, e, exc_info=True)
parts_path = path[len(path_prefix):].strip('/')
parts = parts_path.split("/") if parts_path else []
for part in parts:
if not pathutils.is_safe_filesystem_path_component(part):
logger.debug("Web content with unsafe path %r requested", path)
return NOT_FOUND
if os.path.isdir(filesystem_path) and not path.endswith("/"):
if (not traversable.is_dir() or
all(part != entry.name for entry in traversable.iterdir())):
return NOT_FOUND
traversable = traversable.joinpath(part)
if traversable.is_dir():
if not path.endswith("/"):
return redirect(base_prefix + path + "/")
if os.path.isdir(filesystem_path) and index_file:
filesystem_path = os.path.join(filesystem_path, index_file)
if not os.path.isfile(filesystem_path):
if not index_file:
return NOT_FOUND
traversable = traversable.joinpath(index_file)
if not traversable.is_file():
return NOT_FOUND
content_type = MIMETYPES.get(
os.path.splitext(filesystem_path)[1].lower(), FALLBACK_MIMETYPE)
with open(filesystem_path, "rb") as f:
answer = f.read()
last_modified = time.strftime(
os.path.splitext(traversable.name)[1].lower(), FALLBACK_MIMETYPE)
headers = {"Content-Type": content_type}
if isinstance(traversable, pathlib.Path):
headers["Last-Modified"] = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT",
time.gmtime(os.fstat(f.fileno()).st_mtime))
headers = {
"Content-Type": content_type,
"Last-Modified": last_modified}
time.gmtime(traversable.stat().st_mtime))
answer = traversable.read_bytes()
return client.OK, headers, answer
def serve_resource(
package: str, resource: str, base_prefix: str, path: str,
path_prefix: str = "/.web", index_file: str = "index.html",
mimetypes: Mapping[str, str] = MIMETYPES,
fallback_mimetype: str = FALLBACK_MIMETYPE) -> types.WSGIResponse:
if sys.version_info < (3, 9):
traversable = pathlib.Path(
pkg_resources.resource_filename(package, resource))
else:
traversable = resources.files(package).joinpath(resource)
return _serve_traversable(traversable, base_prefix, path, path_prefix,
index_file, mimetypes, fallback_mimetype)
def serve_folder(
folder: str, base_prefix: str, path: str,
path_prefix: str = "/.web", index_file: str = "index.html",
mimetypes: Mapping[str, str] = MIMETYPES,
fallback_mimetype: str = FALLBACK_MIMETYPE) -> types.WSGIResponse:
# deprecated: use `serve_resource` instead
traversable = pathlib.Path(folder)
return _serve_traversable(traversable, base_prefix, path, path_prefix,
index_file, mimetypes, fallback_mimetype)

View File

@ -29,7 +29,6 @@ from hashlib import sha256
from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set,
Tuple, Union, overload)
import pkg_resources
import vobject
from radicale import config
@ -41,7 +40,7 @@ INTERNAL_TYPES: Sequence[str] = ("multifilesystem", "multifilesystem_nolock",)
CACHE_DEPS: Sequence[str] = ("radicale", "vobject", "python-dateutil",)
CACHE_VERSION: bytes = "".join(
"%s=%s;" % (pkg, pkg_resources.get_distribution(pkg).version)
"%s=%s;" % (pkg, utils.package_version(pkg))
for pkg in CACHE_DEPS).encode()

View File

@ -16,12 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import sys
from importlib import import_module
from typing import Callable, Sequence, Type, TypeVar, Union
from radicale import config
from radicale.log import logger
if sys.version_info < (3, 8):
import pkg_resources
else:
from importlib import metadata
_T_co = TypeVar("_T_co", covariant=True)
@ -43,3 +49,9 @@ def load_plugin(internal_types: Sequence[str], module_name: str,
(module_name, module, e)) from e
logger.info("%s type is %r", module_name, module)
return class_(configuration)
def package_version(name):
if sys.version_info < (3, 8):
return pkg_resources.get_distribution(name).version
return metadata.version(name)

View File

@ -25,9 +25,7 @@ Features:
"""
import pkg_resources
from radicale import config, httputils, types, web
from radicale import httputils, types, web
MIMETYPES = httputils.MIMETYPES # deprecated
FALLBACK_MIMETYPE = httputils.FALLBACK_MIMETYPE # deprecated
@ -35,13 +33,7 @@ FALLBACK_MIMETYPE = httputils.FALLBACK_MIMETYPE # deprecated
class Web(web.BaseWeb):
folder: str
def __init__(self, configuration: config.Configuration) -> None:
super().__init__(configuration)
self.folder = pkg_resources.resource_filename(
__name__, "internal_data")
def get(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
user: str) -> types.WSGIResponse:
return httputils.serve_folder(self.folder, base_prefix, path)
return httputils.serve_resource("radicale.web", "internal_data",
base_prefix, path)

View File

@ -49,6 +49,10 @@ WEB_FILES = ["web/internal_data/css/icon.png",
"web/internal_data/fn.js",
"web/internal_data/index.html"]
install_requires = ["defusedxml", "passlib", "vobject>=0.9.6",
"python-dateutil>=2.7.3"]
if sys.version_info < (3, 9):
install_requires.append("setuptools")
setup_requires = []
if {"pytest", "test", "ptr"}.intersection(sys.argv):
setup_requires.append("pytest-runner")
@ -76,8 +80,7 @@ setup(
exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
package_data={"radicale": [*WEB_FILES, "py.typed"]},
entry_points={"console_scripts": ["radicale = radicale.__main__:run"]},
install_requires=["defusedxml", "passlib", "vobject>=0.9.6",
"python-dateutil>=2.7.3", "setuptools"],
install_requires=install_requires,
setup_requires=setup_requires,
tests_require=tests_require,
extras_require={"test": tests_require,