Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cc88479
basic scaffold done
kushalkolar Sep 13, 2026
08af418
inheritance
kushalkolar Sep 13, 2026
9bf1c7c
done
kushalkolar Sep 14, 2026
364e8bf
config works!
kushalkolar Sep 14, 2026
8cbe3ee
config on graphics
kushalkolar Sep 14, 2026
0082501
axes config
kushalkolar Sep 14, 2026
bce7736
full config implementation basically works
kushalkolar Sep 14, 2026
593111e
fix
kushalkolar Sep 14, 2026
ad75df4
mixins call Graphic construtors with kwargs nothing is positional
kushalkolar Sep 14, 2026
e8e2f0e
print
kushalkolar Sep 14, 2026
97d6e47
config presets
kushalkolar Sep 14, 2026
5fb0ffd
comments, docstrings
kushalkolar Sep 14, 2026
26a5aab
docstrings
kushalkolar Sep 14, 2026
febb665
remove ConfigValue
kushalkolar Sep 15, 2026
62606a4
comments
kushalkolar Sep 15, 2026
a54c021
much better add graphics mixin using descriptors, examples, fix a test
kushalkolar Sep 15, 2026
575c028
anotehr example
kushalkolar Sep 15, 2026
3b92364
GlobalConfig.to_dict()
kushalkolar Sep 15, 2026
bc25682
docs
kushalkolar Sep 15, 2026
82ba3e1
reset to default config after each screenshot test
kushalkolar Sep 15, 2026
c16ee88
add_<graphics>() stub generator, fix maintain_aspect logic w.r.t. con…
kushalkolar Sep 15, 2026
09dfc8d
change so maintain_aspect can be tested better
kushalkolar Sep 15, 2026
7366725
docstring
kushalkolar Sep 15, 2026
23901a3
reset config after running each docs gallery examle
kushalkolar Sep 15, 2026
dcd63bf
better example
kushalkolar Sep 15, 2026
b8dd4b3
docs
kushalkolar Sep 15, 2026
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
Prev Previous commit
Next Next commit
inheritance
  • Loading branch information
kushalkolar committed Sep 13, 2026
commit 08af418eb7152685af3f6cb3d9e4ffa4c19bc51b
85 changes: 57 additions & 28 deletions fastplotlib/utils/_config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import make_dataclass, field, fields, dataclass
from functools import wraps
from functools import wraps, partial
from typing import get_type_hints, Any


def get_method_name(method: Callable) -> str:
# we can't use __init__ as a dataclass field for the config
if method.__name__ == "__init__":
return "init"

return method.__name__


def inv_get_method_name(name: str) -> str:
# inverse of get_method_name
if name == "init":
return "__init__"

return name


class ConfigValue:
"""Just used to mark a configurable argument"""

Expand All @@ -22,7 +38,7 @@ def __get__(self, instance, cls: type = None):
raise AttributeError("set config options on the class, not an instance")

if cls not in self.__classes.keys():
raise LookupError("Class is not registered")
raise AttributeError("Class is not registered")

return self.__classes[cls]

Expand All @@ -35,6 +51,10 @@ class Pending:
method: Callable # the actual method obj
defaults: dict

@property
def name(self) -> str:
return get_method_name(self.method)

@property
def module(self) -> str:
"""the pending module"""
Expand All @@ -61,7 +81,7 @@ def belongs_to(self, cls: type) -> bool:
"""check if this module belongs to this fully created class object, used for @Config.register"""
return self.cls_qual == (cls.__module__, cls.__qualname__)

def to_dataclass(self) -> object:
def to_config(self) -> object:
"""create the config dataclass for this method"""
type_hints = get_type_hints(self.method)

Expand All @@ -72,12 +92,15 @@ def to_dataclass(self) -> object:
# for each parameter: (arg, type, default value)
signature.append((arg, type_annot, field(default=val)))

return make_dataclass(
self.method.__name__,
mc = make_dataclass(
self.name,
fields=signature,
slots=True,
slots=True, # fields are fixed, user can't do method.something_random = value
eq=False, # == operator makes no sense since values can be any object, arrays, buffers, etc.
)

return mc()


class Config:
"""Global config system"""
Expand All @@ -97,27 +120,34 @@ def descriptor(self) -> ConfigDescriptor:

def register(self, cls):
"""Register a class"""
if not self._pending:
# derive from parent classes
# [1:-1] skips the class itself and bare object
for parent in cls.__mro__[1:-1]:
# TODO: last thing to do
if parent in self._registry:
self._register(cls, parent.config)
return cls

raise LookupError(
f"{cls} has no registered defaults nor any parent class with registered defaults to derive from"
)

if not self._pending[-1].belongs_to(cls):
if self._pending and not self._pending[-1].belongs_to(cls):
raise TypeError(
f"{self._pending[-1].cls_qual} is not registered with the global config"
)

method_dcs = {p.method.__name__: p.to_dataclass() for p in self._pending}
method_configs = {p.name: p.to_config() for p in self._pending}

# derive any un-registered methods from closest parent class that has it
# this is mainly for the ImguiFigure class
# we want Figure.config.show to derive from ImguiFigure.show

# [1:-1] skips the class itself and bare object
parents = cls.__mro__[1:-1]
for parent in parents:
if parent in self._registry:
# get the names of all configurable methods on this parent
for f in fields(self._registry[parent]):
# if the parent has a configurable method that this subclass doesn't have defaults for
if f.name not in method_configs.keys() and hasattr(cls, inv_get_method_name(f.name)):
# use the same method dataclass configuration object for this subclass
method_configs[f.name] = getattr(self._registry[parent], f.name)

if not method_configs:
raise LookupError(
f"{cls} has no registered defaults nor any parent class with registered defaults to derive from"
)

self._register(cls, method_dcs)
self._register(cls, method_configs)

self._pending.clear()

Expand All @@ -127,14 +157,13 @@ def _register(self, cls, method_dcs: dict[str, object]):
# actually adds the class along with all the method configurable dataclasses to the registry
dc = make_dataclass(
cls.__name__,
fields=[
(m, object, field(default_factory=mdc)) for m, mdc in method_dcs.items()
],
slots=True,
frozen=True,
fields=[(m, type(mdc), field(default=mdc)) for m, mdc in method_dcs.items()],
slots=True, # fields are fixed, each class set a fixed set of methods
frozen=True, # can't change method config instances
eq=False, # == operator makes no sense here, every config class is unique anyways
)

self._registry[cls] = dc
self._registry[cls] = dc()

def defaults(self, **defaults):
"""register a method with default kwargs"""
Expand Down