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.
Files changed (33) hide show
  1. {webchanges-3.29.0/webchanges.egg-info → webchanges-3.31.0}/PKG-INFO +5 -4
  2. {webchanges-3.29.0 → webchanges-3.31.0}/README.rst +3 -2
  3. {webchanges-3.29.0 → webchanges-3.31.0}/pyproject.toml +11 -0
  4. {webchanges-3.29.0 → webchanges-3.31.0}/requirements.txt +1 -1
  5. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/__init__.py +2 -2
  6. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/_vendored/headers.py +84 -77
  7. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/_vendored/packaging_version.py +55 -51
  8. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/cli.py +79 -21
  9. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/command.py +204 -91
  10. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/config.py +3 -2
  11. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/differs.py +504 -210
  12. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/filters.py +99 -42
  13. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/handler.py +39 -33
  14. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/jobs.py +130 -144
  15. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/reporters.py +69 -34
  16. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/storage.py +36 -66
  17. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/util.py +10 -7
  18. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/worker.py +1 -1
  19. {webchanges-3.29.0 → webchanges-3.31.0/webchanges.egg-info}/PKG-INFO +5 -4
  20. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/requires.txt +1 -1
  21. {webchanges-3.29.0 → webchanges-3.31.0}/LICENSE +0 -0
  22. {webchanges-3.29.0 → webchanges-3.31.0}/MANIFEST.in +0 -0
  23. {webchanges-3.29.0 → webchanges-3.31.0}/setup.cfg +0 -0
  24. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/__main__.py +0 -0
  25. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/_vendored/__init__.py +0 -0
  26. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/mailer.py +0 -0
  27. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/main.py +0 -0
  28. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/py.typed +0 -0
  29. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges/storage_minidb.py +0 -0
  30. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/SOURCES.txt +0 -0
  31. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/dependency_links.txt +0 -0
  32. {webchanges-3.29.0 → webchanges-3.31.0}/webchanges.egg-info/entry_points.txt +0 -0
  33. {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.29.0
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>=5.3.0
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 container and you will find a `Docker <https://www.docker.com/>`__ implementation
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 container and you will find a `Docker <https://www.docker.com/>`__ implementation
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
@@ -3,7 +3,7 @@ cssselect
3
3
  h2
4
4
  html2text
5
5
  httpx
6
- lxml >= 5.3.0 # prior versions don't build in 3.13
6
+ lxml
7
7
  markdown2
8
8
  msgpack
9
9
  platformdirs
@@ -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.29.0'
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.27.0 released on 21-Feb-24
3
- https://github.com/encode/httpx/releases/tag/0.27.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 isn't installed.
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
- from typing import (
13
- Any,
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
- HeaderTypes: TypeAlias = (
27
- 'Headers' | Mapping[str, str] | Mapping[bytes, bytes] | Sequence[tuple[str, str]] | Sequence[tuple[bytes, bytes]]
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
- def normalize_header_key(
32
- value: str | bytes,
33
- lower: bool,
34
- encoding: str | None = None,
35
- ) -> bytes:
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
- if isinstance(value, bytes):
40
- bytes_value = value
41
- else:
42
- bytes_value = value.encode(encoding or 'ascii')
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
- SENSITIVE_HEADERS = {'authorization', 'proxy-authorization'}
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
- def to_str(value: str | bytes, encoding: str = 'utf-8') -> str:
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
- if headers is None:
87
- self._list: list[tuple[bytes, bytes, bytes]] = []
88
- elif isinstance(headers, Headers):
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
- self._list = [
92
- (
93
- normalize_header_key(k, lower=False, encoding=encoding),
94
- normalize_header_key(k, lower=True, encoding=encoding),
95
- normalize_header_value(v, encoding),
96
- )
97
- for k, v in headers.items()
98
- ]
99
- else:
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: list[str] = []
218
+ split_values = []
212
219
  for value in values:
213
- split_values.extend((item.strip() for item in value.split(',')))
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(obfuscate_sensitive_headers(self.multi_items()))
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 v23.2 released on 01-Oct-23
3
- (commit https://github.com/pypa/packaging/commit/7e4f2588923e647bb7c3d5b350473e39e4b279d4).
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) -> 'Version':
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("invalid")
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: '_BaseVersion') -> bool:
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: '_BaseVersion') -> bool:
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("1.0a5")
427
- >>> v2 = Version("1.0")
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"Invalid version: '{version}'")
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 rounded-tripped.
493
+ """A string representation of the version that can be round-tripped.
492
494
 
493
- >>> str(Version("1.0a5"))
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("2.0.0").epoch
529
+ >>> Version('2.0.0').epoch
528
530
  0
529
- >>> Version("1!2.0.0").epoch
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("1.2.3").release
540
+ >>> Version('1.2.3').release
539
541
  (1, 2, 3)
540
- >>> Version("2.0.0").release
542
+ >>> Version('2.0.0').release
541
543
  (2, 0, 0)
542
- >>> Version("1!2.0.0.post0").release
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("1.2.3").pre)
555
+ >>> print(Version('1.2.3').pre)
554
556
  None
555
- >>> Version("1.2.3a1").pre
557
+ >>> Version('1.2.3a1').pre
556
558
  ('a', 1)
557
- >>> Version("1.2.3b1").pre
559
+ >>> Version('1.2.3b1').pre
558
560
  ('b', 1)
559
- >>> Version("1.2.3rc1").pre
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("1.2.3").post)
570
+ >>> print(Version('1.2.3').post)
569
571
  None
570
- >>> Version("1.2.3.post1").post
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("1.2.3").dev)
581
+ >>> print(Version('1.2.3').dev)
580
582
  None
581
- >>> Version("1.2.3.dev1").dev
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("1.2.3").local)
592
+ >>> print(Version('1.2.3').local)
591
593
  None
592
- >>> Version("1.2.3+abc").local
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("1.2.3").public
605
- '1.2.3'
606
- >>> Version("1.2.3+abc").public
606
+ >>> Version('1.2.3').public
607
607
  '1.2.3'
608
- >>> Version("1.2.3+abc.dev1").public
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("1.2.3").base_version
619
+ >>> Version('1.2.3').base_version
618
620
  '1.2.3'
619
- >>> Version("1.2.3+abc").base_version
621
+ >>> Version('1.2.3+abc').base_version
620
622
  '1.2.3'
621
- >>> Version("1!1.2.3+abc.dev1").base_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("1.2.3").is_prerelease
643
+ >>> Version('1.2.3').is_prerelease
642
644
  False
643
- >>> Version("1.2.3a1").is_prerelease
645
+ >>> Version('1.2.3a1').is_prerelease
644
646
  True
645
- >>> Version("1.2.3b1").is_prerelease
647
+ >>> Version('1.2.3b1').is_prerelease
646
648
  True
647
- >>> Version("1.2.3rc1").is_prerelease
649
+ >>> Version('1.2.3rc1').is_prerelease
648
650
  True
649
- >>> Version("1.2.3dev1").is_prerelease
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("1.2.3").is_postrelease
660
+ >>> Version('1.2.3').is_postrelease
659
661
  False
660
- >>> Version("1.2.3.post1").is_postrelease
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("1.2.3").is_devrelease
671
+ >>> Version('1.2.3').is_devrelease
670
672
  False
671
- >>> Version("1.2.3.dev1").is_devrelease
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("1.2.3").major
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("1.2.3").minor
691
+ >>> Version('1.2.3').minor
690
692
  2
691
- >>> Version("1").minor
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("1.2.3").micro
702
+ >>> Version('1.2.3').micro
701
703
  3
702
- >>> Version("1").micro
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
- if not letter and number:
730
- # We assume if we are given a number, but we are not given a letter then this is using the implicit post
731
- # release syntax (e.g. 1.0-1)
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 ("abc", 1, "twelve").
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(