diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 015ceed..293c7ec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,10 @@ Changelog - Add support for Composer in ``purl2url`` and ``url2purl``. https://github.com/package-url/packageurl-python/pull/144 +- Add an option for ``exact_match`` purl QuerySet lookups in the + ``PackageURLQuerySetMixin.for_package_url``method. + https://github.com/package-url/packageurl-python/issues/118 + 0.15.0 (2024-03-12) ------------------- diff --git a/src/packageurl/contrib/django/filters.py b/src/packageurl/contrib/django/filters.py index cc44c5f..7ed173b 100644 --- a/src/packageurl/contrib/django/filters.py +++ b/src/packageurl/contrib/django/filters.py @@ -30,20 +30,24 @@ class PackageURLFilter(django_filters.CharFilter): """ Filter by an exact Package URL string. - The special "EMPTY" value allows to retrieve objects with empty - Package URL. - This filter depends on a `for_package_url` and `empty_package_url` + The special "EMPTY" value allows retrieval of objects with an empty Package URL. + + This filter depends on `for_package_url` and `empty_package_url` methods to be available on the Model Manager, see for example `PackageURLQuerySetMixin`. + + When exact_match_only is True, the filter will match only exact Package URL strings. """ is_empty = "EMPTY" + exact_match_only = False help_text = ( 'Match Package URL. Use "EMPTY" as value to retrieve objects with empty Package URL.' ) def __init__(self, *args, **kwargs): + self.exact_match_only = kwargs.pop("exact_match_only", False) kwargs.setdefault("help_text", self.help_text) super().__init__(*args, **kwargs) @@ -58,4 +62,4 @@ def filter(self, qs, value): if value == self.is_empty: return qs.empty_package_url() - return qs.for_package_url(value) + return qs.for_package_url(value, exact_match=self.exact_match_only) diff --git a/src/packageurl/contrib/django/models.py b/src/packageurl/contrib/django/models.py index ead1f7f..2d9c236 100644 --- a/src/packageurl/contrib/django/models.py +++ b/src/packageurl/contrib/django/models.py @@ -37,12 +37,18 @@ class PackageURLQuerySetMixin: Add Package URL filtering methods to a django.db.models.QuerySet. """ - def for_package_url(self, purl_str, encode=True): + def for_package_url(self, purl_str, encode=True, exact_match=False): """ - Filter the QuerySet with the provided Package URL string. - The purl string is validated and transformed into filtering lookups. + Filter the QuerySet based on a Package URL (purl) string with an option for + exact match filtering. + + When `exact_match` is False (default), the method will match any purl with the + same base fields as `purl_str` and allow variations in other fields. + When `exact_match` is True, only the identical purl will be returned. """ - lookups = purl_to_lookups(purl_str=purl_str, encode=encode) + lookups = purl_to_lookups( + purl_str=purl_str, encode=encode, include_empty_fields=exact_match + ) if lookups: return self.filter(**lookups) return self.none() diff --git a/src/packageurl/contrib/django/utils.py b/src/packageurl/contrib/django/utils.py index e6c19ff..779d11c 100644 --- a/src/packageurl/contrib/django/utils.py +++ b/src/packageurl/contrib/django/utils.py @@ -28,10 +28,14 @@ from packageurl import PackageURL -def purl_to_lookups(purl_str, encode=True): +def purl_to_lookups(purl_str, encode=True, include_empty_fields=False): """ - Return a lookups dict built from the provided `purl` string. - Those lookups can be used as QuerySet filters. + Return a lookups dictionary built from the provided `purl` (Package URL) string. + These lookups can be used as QuerySet filters. + If include_empty_fields is provided, the resulting dictionary will include fields + with empty values. This is useful to get exact match. + Note that empty values are always returned as empty strings as the model fields + are defined with `blank=True` and `null=False`. """ if not purl_str.startswith("pkg:"): purl_str = "pkg:" + purl_str @@ -41,8 +45,11 @@ def purl_to_lookups(purl_str, encode=True): except ValueError: return # Not a valid PackageURL - package_url_dict = package_url.to_dict(encode=encode) - return without_empty_values(package_url_dict) + package_url_dict = package_url.to_dict(encode=encode, empty="") + if include_empty_fields: + return package_url_dict + else: + return without_empty_values(package_url_dict) def without_empty_values(input_dict): diff --git a/tests/contrib/test_utils.py b/tests/contrib/test_utils.py index 23a01b0..9e3b04a 100644 --- a/tests/contrib/test_utils.py +++ b/tests/contrib/test_utils.py @@ -58,6 +58,22 @@ def test_purl_to_lookups_with_encode(): } +def test_purl_to_lookups_include_empty_fields(): + purl_str = "pkg:alpine/openssl" + assert purl_to_lookups(purl_str) == { + "type": "alpine", + "name": "openssl", + } + assert purl_to_lookups(purl_str, include_empty_fields=True) == { + "type": "alpine", + "namespace": "", + "name": "openssl", + "version": "", + "qualifiers": "", + "subpath": "", + } + + def test_get_golang_purl(): assert None == get_golang_purl(None) golang_purl_1 = get_golang_purl(