Skip to content

Commit

Permalink
Merge pull request #1257 from WilliamJamieson/bugfix/enum
Browse files Browse the repository at this point in the history
fixes #1256
  • Loading branch information
WilliamJamieson committed Dec 5, 2022
1 parent 01bed38 commit f10a915
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 0 deletions.
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
2.14.2 (unreleased)
-------------------

The ASDF Standard is at v1.6.0

- Fix issue #1256, where ``enum`` could not be used on tagged objects. [#1257]

2.14.1 (2022-11-23)
-------------------

Expand Down
12 changes: 12 additions & 0 deletions asdf/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ def validate_type(validator, types, instance, schema):
return mvalidators.Draft4Validator.VALIDATORS["type"](validator, types, instance, schema)


def validate_enum(validator, enums, instance, schema):
"""
`asdf.tagged.Tagged` objects will fail in the default enum validator
"""

if isinstance(instance, tagged.Tagged):
instance = instance.base

yield from mvalidators.Draft4Validator.VALIDATORS["enum"](validator, enums, instance, schema)


YAML_VALIDATORS = util.HashableDict(mvalidators.Draft4Validator.VALIDATORS.copy())
YAML_VALIDATORS.update(
{
Expand All @@ -158,6 +169,7 @@ def validate_type(validator, types, instance, schema):
"flowStyle": validate_flowStyle,
"style": validate_style,
"type": validate_type,
"enum": validate_enum,
}
)

Expand Down
14 changes: 14 additions & 0 deletions asdf/tagged.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,22 @@ class Tagged:
with it.
"""

_base_type = None

@property
def base(self):
"""Convert to base type"""

return self._base_type(self)


class TaggedDict(Tagged, UserDict, dict):
"""
A Python dict with a tag attached.
"""

_base_type = dict

flow_style = None
property_order = None

Expand All @@ -72,6 +82,8 @@ class TaggedList(Tagged, UserList, list):
A Python list with a tag attached.
"""

_base_type = list

flow_style = None

def __init__(self, data=None, tag=None):
Expand All @@ -97,6 +109,8 @@ class TaggedString(Tagged, UserString, str):
A Python string with a tag attached.
"""

_base_type = str

style = None

def __eq__(self, other):
Expand Down
100 changes: 100 additions & 0 deletions asdf/tests/test_tagged.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from copy import copy, deepcopy

import pytest

import asdf
from asdf.tagged import TaggedDict, TaggedList, TaggedString


Expand Down Expand Up @@ -32,6 +35,17 @@ def test_tagged_list_isinstance():
assert isinstance(value, list)


def test_tagged_list_base():
value = TaggedList([0, 1, 2, ["foo"]], "tag:nowhere.org:custom/foo-1.0.0")

assert not (value == value.base) # base is not a tagged list
assert value.data == value.base # but the data is

assert isinstance(value.base, list)
assert not isinstance(value.base, TaggedList)
assert value.base.__class__ == list


def test_tagged_dict_deepcopy():
original = TaggedDict({"a": 0, "b": 1, "c": 2, "nested": {"d": 3}}, "tag:nowhere.org:custom/foo-1.0.0")
result = deepcopy(original)
Expand Down Expand Up @@ -61,6 +75,17 @@ def test_tagged_dict_isinstance():
assert isinstance(value, dict)


def test_tagged_dict_base():
value = TaggedDict({"a": 0, "b": 1, "c": 2, "nested": {"d": 3}}, "tag:nowhere.org:custom/foo-1.0.0")

assert not (value == value.base) # base is not a tagged dict
assert value.data == value.base # but the data is

assert isinstance(value.base, dict)
assert not isinstance(value.base, TaggedDict)
assert value.base.__class__ == dict


def test_tagged_string_deepcopy():
original = TaggedString("You're it!")
original._tag = "tag:nowhere.org:custom/foo-1.0.0"
Expand All @@ -80,3 +105,78 @@ def test_tagged_string_copy():
def test_tagged_string_isinstance():
value = TaggedString("You're it!")
assert isinstance(value, str)


def test_tagged_string_base():
value = TaggedString("You're it!")
value._tag = "tag:nowhere.org:custom/foo-1.0.0"

assert not (value == value.base) # base is not a tagged dict
assert value.data == value.base # but the data is

assert isinstance(value.base, str)
assert not isinstance(value.base, TaggedString)
assert value.base.__class__ == str


ASDF_UNIT_TAG = "stsci.edu:asdf/unit/unit-1.0.0"
TAGGED_UNIT_URI = "asdf://stsci.edu/schemas/tagged_unit-1.0.0"
TAGGED_UNIT_SCHEMA = f"""
%YAML 1.1
---
$schema: http://stsci.edu/schemas/yaml-schema/draft-01
id: {TAGGED_UNIT_URI}
properties:
unit:
tag: {ASDF_UNIT_TAG}
enum: [m, kg]
...
"""


def create_units():
meter = TaggedString("m")
meter._tag = ASDF_UNIT_TAG

kilogram = TaggedString("kg")
kilogram._tag = ASDF_UNIT_TAG

return meter, kilogram


@pytest.mark.parametrize("unit", create_units())
def test_check_valid_str_enum(unit):
"""
Regression test for issue #1254
https://github.com/asdf-format/asdf/issues/1256
This ensures that tagged strings can be properly validated against ``enum`` lists.
"""
with asdf.config_context() as conf:
conf.add_resource_mapping({TAGGED_UNIT_URI: TAGGED_UNIT_SCHEMA})
schema = asdf.schema.load_schema(TAGGED_UNIT_URI)

# This should not raise an exception (check_schema will raise error)
asdf.schema.check_schema(schema)

# This should not raise exception (validate will raise error)
asdf.schema.validate({"unit": unit}, schema=schema)


def test_check_invalid_str_enum():
"""
Ensure that a tagged string that is not in the ``enum`` list is properly handled.
"""
with asdf.config_context() as conf:
conf.add_resource_mapping({TAGGED_UNIT_URI: TAGGED_UNIT_SCHEMA})
schema = asdf.schema.load_schema(TAGGED_UNIT_URI)

# This should not raise an exception (check_schema will raise error)
asdf.schema.check_schema(schema)

unit = TaggedString("foo")
unit._tag = ASDF_UNIT_TAG

with pytest.raises(asdf.ValidationError, match=r"'foo' is not one of \['m', 'kg'\]"):
asdf.schema.validate({"unit": unit}, schema=schema)

0 comments on commit f10a915

Please sign in to comment.