chore(lsprotocol): migrate to 2025.0.0 and cleanup artifacts

The changes align the project with the 2025.0.0 lsprotocol
release, removing the old backport and updating type hints
in the protocol hooks to use Sequence where appropriate. The
dist-info and packaging metadata for older lsprotocol
versions are replaced with the new 2025.0.0 artifacts.

- Remove exceptiongroup backport used on Python <3.11
- Use Sequence instead of List in LS protocol hooks
- Replace old dist-info with 2025.0.0 metadata
This commit is contained in:
Christoph Brandau
2026-09-03 08:39:12 +02:00
parent a0a0d38fe5
commit 53ebc5d055
101 changed files with 13838 additions and 7624 deletions
+143 -53
View File
@@ -6,6 +6,7 @@ import email.parser
import email.policy
import keyword
import pathlib
import re
import typing
from typing import (
Any,
@@ -22,6 +23,7 @@ from .errors import ExceptionGroup, _ErrorCollector
if typing.TYPE_CHECKING:
from .licenses import NormalizedLicenseExpression
from .version import Version
T = typing.TypeVar("T")
@@ -42,7 +44,10 @@ def __dir__() -> list[str]:
class InvalidMetadata(ValueError):
"""A metadata field contains invalid data."""
"""A metadata field contains invalid data.
.. versionadded:: 23.2
"""
field: str
"""The name of the field that contains invalid data."""
@@ -51,6 +56,10 @@ class InvalidMetadata(ValueError):
self.field = field
super().__init__(message)
# Support pickling; ``args`` does not match ``__init__``'s signature.
def __reduce__(self) -> tuple[type[InvalidMetadata], tuple[str, str]]:
return (self.__class__, (self.field, self.args[0]))
# The RawMetadata class attempts to make as few assumptions about the underlying
# serialization formats as possible. The idea is that as long as a serialization
@@ -125,11 +134,17 @@ class RawMetadata(TypedDict, total=False):
# Metadata 2.4 - PEP 639
license_expression: str
""".. versionadded:: 24.2"""
license_files: list[str]
""".. versionadded:: 24.2"""
# Metadata 2.5 - PEP 794
import_names: list[str]
""".. versionadded:: 26.0"""
import_namespaces: list[str]
""".. versionadded:: 26.0"""
# Metadata 2.6 - PEP 808 (no new fields, behavior change for Dynamic)
# 'keywords' is special as it's a string in the core metadata spec, but we
@@ -225,13 +240,19 @@ def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
# and we don't need to deal with it.
if isinstance(source, str):
payload = msg.get_payload()
assert isinstance(payload, str)
# A multipart payload makes get_payload() return a list of messages
# rather than a str; route it to ``unparsed``.
if not isinstance(payload, str):
raise ValueError("payload is not a string") # noqa: TRY004
return payload
# If our source is a bytes, then we're managing the encoding and we need
# to deal with it.
else:
bpayload = msg.get_payload(decode=True)
assert isinstance(bpayload, bytes)
# A multipart payload makes get_payload(decode=True) return None;
# route it to ``unparsed``.
if not isinstance(bpayload, bytes):
raise ValueError("payload in an invalid encoding") # noqa: TRY004
try:
return bpayload.decode("utf8", "strict")
except UnicodeDecodeError as exc:
@@ -287,11 +308,19 @@ _EMAIL_TO_RAW_MAPPING = {
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
# A bare "\r" makes the email generator raise ``HeaderWriteError``, and on
# CPython releases without the CVE-2024-6923 fix any ``str.splitlines``
# boundary ends the header line, so fold all of them, not just "\n".
_LINE_BOUNDARY_RE = re.compile(r"\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]")
# This class is for writing RFC822 messages
class RFC822Policy(email.policy.EmailPolicy):
"""
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
implementation that handles multi-line values, and some nice defaults.
.. versionadded:: 26.0
"""
utf8 = True
@@ -300,7 +329,7 @@ class RFC822Policy(email.policy.EmailPolicy):
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
size = len(name) + 2
value = value.replace("\n", "\n" + " " * size)
value = _LINE_BOUNDARY_RE.sub("\n" + " " * size, value)
return (name, value)
@@ -310,6 +339,8 @@ class RFC822Message(email.message.EmailMessage):
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
our `RFC822Policy`, and it correctly writes unicode when being called
with `bytes()`.
.. versionadded:: 26.0
"""
def __init__(self) -> None:
@@ -511,8 +542,20 @@ _NOT_FOUND = object()
# Keep the two values in sync.
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
_VALID_METADATA_VERSIONS = [
"1.0",
"1.1",
"1.2",
"2.1",
"2.2",
"2.3",
"2.4",
"2.5",
"2.6",
]
_MetadataVersion = Literal[
"1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6"
]
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
@@ -572,9 +615,7 @@ class _Validator(Generic[T]):
def _invalid_metadata(
self, msg: str, cause: Exception | None = None
) -> InvalidMetadata:
exc = InvalidMetadata(
self.raw_name, msg.format_map({"field": repr(self.raw_name)})
)
exc = InvalidMetadata(self.raw_name, msg)
exc.__cause__ = cause
return exc
@@ -586,67 +627,82 @@ class _Validator(Generic[T]):
def _process_name(self, value: str) -> str:
if not value:
raise self._invalid_metadata("{field} is a required field")
raise self._invalid_metadata(f"{self.raw_name!r} is a required field")
# Validate the name as a side-effect.
try:
utils.canonicalize_name(value, validate=True)
except utils.InvalidName as exc:
raise self._invalid_metadata(
f"{value!r} is invalid for {{field}}", cause=exc
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
) from exc
else:
return value
def _process_version(self, value: str) -> version_module.Version:
def _process_version(self, value: str) -> Version:
if not value:
raise self._invalid_metadata("{field} is a required field")
raise self._invalid_metadata(f"{self.raw_name!r} is a required field")
try:
return version_module.parse(value)
except version_module.InvalidVersion as exc:
raise self._invalid_metadata(
f"{value!r} is invalid for {{field}}", cause=exc
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
) from exc
def _process_summary(self, value: str) -> str:
"""Check the field contains no newlines."""
if "\n" in value:
raise self._invalid_metadata("{field} must be a single line")
"""Check the field contains no line breaks."""
if _LINE_BOUNDARY_RE.search(value):
raise self._invalid_metadata(f"{self.raw_name!r} must be a single line")
return value
def _process_description_content_type(self, value: str) -> str:
content_types = {"text/plain", "text/x-rst", "text/markdown"}
invalid_msg = (
f"{self.raw_name!r} must be one of {list(content_types)}, not {value!r}"
)
message = email.message.EmailMessage()
message["content-type"] = value
try:
message["content-type"] = value
# The email parser can raise IndexError on malformed RFC 2231
# parameters such as "text/plain; x*".
except (ValueError, IndexError) as exc:
msg = f"{value!r} is not a valid content type for {self.raw_name!r}"
raise self._invalid_metadata(msg, cause=exc) from exc
content_type_header = message["content-type"]
if content_type_header.defects:
defect = content_type_header.defects[0]
msg = (
f"{value!r} is not a valid content type for {self.raw_name!r}: {defect}"
)
raise self._invalid_metadata(msg, cause=defect) from defect
content_type, parameters = (
# Defaults to `text/plain` if parsing failed.
message.get_content_type().lower(),
message["content-type"].params,
content_type_header.params,
)
# Check if content-type is valid or defaulted to `text/plain` and thus was
# not parseable.
if content_type not in content_types or content_type not in value.lower():
raise self._invalid_metadata(
f"{{field}} must be one of {list(content_types)}, not {value!r}"
)
raise self._invalid_metadata(invalid_msg)
charset = parameters.get("charset", "UTF-8")
if charset != "UTF-8":
if charset.lower() != "utf-8":
raise self._invalid_metadata(
f"{{field}} can only specify the UTF-8 charset, not {charset!r}"
f"{self.raw_name!r} can only specify the UTF-8 charset, not {charset!r}"
)
markdown_variants = {"GFM", "CommonMark"}
variant = parameters.get("variant", "GFM") # Use an acceptable default.
if content_type == "text/markdown" and variant not in markdown_variants:
raise self._invalid_metadata(
f"valid Markdown variants for {{field}} are {list(markdown_variants)}, "
f"not {variant!r}",
f"valid Markdown variants for {self.raw_name!r} are "
f"{list(markdown_variants)}, not {variant!r}",
)
return value
def _process_dynamic(self, value: list[str]) -> list[str]:
for dynamic_field in map(str.lower, value):
dynamic_fields = list(map(str.lower, value))
for dynamic_field in dynamic_fields:
if dynamic_field in {"name", "version", "metadata-version"}:
raise self._invalid_metadata(
f"{dynamic_field!r} is not allowed as a dynamic field"
@@ -655,7 +711,7 @@ class _Validator(Generic[T]):
raise self._invalid_metadata(
f"{dynamic_field!r} is not a valid dynamic field"
)
return list(map(str.lower, value))
return dynamic_fields
def _process_provides_extra(
self,
@@ -667,7 +723,7 @@ class _Validator(Generic[T]):
normalized_names.append(utils.canonicalize_name(name, validate=True))
except utils.InvalidName as exc:
raise self._invalid_metadata(
f"{name!r} is invalid for {{field}}", cause=exc
f"{name!r} is invalid for {self.raw_name!r}", cause=exc
) from exc
else:
return normalized_names
@@ -677,7 +733,7 @@ class _Validator(Generic[T]):
return specifiers.SpecifierSet(value)
except specifiers.InvalidSpecifier as exc:
raise self._invalid_metadata(
f"{value!r} is invalid for {{field}}", cause=exc
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
) from exc
def _process_requires_dist(
@@ -690,7 +746,7 @@ class _Validator(Generic[T]):
reqs.append(requirements.Requirement(req))
except requirements.InvalidRequirement as exc:
raise self._invalid_metadata(
f"{req!r} is invalid for {{field}}", cause=exc
f"{req!r} is invalid for {self.raw_name!r}", cause=exc
) from exc
else:
return reqs
@@ -700,7 +756,7 @@ class _Validator(Generic[T]):
return licenses.canonicalize_license_expression(value)
except ValueError as exc:
raise self._invalid_metadata(
f"{value!r} is invalid for {{field}}", cause=exc
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
) from exc
def _process_license_files(self, value: list[str]) -> list[str]:
@@ -708,23 +764,24 @@ class _Validator(Generic[T]):
for path in value:
if ".." in path:
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, "
f"{path!r} is invalid for {self.raw_name!r}, "
"parent directory indicators are not allowed"
)
if "*" in path:
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, paths must be resolved"
f"{path!r} is invalid for {self.raw_name!r}, paths must be resolved"
)
if (
pathlib.PurePosixPath(path).is_absolute()
or pathlib.PureWindowsPath(path).is_absolute()
):
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, paths must be relative"
f"{path!r} is invalid for {self.raw_name!r}, paths must be relative"
)
if pathlib.PureWindowsPath(path).as_posix() != path:
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, paths must use '/' delimiter"
f"{path!r} is invalid for {self.raw_name!r}, "
"paths must use '/' delimiter"
)
paths.append(path)
return paths
@@ -736,17 +793,17 @@ class _Validator(Generic[T]):
for identifier in name.split("."):
if not identifier.isidentifier():
raise self._invalid_metadata(
f"{name!r} is invalid for {{field}}; "
f"{name!r} is invalid for {self.raw_name!r}; "
f"{identifier!r} is not a valid identifier"
)
elif keyword.iskeyword(identifier):
raise self._invalid_metadata(
f"{name!r} is invalid for {{field}}; "
f"{name!r} is invalid for {self.raw_name!r}; "
f"{identifier!r} is a keyword"
)
if semicolon and private.lstrip() != "private":
raise self._invalid_metadata(
f"{import_name!r} is invalid for {{field}}; "
f"{import_name!r} is invalid for {self.raw_name!r}; "
"the only valid option is 'private'"
)
return value
@@ -761,6 +818,12 @@ class Metadata:
metadata fields instead of only using built-in types. Any invalid metadata
will cause :exc:`InvalidMetadata` to be raised (with a
:py:attr:`~BaseException.__cause__` attribute as appropriate).
.. versionadded:: 23.2
.. versionchanged:: 24.0
Optional attributes now return None when the field is absent instead of
raising.
"""
_raw: RawMetadata
@@ -769,8 +832,9 @@ class Metadata:
def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> Metadata:
"""Create an instance from :class:`RawMetadata`.
If *validate* is true, all metadata will be validated. All exceptions
related to validation will be gathered and raised as an :class:`ExceptionGroup`.
If *validate* is true, all metadata will be validated and all related
exceptions will be gathered and raised as an :class:`ExceptionGroup`;
otherwise, validation happens per attribute when it is accessed.
"""
ins = cls()
ins._raw = data.copy() # Mutations occur due to caching enriched values.
@@ -829,20 +893,32 @@ class Metadata:
raw, unparsed = parse_email(data)
if validate:
with _ErrorCollector().on_exit("unparsed") as collector:
with _ErrorCollector().on_exit("invalid or unparsed metadata") as collector:
for unparsed_key in unparsed:
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
message = f"{unparsed_key!r} has invalid data"
else:
message = f"unrecognized field: {unparsed_key!r}"
collector.error(InvalidMetadata(unparsed_key, message))
try:
validated = cls.from_raw(raw, validate=validate)
except ExceptionGroup as exc_group:
# The no-branch pragmas cover arcs only seen by Python 3.9.
for exc in exc_group.exceptions: # pragma: no branch
# A required field reported above as unparsed is absent
# from `raw`, so skip from_raw's duplicate "missing"
# complaint.
if not (
isinstance(exc, InvalidMetadata)
and exc.field in unparsed
and _EMAIL_TO_RAW_MAPPING.get(exc.field) not in raw
):
collector.error(exc)
else:
if not collector.errors: # pragma: no branch
return validated
try:
return cls.from_raw(raw, validate=validate)
except ExceptionGroup as exc_group:
raise ExceptionGroup(
"invalid or unparsed metadata", exc_group.exceptions
) from None
return cls.from_raw(raw, validate=validate)
metadata_version: _Validator[_MetadataVersion] = _Validator()
""":external:ref:`core-metadata-metadata-version`
@@ -853,7 +929,7 @@ class Metadata:
""":external:ref:`core-metadata-name`
(required; validated using :func:`~packaging.utils.canonicalize_name` and its
*validate* parameter)"""
version: _Validator[version_module.Version] = _Validator()
version: _Validator[Version] = _Validator()
""":external:ref:`core-metadata-version` (required)"""
dynamic: _Validator[list[str] | None] = _Validator(
added="2.2",
@@ -889,9 +965,15 @@ class Metadata:
license_expression: _Validator[NormalizedLicenseExpression | None] = _Validator(
added="2.4"
)
""":external:ref:`core-metadata-license-expression`"""
""":external:ref:`core-metadata-license-expression`
.. versionadded:: 24.2
"""
license_files: _Validator[list[str] | None] = _Validator(added="2.4")
""":external:ref:`core-metadata-license-file`"""
""":external:ref:`core-metadata-license-file`
.. versionadded:: 24.2
"""
classifiers: _Validator[list[str] | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-classifier`"""
requires_dist: _Validator[list[requirements.Requirement] | None] = _Validator(
@@ -919,9 +1001,15 @@ class Metadata:
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-obsoletes-dist`"""
import_names: _Validator[list[str] | None] = _Validator(added="2.5")
""":external:ref:`core-metadata-import-name`"""
""":external:ref:`core-metadata-import-name`
.. versionadded:: 26.0
"""
import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5")
""":external:ref:`core-metadata-import-namespace`"""
""":external:ref:`core-metadata-import-namespace`
.. versionadded:: 26.0
"""
requires: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Requires`` (deprecated)"""
provides: _Validator[list[str] | None] = _Validator(added="1.1")
@@ -932,6 +1020,8 @@ class Metadata:
def as_rfc822(self) -> RFC822Message:
"""
Return an RFC822 message with the metadata.
.. versionadded:: 26.0
"""
message = RFC822Message()
self._write_metadata(message)