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

Replace typeguard with beartype #685

Merged
merged 5 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ if not validation_messages:
* rdflib: https://pypi.python.org/pypi/rdflib/ for handling RDF.
* ply: https://pypi.org/project/ply/ for handling tag-value.
* click: https://pypi.org/project/click/ for creating the CLI interface.
* typeguard: https://pypi.org/project/typeguard/ for type checking.
* beartype: https://pypi.org/project/beartype/ for type checking.
* uritools: https://pypi.org/project/uritools/ for validation of URIs.
* license-expression: https://pypi.org/project/license-expression/ for handling SPDX license expressions.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
urls = { Homepage = "https://github.com/spdx/tools-python" }
requires-python = ">=3.7"
dependencies = ["click", "pyyaml", "xmltodict", "rdflib", "typeguard==4.0.0", "uritools", "license_expression", "ply"]
dependencies = ["click", "pyyaml", "xmltodict", "rdflib", "beartype", "uritools", "license_expression", "ply"]
dynamic = ["version"]

[project.optional-dependencies]
Expand Down
47 changes: 42 additions & 5 deletions src/spdx_tools/common/typing/dataclass_with_properties.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
# SPDX-FileCopyrightText: 2022 spdx contributors
#
# SPDX-License-Identifier: Apache-2.0
fholger marked this conversation as resolved.
Show resolved Hide resolved
from dataclasses import dataclass

from beartype import beartype
from beartype.roar import BeartypeCallHintException


def dataclass_with_properties(cls):
# placeholder decorator until we figure out how to do run-time type checking more performant
return dataclass(cls)
"""Decorator to generate a dataclass with properties out of the class' value:type list.
Their getters and setters will be subjected to the @typechecked decorator to ensure type conformity."""
data_cls = dataclass(cls)
for field_name, field_type in data_cls.__annotations__.items():
set_field = make_setter(field_name, field_type)
get_field = make_getter(field_name, field_type)

setattr(data_cls, field_name, property(get_field, set_field))

return data_cls


def make_setter(field_name, field_type):
"""helper method to avoid late binding when generating functions in a for loop"""

@beartype
def set_field(self, value: field_type):
setattr(self, f"_{field_name}", value)

def set_field_with_error_conversion(self, value: field_type):
try:
set_field(self, value)
except BeartypeCallHintException as err:
error_message: str = f"SetterError {self.__class__.__name__}: {err}"

# As setters are created dynamically, their argument name is always "value". We replace it by the
# actual name so the error message is more helpful.
raise TypeError(error_message.replace("value", field_name, 1) + f": {value}")

return set_field_with_error_conversion


def make_getter(field_name, field_type):
"""helper method to avoid late binding when generating functions in a for loop"""

def get_field(self) -> field_type:
return getattr(self, f"_{field_name}")

return get_field
2 changes: 1 addition & 1 deletion src/spdx_tools/common/typing/type_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def check_types_and_set_values(instance_under_construction: Any, local_variables
"""
Helper method to accumulate all type errors encountered during a constructor call and return them in a
ConstructorTypeErrors instance.
Background: Our setters are enhanced with runtime typechecks using typeguard. However, this means that by
Background: Our setters are enhanced with runtime typechecks using beartype. However, this means that by
default, a TypeError is raised on the first type violation that is encountered. We consider it more helpful to
return all type violations in one go.
As an aside, defining constructors "manually" using this utility method helps avoid a nasty PyCharm bug:
Expand Down
4 changes: 2 additions & 2 deletions tests/spdx/jsonschema/test_converter.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# SPDX-FileCopyrightText: 2022 spdx contributors
#
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from enum import auto
from typing import Any, Type

import pytest

from spdx_tools.common.typing.dataclass_with_properties import dataclass_with_properties
from spdx_tools.common.typing.type_checks import check_types_and_set_values
from spdx_tools.spdx.jsonschema.converter import TypedConverter
from spdx_tools.spdx.jsonschema.json_property import JsonProperty
Expand All @@ -18,7 +18,7 @@ class TestPropertyType(JsonProperty):
SECOND_NAME = auto()


@dataclass
@dataclass_with_properties
class TestDataModelType:
first_property: str
second_property: int
Expand Down
23 changes: 22 additions & 1 deletion tests/spdx/model/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,33 @@ def test_correct_initialization():


def test_correct_initialization_with_optional_as_none():
actor = Actor(ActorType.TOOL, "tool_name")
actor = Actor(ActorType.TOOL, "tool_name", None)
assert actor.actor_type == ActorType.TOOL
assert actor.name == "tool_name"
assert actor.email is None


def test_wrong_type_in_actor_type():
with pytest.raises(TypeError):
Actor("PERSON", "name")


def test_wrong_type_in_name():
with pytest.raises(TypeError):
Actor(ActorType.PERSON, 42)


def test_wrong_type_in_email():
with pytest.raises(TypeError):
Actor(ActorType.PERSON, "name", [])


def test_wrong_type_in_email_after_initializing():
with pytest.raises(TypeError):
actor = Actor(ActorType.PERSON, "name")
actor.email = []


@pytest.mark.parametrize(
"actor,expected_string",
[
Expand Down
32 changes: 32 additions & 0 deletions tests/spdx/model/test_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from datetime import datetime
from unittest import mock

import pytest

from spdx_tools.spdx.model import Annotation, AnnotationType


Expand All @@ -16,3 +18,33 @@ def test_correct_initialization(actor):
assert annotation.annotator == actor
assert annotation.annotation_date == datetime(2022, 1, 1)
assert annotation.annotation_comment == "comment"


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_spdx_id(actor):
with pytest.raises(TypeError):
Annotation(42, AnnotationType.OTHER, actor, datetime(2022, 1, 1), "comment")


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_annotation_type(actor):
with pytest.raises(TypeError):
Annotation("id", 42, actor, datetime(2022, 1, 1), "comment")


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_annotator(actor):
with pytest.raises(TypeError):
Annotation("id", AnnotationType.OTHER, 42, datetime(2022, 1, 1), "comment")


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_annotation_date(actor):
with pytest.raises(TypeError):
Annotation("id", AnnotationType.OTHER, actor, 42, "comment")


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_annotation_comment(actor):
with pytest.raises(TypeError):
Annotation("id", AnnotationType.OTHER, actor, datetime(2022, 1, 1), 42)
12 changes: 12 additions & 0 deletions tests/spdx/model/test_checksum.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@
#
# SPDX-License-Identifier: Apache-2.0

import pytest

from spdx_tools.spdx.model import Checksum, ChecksumAlgorithm


def test_correct_initialization():
checksum = Checksum(ChecksumAlgorithm.BLAKE2B_256, "value")
assert checksum.algorithm == ChecksumAlgorithm.BLAKE2B_256
assert checksum.value == "value"


def test_wrong_type_in_algorithm():
with pytest.raises(TypeError):
Checksum(42, "value")


def test_wrong_type_in_value():
with pytest.raises(TypeError):
Checksum(ChecksumAlgorithm.BLAKE2B_256, 42)
75 changes: 75 additions & 0 deletions tests/spdx/model/test_creation_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from datetime import datetime
from unittest import mock

import pytest

from spdx_tools.spdx.model import CreationInfo, Version


Expand Down Expand Up @@ -35,3 +37,76 @@ def test_correct_initialization(actor, ext_ref):
assert creation_info.external_document_refs == [ext_ref, ext_ref]
assert creation_info.license_list_version == Version(6, 3)
assert creation_info.document_comment == "doc_comment"


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_spdx_version(actor):
with pytest.raises(TypeError):
CreationInfo(42, "id", "name", "namespace", [actor, actor], datetime(2022, 1, 1))


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_spdx_id(actor):
with pytest.raises(TypeError):
CreationInfo("version", 42, "name", "namespace", [actor, actor], datetime(2022, 1, 1))


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_name(actor):
with pytest.raises(TypeError):
CreationInfo("version", "id", 42, "namespace", [actor, actor], datetime(2022, 1, 1))


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_document_namespace(actor):
with pytest.raises(TypeError):
CreationInfo("version", "id", "name", 42, [actor, actor], datetime(2022, 1, 1))


def test_wrong_type_in_creators():
with pytest.raises(TypeError):
CreationInfo("version", "id", "name", "namespace", ["person"], datetime(2022, 1, 1))


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_created(actor):
with pytest.raises(TypeError):
CreationInfo("version", "id", "name", "namespace", [actor, actor], "2022-01-01")


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_creator_comment(actor):
with pytest.raises(TypeError):
CreationInfo(
"version", "id", "name", "namespace", [actor, actor], datetime(2022, 1, 1), creator_comment=["string"]
)


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_data_license(actor):
with pytest.raises(TypeError):
CreationInfo("version", "id", "name", "namespace", [actor, actor], datetime(2022, 1, 1), data_license=42)


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_external_document_refs(actor):
with pytest.raises(TypeError):
CreationInfo(
"version", "id", "name", "namespace", [actor, actor], datetime(2022, 1, 1), external_document_refs=()
)


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_license_list_version(actor):
with pytest.raises(TypeError):
CreationInfo(
"version", "id", "name", "namespace", [actor, actor], datetime(2022, 1, 1), license_list_version="6.4"
)


@mock.patch("spdx_tools.spdx.model.Actor", autospec=True)
def test_wrong_type_in_document_comment(actor):
with pytest.raises(TypeError):
CreationInfo(
"version", "id", "name", "namespace", [actor, actor], datetime(2022, 1, 1), document_comment=["1"]
)
43 changes: 43 additions & 0 deletions tests/spdx/model/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from unittest import mock

import pytest

from spdx_tools.spdx.model import Document


Expand Down Expand Up @@ -43,3 +45,44 @@ def test_correct_initialization_with_default_values(creation_info):
assert document.annotations == []
assert document.relationships == []
assert document.extracted_licensing_info == []


def test_wrong_type_in_creation_info():
with pytest.raises(TypeError):
Document("string")


@mock.patch("spdx_tools.spdx.model.CreationInfo", autospec=True)
def test_wrong_type_in_packages(creation_info):
with pytest.raises(TypeError):
Document(creation_info, packages=["string"])


@mock.patch("spdx_tools.spdx.model.CreationInfo", autospec=True)
def test_wrong_type_in_files(creation_info):
with pytest.raises(TypeError):
Document(creation_info, files={})


@mock.patch("spdx_tools.spdx.model.CreationInfo", autospec=True)
def test_wrong_type_in_snippets(creation_info):
with pytest.raises(TypeError):
Document(creation_info, snippets=())


@mock.patch("spdx_tools.spdx.model.CreationInfo", autospec=True)
def test_wrong_type_in_annotations(creation_info):
with pytest.raises(TypeError):
Document(creation_info, annotations=["string"])


@mock.patch("spdx_tools.spdx.model.CreationInfo", autospec=True)
def test_wrong_type_in_relationships(creation_info):
with pytest.raises(TypeError):
Document(creation_info, relationships="string")


@mock.patch("spdx_tools.spdx.model.CreationInfo", autospec=True)
def test_wrong_type_in_extracted_licensing_info(creation_info):
with pytest.raises(TypeError):
Document(creation_info, extracted_licensing_info=42)
19 changes: 19 additions & 0 deletions tests/spdx/model/test_external_document_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from unittest import mock

import pytest

from spdx_tools.spdx.model import ExternalDocumentRef


Expand All @@ -13,3 +15,20 @@ def test_correct_initialization(checksum):
assert external_document_ref.document_ref_id == "id"
assert external_document_ref.document_uri == "uri"
assert external_document_ref.checksum == checksum


@mock.patch("spdx_tools.spdx.model.Checksum", autospec=True)
def test_wrong_type_in_spdx_id(checksum):
with pytest.raises(TypeError):
ExternalDocumentRef(42, "uri", checksum)


@mock.patch("spdx_tools.spdx.model.Checksum", autospec=True)
def test_wrong_type_in_document_uri(checksum):
with pytest.raises(TypeError):
ExternalDocumentRef("id", 42, checksum)


def test_wrong_type_in_checksum():
with pytest.raises(TypeError):
ExternalDocumentRef("id", "uri", 42)
Loading