From b0b6f0701cbe1cc9f24c1e6c4a42a7c3157289cb Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 28 Nov 2022 11:01:34 +0530 Subject: [PATCH 1/5] feat: xblock skill verification event Adds data class and event to send skill verification data for an XBlock. feat: add support for array avro types chore: add changelog docs: update signals docs to be more generic test: fix code coverage refactor: add docstrings --- CHANGELOG.rst | 8 ++++ openedx_events/event_bus/avro/deserializer.py | 10 +++++ openedx_events/event_bus/avro/schema.py | 15 ++++++-- openedx_events/event_bus/avro/serializer.py | 3 +- .../event_bus/avro/tests/test_avro.py | 2 + .../event_bus/avro/tests/test_deserializer.py | 38 +++++++++++++++++++ .../event_bus/avro/tests/test_schema.py | 24 ++++++++++++ openedx_events/learning/data.py | 18 +++++++++ openedx_events/learning/signals.py | 13 +++++++ 9 files changed, 127 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed5d3523..78cfe145 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,14 @@ Changed ~~~~~~~ * Use collections.abc import to use with python 3.8 and 3.10. +[4.2.0] - 2023-01-04 +--------------------- +Added +~~~~~~~ +* Added support for array type. +* Added new XBLOCK_SKILL_VERIFIED event. +* Added XBlockSkillVerificationData classes. + [4.1.0] - 2023-01-03 --------------------- Added diff --git a/openedx_events/event_bus/avro/deserializer.py b/openedx_events/event_bus/avro/deserializer.py index 8e820da5..927f5f78 100644 --- a/openedx_events/event_bus/avro/deserializer.py +++ b/openedx_events/event_bus/avro/deserializer.py @@ -2,6 +2,7 @@ Deserialize Avro record dictionaries to events that can be sent with OpenEdxPublicSignals. """ import json +from typing import get_args, get_origin import attr @@ -29,11 +30,20 @@ def _deserialized_avro_record_dict_to_object(data: dict, data_type, deserializer """ param_deserializers = deserializers or {} all_deserializers = {**DEFAULT_DESERIALIZERS, **param_deserializers} + data_type_origin = get_origin(data_type) if deserializer := all_deserializers.get(data_type, None): return deserializer(data) elif data_type in PYTHON_TYPE_TO_AVRO_MAPPING: return data + elif PYTHON_TYPE_TO_AVRO_MAPPING.get(data_type_origin) == "array": + arg_data_type = get_args(data_type) + if not arg_data_type: + raise TypeError( + "List without annotation type is not supported. The argument should be a type, for eg., List[int]" + ) + if arg_data_type[0] in PYTHON_TYPE_TO_AVRO_MAPPING: + return data elif hasattr(data_type, "__attrs_attrs__"): transformed = {} for attribute in data_type.__attrs_attrs__: diff --git a/openedx_events/event_bus/avro/schema.py b/openedx_events/event_bus/avro/schema.py index 552c42fc..a80058e8 100644 --- a/openedx_events/event_bus/avro/schema.py +++ b/openedx_events/event_bus/avro/schema.py @@ -5,6 +5,8 @@ """ +from typing import get_args, get_origin + from .custom_serializers import DEFAULT_CUSTOM_SERIALIZERS from .types import PYTHON_TYPE_TO_AVRO_MAPPING @@ -51,6 +53,7 @@ def _create_avro_field_definition(data_key, data_type, previously_seen_types, """ field = {"name": data_key} all_field_type_overrides = custom_type_to_avro_type or {} + data_type_origin = get_origin(data_type) # Case 1: data_type has a predetermined avro field representation if field_type := all_field_type_overrides.get(data_type, None): @@ -58,12 +61,18 @@ def _create_avro_field_definition(data_key, data_type, previously_seen_types, # Case 2: data_type is a simple type that can be converted directly to an Avro type elif data_type in PYTHON_TYPE_TO_AVRO_MAPPING: if PYTHON_TYPE_TO_AVRO_MAPPING[data_type] in ["record", "array"]: - # Can implement if needed, but for now it doesn't seem to be necessary. # pylint: disable-next=broad-exception-raised - raise Exception("Unable to generate Avro schema for dict or array fields") + raise Exception("Unable to generate Avro schema for dict or array fields without annotation types.") avro_type = PYTHON_TYPE_TO_AVRO_MAPPING[data_type] field["type"] = avro_type - + elif PYTHON_TYPE_TO_AVRO_MAPPING.get(data_type_origin) == "array": + arg_data_type = get_args(data_type) + if not arg_data_type: + raise TypeError( + "List without annotation type is not supported. The argument should be a type, for eg., List[int]" + ) + avro_type = PYTHON_TYPE_TO_AVRO_MAPPING[arg_data_type[0]] + field["type"] = {"type": PYTHON_TYPE_TO_AVRO_MAPPING[data_type_origin], "items": avro_type} # Case 3: data_type is an attrs class elif hasattr(data_type, "__attrs_attrs__"): # Inner Attrs Class diff --git a/openedx_events/event_bus/avro/serializer.py b/openedx_events/event_bus/avro/serializer.py index 041d4d88..3d06d840 100644 --- a/openedx_events/event_bus/avro/serializer.py +++ b/openedx_events/event_bus/avro/serializer.py @@ -43,7 +43,8 @@ def _serialize_non_attrs_values(inst, field, value): # pylint: disable=unused-a for extended_class, serializer in all_serializers.items(): if field: - if issubclass(field.type, extended_class): + # Make sure that field.type is a class first. + if isinstance(field.type, type) and issubclass(field.type, extended_class): return serializer(value) if issubclass(type(value), extended_class): return serializer(value) diff --git a/openedx_events/event_bus/avro/tests/test_avro.py b/openedx_events/event_bus/avro/tests/test_avro.py index 0761f5fd..dc52cef8 100644 --- a/openedx_events/event_bus/avro/tests/test_avro.py +++ b/openedx_events/event_bus/avro/tests/test_avro.py @@ -1,5 +1,6 @@ """Test interplay of the various Avro helper classes""" from datetime import datetime +from typing import List from unittest import TestCase from opaque_keys.edx.keys import CourseKey, UsageKey @@ -51,6 +52,7 @@ def generate_test_event_data_for_data_type(data_type): UsageKey: UsageKey.from_string( "block-v1:edx+DemoX+Demo_course+type@video+block@UaEBjyMjcLW65gaTXggB93WmvoxGAJa0JeHRrDThk", ), + List[int]: [1, 2, 3], datetime: datetime.now(), } for attribute in data_type.__attrs_attrs__: diff --git a/openedx_events/event_bus/avro/tests/test_deserializer.py b/openedx_events/event_bus/avro/tests/test_deserializer.py index 6d6f1118..38ff8737 100644 --- a/openedx_events/event_bus/avro/tests/test_deserializer.py +++ b/openedx_events/event_bus/avro/tests/test_deserializer.py @@ -1,6 +1,7 @@ """Tests for avro.deserializer""" import json from datetime import datetime +from typing import List from unittest import TestCase from opaque_keys.edx.keys import CourseKey, UsageKey @@ -175,3 +176,40 @@ def test_deserialization_of_nested_optional_fields(self): nested_field = data_dict["data"].field_0 self.assertIsInstance(nested_field, SimpleAttrsWithDefaults) self.assertEqual(nested_field, SimpleAttrsWithDefaults()) + + def test_deserialization_of_list_with_annotation(self): + """ + Check that deserialization works as expected when list data is annotated. + """ + LIST_SIGNAL = create_simple_signal({"list_input": List[int]}) + initial_dict = {"list_input": [1, 3]} + deserializer = AvroSignalDeserializer(LIST_SIGNAL) + event_data = deserializer.from_dict(initial_dict) + expected_event_data = [1, 3] + test_data = event_data["list_input"] + self.assertIsInstance(test_data, list) + self.assertEqual(test_data, expected_event_data) + + def test_deserialization_of_list_without_annotation(self): + """ + Check that deserialization raises error when list data is not annotated. + """ + SIGNAL = create_simple_signal({"list_input": List[int]}) + LIST_SIGNAL = create_simple_signal({"list_input": List}) + initial_dict = {"list_input": [1, 3]} + deserializer = AvroSignalDeserializer(SIGNAL) + deserializer.signal = LIST_SIGNAL + with self.assertRaises(TypeError): + deserializer.from_dict(initial_dict) + + def test_deserialization_of_nested_list_fails(self): + """ + Check that deserialization raises error when nested list data is passed. + """ + SIGNAL = create_simple_signal({"list_input": List[int]}) + LIST_SIGNAL = create_simple_signal({"list_input": List[List[int]]}) + initial_dict = {"list_input": [[1, 3], [4, 5]]} + deserializer = AvroSignalDeserializer(SIGNAL) + deserializer.signal = LIST_SIGNAL + with self.assertRaises(TypeError): + deserializer.from_dict(initial_dict) diff --git a/openedx_events/event_bus/avro/tests/test_schema.py b/openedx_events/event_bus/avro/tests/test_schema.py index a2836d4d..5c82ca1b 100644 --- a/openedx_events/event_bus/avro/tests/test_schema.py +++ b/openedx_events/event_bus/avro/tests/test_schema.py @@ -1,6 +1,7 @@ """ Tests for event_bus.avro.schema module """ +from typing import Dict, List from unittest import TestCase from openedx_events.event_bus.avro.schema import schema_from_signal @@ -243,3 +244,26 @@ def test_throw_exception_to_list_or_dict_types(self): with self.assertRaises(Exception): schema_from_signal(DICT_SIGNAL) + + def test_throw_exception_to_list_or_dict_types_without_annotation(self): + LIST_SIGNAL = create_simple_signal({"list_input": List}) + DICT_SIGNAL = create_simple_signal({"list_input": Dict}) + with self.assertRaises(TypeError): + schema_from_signal(LIST_SIGNAL) + + with self.assertRaises(TypeError): + schema_from_signal(DICT_SIGNAL) + + def test_list_with_annotation_works(self): + LIST_SIGNAL = create_simple_signal({"list_input": List[int]}) + expected_dict = { + 'name': 'CloudEvent', + 'type': 'record', + 'doc': 'Avro Event Format for CloudEvents created with openedx_events/schema', + 'fields': [{ + 'name': 'list_input', + 'type': {'type': 'array', 'items': 'long'}, + }], + } + schema = schema_from_signal(LIST_SIGNAL) + self.assertDictEqual(schema, expected_dict) diff --git a/openedx_events/learning/data.py b/openedx_events/learning/data.py index f6a675d6..be833a1b 100644 --- a/openedx_events/learning/data.py +++ b/openedx_events/learning/data.py @@ -217,3 +217,21 @@ class PersistentCourseGradeData: percent_grade = attr.ib(type=float) letter_grade = attr.ib(type=str) passed_timestamp = attr.ib(type=datetime) + + +@attr.s(frozen=True) +class XBlockSkillVerificationData: + """ + Data needed to update verification count of tags/skills for an XBlock. + + User feedback on whether tags/skills related to an XBlock are valid. + + Arguments: + usage_key (UsageKey): identifier of the XBlock object. + verified_skills (List[int]): list of verified skill ids. + ignored_skills (List[int]): list of ignored skill ids. + """ + + usage_key = attr.ib(type=UsageKey) + verified_skills = attr.ib(type=List[int], factory=list) + ignored_skills = attr.ib(type=List[int], factory=list) diff --git a/openedx_events/learning/signals.py b/openedx_events/learning/signals.py index f0d3d185..2062d50e 100644 --- a/openedx_events/learning/signals.py +++ b/openedx_events/learning/signals.py @@ -15,6 +15,7 @@ CourseEnrollmentData, PersistentCourseGradeData, UserData, + XBlockSkillVerificationData, ) from openedx_events.tooling import OpenEdxPublicSignal @@ -148,3 +149,15 @@ "grade": PersistentCourseGradeData, } ) + + +# .. event_type: org.openedx.learning.xblock.skill.verified.v1 +# .. event_name: XBLOCK_SKILL_VERIFIED +# .. event_description: Fired when an XBlock skill is verified. +# .. event_data: XBlockSkillVerificationData +XBLOCK_SKILL_VERIFIED = OpenEdxPublicSignal( + event_type="org.openedx.content_authoring.xblock.skill.verified.v1", + data={ + "xblock_info": XBlockSkillVerificationData, + } +) From 6c6d3f1e3351057cbb94ab98fdb69ebaed363f59 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 7 Feb 2023 17:35:17 +0530 Subject: [PATCH 2/5] refactor: allow simple types in list --- openedx_events/event_bus/avro/deserializer.py | 9 +++++++-- openedx_events/event_bus/avro/schema.py | 9 +++++++-- .../event_bus/avro/tests/test_deserializer.py | 14 ++++++++++++++ openedx_events/event_bus/avro/types.py | 7 +++++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/openedx_events/event_bus/avro/deserializer.py b/openedx_events/event_bus/avro/deserializer.py index 927f5f78..4fbf25ca 100644 --- a/openedx_events/event_bus/avro/deserializer.py +++ b/openedx_events/event_bus/avro/deserializer.py @@ -8,7 +8,7 @@ from .custom_serializers import DEFAULT_CUSTOM_SERIALIZERS from .schema import schema_from_signal -from .types import PYTHON_TYPE_TO_AVRO_MAPPING +from .types import PYTHON_TYPE_TO_AVRO_MAPPING, SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING # Dict of class to deserialize methods (e.g. datetime => DatetimeAvroSerializer.deserialize) DEFAULT_DESERIALIZERS = {serializer.cls: serializer.deserialize for serializer in DEFAULT_CUSTOM_SERIALIZERS} @@ -30,6 +30,8 @@ def _deserialized_avro_record_dict_to_object(data: dict, data_type, deserializer """ param_deserializers = deserializers or {} all_deserializers = {**DEFAULT_DESERIALIZERS, **param_deserializers} + # get generic type of data_type + # if data_type == List[int], data_type_origin = list data_type_origin = get_origin(data_type) if deserializer := all_deserializers.get(data_type, None): @@ -37,12 +39,15 @@ def _deserialized_avro_record_dict_to_object(data: dict, data_type, deserializer elif data_type in PYTHON_TYPE_TO_AVRO_MAPPING: return data elif PYTHON_TYPE_TO_AVRO_MAPPING.get(data_type_origin) == "array": + # returns types of list contents + # if data_type == List[int], arg_data_type = (int,) arg_data_type = get_args(data_type) if not arg_data_type: raise TypeError( "List without annotation type is not supported. The argument should be a type, for eg., List[int]" ) - if arg_data_type[0] in PYTHON_TYPE_TO_AVRO_MAPPING: + # check whether list items type is in basic types. + if arg_data_type[0] in SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING: return data elif hasattr(data_type, "__attrs_attrs__"): transformed = {} diff --git a/openedx_events/event_bus/avro/schema.py b/openedx_events/event_bus/avro/schema.py index a80058e8..12abb984 100644 --- a/openedx_events/event_bus/avro/schema.py +++ b/openedx_events/event_bus/avro/schema.py @@ -8,7 +8,7 @@ from typing import get_args, get_origin from .custom_serializers import DEFAULT_CUSTOM_SERIALIZERS -from .types import PYTHON_TYPE_TO_AVRO_MAPPING +from .types import PYTHON_TYPE_TO_AVRO_MAPPING, SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING DEFAULT_FIELD_TYPES = {serializer.cls: serializer.field_type for serializer in DEFAULT_CUSTOM_SERIALIZERS} @@ -71,7 +71,12 @@ def _create_avro_field_definition(data_key, data_type, previously_seen_types, raise TypeError( "List without annotation type is not supported. The argument should be a type, for eg., List[int]" ) - avro_type = PYTHON_TYPE_TO_AVRO_MAPPING[arg_data_type[0]] + avro_type = SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING.get(arg_data_type[0]) + if avro_type is None: + raise TypeError( + "Only following types are supported for list arguments:" + f" {set(SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING.keys())}" + ) field["type"] = {"type": PYTHON_TYPE_TO_AVRO_MAPPING[data_type_origin], "items": avro_type} # Case 3: data_type is an attrs class elif hasattr(data_type, "__attrs_attrs__"): diff --git a/openedx_events/event_bus/avro/tests/test_deserializer.py b/openedx_events/event_bus/avro/tests/test_deserializer.py index 38ff8737..8c9b5eed 100644 --- a/openedx_events/event_bus/avro/tests/test_deserializer.py +++ b/openedx_events/event_bus/avro/tests/test_deserializer.py @@ -194,6 +194,7 @@ def test_deserialization_of_list_without_annotation(self): """ Check that deserialization raises error when list data is not annotated. """ + # create dummy signal to test deserializer SIGNAL = create_simple_signal({"list_input": List[int]}) LIST_SIGNAL = create_simple_signal({"list_input": List}) initial_dict = {"list_input": [1, 3]} @@ -206,6 +207,7 @@ def test_deserialization_of_nested_list_fails(self): """ Check that deserialization raises error when nested list data is passed. """ + # create dummy signal to test deserializer SIGNAL = create_simple_signal({"list_input": List[int]}) LIST_SIGNAL = create_simple_signal({"list_input": List[List[int]]}) initial_dict = {"list_input": [[1, 3], [4, 5]]} @@ -213,3 +215,15 @@ def test_deserialization_of_nested_list_fails(self): deserializer.signal = LIST_SIGNAL with self.assertRaises(TypeError): deserializer.from_dict(initial_dict) + + def test_deserialization_of_nested_list_with_complex_types_fails(self): + SIGNAL = create_simple_signal({"list_input": List[list]}) + with self.assertRaises(TypeError): + AvroSignalDeserializer(SIGNAL) + initial_dict = {"list_input": [[1, 3], [4, 5]]} + # create dummy signal to test deserializer + DUMMY_SIGNAL = create_simple_signal({"list_input": List[int]}) + deserializer = AvroSignalDeserializer(DUMMY_SIGNAL) + deserializer.signal = SIGNAL + with self.assertRaises(TypeError): + deserializer.from_dict(initial_dict) diff --git a/openedx_events/event_bus/avro/types.py b/openedx_events/event_bus/avro/types.py index d6bb0eeb..f3bc2536 100644 --- a/openedx_events/event_bus/avro/types.py +++ b/openedx_events/event_bus/avro/types.py @@ -1,11 +1,14 @@ """A mapping of python types to the Avro type that we want to use make valid avro schema.""" -PYTHON_TYPE_TO_AVRO_MAPPING = { - None: "null", +SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING = { bool: "boolean", int: "long", float: "double", bytes: "bytes", str: "string", +} +PYTHON_TYPE_TO_AVRO_MAPPING = { + **SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING, + None: "null", dict: "record", list: "array", } From b9b80eba2639a4f2846563ced6df7e0f235b2c00 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 7 Feb 2023 17:39:50 +0530 Subject: [PATCH 3/5] chore: bump version and update changelog --- CHANGELOG.rst | 16 ++++++++-------- openedx_events/__init__.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78cfe145..36eb552a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,14 @@ Change Log Unreleased ---------- +[5.1.0] - 2023-02-07 +--------------------- +Added +~~~~~~~ +* Added support for array type. +* Added new XBLOCK_SKILL_VERIFIED event. +* Added XBlockSkillVerificationData classes. + [5.0.0] - 2023-02-03 -------------------- Changed @@ -45,14 +53,6 @@ Changed ~~~~~~~ * Use collections.abc import to use with python 3.8 and 3.10. -[4.2.0] - 2023-01-04 ---------------------- -Added -~~~~~~~ -* Added support for array type. -* Added new XBLOCK_SKILL_VERIFIED event. -* Added XBlockSkillVerificationData classes. - [4.1.0] - 2023-01-03 --------------------- Added diff --git a/openedx_events/__init__.py b/openedx_events/__init__.py index 70e77836..92487d62 100644 --- a/openedx_events/__init__.py +++ b/openedx_events/__init__.py @@ -5,4 +5,4 @@ more information about the project. """ -__version__ = "5.0.0" +__version__ = "5.1.0" From 6b6b6465a8eff09bfbcf713149db96d2dc79a095 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 17 Feb 2023 12:31:30 +0530 Subject: [PATCH 4/5] fix: remove duplicate test and add comments to test --- openedx_events/event_bus/avro/deserializer.py | 2 +- openedx_events/event_bus/avro/schema.py | 6 +++++- .../event_bus/avro/tests/test_deserializer.py | 12 +++++++++--- openedx_events/event_bus/avro/tests/test_schema.py | 13 ++----------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/openedx_events/event_bus/avro/deserializer.py b/openedx_events/event_bus/avro/deserializer.py index 4fbf25ca..a5ff67f7 100644 --- a/openedx_events/event_bus/avro/deserializer.py +++ b/openedx_events/event_bus/avro/deserializer.py @@ -38,7 +38,7 @@ def _deserialized_avro_record_dict_to_object(data: dict, data_type, deserializer return deserializer(data) elif data_type in PYTHON_TYPE_TO_AVRO_MAPPING: return data - elif PYTHON_TYPE_TO_AVRO_MAPPING.get(data_type_origin) == "array": + elif data_type_origin == list: # returns types of list contents # if data_type == List[int], arg_data_type = (int,) arg_data_type = get_args(data_type) diff --git a/openedx_events/event_bus/avro/schema.py b/openedx_events/event_bus/avro/schema.py index 12abb984..57ec292e 100644 --- a/openedx_events/event_bus/avro/schema.py +++ b/openedx_events/event_bus/avro/schema.py @@ -53,6 +53,8 @@ def _create_avro_field_definition(data_key, data_type, previously_seen_types, """ field = {"name": data_key} all_field_type_overrides = custom_type_to_avro_type or {} + # get generic type of data_type + # if data_type == List[int], data_type_origin = list data_type_origin = get_origin(data_type) # Case 1: data_type has a predetermined avro field representation @@ -65,7 +67,9 @@ def _create_avro_field_definition(data_key, data_type, previously_seen_types, raise Exception("Unable to generate Avro schema for dict or array fields without annotation types.") avro_type = PYTHON_TYPE_TO_AVRO_MAPPING[data_type] field["type"] = avro_type - elif PYTHON_TYPE_TO_AVRO_MAPPING.get(data_type_origin) == "array": + elif data_type_origin == list: + # returns types of list contents + # if data_type == List[int], arg_data_type = (int,) arg_data_type = get_args(data_type) if not arg_data_type: raise TypeError( diff --git a/openedx_events/event_bus/avro/tests/test_deserializer.py b/openedx_events/event_bus/avro/tests/test_deserializer.py index 8c9b5eed..073f3f24 100644 --- a/openedx_events/event_bus/avro/tests/test_deserializer.py +++ b/openedx_events/event_bus/avro/tests/test_deserializer.py @@ -194,11 +194,13 @@ def test_deserialization_of_list_without_annotation(self): """ Check that deserialization raises error when list data is not annotated. """ - # create dummy signal to test deserializer + # create dummy signal to bypass schema check while initializing deserializer + # This allows us to test whether correct exceptions are raised while deserializing data SIGNAL = create_simple_signal({"list_input": List[int]}) LIST_SIGNAL = create_simple_signal({"list_input": List}) initial_dict = {"list_input": [1, 3]} deserializer = AvroSignalDeserializer(SIGNAL) + # Update signal with incomplete type info deserializer.signal = LIST_SIGNAL with self.assertRaises(TypeError): deserializer.from_dict(initial_dict) @@ -207,11 +209,13 @@ def test_deserialization_of_nested_list_fails(self): """ Check that deserialization raises error when nested list data is passed. """ - # create dummy signal to test deserializer + # create dummy signal to bypass schema check while initializing deserializer + # This allows us to test whether correct exceptions are raised while deserializing data SIGNAL = create_simple_signal({"list_input": List[int]}) LIST_SIGNAL = create_simple_signal({"list_input": List[List[int]]}) initial_dict = {"list_input": [[1, 3], [4, 5]]} deserializer = AvroSignalDeserializer(SIGNAL) + # Update signal with incomplete type info deserializer.signal = LIST_SIGNAL with self.assertRaises(TypeError): deserializer.from_dict(initial_dict) @@ -221,9 +225,11 @@ def test_deserialization_of_nested_list_with_complex_types_fails(self): with self.assertRaises(TypeError): AvroSignalDeserializer(SIGNAL) initial_dict = {"list_input": [[1, 3], [4, 5]]} - # create dummy signal to test deserializer + # create dummy signal to bypass schema check while initializing deserializer + # This allows us to test whether correct exceptions are raised while deserializing data DUMMY_SIGNAL = create_simple_signal({"list_input": List[int]}) deserializer = AvroSignalDeserializer(DUMMY_SIGNAL) + # Update signal with incorrect type info deserializer.signal = SIGNAL with self.assertRaises(TypeError): deserializer.from_dict(initial_dict) diff --git a/openedx_events/event_bus/avro/tests/test_schema.py b/openedx_events/event_bus/avro/tests/test_schema.py index 5c82ca1b..b11f1ed4 100644 --- a/openedx_events/event_bus/avro/tests/test_schema.py +++ b/openedx_events/event_bus/avro/tests/test_schema.py @@ -1,7 +1,7 @@ """ Tests for event_bus.avro.schema module """ -from typing import Dict, List +from typing import List from unittest import TestCase from openedx_events.event_bus.avro.schema import schema_from_signal @@ -236,7 +236,7 @@ class UnextendedClass: with self.assertRaises(TypeError): schema_from_signal(SIGNAL) - def test_throw_exception_to_list_or_dict_types(self): + def test_throw_exception_to_list_or_dict_types_without_annotation(self): LIST_SIGNAL = create_simple_signal({"list_input": list}) DICT_SIGNAL = create_simple_signal({"list_input": dict}) with self.assertRaises(Exception): @@ -245,15 +245,6 @@ def test_throw_exception_to_list_or_dict_types(self): with self.assertRaises(Exception): schema_from_signal(DICT_SIGNAL) - def test_throw_exception_to_list_or_dict_types_without_annotation(self): - LIST_SIGNAL = create_simple_signal({"list_input": List}) - DICT_SIGNAL = create_simple_signal({"list_input": Dict}) - with self.assertRaises(TypeError): - schema_from_signal(LIST_SIGNAL) - - with self.assertRaises(TypeError): - schema_from_signal(DICT_SIGNAL) - def test_list_with_annotation_works(self): LIST_SIGNAL = create_simple_signal({"list_input": List[int]}) expected_dict = { From 58b1f184c056d00c7321f3a90d8972675eac7e10 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 17 Feb 2023 12:39:49 +0530 Subject: [PATCH 5/5] fix: code coverage --- openedx_events/event_bus/avro/tests/test_schema.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openedx_events/event_bus/avro/tests/test_schema.py b/openedx_events/event_bus/avro/tests/test_schema.py index b11f1ed4..b35c7ae9 100644 --- a/openedx_events/event_bus/avro/tests/test_schema.py +++ b/openedx_events/event_bus/avro/tests/test_schema.py @@ -239,12 +239,16 @@ class UnextendedClass: def test_throw_exception_to_list_or_dict_types_without_annotation(self): LIST_SIGNAL = create_simple_signal({"list_input": list}) DICT_SIGNAL = create_simple_signal({"list_input": dict}) + LIST_WITHOUT_ANNOTATION_SIGNAL = create_simple_signal({"list_input": List}) with self.assertRaises(Exception): schema_from_signal(LIST_SIGNAL) with self.assertRaises(Exception): schema_from_signal(DICT_SIGNAL) + with self.assertRaises(TypeError): + schema_from_signal(LIST_WITHOUT_ANNOTATION_SIGNAL) + def test_list_with_annotation_works(self): LIST_SIGNAL = create_simple_signal({"list_input": List[int]}) expected_dict = {