Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.3 branch] address deprecation of werkzeug.urls.url_parse #1144

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion lektor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def cli(ctx, project=None, language=None):
This command can invoke lektor locally and serve up the website. It's
intended for local development of websites.
"""
warnings.simplefilter("default")
if not sys.warnoptions:
warnings.simplefilter("default")
if language is not None:
ctx.ui_lang = language
if project is not None:
Expand Down
10 changes: 4 additions & 6 deletions lektor/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
from datetime import timedelta
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 @@ -957,8 +957,6 @@ def __str__(self):
"frames directly, use .thumbnail()."
)

__unicode__ = __str__

@require_ffmpeg
def thumbnail(self, width=None, height=None, mode=None, upscale=None, quality=None):
"""Utility to create thumbnails."""
Expand Down Expand Up @@ -1628,7 +1626,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 @@ -1646,9 +1644,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 @@ -273,7 +273,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 @@ -282,7 +282,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
6 changes: 3 additions & 3 deletions lektor/markdown.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import threading
from urllib.parse import urlsplit
from weakref import ref as weakref

import mistune
from markupsafe import Markup
from werkzeug.urls import url_parse

from lektor.context import get_ctx

Expand All @@ -18,7 +18,7 @@ def escape(text: str) -> str:
class ImprovedRenderer(mistune.Renderer):
def link(self, link, title, text):
if self.record is not None:
url = url_parse(link)
url = urlsplit(link)
if not url.scheme:
link = self.record.url_to("!" + link, base_url=get_ctx().base_url)
link = escape(link)
Expand All @@ -29,7 +29,7 @@ def link(self, link, title, text):

def image(self, src, title, text):
if self.record is not None:
url = url_parse(src)
url = urlsplit(src)
if not url.scheme:
src = self.record.url_to("!" + src, base_url=get_ctx().base_url)
src = escape(src)
Expand Down
39 changes: 16 additions & 23 deletions lektor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
from pathlib import PurePosixPath
from queue import Queue
from threading import Thread
from urllib.parse import urlsplit

import click
from jinja2 import is_undefined
from markupsafe import Markup
from slugify import slugify as _slugify
from werkzeug import urls
from werkzeug.http import http_date
from werkzeug.urls import url_parse
from werkzeug.urls import iri_to_uri
from werkzeug.urls import uri_to_iri


is_windows = os.name == "nt"
Expand Down Expand Up @@ -388,24 +389,21 @@ def wait_for_completion(self):


class Url:
def __init__(self, value):
def __init__(self, value: str):
self.url = value
u = url_parse(value)
i = u.to_iri_tuple()
self.ascii_url = str(u)
self.host = i.host
self.ascii_host = u.ascii_host
u = urlsplit(value)
i = urlsplit(uri_to_iri(u.geturl()))
self.ascii_url = iri_to_uri(u.geturl())
self.host = i.hostname
self.ascii_host = urlsplit(self.ascii_url).hostname
self.port = u.port
self.path = i.path
self.query = u.query
self.anchor = i.fragment
self.scheme = u.scheme

def __unicode__(self):
return self.url

def __str__(self):
return self.ascii_url
return self.url


def is_unsafe_to_delete(path, base):
Expand Down Expand Up @@ -511,17 +509,12 @@ def is_valid_id(value):
)


def secure_url(url):
url = urls.url_parse(url)
if url.password is not None:
url = url.replace(
netloc="%s@%s"
% (
url.username,
url.netloc.split("@")[-1],
)
)
return url.to_url()
def secure_url(url: str) -> str:
parts = urlsplit(url)
if parts.password is not None:
_, _, host_port = parts.netloc.rpartition("@")
parts = parts._replace(netloc=f"{parts.username}@{host_port}")
return parts.geturl()


def bool_from_string(val, default=None):
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ install_requires =
requests
setuptools>=45.2
watchdog
Werkzeug<3
Werkzeug<2.4

[options.extras_require]
ipython =
Expand Down
47 changes: 47 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
import inspect

import pytest

from lektor.environment.config import Config


def test_custom_attachment_types(env):
attachment_types = env.load_config().values["ATTACHMENT_TYPES"]
assert attachment_types[".foo"] == "text"


@pytest.fixture(scope="function")
def config(tmp_path, project_url):
projectfile = tmp_path / "scratch.lektorproject"
projectfile.write_text(
inspect.cleandoc(
f"""
[project]
url = {project_url}
"""
)
)
return Config(projectfile)


@pytest.mark.parametrize(
"project_url, expected",
[
("", None),
("/path/", None),
("https://example.org", "https://example.org/"),
],
)
def test_base_url(config, expected):
assert config.base_url == expected


@pytest.mark.parametrize(
"project_url, expected",
[
("", "/"),
("/path", "/path/"),
("/path/", "/path/"),
("https://example.org", "/"),
("https://example.org/pth", "/pth/"),
],
)
def test_base_path(config, expected):
assert config.base_path == expected
27 changes: 27 additions & 0 deletions tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,3 +487,30 @@ def test_Page_url_path_raise_error_if_paginated_and_dotted(scratch_pad):
def test_Attachment_url_path_is_for_primary_alt(scratch_pad, alt):
attachment = scratch_pad.get("/test.txt")
assert attachment.url_path == "/en/test.txt"


@pytest.mark.parametrize(
"url, base_url, absolute, external, project_url, expected",
[
("/a/b.html", "/a/", None, None, None, "b.html"),
("/a/b/", "/a/", None, None, None, "b/"),
("/a/b/", "/a", None, None, None, "a/b/"),
("/a/b/", "/a", True, None, None, "/a/b/"),
("/a/b/", "/a", True, None, "https://example.net/pfx/", "/pfx/a/b/"),
("/a/b/", "/a", None, True, "https://example.org", "https://example.org/a/b/"),
],
)
def test_Pad_make_url(url, base_url, absolute, external, project_url, expected, pad):
if project_url is not None:
pad.db.config.values["PROJECT"]["url"] = project_url
assert pad.make_url(url, base_url, absolute, external) == expected


def test_Pad_make_url_raises_runtime_error_if_no_project_url(pad):
with pytest.raises(RuntimeError, match="(?i)configure the url in the project"):
pad.make_url("/a/b", external=True)


def test_Pad_make_url_raises_runtime_error_if_no_base_url(pad):
with pytest.raises(RuntimeError, match="(?i)no base url"):
pad.make_url("/a/b")
3 changes: 3 additions & 0 deletions tests/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from lektor.publisher import RsyncPublisher


pytestmark = pytest.mark.filterwarnings(r"ignore:'werkzeug\.urls:DeprecationWarning")


def test_get_server(env):
server = env.load_config().get_server("production")
assert server.name == "Production"
Expand Down
1 change: 1 addition & 0 deletions tests/test_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def test_Command_triggers_no_warnings():
which("rsync") is None, reason="rsync is not available on this system"
)
@pytest.mark.parametrize("delete", ["yes", "no"])
@pytest.mark.filterwarnings(r"ignore:'werkzeug\.urls:DeprecationWarning")
def test_RsyncPublisher_integration(env, tmp_path, delete):
# Integration test of local rsync deployment
# Ensures that RsyncPublisher can successfully invoke rsync
Expand Down