Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-packaging for
openSUSE:Factory checked in at 2023-04-17 17:40:56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-packaging (Old)
and /work/SRC/openSUSE:Factory/.python-packaging.new.2023 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-packaging"
Mon Apr 17 17:40:56 2023 rev:29 rq:1079575 version:23.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-packaging/python-packaging.changes
2023-03-12 16:22:18.068250899 +0100
+++
/work/SRC/openSUSE:Factory/.python-packaging.new.2023/python-packaging.changes
2023-04-17 17:40:57.530102162 +0200
@@ -1,0 +2,17 @@
+Sat Apr 15 10:28:14 UTC 2023 - Dirk Müller <[email protected]>
+
+- update to 23.1
+ * chore: add typed classifier
+ * Improve parser error messaging around mistakes in/around
+ version specifiers
+ * Upgrade to latest mypy
+ * Delete print() from test_tags
+ * Update our linters
+ * Improve error for local version label with unsupported operators
+ * Parse raw metadata
+ * Handle prefix match with zeros at end of prefix correctly
+ * Relax typing of _key on _BaseVersion
+ * Replace spaces in platform names with underscores
+ * Import underlying parser function as an underscored variable
+
+-------------------------------------------------------------------
Old:
----
packaging-23.0.tar.gz
New:
----
packaging-23.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-packaging.spec ++++++
--- /var/tmp/diff_new_pack.l2VM4p/_old 2023-04-17 17:40:57.994104874 +0200
+++ /var/tmp/diff_new_pack.l2VM4p/_new 2023-04-17 17:40:57.998104897 +0200
@@ -52,7 +52,7 @@
%endif
Name: %{pprefix}-packaging%{?psuffix}
-Version: 23.0
+Version: 23.1
Release: 0
Summary: Core utilities for Python packages
License: Apache-2.0 AND BSD-2-Clause
++++++ packaging-23.0.tar.gz -> packaging-23.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/PKG-INFO new/packaging-23.1/PKG-INFO
--- old/packaging-23.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/packaging-23.1/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: packaging
-Version: 23.0
+Version: 23.1
Summary: Core utilities for Python packages
Author-email: Donald Stufft <[email protected]>
Requires-Python: >=3.7
@@ -19,6 +19,7 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Typing :: Typed
Project-URL: Documentation, https://packaging.pypa.io/
Project-URL: Source, https://github.com/pypa/packaging
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/docs/index.rst
new/packaging-23.1/docs/index.rst
--- old/packaging-23.0/docs/index.rst 2022-11-25 20:59:55.164905800 +0100
+++ new/packaging-23.1/docs/index.rst 2023-04-12 18:05:06.672022600 +0200
@@ -25,6 +25,7 @@
specifiers
markers
requirements
+ metadata
tags
utils
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/docs/metadata.rst
new/packaging-23.1/docs/metadata.rst
--- old/packaging-23.0/docs/metadata.rst 1970-01-01 01:00:00.000000000
+0100
+++ new/packaging-23.1/docs/metadata.rst 2023-04-12 18:05:06.672171400
+0200
@@ -0,0 +1,42 @@
+Metadata
+========
+
+.. currentmodule:: packaging.markers
+
+
+Both `source distributions`_ and `binary distributions`
+(_sdists_ and _wheels_, respectively) contain files recording the
+`core metadata`_ for the distribution. This information is used for
+everything from recording the name of the distribution to the
+installation dependencies.
+
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.metadata import parse_email
+ >>> metadata = "Metadata-Version: 2.3\nName: packaging\nVersion: 24.0"
+ >>> raw, unparsed = parse_email(metadata)
+ >>> raw["metadata_version"]
+ '2.3'
+ >>> raw["name"]
+ 'packaging'
+ >>> raw["version"]
+ '24.0'
+
+
+Reference
+---------
+
+Low Level Interface
+'''''''''''''''''''
+
+.. automodule:: packaging.metadata
+ :members:
+
+
+.. _source distributions:
https://packaging.python.org/en/latest/specifications/source-distribution-format/
+.. _binary distributions:
https://packaging.python.org/en/latest/specifications/binary-distribution-format/
+.. _core metadata:
https://packaging.python.org/en/latest/specifications/core-metadata/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/pyproject.toml
new/packaging-23.1/pyproject.toml
--- old/packaging-23.0/pyproject.toml 2022-12-09 00:11:51.459903200 +0100
+++ new/packaging-23.1/pyproject.toml 2023-01-30 16:29:49.449063000 +0100
@@ -25,6 +25,7 @@
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
+ "Typing :: Typed",
]
dependencies = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/__init__.py
new/packaging-23.1/src/packaging/__init__.py
--- old/packaging-23.0/src/packaging/__init__.py 2023-01-08
19:18:23.340532500 +0100
+++ new/packaging-23.1/src/packaging/__init__.py 2023-04-12
18:10:22.111652100 +0200
@@ -6,7 +6,7 @@
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
-__version__ = "23.0"
+__version__ = "23.1"
__author__ = "Donald Stufft and individual contributors"
__email__ = "[email protected]"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/_manylinux.py
new/packaging-23.1/src/packaging/_manylinux.py
--- old/packaging-23.0/src/packaging/_manylinux.py 2022-12-09
00:11:51.460600000 +0100
+++ new/packaging-23.1/src/packaging/_manylinux.py 2023-04-12
18:05:06.672746200 +0200
@@ -14,6 +14,8 @@
EF_ARM_ABI_FLOAT_HARD = 0x00000400
+# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
+# as the type for `path` until then.
@contextlib.contextmanager
def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]:
try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/_parser.py
new/packaging-23.1/src/packaging/_parser.py
--- old/packaging-23.0/src/packaging/_parser.py 2023-01-08 19:00:19.700918200
+0100
+++ new/packaging-23.1/src/packaging/_parser.py 2023-04-12 18:05:06.673098800
+0200
@@ -163,7 +163,11 @@
if not tokenizer.check("LEFT_BRACKET", peek=True):
return []
- with tokenizer.enclosing_tokens("LEFT_BRACKET", "RIGHT_BRACKET"):
+ with tokenizer.enclosing_tokens(
+ "LEFT_BRACKET",
+ "RIGHT_BRACKET",
+ around="extras",
+ ):
tokenizer.consume("WS")
extras = _parse_extras_list(tokenizer)
tokenizer.consume("WS")
@@ -203,7 +207,11 @@
specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
| WS? version_many WS?
"""
- with tokenizer.enclosing_tokens("LEFT_PARENTHESIS", "RIGHT_PARENTHESIS"):
+ with tokenizer.enclosing_tokens(
+ "LEFT_PARENTHESIS",
+ "RIGHT_PARENTHESIS",
+ around="version specifier",
+ ):
tokenizer.consume("WS")
parsed_specifiers = _parse_version_many(tokenizer)
tokenizer.consume("WS")
@@ -217,7 +225,20 @@
"""
parsed_specifiers = ""
while tokenizer.check("SPECIFIER"):
+ span_start = tokenizer.position
parsed_specifiers += tokenizer.read().text
+ if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
+ tokenizer.raise_syntax_error(
+ ".* suffix can only be used with `==` or `!=` operators",
+ span_start=span_start,
+ span_end=tokenizer.position + 1,
+ )
+ if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
+ tokenizer.raise_syntax_error(
+ "Local version label can only be used with `==` or `!=`
operators",
+ span_start=span_start,
+ span_end=tokenizer.position,
+ )
tokenizer.consume("WS")
if not tokenizer.check("COMMA"):
break
@@ -254,7 +275,11 @@
tokenizer.consume("WS")
if tokenizer.check("LEFT_PARENTHESIS", peek=True):
- with tokenizer.enclosing_tokens("LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS"):
+ with tokenizer.enclosing_tokens(
+ "LEFT_PARENTHESIS",
+ "RIGHT_PARENTHESIS",
+ around="marker expression",
+ ):
tokenizer.consume("WS")
marker: MarkerAtom = _parse_marker(tokenizer)
tokenizer.consume("WS")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/_tokenizer.py
new/packaging-23.1/src/packaging/_tokenizer.py
--- old/packaging-23.0/src/packaging/_tokenizer.py 2022-12-13
08:03:34.352187200 +0100
+++ new/packaging-23.1/src/packaging/_tokenizer.py 2023-04-12
18:05:06.673353400 +0200
@@ -78,6 +78,8 @@
"AT": r"\@",
"URL": r"[^ \t]+",
"IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
+ "VERSION_PREFIX_TRAIL": r"\.\*",
+ "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
"WS": r"[ \t]+",
"END": r"$",
}
@@ -167,21 +169,23 @@
)
@contextlib.contextmanager
- def enclosing_tokens(self, open_token: str, close_token: str) ->
Iterator[bool]:
+ def enclosing_tokens(
+ self, open_token: str, close_token: str, *, around: str
+ ) -> Iterator[None]:
if self.check(open_token):
open_position = self.position
self.read()
else:
open_position = None
- yield open_position is not None
+ yield
if open_position is None:
return
if not self.check(close_token):
self.raise_syntax_error(
- f"Expected closing {close_token}",
+ f"Expected matching {close_token} for {open_token}, after
{around}",
span_start=open_position,
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/markers.py
new/packaging-23.1/src/packaging/markers.py
--- old/packaging-23.0/src/packaging/markers.py 2022-12-27 16:22:45.000761000
+0100
+++ new/packaging-23.1/src/packaging/markers.py 2023-04-12 18:05:29.534912600
+0200
@@ -8,7 +8,14 @@
import sys
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
-from ._parser import MarkerAtom, MarkerList, Op, Value, Variable, parse_marker
+from ._parser import (
+ MarkerAtom,
+ MarkerList,
+ Op,
+ Value,
+ Variable,
+ parse_marker as _parse_marker,
+)
from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
@@ -189,7 +196,7 @@
# packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement.
try:
- self._markers = _normalize_extra_values(parse_marker(marker))
+ self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a
recursive type:
# MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
#
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/metadata.py
new/packaging-23.1/src/packaging/metadata.py
--- old/packaging-23.0/src/packaging/metadata.py 1970-01-01
01:00:00.000000000 +0100
+++ new/packaging-23.1/src/packaging/metadata.py 2023-04-12
18:05:06.674206300 +0200
@@ -0,0 +1,408 @@
+import email.feedparser
+import email.header
+import email.message
+import email.parser
+import email.policy
+import sys
+import typing
+from typing import Dict, List, Optional, Tuple, Union, cast
+
+if sys.version_info >= (3, 8): # pragma: no cover
+ from typing import TypedDict
+else: # pragma: no cover
+ if typing.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+ else:
+ try:
+ from typing_extensions import TypedDict
+ except ImportError:
+
+ class TypedDict:
+ def __init_subclass__(*_args, **_kwargs):
+ pass
+
+
+# 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
+# formats offer some very basic primitives in *some* way then we can support
+# serializing to and from that format.
+class RawMetadata(TypedDict, total=False):
+ """A dictionary of raw core metadata.
+
+ Each field in core metadata maps to a key of this dictionary (when data is
+ provided). The key is lower-case and underscores are used instead of dashes
+ compared to the equivalent core metadata field. Any core metadata field
that
+ can be specified multiple times or can hold multiple values in a single
+ field have a key with a plural name.
+
+ Core metadata fields that can be specified multiple times are stored as a
+ list or dict depending on which is appropriate for the field. Any fields
+ which hold multiple values in a single field are stored as a list.
+
+ """
+
+ # Metadata 1.0 - PEP 241
+ metadata_version: str
+ name: str
+ version: str
+ platforms: List[str]
+ summary: str
+ description: str
+ keywords: List[str]
+ home_page: str
+ author: str
+ author_email: str
+ license: str
+
+ # Metadata 1.1 - PEP 314
+ supported_platforms: List[str]
+ download_url: str
+ classifiers: List[str]
+ requires: List[str]
+ provides: List[str]
+ obsoletes: List[str]
+
+ # Metadata 1.2 - PEP 345
+ maintainer: str
+ maintainer_email: str
+ requires_dist: List[str]
+ provides_dist: List[str]
+ obsoletes_dist: List[str]
+ requires_python: str
+ requires_external: List[str]
+ project_urls: Dict[str, str]
+
+ # Metadata 2.0
+ # PEP 426 attempted to completely revamp the metadata format
+ # but got stuck without ever being able to build consensus on
+ # it and ultimately ended up withdrawn.
+ #
+ # However, a number of tools had started emiting METADATA with
+ # `2.0` Metadata-Version, so for historical reasons, this version
+ # was skipped.
+
+ # Metadata 2.1 - PEP 566
+ description_content_type: str
+ provides_extra: List[str]
+
+ # Metadata 2.2 - PEP 643
+ dynamic: List[str]
+
+ # Metadata 2.3 - PEP 685
+ # No new fields were added in PEP 685, just some edge case were
+ # tightened up to provide better interoptability.
+
+
+_STRING_FIELDS = {
+ "author",
+ "author_email",
+ "description",
+ "description_content_type",
+ "download_url",
+ "home_page",
+ "license",
+ "maintainer",
+ "maintainer_email",
+ "metadata_version",
+ "name",
+ "requires_python",
+ "summary",
+ "version",
+}
+
+_LIST_STRING_FIELDS = {
+ "classifiers",
+ "dynamic",
+ "obsoletes",
+ "obsoletes_dist",
+ "platforms",
+ "provides",
+ "provides_dist",
+ "provides_extra",
+ "requires",
+ "requires_dist",
+ "requires_external",
+ "supported_platforms",
+}
+
+
+def _parse_keywords(data: str) -> List[str]:
+ """Split a string of comma-separate keyboards into a list of keywords."""
+ return [k.strip() for k in data.split(",")]
+
+
+def _parse_project_urls(data: List[str]) -> Dict[str, str]:
+ """Parse a list of label/URL string pairings separated by a comma."""
+ urls = {}
+ for pair in data:
+ # Our logic is slightly tricky here as we want to try and do
+ # *something* reasonable with malformed data.
+ #
+ # The main thing that we have to worry about, is data that does
+ # not have a ',' at all to split the label from the Value. There
+ # isn't a singular right answer here, and we will fail validation
+ # later on (if the caller is validating) so it doesn't *really*
+ # matter, but since the missing value has to be an empty str
+ # and our return value is dict[str, str], if we let the key
+ # be the missing value, then they'd have multiple '' values that
+ # overwrite each other in a accumulating dict.
+ #
+ # The other potentional issue is that it's possible to have the
+ # same label multiple times in the metadata, with no solid "right"
+ # answer with what to do in that case. As such, we'll do the only
+ # thing we can, which is treat the field as unparseable and add it
+ # to our list of unparsed fields.
+ parts = [p.strip() for p in pair.split(",", 1)]
+ parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items
+
+ # TODO: The spec doesn't say anything about if the keys should be
+ # considered case sensitive or not... logically they should
+ # be case-preserving and case-insensitive, but doing that
+ # would open up more cases where we might have duplicate
+ # entries.
+ label, url = parts
+ if label in urls:
+ # The label already exists in our set of urls, so this field
+ # is unparseable, and we can just add the whole thing to our
+ # unparseable data and stop processing it.
+ raise KeyError("duplicate labels in project urls")
+ urls[label] = url
+
+ return urls
+
+
+def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str:
+ """Get the body of the message."""
+ # If our source is a str, then our caller has managed encodings for us,
+ # and we don't need to deal with it.
+ if isinstance(source, str):
+ payload: str = msg.get_payload()
+ return payload
+ # If our source is a bytes, then we're managing the encoding and we need
+ # to deal with it.
+ else:
+ bpayload: bytes = msg.get_payload(decode=True)
+ try:
+ return bpayload.decode("utf8", "strict")
+ except UnicodeDecodeError:
+ raise ValueError("payload in an invalid encoding")
+
+
+# The various parse_FORMAT functions here are intended to be as lenient as
+# possible in their parsing, while still returning a correctly typed
+# RawMetadata.
+#
+# To aid in this, we also generally want to do as little touching of the
+# data as possible, except where there are possibly some historic holdovers
+# that make valid data awkward to work with.
+#
+# While this is a lower level, intermediate format than our ``Metadata``
+# class, some light touch ups can make a massive difference in usability.
+
+# Map METADATA fields to RawMetadata.
+_EMAIL_TO_RAW_MAPPING = {
+ "author": "author",
+ "author-email": "author_email",
+ "classifier": "classifiers",
+ "description": "description",
+ "description-content-type": "description_content_type",
+ "download-url": "download_url",
+ "dynamic": "dynamic",
+ "home-page": "home_page",
+ "keywords": "keywords",
+ "license": "license",
+ "maintainer": "maintainer",
+ "maintainer-email": "maintainer_email",
+ "metadata-version": "metadata_version",
+ "name": "name",
+ "obsoletes": "obsoletes",
+ "obsoletes-dist": "obsoletes_dist",
+ "platform": "platforms",
+ "project-url": "project_urls",
+ "provides": "provides",
+ "provides-dist": "provides_dist",
+ "provides-extra": "provides_extra",
+ "requires": "requires",
+ "requires-dist": "requires_dist",
+ "requires-external": "requires_external",
+ "requires-python": "requires_python",
+ "summary": "summary",
+ "supported-platform": "supported_platforms",
+ "version": "version",
+}
+
+
+def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str,
List[str]]]:
+ """Parse a distribution's metadata.
+
+ This function returns a two-item tuple of dicts. The first dict is of
+ recognized fields from the core metadata specification. Fields that can be
+ parsed and translated into Python's built-in types are converted
+ appropriately. All other fields are left as-is. Fields that are allowed to
+ appear multiple times are stored as lists.
+
+ The second dict contains all other fields from the metadata. This includes
+ any unrecognized fields. It also includes any fields which are expected to
+ be parsed into a built-in type but were not formatted appropriately.
Finally,
+ any fields that are expected to appear only once but are repeated are
+ included in this dict.
+
+ """
+ raw: Dict[str, Union[str, List[str], Dict[str, str]]] = {}
+ unparsed: Dict[str, List[str]] = {}
+
+ if isinstance(data, str):
+ parsed =
email.parser.Parser(policy=email.policy.compat32).parsestr(data)
+ else:
+ parsed =
email.parser.BytesParser(policy=email.policy.compat32).parsebytes(data)
+
+ # We have to wrap parsed.keys() in a set, because in the case of multiple
+ # values for a key (a list), the key will appear multiple times in the
+ # list of keys, but we're avoiding that by using get_all().
+ for name in frozenset(parsed.keys()):
+ # Header names in RFC are case insensitive, so we'll normalize to all
+ # lower case to make comparisons easier.
+ name = name.lower()
+
+ # We use get_all() here, even for fields that aren't multiple use,
+ # because otherwise someone could have e.g. two Name fields, and we
+ # would just silently ignore it rather than doing something about it.
+ headers = parsed.get_all(name)
+
+ # The way the email module works when parsing bytes is that it
+ # unconditionally decodes the bytes as ascii using the surrogateescape
+ # handler. When you pull that data back out (such as with get_all() ),
+ # it looks to see if the str has any surrogate escapes, and if it does
+ # it wraps it in a Header object instead of returning the string.
+ #
+ # As such, we'll look for those Header objects, and fix up the
encoding.
+ value = []
+ # Flag if we have run into any issues processing the headers, thus
+ # signalling that the data belongs in 'unparsed'.
+ valid_encoding = True
+ for h in headers:
+ # It's unclear if this can return more types than just a Header or
+ # a str, so we'll just assert here to make sure.
+ assert isinstance(h, (email.header.Header, str))
+
+ # If it's a header object, we need to do our little dance to get
+ # the real data out of it. In cases where there is invalid data
+ # we're going to end up with mojibake, but there's no obvious, good
+ # way around that without reimplementing parts of the Header object
+ # ourselves.
+ #
+ # That should be fine since, if mojibacked happens, this key is
+ # going into the unparsed dict anyways.
+ if isinstance(h, email.header.Header):
+ # The Header object stores it's data as chunks, and each chunk
+ # can be independently encoded, so we'll need to check each
+ # of them.
+ chunks: List[Tuple[bytes, Optional[str]]] = []
+ for bin, encoding in email.header.decode_header(h):
+ try:
+ bin.decode("utf8", "strict")
+ except UnicodeDecodeError:
+ # Enable mojibake.
+ encoding = "latin1"
+ valid_encoding = False
+ else:
+ encoding = "utf8"
+ chunks.append((bin, encoding))
+
+ # Turn our chunks back into a Header object, then let that
+ # Header object do the right thing to turn them into a
+ # string for us.
+ value.append(str(email.header.make_header(chunks)))
+ # This is already a string, so just add it.
+ else:
+ value.append(h)
+
+ # We've processed all of our values to get them into a list of str,
+ # but we may have mojibake data, in which case this is an unparsed
+ # field.
+ if not valid_encoding:
+ unparsed[name] = value
+ continue
+
+ raw_name = _EMAIL_TO_RAW_MAPPING.get(name)
+ if raw_name is None:
+ # This is a bit of a weird situation, we've encountered a key that
+ # we don't know what it means, so we don't know whether it's meant
+ # to be a list or not.
+ #
+ # Since we can't really tell one way or another, we'll just leave
it
+ # as a list, even though it may be a single item list, because
that's
+ # what makes the most sense for email headers.
+ unparsed[name] = value
+ continue
+
+ # If this is one of our string fields, then we'll check to see if our
+ # value is a list of a single item. If it is then we'll assume that
+ # it was emitted as a single string, and unwrap the str from inside
+ # the list.
+ #
+ # If it's any other kind of data, then we haven't the faintest clue
+ # what we should parse it as, and we have to just add it to our list
+ # of unparsed stuff.
+ if raw_name in _STRING_FIELDS and len(value) == 1:
+ raw[raw_name] = value[0]
+ # If this is one of our list of string fields, then we can just assign
+ # the value, since email *only* has strings, and our get_all() call
+ # above ensures that this is a list.
+ elif raw_name in _LIST_STRING_FIELDS:
+ raw[raw_name] = value
+ # Special Case: Keywords
+ # The keywords field is implemented in the metadata spec as a str,
+ # but it conceptually is a list of strings, and is serialized using
+ # ", ".join(keywords), so we'll do some light data massaging to turn
+ # this into what it logically is.
+ elif raw_name == "keywords" and len(value) == 1:
+ raw[raw_name] = _parse_keywords(value[0])
+ # Special Case: Project-URL
+ # The project urls is implemented in the metadata spec as a list of
+ # specially-formatted strings that represent a key and a value, which
+ # is fundamentally a mapping, however the email format doesn't support
+ # mappings in a sane way, so it was crammed into a list of strings
+ # instead.
+ #
+ # We will do a little light data massaging to turn this into a map as
+ # it logically should be.
+ elif raw_name == "project_urls":
+ try:
+ raw[raw_name] = _parse_project_urls(value)
+ except KeyError:
+ unparsed[name] = value
+ # Nothing that we've done has managed to parse this, so it'll just
+ # throw it in our unparseable data and move on.
+ else:
+ unparsed[name] = value
+
+ # We need to support getting the Description from the message payload in
+ # addition to getting it from the the headers. This does mean, though,
there
+ # is the possibility of it being set both ways, in which case we put both
+ # in 'unparsed' since we don't know which is right.
+ try:
+ payload = _get_payload(parsed, data)
+ except ValueError:
+ unparsed.setdefault("description", []).append(
+ parsed.get_payload(decode=isinstance(data, bytes))
+ )
+ else:
+ if payload:
+ # Check to see if we've already got a description, if so then both
+ # it, and this body move to unparseable.
+ if "description" in raw:
+ description_header = cast(str, raw.pop("description"))
+ unparsed.setdefault("description", []).extend(
+ [description_header, payload]
+ )
+ elif "description" in unparsed:
+ unparsed["description"].append(payload)
+ else:
+ raw["description"] = payload
+
+ # We need to cast our `raw` to a metadata, because a TypedDict only support
+ # literal key names, but we're computing our key names on purpose, but the
+ # way this function is implemented, our `TypedDict` can only have valid key
+ # names.
+ return cast(RawMetadata, raw), unparsed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/requirements.py
new/packaging-23.1/src/packaging/requirements.py
--- old/packaging-23.0/src/packaging/requirements.py 2022-12-09
00:11:51.462091000 +0100
+++ new/packaging-23.1/src/packaging/requirements.py 2023-04-12
18:05:06.674517900 +0200
@@ -5,7 +5,7 @@
import urllib.parse
from typing import Any, List, Optional, Set
-from ._parser import parse_requirement
+from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError
from .markers import Marker, _normalize_extra_values
from .specifiers import SpecifierSet
@@ -32,7 +32,7 @@
def __init__(self, requirement_string: str) -> None:
try:
- parsed = parse_requirement(requirement_string)
+ parsed = _parse_requirement(requirement_string)
except ParserSyntaxError as e:
raise InvalidRequirement(str(e)) from e
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/specifiers.py
new/packaging-23.1/src/packaging/specifiers.py
--- old/packaging-23.0/src/packaging/specifiers.py 2023-01-08
19:00:19.701309200 +0100
+++ new/packaging-23.1/src/packaging/specifiers.py 2023-04-12
18:05:06.674899300 +0200
@@ -252,7 +252,8 @@
# Store whether or not this Specifier should accept prereleases
self._prereleases = prereleases
- @property
+ # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515
+ @property # type: ignore[override]
def prereleases(self) -> bool:
# If there is an explicit prereleases set for this, then we'll just
# blindly use that.
@@ -398,7 +399,9 @@
# We need special logic to handle prefix matching
if spec.endswith(".*"):
# In the case of prefix matching we want to ignore local segment.
- normalized_prospective = canonicalize_version(prospective.public)
+ normalized_prospective = canonicalize_version(
+ prospective.public, strip_trailing_zero=False
+ )
# Get the normalized version string ignoring the trailing .*
normalized_spec = canonicalize_version(spec[:-2],
strip_trailing_zero=False)
# Split the spec out by dots, and pretend that there is an implicit
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/tags.py
new/packaging-23.1/src/packaging/tags.py
--- old/packaging-23.0/src/packaging/tags.py 2022-12-26 20:52:02.396540400
+0100
+++ new/packaging-23.1/src/packaging/tags.py 2023-04-12 18:05:06.675215000
+0200
@@ -111,7 +111,7 @@
def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
- value = sysconfig.get_config_var(name)
+ value: Union[int, str, None] = sysconfig.get_config_var(name)
if value is None and warn:
logger.debug(
"Config variable '%s' is unset, Python ABI tag may be incorrect",
name
@@ -120,7 +120,7 @@
def _normalize_string(string: str) -> str:
- return string.replace(".", "_").replace("-", "_")
+ return string.replace(".", "_").replace("-", "_").replace(" ", "_")
def _abi3_applies(python_version: PythonVersion) -> bool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/src/packaging/version.py
new/packaging-23.1/src/packaging/version.py
--- old/packaging-23.0/src/packaging/version.py 2022-12-09 00:11:51.463324000
+0100
+++ new/packaging-23.1/src/packaging/version.py 2023-04-12 18:05:06.675496300
+0200
@@ -10,7 +10,7 @@
import collections
import itertools
import re
-from typing import Callable, Optional, SupportsInt, Tuple, Union
+from typing import Any, Callable, Optional, SupportsInt, Tuple, Union
from ._structures import Infinity, InfinityType, NegativeInfinity,
NegativeInfinityType
@@ -63,7 +63,7 @@
class _BaseVersion:
- _key: CmpKey
+ _key: Tuple[Any, ...]
def __hash__(self) -> int:
return hash(self._key)
@@ -179,6 +179,7 @@
"""
_regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE |
re.IGNORECASE)
+ _key: CmpKey
def __init__(self, version: str) -> None:
"""Initialize a Version object.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/tests/metadata/everything.metadata
new/packaging-23.1/tests/metadata/everything.metadata
--- old/packaging-23.0/tests/metadata/everything.metadata 1970-01-01
01:00:00.000000000 +0100
+++ new/packaging-23.1/tests/metadata/everything.metadata 2023-04-12
18:05:06.675785500 +0200
@@ -0,0 +1,42 @@
+Metadata-Version: 2.3
+Name: BeagleVote
+Version: 1.0a2
+Platform: ObscureUnix
+Platform: RareDOS
+Supported-Platform: RedHat 7.2
+Supported-Platform: i386-win32-2791
+Summary: A module for collecting votes from beagles.
+Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
+Keywords: dog,puppy,voting,election
+Home-page: http://www.example.com/~cschultz/bvote/
+Download-URL: â¦/BeagleVote-0.45.tgz
+Author: C. Schultz, Universal Features Syndicate,
+ Los Angeles, CA <[email protected]>
+Author-email: "C. Schultz" <[email protected]>
+Maintainer: C. Schultz, Universal Features Syndicate,
+ Los Angeles, CA <[email protected]>
+Maintainer-email: "C. Schultz" <[email protected]>
+License: This software may only be obtained by sending the
+ author a postcard, and then the user promises not
+ to redistribute it.
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Console (Text Based)
+Provides-Extra: pdf
+Requires-Dist: reportlab; extra == 'pdf'
+Requires-Dist: pkginfo
+Requires-Dist: PasteDeploy
+Requires-Dist: zope.interface (>3.5.0)
+Requires-Dist: pywin32 >1.0; sys_platform == 'win32'
+Requires-Python: >=3
+Requires-External: C
+Requires-External: libpng (>=1.5)
+Requires-External: make; sys_platform != "win32"
+Project-URL: Bug Tracker, http://bitbucket.org/tarek/distribute/issues/
+Project-URL: Documentation, https://example.com/BeagleVote
+Provides-Dist: OtherProject
+Provides-Dist: AnotherProject (3.4)
+Provides-Dist: virtual_package; python_version >= "3.4"
+Dynamic: Obsoletes-Dist
+ThisIsNotReal: Hello!
+
+This description intentionally left blank.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/tests/test_manylinux.py
new/packaging-23.1/tests/test_manylinux.py
--- old/packaging-23.0/tests/test_manylinux.py 2022-09-30 11:55:33.606207600
+0200
+++ new/packaging-23.1/tests/test_manylinux.py 2023-04-12 18:05:06.676032800
+0200
@@ -3,6 +3,7 @@
except ImportError:
ctypes = None
import os
+import pathlib
import platform
import sys
import types
@@ -169,11 +170,8 @@
)
def test_parse_elf_bad_executable(monkeypatch, content):
if content:
- path = os.path.join(
- os.path.dirname(__file__),
- "manylinux",
- f"hello-world-{content}",
- )
+ path = pathlib.Path(__file__).parent / "manylinux" /
f"hello-world-{content}"
+ path = os.fsdecode(path)
else:
path = None
with _parse_elf(path) as ef:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/tests/test_metadata.py
new/packaging-23.1/tests/test_metadata.py
--- old/packaging-23.0/tests/test_metadata.py 1970-01-01 01:00:00.000000000
+0100
+++ new/packaging-23.1/tests/test_metadata.py 2023-04-12 18:05:06.676202000
+0200
@@ -0,0 +1,249 @@
+import pathlib
+
+import pytest
+
+from packaging import metadata
+
+_RAW_TO_EMAIL_MAPPING = {
+ raw: email for email, raw in metadata._EMAIL_TO_RAW_MAPPING.items()
+}
+
+
+class TestRawMetadata:
+ @pytest.mark.parametrize("raw_field", metadata._STRING_FIELDS)
+ def test_non_repeating_fields_only_once(self, raw_field):
+ data = "VaLuE"
+ header_field = _RAW_TO_EMAIL_MAPPING[raw_field]
+ single_header = f"{header_field}: {data}"
+ raw, unparsed = metadata.parse_email(single_header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert raw_field in raw
+ assert raw[raw_field] == data
+
+ @pytest.mark.parametrize("raw_field", metadata._STRING_FIELDS)
+ def test_non_repeating_fields_repeated(self, raw_field):
+ header_field = _RAW_TO_EMAIL_MAPPING[raw_field]
+ data = "VaLuE"
+ single_header = f"{header_field}: {data}"
+ repeated_header = "\n".join([single_header] * 2)
+ raw, unparsed = metadata.parse_email(repeated_header)
+ assert not raw
+ assert len(unparsed) == 1
+ assert header_field in unparsed
+ assert unparsed[header_field] == [data] * 2
+
+ @pytest.mark.parametrize("raw_field", metadata._LIST_STRING_FIELDS)
+ def test_repeating_fields_only_once(self, raw_field):
+ data = "VaLuE"
+ header_field = _RAW_TO_EMAIL_MAPPING[raw_field]
+ single_header = f"{header_field}: {data}"
+ raw, unparsed = metadata.parse_email(single_header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert raw_field in raw
+ assert raw[raw_field] == [data]
+
+ @pytest.mark.parametrize("raw_field", metadata._LIST_STRING_FIELDS)
+ def test_repeating_fields_repeated(self, raw_field):
+ header_field = _RAW_TO_EMAIL_MAPPING[raw_field]
+ data = "VaLuE"
+ single_header = f"{header_field}: {data}"
+ repeated_header = "\n".join([single_header] * 2)
+ raw, unparsed = metadata.parse_email(repeated_header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert raw_field in raw
+ assert raw[raw_field] == [data] * 2
+
+ @pytest.mark.parametrize(
+ ["given", "expected"],
+ [
+ ("A", ["A"]),
+ ("A ", ["A"]),
+ (" A", ["A"]),
+ ("A, B", ["A", "B"]),
+ ("A,B", ["A", "B"]),
+ (" A, B", ["A", "B"]),
+ ("A,B ", ["A", "B"]),
+ ("A B", ["A B"]),
+ ],
+ )
+ def test_keywords(self, given, expected):
+ header = f"Keywords: {given}"
+ raw, unparsed = metadata.parse_email(header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert "keywords" in raw
+ assert raw["keywords"] == expected
+
+ @pytest.mark.parametrize(
+ ["given", "expected"],
+ [
+ ("", {"": ""}),
+ ("A", {"A": ""}),
+ ("A,B", {"A": "B"}),
+ ("A, B", {"A": "B"}),
+ (" A,B", {"A": "B"}),
+ ("A,B ", {"A": "B"}),
+ ("A,B,C", {"A": "B,C"}),
+ ],
+ )
+ def test_project_urls_parsing(self, given, expected):
+ header = f"project-url: {given}"
+ raw, unparsed = metadata.parse_email(header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert "project_urls" in raw
+ assert raw["project_urls"] == expected
+
+ def test_duplicate_project_urls(self):
+ header = "project-url: A, B\nproject-url: A, C"
+ raw, unparsed = metadata.parse_email(header)
+ assert not raw
+ assert len(unparsed) == 1
+ assert "project-url" in unparsed
+ assert unparsed["project-url"] == ["A, B", "A, C"]
+
+ def test_str_input(self):
+ name = "Tarek Ziadé"
+ header = f"author: {name}"
+ raw, unparsed = metadata.parse_email(header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert "author" in raw
+ assert raw["author"] == name
+
+ def test_bytes_input(self):
+ name = "Tarek Ziadé"
+ header = f"author: {name}".encode()
+ raw, unparsed = metadata.parse_email(header)
+ assert not unparsed
+ assert len(raw) == 1
+ assert "author" in raw
+ assert raw["author"] == name
+
+ def test_header_mojibake(self):
+ value = "\xc0msterdam"
+ header_name = "value"
+ header_bytes = f"{header_name}: {value}".encode("latin1")
+ raw, unparsed = metadata.parse_email(header_bytes)
+ # Sanity check
+ with pytest.raises(UnicodeDecodeError):
+ header_bytes.decode("utf-8")
+ assert not raw
+ assert len(unparsed) == 1
+ assert header_name in unparsed
+ assert unparsed[header_name] == [value]
+
+ @pytest.mark.parametrize(
+ ["given"], [("hello",), ("description: hello",), (b"hello",)]
+ )
+ def test_description(self, given):
+ raw, unparsed = metadata.parse_email(given)
+ assert not unparsed
+ assert len(raw) == 1
+ assert "description" in raw
+ assert raw["description"] == "hello"
+
+ def test_description_non_utf8(self):
+ header = "\xc0msterdam"
+ header_bytes = header.encode("latin1")
+ raw, unparsed = metadata.parse_email(header_bytes)
+ assert not raw
+ assert len(unparsed) == 1
+ assert "description" in unparsed
+ assert unparsed["description"] == [header_bytes]
+
+ @pytest.mark.parametrize(
+ ["given", "expected"],
+ [
+ ("description: 1\ndescription: 2", ["1", "2"]),
+ ("description: 1\n\n2", ["1", "2"]),
+ ("description: 1\ndescription: 2\n\n3", ["1", "2", "3"]),
+ ],
+ )
+ def test_description_multiple(self, given, expected):
+ raw, unparsed = metadata.parse_email(given)
+ assert not raw
+ assert len(unparsed) == 1
+ assert "description" in unparsed
+ assert unparsed["description"] == expected
+
+ def test_lowercase_keys(self):
+ header = "AUTHOR: Tarek Ziadé\nWhatever: Else"
+ raw, unparsed = metadata.parse_email(header)
+ assert len(raw) == 1
+ assert "author" in raw
+ assert len(unparsed) == 1
+ assert "whatever" in unparsed
+
+ def test_complete(self):
+ """Test all fields (except `Obsoletes-Dist`).
+
+ `Obsoletes-Dist` was sacrificed to provide a value for `Dynamic`.
+ """
+ path = pathlib.Path(__file__).parent / "metadata" /
"everything.metadata"
+ with path.open("r", encoding="utf-8") as file:
+ metadata_contents = file.read()
+ raw, unparsed = metadata.parse_email(metadata_contents)
+ assert len(unparsed) == 1
+ assert unparsed["thisisnotreal"] == ["Hello!"]
+ assert len(raw) == 24
+ assert raw["metadata_version"] == "2.3"
+ assert raw["name"] == "BeagleVote"
+ assert raw["version"] == "1.0a2"
+ assert raw["platforms"] == ["ObscureUnix", "RareDOS"]
+ assert raw["supported_platforms"] == ["RedHat 7.2", "i386-win32-2791"]
+ assert raw["summary"] == "A module for collecting votes from beagles."
+ assert (
+ raw["description_content_type"]
+ == "text/markdown; charset=UTF-8; variant=GFM"
+ )
+ assert raw["keywords"] == ["dog", "puppy", "voting", "election"]
+ assert raw["home_page"] == "http://www.example.com/~cschultz/bvote/"
+ assert raw["download_url"] == "â¦/BeagleVote-0.45.tgz"
+ assert raw["author"] == (
+ "C. Schultz, Universal Features Syndicate,\n"
+ " Los Angeles, CA <[email protected]>"
+ )
+ assert raw["author_email"] == '"C. Schultz" <[email protected]>'
+ assert raw["maintainer"] == (
+ "C. Schultz, Universal Features Syndicate,\n"
+ " Los Angeles, CA <[email protected]>"
+ )
+ assert raw["maintainer_email"] == '"C. Schultz" <[email protected]>'
+ assert raw["license"] == (
+ "This software may only be obtained by sending the\n"
+ " author a postcard, and then the user promises not\n"
+ " to redistribute it."
+ )
+ assert raw["classifiers"] == [
+ "Development Status :: 4 - Beta",
+ "Environment :: Console (Text Based)",
+ ]
+ assert raw["provides_extra"] == ["pdf"]
+ assert raw["requires_dist"] == [
+ "reportlab; extra == 'pdf'",
+ "pkginfo",
+ "PasteDeploy",
+ "zope.interface (>3.5.0)",
+ "pywin32 >1.0; sys_platform == 'win32'",
+ ]
+ assert raw["requires_python"] == ">=3"
+ assert raw["requires_external"] == [
+ "C",
+ "libpng (>=1.5)",
+ 'make; sys_platform != "win32"',
+ ]
+ assert raw["project_urls"] == {
+ "Bug Tracker": "http://bitbucket.org/tarek/distribute/issues/",
+ "Documentation": "https://example.com/BeagleVote",
+ }
+ assert raw["provides_dist"] == [
+ "OtherProject",
+ "AnotherProject (3.4)",
+ 'virtual_package; python_version >= "3.4"',
+ ]
+ assert raw["dynamic"] == ["Obsoletes-Dist"]
+ assert raw["description"] == "This description intentionally left
blank.\n"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/tests/test_requirements.py
new/packaging-23.1/tests/test_requirements.py
--- old/packaging-23.0/tests/test_requirements.py 2023-01-08
19:18:04.410140300 +0100
+++ new/packaging-23.1/tests/test_requirements.py 2023-04-12
18:05:06.676444800 +0200
@@ -279,11 +279,48 @@
# THEN
assert ctx.exconly() == (
"packaging.requirements.InvalidRequirement: "
- "Expected closing RIGHT_PARENTHESIS\n"
+ "Expected matching RIGHT_PARENTHESIS for LEFT_PARENTHESIS, "
+ "after version specifier\n"
" name (>= 1.0\n"
" ~~~~~~~^"
)
+ def test_error_when_prefix_match_is_used_incorrectly(self) -> None:
+ # GIVEN
+ to_parse = "black (>=20.*) ; extra == 'format'"
+
+ # WHEN
+ with pytest.raises(InvalidRequirement) as ctx:
+ Requirement(to_parse)
+
+ # THEN
+ assert ctx.exconly() == (
+ "packaging.requirements.InvalidRequirement: "
+ ".* suffix can only be used with `==` or `!=` operators\n"
+ " black (>=20.*) ; extra == 'format'\n"
+ " ~~~~~^"
+ )
+
+ @pytest.mark.parametrize("operator", [">=", "<=", ">", "<", "~="])
+ def test_error_when_local_version_label_is_used_incorrectly(
+ self, operator: str
+ ) -> None:
+ # GIVEN
+ to_parse = f"name {operator} 1.0+local.version.label"
+ op_tilde = len(operator) * "~"
+
+ # WHEN
+ with pytest.raises(InvalidRequirement) as ctx:
+ Requirement(to_parse)
+
+ # THEN
+ assert ctx.exconly() == (
+ "packaging.requirements.InvalidRequirement: "
+ "Local version label can only be used with `==` or `!=`
operators\n"
+ f" name {operator} 1.0+local.version.label\n"
+ f" {op_tilde}~~~~^"
+ )
+
def test_error_when_bracket_not_closed_correctly(self) -> None:
# GIVEN
to_parse = "name[bar, baz >= 1.0"
@@ -295,7 +332,8 @@
# THEN
assert ctx.exconly() == (
"packaging.requirements.InvalidRequirement: "
- "Expected closing RIGHT_BRACKET\n"
+ "Expected matching RIGHT_BRACKET for LEFT_BRACKET, "
+ "after extras\n"
" name[bar, baz >= 1.0\n"
" ~~~~~~~~~~^"
)
@@ -311,7 +349,8 @@
# THEN
assert ctx.exconly() == (
"packaging.requirements.InvalidRequirement: "
- "Expected closing RIGHT_BRACKET\n"
+ "Expected matching RIGHT_BRACKET for LEFT_BRACKET, "
+ "after extras\n"
" name[bar, baz\n"
" ~~~~~~~~~^"
)
@@ -332,6 +371,23 @@
" ~~~~~~~~~~~~~~~~~~~~~~^"
)
+ def test_error_marker_bracket_unclosed(self) -> None:
+ # GIVEN
+ to_parse = "name; (extra == 'example'"
+
+ # WHEN
+ with pytest.raises(InvalidRequirement) as ctx:
+ Requirement(to_parse)
+
+ # THEN
+ assert ctx.exconly() == (
+ "packaging.requirements.InvalidRequirement: "
+ "Expected matching RIGHT_PARENTHESIS for LEFT_PARENTHESIS, "
+ "after marker expression\n"
+ " name; (extra == 'example'\n"
+ " ~~~~~~~~~~~~~~~~~~~^"
+ )
+
def test_error_no_url_after_at(self) -> None:
# GIVEN
to_parse = "name @ "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/tests/test_specifiers.py
new/packaging-23.1/tests/test_specifiers.py
--- old/packaging-23.0/tests/test_specifiers.py 2022-09-30 11:55:33.607073800
+0200
+++ new/packaging-23.1/tests/test_specifiers.py 2023-04-12 18:05:06.676737300
+0200
@@ -369,6 +369,8 @@
("2!1.0", "==2!1.*"),
("2!1.0", "==2!1.0"),
("2!1.0", "!=1.0"),
+ ("2!1.0.0", "==2!1.0.*"),
+ ("2!1.0.0", "==2!1.*"),
("1.0", "!=2!1.0"),
("1.0", "<=2!0.1"),
("2!1.0", ">=2.0"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/packaging-23.0/tests/test_tags.py
new/packaging-23.1/tests/test_tags.py
--- old/packaging-23.0/tests/test_tags.py 2022-12-26 20:52:02.397271200
+0100
+++ new/packaging-23.1/tests/test_tags.py 2023-04-12 18:05:06.677162400
+0200
@@ -234,7 +234,6 @@
platforms = list(tags.mac_platforms(arch="x86_64"))
if (major, minor) == ("10", "16"):
- print(platforms, "macosx_11+")
# For 10.16, the real version is at least 11.0.
prefix, major, minor, _ = platforms[0].split("_", maxsplit=3)
assert prefix == "macosx"
@@ -242,7 +241,6 @@
assert minor == "0"
else:
expected = f"macosx_{major}_{minor}_"
- print(platforms, expected)
assert platforms[0].startswith(expected)
def test_version_detection_10_15(self, monkeypatch):
@@ -252,7 +250,6 @@
expected = "macosx_10_15_"
platforms = list(tags.mac_platforms(arch="x86_64"))
- print(platforms, expected)
assert platforms[0].startswith(expected)
def test_version_detection_compatibility(self, monkeypatch):
@@ -270,7 +267,6 @@
unexpected = "macosx_10_16_"
platforms = list(tags.mac_platforms(arch="x86_64"))
- print(platforms, unexpected)
assert not platforms[0].startswith(unexpected)
@pytest.mark.parametrize("arch", ["x86_64", "i386"])
@@ -603,6 +599,13 @@
assert tags.platform_tags() == expected
+def test_platform_tags_space(monkeypatch):
+ """Ensure spaces in platform tags are normalized to underscores."""
+ monkeypatch.setattr(platform, "system", lambda: "Isilon OneFS")
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "isilon onefs")
+ assert list(tags.platform_tags()) == ["isilon_onefs"]
+
+
class TestCPythonABI:
@pytest.mark.parametrize(
"py_debug,gettotalrefcount,result",
@@ -774,6 +777,12 @@
result = list(tags.cpython_tags((3, 11), abis=["whatever"]))
assert tags.Tag("cp311", "whatever", "plat1") in result
+ def test_platform_name_space_normalization(self, monkeypatch):
+ """Ensure that spaces are translated to underscores in platform
names."""
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "isilon onefs")
+ for tag in tags.cpython_tags():
+ assert " " not in tag.platform
+
def test_major_only_python_version(self):
result = list(tags.cpython_tags((3,), ["abi"], ["plat"]))
assert result == [
@@ -843,9 +852,9 @@
assert tags._generic_abi() == ["cp37m"]
def test__generic_abi_jp(self, monkeypatch):
- config = {"EXT_SUFFIX": ".return exactly this.so"}
+ config = {"EXT_SUFFIX": ".return_exactly_this.so"}
monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
- assert tags._generic_abi() == ["return exactly this"]
+ assert tags._generic_abi() == ["return_exactly_this"]
def test__generic_abi_graal(self, monkeypatch):
config = {"EXT_SUFFIX": ".graalpy-38-native-x86_64-darwin.so"}
@@ -901,6 +910,12 @@
platform = platform.replace(".", "_")
assert list(tags._generic_platforms()) == [platform]
+ def test_generic_platforms_space(self, monkeypatch):
+ """Ensure platform tags normalize spaces to underscores."""
+ platform_ = "isilon onefs"
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: platform_)
+ assert list(tags._generic_platforms()) == [platform_.replace(" ", "_")]
+
def test_iterator_returned(self):
result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1",
"plat2"])
assert isinstance(result_iterator, collections.abc.Iterator)