Repository: mit-ll-responsible-ai/hydra-zen
Pull request #555 (2023-10-19 16:35:49)

Add support for Python 3.12
`dataclasses.make_dataclass` formally added a `module` argument, which we already support in `zen_dataclass`. `dataclasses.make_dataclass(modle=None)` will attempt to infer the module from the call site whereas previously it just was set as `types`. This new inference of `__module__` is likely not very useful as-is (it points to an internal hydra-zen script), so we overwrite it to be `"types"` for Python 3.12 as well.



Patch:
diff --git a/.github/workflows/tox_run.yml b/.github/workflows/tox_run.yml
index 801fbe43..7cb72ee3 100644
--- a/.github/workflows/tox_run.yml
+++ b/.github/workflows/tox_run.yml
@@ -18,7 +18,7 @@ jobs:
 
     strategy:
       matrix:
-        python-version: [3.8, 3.9, "3.10", 3.11]
+        python-version: [3.8, 3.9, "3.10", 3.11, 3.12]
       fail-fast: false
 
     steps:
diff --git a/README.md b/README.md
index c7fdb044..b74dadf6 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
     <img src="https://img.shields.io/pypi/v/hydra-zen.svg" alt="PyPI" />
   </a>
   <a>
-    <img src="https://img.shields.io/badge/python-3.8%20&#8208;%203.11-blue.svg" alt="Python version support" />
+    <img src="https://img.shields.io/badge/python-3.8%20&#8208;%203.12-blue.svg" alt="Python version support" />
   </a>
   <a href="https://github.com/mit-ll-responsible-ai/hydra-zen/actions?query=workflow%3ATests+branch%3Amain">
     <img src="https://github.com/mit-ll-responsible-ai/hydra-zen/workflows/Tests/badge.svg" alt="GitHub Actions" />
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index 9156f63c..8a8af3f2 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -64,6 +64,7 @@ For more details and examples, see :pull:`553`.
 
 Improvements
 ------------
+- Adds formal support for Python 3.12. See :pull:`555`
 - :func:`~hydra_zen.BuildsFn` was introduced to permit customizable auto-config and type-refinement support in config-creation functions. See :pull:`553`.
 - :func:`~hydra_zen.builds` and :func:`~hydra_zen.make_custom_builds_fn` now accept a `zen_exclude` field for excluding parameters from auto-population, either by name or by pattern. See :pull:`558`.
 
diff --git a/pyproject.toml b/pyproject.toml
index a12c7037..b34cf871 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ classifiers = [
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
+       "Programming Language :: Python :: 3.12",
        "Topic :: Scientific/Engineering",
        "Programming Language :: Python :: 3 :: Only",
 ]
@@ -127,7 +128,7 @@ skip = 'docs/build/*'
 legacy_tox_ini = """
 [tox]
 isolated_build = True
-envlist = py38, py39, py310, py311
+envlist = py38, py39, py310, py311, py312
 
 [gh-actions]
 python =
@@ -135,6 +136,7 @@ python =
   3.9: py39
   3.10: py310
   3.11: py311
+  3.12: py312
 
 
 [testenv]
@@ -143,7 +145,8 @@ description = Runs test suite parallelized in the specified python enviornment a
               Run `tox -e py39 -- -n 0` to run tests in a python 3.9 with 
               parallelization disabled.
 passenv = *
-deps = pytest
+deps = setuptools
+       pytest
        hypothesis
        pytest-xdist
        tzdata
diff --git a/src/hydra_zen/structured_configs/_implementations.py b/src/hydra_zen/structured_configs/_implementations.py
index 2aab8d47..10b48cba 100644
--- a/src/hydra_zen/structured_configs/_implementations.py
+++ b/src/hydra_zen/structured_configs/_implementations.py
@@ -449,7 +449,8 @@ def hydrated_dataclass(
                 "kw_only": kw_only,
                 "slots": slots,
                 "weakref_slot": weakref_slot,
-            }
+            },
+            include_module=False,
         )
         decorated_obj = dataclass(**dc_options)(decorated_obj)  # type: ignore
 
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
index d6a5bc40..d6e7e686 100644
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -284,7 +284,9 @@ _STRICT_DATACLASS_OPTION_KEYS: FrozenSet[str] = (
 _STRICT_DATACLASS_OPTION_KEYS.copy()
 
 
-def parse_dataclass_options(options: Mapping[str, Any]) -> DataclassOptions:
+def parse_dataclass_options(
+    options: Mapping[str, Any], include_module: bool = True
+) -> DataclassOptions:
     """
     Ensures `options` adheres to `DataclassOptions` and merges hydra-zen defaults
     for missing options.
@@ -335,12 +337,16 @@ def parse_dataclass_options(options: Mapping[str, Any]) -> DataclassOptions:
             raise ValueError(f"{name} is not a valid dataclass option.")
 
         if name == "module":
-            if not isinstance(val, str) or not all(
-                v.isidentifier() and not iskeyword(v) for v in val.split(".")
+            if val is not None and (
+                not isinstance(val, str)
+                or not all(
+                    v.isidentifier() and not iskeyword(v) for v in val.split(".")
+                )
             ):
                 raise ValueError(
                     f"dataclass option `{name}` must be a valid module name, got {val}"
                 )
+
         elif name == "cls_name":
             if val is not None and (not isinstance(val, str) or not val.isidentifier()):
                 raise ValueError(
@@ -367,6 +373,15 @@ def parse_dataclass_options(options: Mapping[str, Any]) -> DataclassOptions:
                 f"(type: {type(val)})"
             )
         merged[name] = val
+    if (
+        include_module
+        and "module" not in merged
+        and "module" in _STRICT_DATACLASS_OPTION_KEYS
+    ):  # pragma: no cover
+        # For Python 3.12+ we want the default module to
+        # remain "types" rather than being inferred as some
+        # internal hydra-zen module.
+        merged["module"] = "types"
     return merged
 
 
diff --git a/src/hydra_zen/typing/_implementations.py b/src/hydra_zen/typing/_implementations.py
index 08fb4c14..36099876 100644
--- a/src/hydra_zen/typing/_implementations.py
+++ b/src/hydra_zen/typing/_implementations.py
@@ -393,19 +393,25 @@ class _Py311Dataclass(_Py310Dataclass, total=False):
     weakref_slot: bool
 
 
+class _Py312Dataclass(_Py311Dataclass, total=False):
+    module: Optional[str]
+
+
 if sys.version_info < (3, 10):
     _StrictDataclassOptions = _AllPyDataclassOptions
 elif sys.version_info < (3, 11):
     _StrictDataclassOptions = _Py310Dataclass
-else:  # pragma: no cover
+elif sys.version_info < (3, 12):  # pragma: no cover
     _StrictDataclassOptions = _Py311Dataclass
+else:  # pragma: no cover
+    _StrictDataclassOptions = _Py312Dataclass
 
 
 class StrictDataclassOptions(_StrictDataclassOptions):
     cls_name: Required[str]
 
 
-class DataclassOptions(_Py311Dataclass, total=False):
+class DataclassOptions(_Py312Dataclass, total=False):
     """Specifies dataclass-creation options via `builds`, `just` et al.
 
     Note that, unlike :func:`dataclasses.make_dataclass`, the default value for
@@ -492,6 +498,9 @@ class DataclassOptions(_Py311Dataclass, total=False):
         “__weakref__”, which is required to make an instance weakref-able. It is an
         error to specify `weakref_slot=True` without also specifying `slots=True`.
 
+    module : str | None
+        If module is defined, the __module__ attribute of the dataclass is set to that value. By default, it is set to the module name of the caller.
+
     References
     ----------
     .. [1] https://docs.python.org/3/library/dataclasses.html
@@ -563,7 +572,7 @@ class DataclassOptions(_Py311Dataclass, total=False):
     None
     """
 
-    module: str  # zen-only
+    module: Optional[str]
 
 
 def _permitted_keys(typed_dict: Any) -> FrozenSet[str]:
@@ -572,7 +581,7 @@ def _permitted_keys(typed_dict: Any) -> FrozenSet[str]:
 
 DEFAULT_DATACLASS_OPTIONS = DataclassOptions(unsafe_hash=True)
 PERMITTED_DATACLASS_OPTIONS = _permitted_keys(DataclassOptions)
-UNSUPPORTED_DATACLASS_OPTIONS = _permitted_keys(_Py311Dataclass) - _permitted_keys(
-    StrictDataclassOptions
-)
-del _AllPyDataclassOptions, _Py310Dataclass, _Py311Dataclass
+UNSUPPORTED_DATACLASS_OPTIONS = (
+    _permitted_keys(_Py312Dataclass) - {"module"}
+) - _permitted_keys(StrictDataclassOptions)
+del _AllPyDataclassOptions, _Py310Dataclass, _Py311Dataclass, _Py312Dataclass


Test patch:
diff --git a/tests/test_dataclass_semantics.py b/tests/test_dataclass_semantics.py
index d4507a41..82755be0 100644
--- a/tests/test_dataclass_semantics.py
+++ b/tests/test_dataclass_semantics.py
@@ -410,3 +410,44 @@ def test_unhashable_dataclass_supported():
     Conf = builds(dict, y=unhash, zen_convert={"dataclass": False})
     assert not hasattr(Conf, "y")
     assert Conf().y is unhash
+
+
+@pytest.mark.parametrize(
+    "Conf",
+    [
+        make_config(zen_dataclass={"module": None}),
+        builds(dict, zen_dataclass={"module": None}),
+        make_custom_builds_fn(zen_dataclass={"module": None})(dict),
+    ],
+)
+def test_module_is_None(Conf):
+    if sys.version_info < (3, 12):
+        assert Conf.__module__ == "types"
+    else:
+        assert Conf.__module__.startswith("hydra_zen")
+
+
+@pytest.mark.parametrize(
+    "Conf",
+    [
+        make_config(),
+        builds(
+            dict,
+        ),
+        make_custom_builds_fn()(dict),
+    ],
+)
+def test_module_is_not_specified(Conf):
+    assert Conf.__module__ == "types"
+
+
+@pytest.mark.parametrize(
+    "Conf",
+    [
+        make_config(zen_dataclass={"module": "aaa"}),
+        builds(dict, zen_dataclass={"module": "aaa"}),
+        make_custom_builds_fn(zen_dataclass={"module": "aaa"})(dict),
+    ],
+)
+def test_modules_is_specified(Conf):
+    assert Conf.__module__ == "aaa"