diff --git a/cuda_core/cuda/core/_utils/validators.py b/cuda_core/cuda/core/_utils/validators.py index dac3d1c788e..fb533799e36 100644 --- a/cuda_core/cuda/core/_utils/validators.py +++ b/cuda_core/cuda/core/_utils/validators.py @@ -28,7 +28,11 @@ def check_str_enum(value, enum_class, *, allow_none=False): """ if allow_none and value is None: return - if value not in {m.value for m in enum_class}: + # Membership is tested against a tuple rather than a set on purpose: + # ``x in {...}`` hashes ``x``, so an unhashable argument (a list, a dict, + # a bytearray) would raise ``TypeError: unhashable type`` from inside the + # check instead of the ValueError this function exists to raise. + if value not in tuple(m.value for m in enum_class): valid = sorted(m.value for m in enum_class) if allow_none: valid = [None, *valid] diff --git a/cuda_core/docs/source/release/1.2.0-notes.rst b/cuda_core/docs/source/release/1.2.0-notes.rst index 120d2c2a253..300249ba704 100644 --- a/cuda_core/docs/source/release/1.2.0-notes.rst +++ b/cuda_core/docs/source/release/1.2.0-notes.rst @@ -73,6 +73,12 @@ Fixes and enhancements Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted. (`#2439 `__) +- String-enum options now report an unhashable argument the same way as any + other invalid value. Passing a ``list`` or ``dict`` where a string enum was + expected -- for example ``WorkqueueResourceOptions(sharing_scope=[...])`` -- + raised ``TypeError: unhashable type`` from inside the validator instead of + the documented ``ValueError`` naming the accepted values. + Deprecation Notices ------------------- diff --git a/cuda_core/tests/test_utils_validators.py b/cuda_core/tests/test_utils_validators.py new file mode 100644 index 00000000000..d5047b751a5 --- /dev/null +++ b/cuda_core/tests/test_utils_validators.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from cuda.core import WorkqueueResourceOptions +from cuda.core._utils.validators import check_str_enum, format_or_list +from cuda.core.typing import SourceCodeType + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + ("values", "expected"), + [ + pytest.param([], "", id="empty"), + pytest.param(["a"], "'a'", id="one"), + pytest.param(["a", "b"], "'a' or 'b'", id="two"), + pytest.param(["a", "b", "c"], "'a', 'b' or 'c'", id="three"), + pytest.param([None, "a"], "None or 'a'", id="none-first"), + ], +) +def test_format_or_list(values, expected): + assert format_or_list(values) == expected + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize("value", ["c++", "ptx", "nvvm", SourceCodeType.CXX]) +def test_check_str_enum_accepts_members_and_their_values(value): + check_str_enum(value, SourceCodeType) + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_check_str_enum_allow_none(): + check_str_enum(None, SourceCodeType, allow_none=True) + + with pytest.raises(ValueError, match="None is not a valid SourceCodeType"): + check_str_enum(None, SourceCodeType) + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + "value", + [ + pytest.param("fortran", id="str"), + pytest.param(5, id="int"), + # Unhashable arguments used to blow up inside the membership test with + # "TypeError: unhashable type" before the ValueError was ever built. + pytest.param(["c++"], id="list"), + pytest.param({"code_type": "c++"}, id="dict"), + pytest.param(bytearray(b"c++"), id="bytearray"), + ], +) +def test_check_str_enum_rejects_invalid_values_with_value_error(value): + with pytest.raises(ValueError, match="is not a valid SourceCodeType. Must be "): + check_str_enum(value, SourceCodeType) + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_options_reject_an_unhashable_scope_with_value_error(): + """A public entry point must report a bad option the same way for every + kind of bad value. ``WorkqueueResourceOptions.__post_init__`` validates + ``sharing_scope`` through ``check_str_enum``, so an unhashable argument + used to surface as ``TypeError: unhashable type: 'list'`` while a plain + string got the documented ValueError.""" + with pytest.raises(ValueError, match="is not a valid WorkqueueSharingScopeType. Must be "): + WorkqueueResourceOptions(sharing_scope=["green_ctx_balanced"])