Skip to content

Commit

Permalink
Replace pkg_resources with importlib for Python >= 3.9
Browse files Browse the repository at this point in the history
Fixes #1184
  • Loading branch information
Unrud committed Apr 4, 2022
1 parent a97093d commit 2b8f4b9
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 45 deletions.
6 changes: 2 additions & 4 deletions radicale/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@
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
Expand Down
91 changes: 65 additions & 26 deletions radicale/httputils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,25 @@

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.")
Expand Down Expand Up @@ -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)
return NOT_FOUND
if os.path.isdir(filesystem_path) and 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):
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 (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 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)
3 changes: 1 addition & 2 deletions radicale/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set,
Tuple, Union, overload)

import pkg_resources
import vobject

from radicale import config
Expand All @@ -41,7 +40,7 @@

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()


Expand Down
12 changes: 12 additions & 0 deletions radicale/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -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)
14 changes: 3 additions & 11 deletions radicale/web/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,15 @@
"""

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


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)
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"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")
Expand Down Expand Up @@ -76,8 +80,7 @@
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,
Expand Down

0 comments on commit 2b8f4b9

Please sign in to comment.