3838from binaryornot .helpers import is_binary_string
3939from git import DiffIndex
4040from git import Repo
41+ from license_expression import Licensing
4142from packageurl import PackageURL
4243from univers .version_range import VersionRange
4344from univers .versions import Version
@@ -117,22 +118,28 @@ class AffectedPackage:
117118 """
118119 Contains a range of affected versions and a fixed version of a given package
119120 The PackageURL supplied must *not* have a version
121+ It must contain either `affected_version_range` or `fixed_version`
120122 """
121123
122124 package : PackageURL
123- affected_version_range : VersionRange
125+ affected_version_range : Optional [ VersionRange ] = None
124126 fixed_version : Optional [Version ] = None
125127
126128 def __post_init__ (self ):
127129 if self .package .version :
128- raise ValueError
130+ raise ValueError ("The PackageURL supplied must not have a version" )
131+ if not (self .affected_version_range or self .fixed_version ):
132+ raise ValueError (
133+ "Affected Package should at least have either a fixed version or affected version range"
134+ )
129135
130136 def get_fixed_purl (self ):
131137 """
132138 Return PackageURL corresponding to object's fixed_version
133139 """
134- fixed_version = self .fixed_version
135- fixed_purl = self .package ._replace (version = str (fixed_version ))
140+ if not self .fixed_version :
141+ raise ValueError ("Affected package should have a fixed version" )
142+ fixed_purl = self .package ._replace (version = str (self .fixed_version ))
136143 return fixed_purl
137144
138145 @classmethod
@@ -152,7 +159,8 @@ def merge(cls, affected_packages: Iterable):
152159 fixed_versions = set ()
153160 purls = set ()
154161 for pkg in affected_packages :
155- affected_version_ranges .add (pkg .affected_version_range )
162+ if pkg .affected_version_range :
163+ affected_version_ranges .add (pkg .affected_version_range )
156164 if pkg .fixed_version :
157165 fixed_versions .add (pkg .fixed_version )
158166 purls .add (pkg .package )
@@ -164,9 +172,12 @@ def to_dict(self):
164172 """
165173 Return a serializable dict that can be converted back using self.from_dict
166174 """
175+ affected_version_range = None
176+ if self .affected_version_range :
177+ affected_version_range = str (self .affected_version_range )
167178 return {
168179 "package" : self .package .to_dict (),
169- "affected_version_range" : str ( self . affected_version_range ) ,
180+ "affected_version_range" : affected_version_range ,
170181 "fixed_version" : str (self .fixed_version ) if self .fixed_version else None ,
171182 }
172183
@@ -176,9 +187,13 @@ def from_dict(cls, affected_pkg: dict):
176187 Return an AffectedPackage object from dict generated by self.to_dict
177188 """
178189 package = PackageURL (** affected_pkg ["package" ])
179- affected_version_range = VersionRange .from_string (affected_pkg ["affected_version_range" ])
190+ affected_version_range = None
191+ if affected_pkg ["affected_version_range" ]:
192+ affected_version_range = VersionRange .from_string (
193+ affected_pkg ["affected_version_range" ]
194+ )
180195 fixed_version = affected_pkg ["fixed_version" ]
181- if fixed_version :
196+ if fixed_version and affected_version_range :
182197 # TODO: revisit after https://github.com/nexB/univers/issues/10
183198 fixed_version = affected_version_range .version_class (fixed_version )
184199
@@ -203,7 +218,7 @@ class AdvisoryData:
203218 """
204219
205220 aliases : List [str ] = dataclasses .field (default_factory = list )
206- summary : str = None
221+ summary : Optional [ str ] = None
207222 affected_packages : List [AffectedPackage ] = dataclasses .field (default_factory = list )
208223 references : List [Reference ] = dataclasses .field (default_factory = list )
209224 date_published : Optional [datetime .datetime ] = None
@@ -217,17 +232,29 @@ class NoLicenseError(Exception):
217232 pass
218233
219234
235+ class InvalidSPDXLicense (Exception ):
236+ pass
237+
238+
220239class Importer :
221240 """
222241 An Importer collects data from various upstreams and returns corresponding AdvisoryData objects
223242 in its advisory_data method. Subclass this class to implement an importer
224243 """
225244
226245 spdx_license_expression = ""
246+ license_url = ""
227247
228248 def __init__ (self ):
229249 if not self .spdx_license_expression :
230250 raise Exception (f"Cannot run importer { self !r} without a license" )
251+ licensing = Licensing ()
252+ try :
253+ licensing .parse (self .spdx_license_expression )
254+ except InvalidSPDXLicense as e :
255+ raise ValueError (
256+ f"{ self .spdx_license_expression !r} is not a valid SPDX license expression"
257+ ) from e
231258
232259 @classproperty
233260 def qualified_name (cls ):
0 commit comments