Skip to content

Commit

Permalink
delay attribute computation
Browse files Browse the repository at this point in the history
Using a non-data descriptor (https://docs.python.org/3/howto/descriptor.html#descriptor-protocol)
The attribute is not writeable because the Cosmology dataclass is frozen.

Signed-off-by: nstarman <nstarman@users.noreply.github.com>
  • Loading branch information
nstarman committed Jul 10, 2024
1 parent 00f2bba commit 66c2cfa
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 240 deletions.
77 changes: 53 additions & 24 deletions astropy/cosmology/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@

import functools
import operator
from collections.abc import Callable
from dataclasses import Field
from numbers import Number
from typing import TYPE_CHECKING, Any, TypeVar
from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload

import numpy as np

from astropy.units import Quantity

from . import units as cu
from ._signature_deprecations import _depr_kws_wrap

if TYPE_CHECKING:
from astropy.cosmology import Parameter
from collections.abc import Callable

_F = TypeVar("_F", bound=Callable[..., Any])
from astropy.cosmology import Parameter
from astropy.cosmology.core import Cosmology


def vectorize_redshift_method(func=None, nin=1):
Expand Down Expand Up @@ -78,11 +77,6 @@ def aszarr(z):
if isinstance(z, (Number, np.generic)): # scalars
return z
elif hasattr(z, "shape"): # ducktypes NumPy array
if getattr(z, "__module__", "").startswith("pandas"):
# See https://github.com/astropy/astropy/issues/15576. Pandas does not play
# well with others and will ignore unit-ful calculations so we need to
# convert to it's underlying value.
z = z.values
if hasattr(z, "unit"): # Quantity Column
return (z << cu.redshift).value # for speed only use enabled equivs
return z
Expand All @@ -96,7 +90,7 @@ def all_cls_vars(obj: object | type, /) -> dict[str, Any]:
return functools.reduce(operator.__or__, map(vars, cls.mro()[::-1]))


def all_parameters(obj: object, /) -> dict[str, Field | Parameter]:
def all_parameters(obj: object | type, /) -> dict[str, Field | Parameter]:
"""Get all fields of a dataclass, including those not-yet finalized.
Parameters
Expand All @@ -122,21 +116,56 @@ def all_parameters(obj: object, /) -> dict[str, Field | Parameter]:
}


def deprecated_keywords(*kws, since):
"""Deprecate calling one or more arguments as keywords.
R = TypeVar("R")

Parameters
----------
*kws: str
Names of the arguments that will become positional-only.

since : str or number or sequence of str or number
The release at which the old argument became deprecated.
class CachedInDictPropertyDescriptor(Generic[R]):
"""Descriptor for a property that is cached in the instance's dictionary.
Note that this is a non-data descriptor, not NOT a data descriptor, so
after the property is accessed and cached with the same key as the
property, the instance's dictionary will have the property value
directly, and the descriptor will not be called again, until the
property is deleted from the instance's dictionary. See
https://docs.python.org/3/howto/descriptor.html#descriptor-protocol.
"""
return functools.partial(_depr_kws, kws=kws, since=since)

# __slots__ = ("fget", "name") # TODO: when __doc__ is supported by __slots__

def _depr_kws(func: _F, /, kws: tuple[str, ...], since: str) -> _F:
wrapper = _depr_kws_wrap(func, kws, since)
functools.update_wrapper(wrapper, func)
return wrapper
def __init__(self, fget: Callable[[Cosmology], R]) -> None:
self.fget = fget
self.__doc__ = fget.__doc__

def __set_name__(self, cosmo_cls: type[Cosmology], name: str) -> None:
self.name: str = name

@overload
def __get__(
self, cosmo: None, cosmo_cls: Any
) -> CachedInDictPropertyDescriptor[R]: ...

@overload
def __get__(self, cosmo: Cosmology, cosmo_cls: Any) -> R: ...

def __get__(
self, cosmo: Cosmology | None, cosmo_cls: type[Cosmology] | None
) -> R | CachedInDictPropertyDescriptor[R]:
# Accessed from the class, return the descriptor itself
if cosmo is None:
return self

# If the property is not in the instance's dictionary, calculate and store it.
if self.name not in cosmo.__dict__:
cosmo.__dict__[self.name] = self.fget(cosmo)

# Return the property value from the instance's dictionary
# This is only called once, thereafter the property value is accessed directly
# from the instance's dictionary.
return cosmo.__dict__[self.name]


def cached_on_dict_property(
fget: Callable[[Cosmology], R],
) -> CachedInDictPropertyDescriptor[R]:
"""Descriptor for a property that is cached in the instance's dictionary."""
return CachedInDictPropertyDescriptor(fget)
Loading

0 comments on commit 66c2cfa

Please sign in to comment.