scverse-misc 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from ._extensions import ExtensionNamespace, make_register_namespace_decorator
2
+ from ._pandas_utils import try_convert_dataframe_to_numpy_dtypes, try_convert_series_to_numpy_dtype
3
+ from ._version import __version__, __version_tuple__
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import warnings
5
+ from itertools import islice
6
+ from typing import TYPE_CHECKING, Generic, Literal, Protocol, TypeVar, get_type_hints, overload, runtime_checkable
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable, Set
10
+
11
+
12
+ @runtime_checkable
13
+ class ExtensionNamespace(Protocol):
14
+ """Protocol for extension namespaces.
15
+
16
+ Enforces that the namespace initializer accepts a class with the proper `__init__` method.
17
+ Protocol's can't enforce that the `__init__` accepts the correct types. See
18
+ `_check_namespace_signature` for that. This is mainly useful for static type
19
+ checking with mypy and IDEs.
20
+ """
21
+
22
+ def __init__(self, instance) -> None:
23
+ """Used to enforce the correct signature for extension namespaces."""
24
+
25
+
26
+ # Based off of the extension framework in Polars
27
+ # https://github.com/pola-rs/polars/blob/main/py-polars/polars/api.py
28
+
29
+ __all__ = ["make_register_namespace_decorator", "ExtensionNamespace"]
30
+
31
+
32
+ NameSpT = TypeVar("NameSpT", bound=ExtensionNamespace)
33
+ T = TypeVar("T")
34
+
35
+
36
+ class AccessorNameSpace(ExtensionNamespace, Generic[NameSpT]):
37
+ """Establish property-like namespace object for user-defined functionality."""
38
+
39
+ def __init__(self, name: str, namespace: type[NameSpT]) -> None:
40
+ self._accessor = name
41
+ self._ns = namespace
42
+
43
+ @overload
44
+ def __get__(self, instance: None, cls: type[T]) -> type[NameSpT]: ...
45
+
46
+ @overload
47
+ def __get__(self, instance: T, cls: type[T]) -> NameSpT: ...
48
+
49
+ def __get__(self, instance: T | None, cls: type[T]) -> NameSpT | type[NameSpT]:
50
+ if instance is None:
51
+ return self._ns
52
+
53
+ ns_instance = self._ns(instance) # type: ignore[call-arg]
54
+ setattr(instance, self._accessor, ns_instance)
55
+ return ns_instance
56
+
57
+
58
+ def _check_namespace_signature(ns_class: type, cls: type, canonical_instance_name: str) -> None:
59
+ """Validate the signature of a namespace class for extensions.
60
+
61
+ This function ensures that any class intended to be used as an extension namespace
62
+ has a properly formatted `__init__` method such that:
63
+
64
+ 1. Accepts at least two parameters (self and the instance of the extended class)
65
+ 2. Has `canonical_instance_name` as the name of the second parameter
66
+ 3. Has the second parameter properly type-annotated as `ns_class` or any equivalent import alias
67
+
68
+ The function performs runtime validation of these requirements before a namespace
69
+ can be registered through the `register_namespace` decorator.
70
+
71
+ Args:
72
+ ns_class: The namespace class to validate.
73
+ cls: The class that is being extended.
74
+ canonical_instance_name: The name of the `ns_class` constructor argument.
75
+
76
+ Raises:
77
+ TypeError: If the `__init__` method has fewer than 2 parameters (missing the instance parameter).
78
+ AttributeError: If the second parameter of `__init__` lacks a type annotation.
79
+ TypeError: If the second parameter of `__init__` is not named `canonical_instance_name`.
80
+ TypeError: If the second parameter of `__init__` is not annotated as the `ns_class` class.
81
+ TypeError: If both the name and type annotation of the second parameter are incorrect.
82
+
83
+ """
84
+ sig = inspect.signature(ns_class.__init__)
85
+ params = sig.parameters
86
+
87
+ # Ensure there are at least two parameters (self and mdata)
88
+ if len(params) < 2:
89
+ raise TypeError(f"Namespace initializer must accept a {cls.__name__} instance as the second parameter.")
90
+
91
+ # Get the second parameter (expected to be `canonical_instance_name`)
92
+ param = iter(params.values())
93
+ next(param)
94
+ param = next(param)
95
+ if param.annotation is inspect.Parameter.empty:
96
+ raise AttributeError(
97
+ f"Namespace initializer's second parameter must be annotated as the {cls.__name__!r} class, got empty annotation."
98
+ )
99
+
100
+ name_ok = param.name == canonical_instance_name
101
+
102
+ # Resolve the annotation using get_type_hints to handle forward references and aliases.
103
+ try:
104
+ type_hints = get_type_hints(ns_class.__init__)
105
+ resolved_type = type_hints.get(param.name, param.annotation)
106
+ except NameError as e:
107
+ raise NameError(
108
+ f"Namespace initializer's second parameter must be named {canonical_instance_name!r}, got '{param.name}'."
109
+ ) from e
110
+
111
+ type_ok = resolved_type is cls
112
+
113
+ match (name_ok, type_ok):
114
+ case (True, True):
115
+ return # Signature is correct.
116
+ case (False, True):
117
+ raise TypeError(
118
+ f"Namespace initializer's second parameter must be named {canonical_instance_name!r}, got {param.name!r}."
119
+ )
120
+ case (True, False):
121
+ type_repr = getattr(resolved_type, "__name__", str(resolved_type))
122
+ raise TypeError(
123
+ f"Namespace initializer's second parameter must be annotated as the {cls.__name__!r} class, got {type_repr!r}."
124
+ )
125
+ case _:
126
+ type_repr = getattr(resolved_type, "__name__", str(resolved_type))
127
+ raise TypeError(
128
+ f"Namespace initializer's second parameter must be named {canonical_instance_name!r}, got {param.name!r}. "
129
+ f"And must be annotated as {cls.__name__!r}, got {type_repr!r}."
130
+ )
131
+
132
+
133
+ def _create_namespace(
134
+ name: str, cls: type, reserved_namespaces: Set[str], canonical_instance_name: str
135
+ ) -> Callable[[type[NameSpT]], type[NameSpT]]:
136
+ """Register custom namespace against the underlying class."""
137
+
138
+ def namespace(ns_class: type[NameSpT]) -> type[NameSpT]:
139
+ _check_namespace_signature(ns_class, cls, canonical_instance_name) # Perform the runtime signature check
140
+ if name in reserved_namespaces:
141
+ raise AttributeError(f"cannot override reserved attribute {name!r}")
142
+ elif hasattr(cls, name):
143
+ warnings.warn(
144
+ f"Overriding existing custom namespace {name!r} (on {cls.__name__!r})", UserWarning, stacklevel=2
145
+ )
146
+ setattr(cls, name, AccessorNameSpace(name, ns_class))
147
+ return ns_class
148
+
149
+ return namespace
150
+
151
+
152
+ def _indent_string_lines(string: str, indentation_level: int, skip_lines: int = 0):
153
+ minspace = 1e6
154
+ for line in islice(string.splitlines(), 1, None):
155
+ for i, char in enumerate(line):
156
+ if not char.isspace():
157
+ minspace = min(minspace, i)
158
+ break
159
+ if minspace == 1e6: # single-line string
160
+ minspace = 0
161
+ return "\n".join(
162
+ " " * 4 * indentation_level + sline if i >= skip_lines else sline
163
+ for i, line in enumerate(string.splitlines())
164
+ if (sline := (line[minspace:] if i > 0 else line)) or True
165
+ )
166
+
167
+
168
+ def make_register_namespace_decorator(
169
+ cls: type, canonical_instance_name: str, decorator_name: str, docstring_style: Literal["google", "numpy"] = "google"
170
+ ) -> Callable[[str], Callable[[type[NameSpT]], type[NameSpT]]]:
171
+ """Create a decorator for registering custom functionality with a class.
172
+
173
+ The decorator will allow your users to extend `cls` objects with custom methods and properties
174
+ organized under a namespace. The namespace becomes accessible as an attribute on `cls` instances,
175
+ providing a clean way for users to add domain-specific functionality without modifying the `cls`
176
+ class itself.
177
+
178
+ The return decorator will have a docstring describing how to use it along with examples.
179
+
180
+ Args:
181
+ cls: The class to be made extensible.
182
+ canonical_instance_name: The typical name of an instance of `cls`, e.g. `adata` for `AnnData`. This
183
+ is used for run-time checking of constructor signatures of the namespace classes.
184
+ decorator_name: The name under which the decorator is accessible in your package. This is used for
185
+ the examples in the decorator docstring.
186
+ docstring_style: Whether the docstring of the generated decorator should conform to NumPy or Google
187
+ style.
188
+ """
189
+ # Reserved namespaces include accessors built into cls and all current attributes of cls
190
+ reserved_namespaces = set(dir(cls))
191
+
192
+ def decorator(name: str) -> Callable[[type[NameSpT]], type[NameSpT]]:
193
+ return _create_namespace(name, cls, reserved_namespaces, canonical_instance_name)
194
+
195
+ decorator_arg_description = f"""Name under which the accessor should be registered. This will be the attribute name
196
+ used to access your namespace's functionality on {cls.__name__} objects (e.g., `instance.name`).
197
+ Cannot conflict with existing {cls.__name__} attributes. The list of reserved attributes includes
198
+ everything outputted by `dir({cls.__name__})`."""
199
+ decorator_return_description = "A decorator that registers the decorated class as a custom namespace."
200
+ decorator_notes = f"""Implementation requirements:
201
+
202
+ 1. The decorated class must have an `__init__` method that accepts exactly one parameter
203
+ (besides `self`) named `{canonical_instance_name}` and annotated with type :class:`~{cls.__module__}.{cls.__name__}`.
204
+ 2. The namespace will be initialized with the {cls.__name__} object on first access and then
205
+ cached on the instance.
206
+ 3. If the namespace name conflicts with an existing namespace, a warning is issued.
207
+ 4. If the namespace name conflicts with a built-in {cls.__name__} attribute, an AttributeError is raised."""
208
+ decorator_examples = f""">>> @{decorator_name}("do_something")
209
+ ... class DoSomething:
210
+ ... def __init__(self, {canonical_instance_name}: {cls.__name__}):
211
+ ... self._obj = {canonical_instance_name}
212
+ ...
213
+ ... def has_foo(self) -> bool:
214
+ ... return hasattr(self._obj, "foo")
215
+ >>>
216
+ >>> # Create a {cls.__name__} object
217
+ >>> obj = {cls.__name__}()
218
+ >>>
219
+ >>> # use the registered namespace
220
+ >>> obj.do_something.has_foo()
221
+ False"""
222
+
223
+ decorator.__doc__ = f"""Decorator for registering custom functionality with a :class:`~{cls.__module__}.{cls.__name__}` object.
224
+
225
+ This decorator allows you to extend {cls.__name__} objects with custom methods and properties
226
+ organized under a namespace. The namespace becomes accessible as an attribute on {cls.__name__}
227
+ instances, providing a clean way to you to add domain-specific functionality without modifying
228
+ the {cls.__name__} class itself, or extending the class with additional methods as you see fit in your workflow.
229
+ """
230
+
231
+ if docstring_style == "google":
232
+ decorator.__doc__ += f"""
233
+ Args:
234
+ name: {_indent_string_lines(decorator_arg_description, 3, 1)}
235
+
236
+ Returns:
237
+ {_indent_string_lines(decorator_return_description, 2, 1)}
238
+
239
+ Notes:
240
+ {_indent_string_lines(decorator_notes, 2, 1)}
241
+
242
+ Examples:
243
+ {_indent_string_lines(decorator_examples, 2, 1)}
244
+ """
245
+ else:
246
+ decorator.__doc__ += f"""
247
+ Parameters
248
+ ----------
249
+ name
250
+ {_indent_string_lines(decorator_arg_description, 2, 1)}
251
+
252
+ Returns
253
+ -------
254
+ {_indent_string_lines(decorator_return_description, 1, 1)}
255
+
256
+ Notes
257
+ -----
258
+ {_indent_string_lines(decorator_notes, 1, 1)}
259
+
260
+ Examples
261
+ --------
262
+ {_indent_string_lines(decorator_examples, 1, 1)}
263
+ """
264
+
265
+ return decorator
@@ -0,0 +1,38 @@
1
+ from collections.abc import Mapping
2
+ from contextlib import suppress
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def try_convert_series_to_numpy_dtype(col: pd.Series) -> pd.Series:
8
+ """Attempt to convert a :class:`~pandas.Series` to a non-nullable dtype.
9
+
10
+ Args:
11
+ col: The series to be converted.
12
+
13
+ Returns:
14
+ The converted series, `col` did not contain any :data:`~pandas.NA` values, the unmodified `col` otherwise.
15
+ """
16
+ with suppress(ValueError):
17
+ match col.dtype:
18
+ case pd.BooleanDtype():
19
+ col = col.astype(bool)
20
+ case pd.core.arrays.integer.IntegerDtype(type=dtype) | pd.core.arrays.floating.FloatingDtype(type=dtype):
21
+ col = col.astype(dtype)
22
+ return col
23
+
24
+
25
+ def try_convert_dataframe_to_numpy_dtypes(df: pd.DataFrame | Mapping[str, pd.Series]) -> pd.DataFrame:
26
+ """Attempt to convert all columns of a :class:`~pandas.DataFrame` to their respective non-nullable dtype.
27
+
28
+ Args:
29
+ df: The dataframe to be converted.
30
+
31
+ Returns:
32
+ A new dataframe with each column of `df` that had a nullable dtype but did not contain any :data:`~pandas.NA`
33
+ values converted to the corresponding non-nullable dtype.
34
+ """
35
+ new_cols = {}
36
+ for colname, col in df.items():
37
+ new_cols[colname] = try_convert_series_to_numpy_dtype(col)
38
+ return pd.DataFrame(new_cols)
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.0.1'
32
+ __version_tuple__ = version_tuple = (0, 0, 1)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: scverse-misc
3
+ Version: 0.0.1
4
+ Summary: Miscellaneous utility code used by scverse packages
5
+ Project-URL: Documentation, https://scverse-misc.readthedocs.io/
6
+ Project-URL: Homepage, https://github.com/scverse/scverse-misc
7
+ Project-URL: Source, https://github.com/scverse/scverse-misc
8
+ Author: Ilia Kats
9
+ Maintainer-email: Ilia Kats <i.kats@dkfz.de>
10
+ License: BSD 3-Clause License
11
+
12
+ Copyright (c) 2026, Ilia Kats
13
+ All rights reserved.
14
+
15
+ Redistribution and use in source and binary forms, with or without
16
+ modification, are permitted provided that the following conditions are met:
17
+
18
+ 1. Redistributions of source code must retain the above copyright notice, this
19
+ list of conditions and the following disclaimer.
20
+
21
+ 2. Redistributions in binary form must reproduce the above copyright notice,
22
+ this list of conditions and the following disclaimer in the documentation
23
+ and/or other materials provided with the distribution.
24
+
25
+ 3. Neither the name of the copyright holder nor the names of its
26
+ contributors may be used to endorse or promote products derived from
27
+ this software without specific prior written permission.
28
+
29
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
30
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
31
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
32
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
33
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
34
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
35
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
37
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
+ License-File: LICENSE
40
+ Classifier: Programming Language :: Python :: 3 :: Only
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Programming Language :: Python :: 3.13
44
+ Classifier: Programming Language :: Python :: 3.14
45
+ Requires-Python: >=3.11
46
+ Requires-Dist: pandas>=1
47
+ Requires-Dist: session-info2
48
+ Description-Content-Type: text/markdown
49
+
50
+ # scverse-misc
51
+
52
+ [![Tests][badge-tests]][tests]
53
+ [![codecov][badge-codecov]][codecov]
54
+ [![Documentation][badge-docs]][documentation]
55
+
56
+ [badge-tests]: https://github.com/scverse/scverse-misc/actions/workflows/test.yaml/badge.svg
57
+ [badge-codecov]: https://codecov.io/gh/scverse/scverse-misc/graph/badge.svg?token=EUH9BZZK7T
58
+ [badge-docs]: https://img.shields.io/readthedocs/scverse-misc
59
+
60
+ Miscellaneous utility code used by scverse packages
61
+
62
+ ## Getting started
63
+
64
+ Please refer to the [documentation][],
65
+ in particular, the [API documentation][].
66
+
67
+ ## Installation
68
+
69
+ You need to have Python 3.11 or newer installed on your system.
70
+ If you don't have Python installed, we recommend installing [uv][].
71
+
72
+ There are several alternative options to install scverse-misc:
73
+
74
+ <!--
75
+ 1) Install the latest release of `scverse-misc` from [PyPI][]:
76
+
77
+ ```bash
78
+ pip install scverse-misc
79
+ ```
80
+ -->
81
+
82
+ 1. Install the latest development version:
83
+
84
+ ```bash
85
+ pip install git+https://github.com/scverse/scverse-misc.git@main
86
+ ```
87
+
88
+ ## Release notes
89
+
90
+ See the [changelog][].
91
+
92
+ ## Contact
93
+
94
+ For questions and help requests, you can reach out in the [scverse discourse][].
95
+ If you found a bug, please use the [issue tracker][].
96
+
97
+
98
+ [uv]: https://github.com/astral-sh/uv
99
+ [scverse discourse]: https://discourse.scverse.org/
100
+ [issue tracker]: https://github.com/scverse/scverse-misc/issues
101
+ [tests]: https://github.com/scverse/scverse-misc/actions/workflows/test.yaml
102
+ [codecov]: https://codecov.io/gh/bioFAM/mofaflex
103
+ [documentation]: https://scverse-misc.readthedocs.io
104
+ [changelog]: https://scverse-misc.readthedocs.io/en/latest/changelog.html
105
+ [api documentation]: https://scverse-misc.readthedocs.io/latest/api.html
106
+ [pypi]: https://pypi.org/project/scverse-misc
@@ -0,0 +1,8 @@
1
+ scverse_misc/__init__.py,sha256=xAuMYz1eRg2x5oruK1Jps-OwDUnoIapCGUWMQwaF_AQ,232
2
+ scverse_misc/_extensions.py,sha256=Rjss5vJe9nmwwBZWtL56qsIQ1vjr6ZtxZXJbYoLfWx4,11294
3
+ scverse_misc/_pandas_utils.py,sha256=rZ_POYn0bxb6FnIl418T1vzxEEFtarwt1TR5KEf94Sg,1359
4
+ scverse_misc/_version.py,sha256=qf6R-J7-UyuABBo8c0HgaquJ8bejVbf07HodXgwAwgQ,704
5
+ scverse_misc-0.0.1.dist-info/METADATA,sha256=NFbv_M2qoiMvoSYeirAxw4MYdl4pft1zDthJ19psRYk,4219
6
+ scverse_misc-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ scverse_misc-0.0.1.dist-info/licenses/LICENSE,sha256=gjAb5KNAlstoHZ6PFN5lH7j_DUpda5-FfdwxlIqjDt0,1517
8
+ scverse_misc-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Ilia Kats
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.