Coverage for src/lektor_ng/compat.py: 98%

80 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-05 14:42 +0000

1from __future__ import annotations 

2 

3import importlib.metadata 

4import tempfile 

5import urllib.parse 

6from typing import Any 

7from urllib.parse import urlsplit 

8from warnings import warn 

9 

10from werkzeug import urls as werkzeug_urls 

11from werkzeug.datastructures import MultiDict 

12 

13from lektor_ng.utils import DeprecatedWarning 

14 

15__all__ = ["werkzeug_urls_URL"] 

16 

17 

18_DEPRECATED_ATTRS = { 

19 "TemporaryDirectory": tempfile.TemporaryDirectory, 

20 "importlib_metadata": importlib.metadata, 

21} 

22 

23 

24def __getattr__(name): 

25 try: 

26 value = _DEPRECATED_ATTRS.get(name) 

27 except KeyError: 

28 # pylint: disable=raise-missing-from 

29 raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None 

30 

31 if hasattr(value, "__module__"): 

32 replacement = f"{value.__module__}.{value.__name__}" 

33 else: 

34 replacement = f"{value.__name__}" 

35 warn( 

36 DeprecatedWarning( 

37 name=f"lektor.compat.{name}", 

38 reason=f"use {replacement} instead", 

39 version="3.4.0", 

40 ), 

41 stacklevel=2, 

42 ) 

43 return value 

44 

45 

46class _CompatURL(urllib.parse.SplitResult): 

47 """This is a replacement for ``werkzeug.urls.URL``. 

48 

49 Here we implement those attributes and methods of ``URL`` which are 

50 likely to be used by existing Lektor publishing plugins. 

51 

52 Currently unreimplemented here are the ``encode_netloc``, ``decode_netloc``, 

53 ``get_file_location``, and ``encode`` methods of ``werkzeug.urls.URL``. 

54 

55 NB: Use of this class is deprecated. DO NOT USE THIS IN NEW CODE! 

56 

57 """ 

58 

59 def __str__(self) -> str: 

60 return self.geturl() 

61 

62 def replace(self, **kwargs: Any) -> _CompatURL: 

63 return self._replace(**kwargs) 

64 

65 @property 

66 def host(self) -> str | None: 

67 return self.hostname 

68 

69 @property 

70 def ascii_host(self) -> str | None: 

71 host = self.hostname 

72 if host is None: 

73 return None 

74 try: 

75 return host.encode("idna").decode("ascii") 

76 except UnicodeError: 

77 return host 

78 

79 @property 

80 def auth(self) -> str | None: 

81 auth, _, _ = self.netloc.rpartition("@") 

82 return auth if auth != "" else None 

83 

84 @property 

85 def username(self) -> str | None: 

86 username = super().username 

87 if username is None: 

88 return None 

89 return _unquote_legacy(username) 

90 

91 @property 

92 def raw_username(self) -> str | None: 

93 return super().username 

94 

95 @property 

96 def password(self) -> str | None: 

97 password = super().password 

98 if password is None: 

99 return None 

100 return _unquote_legacy(password) 

101 

102 @property 

103 def raw_password(self) -> str | None: 

104 return super().password 

105 

106 def decode_query( 

107 self, 

108 charset: str = "utf-8", 

109 include_empty: bool = True, 

110 errors: str = "replace", 

111 # parse_qsl does not support the separator parameter in python < 3.7.10. 

112 # separator: str = "&", 

113 ) -> MultiDict: 

114 return MultiDict( 

115 urllib.parse.parse_qsl( 

116 self.query, 

117 keep_blank_values=include_empty, 

118 encoding=charset, 

119 errors=errors, 

120 # separator=separator, 

121 ) 

122 ) 

123 

124 def join(self, url: str | tuple[str, str, str, str, str], allow_fragments: bool = True) -> _CompatURL: 

125 if isinstance(url, tuple): 

126 url = urllib.parse.urlunsplit(url) 

127 joined = urllib.parse.urljoin(self.geturl(), url, allow_fragments) 

128 return _CompatURL._make(urlsplit(joined)) 

129 

130 def to_url(self) -> str: 

131 return self.geturl() 

132 

133 def to_uri_tuple(self) -> _CompatURL: 

134 return _CompatURL._make(urlsplit(werkzeug_urls.iri_to_uri(self.geturl()))) 

135 

136 def to_iri_tuple(self) -> _CompatURL: 

137 return _CompatURL._make(urlsplit(werkzeug_urls.uri_to_iri(self.geturl()))) 

138 

139 

140def _unquote_legacy(value: str) -> str: 

141 try: 

142 return urllib.parse.unquote(value, "utf-8", "strict") 

143 except UnicodeError: 

144 return urllib.parse.unquote(value, "latin1") 

145 

146 

147# Provide a replacement for the deprecated werkzeug.urls.URL class 

148# 

149# NB: Do not use this in new code! 

150# 

151# We only use this in lektor.publishers in order to provide some backward 

152# compatibility for custom publishers from existing Lektor plugins. 

153# At such point as we decide that backward-compatibility is no longer 

154# needed, will be deleted. 

155# 

156werkzeug_urls_URL = getattr(werkzeug_urls, "URL", _CompatURL)