2626
2727import string
2828from collections import namedtuple
29+ from collections .abc import Mapping
2930from typing import TYPE_CHECKING
3031from typing import Any
3132from typing import Union
3738if TYPE_CHECKING :
3839 from collections .abc import Callable
3940 from collections .abc import Iterable
41+ from typing import ClassVar
4042
4143 from typing_extensions import Literal
4244 from typing_extensions import Self
@@ -230,9 +232,12 @@ def normalize_qualifiers(
230232
231233 if not encode :
232234 return qualifiers_map
235+ return _qualifier_map_to_string (qualifiers_map ) or None
233236
234- qualifiers_list = [f"{ key } ={ value } " for key , value in qualifiers_map .items ()]
235- return "&" .join (qualifiers_list ) or None
237+
238+ def _qualifier_map_to_string (qualifiers : dict [str , str ]) -> str :
239+ qualifiers_list = [f"{ key } ={ value } " for key , value in qualifiers .items ()]
240+ return "&" .join (qualifiers_list )
236241
237242
238243def normalize_subpath (subpath : AnyStr | None , encode : bool | None = True ) -> str | None :
@@ -319,6 +324,8 @@ class PackageURL(
319324 https://github.com/package-url/purl-spec
320325 """
321326
327+ SCHEME : ClassVar [str ] = "pkg"
328+
322329 type : str
323330 namespace : str | None
324331 name : str
@@ -400,7 +407,7 @@ def to_dict(self, encode: bool | None = False, empty: Any = None) -> dict[str, A
400407
401408 return data
402409
403- def to_string (self ) -> str :
410+ def to_string (self , encode : bool | None = True ) -> str :
404411 """
405412 Return a purl string built from components.
406413 """
@@ -411,10 +418,10 @@ def to_string(self) -> str:
411418 self .version ,
412419 self .qualifiers ,
413420 self .subpath ,
414- encode = True ,
421+ encode = encode ,
415422 )
416423
417- purl = ["pkg :" , type , "/" ]
424+ purl = [self . SCHEME , " :" , type , "/" ]
418425
419426 if namespace :
420427 purl .extend ((namespace , "/" ))
@@ -427,6 +434,8 @@ def to_string(self) -> str:
427434
428435 if qualifiers :
429436 purl .append ("?" )
437+ if isinstance (qualifiers , Mapping ):
438+ qualifiers = _qualifier_map_to_string (qualifiers )
430439 purl .append (qualifiers )
431440
432441 if subpath :
@@ -445,8 +454,10 @@ def from_string(cls, purl: str) -> Self:
445454 raise ValueError ("A purl string argument is required." )
446455
447456 scheme , sep , remainder = purl .partition (":" )
448- if not sep or scheme != "pkg" :
449- raise ValueError (f'purl is missing the required "pkg" scheme component: { purl !r} .' )
457+ if not sep or scheme != cls .SCHEME :
458+ raise ValueError (
459+ f'purl is missing the required "{ cls .SCHEME } " scheme component: { purl !r} .'
460+ )
450461
451462 # this strip '/, // and /// as possible in :// or :///
452463 remainder = remainder .strip ().lstrip ("/" )
0 commit comments