Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#187 Primitive timestamp tracking #193

Merged
merged 12 commits into from
Jun 14, 2024
27 changes: 25 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,32 @@
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
project adheres to clauses 1–8 of [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.13.0b1] (Jun 13 2024)

### Added

- The timestamps of non-`ParamData` children are now tracked internally and can be
accessed via the new method `ParamData.child_last_updated()`.
- The class `ParamDBKey` contains the keys used in the JSON representation of a commit.

### Changed

- `ParamDict` dot notation now treates names of existing attributes and names of class
type annotations as attributes (rather than treating all names beginning with
underscores as attributes).
- The JSON format of a commit has been changed, as specified in the docstring for
`ParamDB.load()`.
- `ParamData.to_dict()` and `ParamData.from_dict()` have been replaced by
`ParamData.to_json()` and `ParamData.from_json()`.

### Removed

- Parameter primitive classes have been replaced by the new timestamp tracking.

## [0.12.0] (May 8 2024)

### Added
Expand Down Expand Up @@ -169,7 +191,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Database class `ParamDB` to store parameters in a SQLite file
- Ability to retrieve the commit history as `CommitEntry` objects

[unreleased]: https://github.com/PainterQubits/paramdb/compare/v0.12.0...develop
[unreleased]: https://github.com/PainterQubits/paramdb/compare/v0.13.0b1...develop
[0.13.0b1]: https://github.com/PainterQubits/paramdb/releases/tag/v0.13.0b1
[0.12.0]: https://github.com/PainterQubits/paramdb/releases/tag/v0.12.0
[0.11.0]: https://github.com/PainterQubits/paramdb/releases/tag/v0.11.0
[0.10.2]: https://github.com/PainterQubits/paramdb/releases/tag/v0.10.2
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ authors:
- family-names: "Hadley"
given-names: "Alex"
title: "ParamDB"
version: 0.12.0
date-released: 2024-05-08
version: 0.13.0b1
date-released: 2024-06-13
url: "https://github.com/PainterQubits/paramdb"
17 changes: 1 addition & 16 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 All @@ -34,15 +29,5 @@ All of the following can be imported from `paramdb`.
.. autoclass:: ParamDB
.. autoclass:: CommitEntry
.. autoclass:: CommitEntryWithData
```

<!--
Changing the current module to paramdb._database is necessary in order to show the correct
docstring.
-->

```{eval-rst}
.. py:currentmodule:: paramdb._database
.. autodata:: CLASS_NAME_KEY
.. py:currentmodule:: paramdb
.. autoclass:: ParamDBKey
```
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
project = "ParamDB"
copyright = "2023–2024, California Institute of Technology"
author = "Alex Hadley"
release = "0.12.0"
release = "0.13.0b1"

# General configuration
extensions = [
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
16 changes: 2 additions & 14 deletions paramdb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
"""Python package for storing and retrieving experiment parameters."""

from paramdb._param_data._param_data import ParamData
from paramdb._param_data._primitives import (
ParamInt,
ParamBool,
ParamFloat,
ParamStr,
ParamNone,
)
from paramdb._param_data._dataclasses import ParamDataclass
from paramdb._param_data._files import ParamFile
from paramdb._param_data._collections import ParamList, ParamDict
from paramdb._param_data._type_mixins import ParentType, RootType
from paramdb._database import CLASS_NAME_KEY, ParamDB, CommitEntry, CommitEntryWithData
from paramdb._database import ParamDB, CommitEntry, CommitEntryWithData, ParamDBKey

__all__ = [
"ParamData",
"ParamInt",
"ParamBool",
"ParamFloat",
"ParamStr",
"ParamNone",
"ParamDataclass",
"ParamFile",
"ParamList",
"ParamDict",
"ParentType",
"RootType",
"CLASS_NAME_KEY",
"ParamDB",
"CommitEntry",
"CommitEntryWithData",
"ParamDBKey",
]

try:
Expand Down
Loading
Loading