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

#170 Improve last updated tracking #175

Merged
merged 6 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ build:
jobs:
post_create_environment:
- pip install poetry==1.8.2
- poetry config virtualenvs.create false
post_install:
- poetry install --without dev
- poetry run python -m ipykernel install --user
# See https://docs.readthedocs.io/en/stable/build-customization.html#install-dependencies-with-poetry
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --without dev
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry run python -m ipykernel install --user

sphinx:
configuration: docs/conf.py
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- All `ParamData` objects now internally track the latest time that they or any of their
children were last updated, which is returned by `ParamData.last_updated`.
- `Param` and `Struct` are combined into a single class `ParamDataclass`.

## [0.11.0] (Jan 31 2024)

### Added
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# ParamDB

<!-- start badges -->

[![PyPI Latest Release](https://img.shields.io/pypi/v/paramdb)](https://pypi.org/project/paramdb/)
[![PyPI Python Versions](https://img.shields.io/pypi/pyversions/paramdb)](https://pypi.org/project/paramdb/)
[![License](https://img.shields.io/pypi/l/paramdb)](https://github.com/PainterQubits/paramdb/blob/main/LICENSE)
[![CI](https://github.com/PainterQubits/paramdb/actions/workflows/ci.yml/badge.svg)](https://github.com/PainterQubits/paramdb/actions/workflows/ci.yml)
[![Codecov](https://codecov.io/github/PainterQubits/paramdb/branch/main/graph/badge.svg?token=PQEJWLBTBK)](https://codecov.io/github/PainterQubits/paramdb)
[![Documentation Status](https://readthedocs.org/projects/paramdb/badge/?version=stable)](https://paramdb.readthedocs.io/en/stable/?badge=stable)

<!-- end badges -->

<!-- start intro -->

Python package for storing and retrieving experiment parameters.
Expand Down
17 changes: 5 additions & 12 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ All of the following can be imported from `paramdb`.

```{eval-rst}
.. autoclass:: ParamData
.. autoclass:: Param
.. autoclass:: Struct
.. autoclass:: ParamDataclass
.. autoclass:: ParamList
.. autoclass:: ParamDict
.. autoclass:: ParentType
Expand All @@ -26,19 +25,13 @@ All of the following can be imported from `paramdb`.
.. autoclass:: CommitEntryWithData
```

## Keys

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

```{py:currentmodule} paramdb._keys

```

```{eval-rst}
.. py:currentmodule:: paramdb._database
.. autodata:: CLASS_NAME_KEY
.. autodata:: PARAMLIST_ITEMS_KEY
.. autodata:: LAST_UPDATED_KEY
.. py:currentmodule:: paramdb
```
6 changes: 3 additions & 3 deletions docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ the root data type in order for its methods (e.g. {py:meth}`ParamDB.commit`) wor
with type checking. For example:

```{jupyter-execute}
from paramdb import Struct, Param, ParamDB
from paramdb import ParamDataclass, ParamDB

class Root(Struct):
class Root(ParamDataclass):
param: CustomParam

class CustomParam(Param):
class CustomParam(ParamDataclass):
value: float

param_db = ParamDB[Root]("path/to/param.db")
Expand Down
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# ParamDB

```{include} ../README.md
:start-after: <!-- start badges -->
:end-before: <!-- end badges -->
```

```{include} ../README.md
:start-after: <!-- start intro -->
:end-before: <!-- end intro -->
Expand Down
150 changes: 81 additions & 69 deletions docs/parameter-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ A ParamDB database stores parameter data. The abstract base class {py:class}`Par
defines some core functionality for this data, including the
{py:class}`~ParamData.last_updated`, {py:class}`~ParamData.parent`, and
{py:class}`~ParamData.root` properties. Internally, any subclasses of
{py:class}`ParamData` automatically registered with ParamDB so that they can be loaded
to and from JSON, which is how they are stored in the database.
{py:class}`ParamData` are automatically registered with ParamDB so that they can be
loaded to and from JSON, which is how they are stored in the database.

All of the classes described on this page are subclasses of {py:class}`ParamData`.

Expand All @@ -28,33 +28,34 @@ type (`str`, `int`, `float`, `bool`, `None`, `dict`, or `list`), a [`datetime`],
a `TypeError` will be raised when they are committed to the database.
```

## Parameters
## Data Classes

A parameter is defined from the base class {py:class}`Param`. This custom class is
automatically converted into a [`dataclass`], meaning that class variables with type
annotations become object properties and the corresponding [`__init__`] function is
generated. An example of a defining a custom parameter is shown below.
A parameter data class is defined from the base class {py:class}`ParamDataclass`. This
custom class is automatically converted into a [data class], meaning that class variables
with type annotations become object properties and the corresponding [`__init__`]
function is generated. An example of a defining a custom parameter Data Class is shown
below.

```{jupyter-execute}
from paramdb import Param
from paramdb import ParamDataclass

class CustomParam(Param):
class CustomParam(ParamDataclass):
value: float

param = CustomParam(value=1.23)
custom_param = CustomParam(value=1.23)
```

These properties can then be accessed and updated.

```{jupyter-execute}
param.value += 0.004
param.value
custom_param.value += 0.004
custom_param.value
```

The dataclass aspects of the subclass can be customized by passing keyword arguments when
The data class aspects of the subclass can be customized by passing keyword arguments when
defining the custom class (the same arguments that would be passed to the [`@dataclass`]
decorator), and by using the dataclass [`field`] function. The class arguments have the
same default values as in [`@dataclass`]. An example of dataclass customization is shown
same default values as in [`@dataclass`]. An example of data class customization is shown
below.

```{note}
Expand All @@ -66,24 +67,24 @@ when building up dataclasses through inheritance.
```{jupyter-execute}
from dataclasses import field

class CustomizedDataclassParam(Param, kw_only=True):
class KeywordOnlyParam(ParamDataclass, kw_only=True):
values: list[int] = field(default_factory=list)
count: int

customized_dataclass_param = CustomizedDataclassParam(count=123)
customized_dataclass_param
keyword_only_param = KeywordOnlyParam(count=123)
keyword_only_param
```

```{warning}
For mutable default values, `default_factory` should generally be used. See Python
dataclass documentation for [mutable default values] for more information.
For mutable default values, `default_factory` should generally be used. See the Python
data class documentation on [mutable default values] for more information.
```

Methods can also be added, including dynamic read-only properties using the
[`@property`] decorator. For example:
Custom methods can also be added, including dynamic properties using the [`@property`]
decorator. For example:

```{jupyter-execute}
class ParamWithProperty(Param):
class ParamWithProperty(ParamDataclass):
value: int

@property
Expand All @@ -95,13 +96,13 @@ param_with_property.value_cubed
```

````{important}
Since [`__init__`] is generated for dataclasses, other initialization must be done using
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}`ParamData`, {py:class}`Param`, and {py:class}`Struct` to perform
initialization, always call the superclass's [`__post_init__`] at the end. For example:
{py:class}`ParamDataclass` to perform initialization, always call the superclass's
[`__post_init__`]. For example:

```{jupyter-execute}
class ParamCustomInit(Param):
class ParamCustomInit(ParamDataclass):
def __post_init__(self) -> None:
print("Initializing...") # Replace with custom initialization code
super().__post_init__()
Expand All @@ -110,63 +111,52 @@ param_custom_init = ParamCustomInit()
```
````

```{tip}
Since the base class of all parameter classes, {py:class}`ParamData`, is an abstract class
that inherits from [`abc.ABC`], you can use abstract decorators in parameter and structure
classes without inheriting from [`abc.ABC`] again.
```

Parameters track when any of their properties was last updated in the read-only
{py:attr}`~Param.last_updated` property. For example:
Parameter data track when any of their properties were last updated, and this value can be
accessed by the read-only {py:attr}`~ParamData.last_updated` property. For example:

```{jupyter-execute}
param.last_updated
custom_param.last_updated
```

```{jupyter-execute}
import time

time.sleep(1)
param.value += 1
param.last_updated
custom_param.value += 1
custom_param.last_updated
```

## Structures

A structure is defined from the base class {py:class}`Struct` and is intended
to be defined as a dataclass. The key difference from {py:class}`Param` is that
structures do not store their own last updated time; instead, the
{py:attr}`ParamData.last_updated` property returns the most recent last updated time
of any {py:class}`ParamData` they contain. For example:
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:

```{jupyter-execute}
from paramdb import Struct, ParamDict

class CustomStruct(Struct):
class NestedParam(ParamDataclass):
value: float
param: CustomParam
child_param: CustomParam

struct = CustomStruct(value=1.23, param=CustomParam(value=4.56))
struct.last_updated
nested_param = NestedParam(value=1.23, child_param=CustomParam(value=4.56))
nested_param.last_updated
```

```{jupyter-execute}
time.sleep(1)
struct.param.value += 1
struct.last_updated
nested_param.child_param.value += 1
nested_param.last_updated
```

You can access the parent of any parameter data using the {py:attr}`ParamData.parent`
property. For example:

```{jupyter-execute}
struct.param.parent == struct
nested_param.child_param.parent is nested_param
```

Similarly, the root can be accessed via {py:attr}`ParamData.root`:

```{jupyter-execute}
struct.param.root == struct
nested_param.child_param.root is nested_param
```

See [Type Mixins](#type-mixins) for information on how to get the parent and root
Expand All @@ -175,28 +165,40 @@ properties to work better with static type checkers.
## Collections

Ordinary lists and dictionaries can be used within parameter data; however, any
parameter data objects they contain will not have a parent object. This is because
internally, the parent is set by the {py:class}`ParamData` object that most recently
added the given parameter data as a child. Therefore, it is not recommended to use
ordinary lists and dictionaries to store parameter data. Instead, {py:class}`ParamList`
and {py:class}`ParamDict` can be used.
parameter data objects they contain will not have a last updated time or a parent object.
Therefore, it is not recommended to use ordinary lists and dictionaries to store parameter
data. Instead, {py:class}`ParamList` and {py:class}`ParamDict` can be used.

### Parameter Lists

{py:class}`ParamList` implements the abstract base class `MutableSequence` from
[`collections.abc`], so it behaves similarly to a list. It is also a subclass of
{py:class}`ParamData`, so the parent and root properties will work properly. For
example,
{py:class}`ParamData`, so the last updated, parent, and root properties will work
properly. For example:

```{jupyter-execute}
from paramdb import ParamList

param_list = ParamList([CustomParam(value=1), CustomParam(value=2), CustomParam(value=3)])
param_list[1].parent == param_list
param_list[1].parent is param_list
```

```{jupyter-execute}
param_list.last_updated
```

```{jupyter-execute}
time.sleep(1)
param_list[1].value += 1
param_list.last_updated
```

### 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,
example:

```{jupyter-execute}
from paramdb import ParamDict
Expand All @@ -209,6 +211,16 @@ param_dict = ParamDict(
param_dict.p2.root == param_dict
```

```{jupyter-execute}
param_list.last_updated
```

```{jupyter-execute}
time.sleep(1)
param_list[1].value += 1
param_list.last_updated
```

Parameter collections can also be subclassed to provide custom functionality. For example:

```{jupyter-execute}
Expand All @@ -232,21 +244,21 @@ example:
```{jupyter-execute}
from paramdb import ParentType

class ParentStruct(Struct):
param: Child
class ParentParam(ParamDataclass):
child_param: ChildParam

class ChildParam(Param, ParentType[ParentStruct]):
class ChildParam(ParamDataclass, ParentType[ParentParam]):
value: float

struct = ParentStruct(param=ChildParam(value=1.23))
parent_param = ParentParam(child_param=ChildParam(value=1.23))
```

This does nothing to the functionality, but static type checkers will now know that
`struct.param.parent` in the example above is a `ParentStruct` object.
`parent_param.child_param.parent` in the example above is a `ParentParam` object.

[`datetime`]: https://docs.python.org/3/library/datetime.html#datetime-objects
[`astropy.units.quantity`]: https://docs.astropy.org/en/stable/api/astropy.units.Quantity.html#astropy.units.Quantity
[`dataclass`]: https://docs.python.org/3/library/dataclasses.html
[data class]: https://docs.python.org/3/library/dataclasses.html
[`@dataclass`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
[`field`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.field
[`__init__`]: https://docs.python.org/3/reference/datamodel.html#object.__init__
Expand Down
Loading
Loading