Skip to content

Commit

Permalink
Type Hints, Lazy Evaluation (Inheritance & Dependencies), New Top Lev…
Browse files Browse the repository at this point in the history
…el Interfaces (#205)

* adds some stubs to the underlying decorator that should help with type hinting in VSCode (via pylance/pyright). changed packaging to reflect the stubs and partial type info. added in two forms of lazy evaluation: (1) the @spock decorator now has a boolean flag  that allows inherited classes to not be @spock decorated -- will automatically cast parent classes to a spock class by traversing the MRO, (2) the ConfigArgBuilder now takes a  keyword argument that will attempt to lazily handle dependencies between @spock decorated classes thus preventing the need for every @spock decorated classes to be passed into *args within the  method

* fixing spockTuner class based on new underlying functions. adding in stubs for type hinting

* linted

* added more tests that should cover new graph and inheritance laziness

* updated docs for lazy evaluation. added in method that lazily finds parents and adds them to the input_class list if  within the underlying spock module
  • Loading branch information
ncilfone committed Jan 20, 2022
1 parent 5b097d1 commit 32448c2
Show file tree
Hide file tree
Showing 44 changed files with 728 additions and 178 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include versioneer.py
include spock/_version.py
include "README.md", "LICENSE.txt", "NOTICE.txt", "CONTRIBUTING.md"
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ hierarchical configuration by composition.
defined within a `@spock` decorated class. Supports required/optional and automatic defaults.
* Easily Managed Parameter Groups: Each class automatically generates its own object within a single namespace.
* [Parameter Inheritance](https://fidelity.github.io/spock/docs/advanced_features/Inheritance/): Classes support
inheritance allowing for complex configurations derived from a common base set of parameters.
inheritance (w/ lazy evaluation of inheritance/dependencies) allowing for complex configurations derived from
a common base set of parameters.
* [Complex Types](https://fidelity.github.io/spock/docs/advanced_features/Advanced-Types/): Nested Lists/Tuples,
List/Tuples of Enum of `@spock` classes, List of repeated `@spock` classes
* Multiple Configuration File Types: Configurations are specified from YAML, TOML, or JSON files.
Expand Down Expand Up @@ -66,17 +67,20 @@ Example `spock` usage is located [here](https://github.com/fidelity/spock/blob/m

See [Releases](https://github.com/fidelity/spock/releases) for more information.

#### January 18th, 2022
* Support for lazy evaluation: (1) inherited classes do not need to be `@spock` decorated, (2) dependencies/references
between `spock` classes can be lazily handled thus preventing the need for every `@spock` decorated classes to be
passed into `*args` within the main `SpockBuilder` API
* Updated main API interface for better top-level imports (backwards compatible): `ConfigArgBuilder`->`spockBuilder`
* Added stubs to the underlying decorator that should help with type hinting in VSCode (pylance/pyright)

#### December 14, 2021
* Refactored the backend to better handle nested dependencies (and for clarity)
* Refactored the docs to use Docusaurus

#### August 17, 2021
* Added hyper-parameter tuning backend support for Ax via Service API

#### July 21, 2021
* Added hyper-parameter tuning support with `pip install spock-config[tune]`
* Hyper-parameter tuning backend support for Optuna define-and-run API (WIP for Ax)

## Original Implementation

[Nicholas Cilfone](https://github.com/ncilfone), [Siddharth Narayanan](https://github.com/sidnarayanan)
Expand Down
6 changes: 3 additions & 3 deletions examples/quick-start/simple.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List

from spock.builder import ConfigArgBuilder
from spock.config import spock
from spock import SpockBuilder
from spock import spock


@spock
Expand Down Expand Up @@ -47,7 +47,7 @@ def add_by_parameter(multiply_param, list_vals, add_param, tf_round):

def main():
# Chain the generate function to the class call
config = ConfigArgBuilder(BasicConfig, desc="Quick start example").generate()
config = SpockBuilder(BasicConfig, desc="Quick start example").generate()
# One can now access the Spock config object by class name with the returned namespace
print(config.BasicConfig.parameter)
# And pass the namespace to our first function
Expand Down
6 changes: 3 additions & 3 deletions examples/tune/ax/tune.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
RangeHyperParameter,
spockTuner,
)
from spock.builder import ConfigArgBuilder
from spock.config import spock
from spock import SpockBuilder
from spock import spock


@spock
Expand Down Expand Up @@ -46,7 +46,7 @@ def main():
# Use the builder to setup
# Call tuner to indicate that we are going to do some HP tuning -- passing in an ax study object
attrs_obj = (
ConfigArgBuilder(
SpockBuilder(
LogisticRegressionHP,
BasicParams,
desc="Example Logistic Regression Hyper-Parameter Tuning -- Ax Backend",
Expand Down
6 changes: 3 additions & 3 deletions examples/tune/optuna/tune.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
RangeHyperParameter,
spockTuner,
)
from spock.builder import ConfigArgBuilder
from spock.config import spock
from spock import SpockBuilder
from spock import spock


@spock
Expand Down Expand Up @@ -48,7 +48,7 @@ def main():
# Use the builder to setup
# Call tuner to indicate that we are going to do some HP tuning -- passing in an optuna study object
attrs_obj = (
ConfigArgBuilder(
SpockBuilder(
LogisticRegressionHP,
BasicParams,
desc="Example Logistic Regression Hyper-Parameter Tuning -- Optuna Backend",
Expand Down
8 changes: 3 additions & 5 deletions examples/tutorial/advanced/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import torch
from basic_nn import BasicNet

from spock.args import SavePath
from spock.builder import ConfigArgBuilder
from spock.config import spock
from spock import SpockBuilder
from spock import spock


class Activation(Enum):
Expand All @@ -22,7 +21,6 @@ class Optimizer(Enum):

@spock
class ModelConfig:
save_path: SavePath
n_features: int
dropout: Optional[List[float]]
hidden_sizes: Tuple[int, int, int] = (32, 32, 32)
Expand Down Expand Up @@ -90,7 +88,7 @@ def main():
# A simple description
description = "spock Advanced Tutorial"
# Build out the parser by passing in Spock config objects as *args after description
config = ConfigArgBuilder(
config = SpockBuilder(
ModelConfig, DataConfig, SGDConfig, desc=description
).generate()
# Instantiate our neural net using
Expand Down
8 changes: 3 additions & 5 deletions examples/tutorial/basic/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import torch
from basic_nn import BasicNet

from spock.args import SavePath
from spock.builder import ConfigArgBuilder
from spock.config import spock
from spock import SpockBuilder
from spock import spock


class Activation(Enum):
Expand Down Expand Up @@ -35,7 +34,6 @@ class ModelConfig:
activation: choice from the Activation enum of the activation function to use
"""

save_path: SavePath
n_features: int
dropout: List[float]
hidden_sizes: Tuple[int, int, int]
Expand All @@ -47,7 +45,7 @@ def main():
description = "spock Basic Tutorial"
# Build out the parser by passing in Spock config objects as *args after description
config = (
ConfigArgBuilder(ModelConfig, desc=description, create_save_path=True)
SpockBuilder(ModelConfig, desc=description, create_save_path=True)
.save(file_extension=".toml")
.generate()
)
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
packages=setuptools.find_packages(
exclude=["*.tests", "*.tests.*", "tests.*", "tests"]
),
package_data={
"spock": ["py.typed", "*.pyi"],
},
include_package_data=True,
python_requires=">=3.6",
install_requires=install_reqs,
extras_require={"s3": s3_reqs, "tune": tune_reqs},
Expand Down
6 changes: 5 additions & 1 deletion spock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
"""

from spock._version import get_versions
from spock.builder import ConfigArgBuilder
from spock.config import spock, spock_attr

__all__ = ["args", "builder", "config"]
SpockBuilder = ConfigArgBuilder

__all__ = ["args", "builder", "config", "spock", "spock_attr", "SpockBuilder"]

__version__ = get_versions()["version"]
del get_versions
60 changes: 51 additions & 9 deletions spock/addons/tune/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,72 @@ class OptunaTunerConfig:
directions: Optional[Sequence[Union[str, optuna.study.StudyDirection]]] = None


def _spock_tune(cls):
"""Ovverides basic spock_attr decorator with another name
Using a different name allows spock to easily determine which parameters are normal and which are
meant to be used in a hyper-parameter tuning backend
def _process_class(cls, kw_only: bool, make_init: bool, dynamic: bool):
"""Process a given class
Args:
cls: basic class def
cls: basic class definition
kw_only: set kwarg only
make_init: make an init function
dynamic: allows inherited classes to not be @spock decorated
Returns:
cls: slotted attrs class that is frozen and kw only
cls with attrs dunder methods added
"""
bases, attrs_dict = _base_attr(cls)
# Handles the MRO and gets old annotations
bases, attrs_dict, merged_annotations = _base_attr(cls, kw_only, make_init, dynamic)
# Dynamically make an attr class
obj = attr.make_class(
name=cls.__name__, bases=bases, attrs=attrs_dict, kw_only=True, frozen=True
name=cls.__name__,
bases=bases,
attrs=attrs_dict,
kw_only=kw_only,
frozen=True,
auto_attribs=True,
init=make_init,
)
# For each class we dynamically create we need to register it within the system modules for pickle to work
setattr(sys.modules["spock"].addons.tune.config, obj.__name__, obj)
# Swap the __doc__ string from cls to obj
obj.__doc__ = cls.__doc__
# Set the __init__ function
# Handle __annotations__ from the MRO
obj.__annotations__ = merged_annotations
return obj


def _spock_tune(
maybe_cls=None,
kw_only=True,
make_init=True,
):
"""Ovverides basic spock_attr decorator with another name
Using a different name allows spock to easily determine which parameters are normal and which are
meant to be used in a hyper-parameter tuning backend
Args:
maybe_cls: maybe a basic class def maybe None depending on call type
kw_only: Make all attributes keyword-only
make_init: bool, define a __init__() method
Returns:
cls: attrs class that is frozen and kw only
"""

def wrap(cls):
return _process_class(cls, kw_only=kw_only, make_init=make_init, dynamic=False)

# Note: Taken from dataclass/attr definition(s)
# maybe_cls's type depends on the usage of the decorator. It's a class
# if it's used as `@spockTuner` but ``None`` if used as `@spockTuner()`.
if maybe_cls is None:
return wrap
else:
return wrap(maybe_cls)


# Make the alias for the decorator
spockTuner = _spock_tune

Expand Down
39 changes: 39 additions & 0 deletions spock/addons/tune/config.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Any, Callable, Tuple, TypeVar, Union, overload

from attr import attrib, field

_T = TypeVar("_T")
_C = TypeVar("_C", bound=type)

# Note: from here
# https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.pyi

# Static type inference support via __dataclass_transform__ implemented as per:
# https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md
# This annotation must be applied to all overloads of "spock_attr"

# NOTE: This is a typing construct and does not exist at runtime. Extensions
# wrapping attrs decorators should declare a separate __dataclass_transform__
# signature in the extension module using the specification linked above to
# provide pyright support -- this currently doesn't work in PyCharm
def __dataclass_transform__(
*,
eq_default: bool = True,
order_default: bool = False,
kw_only_default: bool = False,
field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
) -> Callable[[_T], _T]: ...
@overload
@__dataclass_transform__(kw_only_default=True, field_descriptors=(attrib, field))
def _spock_tune(
maybe_cls: _C,
kw_only: bool = True,
make_init: bool = True,
) -> _C: ...
@overload
@__dataclass_transform__(kw_only_default=True, field_descriptors=(attrib, field))
def _spock_tune(
maybe_cls: None = ...,
kw_only: bool = True,
make_init: bool = True,
) -> Callable[[_C], _C]: ...
13 changes: 9 additions & 4 deletions spock/backend/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ class BaseBuilder(ABC): # pylint: disable=too-few-public-methods
_max_indent: maximum to indent between help prints
_module_name: module name to register in the spock module space
save_path: list of path(s) to save the configs to
_lazy: attempts to lazily find @spock decorated classes registered within sys.modules["spock"].backend.config
"""

def __init__(self, *args, max_indent: int = 4, module_name: str, **kwargs):
def __init__(
self, *args, max_indent: int = 4, module_name: str, lazy: bool, **kwargs
):
"""Init call for BaseBuilder
Args:
Expand All @@ -46,7 +49,10 @@ def __init__(self, *args, max_indent: int = 4, module_name: str, **kwargs):
**kwargs: keyword args
"""
self._input_classes = args
self._graph = Graph(input_classes=self.input_classes)
self._lazy = lazy
self._graph = Graph(input_classes=self.input_classes, lazy=self._lazy)
# Make sure the input classes are updated -- lazy evaluation
self._input_classes = self._graph.nodes
self._module_name = module_name
self._max_indent = max_indent
self.save_path = None
Expand Down Expand Up @@ -103,8 +109,7 @@ def generate(self, dict_args):
Returns:
namespace containing automatically generated instances of the classes
"""
graph = Graph(input_classes=self.input_classes)
spock_space_kwargs = self.resolve_spock_space_kwargs(graph, dict_args)
spock_space_kwargs = self.resolve_spock_space_kwargs(self._graph, dict_args)
return Spockspace(**spock_space_kwargs)

def resolve_spock_space_kwargs(self, graph: Graph, dict_args: dict) -> dict:
Expand Down
Loading

0 comments on commit 32448c2

Please sign in to comment.