Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: qualifiers type annotation #172

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 51 additions & 56 deletions src/packageurl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
# Visit https://github.com/package-url/packageurl-python for support and
# download.

from __future__ import annotations

import string
from collections import namedtuple
from typing import TYPE_CHECKING
Expand All @@ -43,12 +45,13 @@
from collections.abc import Iterable

from typing_extensions import Literal
from typing_extensions import Self

# Python 3
basestring = (
bytes,
str,
) # NOQA
)

"""
A purl (aka. Package URL) implementation as specified at:
Expand Down Expand Up @@ -84,16 +87,16 @@ def unquote(s: AnyStr) -> str:


@overload
def get_quoter(encode: bool = True) -> "Callable[[AnyStr], str]": ...
def get_quoter(encode: bool = True) -> Callable[[AnyStr], str]: ...


@overload
def get_quoter(encode: None) -> "Callable[[str], str]": ...
def get_quoter(encode: None) -> Callable[[str], str]: ...


def get_quoter(
encode: Optional[bool] = True,
) -> "Union[Callable[[AnyStr], str], Callable[[str], str]]":
) -> Union[Callable[[AnyStr], str], Callable[[str], str]]:
"""
Return quoting callable given an `encode` tri-boolean (True, False or None)
"""
Expand All @@ -105,22 +108,22 @@ def get_quoter(
return lambda x: x


def normalize_type(type: Optional[AnyStr], encode: Optional[bool] = True) -> Optional[str]: # NOQA
def normalize_type(type: Optional[AnyStr], encode: Optional[bool] = True) -> Optional[str]:
if not type:
return None
if not isinstance(type, str):
type_str = type.decode("utf-8") # NOQA
type_str = type.decode("utf-8")
else:
type_str = type

quoter = get_quoter(encode)
type_str = quoter(type_str) # NOQA
type_str = quoter(type_str)
return type_str.strip().lower() or None


def normalize_namespace(
namespace: Optional[AnyStr], ptype: Optional[str], encode: Optional[bool] = True
) -> Optional[str]: # NOQA
) -> Optional[str]:
if not namespace:
return None
if not isinstance(namespace, str):
Expand All @@ -138,7 +141,7 @@ def normalize_namespace(

def normalize_name(
name: Optional[AnyStr], ptype: Optional[str], encode: Optional[bool] = True
) -> Optional[str]: # NOQA
) -> Optional[str]:
if not name:
return None
if not isinstance(name, str):
Expand All @@ -156,9 +159,7 @@ def normalize_name(
return name_str or None


def normalize_version(
version: Optional[AnyStr], encode: Optional[bool] = True
) -> Optional[str]: # NOQA
def normalize_version(version: Optional[AnyStr], encode: Optional[bool] = True) -> Optional[str]:
if not version:
return None
if not isinstance(version, str):
Expand All @@ -173,25 +174,25 @@ def normalize_version(

@overload
def normalize_qualifiers(
qualifiers: Union[AnyStr, Dict[str, str], None], encode: "Literal[True]" = ...
qualifiers: Optional[Union[AnyStr, Dict[str, str]]], encode: Literal[True] = ...
) -> Optional[str]: ...


@overload
def normalize_qualifiers(
qualifiers: Union[AnyStr, Dict[str, str], None], encode: "Optional[Literal[False]]"
) -> Optional[Dict[str, str]]: ...
qualifiers: Optional[Union[AnyStr, Dict[str, str]]], encode: Optional[Literal[False]]
) -> Dict[str, str]: ...


@overload
def normalize_qualifiers(
qualifiers: Union[AnyStr, Dict[str, str], None], encode: Optional[bool] = ...
) -> Union[str, Dict[str, str], None]: ...
qualifiers: Optional[Union[AnyStr, Dict[str, str]]], encode: Optional[bool] = ...
) -> Optional[Union[str, Dict[str, str]]]: ...


def normalize_qualifiers(
qualifiers: Union[AnyStr, Dict[str, str], None], encode: Optional[bool] = True
) -> Union[str, Dict[str, str], None]: # NOQA
qualifiers: Optional[Union[AnyStr, Dict[str, str]]], encode: Optional[bool] = True
) -> Optional[Union[str, Dict[str, str]]]:
"""
Return normalized `qualifiers` as a mapping (or as a string if `encode` is
True). The `qualifiers` arg is either a mapping or a string.
Expand All @@ -213,7 +214,7 @@ def normalize_qualifiers(
f"Invalid qualifier. Must be a string of key=value pairs:{repr(qualifiers_list)}"
)
qualifiers_parts = [kv.partition("=") for kv in qualifiers_list]
qualifiers_pairs: "Iterable[Tuple[str, str]]" = [(k, v) for k, _, v in qualifiers_parts]
qualifiers_pairs: Iterable[Tuple[str, str]] = [(k, v) for k, _, v in qualifiers_parts]
elif isinstance(qualifiers, dict):
qualifiers_pairs = qualifiers.items()
else:
Expand Down Expand Up @@ -255,9 +256,7 @@ def normalize_qualifiers(
return qualifiers_map


def normalize_subpath(
subpath: Optional[AnyStr], encode: Optional[bool] = True
) -> Optional[str]: # NOQA
def normalize_subpath(subpath: Optional[AnyStr], encode: Optional[bool] = True) -> Optional[str]:
if not subpath:
return None
if not isinstance(subpath, str):
Expand All @@ -278,9 +277,9 @@ def normalize(
namespace: Optional[AnyStr],
name: Optional[AnyStr],
version: Optional[AnyStr],
qualifiers: Union[AnyStr, Dict[str, str], None],
qualifiers: Optional[Union[AnyStr, Dict[str, str]]],
subpath: Optional[AnyStr],
encode: "Literal[True]" = ...,
encode: Literal[True] = ...,
) -> Tuple[str, Optional[str], str, Optional[str], Optional[str], Optional[str]]: ...


Expand All @@ -290,10 +289,10 @@ def normalize(
namespace: Optional[AnyStr],
name: Optional[AnyStr],
version: Optional[AnyStr],
qualifiers: Union[AnyStr, Dict[str, str], None],
qualifiers: Optional[Union[AnyStr, Dict[str, str]]],
subpath: Optional[AnyStr],
encode: "Optional[Literal[False]]",
) -> Tuple[str, Optional[str], str, Optional[str], Optional[Dict[str, str]], Optional[str]]: ...
encode: Optional[Literal[False]],
) -> Tuple[str, Optional[str], str, Optional[str], Dict[str, str], Optional[str]]: ...


@overload
Expand All @@ -302,11 +301,11 @@ def normalize(
namespace: Optional[AnyStr],
name: Optional[AnyStr],
version: Optional[AnyStr],
qualifiers: Union[AnyStr, Dict[str, str], None],
qualifiers: Optional[Union[AnyStr, Dict[str, str]]],
subpath: Optional[AnyStr],
encode: Optional[bool] = ...,
) -> Tuple[
str, Optional[str], str, Optional[str], Union[str, Dict[str, str], None], Optional[str]
str, Optional[str], str, Optional[str], Optional[Union[str, Dict[str, str]]], Optional[str]
]: ...


Expand All @@ -315,21 +314,21 @@ def normalize(
namespace: Optional[AnyStr],
name: Optional[AnyStr],
version: Optional[AnyStr],
qualifiers: Union[AnyStr, Dict[str, str], None],
qualifiers: Optional[Union[AnyStr, Dict[str, str]]],
subpath: Optional[AnyStr],
encode: Optional[bool] = True,
) -> Tuple[
Optional[str],
Optional[str],
Optional[str],
Optional[str],
Union[str, Dict[str, str], None],
Optional[Union[str, Dict[str, str]]],
Optional[str],
]: # NOQA
]:
"""
Return normalized purl components
"""
type_norm = normalize_type(type, encode) # NOQA
type_norm = normalize_type(type, encode)
namespace_norm = normalize_namespace(namespace, type_norm, encode)
name_norm = normalize_name(name, type_norm, encode)
version_norm = normalize_version(version, encode)
Expand All @@ -346,27 +345,25 @@ class PackageURL(
https://github.com/package-url/purl-spec
"""

name: str
namespace: Optional[str]
qualifiers: Union[str, Dict[str, str], None]
subpath: Optional[str]
type: str
namespace: Optional[str]
name: str
version: Optional[str]
qualifiers: Dict[str, str]
subpath: Optional[str]

def __new__(
self,
cls,
type: Optional[AnyStr] = None,
namespace: Optional[AnyStr] = None,
name: Optional[AnyStr] = None, # NOQA
name: Optional[AnyStr] = None,
version: Optional[AnyStr] = None,
qualifiers: Union[AnyStr, Dict[str, str], None] = None,
qualifiers: Optional[Union[AnyStr, Dict[str, str]]] = None,
subpath: Optional[AnyStr] = None,
) -> "PackageURL": # this should be 'Self' https://github.com/python/mypy/pull/13133
required = dict(type=type, name=name)
for key, value in required.items():
if value:
continue
raise ValueError(f"Invalid purl: {key} is a required argument.")
) -> Self:
for arg in type, name:
if not arg:
raise ValueError(f"Invalid purl: {arg} is a required argument.")

strings = dict(
type=type,
Expand Down Expand Up @@ -399,12 +396,10 @@ def __new__(
version_norm,
qualifiers_norm,
subpath_norm,
) = normalize( # NOQA
type, namespace, name, version, qualifiers, subpath, encode=None
)
) = normalize(type, namespace, name, version, qualifiers, subpath, encode=None)

return super().__new__(
PackageURL,
cls,
type=type_norm,
namespace=namespace_norm,
name=name_norm,
Expand Down Expand Up @@ -439,7 +434,7 @@ def to_string(self) -> str:
"""
Return a purl string built from components.
"""
type, namespace, name, version, qualifiers, subpath = normalize( # NOQA
type, namespace, name, version, qualifiers, subpath = normalize(
self.type,
self.namespace,
self.name,
Expand Down Expand Up @@ -472,7 +467,7 @@ def to_string(self) -> str:
return "".join(purl)

@classmethod
def from_string(cls, purl: str) -> "PackageURL":
def from_string(cls, purl: str) -> Self:
"""
Return a PackageURL object parsed from a string.
Raise ValueError on errors.
Expand All @@ -490,7 +485,7 @@ def from_string(cls, purl: str) -> "PackageURL":
version: Optional[str] # this line is just for type hinting
subpath: Optional[str] # this line is just for type hinting

type, sep, remainder = remainder.partition("/") # NOQA
type, sep, remainder = remainder.partition("/")
if not type or not sep:
raise ValueError(f"purl is missing the required type component: {repr(purl)}.")

Expand Down Expand Up @@ -536,7 +531,7 @@ def from_string(cls, purl: str) -> "PackageURL":
if not name:
raise ValueError(f"purl is missing the required name component: {repr(purl)}")

type, namespace, name, version, qualifiers, subpath = normalize( # NOQA
type, namespace, name, version, qualifiers, subpath = normalize(
type,
namespace,
name,
Expand All @@ -546,4 +541,4 @@ def from_string(cls, purl: str) -> "PackageURL":
encode=False,
)

return PackageURL(type, namespace, name, version, qualifiers, subpath)
return cls(type, namespace, name, version, qualifiers, subpath) # type: ignore[arg-type]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can easily fix this, then I don't need to create a separate PR 😅

Just create your own AnyStr type alias and remove the one coming from the typing module.

Add

AnyStr = Union[str, bytes]

directly under basestring at the top and remove the from typing import AnyStr. AnyStr was deprecated in Python 3.13 anyway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! One small alteration: since it's only used for type hints, I put it into the TYPE_CHECKING block. Tests still pass locally. Are you good with that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, also sounds good