From 401bb33804be717b1b8420efb120588c6fdd837c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Apr 2021 10:11:24 -0400 Subject: [PATCH] Honor case-insensitivity of metadata keys using FoldedCase. --- importlib_metadata/_adapters.py | 128 ++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 13 deletions(-) diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index ea70f5b6..e9233409 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -2,6 +2,8 @@ import textwrap import email.message +from ._functools import method_cache + class Message(email.message.Message): def __new__(cls, orig: email.message.Message): @@ -34,18 +36,22 @@ def json(self): Convert PackageMetadata to a JSON-compatible format per PEP 0566. """ - # TODO: Need to match case-insensitive - multiple_use = { - 'Classifier', - 'Obsoletes-Dist', - 'Platform', - 'Project-URL', - 'Provides-Dist', - 'Provides-Extra', - 'Requires-Dist', - 'Requires-External', - 'Supported-Platform', - } + multiple_use = set( + map( + FoldedCase, + [ + 'Classifier', + 'Obsoletes-Dist', + 'Platform', + 'Project-URL', + 'Provides-Dist', + 'Provides-Extra', + 'Requires-Dist', + 'Requires-External', + 'Supported-Platform', + ], + ) + ) def transform(key): value = self.get_all(key) if key in multiple_use else self[key] @@ -54,4 +60,100 @@ def transform(key): tk = key.lower().replace('-', '_') return tk, value - return dict(map(transform, self)) + return dict(map(transform, map(FoldedCase, self))) + + +# from jaraco.text 3.5 +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s != 'Hello World' + False + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta'])) + ['alpha', 'Beta', 'GAMMA'] + + Sequence membership is straightforward. + + >>> "Hello World" in [s] + True + >>> s in ["Hello World"] + True + + You may test for set inclusion, but candidate and elements + must both be folded. + + >>> FoldedCase("Hello World") in {s} + True + >>> s in {FoldedCase("Hello World")} + True + + String inclusion works as long as the FoldedCase object + is on the right. + + >>> "hello" in FoldedCase("Hello World") + True + + But not if the FoldedCase object is on the left: + + >>> FoldedCase('hello') in 'Hello World' + False + + In that case, use in_: + + >>> FoldedCase('hello').in_('Hello World') + True + + >>> FoldedCase('hello') > FoldedCase('Hello') + False + """ + + def __lt__(self, other): + return self.lower() < other.lower() + + def __gt__(self, other): + return self.lower() > other.lower() + + def __eq__(self, other): + return self.lower() == other.lower() + + def __ne__(self, other): + return self.lower() != other.lower() + + def __hash__(self): + return hash(self.lower()) + + def __contains__(self, other): + return super(FoldedCase, self).lower().__contains__(other.lower()) + + def in_(self, other): + "Does self appear in other?" + return self in FoldedCase(other) + + # cache lower since it's likely to be called frequently. + @method_cache + def lower(self): + return super(FoldedCase, self).lower() + + def index(self, sub): + return self.lower().index(sub.lower()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit)