webchanges 3.29.0__tar.gz → 3.31.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webchanges-3.29.0/webchanges.egg-info → webchanges-3.31.0}/PKG-INFO +5 -4
- {webchanges-3.29.0 → webchanges-3.31.0}/README.rst +3 -2
- {webchanges-3.29.0 → webchanges-3.31.0}/pyproject.toml +11 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/requirements.txt +1 -1
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/__init__.py +2 -2
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/_vendored/headers.py +84 -77
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/_vendored/packaging_version.py +55 -51
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/cli.py +79 -21
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/command.py +204 -91
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/config.py +3 -2
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/differs.py +504 -210
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/filters.py +99 -42
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/handler.py +39 -33
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/jobs.py +130 -144
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/reporters.py +69 -34
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/storage.py +36 -66
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/util.py +10 -7
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/worker.py +1 -1
- {webchanges-3.29.0 → webchanges-3.31.0/webchanges.egg-info}/PKG-INFO +5 -4
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/requires.txt +1 -1
- {webchanges-3.29.0 → webchanges-3.31.0}/LICENSE +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/MANIFEST.in +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/setup.cfg +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/__main__.py +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/_vendored/__init__.py +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/mailer.py +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/main.py +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/py.typed +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/storage_minidb.py +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/SOURCES.txt +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/dependency_links.txt +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/entry_points.txt +0 -0
- {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webchanges
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.31.0
|
|
4
4
|
Summary: Web Changes Delivered. AI-Summarized. Totally Anonymous.
|
|
5
5
|
Author-email: Mike Borsetti <mike+webchanges@borsetti.com>
|
|
6
6
|
Maintainer-email: Mike Borsetti <mike+webchanges@borsetti.com>
|
|
@@ -108,7 +108,7 @@ Requires-Dist: cssselect
|
|
|
108
108
|
Requires-Dist: h2
|
|
109
109
|
Requires-Dist: html2text
|
|
110
110
|
Requires-Dist: httpx
|
|
111
|
-
Requires-Dist: lxml
|
|
111
|
+
Requires-Dist: lxml
|
|
112
112
|
Requires-Dist: markdown2
|
|
113
113
|
Requires-Dist: msgpack
|
|
114
114
|
Requires-Dist: platformdirs
|
|
@@ -206,8 +206,9 @@ Install **webchanges** with:
|
|
|
206
206
|
|
|
207
207
|
Running in Docker
|
|
208
208
|
-----------------
|
|
209
|
-
**webchanges** can easily run in a
|
|
210
|
-
`here <https://github.com/yubiuser/webchanges-docker>`__
|
|
209
|
+
**webchanges** can easily run in a `Docker <https://www.docker.com/>`__ container! You will find a minimal
|
|
210
|
+
implementation (no browser) `here <https://github.com/yubiuser/webchanges-docker>`__, and one with a browser
|
|
211
|
+
`here <https://github.com/jhedlund/webchanges-docker>`__.
|
|
211
212
|
|
|
212
213
|
|
|
213
214
|
Documentation |readthedocs|
|
|
@@ -39,8 +39,9 @@ Install **webchanges** with:
|
|
|
39
39
|
|
|
40
40
|
Running in Docker
|
|
41
41
|
-----------------
|
|
42
|
-
**webchanges** can easily run in a
|
|
43
|
-
`here <https://github.com/yubiuser/webchanges-docker>`__
|
|
42
|
+
**webchanges** can easily run in a `Docker <https://www.docker.com/>`__ container! You will find a minimal
|
|
43
|
+
implementation (no browser) `here <https://github.com/yubiuser/webchanges-docker>`__, and one with a browser
|
|
44
|
+
`here <https://github.com/jhedlund/webchanges-docker>`__.
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
Documentation |readthedocs|
|
|
@@ -319,3 +319,14 @@ asyncio_default_fixture_loop_scope = 'function'
|
|
|
319
319
|
# https://github.com/pytest-dev/pytest-cov/issues/131) and to enable running tox --parallel
|
|
320
320
|
# Instead of below, now runs with $ coverage run --parallel-mode
|
|
321
321
|
# addopts = --cov=./ --cov-report=term --cov-report=html
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# -------------------------- pyright --------------------------
|
|
325
|
+
# Config file documentation at https://microsoft.github.io/pyright/#/configuration
|
|
326
|
+
[tool.pyright]
|
|
327
|
+
ignore = ['webchanges/storage_minidb.py']
|
|
328
|
+
|
|
329
|
+
reportMissingImports = false
|
|
330
|
+
reportMissingModuleSource = false
|
|
331
|
+
reportDeprecated = true
|
|
332
|
+
# reportUnnecessaryTypeIgnoreComment = true
|
|
@@ -15,14 +15,14 @@ from __future__ import annotations
|
|
|
15
15
|
__min_python_version__ = (3, 10) # minimum version of Python required to run; supported until fall 2025
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
__project_name__ = __package__
|
|
18
|
+
__project_name__ = str(__package__)
|
|
19
19
|
# Version numbering is PEP440-compliant https://www.python.org/dev/peps/pep-0440/
|
|
20
20
|
# Release numbering largely follows Semantic Versioning https://semver.org/spec/v2.0.0.html#semantic-versioning-200
|
|
21
21
|
# * MAJOR version when you make incompatible API changes,
|
|
22
22
|
# * MINOR version when you add functionality in a backwards compatible manner, and
|
|
23
23
|
# * MICRO or PATCH version when you make backwards compatible bug fixes. We no longer use '0'
|
|
24
24
|
# If unsure on increments, use pkg_resources.parse_version to parse
|
|
25
|
-
__version__ = '3.
|
|
25
|
+
__version__ = '3.31.0'
|
|
26
26
|
__description__ = (
|
|
27
27
|
'Check web (or command output) for changes since last run and notify.\n'
|
|
28
28
|
'\n'
|
|
@@ -1,79 +1,94 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Vendored version of httpx.Headers class from httpx v0.
|
|
3
|
-
https://github.com/encode/httpx/releases/tag/0.
|
|
2
|
+
Vendored version of httpx.Headers class from httpx v0.28.1 released on 06-Dec-24
|
|
3
|
+
https://github.com/encode/httpx/releases/tag/0.28.1.
|
|
4
|
+
(commit https://github.com/encode/httpx/commit/26d48e0634e6ee9cdc0533996db289ce4b430177).
|
|
4
5
|
|
|
5
|
-
Allows us to load this class in case httpx
|
|
6
|
-
|
|
7
|
-
See https://github.com/psf/requests and https://github.com/encode/httpx/blob/master/httpx/_models.py
|
|
6
|
+
Allows us to load this class in case httpx is not installed.
|
|
8
7
|
"""
|
|
9
8
|
|
|
9
|
+
# Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
|
|
10
|
+
# All rights reserved.
|
|
11
|
+
#
|
|
12
|
+
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
13
|
+
# following conditions are met:
|
|
14
|
+
#
|
|
15
|
+
# * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
16
|
+
# disclaimer.
|
|
17
|
+
#
|
|
18
|
+
# * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
|
19
|
+
# disclaimer in the documentation and/or other materials provided with the distribution.
|
|
20
|
+
#
|
|
21
|
+
# * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
|
|
22
|
+
# products derived from this software without specific prior written permission.
|
|
23
|
+
#
|
|
24
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
25
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
26
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
27
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
28
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
29
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
|
|
10
32
|
from __future__ import annotations
|
|
11
33
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
AnyStr,
|
|
15
|
-
ItemsView,
|
|
16
|
-
Iterable,
|
|
17
|
-
Iterator,
|
|
18
|
-
KeysView,
|
|
19
|
-
Mapping,
|
|
20
|
-
MutableMapping,
|
|
21
|
-
Sequence,
|
|
22
|
-
TypeAlias,
|
|
23
|
-
ValuesView,
|
|
24
|
-
)
|
|
34
|
+
import typing
|
|
35
|
+
from collections.abc import Mapping
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
# from https://github.com/encode/httpx/blob/master/httpx/_types.py
|
|
38
|
+
HeaderTypes: typing.TypeAlias = (
|
|
39
|
+
'Headers'
|
|
40
|
+
| typing.Mapping[str, str]
|
|
41
|
+
| typing.Mapping[bytes, bytes]
|
|
42
|
+
| typing.Sequence[tuple[str, str]]
|
|
43
|
+
| typing.Sequence[tuple[bytes, bytes]]
|
|
28
44
|
)
|
|
29
45
|
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
# from https://github.com/encode/httpx/blob/master/httpx/_utils.py
|
|
48
|
+
def to_str(value: str | bytes, encoding: str = 'utf-8') -> str:
|
|
49
|
+
return value if isinstance(value, str) else value.decode(encoding) # pyright: ignore[reportAttributeAccessIssue]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr:
|
|
53
|
+
return value if isinstance(match_type_of, str) else value.encode() # pyright: ignore[reportReturnType]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# from https://github.com/encode/httpx/blob/master/httpx/_models.py
|
|
57
|
+
SENSITIVE_HEADERS = {'authorization', 'proxy-authorization'}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes:
|
|
36
61
|
"""
|
|
37
62
|
Coerce str/bytes into a strictly byte-wise HTTP header key.
|
|
38
63
|
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
return (
|
|
65
|
+
key
|
|
66
|
+
if isinstance(key, bytes)
|
|
67
|
+
else key.encode(encoding or 'ascii') # pyright: ignore[reportAttributeAccessIssue]
|
|
68
|
+
)
|
|
43
69
|
|
|
44
|
-
return bytes_value.lower() if lower else bytes_value
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
def normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:
|
|
71
|
+
def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:
|
|
48
72
|
"""
|
|
49
73
|
Coerce str/bytes into a strictly byte-wise HTTP header value.
|
|
50
74
|
"""
|
|
51
75
|
if isinstance(value, bytes):
|
|
52
76
|
return value
|
|
77
|
+
if not isinstance(value, str):
|
|
78
|
+
raise TypeError(f'Header value must be str or bytes, not {type(value)}')
|
|
53
79
|
return value.encode(encoding or 'ascii')
|
|
54
80
|
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def obfuscate_sensitive_headers(
|
|
60
|
-
items: Iterable[tuple[AnyStr, AnyStr]],
|
|
61
|
-
) -> Iterator[tuple[AnyStr, AnyStr]]:
|
|
82
|
+
def _obfuscate_sensitive_headers(
|
|
83
|
+
items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]],
|
|
84
|
+
) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]:
|
|
62
85
|
for k, v in items:
|
|
63
86
|
if to_str(k.lower()) in SENSITIVE_HEADERS:
|
|
64
87
|
v = to_bytes_or_str('[secure]', match_type_of=v)
|
|
65
88
|
yield k, v
|
|
66
89
|
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
return value if isinstance(value, str) else value.decode(encoding)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def to_bytes_or_str(value: str, match_type_of: AnyStr) -> AnyStr:
|
|
73
|
-
return value if isinstance(match_type_of, str) else value.encode()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class Headers(MutableMapping[str, str]):
|
|
91
|
+
class Headers(typing.MutableMapping[str, str]):
|
|
77
92
|
"""
|
|
78
93
|
HTTP headers, as a case-insensitive multi-dict.
|
|
79
94
|
"""
|
|
@@ -83,28 +98,20 @@ class Headers(MutableMapping[str, str]):
|
|
|
83
98
|
headers: HeaderTypes | None = None,
|
|
84
99
|
encoding: str | None = None,
|
|
85
100
|
) -> None:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
self._list: list[tuple[bytes, bytes, bytes]] = []
|
|
102
|
+
|
|
103
|
+
if isinstance(headers, Headers):
|
|
89
104
|
self._list = list(headers._list)
|
|
90
105
|
elif isinstance(headers, Mapping):
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
self._list = [
|
|
101
|
-
(
|
|
102
|
-
normalize_header_key(k, lower=False, encoding=encoding),
|
|
103
|
-
normalize_header_key(k, lower=True, encoding=encoding),
|
|
104
|
-
normalize_header_value(v, encoding),
|
|
105
|
-
)
|
|
106
|
-
for k, v in headers
|
|
107
|
-
]
|
|
106
|
+
for k, v in headers.items():
|
|
107
|
+
bytes_key = _normalize_header_key(k, encoding)
|
|
108
|
+
bytes_value = _normalize_header_value(v, encoding)
|
|
109
|
+
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
|
|
110
|
+
elif headers is not None:
|
|
111
|
+
for k, v in headers:
|
|
112
|
+
bytes_key = _normalize_header_key(k, encoding)
|
|
113
|
+
bytes_value = _normalize_header_value(v, encoding)
|
|
114
|
+
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
|
|
108
115
|
|
|
109
116
|
self._encoding = encoding
|
|
110
117
|
|
|
@@ -144,10 +151,10 @@ class Headers(MutableMapping[str, str]):
|
|
|
144
151
|
"""
|
|
145
152
|
return [(raw_key, value) for raw_key, _, value in self._list]
|
|
146
153
|
|
|
147
|
-
def keys(self) -> KeysView[str]:
|
|
154
|
+
def keys(self) -> typing.KeysView[str]:
|
|
148
155
|
return {key.decode(self.encoding): None for _, key, value in self._list}.keys()
|
|
149
156
|
|
|
150
|
-
def values(self) -> ValuesView[str]:
|
|
157
|
+
def values(self) -> typing.ValuesView[str]:
|
|
151
158
|
values_dict: dict[str, str] = {}
|
|
152
159
|
for _, key, value in self._list:
|
|
153
160
|
str_key = key.decode(self.encoding)
|
|
@@ -158,7 +165,7 @@ class Headers(MutableMapping[str, str]):
|
|
|
158
165
|
values_dict[str_key] = str_value
|
|
159
166
|
return values_dict.values()
|
|
160
167
|
|
|
161
|
-
def items(self) -> ItemsView[str, str]:
|
|
168
|
+
def items(self) -> typing.ItemsView[str, str]:
|
|
162
169
|
"""
|
|
163
170
|
Return `(key, value)` items of headers. Concatenate headers
|
|
164
171
|
into a single comma separated value when a key occurs multiple times.
|
|
@@ -181,7 +188,7 @@ class Headers(MutableMapping[str, str]):
|
|
|
181
188
|
"""
|
|
182
189
|
return [(key.decode(self.encoding), value.decode(self.encoding)) for _, key, value in self._list]
|
|
183
190
|
|
|
184
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
191
|
+
def get(self, key: str, default: typing.Any = None) -> typing.Any:
|
|
185
192
|
"""
|
|
186
193
|
Return a header value. If multiple occurrences of the header occur
|
|
187
194
|
then concatenate them together with commas.
|
|
@@ -208,9 +215,9 @@ class Headers(MutableMapping[str, str]):
|
|
|
208
215
|
if not split_commas:
|
|
209
216
|
return values
|
|
210
217
|
|
|
211
|
-
split_values
|
|
218
|
+
split_values = []
|
|
212
219
|
for value in values:
|
|
213
|
-
split_values.extend(
|
|
220
|
+
split_values.extend([item.strip() for item in value.split(',')])
|
|
214
221
|
return split_values
|
|
215
222
|
|
|
216
223
|
def update(self, headers: HeaderTypes | None = None) -> None: # type: ignore[override]
|
|
@@ -277,17 +284,17 @@ class Headers(MutableMapping[str, str]):
|
|
|
277
284
|
for idx in reversed(pop_indexes):
|
|
278
285
|
del self._list[idx]
|
|
279
286
|
|
|
280
|
-
def __contains__(self, key: Any) -> bool:
|
|
287
|
+
def __contains__(self, key: typing.Any) -> bool:
|
|
281
288
|
header_key = key.lower().encode(self.encoding)
|
|
282
289
|
return header_key in [key for _, key, _ in self._list]
|
|
283
290
|
|
|
284
|
-
def __iter__(self) -> Iterator[Any]:
|
|
291
|
+
def __iter__(self) -> typing.Iterator[typing.Any]:
|
|
285
292
|
return iter(self.keys())
|
|
286
293
|
|
|
287
294
|
def __len__(self) -> int:
|
|
288
295
|
return len(self._list)
|
|
289
296
|
|
|
290
|
-
def __eq__(self, other: Any) -> bool:
|
|
297
|
+
def __eq__(self, other: typing.Any) -> bool:
|
|
291
298
|
try:
|
|
292
299
|
other_headers = Headers(other)
|
|
293
300
|
except ValueError:
|
|
@@ -304,7 +311,7 @@ class Headers(MutableMapping[str, str]):
|
|
|
304
311
|
if self.encoding != 'ascii':
|
|
305
312
|
encoding_str = f', encoding={self.encoding!r}'
|
|
306
313
|
|
|
307
|
-
as_list = list(
|
|
314
|
+
as_list = list(_obfuscate_sensitive_headers(self.multi_items()))
|
|
308
315
|
as_dict = dict(as_list)
|
|
309
316
|
|
|
310
317
|
no_duplicate_keys = len(as_dict) == len(as_list)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Vendored version of packaging.version.parse() from packaging
|
|
3
|
-
(commit https://github.com/pypa/packaging/commit/
|
|
2
|
+
Vendored version of packaging.version.parse() from packaging v24.2 released on 08-Nov-24
|
|
3
|
+
(commit https://github.com/pypa/packaging/commit/d8e3b31b734926ebbcaff654279f6855a73e052f).
|
|
4
|
+
|
|
5
|
+
Allows us to load this class in case packaging is not installed.
|
|
4
6
|
|
|
5
7
|
See https://github.com/pypa/packaging and https://github.com/pypa/packaging/blob/main/src/packaging/version.py.
|
|
6
8
|
|
|
@@ -309,7 +311,7 @@ class _Version(NamedTuple):
|
|
|
309
311
|
local: LocalType | None
|
|
310
312
|
|
|
311
313
|
|
|
312
|
-
def parse(version: str) ->
|
|
314
|
+
def parse(version: str) -> Version:
|
|
313
315
|
"""Parse the given version string.
|
|
314
316
|
>>> parse('1.0.dev1')
|
|
315
317
|
<Version('1.0.dev1')>
|
|
@@ -321,7 +323,7 @@ def parse(version: str) -> 'Version':
|
|
|
321
323
|
|
|
322
324
|
class InvalidVersion(ValueError):
|
|
323
325
|
"""Raised when a version string is not a valid version.
|
|
324
|
-
>>> Version(
|
|
326
|
+
>>> Version('invalid')
|
|
325
327
|
Traceback (most recent call last):
|
|
326
328
|
...
|
|
327
329
|
packaging.version.InvalidVersion: Invalid version: 'invalid'
|
|
@@ -354,13 +356,13 @@ class _BaseVersion:
|
|
|
354
356
|
|
|
355
357
|
return self._key == other._key
|
|
356
358
|
|
|
357
|
-
def __ge__(self, other:
|
|
359
|
+
def __ge__(self, other: _BaseVersion) -> bool:
|
|
358
360
|
if not isinstance(other, _BaseVersion):
|
|
359
361
|
return NotImplemented
|
|
360
362
|
|
|
361
363
|
return self._key >= other._key
|
|
362
364
|
|
|
363
|
-
def __gt__(self, other:
|
|
365
|
+
def __gt__(self, other: _BaseVersion) -> bool:
|
|
364
366
|
if not isinstance(other, _BaseVersion):
|
|
365
367
|
return NotImplemented
|
|
366
368
|
|
|
@@ -423,8 +425,8 @@ class Version(_BaseVersion):
|
|
|
423
425
|
|
|
424
426
|
A :class:`Version` instance is comparison aware and can be compared and sorted using the standard Python interfaces.
|
|
425
427
|
|
|
426
|
-
>>> v1 = Version(
|
|
427
|
-
>>> v2 = Version(
|
|
428
|
+
>>> v1 = Version('1.0a5')
|
|
429
|
+
>>> v2 = Version('1.0')
|
|
428
430
|
>>> v1
|
|
429
431
|
<Version('1.0a5')>
|
|
430
432
|
>>> v2
|
|
@@ -457,7 +459,7 @@ class Version(_BaseVersion):
|
|
|
457
459
|
# Validate the version and parse it into pieces
|
|
458
460
|
match = self._regex.search(version)
|
|
459
461
|
if not match:
|
|
460
|
-
raise InvalidVersion(f
|
|
462
|
+
raise InvalidVersion(f'Invalid version: {version!r}')
|
|
461
463
|
|
|
462
464
|
# Store the parsed out pieces of the version
|
|
463
465
|
self._version = _Version(
|
|
@@ -488,9 +490,9 @@ class Version(_BaseVersion):
|
|
|
488
490
|
return f"<Version('{self}')>"
|
|
489
491
|
|
|
490
492
|
def __str__(self) -> str:
|
|
491
|
-
"""A string representation of the version that can be
|
|
493
|
+
"""A string representation of the version that can be round-tripped.
|
|
492
494
|
|
|
493
|
-
>>> str(Version(
|
|
495
|
+
>>> str(Version('1.0a5'))
|
|
494
496
|
'1.0a5'
|
|
495
497
|
"""
|
|
496
498
|
parts = []
|
|
@@ -524,9 +526,9 @@ class Version(_BaseVersion):
|
|
|
524
526
|
def epoch(self) -> int:
|
|
525
527
|
"""The epoch of the version.
|
|
526
528
|
|
|
527
|
-
>>> Version(
|
|
529
|
+
>>> Version('2.0.0').epoch
|
|
528
530
|
0
|
|
529
|
-
>>> Version(
|
|
531
|
+
>>> Version('1!2.0.0').epoch
|
|
530
532
|
1
|
|
531
533
|
"""
|
|
532
534
|
return self._version.epoch
|
|
@@ -535,11 +537,11 @@ class Version(_BaseVersion):
|
|
|
535
537
|
def release(self) -> tuple[int, ...]:
|
|
536
538
|
"""The components of the "release" segment of the version.
|
|
537
539
|
|
|
538
|
-
>>> Version(
|
|
540
|
+
>>> Version('1.2.3').release
|
|
539
541
|
(1, 2, 3)
|
|
540
|
-
>>> Version(
|
|
542
|
+
>>> Version('2.0.0').release
|
|
541
543
|
(2, 0, 0)
|
|
542
|
-
>>> Version(
|
|
544
|
+
>>> Version('1!2.0.0.post0').release
|
|
543
545
|
(2, 0, 0)
|
|
544
546
|
|
|
545
547
|
Includes trailing zeroes but not the epoch or any pre-release / development / post-release suffixes.
|
|
@@ -550,13 +552,13 @@ class Version(_BaseVersion):
|
|
|
550
552
|
def pre(self) -> tuple[str, int] | None:
|
|
551
553
|
"""The pre-release segment of the version.
|
|
552
554
|
|
|
553
|
-
>>> print(Version(
|
|
555
|
+
>>> print(Version('1.2.3').pre)
|
|
554
556
|
None
|
|
555
|
-
>>> Version(
|
|
557
|
+
>>> Version('1.2.3a1').pre
|
|
556
558
|
('a', 1)
|
|
557
|
-
>>> Version(
|
|
559
|
+
>>> Version('1.2.3b1').pre
|
|
558
560
|
('b', 1)
|
|
559
|
-
>>> Version(
|
|
561
|
+
>>> Version('1.2.3rc1').pre
|
|
560
562
|
('rc', 1)
|
|
561
563
|
"""
|
|
562
564
|
return self._version.pre
|
|
@@ -565,9 +567,9 @@ class Version(_BaseVersion):
|
|
|
565
567
|
def post(self) -> int | None:
|
|
566
568
|
"""The post-release number of the version.
|
|
567
569
|
|
|
568
|
-
>>> print(Version(
|
|
570
|
+
>>> print(Version('1.2.3').post)
|
|
569
571
|
None
|
|
570
|
-
>>> Version(
|
|
572
|
+
>>> Version('1.2.3.post1').post
|
|
571
573
|
1
|
|
572
574
|
"""
|
|
573
575
|
return self._version.post[1] if self._version.post else None
|
|
@@ -576,9 +578,9 @@ class Version(_BaseVersion):
|
|
|
576
578
|
def dev(self) -> int | None:
|
|
577
579
|
"""The development number of the version.
|
|
578
580
|
|
|
579
|
-
>>> print(Version(
|
|
581
|
+
>>> print(Version('1.2.3').dev)
|
|
580
582
|
None
|
|
581
|
-
>>> Version(
|
|
583
|
+
>>> Version('1.2.3.dev1').dev
|
|
582
584
|
1
|
|
583
585
|
"""
|
|
584
586
|
return self._version.dev[1] if self._version.dev else None
|
|
@@ -587,9 +589,9 @@ class Version(_BaseVersion):
|
|
|
587
589
|
def local(self) -> str | None:
|
|
588
590
|
"""The local version segment of the version.
|
|
589
591
|
|
|
590
|
-
>>> print(Version(
|
|
592
|
+
>>> print(Version('1.2.3').local)
|
|
591
593
|
None
|
|
592
|
-
>>> Version(
|
|
594
|
+
>>> Version('1.2.3+abc').local
|
|
593
595
|
'abc'
|
|
594
596
|
"""
|
|
595
597
|
if self._version.local:
|
|
@@ -601,12 +603,12 @@ class Version(_BaseVersion):
|
|
|
601
603
|
def public(self) -> str:
|
|
602
604
|
"""The public portion of the version.
|
|
603
605
|
|
|
604
|
-
>>> Version(
|
|
605
|
-
'1.2.3'
|
|
606
|
-
>>> Version("1.2.3+abc").public
|
|
606
|
+
>>> Version('1.2.3').public
|
|
607
607
|
'1.2.3'
|
|
608
|
-
>>> Version(
|
|
608
|
+
>>> Version('1.2.3+abc').public
|
|
609
609
|
'1.2.3'
|
|
610
|
+
>>> Version('1!1.2.3dev1+abc').public
|
|
611
|
+
'1!1.2.3.dev1'
|
|
610
612
|
"""
|
|
611
613
|
return str(self).split('+', 1)[0]
|
|
612
614
|
|
|
@@ -614,11 +616,11 @@ class Version(_BaseVersion):
|
|
|
614
616
|
def base_version(self) -> str:
|
|
615
617
|
"""The "base version" of the version.
|
|
616
618
|
|
|
617
|
-
>>> Version(
|
|
619
|
+
>>> Version('1.2.3').base_version
|
|
618
620
|
'1.2.3'
|
|
619
|
-
>>> Version(
|
|
621
|
+
>>> Version('1.2.3+abc').base_version
|
|
620
622
|
'1.2.3'
|
|
621
|
-
>>> Version(
|
|
623
|
+
>>> Version('1!1.2.3dev1+abc').base_version
|
|
622
624
|
'1!1.2.3'
|
|
623
625
|
|
|
624
626
|
The "base version" is the public version of the project without any pre or post release markers.
|
|
@@ -638,15 +640,15 @@ class Version(_BaseVersion):
|
|
|
638
640
|
def is_prerelease(self) -> bool:
|
|
639
641
|
"""Whether this version is a pre-release.
|
|
640
642
|
|
|
641
|
-
>>> Version(
|
|
643
|
+
>>> Version('1.2.3').is_prerelease
|
|
642
644
|
False
|
|
643
|
-
>>> Version(
|
|
645
|
+
>>> Version('1.2.3a1').is_prerelease
|
|
644
646
|
True
|
|
645
|
-
>>> Version(
|
|
647
|
+
>>> Version('1.2.3b1').is_prerelease
|
|
646
648
|
True
|
|
647
|
-
>>> Version(
|
|
649
|
+
>>> Version('1.2.3rc1').is_prerelease
|
|
648
650
|
True
|
|
649
|
-
>>> Version(
|
|
651
|
+
>>> Version('1.2.3dev1').is_prerelease
|
|
650
652
|
True
|
|
651
653
|
"""
|
|
652
654
|
return self.dev is not None or self.pre is not None
|
|
@@ -655,9 +657,9 @@ class Version(_BaseVersion):
|
|
|
655
657
|
def is_postrelease(self) -> bool:
|
|
656
658
|
"""Whether this version is a post-release.
|
|
657
659
|
|
|
658
|
-
>>> Version(
|
|
660
|
+
>>> Version('1.2.3').is_postrelease
|
|
659
661
|
False
|
|
660
|
-
>>> Version(
|
|
662
|
+
>>> Version('1.2.3.post1').is_postrelease
|
|
661
663
|
True
|
|
662
664
|
"""
|
|
663
665
|
return self.post is not None
|
|
@@ -666,9 +668,9 @@ class Version(_BaseVersion):
|
|
|
666
668
|
def is_devrelease(self) -> bool:
|
|
667
669
|
"""Whether this version is a development release.
|
|
668
670
|
|
|
669
|
-
>>> Version(
|
|
671
|
+
>>> Version('1.2.3').is_devrelease
|
|
670
672
|
False
|
|
671
|
-
>>> Version(
|
|
673
|
+
>>> Version('1.2.3.dev1').is_devrelease
|
|
672
674
|
True
|
|
673
675
|
"""
|
|
674
676
|
return self.dev is not None
|
|
@@ -677,7 +679,7 @@ class Version(_BaseVersion):
|
|
|
677
679
|
def major(self) -> int:
|
|
678
680
|
"""The first item of :attr:`release` or ``0`` if unavailable.
|
|
679
681
|
|
|
680
|
-
>>> Version(
|
|
682
|
+
>>> Version('1.2.3').major
|
|
681
683
|
1
|
|
682
684
|
"""
|
|
683
685
|
return self.release[0] if len(self.release) >= 1 else 0
|
|
@@ -686,9 +688,9 @@ class Version(_BaseVersion):
|
|
|
686
688
|
def minor(self) -> int:
|
|
687
689
|
"""The second item of :attr:`release` or ``0`` if unavailable.
|
|
688
690
|
|
|
689
|
-
>>> Version(
|
|
691
|
+
>>> Version('1.2.3').minor
|
|
690
692
|
2
|
|
691
|
-
>>> Version(
|
|
693
|
+
>>> Version('1').minor
|
|
692
694
|
0
|
|
693
695
|
"""
|
|
694
696
|
return self.release[1] if len(self.release) >= 2 else 0
|
|
@@ -697,9 +699,9 @@ class Version(_BaseVersion):
|
|
|
697
699
|
def micro(self) -> int:
|
|
698
700
|
"""The third item of :attr:`release` or ``0`` if unavailable.
|
|
699
701
|
|
|
700
|
-
>>> Version(
|
|
702
|
+
>>> Version('1.2.3').micro
|
|
701
703
|
3
|
|
702
|
-
>>> Version(
|
|
704
|
+
>>> Version('1').micro
|
|
703
705
|
0
|
|
704
706
|
"""
|
|
705
707
|
return self.release[2] if len(self.release) >= 3 else 0
|
|
@@ -726,9 +728,11 @@ def _parse_letter_version(letter: str | None, number: str | bytes | SupportsInt
|
|
|
726
728
|
letter = 'post'
|
|
727
729
|
|
|
728
730
|
return letter, int(number)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
731
|
+
|
|
732
|
+
assert not letter
|
|
733
|
+
if number:
|
|
734
|
+
# We assume if we are given a number, but we are not given a letter
|
|
735
|
+
# then this is using the implicit post release syntax (e.g. 1.0-1)
|
|
732
736
|
letter = 'post'
|
|
733
737
|
|
|
734
738
|
return letter, int(number)
|
|
@@ -741,7 +745,7 @@ _local_version_separators = re.compile(r'[\._-]')
|
|
|
741
745
|
|
|
742
746
|
def _parse_local_version(local: str | None) -> LocalType | None:
|
|
743
747
|
"""
|
|
744
|
-
Takes a string like abc.1.twelve and turns it into (
|
|
748
|
+
Takes a string like abc.1.twelve and turns it into ('abc', 1, 'twelve').
|
|
745
749
|
"""
|
|
746
750
|
if local is not None:
|
|
747
751
|
return tuple(
|