Skip to content

Commit 8317634

Browse files
committed
Adjust test structure
Signed-off-by: Tushar Goel <[email protected]>
1 parent 1ac0235 commit 8317634

File tree

4 files changed

+102
-40
lines changed

4 files changed

+102
-40
lines changed

src/packageurl/__init__.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ def validate(self, strict: bool = False) -> list[str]:
538538
return [f"Given type: {self.type} can not be validated"]
539539

540540
@classmethod
541-
def from_string(cls, purl: str) -> Self:
541+
def from_string(cls, purl: str, normalize_purl: bool = True) -> Self:
542542
"""
543543
Return a PackageURL object parsed from a string.
544544
Raise ValueError on errors.
@@ -622,14 +622,18 @@ def from_string(cls, purl: str) -> Self:
622622
if not name:
623623
raise ValueError(f"purl is missing the required name component: {purl!r}")
624624

625-
type_, namespace, name, version, qualifiers, subpath = normalize(
626-
type_,
627-
namespace,
628-
name,
629-
version,
630-
qualifiers_str,
631-
subpath,
632-
encode=False,
625+
if normalize_purl:
626+
type_, namespace, name, version, qualifiers, subpath = normalize(
627+
type_,
628+
namespace,
629+
name,
630+
version,
631+
qualifiers_str,
632+
subpath,
633+
encode=False,
634+
)
635+
else:
636+
qualifiers = normalize_qualifiers(qualifiers_str, encode=False) or {}
637+
return cls(
638+
type_, namespace, name, version, qualifiers, subpath, normalize_purl=normalize_purl
633639
)
634-
635-
return cls(type_, namespace, name, version, qualifiers, subpath)

src/packageurl/validate.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@
2626
Validate each type according to the PURL spec type definitions
2727
"""
2828

29+
from enum import Enum
30+
from dataclasses import dataclass
31+
32+
33+
class ValidationSeverity(Enum):
34+
ERROR = "error"
35+
WARNING = "warning"
36+
INFO = "info"
37+
38+
39+
@dataclass
40+
class ValidationMessage:
41+
severity: ValidationSeverity
42+
message: str
43+
2944

3045
class TypeValidator:
3146
@classmethod
@@ -34,26 +49,45 @@ def validate(cls, purl, strict=False):
3449
purl = cls.normalize(purl)
3550

3651
if cls.namespace_requirement == "prohibited" and purl.namespace:
37-
yield f"Namespace is prohibited for purl type: {cls.type!r}"
52+
yield ValidationMessage(
53+
severity=ValidationSeverity.ERROR,
54+
message=f"Namespace is prohibited for purl type: {cls.type!r}",
55+
)
3856

3957
elif cls.namespace_requirement == "required" and not purl.namespace:
40-
yield f"Namespace is required for purl type: {cls.type!r}"
58+
yield ValidationMessage(
59+
severity=ValidationSeverity.ERROR,
60+
message=f"Namespace is required for purl type: {cls.type!r}",
61+
)
4162

4263
if purl.type == "cpan":
4364
if purl.namespace and purl.namespace != purl.namespace.upper():
44-
yield f"Namespace must be uppercase for purl type: {cls.type!r}"
65+
yield ValidationMessage(
66+
severity=ValidationSeverity.WARNING,
67+
message=f"Namespace must be uppercase for purl type: {cls.type!r}",
68+
)
69+
# TODO: Check pending CPAN PR and decide if we want to upgrade the type definition schema
4570
elif (
4671
not cls.namespace_case_sensitive
4772
and purl.namespace
4873
and purl.namespace.lower() != purl.namespace
4974
):
50-
yield f"Namespace is not lowercased for purl type: {cls.type!r}"
75+
yield ValidationMessage(
76+
severity=ValidationSeverity.WARNING,
77+
message=f"Namespace is not lowercased for purl type: {cls.type!r}",
78+
)
5179

5280
if not cls.name_case_sensitive and purl.name and purl.name.lower() != purl.name:
53-
yield f"Name is not lowercased for purl type: {cls.type!r}"
81+
yield ValidationMessage(
82+
severity=ValidationSeverity.WARNING,
83+
message=f"Name is not lowercased for purl type: {cls.type!r}",
84+
)
5485

5586
if not cls.version_case_sensitive and purl.version and purl.version.lower() != purl.version:
56-
yield f"Version is not lowercased for purl type: {cls.type!r}"
87+
yield ValidationMessage(
88+
severity=ValidationSeverity.WARNING,
89+
message=f"Version is not lowercased for purl type: {cls.type!r}",
90+
)
5791

5892
messages = cls.validate_type(purl, strict=strict)
5993
if messages:
@@ -87,7 +121,8 @@ def normalize(cls, purl):
87121

88122
@classmethod
89123
def validate_type(cls, purl, strict=False):
90-
return
124+
if strict:
125+
yield from cls.validate_qualifiers(purl)
91126

92127
@classmethod
93128
def validate_qualifiers(cls, purl):
@@ -100,9 +135,12 @@ def validate_qualifiers(cls, purl):
100135
disallowed = purl_qualifiers_keys - allowed_qualifiers_set
101136

102137
if disallowed:
103-
yield (
104-
f"Invalid qualifiers found: {', '.join(sorted(disallowed))}. "
105-
f"Allowed qualifiers are: {', '.join(sorted(allowed_qualifiers_set))}"
138+
yield ValidationMessage(
139+
severity=ValidationSeverity.INFO,
140+
message=(
141+
f"Invalid qualifiers found: {', '.join(sorted(disallowed))}. "
142+
f"Allowed qualifiers are: {', '.join(sorted(allowed_qualifiers_set))}"
143+
),
106144
)
107145

108146

@@ -248,9 +286,15 @@ class CpanTypeValidator(TypeValidator):
248286
@classmethod
249287
def validate_type(cls, purl, strict=False):
250288
if purl.namespace and "::" in purl.name:
251-
yield f"Name must not contain '::' when Namespace is absent for purl type: {cls.type!r}"
289+
yield ValidationMessage(
290+
severity=ValidationSeverity.ERROR,
291+
message=f"Name must not contain '::' when Namespace is present for purl type: {cls.type!r}",
292+
)
252293
if not purl.namespace and "-" in purl.name:
253-
yield f"Name must not contain '-' when Namespace is absent for purl type: {cls.type!r}"
294+
yield ValidationMessage(
295+
severity=ValidationSeverity.ERROR,
296+
message=f"Name must not contain '-' when Namespace is absent for purl type: {cls.type!r}",
297+
)
254298
messages = super().validate_type(purl, strict)
255299
if messages:
256300
yield from messages
@@ -370,7 +414,10 @@ class HackageTypeValidator(TypeValidator):
370414
@classmethod
371415
def validate_type(cls, purl, strict=False):
372416
if "_" in purl.name:
373-
yield f"Name contains underscores but should be kebab-case for purl type: {cls.type!r}"
417+
yield ValidationMessage(
418+
severity=ValidationSeverity.WARNING,
419+
message=f"Name cannot contain underscores for purl type:{cls.type!r}",
420+
)
374421
messages = super().validate_type(purl, strict)
375422
if messages:
376423
yield from messages
@@ -503,10 +550,17 @@ class PubTypeValidator(TypeValidator):
503550

504551
@classmethod
505552
def validate_type(cls, purl, strict=False):
506-
if any(not (c.islower() or c.isdigit() or c == "_") for c in purl.name):
507-
yield f"Name contains invalid characters but should only contain lowercase letters, digits, or underscores for purl type: {cls.type!r}"
553+
if not all(c.isalnum() or c == "_" for c in purl.name):
554+
yield ValidationMessage(
555+
severity=ValidationSeverity.ERROR,
556+
message=f"Name contains invalid characters but should only contain letters, digits, or underscores for purl type: {cls.type!r}",
557+
)
558+
508559
if " " in purl.name:
509-
yield f"Name contains spaces but should use underscores instead for purl type: {cls.type!r}"
560+
yield ValidationMessage(
561+
severity=ValidationSeverity.ERROR,
562+
message=f"Name contains spaces but should use underscores instead for purl type: {cls.type!r}",
563+
)
510564
messages = super().validate_type(purl, strict)
511565
if messages:
512566
yield from messages
@@ -528,7 +582,10 @@ class PypiTypeValidator(TypeValidator):
528582
@classmethod
529583
def validate_type(cls, purl, strict=False):
530584
if "_" in purl.name:
531-
yield f"Name cannot contain `_` for purl type:{cls.type!r}"
585+
yield ValidationMessage(
586+
severity=ValidationSeverity.WARNING,
587+
message=f"Name cannot contain underscores for purl type:{cls.type!r}",
588+
)
532589
messages = super().validate_type(purl, strict)
533590
if messages:
534591
yield from messages

tests/test_purl_spec.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,18 @@ def run_test_case(case, test_type, desc):
160160
strict = True
161161
if test_group == "advanced":
162162
strict = False
163-
purl = PackageURL(
164-
type=input_data["type"],
165-
namespace=input_data["namespace"],
166-
name=input_data["name"],
167-
version=input_data["version"],
168-
qualifiers=input_data.get("qualifiers"),
169-
subpath=input_data.get("subpath"),
170-
normalize_purl=not strict,
171-
)
163+
purl = PackageURL.from_string(input_data, normalize_purl=False)
172164
messages = purl.validate(strict=strict)
173-
if case.get("expected_messages"):
174-
assert messages == case["expected_messages"]
165+
messages = list(change_messages_to_json(messages))
166+
if case.get("expected_output"):
167+
assert messages == case["expected_output"]
175168
else:
176169
assert not messages
170+
171+
172+
def change_messages_to_json(messages):
173+
for message in messages:
174+
yield {
175+
"severity": message.severity.value,
176+
"message": message.message,
177+
}

0 commit comments

Comments
 (0)