Repository: mit-ll-responsible-ai/hydra-zen
Pull request #331 (2022-10-26 02:19:19)

Adds `hydra_zen.store`
`hydra_zen.ZenStore` is an abstraction over Hydra's config store that streamlines the process of generating a config and then storing it in Hydra's config store. It enables users to isolate stores from Hydra's global config store and from one another, so that many stores can be maintained within a project without "clobbering" one another.

It is designed to be highly customizable while having defaults that seldom need to be tinkered with. It also has some nice ergonomics (see the self-partialing feature 😄 ).

## Basic Usage

This code:

```python
from hydra_zen import builds
from hydra.core.config_store import ConfigStore

def repeat(x: int, y: str):
    return x * y

cs = ConfigStore.instance()
cs.store(name=repeat.__name__, node=builds(func, populate_full_signature=True))
```

is equivalent to:

```python
from hydra_zen import store

@store
def repeat(x: int, y: str):
    return x * y

store.add_to_hydra_store()  # (default) have to explicitly opt-in to populating Hydra's global store
```

and this:

```python
cs.store(name="repeat1", node=builds(repeat, x=1, populate_full_signature=True))
cs.store(name="repeat2", node=builds(repeat, x=2, populate_full_signature=True))
```

can be rewritten as:

```python
@store(name="repeat2", x=2)
@store(name="repeat1", x=1)
def repeat(x: int, y: str):
    return x * y
```

or

```python
store(repeat, name="repeat1", x=1)
store(repeat, name="repeat2", x=2)
```

## Basic Implementation Details

There is a lot more here than meets the eye. It is useful to take a peek at the implementation, which, at its core, is quite simple. Here is a stripped-down implementation:

```python
def store(       
    f: Optional[F] = None,
    *,
    name: str | Callable[[Any, Any], str] = lambda f, _: f.__name__,
    group: None | str | Callable[[Any, Any], str] = None,
    package: None | str | Callable[[Any, Any], str] = None,
    provider: Optional[str] = None,
    to_config: Callable[[F], Any] = lambda x, **kw: builds(x, **kw, populate_full_signature=True),
    store_fn: HydraStoreFn = ConfigStore.instance().store,
    **to_config_kw: Any) -> F:
    
    # this excludes some fancy decorator & self-partialing stuff, as well as annotation overrides

    cfg = to_config(f, **to_config_kw)

    _name = name(f, cfg) if callable(name) else name
    _group = group(f, cfg) if callable(group) else group
    _pkg = package(f, cfg) if callable(package) else package
    store_fn(
            name=_name,
            node=cfg,
            group=_group,
            package=_pkg,
            provider=provider,
        )
    return f
```

Note that nearly every aspect of `store`, including how it generates configs, can be customized. Furthermore, the `name`, `group`, and `package` fields can be specified as callables, which can be used to, for instance, auto-generate the node's name based on details like its module's path.

## Bells & Whistles

### `store` is both a function and a decorator

As shown above `store` can be used as a function or a decorator

```python
# as a decorator
@store(group="math")
def add(x, y): 
    return x + y

# as a function
def add(x, y): 
    return x + y

store(add, group="math")
```

### `store` is a passthrough
That is, `store(f) is f`, and extra keywords passed to `store` are passed to the config-creation function. This means that you can stack `@store` decorators to store different configs for the same target

```python
@store(name="grid_trainer", device="gpu")  # user has to specify `num_epoch` on launch
@store(name="test_trainer", num_epoch=1, device="cpu")  # all defaults provided here
def trainer(num_epochs, device):
    ...
```

this also streamlines things like creating partial configs

```python
from torch.optim import Adam

store(Adam, name="adam", zen_partial=True)  # `zen_partial=True` is passed to `builds` 
```

### `store` is self-partialing

This is really cool.. you can overwrite the default arguments of `store` without using `functools.partial`

```python
math_store = store(group="math")

@math_store(name="add_it")  # equivalent to  @store(name="add_it", group="math")
def add(x, y): return x + y
```

You can specify new defaults endlessly

```python
new_store = store(name="a")(group="b")(package="c")

new_store(func)  # equivalent to store(func, name="a", group="b", package="c")
```

This makes it easy to customize the behavior of `store` while preserving its signature and type annotations. You can seriously override everything [^2] 

### Specialized support for dataclasses

The default `to_config` function will create configs that are proper subclasses of a target dataclass. Consider:

```python
from dataclasses import dataclass

from hydra.core.config_store import ConfigStore
from omegaconf import OmegaConf

from hydra_zen import store


@store(name="a2", x=2)
@store(name="a1", x=1)
@store(name="a")
@dataclass
class A:
    x: int
```

```python
>>> config = OmegaConf.to_object(ConfigStore.instance().repo["a1.yaml"].node)
>>> issubclass(type(config), A)
True
```

Thus for each application of `store`, `builds` produces a new dataclass-type, which is a subtype of `A`.


[^2]: A library author might be interested in patterns similar to the following:

    ```python
    from collections import defaultdict

    from hydra_zen import store as orig_store, builds, get_target
    from hydra.core.config_store import ConfigStore

    from my_lib import Dense, Conv2d, Cifar10, MNIST

    config_repo = defaultdict(dict)  # makes for easy-access to the generated configs

    def override_store(
        name: str,
        node: Any,
        group: Optional[str] = None,
        package: Optional[str] = None,
        provider: Optional[str] = None,
    ):
        ConfigStore.instance().store(name, node, group, package, provider)
        target = get_target(node) 
        config_repo[target][(name, node, group, package, provider)] = node
        return None

    store = orig_store(provider="my_lib", store_fn=override_store)

    model_store = store(group="model")
    model_store(Dense)
    model_store(Conv2d)

    data_store = store(group="data")
    data_store(Cifar10)
    data_store(MNIST)
    ```

### Responsive type annotations

pyright impresses once again (unfortunately, mypy can't catch the following true positive -- I will open an issue)

```python
from hydra_zen import store,  just

# type checker: error  (you can't pass a dict as a target to `builds`)
# runtime: error
store({"a": 1}, name="some_dict", to_config=builds)  

# type checker: OK
# runtime: OK
store({"a": 1}, name="some_dict", to_config=just)  
```


## Feedback

### Does `@store` encourage an anti-pattern?
Although I like the `@store` syntax, I am not crazy about having library authors decorate functions throughout their library -- it leads to a "non-locality", where those functions will only be added to the store if they are accessed. E.g. if I have

```python
# contents of my_lib/math.py
@store
def add(x, y): ...
```

The config for `add` only becomes available in Hydra's config store if `my_lib.math` gets imported somewhere along the way. This feels like an anti-pattern as it leads to spooky action-at-a-distance behavior for unwitting users who may wonder why `add` is suddenly in their config store[^1]. 

I realize that library authors could just as easily write:

```python
# contents of my_lib/math.py
def add(x, y): ...

store(add)
```

but this feels like more of a deliberate design decision, whereas the availability of the decorator syntax feels like it is practically begging for this pattern.

Is there a way to discourage or even prevent this? I have considered having some sort of intermediary function, `add_configs_to_store`, that users have to explicitly call to actually update Hydra's config store. 

I am also concerned with configs silently getting overwritten by configs of the same name.

This is just a preliminary idea, and I'd love to hear other ideas for encouraging best practices with Hydra's config store.

[^1]: That being said, it is *quite* nice to be able to do:

    ```python
    from hydra_zen import store, zen

    @store(name="task2", x=1, y=2)
    @store(name="task1", x=1, y=2)
    def task_fn(x, y):
        # stuff

    if __name__ == "__main__":
        zen(task_fn).hydra_main(config_path=None, config_name="task1")
    ```

 


Patch:
diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst
index 752de38f..5a3fd981 100644
--- a/docs/source/api_reference.rst
+++ b/docs/source/api_reference.rst
@@ -53,6 +53,17 @@ Creating Configs
    hydrated_dataclass
 
 
+Storing Configs
+***************
+.. currentmodule:: hydra_zen
+
+.. autosummary::
+   :toctree: generated/
+
+   ZenStore
+   wrapper.default_to_config
+
+
 Instantiating and Resolving Configs
 ***********************************
 
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index 59cf52d9..cfcb51f7 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -11,7 +11,7 @@ chronological order. All previous releases should still be available on pip.
 .. _v0.9.0:
 
 ---------------------
-0.9.0rc3 - 2022-11-17
+0.9.0rc4 - 2022-11-21
 ---------------------
 
 .. note:: This is documentation for an unreleased version of hydra-zen. You can try out this pre-release version using `pip install --pre hydra-zen`
@@ -19,9 +19,9 @@ chronological order. All previous releases should still be available on pip.
 
 Release Highlights
 ------------------
-This release adds the :func:`~hydra_zen.zen` decorator, which enables users to use Hydra-agnostic task functions for their Hydra app; the decorator will automatically extract, resolve, and instantiate fields from an input config based on the function's signature. 
+This release introduces :func:`~hydra_zen.zen` and :class:`~hydra_zen.ZenStore`, which enable hydra-zen users to eliminate Hydra-specific boilerplate code from their projects, and to utilize new patterns and best practices for working with config stores.
 
-This encourages users to eliminate Hydra-specific boilerplate code from their projects and to instead opt for task functions with explicit signatures, including functions from third parties.
+The :func:`~hydra_zen.zen` decorator enables of use Hydra-agnostic task functions in Hydra apps; the decorator will automatically extract, resolve, and instantiate fields from an input config based on the function's signature. This encourages users to eliminate Hydra-specific boilerplate code from their projects and to instead opt for task functions with explicit signatures, which can include functions from third parties.
 
 E.g., :func:`~hydra_zen.zen` enables us to replace the following Hydra-specific task function:
 
@@ -49,24 +49,72 @@ with a Hydra-agnostic task function that has an explicit signature:
 .. code-block:: python
    :caption: Using `zen` to design a Hydra-agnostic task function
 
+
    # note: no Hydra or hydra-zen specific logic here
    def trainer_task_fn(model, data, partial_optim, trainer, num_epochs):
       optim = partial_optim(model.parameters())
       trainer(model, optim, data).fit(num_epochs)
    
    if __name__ == "__main__":
-      from hydra_zen import zen
-      
-      # All config-field extraction & instantiation is automated/mediated by zen
-      zen(trainer_task_fn).hydra_main(config_name="my_app", config_path=None,version_base="1.2")
+       from hydra_zen import zen
+       
+       # All config-field extraction & instantiation is automated/mediated by zen.
+       # I.e. `zen` will extract & instantiate model, data, etc. from the input
+       # config and pass it to `trainer_task_fn`
+       zen(trainer_task_fn).hydra_main(config_name="my_app", config_path=None)
 
 
 There are plenty more bells and whistles to :func:`~hydra_zen.zen`, refer to :pull:`310` and its reference documentation for more details.
 
+:class:`~hydra_zen.ZenStore` is an abstraction over Hydra's config store.
+It enables users to maintain multiple, isolated store instances before populating Hydra's global config store.
+It also protects users from accidentally overwriting  entries in Hydra's global store
+
+This enables objects to be stored using a decorator pattern, e.g.
+
+.. code-block:: python
+   :caption: Using `hydra_zen.store` as a decorator to auto-configure and store objects.
+
+   from dataclasses import dataclass
+   from hydra_zen import store
+
+   profile_store = store(group="profile")
+
+   # Adds two store entries under the "profile" group of the store
+   # with configured defaults for `has_root`
+   @profile_store(name="admin", has_root=True)
+   @profile_store(name="basic", has_root=False)
+   @dataclass
+   class Profile:
+       username: str
+       schema: str
+       has_root: bool
+
+:class:`~hydra_zen.ZenStore` also possesses auto-config capabilities: it will automatically apply :func:`~hyda_zen.builds` and :func:`~hyda_zen.just` in intuitive ways on inputs to generate the stored configs.
+
+.. code-block:: python
+   :caption: Using `hydra_zen.store` auto-generate and store configs
+
+   from hydra_zen import ZenStore
+   from torch.optim import Adam, AdamW, RMSprop
+
+   torch_store = ZenStore("torch_store")
+
+   # Specify defaults for storing entries (group=optim)
+   # and for generating configs (_partial_=True and lr=1e-3)
+   torch_store = torch_store(group="optim", zen_partial=True, lr=0.001)
+
+   # Automatically applies `builds(<obj>, zen_partial=True, lr=0.001)` 
+   # to create and then store configs under the "optim" group
+   torch_store(Adam, name="adam", amsgrad=True)
+   torch_store(AdamW, name="adamw", betas=(0.1, 0.999))
+   torch_store(RMSprop, name="rmsprop")
+
 New Features
 ------------
 - Adds the :func:`~hydra_zen.zen` decorator (see :pull:`310`)
 - Adds the :func:`~hydra_zen.wrapper.Zen` decorator-class (see :pull:`310`)
+- Adds the :class:`~hydra_zen.ZenStore` decorator (see :pull:`331`)
 
 
 Improvements
diff --git a/docs/source/conf.py b/docs/source/conf.py
index bff4f982..bda04fcf 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -77,13 +77,13 @@ intersphinx_mapping = {
 #   alias ->  (base-URL, prefix)
 _repo = "https://github.com/mit-ll-responsible-ai/hydra-zen/"
 extlinks = {
-    "commit": (_repo + "commit/%s", "commit "),
-    "gh-file": (_repo + "blob/master/%s", ""),
-    "gh-link": (_repo + "%s", ""),
-    "issue": (_repo + "issues/%s", "issue #"),
-    "pull": (_repo + "pull/%s", "pull request #"),
-    "plymi": ("https://www.pythonlikeyoumeanit.com/%s", ""),
-    "hydra": ("https://hydra.cc/docs/%s", ""),
+    "commit": (_repo + "commit/%s", "commit %s"),
+    "gh-file": (_repo + "blob/master/%s", "%s"),
+    "gh-link": (_repo + "%s", "%s"),
+    "issue": (_repo + "issues/%s", "issue #%s"),
+    "pull": (_repo + "pull/%s", "pull request #%s"),
+    "plymi": ("https://www.pythonlikeyoumeanit.com/%s", "%s"),
+    "hydra": ("https://hydra.cc/docs/%s", "%s"),
 }
 
 # -- Options for HTML output -------------------------------------------------
diff --git a/docs/source/generated/hydra_zen.ZenField.rst b/docs/source/generated/hydra_zen.ZenField.rst
index 22164b73..875c6600 100644
--- a/docs/source/generated/hydra_zen.ZenField.rst
+++ b/docs/source/generated/hydra_zen.ZenField.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.ZenField
+hydra\_zen.ZenField
 ===================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.builds.rst b/docs/source/generated/hydra_zen.builds.rst
index e5ddf130..fe00d40d 100644
--- a/docs/source/generated/hydra_zen.builds.rst
+++ b/docs/source/generated/hydra_zen.builds.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.builds
+hydra\_zen.builds
 =================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.get_target.rst b/docs/source/generated/hydra_zen.get_target.rst
index 91434c4f..2cdce9c2 100644
--- a/docs/source/generated/hydra_zen.get_target.rst
+++ b/docs/source/generated/hydra_zen.get_target.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.get\_target
+hydra\_zen.get\_target
 ======================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.hydrated_dataclass.rst b/docs/source/generated/hydra_zen.hydrated_dataclass.rst
index 5079cbe8..3e42fab2 100644
--- a/docs/source/generated/hydra_zen.hydrated_dataclass.rst
+++ b/docs/source/generated/hydra_zen.hydrated_dataclass.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.hydrated\_dataclass
+hydra\_zen.hydrated\_dataclass
 ==============================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.instantiate.rst b/docs/source/generated/hydra_zen.instantiate.rst
index debeeb03..4c8cb01f 100644
--- a/docs/source/generated/hydra_zen.instantiate.rst
+++ b/docs/source/generated/hydra_zen.instantiate.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.instantiate
+hydra\_zen.instantiate
 ======================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.is_partial_builds.rst b/docs/source/generated/hydra_zen.is_partial_builds.rst
index d2149d29..a1bca5cb 100644
--- a/docs/source/generated/hydra_zen.is_partial_builds.rst
+++ b/docs/source/generated/hydra_zen.is_partial_builds.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.is\_partial\_builds
+hydra\_zen.is\_partial\_builds
 ==============================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.just.rst b/docs/source/generated/hydra_zen.just.rst
index b4429522..dd6a904b 100644
--- a/docs/source/generated/hydra_zen.just.rst
+++ b/docs/source/generated/hydra_zen.just.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.just
+hydra\_zen.just
 ===============
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.launch.rst b/docs/source/generated/hydra_zen.launch.rst
index 851ff07b..a3df0b15 100644
--- a/docs/source/generated/hydra_zen.launch.rst
+++ b/docs/source/generated/hydra_zen.launch.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.launch
+hydra\_zen.launch
 =================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.load_from_yaml.rst b/docs/source/generated/hydra_zen.load_from_yaml.rst
index 2e4093a8..242d0344 100644
--- a/docs/source/generated/hydra_zen.load_from_yaml.rst
+++ b/docs/source/generated/hydra_zen.load_from_yaml.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.load\_from\_yaml
+hydra\_zen.load\_from\_yaml
 ===========================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.make_config.rst b/docs/source/generated/hydra_zen.make_config.rst
index 3f69df92..e479df76 100644
--- a/docs/source/generated/hydra_zen.make_config.rst
+++ b/docs/source/generated/hydra_zen.make_config.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.make\_config
+hydra\_zen.make\_config
 =======================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.make_custom_builds_fn.rst b/docs/source/generated/hydra_zen.make_custom_builds_fn.rst
index 275eeead..74997fed 100644
--- a/docs/source/generated/hydra_zen.make_custom_builds_fn.rst
+++ b/docs/source/generated/hydra_zen.make_custom_builds_fn.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.make\_custom\_builds\_fn
+hydra\_zen.make\_custom\_builds\_fn
 ===================================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.save_as_yaml.rst b/docs/source/generated/hydra_zen.save_as_yaml.rst
index 1bce0d3a..047ed343 100644
--- a/docs/source/generated/hydra_zen.save_as_yaml.rst
+++ b/docs/source/generated/hydra_zen.save_as_yaml.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.save\_as\_yaml
+hydra\_zen.save\_as\_yaml
 =========================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.third_party.beartype.validates_with_beartype.rst b/docs/source/generated/hydra_zen.third_party.beartype.validates_with_beartype.rst
index 966ead73..52a49eb9 100644
--- a/docs/source/generated/hydra_zen.third_party.beartype.validates_with_beartype.rst
+++ b/docs/source/generated/hydra_zen.third_party.beartype.validates_with_beartype.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.third\_party.beartype.validates\_with\_beartype
+hydra\_zen.third\_party.beartype.validates\_with\_beartype
 ==========================================================
 
 .. currentmodule:: hydra_zen.third_party.beartype
diff --git a/docs/source/generated/hydra_zen.third_party.pydantic.validates_with_pydantic.rst b/docs/source/generated/hydra_zen.third_party.pydantic.validates_with_pydantic.rst
index f0cf40f8..4f86d940 100644
--- a/docs/source/generated/hydra_zen.third_party.pydantic.validates_with_pydantic.rst
+++ b/docs/source/generated/hydra_zen.third_party.pydantic.validates_with_pydantic.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.third\_party.pydantic.validates\_with\_pydantic
+hydra\_zen.third\_party.pydantic.validates\_with\_pydantic
 ==========================================================
 
 .. currentmodule:: hydra_zen.third_party.pydantic
diff --git a/docs/source/generated/hydra_zen.to_yaml.rst b/docs/source/generated/hydra_zen.to_yaml.rst
index a19d9b85..7f15fd18 100644
--- a/docs/source/generated/hydra_zen.to_yaml.rst
+++ b/docs/source/generated/hydra_zen.to_yaml.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.to\_yaml
+hydra\_zen.to\_yaml
 ===================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.typing.ZenConvert.rst b/docs/source/generated/hydra_zen.typing.ZenConvert.rst
index 3d4ce675..f19ae8cb 100644
--- a/docs/source/generated/hydra_zen.typing.ZenConvert.rst
+++ b/docs/source/generated/hydra_zen.typing.ZenConvert.rst
@@ -1,4 +1,4 @@
-﻿Each of hydra-zen's :ref:`config-creation functions <create-config>` has a `zen_config` 
+Each of hydra-zen's :ref:`config-creation functions <create-config>` has a `zen_config` 
 parameter, which can be passed a dictionary to modify the function's value and type conversion behaviors. `hydra_zen.typing.ZenConfig` is a :py:class:`typing.TypedDict` 
 that can be used as a convenience function to view and override these options.
 
diff --git a/docs/source/generated/hydra_zen.uses_zen_processing.rst b/docs/source/generated/hydra_zen.uses_zen_processing.rst
index 0fd19397..c2b3772a 100644
--- a/docs/source/generated/hydra_zen.uses_zen_processing.rst
+++ b/docs/source/generated/hydra_zen.uses_zen_processing.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.uses\_zen\_processing
+hydra\_zen.uses\_zen\_processing
 ================================
 
 .. currentmodule:: hydra_zen
diff --git a/docs/source/generated/hydra_zen.wrapper.Zen.rst b/docs/source/generated/hydra_zen.wrapper.Zen.rst
index 0b74bc37..66ebcf49 100644
--- a/docs/source/generated/hydra_zen.wrapper.Zen.rst
+++ b/docs/source/generated/hydra_zen.wrapper.Zen.rst
@@ -1,4 +1,4 @@
-﻿hydra\_zen.wrapper.Zen
+hydra\_zen.wrapper.Zen
 ======================
 
 .. currentmodule:: hydra_zen.wrapper
diff --git a/docs/source/generated/hydra_zen.zen.rst b/docs/source/generated/hydra_zen.zen.rst
index dd6fdc11..64a0c54d 100644
--- a/docs/source/generated/hydra_zen.zen.rst
+++ b/docs/source/generated/hydra_zen.zen.rst
@@ -1,5 +1,5 @@
-﻿hydra\_zen.zen
-===============
+hydra\_zen.zen
+==============
 
 .. currentmodule:: hydra_zen
 
diff --git a/pyproject.toml b/pyproject.toml
index c83e57db..62b96fd3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -82,6 +82,9 @@ combine_as_imports = true
 omit = ["src/hydra_zen/_version.py"]
 
 
+[tool.pytest.ini_options]
+xfail_strict=true
+
 
 [tool.pyright]
 include = ["src"]
diff --git a/src/hydra_zen/__init__.py b/src/hydra_zen/__init__.py
index e6851f80..51a78c01 100644
--- a/src/hydra_zen/__init__.py
+++ b/src/hydra_zen/__init__.py
@@ -21,7 +21,7 @@ from .structured_configs import (
 )
 from .structured_configs._implementations import get_target
 from .structured_configs._type_guards import is_partial_builds, uses_zen_processing
-from .wrapper import zen
+from .wrapper import ZenStore, store, zen
 
 __all__ = [
     "builds",
@@ -41,6 +41,8 @@ __all__ = [
     "is_partial_builds",
     "uses_zen_processing",
     "zen",
+    "store",
+    "ZenStore",
 ]
 
 if not TYPE_CHECKING:
diff --git a/src/hydra_zen/typing/__init__.py b/src/hydra_zen/typing/__init__.py
index 6b287c34..5412fee6 100644
--- a/src/hydra_zen/typing/__init__.py
+++ b/src/hydra_zen/typing/__init__.py
@@ -8,6 +8,7 @@ from ._implementations import (
     Just,
     Partial,
     PartialBuilds,
+    StoreEntry,
     SupportedPrimitive,
     ZenConvert,
     ZenPartialBuilds,
@@ -25,4 +26,5 @@ __all__ = [
     "ZenPartialBuilds",
     "HydraPartialBuilds",
     "ZenConvert",
+    "StoreEntry",
 ]
diff --git a/src/hydra_zen/typing/_implementations.py b/src/hydra_zen/typing/_implementations.py
index 682a3a0f..90808457 100644
--- a/src/hydra_zen/typing/_implementations.py
+++ b/src/hydra_zen/typing/_implementations.py
@@ -261,6 +261,19 @@ class AllConvert(TypedDict, total=True):
 # used for runtime type-checking
 convert_types: Final = {"dataclass": bool}
 
+GroupName: TypeAlias = Optional[str]
+NodeName: TypeAlias = str
+Node: TypeAlias = Any
+
+
+# TODO: make immutable
+class StoreEntry(TypedDict):
+    name: NodeName
+    group: GroupName
+    package: Optional[str]
+    provider: Optional[str]
+    node: Node
+
 
 class ZenConvert(TypedDict, total=False):
     """A TypedDict that provides a type-checked interface for specifying zen-convert
diff --git a/src/hydra_zen/wrapper/__init__.py b/src/hydra_zen/wrapper/__init__.py
index 55ca40cc..60ee44d9 100644
--- a/src/hydra_zen/wrapper/__init__.py
+++ b/src/hydra_zen/wrapper/__init__.py
@@ -3,6 +3,6 @@
 
 # pyright: strict
 
-from ._implementations import Zen, zen
+from ._implementations import Zen, ZenStore, default_to_config, store, zen
 
-__all__ = ["zen", "Zen"]
+__all__ = ["zen", "Zen", "store", "ZenStore", "default_to_config"]
diff --git a/src/hydra_zen/wrapper/_implementations.py b/src/hydra_zen/wrapper/_implementations.py
index 0028f07a..41fff504 100644
--- a/src/hydra_zen/wrapper/_implementations.py
+++ b/src/hydra_zen/wrapper/_implementations.py
@@ -2,11 +2,16 @@
 # SPDX-License-Identifier: MIR
 # pyright: strict
 
+from collections import defaultdict, deque
 from inspect import Parameter, signature
 from typing import (
     Any,
     Callable,
+    DefaultDict,
+    Deque,
     Dict,
+    FrozenSet,
+    Generator,
     Generic,
     Iterable,
     List,
@@ -18,29 +23,49 @@ from typing import (
     Type,
     TypeVar,
     Union,
+    cast,
     overload,
 )
 
 import hydra
-from hydra.main import _UNSPECIFIED_  # type: ignore
+from hydra.core.config_store import ConfigStore
+from hydra.utils import instantiate
 from omegaconf import DictConfig, ListConfig, OmegaConf
-from typing_extensions import Literal, ParamSpec, TypeAlias, TypeGuard
+from typing_extensions import (
+    Final,
+    Literal,
+    ParamSpec,
+    Protocol,
+    TypeAlias,
+    TypedDict,
+    TypeGuard,
+)
 
-from hydra_zen import instantiate
+from hydra_zen import instantiate, just, make_custom_builds_fn
 from hydra_zen.errors import HydraZenValidationError
-from hydra_zen.typing._implementations import DataClass_
+from hydra_zen.structured_configs._utils import get_obj_path
+from hydra_zen.typing._implementations import (
+    DataClass_,
+    GroupName,
+    Node,
+    NodeName,
+    StoreEntry,
+)
 
 from .._compatibility import HYDRA_SUPPORTS_LIST_INSTANTIATION, SUPPORTS_VERSION_BASE
 from ..structured_configs._type_guards import is_dataclass
 from ..structured_configs._utils import safe_name
 
-__all__ = ["zen"]
+__all__ = ["zen", "store", "Zen"]
 
 
 R = TypeVar("R")
 P = ParamSpec("P")
+F = TypeVar("F")
 
 
+_UNSPECIFIED_: Any = object()
+
 if HYDRA_SUPPORTS_LIST_INSTANTIATION:
     _SUPPORTED_INSTANTIATION_TYPES: Tuple[Any, ...] = (dict, DictConfig, list, ListConfig)  # type: ignore
 else:  # pragma: no cover
@@ -249,6 +274,7 @@ class Zen(Generic[P, R]):
             raise HydraZenValidationError(
                 f"`cfg._args_` must be a sequence type (e.g. a list), got {_args_}"
             )
+
         if num_pos_only and len(_args_) != num_pos_only:
             raise HydraZenValidationError(
                 f"{self.func} has {num_pos_only} positional-only arguments, but "
@@ -336,7 +362,7 @@ class Zen(Generic[P, R]):
         version_base: Optional[str] = _UNSPECIFIED_,
     ) -> Callable[[Any], Any]:
         """
-        Returns a Hydra-CLI compatible version of the wrapped function: `hydra.main(zen(func))`
+        Generates a Hydra-CLI for the wrapped function. Equivalent to `hydra.main(zen(func), [...])()`
 
         Parameters
         ----------
@@ -361,8 +387,14 @@ class Zen(Generic[P, R]):
             Equivalent to `hydra.main(zen(func), [...])()`
         """
 
-        kw = dict(config_path=config_path, config_name=config_name)
-        if SUPPORTS_VERSION_BASE:
+        kw = dict(config_name=config_name)
+
+        if config_path is not _UNSPECIFIED_:
+            kw["config_path"] = config_path
+
+        if (
+            SUPPORTS_VERSION_BASE and version_base is not _UNSPECIFIED_
+        ):  # pragma: no cover
             kw["version_base"] = version_base
 
         return hydra.main(**kw)(self)()
@@ -541,7 +573,7 @@ def zen(
     ...     return x, zen_cfg
     >>> f(dict(x=1, y="${x}"))
     (1, {'x': 1, 'y': 1})
-    
+
     **Including a pre-call function**
 
     Given that a zen-wrapped function will automatically extract and instantiate config
@@ -629,3 +661,827 @@ def zen(
         )
 
     return wrap
+
+
+fbuilds = make_custom_builds_fn(populate_full_signature=True)
+
+
+def default_to_config(
+    target: Union[
+        Callable[..., Any],
+        DataClass_,
+        List[Any],
+        Dict[Any, Any],
+        ListConfig,
+        DictConfig,
+    ],
+    **kw: Any,
+) -> Union[DataClass_, Type[DataClass_], ListConfig, DictConfig]:
+    """Creates a config that describes `target`.
+
+    This function is designed to selectively apply `hydra_zen.builds` or
+    `hydra_zen.just` in a way that permits maximum compatibility with common
+    inputs to `hydra_zen.ZenStore`. It behavior can be summarized based on the type of
+    `target`
+
+    - OmegaConf containers and dataclass *instances* are returned unchanged
+    - A dataclass type is processed as `builds(target, **kw, populate_full_signature=True, builds_bases=(target,))`
+    - Lists and dictionaries are processed by `hydra_zen.just`
+    - All other inputs are processed as `builds(target, **kw, populate_full_signature=True)`
+
+    Parameters
+    ----------
+    target : Callable[..., Any] | DataClass | Type[DataClass] | list | dict
+
+    **kw : Any
+        Keyword arguments to be passed to `builds`.
+
+    Returns
+    -------
+    target_config :  DataClass | Type[DataClass] | list | dict
+
+    Examples
+    --------
+    Lists and dictionaries
+
+    >>> from hydra_zen.wrapper import default_to_config
+    >>> default_to_config([1, {"z": 2+2j}])
+    [1, {'z': ConfigComplex(real=2.0, imag=2.0, _target_='builtins.complex')}]
+
+    Dataclass types
+
+    >>> from dataclasses import dataclass
+    >>>
+    >>> @dataclass
+    ... class A:
+    ...     x: int
+    ...     y: int
+
+    >>> Builds_A = default_to_config(A, y=22)
+    >>> Builds_A(x=1)
+    Builds_A(x=1, y=22, _target_='__main__.A')
+    >>> issubclass(Builds_A, A)
+    True
+
+    A function
+
+    >>> from hydra_zen import to_yaml
+    >>> def func(x: int, y: int): ...
+    >>> print(to_yaml(default_to_config(func)))
+    _target_: __main__.func
+    x: ???
+    'y': ???
+    """
+    if is_dataclass(target):
+        if isinstance(target, type):
+            if get_obj_path(target).startswith("types."):
+                # handles dataclasses returned by make_config()
+                return target
+            return fbuilds(target, **kw, builds_bases=(target,))
+        if kw:
+            raise ValueError(
+                "store(<dataclass-instance>, [...]) does not support specifying "
+                "keyword arguments"
+            )
+        return target
+
+    elif isinstance(target, (dict, list)):
+        # TODO: convert to OmegaConf containers?
+        return just(target)
+    elif isinstance(target, (DictConfig, ListConfig)):
+        return target
+    else:
+        t = cast(Callable[..., Any], target)
+        return fbuilds(t, **kw)
+
+
+class _HasName(Protocol):
+    __name__: str
+
+
+# TODO: Should we automatically snake-case?
+def get_name(target: _HasName) -> str:
+    name = getattr(target, "__name__", None)
+    if not isinstance(name, str):
+        raise TypeError(
+            f"Cannot infer config store entry name for {target}. It does not have a "
+            f"`__name__` attribute. Please manually specify `store({target}, "
+            f"name=<some name>, [...])`"
+        )
+    return name
+
+
+class _StoreCallSig(TypedDict):
+    """Arguments for ZenStore.__call__
+
+    This default dict enables us to easily update/merge the default arguments for a
+    specific ZenStore instance, in support of self-partialing behavior."""
+
+    name: Union[NodeName, Callable[[Any], NodeName]]
+    group: Union[GroupName, Callable[[Any], GroupName]]
+    package: Optional[Union[str, Callable[[Any], str]]]
+    provider: Optional[str]
+    __kw: Dict[str, Any]  # kwargs passed to to_config
+    to_config: Callable[[Any], Any]
+
+
+# TODO: make frozen dict
+defaults: Final = _StoreCallSig(
+    name=get_name,
+    group=None,
+    package=None,
+    provider=None,
+    to_config=default_to_config,
+    __kw={},
+)
+
+_DEFAULT_KEYS: Final[FrozenSet[str]] = frozenset(_StoreCallSig.__required_keys__ - {"__kw"})  # type: ignore
+
+
+class _Deferred:
+    __slots__ = ("to_config", "target", "kw")
+
+    def __init__(
+        self, to_config: Callable[[F], Node], target: F, kw: Dict[str, Any]
+    ) -> None:
+        self.to_config = to_config
+        self.target = target
+        self.kw = kw
+
+    def __call__(self) -> Any:
+        return self.to_config(self.target, **self.kw)
+
+
+def _resolve_node(entry: StoreEntry, copy: bool) -> StoreEntry:
+    """Given an entry, updates the entry so that its node is not deferred, and returns
+    the entry. This function is a passthrough for an entry whose node is not deferred"""
+    item = entry["node"]
+    if isinstance(item, _Deferred):
+        entry["node"] = item()
+
+    if copy:
+        entry = entry.copy()
+    return entry
+
+
+class ZenStore:
+    """An abstraction over Hydra's config store, which enables users to maintain
+    multiple, isolated store instances before populating Hydra's global config store.
+
+    `ZenStore` is also designed to consolidate the config-creation and storage process;
+    it can be used to decorate config-targets and dataclasses, enabling "inline" config
+    creation and storage patterns. This is also a "self-partialing" object, meaning a
+    store instance can overwrite its own default values. Please consult the
+    :ref:`examples <self-partial>` for more details.
+
+    `hydra_zen.store` is available as a pre-instantiated, globally-available store,
+    which is initialized as:
+
+    .. code-block:: python
+
+       store = ZenStore(
+           name="zen_store",
+           deferred_to_config=True,
+           deferred_hydra_store=True,
+       )
+
+    Examples
+    --------
+    **Basic usage**
+
+    Let's add a config to hydra-zen's pre-instantiated `ZenStore` instance. Each config
+    must have an associated name. Optionally, a group, package, and/or provider may be
+    specified for the entry as well.
+
+    >>> from hydra_zen import store
+    >>> _ = store({"lr": 0.01, "momentum": 0.9}, name="sgd", group="optim")
+    >>> _ = store({"lr": 0.001}, name="adam", group="optim")
+    zen_store
+    {'optim': ['sgd', 'adam']}
+
+    A store's entries are keyed by their `(group, name)` pair (the default group is
+    `None`).
+
+    >>> store["optim", "sgd"]  # (group, name) -> config node
+    {'lr': 0.01, 'momentum': 0.9}
+
+    By default, the stored config(s) will be "enqueued" for addition to Hydra's config
+    store, and `.add_to_hydra_store()` must be called to add the enqueued configs
+    to Hydra's central store.
+
+    >>> store.has_enqueued()
+    True
+    >>> store.add_to_hydra_store()  # adds all enqueued entries to Hydra's global store
+    >>> store.has_enqueued()
+    False
+
+    By default, attempting to overwrite an entry will result in an error.
+
+    >>> store({}, name="sgd", group="optim")  # same name and group as above
+    ValueError: (name=sgd group=optim): Hydra config store entry already exists.
+    Specify `overwrite_ok=True` to enable replacing config store entries
+
+    We can create a distinct store that has an independent internal repository of
+    configs.
+
+    >>> from hydra_zen import ZenStore
+    >>> new_store = ZenStore("new_store")
+    >>> _ = new_store([1, 2, 3], name="backbone")
+
+    >>> store
+    zen_store
+    {'optim': ['sgd', 'adam']}
+    >>> new_store
+    new_store
+    {None: ['backbone']}
+
+    **Auto-config capabilities**
+
+    The input to a store is processed by the store's `to_config` function prior to
+    creating the stored config node. By default this calls
+    `hydra_zen.wrapper.default_to_config`, which applies `hydra_zen.builds` or
+    `hydra_zen.just` to inputs based on their types.
+
+    For instance, consider the following function:
+
+    >>> def sum_it(a: int, b: int): return a + b
+
+    We can pass `sum_it` directly to our store to leverage auto-config and auto-naming
+    capabilities. Here, `builds(sum_it, a=1, b=2)` will be called under the hood by
+    `new_store` to create the config for `sum_it`.
+
+    >>> from hydra_zen import to_yaml
+    >>> def pp(x): print(to_yaml(x))  # for pretty printing configs
+
+    >>> store2 = ZenStore()
+    >>> _ = store2(sum_it, a=1, b=2)  # entry name defaults to `sum_it.__name__`
+    >>> config = store2[None, "sum_it"]
+    >>> pp(config)
+    _target_: __main__.sum_it
+    a: 1
+    b: 2
+
+    Refer to `hydra_zen.wrapper.default_to_config` for more details about the default
+    auto-config behaviors of `ZenStore`.
+
+    **Support for decorator patterns**
+
+    `ZenStore.__call__` is a pass-through and can be used as a decorator. Let's add two
+    store entries for `func` by decorating it.
+
+    >>> from hydra_zen import ZenStore, to_yaml
+    >>> store = ZenStore()
+
+    >>> @store(a=1, b=22, name="func1")
+    ... @store(a=-10, name="func2")
+    ... def func(a: int, b: int):
+    ...     return a - b
+
+    >>> func(10, 3)  # the decorated function is left unchanged
+    7
+    >>> pp(store[None, "func1"])
+    _target_: __main__.func
+    a: 1
+    b: 22
+    >>> pp(store[None, "func2"])
+    _target_: __main__.func
+    b: ???
+    a: -10
+
+    Note that, by default, the application of `to_config` via the store is deferred
+    until that entry is actually accessed. This defers the runtime cost of constructing
+    configs for the decorated function so that it need not be paid until necessary.
+
+    .. _self-partial:
+
+    **Customizable store defaults via 'self-partialing' patterns**
+
+    The default values for a store's `__call__` parameters – `group`, `to_config`, etc.
+    – can easily be customized. Simpy call the store with those new values and
+    without specifying an object to be stored. This will return a "mirrored" store
+    instance – with the same internal state as the original store – with updated
+    defaults.
+
+    For example, let's create a store where we want to store multiple configs under a
+    `'math'` group and under a `'functools'` group, respectively.
+
+    >>> import math
+    >>> import functools
+
+    >>> new_store = ZenStore()
+    >>> math_store = new_store(group="math")  # overwrites group default
+    >>> tool_store = new_store(group="functools")  # overwrites group default
+
+    >>> math_store(math.floor)  # equivalent to: `new_store(math.floor, group="math")`
+    >>> math_store(math.ceil)
+    >>> tool_store(functools.lru_cache)
+    >>> tool_store(functools.wraps)
+
+    `math_store` and `tool_store` both share the same internal state as `new_store`, but
+    have overwritten default values for the `group`. See that `new_store` has entries
+    under these corresponding groups:
+
+    >>> new_store
+    custom_store
+    {'math': ['floor', 'ceil'], 'functools': ['lru_cache', 'wraps']}
+
+    These "self-partialing" patterns can be chained indefinitely and can be used to set
+    partial defaults for the `to_config` function.
+
+    >>> profile_store = new_store(group="profile")
+    >>> schemaless = profile_store(schema="<none>")
+
+    >>> from dataclasses import dataclass
+    >>> @profile_store(name="admin", has_root=True)
+    >>> @schemaless(name="test_admin", has_root=True)
+    >>> @schemaless(name="test_user", has_root=False)
+    >>> @dataclass
+    >>> class Profile:
+    >>>     username: str
+    >>>     schema: str
+    >>>     has_root: bool
+
+    >>> pp(new_store["profile", "admin"])
+    username: ???
+    schema: ???
+    has_root: true
+    _target_: __main__.Profile
+
+    >>> pp(new_store["profile", "test_admin"])
+    username: ???
+    schema: <none>
+    has_root: true
+    _target_: __main__.Profile
+    """
+
+    __slots__ = (
+        "name",
+        "_internal_repo",
+        "_defaults",
+        "_queue",
+        "_deferred_to_config",
+        "_deferred_store",
+        "_overwrite_ok",
+    )
+
+    def __init__(
+        self,
+        name: Optional[str] = None,
+        *,
+        deferred_to_config: bool = True,
+        deferred_hydra_store: bool = True,
+        overwrite_ok: bool = False,
+    ) -> None:
+        """
+        Parameters
+        ----------
+        name : Optional[str]
+            The name for this store.
+
+        deferred_to_config : bool, optional (default=True)
+            If `True` (default), this store will a not apply `to_config` to the
+            target until that specific entry is accessed by the store.
+
+        deferred_hydra_store : bool, optional (default=True)
+            If `True` (default), this store will not add entries to Hydra's global
+            config store until `store.add_to_hydra_store` is called explicitly.
+
+        overwrite_ok : bool, optional (default=False)
+            If `False` (default), attempting to overwrite entries in this store and
+            trying to use this store to overwrite entries in Hydra's global store
+            will raise a `ValueError`.
+        """
+        if not isinstance(deferred_to_config, bool):  # type: ignore
+            raise TypeError(
+                f"deferred_to_config must be a bool, got {deferred_to_config}"
+            )
+
+        if not isinstance(overwrite_ok, bool):  # type: ignore
+            raise TypeError(f"overwrite_ok must be a bool, got {overwrite_ok}")
+
+        if not isinstance(deferred_hydra_store, bool):  # type: ignore
+            raise TypeError(
+                f"deferred_hydra_store must be a bool, got {deferred_hydra_store}"
+            )
+
+        self.name: str = "custom_store" if name is None else name
+
+        # The following attributes are mirrored across store instances that are
+        # created via the 'self-partialing' process
+        self._internal_repo: Dict[Tuple[GroupName, NodeName], StoreEntry] = {}
+        # Internal repo entries that have yet to be added to Hydra's config store
+        self._queue: Deque[StoreEntry] = deque([])
+
+        self._deferred_to_config = deferred_to_config
+        self._deferred_store = deferred_hydra_store
+        self._overwrite_ok = overwrite_ok
+        # Contains the current default arguments for `self.__call__`
+        self._defaults: _StoreCallSig = defaults.copy()
+
+    def __repr__(self) -> str:
+        # TODO: nicer repr?
+        groups_contents: DefaultDict[Optional[str], List[str]] = defaultdict(list)
+        for grp, name in self._internal_repo:
+            groups_contents[grp].append(name)
+
+        return f"{self.name}\n{repr(dict(groups_contents))}"
+
+    def __eq__(self, __o: object) -> bool:
+        """Returns `True` if two stores share identical internal repos and queues.
+
+        Examples
+        --------
+        >>> from hydra_zen import ZenStore
+        >>> store1 = ZenStore()
+        >>> store2 = ZenStore()
+        >>> store1_a = store1(group='a')
+        >>> _ = store1_a(dict(x=1), name="foo")
+
+        >>> store1 == store1_a
+        True
+        >>> store1 == store2
+        False
+        """
+        if not isinstance(__o, ZenStore):
+            return False
+        return __o._internal_repo is self._internal_repo and __o._queue is self._queue
+
+    # TODO: support *to_config_pos_args
+    @overload
+    def __call__(
+        self,
+        __target: F,
+        *,
+        name: Union[NodeName, Callable[[F], NodeName]] = ...,
+        group: Union[GroupName, Callable[[F], GroupName]] = ...,
+        package: Optional[Union[str, Callable[[F], str]]] = ...,
+        provider: Optional[str] = ...,
+        to_config: Callable[[F], Node] = default_to_config,
+        **to_config_kw: Any,
+    ) -> F:  # pragma: no cover
+        ...
+
+    # TODO: ZenStore -> Self when mypy adds support for Self
+    #       https://github.com/python/mypy/pull/11666
+    @overload
+    def __call__(
+        self,
+        __target: Literal[None] = None,
+        *,
+        name: Union[NodeName, Callable[[Any], NodeName]] = ...,
+        group: Union[GroupName, Callable[[Any], GroupName]] = ...,
+        package: Optional[Union[str, Callable[[Any], str]]] = ...,
+        provider: Optional[str] = ...,
+        to_config: Callable[[Any], Node] = ...,
+        **to_config_kw: Any,
+    ) -> "ZenStore":  # pragma: no cover
+        ...
+
+    def __call__(self, __target: Optional[F] = None, **kw: Any) -> Union[F, "ZenStore"]:
+        """__call__(target : Optional[T] = None, /, name: NodeName | Callable[[Any], NodeName]] = ..., group: GroupName | Callable[[T], GroupName]] = None, package: Optional[str | Callable[[T], str]]] | None], provider: Optional[str], to_config: Callable[[T], Node] = ..., **to_config_kw: Any) -> T | ZenStore
+
+        The interface to an initialized store. Can be used to store a config or to
+        :ref:`customize the default values <self-partial>` of the store.
+
+        Parameters
+        ----------
+        obj : Optional[T]
+            The object to be stored. This is a **positional-only** argument.
+
+            If `obj` is not specified, then the provided arguments are used to create a
+            mirrored store instance with updated default arguments.
+
+        name : NodeName | Callable[[T], NodeName]
+            The entry's name, or a callable that will be called as
+            `(obj) -> entry-name`. The default is `lambda obj: obj.__name__`.
+            Store entries are keyed off of `(group, name)`.
+
+        group : Optional[GroupName | Callable[[T], GroupName]]
+            The entry's group's name, or a callable that will be called as
+            `(obj) -> entry-group`. The default is `None`. Subgroups can be
+            specified using / within the group name.
+
+            Store entries are keyed off of `(group, name)`.
+
+        to_config : Callable[[T], Node] = default_to_config
+            Called on `obj` to produce the entry's "node" (the config).
+            Refer to  `hydra_zen.wrapper.default_to_config` for the default
+            behavior. Specify `lambda x: x` to have `obj` be stored directly
+            as the entry's node.
+
+            By default the call to `to_config` is deferred until the entry
+            is actually accessed by the store.
+
+        package : Optional[str | Callable[[Any], str]]
+            The entry's package. Default is `None`.
+
+        provider : Optional[str]
+            An optional provider name for the entry.
+
+        **to_config_kw : Any
+            Additional arguments that will be passed to `to_config`.
+
+        Returns
+        -------
+        T | ZenStore
+            If `obj` was specified, it is returned unchanged. Otherwise a new instance
+            of `ZenStore` is return, which mirrors the internal state of this store and
+            has updated default arguments.
+        """
+        if __target is None:
+            _s = type(self)(
+                self.name,
+                deferred_to_config=self._deferred_to_config,
+                deferred_hydra_store=self._deferred_store,
+                overwrite_ok=self._overwrite_ok,
+            )
+            _s._defaults = self._defaults.copy()
+
+            # Important: mirror internal state *by reference* to ensure `_s` and
+            # `self` remain in sync
+            _s._internal_repo = self._internal_repo
+            _s._queue = self._queue
+
+            new_defaults: _StoreCallSig = {k: kw[k] for k in _DEFAULT_KEYS if k in kw}  # type: ignore
+
+            new_defaults["__kw"] = {
+                **_s._defaults["__kw"],
+                **{k: kw[k] for k in set(kw) - _DEFAULT_KEYS},
+            }
+            _s._defaults.update(new_defaults)
+            return _s
+        else:
+            to_config = kw.get("to_config", self._defaults["to_config"])
+            name = kw.get("name", self._defaults["name"])
+            group = kw.get("group", self._defaults["group"])
+            package = kw.get("package", self._defaults["package"])
+            provider = kw.get("provider", self._defaults["provider"])
+
+            _name: NodeName = name(__target) if callable(name) else name
+            if not isinstance(_name, str):  # type: ignore
+                raise TypeError(f"`name` must be a string, got {_name}")
+            del name
+
+            _group: GroupName = group(__target) if callable(group) else group
+            if _group is not None and not isinstance(_group, str):  # type: ignore
+                raise TypeError(f"`group` must be a string or None, got {_group}")
+            del group
+
+            _pkg = package(__target) if callable(package) else package
+            if _pkg is not None and not isinstance(_pkg, str):
+                raise TypeError(f"`package` must be a string or None, got {_pkg}")
+            del package
+
+            merged_kw = {
+                **self._defaults["__kw"],
+                **{k: kw[k] for k in set(kw) - _DEFAULT_KEYS},
+            }
+
+            if self._deferred_to_config:
+                node = _Deferred(to_config, __target, merged_kw)
+            else:
+                node = to_config(__target, **merged_kw)
+
+            entry = StoreEntry(
+                name=_name,
+                group=_group,
+                package=_pkg,
+                provider=provider,
+                node=node,
+            )
+
+            if not self._overwrite_ok and (_group, _name) in self._internal_repo:
+                raise ValueError(
+                    f"(name={entry['name']} group={entry['group']}): "
+                    f"Store entry already exists. Use a store initialized "
+                    f"with `ZenStore(overwrite_ok=True)` to overwrite config store "
+                    f"entries."
+                )
+            self._internal_repo[_group, _name] = entry
+            self._queue.append(entry)
+
+            if not self._deferred_store:
+                self.add_to_hydra_store()
+            return __target
+
+    @property
+    def groups(self) -> Sequence[GroupName]:
+        """Returns a sorted list of the groups registered with this store"""
+        set_: Set[GroupName] = set(group for group, _ in self._internal_repo)
+        if None in set_:
+            set_.remove(None)
+            no_none = cast(Set[str], set_)
+            return [None] + sorted(no_none)
+        else:
+            no_none = cast(Set[str], set_)
+            return sorted(no_none)
+
+    def has_enqueued(self) -> bool:
+        """`True` if this store has entries that have not yet been added to
+        Hydra's config store.
+
+        Returns
+        -------
+        bool
+
+        Examples
+        --------
+        >>> from hydra_zen import ZenStore
+        >>> store = ZenStore(deferred_hydra_store=True)
+        >>> store.has_enqueued()
+        False
+
+        >>> store({"a": 1}, name)
+        >>> store.has_enqueued()
+        True
+
+        >>> store.add_to_hydra_store()
+        >>> store.has_enqueued()
+        False
+        """
+        return bool(self._queue)
+
+    def __bool__(self) -> bool:
+        """`True` if entries have been added to this store, regardless of whether or
+        not they have been added to Hydra's config store"""
+        return bool(self._internal_repo)
+
+    @overload
+    def __getitem__(self, key: Tuple[GroupName, NodeName]) -> Node:  # pragma: no cover
+        ...
+
+    @overload
+    def __getitem__(
+        self, key: GroupName
+    ) -> Dict[Tuple[GroupName, NodeName], Node]:  # pragma: no cover
+        ...
+
+    def __getitem__(self, key: Union[GroupName, Tuple[GroupName, NodeName]]) -> Node:
+        """Access a entry's config node by specifying `(group, name)`. Or, access a
+        mapping of `(group, name) -> node` for all nodes in a specified group,
+        including nodes within subgroups.
+
+        See Also
+        --------
+        ZenStore.get_entry
+
+        Examples
+        --------
+        >>> from hydra_zen import store
+        >>> store(dict(x=1), name="a", group="fruit")
+        >>> store(dict(x=2), name="b", group="fruit/apple")
+        >>> store(dict(x=3), name="c", group="fruit/apple")
+        >>> store(dict(x=4), name="d", group="fruit/orange")
+        >>> store(dict(x=5), name="e", group="veggie")
+
+        Accessing an individual entry's config node.
+
+        >>> store["fruit/apple", "b"]
+        {'x': 2}
+
+        Accessing all config nodes under the "fruit/apple" group
+
+        >>> store["fruit/apple"]
+        {('fruit/apple', 'b'): {'x': 2}, ('fruit/apple', 'c'): {'x': 3}}
+
+        Accessing all config nodes under the "fruit" group
+
+        >>> store["fruit"]
+        {('fruit', 'a'): {'x': 1},
+         ('fruit/apple', 'b'): {'x': 2},
+         ('fruit/apple', 'c'): {'x': 3},
+         ('fruit/orange', 'd'): {'x': 4}}
+        """
+        # store[group] ->
+        #  {(group, name): node1, (group, name2): node2, (group/subgroup, name3): node3}
+        #
+        # store[group, name] -> node
+        if isinstance(key, str) or key is None:
+            key_not_none = key is not None
+            key_w_ender = key + "/" if key is not None else "<ZEN_NEVER>"
+            return {
+                (group, name): _resolve_node(entry, copy=False)["node"]
+                for (group, name), entry in self._internal_repo.items()
+                if group == key
+                or (
+                    key_not_none and group is not None and group.startswith(key_w_ender)
+                )
+            }
+        return _resolve_node(self._internal_repo[key], copy=False)["node"]
+
+    def get_entry(self, group: GroupName, name: NodeName) -> StoreEntry:
+        """Access a store entry, which is a mapping that specifies the entry's
+        name, group, package, provider, and node.
+
+        Notes
+        -----
+        Mutating the returned mapping will not affect the store's internal entry.
+        Mutating node in the returned entry may have unintended consequences and
+        is not advised.
+
+        Examples
+        --------
+        >>> from hydra_zen import store, ZenStore
+        >>> store(dict(x=1), name="a", group="fruit")
+        >>> store.get_entry("fruit", "a")
+        {'name': 'a',
+         'group': 'fruit',
+         'package': None,
+         'provider': None,
+         'node': {'x': 1}}
+        """
+        return _resolve_node(self._internal_repo[(group, name)], copy=True)
+
+    def __contains__(self, key: Union[GroupName, Tuple[GroupName, NodeName]]) -> bool:
+        """Checks if group or (group, node-name) exists in zen-store."""
+        if key is None:
+            return any(k[0] is None for k in self._internal_repo)
+
+        if isinstance(key, str):
+            key_w_end: str = key + "/"
+            return any(
+                key == group or group.startswith(key_w_end)
+                for group, _ in self._internal_repo
+                if group is not None
+            )
+        return key in self._internal_repo
+
+    def __iter__(self) -> Generator[StoreEntry, None, None]:
+        """Yields all entries in this store.
+
+        Notes
+        -----
+        Mutating the returned mappings will not affect the store's internal entries.
+        Mutating node in an entry may have unintended consequences and is not advised.
+
+        Examples
+        --------
+        >>> from hydra_zen import store
+        >>> store(dict(x=1), name="a", group="fruit")
+        >>> store(dict(x=2), name="b", group="fruit/orange")
+        >>> store(dict(x=3), name="c", group="veggie")
+
+        >>> list(store)
+        [{'name': 'a',
+          'group': 'fruit',
+          'package': None,
+          'provider': None,
+          'node': {'x': 1}},
+         {'name': 'b',
+          'group': 'fruit/orange',
+          'package': None,
+          'provider': None,
+          'node': {'x': 2}},
+         {'name': 'c',
+          'group': 'veggie',
+          'package': None,
+          'provider': None,
+          'node': {'x': 3}}]
+        """
+        yield from (_resolve_node(v, copy=True) for v in self._internal_repo.values())
+
+    def add_to_hydra_store(self, overwrite_ok: Optional[bool] = None) -> None:
+        """Adds all of this store's enqueued entries to Hydra's global config store.
+
+        Parameters
+        ----------
+        overwrite_ok : Optional[bool]
+            If `False`, this method raises `ValueError` if an entry in Hydra's config
+            store will be overwritten. Defaults to the value of `overwrite_ok`
+            specified when initializing this store."""
+
+        while self._queue:
+            entry = _resolve_node(self._queue.popleft(), copy=False)
+            if (
+                overwrite_ok is False
+                or (overwrite_ok is None and not self._overwrite_ok)
+            ) and self._exists_in_hydra_store(name=entry["name"], group=entry["group"]):
+                raise ValueError(
+                    f"(name={entry['name']} group={entry['group']}): "
+                    f"Hydra config store entry already exists. Specify "
+                    f"`overwrite_ok=True` to enable replacing config store entries"
+                )
+            ConfigStore.instance().store(**entry)
+
+    def _exists_in_hydra_store(
+        self,
+        *,
+        name: NodeName,
+        group: GroupName,
+        hydra_store: ConfigStore = ConfigStore().instance(),
+    ) -> bool:
+        repo = hydra_store.repo
+
+        if group is not None:
+            for group_name in group.split("/"):
+                repo = repo.get(group_name)
+                if repo is None:
+                    return False
+        return name + ".yaml" in repo
+
+
+store: ZenStore = ZenStore(
+    name="zen_store",
+    deferred_to_config=True,
+    deferred_hydra_store=True,
+)


Test patch:
diff --git a/tests/annotations/declarations.py b/tests/annotations/declarations.py
index 56c17792..e8ce51f4 100644
--- a/tests/annotations/declarations.py
+++ b/tests/annotations/declarations.py
@@ -1,7 +1,7 @@
 # Copyright (c) 2022 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
 
-# These tests help to ensure that our typed interfaces have the desired behvarior, when
+# These tests help to ensure that our typed interfaces have the desired behavior, when
 # being processed by static type-checkers. Specifically we test using pyright.
 #
 # We perform contrapositive testing using lines with the pattern:
@@ -18,13 +18,25 @@ from dataclasses import dataclass
 from enum import Enum
 from functools import partial
 from pathlib import Path
-from typing import Any, Callable, List, Mapping, Optional, Tuple, Type, TypeVar, Union
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+)
 
 from omegaconf import MISSING, DictConfig, ListConfig
 from typing_extensions import Literal, assert_type
 
 from hydra_zen import (
     ZenField,
+    ZenStore,
     builds,
     get_target,
     instantiate,
@@ -32,6 +44,7 @@ from hydra_zen import (
     make_config,
     make_custom_builds_fn,
     mutable_value,
+    store,
     zen,
 )
 from hydra_zen.structured_configs._value_conversion import ConfigComplex, ConfigPath
@@ -47,6 +60,7 @@ from hydra_zen.typing import (
 from hydra_zen.typing._builds_overloads import FullBuilds, PBuilds, StdBuilds
 from hydra_zen.typing._implementations import DataClass_, HydraPartialBuilds
 from hydra_zen.wrapper import Zen
+from hydra_zen.wrapper._implementations import StoreEntry
 
 T = TypeVar("T")
 
@@ -1133,3 +1147,93 @@ def check_zen():
     @zen(exclude=1)  # type: ignore
     def p3():
         ...
+
+
+def check_store():
+    @store
+    def f(x: int, y: int) -> str:
+        ...
+
+    @store(name="hi")
+    def f2(x: int, y: int) -> str:
+        ...
+
+    reveal_type(f, expected_text="(x: int, y: int) -> str")
+    reveal_type(f2, expected_text="(x: int, y: int) -> str")
+
+    reveal_type(store(f), expected_text="(x: int, y: int) -> str")
+    reveal_type(store(f, name="bye"), expected_text="(x: int, y: int) -> str")
+    reveal_type(store(name="bye")(f), expected_text="(x: int, y: int) -> str")
+
+    apple_store = store(group="apple")
+
+    @apple_store
+    def a1(x: int) -> bool:
+        ...
+
+    @apple_store(name="hello")
+    def a2(x: int) -> bool:
+        ...
+
+    reveal_type(a1, expected_text="(x: int) -> bool")
+    reveal_type(a2, expected_text="(x: int) -> bool")
+
+    reveal_type(apple_store(a1), expected_text="(x: int) -> bool")
+    reveal_type(apple_store(a1, name="bye"), expected_text="(x: int) -> bool")
+    reveal_type(apple_store(name="bye")(a1), expected_text="(x: int) -> bool")
+
+    @store(f)  # type: ignore
+    def bad(x: int, y: int) -> str:
+        ...
+
+    # checking that store type-checks against to_config
+    store(1)  # type: ignore
+    store(1, to_config=just)
+
+    store()(1, to_config=builds)  # type: ignore
+    store()(1, to_config=just)
+    store()()(1, to_config=builds)  # type: ignore
+
+    @store
+    @dataclass
+    class A:
+        x: int
+
+    @store(name="hi")
+    @dataclass
+    class B:
+        y: str
+
+    assert_type(A(1).x, int)
+    assert_type(B("a").y, str)
+
+    store(A)
+    store(A(1))
+
+    class SubStore(ZenStore):
+        ...
+
+    substore = SubStore()
+    substore1 = substore(a=1)
+    x = substore1(dict(a=1))
+
+    # TODO: Enable when mypy supports Self
+    # assert_type(substore1, SubStore)
+
+    assert_type(x, Dict[str, int])
+
+    # check __getitem__
+    assert_type(store["group"], Dict[Tuple[Optional[str], str], Any])
+    assert_type(store[None, "name"], Any)
+    assert_type(store["group", "name"], Any)
+    store["name", "group", "bad"]  # type: ignore
+
+    # check __call__overrides
+    store({}, name=1)  # type: ignore
+    store({}, group=1)  # type: ignore
+    store(name=1)  # type: ignore
+    store(group=1)  # type: ignore
+
+    assert_type(list(store), List[StoreEntry])
+    assert_type("group" in store, bool)
+    assert_type(("group", "name") in store, bool)
diff --git a/tests/annotations/mypy_checks.py b/tests/annotations/mypy_checks.py
index 07c22b14..2287999f 100644
--- a/tests/annotations/mypy_checks.py
+++ b/tests/annotations/mypy_checks.py
@@ -3,7 +3,8 @@
 
 from typing_extensions import assert_type
 
-from hydra_zen import builds, instantiate, make_custom_builds_fn
+from hydra_zen import builds, instantiate, just, make_custom_builds_fn, store
+from hydra_zen.wrapper import ZenStore
 
 
 def check_builds() -> None:
@@ -46,6 +47,56 @@ def check_make_custom_builds() -> None:
     full_builds(f)(y=2)  # type: ignore [call-arg]
 
 
+def check_store() -> None:
+    @store
+    def f(x: int, y: int) -> str:
+        ...
+
+    @store(name="hi")
+    def f2(x: int, y: int) -> str:
+        ...
+
+    # reveal_type(f)
+    # reveal_type(f2)
+
+    # reveal_type(store(f))
+    # reveal_type(store(f, name="bye"))
+    # reveal_type(store(name="bye")(f))
+
+    apple_store = store(group="apple")
+    assert_type(apple_store, ZenStore)
+
+    @apple_store
+    def a1(x: int) -> bool:
+        ...
+
+    @apple_store(name="hello")
+    def a2(x: int) -> bool:
+        ...
+
+    assert_type(a2(1), bool)
+    assert_type(store()(a1)(1), bool)
+    assert_type(store()(a1)("a"), bool)  # type: ignore [arg-type]
+    apple_store(name="bye")
+    apple_store(name=22)  # type: ignore [call-overload]
+
+    # reveal_type(apple_store(a1))
+    # reveal_type(apple_store(a1, name="bye"))
+    # reveal_type(apple_store(name="bye")(a1))
+
+    @store(f)  # type: ignore [arg-type, call-arg]
+    def bad(x: int, y: int) -> str:
+        ...
+
+    # checking that store type-checks against to_config
+    # mypy isn't as good as pyright here
+
+    # store(1)  # false negative
+    # store()(1, to_config=builds)  # false negative
+    store(1, to_config=just)
+    store()(1, to_config=just)
+
+
 # def check_just() -> None:
 #     from hydra_zen import just
 
diff --git a/tests/conftest.py b/tests/conftest.py
index ee0a7d95..2ad2a8e9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,19 +1,23 @@
 # Copyright (c) 2022 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
-
 import logging
 import os
 import sys
 import tempfile
+from copy import deepcopy
 from typing import Dict, Iterable, Optional
 
 import hypothesis.strategies as st
 import pkg_resources
 import pytest
+from hydra.core.config_store import ConfigStore
 from omegaconf import DictConfig, ListConfig
 
+from hydra_zen import store
 from hydra_zen._compatibility import HYDRA_VERSION
 
+_store = ConfigStore.instance()
+
 # Skip collection of tests that don't work on the current version of Python.
 collect_ignore_glob = []
 
@@ -60,6 +64,18 @@ def cleandir() -> Iterable[str]:
         logging.shutdown()
 
 
+@pytest.fixture()
+def clean_store() -> Iterable[dict]:
+    """Provides access to configstore repo and restores state after test"""
+    prev_state = deepcopy(_store.repo)
+    zen_prev_state = (store._internal_repo.copy(), store._queue.copy())
+    yield _store.repo
+    _store.repo = prev_state
+    int_repo, queue = zen_prev_state
+    store._internal_repo = int_repo
+    store._queue = queue
+
+
 @pytest.fixture()
 def version_base() -> Dict[str, Optional[str]]:
     """Return version_base according to local version, or empty dict for versions
diff --git a/tests/custom_strategies.py b/tests/custom_strategies.py
index 82827df1..51b1f39d 100644
--- a/tests/custom_strategies.py
+++ b/tests/custom_strategies.py
@@ -1,9 +1,20 @@
 import string
-from typing import Any, Deque, Dict, List, Sequence, Tuple, TypeVar, Union, cast
+from typing import (
+    Any,
+    Deque,
+    Dict,
+    List,
+    Optional,
+    Sequence,
+    Tuple,
+    TypeVar,
+    Union,
+    cast,
+)
 
 import hypothesis.strategies as st
 
-from hydra_zen import builds
+from hydra_zen import ZenStore, builds
 from hydra_zen.structured_configs._utils import get_obj_path
 from hydra_zen.typing._implementations import ZenConvert
 
@@ -98,7 +109,7 @@ def partitions(
 ) -> st.SearchStrategy[Tuple[T, T]]:
     """Randomly partitions a collection or dictionary into two partitions."""
     if isinstance(collection, st.SearchStrategy):
-        return collection.flatmap(lambda x: _partition(x, ordered=ordered))  # type: ignore
+        return collection.flatmap(lambda x: _partition(x, ordered=ordered))
     return cast(st.SearchStrategy[Tuple[T, T]], _partition(collection, ordered))
 
 
@@ -108,3 +119,51 @@ def everything_except(excluded_types):
         .flatmap(st.from_type)
         .filter(lambda x: not isinstance(x, excluded_types))
     )
+
+
+def f():
+    pass
+
+
+f_Build = builds(f, dataclass_name="f_Build")
+
+
+@st.composite
+def store_entries(draw: st.DrawFn):
+    group = draw(st.lists(st.sampled_from("abcde")).map(lambda x: None if not x else x))
+    if group is not None:
+        group = "/".join(group)
+    name = draw(st.uuids().map(str))
+    package = draw(st.none() | st.sampled_from("xyz"))
+    target = draw(st.sampled_from([{"a": 1}, f_Build, f]))
+    return (target, {"name": name, "group": group, "package": package})
+
+
+@st.composite
+def new_stores(draw: st.DrawFn, deferred_hydra_store: Optional[bool] = None):
+    name = draw(st.none() | st.sampled_from(["foo", "bar", "baz"]))
+    return ZenStore(
+        name,
+        deferred_hydra_store=draw(
+            st.booleans().map(lambda x: not x), label="deferred_hydra_store"
+        )
+        if deferred_hydra_store is None
+        else deferred_hydra_store,
+        overwrite_ok=draw(st.booleans(), label="overwrite_ok"),
+        deferred_to_config=draw(
+            st.booleans().map(lambda x: not x), label="deferred_to_config"
+        ),
+    )
+
+
+@st.composite
+def stores(draw: st.DrawFn):
+    """Returned stores always defer storing to hydra store to prevent unexpected
+    change in global state"""
+    store = draw(new_stores(deferred_hydra_store=True))
+    for target, kw in draw(st.lists(store_entries())):
+        store(target, **kw)
+    return store
+
+
+st.register_type_strategy(ZenStore, stores())
diff --git a/tests/test_store.py b/tests/test_store.py
new file mode 100644
index 00000000..12c42c72
--- /dev/null
+++ b/tests/test_store.py
@@ -0,0 +1,726 @@
+# Copyright (c) 2022 Massachusetts Institute of Technology
+# SPDX-License-Identifier: MIT
+import re
+from contextlib import contextmanager
+from copy import deepcopy
+from dataclasses import dataclass
+from functools import partial
+from typing import Any, Callable, Hashable, Optional
+
+import hypothesis.strategies as st
+import pytest
+from hydra.core.config_store import ConfigStore
+from hypothesis import assume, given, note, settings
+from omegaconf import DictConfig, ListConfig
+
+from hydra_zen import (
+    ZenStore,
+    builds,
+    instantiate,
+    just,
+    make_config,
+    store as default_store,
+)
+from hydra_zen._compatibility import HYDRA_SUPPORTS_LIST_INSTANTIATION
+from tests.custom_strategies import new_stores, store_entries
+
+cs = ConfigStore().instance()
+
+
+@contextmanager
+def clean_store():
+    """Provides access to configstore repo and restores state after test"""
+    prev_state = deepcopy(cs.repo)
+    zen_prev_state = (default_store._internal_repo.copy(), default_store._queue.copy())
+    try:
+        yield cs.repo
+    finally:
+        cs.repo = prev_state
+        int_repo, queue = zen_prev_state
+        default_store._internal_repo = int_repo
+        default_store._queue = queue
+
+
+def func(a: int, b: int):
+    assert isinstance(a, int)
+    assert isinstance(b, int)
+    return (a, b)
+
+
+def instantiate_from_repo(
+    name: str,
+    group: Optional[str] = None,
+    package: Optional[str] = None,
+    provider: Optional[str] = None,
+    **kw,
+):
+    """Fetches config-node from repo by name and instantiates it
+    with provided kwargs"""
+    if group is not None:
+        repo = cs.repo
+        for group_name in group.split("/"):
+            repo = repo[group_name]
+        item = repo[f"{name}.yaml"]
+    else:
+        item = cs.repo[f"{name}.yaml"]
+    assert item.package == package
+    assert item.provider == provider
+    return instantiate(item.node, **kw)
+
+
+@pytest.mark.parametrize(
+    "apply_store",
+    [
+        pytest.param(lambda: default_store(func, a=1, b=2), id="inline"),
+        pytest.param(lambda: default_store(a=1, b=2)(func), id="decorated"),
+        pytest.param(lambda: default_store()(func, a=1, b=2), id="partiald_inline"),
+        pytest.param(lambda: default_store()(a=1, b=2)(func), id="partiald_decorated"),
+        pytest.param(
+            lambda: default_store(a=-22)(a=1, b=-22)(func, b=2), id="kw_overrides"
+        ),
+        pytest.param(
+            lambda: default_store(name="BAD")(func),
+            id="ensure_can_fail1",
+            marks=pytest.mark.xfail,
+        ),
+        pytest.param(
+            lambda: default_store(a=22, b=10)(func),
+            id="ensure_can_fail2",
+            marks=pytest.mark.xfail,
+        ),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_kw_overrides(apply_store: Callable[[], Any]):
+    out = apply_store()
+    default_store.add_to_hydra_store()
+    assert out is func
+    assert instantiate_from_repo("func") == (1, 2)
+
+
+@pytest.mark.parametrize(
+    "apply_store",
+    [
+        pytest.param(lambda: default_store(func, name="dunk", a=1, b=2), id="inline"),
+        pytest.param(lambda: default_store(name="dunk")(func), id="decorated"),
+        pytest.param(
+            lambda: default_store(name="O1")(func, name="dunk"), id="partiald_inline"
+        ),
+        pytest.param(
+            lambda: default_store(name="O1")(name="dunk")(func), id="partiald_decorated"
+        ),
+        pytest.param(
+            lambda: default_store(name="O1")(name="O2")(func, name="dunk"),
+            id="kw_overrides",
+        ),
+        pytest.param(
+            lambda: default_store(name="BAD")(func),
+            id="ensure_can_fail",
+            marks=pytest.mark.xfail,
+        ),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_name_overrides(apply_store: Callable[[], Any]):
+    out = apply_store()
+    assert out is func
+    default_store.add_to_hydra_store()
+    assert instantiate_from_repo("dunk", a=1, b=2) == (1, 2)
+
+
+@pytest.mark.parametrize(
+    "apply_store",
+    [
+        pytest.param(lambda: default_store(func, group="dunk", a=1, b=2), id="inline"),
+        pytest.param(lambda: default_store(group="dunk")(func), id="decorated"),
+        pytest.param(
+            lambda: default_store(group="O1")(func, group="dunk"), id="partiald_inline"
+        ),
+        pytest.param(
+            lambda: default_store(group="O1")(group="dunk")(func),
+            id="partiald_decorated",
+        ),
+        pytest.param(
+            lambda: default_store(group="O1")(group="O2")(func, group="dunk"),
+            id="kw_overrides",
+        ),
+        pytest.param(
+            lambda: default_store(group="BAD")(func),
+            id="ensure_can_fail",
+            marks=pytest.mark.xfail,
+        ),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_group_overrides(apply_store: Callable[[], Any]):
+    out = apply_store()
+    assert out is func
+    default_store.add_to_hydra_store()
+    assert instantiate_from_repo(name="func", group="dunk", a=1, b=2) == (1, 2)
+
+
+@pytest.mark.parametrize(
+    "apply_store",
+    [
+        pytest.param(
+            lambda: default_store(func, package="dunk", a=1, b=2), id="inline"
+        ),
+        pytest.param(lambda: default_store(package="dunk")(func), id="decorated"),
+        pytest.param(
+            lambda: default_store(package="O1")(func, package="dunk"),
+            id="partiald_inline",
+        ),
+        pytest.param(
+            lambda: default_store(package="O1")(package="dunk")(func),
+            id="partiald_decorated",
+        ),
+        pytest.param(
+            lambda: default_store(package="O1")(package="O2")(func, package="dunk"),
+            id="kw_overrides",
+        ),
+        pytest.param(
+            lambda: default_store(package="BAD")(func),
+            id="ensure_can_fail",
+            marks=pytest.mark.xfail,
+        ),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_package_overrides(apply_store: Callable[[], Any]):
+    out = apply_store()
+    assert out is func
+    default_store.add_to_hydra_store()
+    assert instantiate_from_repo(name="func", package="dunk", a=1, b=2) == (1, 2)
+
+
+def special_fn(x, **kw):
+    kw["target"] = just(x)
+    return kw
+
+
+def never_call(*a, **k):
+    assert False
+
+
+@pytest.mark.parametrize(
+    "apply_store",
+    [
+        pytest.param(
+            lambda: default_store(func, to_config=special_fn, a=1, b=2), id="inline"
+        ),
+        pytest.param(
+            lambda: default_store(to_config=special_fn, a=1, b=2)(func),
+            id="decorated",
+        ),
+        pytest.param(
+            lambda: default_store(to_config=never_call, a=1, b=2)(
+                func, to_config=special_fn
+            ),
+            id="partiald_inline",
+        ),
+        pytest.param(
+            lambda: default_store(to_config=never_call, a=1, b=2)(to_config=special_fn)(
+                func
+            ),
+            id="partiald_decorated",
+        ),
+        pytest.param(
+            lambda: default_store(to_config=never_call, a=22, b=2)(
+                to_config=never_call
+            )(func, to_config=special_fn, a=1),
+            id="kw_overrides",
+        ),
+        pytest.param(
+            lambda: default_store(to_config=never_call, a=1, b=2)(func),
+            id="ensure_can_fail1",
+            marks=pytest.mark.xfail,
+        ),
+        pytest.param(
+            lambda: default_store(func, to_config=never_call, a=1, b=2),
+            id="ensure_can_fail2",
+            marks=pytest.mark.xfail,
+        ),
+        pytest.param(
+            lambda: default_store(func, a=1, b=2),
+            id="ensure_can_fail3",
+            marks=pytest.mark.xfail,
+        ),
+        pytest.param(
+            lambda: default_store(func, a=1, b=3, to_config=special_fn),
+            id="ensure_can_fail4",
+            marks=pytest.mark.xfail,
+        ),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_to_config_overrides(apply_store: Callable[[], Any]):
+    out = apply_store()
+    assert out is func
+    default_store.add_to_hydra_store()
+    assert instantiate_from_repo(name="func") == dict(a=1, b=2, target=func)
+
+
+def override_store(
+    name: str,
+    node: Any,
+    group: Optional[str] = None,
+    package: Optional[str] = None,
+    provider: Optional[str] = None,
+):
+    assert False
+
+
+@pytest.mark.parametrize(
+    "apply_store",
+    [
+        pytest.param(
+            lambda: default_store(func, provider="dunk", a=1, b=2), id="inline"
+        ),
+        pytest.param(lambda: default_store(provider="dunk")(func), id="decorated"),
+        pytest.param(
+            lambda: default_store(provider="O1")(func, provider="dunk"),
+            id="partiald_inline",
+        ),
+        pytest.param(
+            lambda: default_store(provider="O1")(provider="dunk")(func),
+            id="partiald_decorated",
+        ),
+        pytest.param(
+            lambda: default_store(provider="O1")(provider="O2")(func, provider="dunk"),
+            id="kw_overrides",
+        ),
+        pytest.param(
+            lambda: default_store(provider="BAD")(func),
+            id="ensure_can_fail",
+            marks=pytest.mark.xfail,
+        ),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_provider_overrides(apply_store: Callable[[], Any]):
+    out = apply_store()
+    assert out is func
+    default_store.add_to_hydra_store()
+    assert instantiate_from_repo(name="func", provider="dunk", a=1, b=2) == (1, 2)
+
+
+@pytest.mark.parametrize("include_none", [True, False])
+@pytest.mark.usefixtures("clean_store")
+def test_store_nested_groups(include_none: bool):
+    # Tests that nested groups are stored in Hydra store as-expected
+    # and that ZenStore's __getitem__ has parity with the store
+    local_store = ZenStore(deferred_hydra_store=False)
+    if include_none:
+        local_store({"a": 0}, name="a")
+
+    def check_repr():
+        assert isinstance(repr(local_store), str)
+
+    check_repr()
+
+    local_store({"a": 1}, group="A", name="a")
+    check_repr()
+
+    local_store({"a": 2}, group="A", name="b")
+    check_repr()
+
+    local_store({"a": 3}, group="A/B", name="ab")
+    check_repr()
+
+    local_store({"a": 4}, group="A/B/C", name="abc")
+    check_repr()
+
+    if include_none:
+        assert instantiate_from_repo(name="a") == instantiate(local_store[None, "a"])
+        assert instantiate_from_repo(name="a") == {"a": 0}
+
+    assert instantiate_from_repo(name="a", group="A") == instantiate(
+        local_store["A", "a"]
+    )
+    assert instantiate_from_repo(name="a", group="A") == {"a": 1}
+
+    assert instantiate_from_repo(name="b", group="A") == instantiate(
+        local_store["A", "b"]
+    )
+
+    assert instantiate_from_repo(name="b", group="A") == {"a": 2}
+
+    assert instantiate_from_repo(name="ab", group="A/B") == instantiate(
+        local_store["A/B", "ab"]
+    )
+    assert instantiate_from_repo(name="ab", group="A/B") == {"a": 3}
+
+    assert instantiate_from_repo(name="abc", group="A/B/C") == instantiate(
+        local_store["A/B/C", "abc"]
+    )
+    assert instantiate_from_repo(name="abc", group="A/B/C") == {"a": 4}
+
+    if include_none:
+        assert local_store.groups == [None, "A", "A/B", "A/B/C"]
+        assert len(local_store[None]) == 1
+    else:
+        assert local_store.groups == ["A", "A/B", "A/B/C"]
+    assert len(local_store["A"]) == 4
+    assert len(local_store["A/B"]) == 2
+    assert len(local_store["A/B/C"]) == 1
+
+    assert set(local_store["A/B"]) < set(local_store["A"])
+    assert set(local_store["A/B/C"]) < set(local_store["A/B"])
+
+
+@pytest.mark.parametrize("bad_val", [1, True, ("a",)])
+@pytest.mark.parametrize("field_name", ["name", "group", "package"])
+@pytest.mark.usefixtures("clean_store")
+def test_store_param_validation(bad_val, field_name: str):
+    with pytest.raises(TypeError, match=rf"`{field_name}` must be"):
+        default_store(func, **{field_name: bad_val})
+        default_store.add_to_hydra_store()
+
+
+@dataclass
+class DC:
+    ...
+
+
+dc = DC()
+
+
+def test_validate_get_name():
+    with pytest.raises(TypeError, match=r"Cannot infer config store entry name"):
+        default_store(dc)
+
+
+@pytest.mark.parametrize("name1", "ab")
+@pytest.mark.parametrize("name2", "ab")
+@pytest.mark.parametrize("group1", [None, "c", "d", "e/f", "e/f/g"])
+@pytest.mark.parametrize("group2", [None, "c", "d", "e/f", "e/f/g"])
+@pytest.mark.usefixtures("clean_store")
+def test_raise_on_redundant_store(
+    name1: str, name2: str, group1: Optional[str], group2: Optional[str]
+):
+    _store = ZenStore(overwrite_ok=False)
+
+    _store({"a": 1}, name=name1, group=group1)
+    if (name1, group1) == (name2, group2):
+        with pytest.raises(ValueError):
+            _store({"b": 2}, name=name2, group=group2)
+    else:
+        _store({"b": 2}, name=name2, group=group2)
+
+
+@pytest.mark.parametrize("name", "ab")
+@pytest.mark.parametrize("group", [None, "c", "d", "e/f", "e/f/g"])
+@pytest.mark.parametrize("outer", [True, False])
+@pytest.mark.parametrize("inner", [True, False])
+@pytest.mark.usefixtures("clean_store")
+def test_overwrite_ok(outer: bool, inner: bool, name, group):
+    _store = ZenStore(overwrite_ok=outer)
+    _store({}, name=name, group=group)
+    if not outer:
+        with pytest.raises(
+            ValueError,
+            match=re.escape(f"(name={name} group={group}): Store entry already exists"),
+        ):
+            _store({}, name=name, group=group)
+        return
+    _store({}, name=name, group=group)
+    if not inner:
+        with pytest.raises(
+            ValueError,
+            match=re.escape(
+                f"(name={name} group={group}): Hydra config store entry already exists"
+            ),
+        ):
+            _store.add_to_hydra_store(overwrite_ok=inner)
+    else:
+        _store.add_to_hydra_store(overwrite_ok=inner)
+
+
+@pytest.mark.parametrize(
+    "target",
+    [
+        {"a": 1},
+        ["a", "b"],
+        DictConfig({"a": 1}),
+        ListConfig(["a", "b"]),
+        make_config(),
+        make_config()(),
+        builds(dict, a=1),
+        DC,
+        dc,
+        partial(func, a=1, b=1),
+    ],
+)
+@pytest.mark.usefixtures("clean_store")
+def test_default_to_config_produces_instantiable_configs(target):
+    if not HYDRA_SUPPORTS_LIST_INSTANTIATION and isinstance(target, (list, ListConfig)):
+        pytest.xfail("Hydra doesn't support list instantiation")
+    default_store(target, name="target")
+    default_store.add_to_hydra_store()
+    instantiate_from_repo("target")
+
+
+@given(zstore=new_stores())
+def test_self_partialing_reflects_mutable_state(zstore: ZenStore):
+    with clean_store():
+        zstore2 = zstore()
+        zstore3 = zstore(a=22)
+        stores = [zstore2, zstore3]
+
+        for attr in zstore.__slots__:
+            if attr == "_defaults":
+                continue
+
+            for s in stores:
+                assert getattr(zstore, attr) == getattr(s, attr)
+        # assert not zstore._internal_repo
+        zstore(dict(a=1), name="a")
+        zstore2(dict(a=2), name="b")
+        zstore3(dict(a=3), name="c")
+
+        assert zstore == zstore2
+        assert zstore == zstore3
+        assert zstore2 == zstore3
+
+        assert len(zstore._queue) == (3 if zstore._deferred_store else 0)
+        assert len(zstore._internal_repo) == 3
+        for attr in zstore.__slots__:
+            x = getattr(zstore, attr)
+            if not isinstance(x, Hashable):
+                continue
+            assert x is getattr(zstore2, attr)
+            assert x is getattr(zstore3, attr)
+
+
+@given(...)
+def test_stores_have_independent_mutable_state(store1: ZenStore, store2: ZenStore):
+    for attr in store1.__slots__:
+        x = getattr(store1, attr)
+        if isinstance(x, Hashable):
+            continue
+        assert x is not getattr(store2, attr)
+        assert x is not getattr(default_store, attr)  # check default store
+
+
+@pytest.mark.parametrize("name", "ab")
+@pytest.mark.parametrize("group", [None, "c", "d", "e/f", "e/f/g"])
+@pytest.mark.usefixtures("clean_store")
+def test_deferred_to_config(name, group):
+    Store = partial(ZenStore, deferred_hydra_store=True)
+
+    s = Store(deferred_to_config=True)
+    s(1, name=name, group=group, to_config=never_call)
+    with pytest.raises(AssertionError):
+        s.add_to_hydra_store()
+
+    s = Store(deferred_to_config=True)
+    s(1, name=name, group=group, to_config=never_call)
+    with pytest.raises(AssertionError):
+        s[group, name]
+
+    s = ZenStore(deferred_to_config=False)
+    s = s(to_config=never_call)
+    with pytest.raises(AssertionError):
+        s(dict(a=1), name=name, group=group)
+
+
+def test_self_partialing_preserves_subclass():
+    class SubStore(ZenStore):
+        ...
+
+    s1 = SubStore()
+    s2 = s1()
+    assert s1 is not s2
+    assert isinstance(s2, SubStore)
+
+
+@pytest.mark.usefixtures("clean_store")
+def test_default_to_config_validates_dataclass_instance_with_kw():
+    default_store(make_config(a=2)(), name="dc", a=1)
+
+    with pytest.raises(ValueError):
+        default_store[None, "dc"]
+
+
+@pytest.mark.parametrize(
+    "param_name", ["deferred_to_config", "deferred_hydra_store", "overwrite_ok"]
+)
+def test_validate_init(param_name):
+    with pytest.raises(TypeError, match=f"{param_name} must be a bool"):
+        ZenStore(**{param_name: "bad"})
+
+
+def contains(key, store_):
+    assert key in store_ and store_[key]
+
+
+def not_contains(key, store_):
+    assert key not in store_
+    try:
+        v = store_["key"]
+    except KeyError:
+        return
+    assert not v
+
+
+@pytest.mark.usefixtures("clean_store")
+def test_contains_manual():
+    _store = ZenStore()
+    _store({"": 2}, name="grape")
+    _store({"": 1}, group="a/b", name="apple")
+    assert_contains = partial(contains, store_=_store)
+    assert_not_contains = partial(not_contains, store_=_store)
+    assert_contains("a")
+    assert_contains("a/b")
+    assert_not_contains("b")
+    assert_not_contains("b/a")
+    assert_not_contains("c")
+    assert_not_contains("a/c")
+    assert_not_contains("a/b/c")
+    assert_not_contains("a/c/b")
+    assert_contains(("a/b", "apple"))
+    assert_not_contains(("a", "apple"))
+    assert_not_contains(("a/b", "pear"))
+    assert_contains(None)
+    assert_contains((None, "grape"))
+    assert_not_contains((None, "apple"))
+    assert_not_contains(1)
+    assert_not_contains((1, 2))
+    assert_not_contains(("a", "apple", "grape"))
+
+
+@given(...)
+def test_contains_consistent_with_getitem(store: ZenStore):
+    assert "NOTAGROUP" not in store
+    assert "NOTAGROUP/" not in store
+    assert "/NOTAGROUP" not in store
+    for entry in store:
+        group = entry["group"]
+        name = entry["name"]
+        assert group in store
+        assert (group, name) in store
+        assert name not in store
+        assert not store[name]
+
+        assert (name, group) not in store  # type: ignore
+        with pytest.raises(KeyError):
+            store[name, group]  # type: ignore
+
+        if group is None:
+            continue
+
+        group_parts = group.split("/")
+        for n in range(1, len(group_parts) + 1):
+            assert "/".join(group_parts[:n]) in store
+
+
+@given(entries=st.lists(store_entries()), sub_store=st.booleans())
+def test_iter(entries, sub_store):
+    with clean_store():
+        _store = ZenStore()
+        _store_it = _store() if sub_store else _store
+
+        for target, kw in entries:
+            _store_it(target, **kw)
+
+        del _store_it
+
+        iter_out = list(_store)
+        assert len(iter_out) == len(entries)
+
+
+@settings(max_examples=10)
+@given(entries=st.lists(store_entries()), store=new_stores(), sub_store=st.booleans())
+def test_bool(entries, store: ZenStore, sub_store: bool):
+    with clean_store():
+        _store_it = store() if sub_store else store
+
+        for target, kw in entries:
+            _store_it(target, **kw)
+        del _store_it
+
+        assert bool(entries) is bool(store)
+
+
+@settings(max_examples=20)
+@given(...)
+def test_repr(store: ZenStore):
+    assert isinstance(repr(store), str)
+
+
+@given(store=..., num_adds=st.integers(1, 5))
+def test_repeated_add_to_hydra_store_ok(store: ZenStore, num_adds: int):
+    # store should clear internal queue so that multiple add-to-store calls
+    # don't conflict
+    assert bool(store) is store.has_enqueued()
+    with clean_store():
+        for _ in range(num_adds):
+            store.add_to_hydra_store()
+            assert not store.has_enqueued()
+
+
+@given(...)
+def test_store_protects_overwriting_entries_in_hydra_store(store: ZenStore):
+    with clean_store():
+        assume(store and store.has_enqueued())
+        entry, *_ = store
+        note(f"entry: {entry}")
+        note(f"{store._internal_repo}, {store._queue}")
+        cs.store(**entry)
+
+        with pytest.raises(ValueError):
+            store.add_to_hydra_store(overwrite_ok=False)
+        store.add_to_hydra_store(overwrite_ok=True)
+
+
+@settings(max_examples=20)
+@given(...)
+def test_getitem(store: ZenStore):
+    assume(store)
+    with clean_store():
+        for entry in store:
+            assert store[entry["group"], entry["name"]] is entry["node"]
+            assert len(store[entry["group"]]) > 0
+
+
+@settings(max_examples=20)
+@given(...)
+def test_eq(store1: ZenStore, store2: ZenStore):
+    with clean_store():
+        assert store1 != store2
+        assert store1 != 1
+        assert store1 == store1
+        assert store1 == store1(param="blah")
+
+        store1.add_to_hydra_store()
+        store2.add_to_hydra_store()
+        assert store1 != store2
+        assert store1 == store1(param="blah")
+
+
+@settings(max_examples=20)
+@given(...)
+def test_get_entry(store: ZenStore):
+    assume(store)
+    entry, *_ = store
+    entry_ = store.get_entry(entry["group"], name=entry["name"])
+    assert entry == entry_
+
+
+@settings(max_examples=20)
+@given(store=...)
+@pytest.mark.parametrize(
+    "getter",
+    [
+        lambda store, *_: next(iter(store)),
+        lambda store, group, name: store.get_entry(group, name),
+    ],
+)
+def test_entry_access_cannot_mutate_store(store: ZenStore, getter):
+    assume(store)
+    entries = tuple(d.copy() for d in store)
+
+    entry = getter(store, entries[0]["group"], entries[0]["name"])
+    entry["name"] = 2222
+    new_entries = tuple(d.copy() for d in store)
+    assert all(e in new_entries for e in entries)