Skip to content

Commit

Permalink
#187 Update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
alexhad6 committed Jun 13, 2024
1 parent 54b3eac commit ae3ebae
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 114 deletions.
5 changes: 0 additions & 5 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 40 additions & 92 deletions docs/parameter-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -112,44 +73,46 @@ 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`]
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()
```
Expand All @@ -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)
```

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/_param_data/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)


Expand Down
8 changes: 6 additions & 2 deletions tests/_param_data/test_param_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ae3ebae

Please sign in to comment.