diff --git a/pyproject.toml b/pyproject.toml index 200f926e8..52dc76e75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "legacy-api-wrap", "zarr >=3.1", "typing-extensions; python_version<'3.13'", - "scverse-misc>=0.0.3", + "scverse-misc[settings]@git+https://github.com/scverse/scverse-misc@pa/reset", ] dynamic = [ "version" ] @@ -130,6 +130,8 @@ dask = [ [tool.hatch.version] source = "vcs" raw-options.version_scheme = "release-branch-semver" +[tool.hatch.metadata] +allow-direct-references = true [tool.hatch.build.targets.wheel] packages = [ "src/anndata", "src/testing" ] @@ -259,6 +261,7 @@ extend-immutable-calls = [ "slice" ] "warnings.warn".msg = "Use anndata.utils.warn instead" [tool.ruff.lint.flake8-type-checking] exempt-modules = [ ] +runtime-evaluated-base-classes = [ "scverse_misc.Settings" ] strict = true [tool.ruff.lint.pylint] max-args = 7 diff --git a/src/anndata/_settings.py b/src/anndata/_settings.py index a08c80d4f..e4eb6556d 100644 --- a/src/anndata/_settings.py +++ b/src/anndata/_settings.py @@ -1,532 +1,42 @@ from __future__ import annotations -import inspect -import os -import textwrap -from collections.abc import Iterable -from contextlib import contextmanager -from dataclasses import dataclass, field, fields -from enum import Enum -from functools import partial -from inspect import Parameter, signature -from types import GenericAlias, NoneType -from typing import TYPE_CHECKING, Any, NamedTuple, cast +from typing import Literal -from ._warnings import warn -from .compat import old_positionals +import scverse_misc -if TYPE_CHECKING: - from collections.abc import Callable, Generator, Sequence - from typing import Any, Self, TypeGuard +class Settings( + scverse_misc.Settings, exported_object_name="settings", docstring_style="scverse" +): + remove_unused_categories: bool = True + """Whether or not to remove unused categories with :class:`~pandas.Categorical`.""" -class DeprecatedOption(NamedTuple): - option: str - message: str | None - removal_version: str | None + check_uniqueness: bool = True + """Whether or not to check uniqueness of the `obs` indices on `__init__` of :class:`~anndata.AnnData`.""" + copy_on_write_X: bool = False + """Whether to copy-on-write X. Currently `my_adata_view[subset].X = value` will write back to the original AnnData object at the `subset` location. `X` is the only element where this behavior is implemented though.""" -def _is_plain_type(obj: object) -> TypeGuard[type]: - return isinstance(obj, type) and not isinstance(obj, GenericAlias) + allow_write_nullable_strings: bool | None = None + """Whether or not to allow writing of `pd.arrays.[Arrow]StringArray`. When set to `None`, it will be inferred from `pd.options.future.infer_string`. When set to `False` explicitly, we will try writing `string` arrays in the old, non-nullable format.""" + zarr_write_format: Literal[2, 3] = 2 + """Which version of zarr to write to when anndata must internally open a write-able zarr group.""" -def describe(self: RegisteredOption, *, as_rst: bool = False) -> str: - type_str = self.type.__name__ if _is_plain_type(self.type) else str(self.type) - if as_rst: - default_str = repr(self.default_value).replace("\\", "\\\\") - doc = f"""\ - .. attribute:: settings.{self.option} - :type: {type_str} - :value: {default_str} + use_sparse_array_on_read: bool = False + """Whether or not to use :class:`scipy.sparse.sparray` as the default class when reading in data""" - {self.description} - """ - else: - doc = f"""\ - {self.option}: `{type_str}` - {self.description} (default: `{self.default_value!r}`). - """ - return textwrap.dedent(doc) + min_rows_for_chunked_h5_copy: int = 1000 + """Minimum number of rows at a time to copy when writing out an H5 Dataset to a new location""" + disallow_forward_slash_in_h5ad: bool = False + """Whether or not to disallow the `/` character in keys for h5ad files""" -class RegisteredOption[T](NamedTuple): - option: str - default_value: T - description: str - validate: Callable[[T, SettingsManager], None] - type: object + write_csr_csc_indices_with_min_possible_dtype: bool = False + """Write a csr or csc matrix with the minimum possible data type for `indices`, always unsigned integer.""" - describe = describe + auto_shard_zarr_v3: bool | None = None + """Whether or not to use zarr's auto computation of sharding for v3. For v2 this setting will be ignored. The setting will apply to all calls to anndata's writing mechanism (write_zarr / write_elem) and will **not** override any user-defined kwargs for shards.""" -def check_and_get_environ_var[T]( - key: str, - default_value: str, - allowed_values: Sequence[str] | None = None, - cast: Callable[[Any], T] | type[Enum] = lambda x: x, -) -> T: - """Get the environment variable and return it is a (potentially) non-string, usable value. - - Parameters - ---------- - key - The environment variable name. - default_value - The default value for `os.environ.get`. - allowed_values - Allowable string values., by default None - cast - Casting from the string to a (potentially different) python object, by default lambdax:x - - Returns - ------- - The casted value. - """ - environ_value_or_default_value = os.environ.get(key, default_value) - if ( - allowed_values is not None - and environ_value_or_default_value not in allowed_values - ): - msg = ( - f"Value {environ_value_or_default_value!r} is not in allowed {allowed_values} for environment variable {key}. " - f"Default {default_value} will be used." - ) - warn(msg, UserWarning) - environ_value_or_default_value = default_value - return ( - cast(environ_value_or_default_value) - if not isinstance(cast, type(Enum)) - else cast[environ_value_or_default_value] - ) - - -def check_and_get_bool(option: str, default_value: bool) -> bool: # noqa: FBT001 - return check_and_get_environ_var( - f"ANNDATA_{option.upper()}", - str(int(default_value)), - ["0", "1"], - lambda x: bool(int(x)), - ) - - -def check_and_get_bool_or_none(option: str, default_value: bool | None) -> bool | None: # noqa: FBT001 - return check_and_get_environ_var( - f"ANNDATA_{option.upper()}", - "" if default_value is None else str(int(default_value)), - ["0", "1", ""], - lambda x: None if x == "" else bool(int(x)), - ) - - -def check_and_get_int(option: str, default_value: int) -> int: - return check_and_get_environ_var( - f"ANNDATA_{option.upper()}", str(int(default_value)), None, int - ) - - -_docstring = """ -This manager allows users to customize settings for the anndata package. -Settings here will generally be for advanced use-cases and should be used with caution. - -The following options are available: - -{options_description} - -For setting an option please use :func:`~anndata.settings.override` (local) or set the above attributes directly (global) i.e., `anndata.settings.my_setting = foo`. -For assignment by environment variable, use the variable name in all caps with `ANNDATA_` as the prefix before import of :mod:`anndata`. -For boolean environment variable setting, use 1 for `True` and 0 for `False`. -""" - - -@dataclass -class SettingsManager: - _registered_options: dict[str, RegisteredOption] = field(default_factory=dict) - _deprecated_options: dict[str, DeprecatedOption] = field(default_factory=dict) - _config: dict[str, object] = field(default_factory=dict) - __doc_tmpl__: str = _docstring - - def describe( - self, - option: str | Iterable[str] | None = None, - *, - should_print_description: bool = True, - as_rst: bool = False, - ) -> str: - """Print and/or return a (string) description of the option(s). - - Parameters - ---------- - option - Option(s) to be described, by default None (i.e., do all option) - should_print_description - Whether or not to print the description in addition to returning it. - - Returns - ------- - The description. - """ - describe = partial( - self.describe, - should_print_description=should_print_description, - as_rst=as_rst, - ) - if option is None: - return describe(self._registered_options.keys()) - if isinstance(option, Iterable) and not isinstance(option, str): - return "\n".join([describe(k) for k in option]) - registered_option = self._registered_options[option] - doc = registered_option.describe(as_rst=as_rst).rstrip("\n") - if option in self._deprecated_options: - opt = self._deprecated_options[option] - if opt.message is not None: - doc += f" *{opt.message}" - doc += f" {option} will be removed in {opt.removal_version}.*" - if should_print_description: - print(doc) - return doc - - def deprecate( - self, option: str, removal_version: str, message: str | None = None - ) -> None: - """Deprecate options with a message at a version. - - Parameters - ---------- - option - Which option should be deprecated. - removal_version - The version targeted for removal. - message - A custom message. - """ - self._deprecated_options[option] = DeprecatedOption( - option, message, removal_version - ) - - @old_positionals("default_value", "description", "validate", "option_type") - def register[T]( - self, - option: str, - *, - default_value: T, - description: str, - validate: Callable[[T, Self], None], - option_type: object | None = None, - get_from_env: Callable[[str, T], T] = lambda x, y: y, - ) -> None: - """Register an option so it can be set/described etc. by end-users - - Parameters - ---------- - option - Option to be set. - default_value - Default value with which to set the option. - description - Description to be used in the docstring. - validate - A function which raises a `ValueError` or `TypeError` if the value is invalid. - option_type - Optional override for the option type to be displayed. Otherwise `type(default_value)`. - get_from_env - An optional function which takes as arguments the name of the option and a default value and returns the value from the environment variable `ANNDATA_CAPS_OPTION` (or default if not present). - Default behavior is to return `default_value` without checking the environment. - """ - try: - validate(default_value, self) - except (ValueError, TypeError) as e: - e.add_note(f"for option {option!r}") - raise e - option_type = type(default_value) if option_type is None else option_type - self._registered_options[option] = RegisteredOption( - option, default_value, description, validate, option_type - ) - self._config[option] = get_from_env(option, default_value) - self._update_override_function_for_new_option(option) - - def _update_override_function_for_new_option( - self, - option: str, - ) -> None: - """This function updates the keyword arguments, docstring, and annotations of the `SettingsManager.override` function as the `SettingsManager.register` method is called. - - Parameters - ---------- - option - The option being registered for which the override function needs updating. - """ - option_type = self._registered_options[option].type - # Update annotations for type checking. - self.override.__annotations__[option] = option_type - # __signature__ needs to be updated for tab autocompletion in IPython. - # See https://github.com/ipython/ipython/issues/11624 for inspiration. - self.override.__func__.__signature__ = signature(self.override).replace( - parameters=[ - Parameter(name="self", kind=Parameter.POSITIONAL_ONLY), - *[ - Parameter( - name=k, - annotation=option_type, - kind=Parameter.KEYWORD_ONLY, - ) - for k in self._registered_options - ], - ] - ) - # Update docstring for `SettingsManager.override` as well. - doc = textwrap.dedent(cast("str", self.override.__doc__)) - insert_index = doc.find("\n\nYields") - assert insert_index != -1 - option_docstring = "".join( - self.describe(option, should_print_description=False).splitlines( - keepends=True - ) - ) - self.override.__func__.__doc__ = ( - f"{doc[:insert_index]}\n{option_docstring}{doc[insert_index:]}" - ) - - def __setattr__(self, option: str, val: object) -> None: - """ - Set an option to a value. To see the allowed option to be set and their description, - use describe_option. - - Parameters - ---------- - option - Option to be set. - val - Value with which to set the option. - - Raises - ------ - AttributeError - If the option has not been registered, this function will raise an error. - """ - if option in {f.name for f in fields(self)}: - return super().__setattr__(option, val) - elif option not in self._registered_options: - msg = ( - f"{option} is not an available option for anndata. " - "Please open an issue if you believe this is a mistake." - ) - raise AttributeError(msg) - registered_option = self._registered_options[option] - registered_option.validate(val, self) - self._config[option] = val - - def __getattr__(self, option: str) -> object: - """ - Gets the option's value. - - Parameters - ---------- - option - Option to be got. - - Returns - ------- - Value of the option. - """ - if option in self._deprecated_options: - deprecated = self._deprecated_options[option] - msg = f"{option!r} will be removed in {deprecated.removal_version}. {deprecated.message}" - warn(msg, FutureWarning) - if option in self._config: - return self._config[option] - msg = f"{option} not found." - raise AttributeError(msg) - - def __dir__(self) -> Iterable[str]: - return sorted((*super().__dir__(), *self._config.keys())) - - def reset(self, option: Iterable[str] | str) -> None: - """ - Resets option(s) to its (their) default value(s). - - Parameters - ---------- - option - The option(s) to be reset. - """ - if isinstance(option, Iterable) and not isinstance(option, str): - for opt in option: - self.reset(opt) - else: - self._config[option] = self._registered_options[option].default_value - - @contextmanager - def override(self, **overrides) -> Generator[None]: - """ - Provides local override via keyword arguments as a context manager. - - Parameters - ---------- - - Yields - ------ - None - """ - restore = {a: getattr(self, a) for a in overrides} - try: - # Preserve order so that settings that depend on each other can be overridden together i.e., always override zarr version before sharding. - # Otherwise an error would be raised setting sharding before zarr version if the zarr version is 2. - for k in self._config: - if k in overrides: - setattr(self, k, overrides.get(k)) - yield None - finally: - # In the try block, we went in the forward order i.e., zarr version before sharding, but in the reset here, we go in the reverse order i.e., sharding before zarr version. - # Otherwise an error would be raised if we reversed the zarr version first and it was 3 previously. - for k in reversed(self._config.keys()): - if k in restore: - setattr(self, k, restore.get(k)) - - def __repr__(self) -> str: - params = "".join(f"\t{k}={v!r},\n" for k, v in self._config.items()) - return f"{type(self).__name__}(\n{params}\n)" - - @property - def __doc__(self): - in_sphinx = any("/sphinx/" in frame.filename for frame in inspect.stack()) - options_description = self.describe( - should_print_description=False, as_rst=in_sphinx - ) - return self.__doc_tmpl__.format( - options_description=options_description, - ) - - -settings = SettingsManager() - -################################################################################## -# PLACE REGISTERED SETTINGS HERE SO THEY CAN BE PICKED UP FOR DOCSTRING CREATION # -################################################################################## - - -def gen_validator[V]( - _type: type[V] | tuple[type[V], ...], / -) -> Callable[[V, SettingsManager], None]: - def validate_type(val: V, settings: SettingsManager) -> None: - if not isinstance(val, _type): - msg = f"{val} not valid {_type}" - raise TypeError(msg) - - return validate_type - - -validate_bool = gen_validator(bool) -validate_int = gen_validator(int) - - -settings.register( - "remove_unused_categories", - default_value=True, - description="Whether or not to remove unused categories with :class:`~pandas.Categorical`.", - validate=validate_bool, - get_from_env=check_and_get_bool, -) - -settings.register( - "check_uniqueness", - default_value=True, - description=( - "Whether or not to check uniqueness of the `obs` indices on `__init__` of :class:`~anndata.AnnData`." - ), - validate=validate_bool, - get_from_env=check_and_get_bool, -) - -settings.register( - "allow_write_nullable_strings", - default_value=None, - description=( - "Whether or not to allow writing of `pd.arrays.[Arrow]StringArray`. " - "When set to `None`, it will be inferred from `pd.options.future.infer_string`. " - "When set to `False` explicitly, we will try writing `string` arrays in the old, non-nullable format." - ), - validate=gen_validator((bool, NoneType)), - option_type=bool | None, - get_from_env=check_and_get_bool_or_none, -) - - -def validate_zarr_write_format(format: int, settings: SettingsManager): - validate_int(format, settings) - if format not in {2, 3}: - msg = "non-v2 zarr on-disk format not supported" - raise ValueError(msg) - - -settings.register( - "zarr_write_format", - default_value=2, - description="Which version of zarr to write to when anndata must internally open a write-able zarr group.", - validate=validate_zarr_write_format, - get_from_env=lambda name, default: check_and_get_environ_var( - f"ANNDATA_{name.upper()}", str(default), ["2", "3"], int - ), -) - - -def validate_sparse_settings(val: Any, settings: SettingsManager) -> None: - validate_bool(val, settings) - - -settings.register( - "use_sparse_array_on_read", - default_value=False, - description="Whether or not to use :class:`scipy.sparse.sparray` as the default class when reading in data", - validate=validate_bool, - get_from_env=check_and_get_bool, -) - -settings.register( - "min_rows_for_chunked_h5_copy", - default_value=1000, - description="Minimum number of rows at a time to copy when writing out an H5 Dataset to a new location", - validate=validate_int, - get_from_env=check_and_get_int, -) - -settings.register( - "disallow_forward_slash_in_h5ad", - default_value=False, - description="Whether or not to disallow the `/` character in keys for h5ad files", - validate=validate_bool, - get_from_env=check_and_get_bool, -) - -settings.register( - "write_csr_csc_indices_with_min_possible_dtype", - default_value=False, - description="Write a csr or csc matrix with the minimum possible data type for `indices`, always unsigned integer.", - validate=validate_bool, - get_from_env=check_and_get_bool, -) - -settings.register( - "auto_shard_zarr_v3", - default_value=None, - description="Whether or not to use zarr's auto computation of sharding for v3. For v2 this setting will be ignored. The setting will apply to all calls to anndata's writing mechanism (write_zarr / write_elem) and will **not** override any user-defined kwargs for shards.", - validate=gen_validator((bool, NoneType)), - option_type=bool | None, - get_from_env=check_and_get_bool_or_none, -) - - -settings.register( - "copy_on_write_X", - default_value=False, - description=( - "Whether to copy-on-write X. " - "Currently `my_adata_view[subset].X = value` will write back to the original AnnData object at the `subset` location. " - "`X` is the only element where this behavior is implemented though." - ), - validate=validate_bool, - get_from_env=check_and_get_bool, -) - - -################################################################################## -################################################################################## +settings = Settings() diff --git a/src/testing/anndata/_pytest.py b/src/testing/anndata/_pytest.py index 5a96c4b79..2355858eb 100644 --- a/src/testing/anndata/_pytest.py +++ b/src/testing/anndata/_pytest.py @@ -38,7 +38,7 @@ def setup_env() -> None: import anndata - anndata.settings.reset(anndata.settings._registered_options.keys()) + anndata.settings.reset(*list(anndata.settings.model_fields_set)) if IS_PRE: # https://pandas.pydata.org/docs/whatsnew/v2.3.0.html#upcoming-changes-in-pandas-3-0 diff --git a/tests/test_settings.py b/tests/test_settings.py deleted file mode 100644 index 4e4c2b537..000000000 --- a/tests/test_settings.py +++ /dev/null @@ -1,280 +0,0 @@ -from __future__ import annotations - -import importlib.machinery -import os -import re -import types -from enum import Enum -from pathlib import Path - -import pytest - -import anndata as ad -from anndata._settings import ( - SettingsManager, - check_and_get_bool, - check_and_get_environ_var, - validate_bool, -) - -option = "test_var" -default_val = False -description = "My doc string!" - -option_2 = "test_var_2" -default_val_2 = False -description_2 = "My doc string 2!" - -option_3 = "test_var_3" -default_val_3 = [1, 2] -description_3 = "My doc string 3!" -type_3 = list[int] - - -def validate_int_list(val: list, settings: SettingsManager) -> bool: - if not isinstance(val, list) or not [isinstance(type(e), int) for e in val]: - msg = f"{val!r} is not a valid int list" - raise TypeError(msg) - return True - - -@pytest.fixture -def settings() -> SettingsManager: - settings = SettingsManager() - settings.register( - option, - default_value=default_val, - description=description, - validate=validate_bool, - ) - settings.register( - option_2, - default_value=default_val_2, - description=description_2, - validate=validate_bool, - ) - settings.register( - option_3, - default_value=default_val_3, - description=description_3, - validate=validate_int_list, - option_type=type_3, - ) - return settings - - -def test_register_option_default(settings: SettingsManager): - assert getattr(settings, option) == default_val - assert description in settings.describe(option) - - -def test_register_with_env(settings: SettingsManager, monkeypatch: pytest.MonkeyPatch): - option_env = "test_var_env" - default_val_env = False - description_env = "My doc string env!" - option_env_var = "ANNDATA_" + option_env.upper() - monkeypatch.setenv(option_env_var, "1") - - settings.register( - option_env, - default_value=default_val_env, - description=description_env, - validate=validate_bool, - get_from_env=check_and_get_bool, - ) - - assert settings.test_var_env - - -def test_register_with_env_enum( - settings: SettingsManager, monkeypatch: pytest.MonkeyPatch -): - option_env = "test_var_env" - default_val_env = False - description_env = "My doc string env!" - option_env_var = "ANNDATA_" + option_env.upper() - monkeypatch.setenv(option_env_var, "b") - - class TestEnum(Enum): - a = False - b = True - - def check_and_get_bool_enum(option, default_value): - return check_and_get_environ_var( - "ANNDATA_" + option.upper(), "a", cast=TestEnum - ).value - - settings.register( - option_env, - default_value=default_val_env, - description=description_env, - validate=validate_bool, - get_from_env=check_and_get_bool_enum, - ) - - assert settings.test_var_env - - -def test_register_bad_option(settings: SettingsManager): - with pytest.raises(TypeError, match=r"'foo' is not a valid int list"): - settings.register( - "test_var_4", - default_value="foo", # should be a list of ints - description=description_3, - validate=validate_int_list, - option_type=type_3, - ) - - -def test_set_option(settings: SettingsManager): - setattr(settings, option, not default_val) - assert getattr(settings, option) == (not default_val) - settings.reset(option) - assert getattr(settings, option) == default_val - - -def test_dir(settings: SettingsManager): - assert {option, option_2, option_3} <= set(dir(settings)) - assert dir(settings) == sorted(dir(settings)) - - -def test_reset_multiple(settings: SettingsManager): - setattr(settings, option, not default_val) - setattr(settings, option_2, not default_val_2) - settings.reset([option, option_2]) - assert getattr(settings, option) == default_val - assert getattr(settings, option_2) == default_val_2 - - -def test_get_unregistered_option(settings: SettingsManager): - with pytest.raises(AttributeError): - setattr(settings, option + "_different", default_val) - - -def test_override(settings: SettingsManager): - with settings.override(**{option: not default_val}): - assert getattr(settings, option) == (not default_val) - assert getattr(settings, option) == default_val - - -def test_override_multiple(settings: SettingsManager): - with settings.override(**{option: not default_val, option_2: not default_val_2}): - assert getattr(settings, option) == (not default_val) - assert getattr(settings, option_2) == (not default_val_2) - assert getattr(settings, option) == default_val - assert getattr(settings, option_2) == default_val_2 - - -def test_deprecation(settings: SettingsManager): - warning = "This is a deprecation warning!" - version = "0.1.0" - settings.deprecate(option, version, warning) - described_option = settings.describe(option, should_print_description=False) - # first line is message, second two from deprecation - default_deprecation_message = f"{option} will be removed in {version}.*" - assert described_option.endswith(default_deprecation_message) - described_option = ( - described_option.rstrip().removesuffix(default_deprecation_message).rstrip() - ) - assert described_option.endswith(warning) - with pytest.warns( - FutureWarning, - match=r"'test_var' will be removed in 0\.1\.0\. This is a deprecation warning!", - ): - assert getattr(settings, option) == default_val - - -def test_deprecation_no_message(settings: SettingsManager): - version = "0.1.0" - settings.deprecate(option, version) - described_option = settings.describe(option, should_print_description=False) - # first line is message, second from deprecation version - assert described_option.endswith(f"{option} will be removed in {version}.*") - - -def test_option_typing(settings: SettingsManager): - assert settings._registered_options[option_3].type == type_3 - assert str(type_3) in settings.describe(option_3, should_print_description=False) - - -def test_check_and_get_environ_var(monkeypatch: pytest.MonkeyPatch): - option_env_var = "ANNDATA_OPTION" - assert hash("foo") == check_and_get_environ_var( - option_env_var, "foo", ["foo", "bar"], hash - ) - monkeypatch.setenv(option_env_var, "bar") - assert hash("bar") == check_and_get_environ_var( - option_env_var, "foo", ["foo", "bar"], hash - ) - monkeypatch.setenv(option_env_var, "Not foo or bar") - with pytest.warns( - match=f"Value '{re.escape(os.environ[option_env_var])}' is not in allowed" - ): - check_and_get_environ_var(option_env_var, "foo", ["foo", "bar"], hash) - assert hash("Not foo or bar") == check_and_get_environ_var( - option_env_var, "foo", cast=hash - ) - - -def test_check_and_get_bool(monkeypatch: pytest.MonkeyPatch): - option_env_var = f"ANNDATA_{option.upper()}" - assert not check_and_get_bool(option, default_val) - monkeypatch.setenv(option_env_var, "1") - assert check_and_get_bool(option, default_val) - monkeypatch.setenv(option_env_var, "Not 0 or 1") - with pytest.warns( - match=f"Value '{re.escape(os.environ[option_env_var])}' is not in allowed" - ): - check_and_get_bool(option, default_val) - - -def test_check_and_get_bool_enum(monkeypatch: pytest.MonkeyPatch): - option_env_var = f"ANNDATA_{option.upper()}" - monkeypatch.setenv(option_env_var, "b") - - class TestEnum(Enum): - a = False - b = True - - assert check_and_get_environ_var(option_env_var, "a", cast=TestEnum).value - - -@pytest.mark.parametrize( - ("as_rst", "expected"), - [ - pytest.param( - True, - ( - ".. attribute:: settings.test_var_3\n" - " :type: list[int]\n" - " :value: [1, 2]\n" - "\n" - " My doc string 3!" - ), - id="rst", - ), - pytest.param( - False, - "test_var_3: `list[int]`\n My doc string 3! (default: `[1, 2]`).", - id="plain", - ), - ], -) -def test_describe(*, as_rst: bool, expected: str, settings: SettingsManager): - assert settings.describe("test_var_3", as_rst=as_rst) == expected - - -def test_hints() -> None: - settings = ad.settings - types_loader = importlib.machinery.SourceFileLoader( - "settings_types", - Path(ad.__file__).parent / "_settings.pyi", - ) - settings_types_mod = types.ModuleType(types_loader.name) - types_loader.exec_module(settings_types_mod) - - obj_attrs, typing_attrs = ( - {k for k in dir(o) if not k.startswith("_")} - for o in (settings, settings_types_mod._AnnDataSettingsManager) - ) - assert obj_attrs == typing_attrs