From 8bd45f4a2e0a7cba8f38bfdf8ce7811d00eb23ea Mon Sep 17 00:00:00 2001 From: "Jonathan \"Jono\" Yang" Date: Wed, 3 Oct 2018 17:18:04 -0700 Subject: [PATCH 1/3] Break out normalization of qualifiers as its own function * Modify PackageURL constructor to accept a string of qualifiers in addition to a dictionary of qualifiers Signed-off-by: Jono Yang --- src/packageurl.py | 80 ++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/src/packageurl.py b/src/packageurl.py index 6743280..6be3058 100644 --- a/src/packageurl.py +++ b/src/packageurl.py @@ -65,9 +65,15 @@ def quote(s): return quoted.replace('%3A', ':') -def normalize(type, namespace, name, version, qualifiers, subpath, encode=True): # NOQA +def normalize_qualifiers(qualifiers, encode=True): """ - Return normalized purl components. + Return normalized qualifiers. + + If `qualifiers` is a dictionary of qualifiers and values and `encode` is true, + the dictionary is then converted to a string of qualifiers, formatted to the purl specifications. + + If `qualifiers` is a string of qualfiers, formatted to the purl specifications, and `encode` + is false, the string is then converted to a dictionary of qualifiers and their values. """ if encode is True: quoting = quote @@ -76,31 +82,6 @@ def normalize(type, namespace, name, version, qualifiers, subpath, encode=True): elif encode is None: quoting = lambda x: x - if type: - type = type.strip().lower() # NOQA - - if namespace: - namespace = namespace.strip().strip('/') - if type and type in ('bitbucket', 'github', 'pypi'): - namespace = namespace.lower() - segments = namespace.split('/') - segments = [seg for seg in segments if seg and seg.strip()] - segments = map(quoting, segments) - namespace = '/'.join(segments) - - if name: - name = name.strip().strip('/') - if type in ('bitbucket', 'github', 'pypi',): - name = name.lower() - if type in ('pypi',): - name = name.replace('_', '-') - name = quoting(name) - - name = name or None - - if version: - version = quoting(version.strip()) - if qualifiers: if isinstance(qualifiers, basestring): # decode string to dict @@ -130,6 +111,47 @@ def normalize(type, namespace, name, version, qualifiers, subpath, encode=True): qualifiers = ['{}={}'.format(k, v) for k, v in qualifiers] qualifiers = '&'.join(qualifiers) + return qualifiers + + +def normalize(type, namespace, name, version, qualifiers, subpath, encode=True): # NOQA + """ + Return normalized purl components. + """ + if encode is True: + quoting = quote + elif encode is False: + quoting = percent_unquote + elif encode is None: + quoting = lambda x: x + + if type: + type = type.strip().lower() # NOQA + + if namespace: + namespace = namespace.strip().strip('/') + if type and type in ('bitbucket', 'github', 'pypi'): + namespace = namespace.lower() + segments = namespace.split('/') + segments = [seg for seg in segments if seg and seg.strip()] + segments = map(quoting, segments) + namespace = '/'.join(segments) + + if name: + name = name.strip().strip('/') + if type in ('bitbucket', 'github', 'pypi',): + name = name.lower() + if type in ('pypi',): + name = name.replace('_', '-') + name = quoting(name) + + name = name or None + + if version: + version = quoting(version.strip()) + + qualifiers = normalize_qualifiers(qualifiers, encode) + if subpath: segments = subpath.split('/') segments = [quoting(s) for s in segments if s and s.strip() @@ -167,8 +189,8 @@ def __new__(self, type=None, namespace=None, name=None, # NOQA raise ValueError('Invalid purl: {} argument must be a string: {}.' .format(key, repr(value))) - if qualifiers and not isinstance(qualifiers, (dict, OrderedDict,)): - raise ValueError('Invalid purl: {} argument must be a dict: {}.' + if qualifiers and not isinstance(qualifiers, (basestring, dict, OrderedDict,)): + raise ValueError('Invalid purl: {} argument must be a dict or a string: {}.' .format('qualifiers', repr(qualifiers))) type, namespace, name, version, qualifiers, subpath = normalize(# NOQA From c2ffbd70808f30e138769bce069b4282839e507a Mon Sep 17 00:00:00 2001 From: "Jonathan \"Jono\" Yang" Date: Thu, 4 Oct 2018 12:56:49 -0700 Subject: [PATCH 2/3] Move quoter selection logic to its own function Signed-off-by: Jono Yang --- src/packageurl.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/packageurl.py b/src/packageurl.py index 6be3058..5916f6e 100644 --- a/src/packageurl.py +++ b/src/packageurl.py @@ -65,6 +65,18 @@ def quote(s): return quoted.replace('%3A', ':') +def get_quoter(encode=True): + """ + Return quoting callable given an `encode` tri-boolean (True, False or None) + """ + if encode is True: + return quote + elif encode is False: + return percent_unquote + elif encode is None: + return lambda x: x + + def normalize_qualifiers(qualifiers, encode=True): """ Return normalized qualifiers. @@ -75,12 +87,7 @@ def normalize_qualifiers(qualifiers, encode=True): If `qualifiers` is a string of qualfiers, formatted to the purl specifications, and `encode` is false, the string is then converted to a dictionary of qualifiers and their values. """ - if encode is True: - quoting = quote - elif encode is False: - quoting = percent_unquote - elif encode is None: - quoting = lambda x: x + quoting = get_quoter(encode) if qualifiers: if isinstance(qualifiers, basestring): @@ -111,26 +118,21 @@ def normalize_qualifiers(qualifiers, encode=True): qualifiers = ['{}={}'.format(k, v) for k, v in qualifiers] qualifiers = '&'.join(qualifiers) - return qualifiers + return qualifiers or None def normalize(type, namespace, name, version, qualifiers, subpath, encode=True): # NOQA """ Return normalized purl components. """ - if encode is True: - quoting = quote - elif encode is False: - quoting = percent_unquote - elif encode is None: - quoting = lambda x: x + quoting = get_quoter(encode) if type: type = type.strip().lower() # NOQA if namespace: namespace = namespace.strip().strip('/') - if type and type in ('bitbucket', 'github', 'pypi'): + if type in ('bitbucket', 'github', 'pypi'): namespace = namespace.lower() segments = namespace.split('/') segments = [seg for seg in segments if seg and seg.strip()] From 2a4ceec6e136519fdf6aa8f049411634af0ecccd Mon Sep 17 00:00:00 2001 From: "Jonathan \"Jono\" Yang" Date: Thu, 4 Oct 2018 16:05:04 -0700 Subject: [PATCH 3/3] Add test for qualifiers normalization Signed-off-by: Jono Yang --- test_purl.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test_purl.py b/test_purl.py index 811ce2d..5871d68 100644 --- a/test_purl.py +++ b/test_purl.py @@ -29,6 +29,7 @@ import re import unittest +from packageurl import normalize_qualifiers from packageurl import PackageURL # Python 2 and 3 support @@ -141,3 +142,31 @@ def build_tests(clazz=PurlTest, test_file='test-suite-data.json'): build_tests() + + +class NormalizePurlQualifiersTest(unittest.TestCase): + canonical_purl = 'pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io/release' + type = 'maven' + namespace = 'org.apache.xmlgraphics' + name = 'batik-anim' + version = '1.9.1' + qualifiers_as_dict = { + 'classifier': 'sources', + 'repository_url': 'repo.spring.io/release' + } + qualifiers_as_string = 'classifier=sources&repository_url=repo.spring.io/release' + subpath = None + + def test_normalize_qualifiers_as_string(self): + assert self.qualifiers_as_string == normalize_qualifiers(self.qualifiers_as_dict, encode=True) + + def test_normalize_qualifiers_as_dict(self): + assert self.qualifiers_as_dict == normalize_qualifiers(self.qualifiers_as_string, encode=False) + + def test_create_PackageURL_from_qualifiers_string(self): + assert self.canonical_purl == PackageURL(self.type, self.namespace, self.name, self.version, + self.qualifiers_as_string, self.subpath).to_string() + + def test_create_PackageURL_from_qualifiers_dict(self): + assert self.canonical_purl == PackageURL(self.type, self.namespace, self.name, self.version, + self.qualifiers_as_dict, self.subpath).to_string()