Skip to content

Commit 29dd138

Browse files
authored
Add an option for exact_match purl QuerySet lookups #118 (#156)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 18672be commit 29dd138

5 files changed

Lines changed: 50 additions & 13 deletions

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Changelog
77
- Add support for Composer in ``purl2url`` and ``url2purl``.
88
https://github.com/package-url/packageurl-python/pull/144
99

10+
- Add an option for ``exact_match`` purl QuerySet lookups in the
11+
``PackageURLQuerySetMixin.for_package_url``method.
12+
https://github.com/package-url/packageurl-python/issues/118
13+
1014
0.15.0 (2024-03-12)
1115
-------------------
1216

src/packageurl/contrib/django/filters.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,24 @@
3030
class PackageURLFilter(django_filters.CharFilter):
3131
"""
3232
Filter by an exact Package URL string.
33-
The special "EMPTY" value allows to retrieve objects with empty
34-
Package URL.
3533
36-
This filter depends on a `for_package_url` and `empty_package_url`
34+
The special "EMPTY" value allows retrieval of objects with an empty Package URL.
35+
36+
This filter depends on `for_package_url` and `empty_package_url`
3737
methods to be available on the Model Manager,
3838
see for example `PackageURLQuerySetMixin`.
39+
40+
When exact_match_only is True, the filter will match only exact Package URL strings.
3941
"""
4042

4143
is_empty = "EMPTY"
44+
exact_match_only = False
4245
help_text = (
4346
'Match Package URL. Use "EMPTY" as value to retrieve objects with empty Package URL.'
4447
)
4548

4649
def __init__(self, *args, **kwargs):
50+
self.exact_match_only = kwargs.pop("exact_match_only", False)
4751
kwargs.setdefault("help_text", self.help_text)
4852
super().__init__(*args, **kwargs)
4953

@@ -58,4 +62,4 @@ def filter(self, qs, value):
5862
if value == self.is_empty:
5963
return qs.empty_package_url()
6064

61-
return qs.for_package_url(value)
65+
return qs.for_package_url(value, exact_match=self.exact_match_only)

src/packageurl/contrib/django/models.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ class PackageURLQuerySetMixin:
3737
Add Package URL filtering methods to a django.db.models.QuerySet.
3838
"""
3939

40-
def for_package_url(self, purl_str, encode=True):
40+
def for_package_url(self, purl_str, encode=True, exact_match=False):
4141
"""
42-
Filter the QuerySet with the provided Package URL string.
43-
The purl string is validated and transformed into filtering lookups.
42+
Filter the QuerySet based on a Package URL (purl) string with an option for
43+
exact match filtering.
44+
45+
When `exact_match` is False (default), the method will match any purl with the
46+
same base fields as `purl_str` and allow variations in other fields.
47+
When `exact_match` is True, only the identical purl will be returned.
4448
"""
45-
lookups = purl_to_lookups(purl_str=purl_str, encode=encode)
49+
lookups = purl_to_lookups(
50+
purl_str=purl_str, encode=encode, include_empty_fields=exact_match
51+
)
4652
if lookups:
4753
return self.filter(**lookups)
4854
return self.none()

src/packageurl/contrib/django/utils.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
from packageurl import PackageURL
2929

3030

31-
def purl_to_lookups(purl_str, encode=True):
31+
def purl_to_lookups(purl_str, encode=True, include_empty_fields=False):
3232
"""
33-
Return a lookups dict built from the provided `purl` string.
34-
Those lookups can be used as QuerySet filters.
33+
Return a lookups dictionary built from the provided `purl` (Package URL) string.
34+
These lookups can be used as QuerySet filters.
35+
If include_empty_fields is provided, the resulting dictionary will include fields
36+
with empty values. This is useful to get exact match.
37+
Note that empty values are always returned as empty strings as the model fields
38+
are defined with `blank=True` and `null=False`.
3539
"""
3640
if not purl_str.startswith("pkg:"):
3741
purl_str = "pkg:" + purl_str
@@ -41,8 +45,11 @@ def purl_to_lookups(purl_str, encode=True):
4145
except ValueError:
4246
return # Not a valid PackageURL
4347

44-
package_url_dict = package_url.to_dict(encode=encode)
45-
return without_empty_values(package_url_dict)
48+
package_url_dict = package_url.to_dict(encode=encode, empty="")
49+
if include_empty_fields:
50+
return package_url_dict
51+
else:
52+
return without_empty_values(package_url_dict)
4653

4754

4855
def without_empty_values(input_dict):

tests/contrib/test_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ def test_purl_to_lookups_with_encode():
5858
}
5959

6060

61+
def test_purl_to_lookups_include_empty_fields():
62+
purl_str = "pkg:alpine/openssl"
63+
assert purl_to_lookups(purl_str) == {
64+
"type": "alpine",
65+
"name": "openssl",
66+
}
67+
assert purl_to_lookups(purl_str, include_empty_fields=True) == {
68+
"type": "alpine",
69+
"namespace": "",
70+
"name": "openssl",
71+
"version": "",
72+
"qualifiers": "",
73+
"subpath": "",
74+
}
75+
76+
6177
def test_get_golang_purl():
6278
assert None == get_golang_purl(None)
6379
golang_purl_1 = get_golang_purl(

0 commit comments

Comments
 (0)