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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user