Fix QuamRoot.load() NameError with TYPE_CHECKING forward references#200
Merged
Conversation
When a quam_dataclass uses `from __future__ import annotations` together with TYPE_CHECKING-only imports to avoid circular dependencies, get_type_hints() raises NameError because the referenced type isn't in the module's global namespace at runtime. Fix: in get_dataclass_attr_annotations(), catch NameError from get_type_hints() and fall back to resolving each annotation individually against the module's globals. Annotations that still can't be resolved are replaced with typing.Any — loading then relies on the __class__ key already present in every serialised QuAM dict to determine the concrete type, so no information is lost. Also catches NameError in _get_value_annotation() (returns None, meaning no QuamDict/QuamList element-type annotation — safe default). Adds [tool.pytest.ini_options] pythonpath=["."] so the worktree's quam package is used during tests rather than the installed editable version.
Contributor
Author
|
@JacobHast could you verify whether this resolves your circular naming issue? |
- Move forward_ref_helpers from quam/components/ to tests/serialisation/ so test-only classes aren't shipped with the library - Fix loading of Dict[str, ForwardRef] attributes: when annotation resolves to Any, process dict values through instantiate_attrs_from_dict so __class__ keys on individual values are still honoured - Add tests for Dict and List collection roundtrips with forward refs - Remove unnecessary pythonpath setting from pyproject.toml
tests/ and tests/serialisation/ need __init__.py so that from tests.serialisation.forward_ref_helpers.* imports work. Also fix __class__ strings in test_save_with_defaults_config.py to use the full package path (tests.serialisation.*) now that tests is a proper package.
52674b7 to
e92d8b7
Compare
… entry Document the TYPE_CHECKING pattern for components in separate files that reference each other's types. Simplify the changelog entry to focus on the user-facing fix rather than implementation details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
When a
quam_dataclasscomponent class usesfrom __future__ import annotationstogether withTYPE_CHECKING-only imports to avoid circular dependencies,QuamRoot.load()raises aNameError:During loading,
instantiate_quam_class(Qubit, ...)callsget_dataclass_attr_annotations(Qubit), which callstyping.get_type_hints(Qubit). That function tries to resolve the string annotation"Resonator"againstqubit.py's global namespace — but since the import only happened underTYPE_CHECKING,Resonatoris absent at runtime and aNameErroris raised.Saving (
to_dict()) already worked because it never needs to resolve annotations.How it's fixed
The fix introduces a fallback path in
get_dataclass_attr_annotations()(quam/utils/dataclass.py):The key insight is that every serialised QuAM object already carries a
__class__key in its dict representation.instantiate_attrs()already reads that key to look up the concrete class viaget_class_from_path()— completely independently of the annotation. So substitutingAnyfor an unresolvable annotation is safe: the concrete type is recovered from__class__at runtime instead.The same
NameErrorguard is added to_get_value_annotation()inquam_classes.py(returnsNone, meaning no element-type annotation forQuamDict/QuamList— already the safe default).Changes
quam/utils/dataclass.py: add_get_type_hints_with_fallback(), catchNameErroringet_dataclass_attr_annotations()quam/core/quam_classes.py: catchNameErrorin_get_value_annotation()quam/components/helper_files/:QubitandResonatortest fixtures with circularTYPE_CHECKINGimportstests/serialisation/test_forward_references.py: 6 TDD tests covering save, load, and full roundtrippyproject.toml: add[tool.pytest.ini_options] pythonpath = ["."]so the local source tree is used during test runsCHANGELOG.md: entry under### FixedTest plan
TestForwardRefSaving—to_dict()works and produces correct__class__keys (baseline, was already passing)TestForwardRefLoading—Root.load(d)instantiates the correct component types and preserves reference stringsTestForwardRefRoundtrip— save → load produces identical instances and identical dictsCloses #90