From ae3ebaea6a0c13abdf465d009cbc5490c3562743 Mon Sep 17 00:00:00 2001 From: Alex Hadley Date: Thu, 13 Jun 2024 11:00:14 -0700 Subject: [PATCH] #187 Update docs --- docs/api-reference.md | 5 - docs/parameter-data.md | 132 ++++++++------------------ tests/_param_data/test_dataclasses.py | 4 +- tests/_param_data/test_param_data.py | 8 +- tests/conftest.py | 24 ++--- tests/helpers.py | 2 +- 6 files changed, 61 insertions(+), 114 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index edbf045..556e041 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -10,11 +10,6 @@ All of the following can be imported from `paramdb`. ```{eval-rst} .. autoclass:: ParamData -.. autoclass:: ParamInt -.. autoclass:: ParamFloat -.. autoclass:: ParamBool -.. autoclass:: ParamStr -.. autoclass:: ParamNone .. autoclass:: ParamDataclass .. autoclass:: ParamFile .. autoclass:: ParamDataFrame diff --git a/docs/parameter-data.md b/docs/parameter-data.md index b07a10f..3c7a653 100644 --- a/docs/parameter-data.md +++ b/docs/parameter-data.md @@ -32,46 +32,6 @@ type (`str`, `int`, `float`, `bool`, `None`, `dict`, or `list`), a [`datetime`], a `TypeError` will be raised when they are committed to the database. ``` -## Primitives - -Primitives are the building blocks of parameter data. While builtin primitive types can -be used in a ParamDB (`int`, `float`, `str`, `bool`, and `None`), they will not store a -{py:class}`~ParamData.last_updated` time and will not have {py:class}`~ParamData.parent` -or {py:class}`~ParamData.root` properties. When these features are desired, we can wrap -primitive values in the following types: - -- {py:class}`ParamInt` for integers -- {py:class}`ParamFloat` for float -- {py:class}`ParamBool` for booleans -- {py:class}`ParamStr` for strings -- {py:class}`ParamNone` for `None` - -For example: - -```{jupyter-execute} -from paramdb import ParamInt - -param_int = ParamInt(123) -param_int -``` - -```{jupyter-execute} -print(param_int.last_updated) -``` - -````{tip} -Methods from the builtin primitive types work on parameter primitives, with the caveat -that they return the builtin type. For example: - -```{jupyter-execute} -param_int + 123 -``` - -```{jupyter-execute} -type(param_int + 123) -``` -```` - ## Data Classes A parameter data class is defined from the base class {py:class}`ParamDataclass`. This @@ -81,19 +41,20 @@ function is generated. An example of a defining a custom parameter Data Class is below. ```{jupyter-execute} -from paramdb import ParamFloat, ParamDataclass +from paramdb import ParamDataclass class CustomParam(ParamDataclass): - value: ParamFloat + value: float -custom_param = CustomParam(value=ParamFloat(1.23)) +custom_param = CustomParam(value=1.23) +print(custom_param) ``` These properties can then be accessed and updated. ```{jupyter-execute} -custom_param.value = ParamFloat(1.234) -custom_param.value +custom_param.value += 0.004 +print(custom_param) ``` The data class aspects of the subclass can be customized by passing keyword arguments when @@ -112,16 +73,18 @@ when building up dataclasses through inheritance. from dataclasses import field class KeywordOnlyParam(ParamDataclass, kw_only=True): - count: int + num_values: int = 0 values: list[int] = field(default_factory=list) + type: str -keyword_only_param = KeywordOnlyParam(count=123) -keyword_only_param +keyword_only_param = KeywordOnlyParam(type="example") +print(keyword_only_param) ``` ```{warning} -For mutable default values, `default_factory` should generally be used. See the Python -data class documentation on [mutable default values] for more information. +For mutable default values, `default_factory` should generally be used (see the example +above). See the Python data class documentation on [mutable default values] for more +information. ``` Custom methods can also be added, including dynamic properties using the [`@property`] @@ -129,27 +92,27 @@ decorator. For example: ```{jupyter-execute} class ParamWithProperty(ParamDataclass): - value: ParamInt + value: int @property def value_cubed(self) -> int: return self.value ** 3 -param_with_property = ParamWithProperty(value=ParamInt(16)) -param_with_property.value_cubed +param_with_property = ParamWithProperty(value=16) +print(param_with_property.value_cubed) ``` ````{important} Since [`__init__`] is generated for data classes, other initialization must be done using the [`__post_init__`] function. Furthermore, since [`__post_init__`] is used internally by {py:class}`ParamDataclass` to perform initialization, always call the superclass's -[`__post_init__`]. For example: +[`__post_init__`] first. For example: ```{jupyter-execute} class ParamCustomInit(ParamDataclass): def __post_init__(self) -> None: + super().__post_init__() # Always call the superclass __post_init__() first print("Initializing...") # Replace with custom initialization code - super().__post_init__() param_custom_init = ParamCustomInit() ``` @@ -166,27 +129,33 @@ print(custom_param.last_updated) import time time.sleep(1) -custom_param.value = ParamFloat(4.56) +custom_param.value = 4.56 print(custom_param.last_updated) ``` -Parameter dataclasses can also be nested, in which case the -{py:attr}`ParamData.last_updated` property returns the most recent last updated time stamp -among its own last updated time and the last updated times of any {py:class}`ParamData` -it contains. For example: +Last updated times for properties can also be accessed using by calling +{py:meth}`ParamData.child_last_updated` on the parent object. This is particularly useful +for property values which are not {py:class}`ParamData`. For example: + +```{jupyter-execute} +print(custom_param.child_last_updated("value")) +``` + +When parameter dataclasses are nested, updating a child also updates the last updated +times of its parents. For example: ```{jupyter-execute} class NestedParam(ParamDataclass): value: float child_param: CustomParam -nested_param = NestedParam(value=1.23, child_param=CustomParam(value=ParamFloat(4.56))) +nested_param = NestedParam(value=1.23, child_param=CustomParam(value=4.56)) print(nested_param.last_updated) ``` ```{jupyter-execute} time.sleep(1) -nested_param.child_param.value = ParamFloat(2) +nested_param.child_param.value += 1 print(nested_param.last_updated) ``` @@ -273,54 +242,33 @@ properly. For example: ```{jupyter-execute} from paramdb import ParamList -param_list = ParamList([ParamInt(1), ParamInt(2), ParamInt(3)]) -param_list[1].parent is param_list -``` - -```{jupyter-execute} -print(param_list.last_updated) -``` - -```{jupyter-execute} -time.sleep(1) -param_list[1] = ParamInt(4) -print(param_list.last_updated) +param_list = ParamList([1, 2, 3]) +print(param_list.child_last_updated(1)) ``` ### Parameter Dictionaries Similarly, {py:class}`ParamDict` implements `MutableMapping` from [`collections.abc`], -so it behaves similarly to a dictionary. Additionally, its items can be accessed via -dot notation in addition to index brackets (unless they begin with an underscore). For -example: +so it behaves similarly to a dictionary. Additionally, items can be accessed via dot +notation in addition to index brackets. For example: ```{jupyter-execute} from paramdb import ParamDict -param_dict = ParamDict(p1=ParamFloat(1.23), p2=ParamFloat(4.56), p3=ParamFloat(7.89)) -param_dict.p2.root == param_dict -``` - -```{jupyter-execute} -print(param_dict.last_updated) -``` - -```{jupyter-execute} -time.sleep(1) -param_dict.p2 = ParamFloat(0) -print(param_dict.last_updated) +param_dict = ParamDict(p1=1.23, p2=4.56, p3=7.89) +print(param_dict.child_last_updated("p2")) ``` Parameter collections can also be subclassed to provide custom functionality. For example: ```{jupyter-execute} -class CustomDict(ParamDict[ParamFloat]): +class CustomDict(ParamDict[float]): @property def total(self) -> float: - return sum(param.value for param in self.values()) + return sum(self.values()) custom_dict = CustomDict(param_dict) -custom_dict.total +print(custom_dict.total) ``` ## Type Mixins diff --git a/tests/_param_data/test_dataclasses.py b/tests/_param_data/test_dataclasses.py index 039697c..e447a08 100644 --- a/tests/_param_data/test_dataclasses.py +++ b/tests/_param_data/test_dataclasses.py @@ -147,7 +147,7 @@ def test_param_dataclass_init_wrong_type( assert "Input should be a valid number" in str(exc_info.value) -def test_param_dataclass_init_default_wrong_type() -> None: +def test_param_dataclass_init_default_wrong_type(number: float) -> None: """ Fails or succeeds to initialize a parameter object with a default value having the wrong type @@ -159,7 +159,7 @@ class DefaultWrongTypeParam(SimpleParam): default_number: float = "123" # type: ignore[assignment] with pytest.raises(pydantic.ValidationError) as exc_info: - DefaultWrongTypeParam() + DefaultWrongTypeParam(number=number) assert "Input should be a valid number" in str(exc_info.value) diff --git a/tests/_param_data/test_param_data.py b/tests/_param_data/test_param_data.py index 7432568..f2e0fe7 100644 --- a/tests/_param_data/test_param_data.py +++ b/tests/_param_data/test_param_data.py @@ -4,7 +4,7 @@ from dataclasses import is_dataclass from copy import deepcopy import pytest -from tests.helpers import ComplexParam, Times, capture_start_end_times +from tests.helpers import SimpleParam, ComplexParam, Times, capture_start_end_times from paramdb import ParamData, ParamDataFrame from paramdb._param_data._param_data import get_param_class @@ -48,12 +48,16 @@ def test_get_param_class(param_data: ParamData[Any]) -> None: assert get_param_class(param_class.__name__) is param_class -def test_param_data_initial_last_updated(param_data_type: type[ParamData[Any]]) -> None: +def test_param_data_initial_last_updated( + number: float, param_data_type: type[ParamData[Any]] +) -> None: """New parameter data objects are initialized with a last updated timestamp.""" with capture_start_end_times() as times: new_param_data: ParamData[Any] if issubclass(param_data_type, ParamDataFrame): new_param_data = param_data_type("") + elif issubclass(param_data_type, SimpleParam): + new_param_data = param_data_type(number=number) else: new_param_data = param_data_type() assert new_param_data.last_updated is not None diff --git a/tests/conftest.py b/tests/conftest.py index 397ef01..6c42b91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,12 +99,12 @@ def fixture_complex_param(number: float, string: str) -> ComplexParam: string=string, param_data_frame=ParamDataFrame(string), empty_param=EmptyParam(), - simple_param=SimpleParam(), - no_type_validation_param=NoTypeValidationParam(), - with_type_validation_param=WithTypeValidationParam(), - no_assignment_validation_param=NoAssignmentValidationParam(), - with_assignment_validation_param=WithAssignmentValidationParam(), - subclass_param=SubclassParam(), + simple_param=SimpleParam(number=number), + no_type_validation_param=NoTypeValidationParam(number=number), + with_type_validation_param=WithTypeValidationParam(number=number), + no_assignment_validation_param=NoAssignmentValidationParam(number=number), + with_assignment_validation_param=WithAssignmentValidationParam(number=number), + subclass_param=SubclassParam(number=number), complex_param=ComplexParam(), param_list=ParamList(), param_dict=ParamDict(), @@ -119,12 +119,12 @@ def fixture_param_list_contents(number: float, string: str) -> list[Any]: string, ParamDataFrame(string), EmptyParam(), - SimpleParam(), - NoTypeValidationParam(), - WithTypeValidationParam(), - NoAssignmentValidationParam(), - WithAssignmentValidationParam(), - SubclassParam(), + SimpleParam(number=number), + NoTypeValidationParam(number=number), + WithTypeValidationParam(number=number), + NoAssignmentValidationParam(number=number), + WithAssignmentValidationParam(number=number), + SubclassParam(number=number), ComplexParam(), ParamList(), ParamDict(), diff --git a/tests/helpers.py b/tests/helpers.py index 780981e..64a05cb 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -45,7 +45,7 @@ class EmptyParam(ParamDataclass): class SimpleParam(ParamDataclass): """Simple parameter data class.""" - number: float = DEFAULT_NUMBER + number: float # No default to verify that non-default properties work number_init_false: float = field(init=False, default=DEFAULT_NUMBER) number_with_units: Quantity = Quantity(12, "m") string: str = DEFAULT_STRING