Skip to content

Commit

Permalink
fix: address deprecation of werkzeug.urls.url_parse (#1143)
Browse files Browse the repository at this point in the history
* refactor: deprecate unused method Pad.make_absolute_url

* refactor!: change Url.query from URI-encoded to IRI-decoded value

* fix: disuse deprecated werkzeug.urls functions

* refactor(publisher): pass URLs as str rather than SplitResult tuples

* fix(publisher): restore compatibility with existing publisher plugins

* fix: parse_qsl has no 'separator' parameter for python < 3.7.10
  • Loading branch information
dairiki committed May 5, 2023
1 parent cf6e23a commit 5878799
Show file tree
Hide file tree
Showing 12 changed files with 662 additions and 83 deletions.
125 changes: 124 additions & 1 deletion lektor/compat.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from __future__ import annotations

import os
import stat
import sys
import tempfile
import urllib.parse
from functools import partial
from itertools import chain
from typing import Any
from urllib.parse import urlsplit

from werkzeug import urls as werkzeug_urls
from werkzeug.datastructures import MultiDict

__all__ = ["TemporaryDirectory", "importlib_metadata"]
__all__ = ["TemporaryDirectory", "importlib_metadata", "werkzeug_urls_URL"]


def _ensure_tree_writeable(path: str) -> None:
Expand Down Expand Up @@ -55,3 +63,118 @@ def cleanup(self) -> None:
else:
TemporaryDirectory = FixedTemporaryDirectory
import importlib_metadata


class _CompatURL(urllib.parse.SplitResult):
"""This is a replacement for ``werkzeug.urls.URL``.
Here we implement those attributes and methods of ``URL`` which are
likely to be used by existing Lektor publishing plugins.
Currently unreimplemented here are the ``encode_netloc``, ``decode_netloc``,
``get_file_location``, and ``encode`` methods of ``werkzeug.urls.URL``.
NB: Use of this class is deprecated. DO NOT USE THIS IN NEW CODE!
"""

def __str__(self) -> str:
return self.geturl()

def replace(self, **kwargs: Any) -> _CompatURL:
return self._replace(**kwargs)

@property
def host(self) -> str | None:
return self.hostname

@property
def ascii_host(self) -> str | None:
host = self.hostname
if host is None:
return None
try:
return host.encode("idna").decode("ascii")
except UnicodeError:
return host

@property
def auth(self) -> str | None:
auth, _, _ = self.netloc.rpartition("@")
return auth if auth != "" else None

@property
def username(self) -> str | None:
username = super().username
if username is None:
return None
return _unquote_legacy(username)

@property
def raw_username(self) -> str | None:
return super().username

@property
def password(self) -> str | None:
password = super().password
if password is None:
return None
return _unquote_legacy(password)

@property
def raw_password(self) -> str | None:
return super().password

def decode_query(
self,
charset: str = "utf-8",
include_empty: bool = True,
errors: str = "replace",
# parse_qsl does not support the separator parameter in python < 3.7.10.
# separator: str = "&",
) -> MultiDict:
return MultiDict(
urllib.parse.parse_qsl(
self.query,
keep_blank_values=include_empty,
encoding=charset,
errors=errors,
# separator=separator,
)
)

def join(
self, url: str | tuple[str, str, str, str, str], allow_fragments: bool = True
) -> _CompatURL:
if isinstance(url, tuple):
url = urllib.parse.urlunsplit(url)
joined = urllib.parse.urljoin(self.geturl(), url, allow_fragments)
return _CompatURL._make(urlsplit(joined))

def to_url(self) -> str:
return self.geturl()

def to_uri_tuple(self) -> _CompatURL:
return _CompatURL._make(urlsplit(werkzeug_urls.iri_to_uri(self.geturl())))

def to_iri_tuple(self) -> _CompatURL:
return _CompatURL._make(urlsplit(werkzeug_urls.uri_to_iri(self.geturl())))


def _unquote_legacy(value: str) -> str:
try:
return urllib.parse.unquote(value, "utf-8", "strict")
except UnicodeError:
return urllib.parse.unquote(value, "latin1")


# Provide a replacement for the deprecated werkzeug.urls.URL class
#
# NB: Do not use this in new code!
#
# We only use this in lektor.publishers in order to provide some backward
# compatibility for custom publishers from existing Lektor plugins.
# At such point as we decide that backward-compatibility is no longer
# needed, will be deleted.
#
werkzeug_urls_URL = getattr(werkzeug_urls, "URL", _CompatURL)
9 changes: 5 additions & 4 deletions lektor/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
from functools import total_ordering
from itertools import islice
from operator import methodcaller
from urllib.parse import urljoin

from jinja2 import is_undefined
from jinja2 import Undefined
from jinja2.exceptions import UndefinedError
from jinja2.utils import LRUCache
from werkzeug.urls import url_join
from werkzeug.utils import cached_property

from lektor import metaformat
Expand Down Expand Up @@ -1580,6 +1580,7 @@ def env(self):
"""The env for this pad."""
return self.db.env

@deprecated("use Pad.make_url instead", version="3.4.0")
def make_absolute_url(self, url):
"""Given a URL this makes it absolute if this is possible."""
base_url = self.db.config["PROJECT"].get("url")
Expand All @@ -1588,7 +1589,7 @@ def make_absolute_url(self, url):
"To use absolute URLs you need to configure "
"the URL in the project config."
)
return url_join(base_url.rstrip("/") + "/", url.lstrip("/"))
return urljoin(base_url.rstrip("/") + "/", url.lstrip("/"))

def make_url(self, url, base_url=None, absolute=None, external=None):
"""Helper method that creates a finalized URL based on the parameters
Expand All @@ -1613,9 +1614,9 @@ def make_url(self, url, base_url=None, absolute=None, external=None):
"To use absolute URLs you need to "
"configure the URL in the project config."
)
return url_join(external_base_url, url.lstrip("/"))
return urljoin(external_base_url, url.lstrip("/"))
if absolute:
return url_join(self.db.config.base_path, url.lstrip("/"))
return urljoin(self.db.config.base_path, url.lstrip("/"))
if base_url is None:
raise RuntimeError(
"Cannot calculate a relative URL if no base URL has been provided."
Expand Down
6 changes: 3 additions & 3 deletions lektor/environment/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import os
import re
from collections import OrderedDict
from urllib.parse import urlsplit

from inifile import IniFile
from werkzeug.urls import url_parse
from werkzeug.utils import cached_property

from lektor.constants import PRIMARY_ALT
Expand Down Expand Up @@ -257,7 +257,7 @@ def primary_alternative(self):
def base_url(self):
"""The external base URL."""
url = self.values["PROJECT"].get("url")
if url and url_parse(url).scheme:
if url and urlsplit(url).scheme:
return url.rstrip("/") + "/"
return None

Expand All @@ -266,7 +266,7 @@ def base_path(self):
"""The base path of the URL."""
url = self.values["PROJECT"].get("url")
if url:
return url_parse(url).path.rstrip("/") + "/"
return urlsplit(url).path.rstrip("/") + "/"
return "/"

@cached_property
Expand Down

0 comments on commit 5878799

Please sign in to comment.