From 1e028b5d10bade267d4d4b34f6ad847219c6d101 Mon Sep 17 00:00:00 2001 From: Nicholas Cilfone Date: Fri, 14 Jan 2022 15:25:20 -0500 Subject: [PATCH 1/6] 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 --- MANIFEST.in | 1 + setup.py | 4 + spock/__init__.py | 13 ++- spock/backend/builder.py | 11 ++- spock/backend/config.py | 125 +++++++++++++++++++++----- spock/backend/config.pyi | 52 +++++++++++ spock/backend/typed.py | 2 + spock/builder.py | 11 ++- spock/exceptions.py | 5 ++ spock/graph.py | 79 +++++++++++++--- spock/handlers.py | 4 +- spock/py.typed | 1 + tests/base/attr_configs_test.py | 17 +++- tests/base/base_asserts_test.py | 9 ++ tests/base/test_config_arg_builder.py | 12 +++ 15 files changed, 302 insertions(+), 44 deletions(-) create mode 100644 spock/backend/config.pyi create mode 100644 spock/exceptions.py create mode 100644 spock/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index a725ecf0..417570b0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include versioneer.py include spock/_version.py +include "README.md", "LICENSE.txt", "NOTICE.txt", "CONTRIBUTING.md" \ No newline at end of file diff --git a/setup.py b/setup.py index ee2e2ce0..a1413300 100644 --- a/setup.py +++ b/setup.py @@ -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}, diff --git a/spock/__init__.py b/spock/__init__.py index 46d8e091..dd87faea 100644 --- a/spock/__init__.py +++ b/spock/__init__.py @@ -10,8 +10,19 @@ """ 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 diff --git a/spock/backend/builder.py b/spock/backend/builder.py index caf56b1a..699d0953 100644 --- a/spock/backend/builder.py +++ b/spock/backend/builder.py @@ -33,10 +33,11 @@ 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: @@ -46,7 +47,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 @@ -103,8 +107,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: diff --git a/spock/backend/config.py b/spock/backend/config.py index 6e85fe87..c3c9665a 100644 --- a/spock/backend/config.py +++ b/spock/backend/config.py @@ -10,60 +10,137 @@ import attr from spock.backend.typed import katra +from spock.utils import _is_spock_instance +from spock.exceptions import _SpockUndecoratedClass -def _base_attr(cls): +def _base_attr(cls, kw_only: bool, make_init: bool, dynamic: bool): """Map type hints to katras Connector function that maps type hinting style to the defined katra style which uses the more strict attr.ib() definition + Handles dynamic decorators which allows for inheritance of non @spock decorated classes + Args: cls: basic class def + dynamic: allows inherited classes to not be @spock decorated Returns: - cls: slotted attrs class that is frozen and kw only + cls: base spock classes derived from the MRO + attrs_dict: the current dictionary of attr.attribute values + merged_annotations: dictionary of type annotations + """ # Since we are not using the @attr.s decorator we need to get the parent classes for inheritance # We do this by using the mro and grabbing anything that is not the first and last indices in the list and wrapping # it into a tuple + bases = () + base_annotation = {} + base_defaults = {} if len(cls.__mro__[1:-1]) > 0: - bases = tuple(cls.__mro__[1:-1]) - # if there are not parents pass a blank tuple + # Get bases minus self and python class object + bases = list(cls.__mro__[1:-1]) + for idx, base_cls in enumerate(bases): + if not _is_spock_instance(base_cls) and not dynamic: + raise _SpockUndecoratedClass( + f"Class `{base_cls.__name__}` was not decorated with the @spock decorator " + f"and `dynamic={dynamic}` was set for child class `{cls.__name__}`") + elif not _is_spock_instance(base_cls) and dynamic: + bases[idx] = _process_class(base_cls, kw_only, make_init, dynamic) + bases = tuple(bases) + base_annotation = {} + for val in bases: + for attribute in val.__attrs_attrs__: + # Since we are moving left to right via the MRO only update if not currently present + # this maintains parity to how the MRO is handled in base python + if attribute.name not in base_annotation: + if 'type' in attribute.metadata: + base_annotation.update({attribute.name: attribute.metadata['og_type']}) + else: + base_annotation.update({attribute.name: attribute.type}) + base_defaults = {attribute.name: attribute.default for val in bases for attribute in val.__attrs_attrs__ if attribute.default is not (None or attr.NOTHING)} + # Merge the annotations -- always override as this is the lowest level of the MRO + if hasattr(cls, '__annotations__'): + new_annotations = {k: v for k, v in cls.__annotations__.items()} else: - bases = () + new_annotations = {} + merged_annotations = {**base_annotation, **new_annotations} + # Make a blank attrs dict for new attrs attrs_dict = {} - if hasattr(cls, "__annotations__"): - for k, v in cls.__annotations__.items(): - # If the cls has the attribute then a default was set - if hasattr(cls, k): - default = getattr(cls, k) - else: - default = None - attrs_dict.update({k: katra(typed=v, default=default)}) - return bases, attrs_dict - - -def spock_attr(cls): - """Map type hints to katras - Connector function that maps type hinting style to the defined katra style which uses the more strict - attr.ib() definition + for k, v in merged_annotations.items(): + # If the cls has the attribute then a default was set + if hasattr(cls, k): + default = getattr(cls, k) + elif k in base_defaults: + default = base_defaults[k] + else: + default = None + attrs_dict.update({k: katra(typed=v, default=default)}) + return bases, attrs_dict, merged_annotations + + +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"].backend.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_attr( + maybe_cls=None, + kw_only=True, + make_init=True, + dynamic=False, +): + """Map type hints to katras + + Connector function that maps type hinting style to the defined katra style which uses the more strict + attr.ib() definition -- this allows us to attach the correct validators for types before the attrs class is + built + + 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 + dynamic: allows inherited classes to not be @spock decorated -- will automatically cast parent classes to a + spock class by traversing the MRO + + 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=dynamic) + # 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 `@spock` but ``None`` if used as `@spock()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) diff --git a/spock/backend/config.pyi b/spock/backend/config.pyi new file mode 100644 index 00000000..c11151e5 --- /dev/null +++ b/spock/backend/config.pyi @@ -0,0 +1,52 @@ +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_attr( + maybe_cls: _C, + kw_only: bool = True, + make_init: bool = True, + dynamic: bool = False, +) -> _C: ... + + +@overload +@__dataclass_transform__(kw_only_default=True, field_descriptors=(attrib, field)) +def spock_attr( + maybe_cls: None = ..., + kw_only: bool = True, + make_init: bool = True, + dynamic: bool = False, +) -> Callable[[_C], _C]: ... diff --git a/spock/backend/typed.py b/spock/backend/typed.py index 135ad091..7d4c2e77 100644 --- a/spock/backend/typed.py +++ b/spock/backend/typed.py @@ -463,4 +463,6 @@ def katra(typed, default=None): x = _enum_katra(typed=typed, default=default, optional=optional) else: x = _type_katra(typed=typed, default=default, optional=optional) + # Add back in the OG type + x.metadata.update({'og_type': typed}) return x diff --git a/spock/builder.py b/spock/builder.py index e88fd0e8..cc8b299d 100644 --- a/spock/builder.py +++ b/spock/builder.py @@ -42,6 +42,11 @@ class ConfigArgBuilder: _tune_namespace: namespace that hold the generated tuner related parameters _sample_count: current call to the sample function _fixed_uuid: fixed uuid to write the best file to the same path + _configs = configs if configs is None else [Path(c) for c in configs] + _lazy: flag to lazily find @spock decorated classes registered within sys.modules["spock"].backend.config + thus alleviating the need to pass all @spock decorated classes to *args + _no_cmd_line: turn off cmd line args + _desc: description for help """ @@ -50,6 +55,7 @@ def __init__( *args, configs: typing.Optional[typing.List] = None, desc: str = "", + lazy: bool = False, no_cmd_line: bool = False, s3_config=None, **kwargs, @@ -60,6 +66,8 @@ def __init__( *args: tuple of spock decorated classes to process configs: list of config paths desc: description for help + lazy: attempts to lazily find @spock decorated classes registered within sys.modules["spock"].backend.config + thus alleviating the need to pass all @spock decorated classes to *args no_cmd_line: turn off cmd line args s3_config: s3Config object for S3 support **kwargs: keyword args @@ -68,6 +76,7 @@ def __init__( # Do some verification first self._verify_attr(args) self._configs = configs if configs is None else [Path(c) for c in configs] + self._lazy = lazy self._no_cmd_line = no_cmd_line self._desc = desc # Build the payload and saver objects @@ -76,7 +85,7 @@ def __init__( # Split the fixed parameters from the tuneable ones (if present) fixed_args, tune_args = self._strip_tune_parameters(args) # The fixed parameter builder - self._builder_obj = AttrBuilder(*fixed_args, **kwargs) + self._builder_obj = AttrBuilder(*fixed_args, lazy=lazy, **kwargs) # The possible tunable parameter builder -- might return None self._tune_obj, self._tune_payload_obj = self._handle_tuner_objects( tune_args, s3_config, kwargs diff --git a/spock/exceptions.py b/spock/exceptions.py new file mode 100644 index 00000000..ff442a23 --- /dev/null +++ b/spock/exceptions.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +class _SpockUndecoratedClass(Exception): + """Custom exception type for non spock decorated classes and not dynamic""" + + pass diff --git a/spock/graph.py b/spock/graph.py index 78339cd7..8fbd6ccb 100644 --- a/spock/graph.py +++ b/spock/graph.py @@ -7,7 +7,8 @@ from typing import Type -import attr +import sys +from warnings import warn from spock.utils import _find_all_spock_classes @@ -18,17 +19,23 @@ class Graph: Attributes: _input_classes: list of input classes that link to a backend _dag: graph of the dependencies between spock classes + _lazy: attempts to lazily find @spock decorated classes registered within sys.modules["spock"].backend.config """ - def __init__(self, input_classes): + def __init__(self, input_classes, lazy: bool): """Init call for Graph class Args: input_classes: list of input classes that link to a backend + lazy: attempts to lazily find @spock decorated classes registered within sys.modules["spock"].backend.config """ self._input_classes = input_classes - # Build + self._lazy = lazy + # Maybe find classes lazily -- roll them into the input class tuple + if self._lazy: + self._input_classes = (*self._input_classes, *self._lazily_find_classes(self._input_classes)) + # Build -- post lazy eval self._dag = self._build() # Validate (No cycles in DAG) if self._has_cycles() is True: @@ -61,6 +68,56 @@ def roots(self): """Returns the roots of the dependency graph""" return [self.node_map[k] for k, v in self.dag.items() if len(v) == 0] + @staticmethod + def _yield_class_deps(classes): + """Generator to iterate through nodes and find dependencies + + Args: + classes: list of classes to iterate through + + Yields: + tuple or the base input class and the current name of the dependent class + + """ + for input_class in classes: + dep_names = {f"{v.__name__}" for v in _find_all_spock_classes(input_class)} + for v in dep_names: + yield input_class, v + + def _lazily_find_classes(self, classes): + """Searches within the spock sys modules attributes to lazily find @spock decorated classes + + These classes have been decorated with @spock but might not have been passes into the ConfigArgBuilder so + this allows for 'lazy' lookup of these classes to make the call to ConfigArgBuilder a little less verbose + when there are a lot of spock classes + + Returns: + tuple of any lazily discovered classes + + """ + # Iterate thorough all of the base spock classes to get the dependencies and reverse dependencies + lazy_classes = [] + for _, v in self._yield_class_deps(classes): + if hasattr(sys.modules["spock"].backend.config, v): + warn(f"Lazy evaluation found a @spock decorated class named `{v}` within the registered types of " + f"sys.modules['spock'].backend.config -- Attempting to use the class " + f"`{getattr(sys.modules['spock'].backend.config, v)}`...") + # Get the lazily discovered class + lazy_class = getattr(sys.modules["spock"].backend.config, v) + # Recursive check the lazy class for other lazy classes + dependent_lazy_classes = self._lazily_find_classes([lazy_class]) + # extend the list if the recursive check finds any other lazy classes + if len(dependent_lazy_classes) > 0: + lazy_classes.extend(dependent_lazy_classes) + lazy_classes.append(lazy_class) + else: + raise ValueError( + f"Missing @spock decorated class -- `{v}` was not passed as an *arg to " + f"ConfigArgBuilder and lazy evaluation could not find it within " + f"sys.modules['spock'].backend.config" + ) + return tuple(lazy_classes) + def _build(self): """Builds a dictionary of nodes and their edges (essentially builds the DAG) @@ -71,15 +128,13 @@ def _build(self): # Build a dictionary of all nodes (base spock classes) nodes = {val: [] for val in self.node_names} # Iterate thorough all of the base spock classes to get the dependencies and reverse dependencies - for input_class in self.nodes: - dep_names = {f"{v.__name__}" for v in _find_all_spock_classes(input_class)} - for v in dep_names: - if v not in self.node_names: - raise ValueError( - f"Missing @spock decorated class -- `{v}` was not passed as an *arg to " - f"ConfigArgBuilder" - ) - nodes.get(v).append(input_class) + for input_class, v in self._yield_class_deps(self._input_classes): + if v not in self.node_names: + raise ValueError( + f"Missing @spock decorated class -- `{v}` was not passed as an *arg to " + f"ConfigArgBuilder and/or could not be found via lazy evaluation" + ) + nodes.get(v).append(input_class) nodes = {key: set(val) for key, val in nodes.items()} return nodes diff --git a/spock/handlers.py b/spock/handlers.py index 9c458326..c367307e 100644 --- a/spock/handlers.py +++ b/spock/handlers.py @@ -16,9 +16,11 @@ import pytomlpp import yaml -from spock import __version__ +from spock._version import get_versions from spock.utils import check_path_s3, path_object_to_s3path +__version__ = get_versions()["version"] + class Handler(ABC): """Base class for file type loaders diff --git a/spock/py.typed b/spock/py.typed new file mode 100644 index 00000000..5fcb8520 --- /dev/null +++ b/spock/py.typed @@ -0,0 +1 @@ +partial \ No newline at end of file diff --git a/tests/base/attr_configs_test.py b/tests/base/attr_configs_test.py index 0a1cebf2..43a78d59 100644 --- a/tests/base/attr_configs_test.py +++ b/tests/base/attr_configs_test.py @@ -304,10 +304,25 @@ class TypeDefaultOptConfig: @spock +# class TypeInherited(TypeDefaultOptConfig, TypeConfig): class TypeInherited(TypeConfig, TypeDefaultOptConfig): """This tests inheritance with mixed default and non-default arguments""" + pass - ... + +class Foo: + p: int = 1 + + +class Bar: + q: str = 'shhh' + + +@spock(dynamic=True) +class TestConfigDynamicDefaults(Foo, Bar): + x: int = 235 + y: str = 'yarghhh' + z: List[int] = [10, 20] all_configs = [ diff --git a/tests/base/base_asserts_test.py b/tests/base/base_asserts_test.py index 8f1c3dd9..f21dc6e1 100644 --- a/tests/base/base_asserts_test.py +++ b/tests/base/base_asserts_test.py @@ -220,3 +220,12 @@ def test_all_inherited(self, arg_builder): assert arg_builder.TypeInherited.tuple_p_opt_def_int == (10, 20) assert arg_builder.TypeInherited.tuple_p_opt_def_str == ("Spock", "Package") assert arg_builder.TypeInherited.tuple_p_opt_def_bool == (True, False) + + +class AllDynamic: + def test_all_dynamic(self, arg_builder): + assert arg_builder.TestConfigDynamicDefaults.x == 235 + assert arg_builder.TestConfigDynamicDefaults.y == "yarghhh" + assert arg_builder.TestConfigDynamicDefaults.z == [10, 20] + assert arg_builder.TestConfigDynamicDefaults.p == 1 + assert arg_builder.TestConfigDynamicDefaults.q == 'shhh' \ No newline at end of file diff --git a/tests/base/test_config_arg_builder.py b/tests/base/test_config_arg_builder.py index aeeab037..788a41a5 100644 --- a/tests/base/test_config_arg_builder.py +++ b/tests/base/test_config_arg_builder.py @@ -194,3 +194,15 @@ def test_class_unknown(self, monkeypatch): ConfigArgBuilder( *all_configs, desc="Test Builder" ) + + +class TestDynamic(AllDynamic): + """Testing basic functionality""" + + @staticmethod + @pytest.fixture + def arg_builder(monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", [""]) + config = ConfigArgBuilder(TestConfigDynamicDefaults) + return config.generate() From 7c760ad2f45e8caf3918bfd466aee27a1807892c Mon Sep 17 00:00:00 2001 From: Nicholas Cilfone Date: Fri, 14 Jan 2022 15:30:23 -0500 Subject: [PATCH 2/6] linted --- spock/__init__.py | 9 +-------- spock/backend/builder.py | 4 +++- spock/backend/config.py | 42 +++++++++++++++++++++++++++------------- spock/backend/config.pyi | 29 +++++++++------------------ spock/backend/typed.py | 2 +- spock/graph.py | 16 +++++++++------ 6 files changed, 53 insertions(+), 49 deletions(-) diff --git a/spock/__init__.py b/spock/__init__.py index dd87faea..4c7cfe78 100644 --- a/spock/__init__.py +++ b/spock/__init__.py @@ -15,14 +15,7 @@ SpockBuilder = ConfigArgBuilder -__all__ = [ - "args", - "builder", - "config", - "spock", - "spock_attr", - "SpockBuilder" -] +__all__ = ["args", "builder", "config", "spock", "spock_attr", "SpockBuilder"] __version__ = get_versions()["version"] del get_versions diff --git a/spock/backend/builder.py b/spock/backend/builder.py index 699d0953..e38db2f0 100644 --- a/spock/backend/builder.py +++ b/spock/backend/builder.py @@ -37,7 +37,9 @@ class BaseBuilder(ABC): # pylint: disable=too-few-public-methods """ - def __init__(self, *args, max_indent: int = 4, module_name: str, lazy: bool, **kwargs): + def __init__( + self, *args, max_indent: int = 4, module_name: str, lazy: bool, **kwargs + ): """Init call for BaseBuilder Args: diff --git a/spock/backend/config.py b/spock/backend/config.py index c3c9665a..dfd0a195 100644 --- a/spock/backend/config.py +++ b/spock/backend/config.py @@ -10,8 +10,8 @@ import attr from spock.backend.typed import katra -from spock.utils import _is_spock_instance from spock.exceptions import _SpockUndecoratedClass +from spock.utils import _is_spock_instance def _base_attr(cls, kw_only: bool, make_init: bool, dynamic: bool): @@ -45,7 +45,8 @@ def _base_attr(cls, kw_only: bool, make_init: bool, dynamic: bool): if not _is_spock_instance(base_cls) and not dynamic: raise _SpockUndecoratedClass( f"Class `{base_cls.__name__}` was not decorated with the @spock decorator " - f"and `dynamic={dynamic}` was set for child class `{cls.__name__}`") + f"and `dynamic={dynamic}` was set for child class `{cls.__name__}`" + ) elif not _is_spock_instance(base_cls) and dynamic: bases[idx] = _process_class(base_cls, kw_only, make_init, dynamic) bases = tuple(bases) @@ -55,13 +56,20 @@ def _base_attr(cls, kw_only: bool, make_init: bool, dynamic: bool): # Since we are moving left to right via the MRO only update if not currently present # this maintains parity to how the MRO is handled in base python if attribute.name not in base_annotation: - if 'type' in attribute.metadata: - base_annotation.update({attribute.name: attribute.metadata['og_type']}) + if "type" in attribute.metadata: + base_annotation.update( + {attribute.name: attribute.metadata["og_type"]} + ) else: base_annotation.update({attribute.name: attribute.type}) - base_defaults = {attribute.name: attribute.default for val in bases for attribute in val.__attrs_attrs__ if attribute.default is not (None or attr.NOTHING)} + base_defaults = { + attribute.name: attribute.default + for val in bases + for attribute in val.__attrs_attrs__ + if attribute.default is not (None or attr.NOTHING) + } # Merge the annotations -- always override as this is the lowest level of the MRO - if hasattr(cls, '__annotations__'): + if hasattr(cls, "__annotations__"): new_annotations = {k: v for k, v in cls.__annotations__.items()} else: new_annotations = {} @@ -99,8 +107,13 @@ def _process_class(cls, kw_only: bool, make_init: bool, dynamic: bool): 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=kw_only, frozen=True, auto_attribs=True, - init=make_init + 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"].backend.config, obj.__name__, obj) @@ -113,10 +126,10 @@ def _process_class(cls, kw_only: bool, make_init: bool, dynamic: bool): def spock_attr( - maybe_cls=None, - kw_only=True, - make_init=True, - dynamic=False, + maybe_cls=None, + kw_only=True, + make_init=True, + dynamic=False, ): """Map type hints to katras @@ -136,7 +149,10 @@ def spock_attr( """ def wrap(cls): - return _process_class(cls, kw_only=kw_only, make_init=make_init, dynamic=dynamic) + return _process_class( + cls, kw_only=kw_only, make_init=make_init, dynamic=dynamic + ) + # 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 `@spock` but ``None`` if used as `@spock()`. diff --git a/spock/backend/config.pyi b/spock/backend/config.pyi index c11151e5..5ccc44bc 100644 --- a/spock/backend/config.pyi +++ b/spock/backend/config.pyi @@ -1,11 +1,4 @@ -from typing import ( - Any, - Callable, - Tuple, - TypeVar, - Union, - overload, -) +from typing import Any, Callable, Tuple, TypeVar, Union, overload from attr import attrib, field @@ -30,23 +23,19 @@ def __dataclass_transform__( 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_attr( - maybe_cls: _C, - kw_only: bool = True, - make_init: bool = True, - dynamic: bool = False, + maybe_cls: _C, + kw_only: bool = True, + make_init: bool = True, + dynamic: bool = False, ) -> _C: ... - - @overload @__dataclass_transform__(kw_only_default=True, field_descriptors=(attrib, field)) def spock_attr( - maybe_cls: None = ..., - kw_only: bool = True, - make_init: bool = True, - dynamic: bool = False, + maybe_cls: None = ..., + kw_only: bool = True, + make_init: bool = True, + dynamic: bool = False, ) -> Callable[[_C], _C]: ... diff --git a/spock/backend/typed.py b/spock/backend/typed.py index 7d4c2e77..fef68de2 100644 --- a/spock/backend/typed.py +++ b/spock/backend/typed.py @@ -464,5 +464,5 @@ def katra(typed, default=None): else: x = _type_katra(typed=typed, default=default, optional=optional) # Add back in the OG type - x.metadata.update({'og_type': typed}) + x.metadata.update({"og_type": typed}) return x diff --git a/spock/graph.py b/spock/graph.py index 8fbd6ccb..2e4a5421 100644 --- a/spock/graph.py +++ b/spock/graph.py @@ -5,9 +5,8 @@ """Handles creation and ops for DAGs""" -from typing import Type - import sys +from typing import Type from warnings import warn from spock.utils import _find_all_spock_classes @@ -34,7 +33,10 @@ def __init__(self, input_classes, lazy: bool): self._lazy = lazy # Maybe find classes lazily -- roll them into the input class tuple if self._lazy: - self._input_classes = (*self._input_classes, *self._lazily_find_classes(self._input_classes)) + self._input_classes = ( + *self._input_classes, + *self._lazily_find_classes(self._input_classes), + ) # Build -- post lazy eval self._dag = self._build() # Validate (No cycles in DAG) @@ -99,9 +101,11 @@ def _lazily_find_classes(self, classes): lazy_classes = [] for _, v in self._yield_class_deps(classes): if hasattr(sys.modules["spock"].backend.config, v): - warn(f"Lazy evaluation found a @spock decorated class named `{v}` within the registered types of " - f"sys.modules['spock'].backend.config -- Attempting to use the class " - f"`{getattr(sys.modules['spock'].backend.config, v)}`...") + warn( + f"Lazy evaluation found a @spock decorated class named `{v}` within the registered types of " + f"sys.modules['spock'].backend.config -- Attempting to use the class " + f"`{getattr(sys.modules['spock'].backend.config, v)}`..." + ) # Get the lazily discovered class lazy_class = getattr(sys.modules["spock"].backend.config, v) # Recursive check the lazy class for other lazy classes From 8ddae3ce3adb19df3d8cf6dd1695c69d276278ec Mon Sep 17 00:00:00 2001 From: Nicholas Cilfone Date: Fri, 14 Jan 2022 15:58:54 -0500 Subject: [PATCH 3/6] fixing spockTuner class based on new underlying functions. adding in stubs for type hinting --- spock/addons/tune/config.py | 62 +++++++++++++++++++++++++++++++------ spock/backend/config.py | 2 +- spock/backend/config.pyi | 9 +++++- spock/builder.py | 5 ++- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/spock/addons/tune/config.py b/spock/addons/tune/config.py index 20a2167e..6d064a0e 100644 --- a/spock/addons/tune/config.py +++ b/spock/addons/tune/config.py @@ -44,30 +44,74 @@ 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 diff --git a/spock/backend/config.py b/spock/backend/config.py index dfd0a195..efe6b34f 100644 --- a/spock/backend/config.py +++ b/spock/backend/config.py @@ -14,7 +14,7 @@ from spock.utils import _is_spock_instance -def _base_attr(cls, kw_only: bool, make_init: bool, dynamic: bool): +def _base_attr(cls, kw_only, make_init, dynamic): """Map type hints to katras Connector function that maps type hinting style to the defined katra style which uses the more strict diff --git a/spock/backend/config.pyi b/spock/backend/config.pyi index 5ccc44bc..7b21a958 100644 --- a/spock/backend/config.pyi +++ b/spock/backend/config.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Tuple, TypeVar, Union, overload +from typing import Any, Callable, Tuple, TypeVar, Union, overload, List, Dict from attr import attrib, field @@ -39,3 +39,10 @@ def spock_attr( make_init: bool = True, dynamic: bool = False, ) -> Callable[[_C], _C]: ... + +def _base_attr( + cls: _C, + kw_only: bool = ..., + make_init: bool = ..., + dynamic: bool = ..., +) -> (List[_C], Dict, Dict): ... diff --git a/spock/builder.py b/spock/builder.py index cc8b299d..507c67dd 100644 --- a/spock/builder.py +++ b/spock/builder.py @@ -229,8 +229,7 @@ def _print_usage_and_exit(self, msg=None, sys_exit=True, exit_code=1): if sys_exit: sys.exit(exit_code) - @staticmethod - def _handle_tuner_objects(tune_args, s3_config, kwargs): + def _handle_tuner_objects(self, tune_args, s3_config, kwargs): """Handles creating the tuner builder object if @spockTuner classes were passed in Args: @@ -247,7 +246,7 @@ def _handle_tuner_objects(tune_args, s3_config, kwargs): from spock.addons.tune.builder import TunerBuilder from spock.addons.tune.payload import TunerPayload - tuner_builder = TunerBuilder(*tune_args, **kwargs) + tuner_builder = TunerBuilder(*tune_args, **kwargs, lazy=self._lazy) tuner_payload = TunerPayload(s3_config=s3_config) return tuner_builder, tuner_payload except ImportError: From fe8e089be3a4cca29f5661051e0150dfe244b7e7 Mon Sep 17 00:00:00 2001 From: Nicholas Cilfone Date: Fri, 14 Jan 2022 15:59:01 -0500 Subject: [PATCH 4/6] fixing spockTuner class based on new underlying functions. adding in stubs for type hinting --- spock/addons/tune/config.pyi | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 spock/addons/tune/config.pyi diff --git a/spock/addons/tune/config.pyi b/spock/addons/tune/config.pyi new file mode 100644 index 00000000..422539bc --- /dev/null +++ b/spock/addons/tune/config.pyi @@ -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]: ... From cd39c4612f33c6e1951387dab384bdd22cf2fb3b Mon Sep 17 00:00:00 2001 From: Nicholas Cilfone Date: Fri, 14 Jan 2022 16:02:27 -0500 Subject: [PATCH 5/6] linted --- spock/addons/tune/config.py | 10 ++++------ spock/backend/config.pyi | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/spock/addons/tune/config.py b/spock/addons/tune/config.py index 6d064a0e..f2c8eac6 100644 --- a/spock/addons/tune/config.py +++ b/spock/addons/tune/config.py @@ -80,9 +80,9 @@ def _process_class(cls, kw_only: bool, make_init: bool, dynamic: bool): def _spock_tune( - maybe_cls=None, - kw_only=True, - make_init=True, + maybe_cls=None, + kw_only=True, + make_init=True, ): """Ovverides basic spock_attr decorator with another name @@ -99,9 +99,7 @@ def _spock_tune( """ def wrap(cls): - return _process_class( - cls, kw_only=kw_only, make_init=make_init, dynamic=False - ) + 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 diff --git a/spock/backend/config.pyi b/spock/backend/config.pyi index 7b21a958..3f29e992 100644 --- a/spock/backend/config.pyi +++ b/spock/backend/config.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Tuple, TypeVar, Union, overload, List, Dict +from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union, overload from attr import attrib, field @@ -39,7 +39,6 @@ def spock_attr( make_init: bool = True, dynamic: bool = False, ) -> Callable[[_C], _C]: ... - def _base_attr( cls: _C, kw_only: bool = ..., From 857563fd3311924b6b176844999c02540c16ac5e Mon Sep 17 00:00:00 2001 From: Nicholas Cilfone Date: Tue, 18 Jan 2022 11:47:33 -0500 Subject: [PATCH 6/6] added more tests that should cover new graph and inheritance laziness --- spock/backend/config.py | 2 +- spock/backend/field_handlers.py | 33 ++++++--------- spock/exceptions.py | 12 ++++++ spock/graph.py | 21 +++++----- tests/base/attr_configs_test.py | 19 ++++++++- tests/base/test_config_arg_builder.py | 60 +++++++++++++++++++++++++++ tests/base/test_type_specific.py | 8 ++-- 7 files changed, 119 insertions(+), 36 deletions(-) diff --git a/spock/backend/config.py b/spock/backend/config.py index efe6b34f..0ee2c7ee 100644 --- a/spock/backend/config.py +++ b/spock/backend/config.py @@ -45,7 +45,7 @@ def _base_attr(cls, kw_only, make_init, dynamic): if not _is_spock_instance(base_cls) and not dynamic: raise _SpockUndecoratedClass( f"Class `{base_cls.__name__}` was not decorated with the @spock decorator " - f"and `dynamic={dynamic}` was set for child class `{cls.__name__}`" + f"and `dynamic={dynamic}` was set for child class `{cls.__name__}` -- Please remedy one of these" ) elif not _is_spock_instance(base_cls) and dynamic: bases[idx] = _process_class(base_cls, kw_only, make_init, dynamic) diff --git a/spock/backend/field_handlers.py b/spock/backend/field_handlers.py index 10808ded..48b4ff9d 100644 --- a/spock/backend/field_handlers.py +++ b/spock/backend/field_handlers.py @@ -13,21 +13,10 @@ from spock.args import SpockArguments from spock.backend.spaces import AttributeSpace, BuilderSpace, ConfigSpace +from spock.exceptions import _SpockInstantiationError, _SpockNotOptionalError from spock.utils import _check_iterable, _is_spock_instance, _is_spock_tune_instance -class SpockInstantiationError(Exception): - """Custom exception for when the spock class cannot be instantiated correctly""" - - pass - - -class SpockNotOptionalError(Exception): - """Custom exception for missing value""" - - pass - - class RegisterFieldTemplate(ABC): """Base class for handing different field types @@ -216,7 +205,7 @@ def handle_optional_attribute_value( val() if type(val) is type else val for val in attr_space.field ] except Exception as e: - raise SpockInstantiationError( + raise _SpockInstantiationError( f"Spock class `{spock_cls.__name__}` could not be instantiated -- attrs message: {e}" ) builder_space.spock_space[spock_cls.__name__] = attr_space.field @@ -370,10 +359,14 @@ def handle_optional_attribute_type( builder_space: named_tuple containing the arguments and spock_space Raises: - SpockNotOptionalError + _SpockNotOptionalError """ - raise SpockNotOptionalError() + raise _SpockNotOptionalError( + f"Parameter `{attr_space.attribute.name}` within `{attr_space.config_space.name}` is of " + f"type `{type(attr_space.attribute.type)}` which seems to be unsupported -- " + f"are you missing an @spock decorator on a base python class?" + ) def handle_optional_attribute_value( self, attr_space: AttributeSpace, builder_space: BuilderSpace @@ -463,10 +456,10 @@ def handle_optional_attribute_value( builder_space: named_tuple containing the arguments and spock_space Raises: - SpockNotOptionalError + _SpockNotOptionalError """ - raise SpockNotOptionalError() + raise _SpockNotOptionalError() def handle_optional_attribute_type( self, attr_space: AttributeSpace, builder_space: BuilderSpace @@ -478,10 +471,10 @@ def handle_optional_attribute_type( builder_space: named_tuple containing the arguments and spock_space Raises: - SpockNotOptionalError + _SpockNotOptionalError """ - raise SpockNotOptionalError() + raise _SpockNotOptionalError() class RegisterSpockCls(RegisterFieldTemplate): @@ -625,7 +618,7 @@ def recurse_generate(cls, spock_cls, builder_space: BuilderSpace): try: spock_instance = spock_cls(**fields) except Exception as e: - raise SpockInstantiationError( + raise _SpockInstantiationError( f"Spock class `{spock_cls.__name__}` could not be instantiated -- attrs message: {e}" ) return spock_instance, special_keys diff --git a/spock/exceptions.py b/spock/exceptions.py index ff442a23..6c2822b5 100644 --- a/spock/exceptions.py +++ b/spock/exceptions.py @@ -3,3 +3,15 @@ class _SpockUndecoratedClass(Exception): """Custom exception type for non spock decorated classes and not dynamic""" pass + + +class _SpockInstantiationError(Exception): + """Custom exception for when the spock class cannot be instantiated correctly""" + + pass + + +class _SpockNotOptionalError(Exception): + """Custom exception for missing value""" + + pass diff --git a/spock/graph.py b/spock/graph.py index 2e4a5421..2889ca0d 100644 --- a/spock/graph.py +++ b/spock/graph.py @@ -7,7 +7,6 @@ import sys from typing import Type -from warnings import warn from spock.utils import _find_all_spock_classes @@ -32,10 +31,11 @@ def __init__(self, input_classes, lazy: bool): self._input_classes = input_classes self._lazy = lazy # Maybe find classes lazily -- roll them into the input class tuple + # make sure to cast as a set first since the lazy search might find duplicate references if self._lazy: self._input_classes = ( *self._input_classes, - *self._lazily_find_classes(self._input_classes), + *set(self._lazily_find_classes(self._input_classes)), ) # Build -- post lazy eval self._dag = self._build() @@ -101,7 +101,7 @@ def _lazily_find_classes(self, classes): lazy_classes = [] for _, v in self._yield_class_deps(classes): if hasattr(sys.modules["spock"].backend.config, v): - warn( + print( f"Lazy evaluation found a @spock decorated class named `{v}` within the registered types of " f"sys.modules['spock'].backend.config -- Attempting to use the class " f"`{getattr(sys.modules['spock'].backend.config, v)}`..." @@ -114,12 +114,12 @@ def _lazily_find_classes(self, classes): if len(dependent_lazy_classes) > 0: lazy_classes.extend(dependent_lazy_classes) lazy_classes.append(lazy_class) - else: - raise ValueError( - f"Missing @spock decorated class -- `{v}` was not passed as an *arg to " - f"ConfigArgBuilder and lazy evaluation could not find it within " - f"sys.modules['spock'].backend.config" - ) + # else: + # raise ValueError( + # f"Missing @spock decorated class -- `{v}` was not passed as an *arg to " + # f"ConfigArgBuilder and lazy evaluation could not find it within " + # f"sys.modules['spock'].backend.config" + # ) return tuple(lazy_classes) def _build(self): @@ -136,7 +136,8 @@ def _build(self): if v not in self.node_names: raise ValueError( f"Missing @spock decorated class -- `{v}` was not passed as an *arg to " - f"ConfigArgBuilder and/or could not be found via lazy evaluation" + f"ConfigArgBuilder and/or could not be found via lazy evaluation (currently lazy=`{self._lazy}`) " + f"within sys.modules['spock'].backend.config" ) nodes.get(v).append(input_class) nodes = {key: set(val) for key, val in nodes.items()} diff --git a/tests/base/attr_configs_test.py b/tests/base/attr_configs_test.py index 43a78d59..54b697e6 100644 --- a/tests/base/attr_configs_test.py +++ b/tests/base/attr_configs_test.py @@ -334,4 +334,21 @@ class TestConfigDynamicDefaults(Foo, Bar): FirstDoubleNestedConfig, SecondDoubleNestedConfig, NestedStuffOpt -] \ No newline at end of file +] + + +@spock +class OtherBar: + hello: str = 'goodbye' + + +@spock +class RaiseNotFlagged: + test: int = 1 + other: OtherBar = OtherBar + + +@spock +class RaiseNotDecorated: + test: int = 1 + other: Bar = Bar diff --git a/tests/base/test_config_arg_builder.py b/tests/base/test_config_arg_builder.py index 788a41a5..57c19eca 100644 --- a/tests/base/test_config_arg_builder.py +++ b/tests/base/test_config_arg_builder.py @@ -4,6 +4,7 @@ import pytest from spock.builder import ConfigArgBuilder +from spock.exceptions import _SpockUndecoratedClass, _SpockNotOptionalError from tests.base.attr_configs_test import * from tests.base.base_asserts_test import * @@ -206,3 +207,62 @@ def arg_builder(monkeypatch): m.setattr(sys, "argv", [""]) config = ConfigArgBuilder(TestConfigDynamicDefaults) return config.generate() + + +class TestBasicLazy(AllTypes): + """Testing basic lazy evaluation""" + @staticmethod + @pytest.fixture + def arg_builder(monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", ["", "--config", "./tests/conf/yaml/test.yaml"]) + config = ConfigArgBuilder(TypeConfig, TypeOptConfig, lazy=True) + return config.generate() + + +class TestLazyNotFlagged: + """Testing failed lazy evaluation""" + def test_lazy_raise(self, monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", [""]) + with pytest.raises(ValueError): + config = ConfigArgBuilder(RaiseNotFlagged, lazy=False) + config.generate() + + +class TestLazyNotDecorated: + """Testing failed lazy evaluation""" + def test_lazy_raise(self, monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", [""]) + with pytest.raises(_SpockNotOptionalError): + config = ConfigArgBuilder(RaiseNotDecorated, lazy=False) + config.generate() + + +class TestDynamic(AllDynamic): + """Testing basic dynamic inheritance""" + @staticmethod + @pytest.fixture + def arg_builder(monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", [""]) + config = ConfigArgBuilder(TestConfigDynamicDefaults) + return config.generate() + + +class TestDynamicRaise: + """Testing dynamic raise fail""" + def test_dynamic_raise(self, monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", [""]) + with pytest.raises(_SpockUndecoratedClass): + + @spock + class TestConfigDefaultsFail(Foo, Bar): + x: int = 235 + y: str = 'yarghhh' + z: List[int] = [10, 20] + + config = ConfigArgBuilder(TestConfigDefaultsFail) + return config.generate() diff --git a/tests/base/test_type_specific.py b/tests/base/test_type_specific.py index e66a4170..096529b9 100644 --- a/tests/base/test_type_specific.py +++ b/tests/base/test_type_specific.py @@ -4,7 +4,7 @@ import pytest from spock.builder import ConfigArgBuilder -from spock.backend.field_handlers import SpockInstantiationError +from spock.exceptions import _SpockInstantiationError from tests.base.attr_configs_test import * @@ -14,7 +14,7 @@ class TestChoiceRaises: def test_choice_raise(self, monkeypatch): with monkeypatch.context() as m: m.setattr(sys, "argv", ["", "--config", "./tests/conf/yaml/choice.yaml"]) - with pytest.raises(SpockInstantiationError): + with pytest.raises(_SpockInstantiationError): ConfigArgBuilder(ChoiceFail, desc="Test Builder") @@ -23,7 +23,7 @@ class TestOptionalRaises: def test_coptional_raise(self, monkeypatch): with monkeypatch.context() as m: # m.setattr(sys, "argv", ["", "--config", "./tests/conf/yaml/empty.yaml"]) - with pytest.raises(SpockInstantiationError): + with pytest.raises(_SpockInstantiationError): ConfigArgBuilder(OptionalFail, desc="Test Builder", configs=[], no_cmd_line=True) @@ -108,6 +108,6 @@ def test_repeated_defs_fail(self, monkeypatch): "argv", [""], ) - with pytest.raises(SpockInstantiationError): + with pytest.raises(_SpockInstantiationError): config = ConfigArgBuilder(RepeatedDefsFailConfig, NestedListStuff, desc="Test Builder") config.generate()