scverse-misc 0.0.2__tar.gz → 0.0.4__tar.gz

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.
Files changed (47) hide show
  1. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/workflows/test.yaml +4 -4
  2. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.pre-commit-config.yaml +13 -3
  3. scverse_misc-0.0.4/CHANGELOG.md +40 -0
  4. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/PKG-INFO +7 -5
  5. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/README.md +1 -1
  6. scverse_misc-0.0.4/docs/api/settings.rst +8 -0
  7. scverse_misc-0.0.4/docs/api.md +44 -0
  8. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/conf.py +3 -2
  9. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/extensions/typed_returns.py +6 -4
  10. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/pyproject.toml +12 -6
  11. scverse_misc-0.0.4/src/scverse_misc/__init__.py +11 -0
  12. scverse_misc-0.0.4/src/scverse_misc/_deprecated.py +81 -0
  13. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/src/scverse_misc/_extensions.py +22 -24
  14. scverse_misc-0.0.4/src/scverse_misc/_settings.py +166 -0
  15. scverse_misc-0.0.4/src/scverse_misc/_utils.py +25 -0
  16. scverse_misc-0.0.4/stubs/sphinxcontrib/__init__.pyi +2 -0
  17. scverse_misc-0.0.4/stubs/sphinxcontrib/katex.pyi +8 -0
  18. scverse_misc-0.0.4/tests/test_deprecation_decorator.py +65 -0
  19. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/tests/test_extensions.py +16 -10
  20. scverse_misc-0.0.4/tests/test_settings.py +129 -0
  21. scverse_misc-0.0.2/CHANGELOG.md +0 -15
  22. scverse_misc-0.0.2/docs/api.md +0 -22
  23. scverse_misc-0.0.2/src/scverse_misc/__init__.py +0 -2
  24. scverse_misc-0.0.2/src/scverse_misc/_version.py +0 -34
  25. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.codecov.yaml +0 -0
  26. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.cruft.json +0 -0
  27. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.editorconfig +0 -0
  28. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  29. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  30. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/workflows/build.yaml +0 -0
  32. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/workflows/release.yaml +0 -0
  33. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.gitignore +0 -0
  34. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.readthedocs.yaml +0 -0
  35. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/LICENSE +0 -0
  36. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/biome.jsonc +0 -0
  37. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/Makefile +0 -0
  38. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_static/.gitkeep +0 -0
  39. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_static/css/custom.css +0 -0
  40. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_templates/.gitkeep +0 -0
  41. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_templates/autosummary/class.rst +0 -0
  42. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/changelog.md +0 -0
  43. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/contributing.md +0 -0
  44. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/index.md +0 -0
  45. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/references.bib +0 -0
  46. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/references.md +0 -0
  47. {scverse_misc-0.0.2 → scverse_misc-0.0.4}/tests/conftest.py +0 -0
@@ -27,12 +27,12 @@ jobs:
27
27
  outputs:
28
28
  envs: ${{ steps.get-envs.outputs.envs }}
29
29
  steps:
30
- - uses: actions/checkout@v5
30
+ - uses: actions/checkout@v6
31
31
  with:
32
32
  filter: blob:none
33
33
  fetch-depth: 0
34
34
  - name: Install uv
35
- uses: astral-sh/setup-uv@v7
35
+ uses: astral-sh/setup-uv@v8.1.0
36
36
  - name: Get test environments
37
37
  id: get-envs
38
38
  run: |
@@ -64,12 +64,12 @@ jobs:
64
64
  continue-on-error: ${{ contains(matrix.env.name, 'pre') }} # make "all-green" pass even if pre-release job fails
65
65
 
66
66
  steps:
67
- - uses: actions/checkout@v5
67
+ - uses: actions/checkout@v6
68
68
  with:
69
69
  filter: blob:none
70
70
  fetch-depth: 0
71
71
  - name: Install uv
72
- uses: astral-sh/setup-uv@v7
72
+ uses: astral-sh/setup-uv@v8.1.0
73
73
  with:
74
74
  python-version: ${{ matrix.env.python }}
75
75
  - name: create hatch environment
@@ -7,16 +7,16 @@ default_stages:
7
7
  minimum_pre_commit_version: 2.16.0
8
8
  repos:
9
9
  - repo: https://github.com/biomejs/pre-commit
10
- rev: v2.4.6
10
+ rev: v2.4.12
11
11
  hooks:
12
12
  - id: biome-format
13
13
  exclude: ^\.cruft\.json$ # inconsistent indentation with cruft - file never to be modified manually.
14
14
  - repo: https://github.com/tox-dev/pyproject-fmt
15
- rev: v2.16.2
15
+ rev: v2.21.1
16
16
  hooks:
17
17
  - id: pyproject-fmt
18
18
  - repo: https://github.com/astral-sh/ruff-pre-commit
19
- rev: v0.15.5
19
+ rev: v0.15.11
20
20
  hooks:
21
21
  - id: ruff-check
22
22
  types_or: [python, pyi, jupyter]
@@ -36,3 +36,13 @@ repos:
36
36
  # Check that there are no merge conflicts (could be generated by template sync)
37
37
  - id: check-merge-conflict
38
38
  args: [--assume-in-merge]
39
+ - repo: https://github.com/pre-commit/mirrors-mypy
40
+ rev: v1.20.1
41
+ hooks:
42
+ - id: mypy
43
+ args: []
44
+ additional_dependencies:
45
+ - pydantic-settings
46
+ - pytest
47
+ - sphinx
48
+ - sphinxcontrib-katex
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog][],
6
+ and this project adheres to [Semantic Versioning][].
7
+
8
+ [keep a changelog]: https://keepachangelog.com/en/1.1.0/
9
+ [semantic versioning]: https://semver.org/spec/v2.0.0.html
10
+
11
+ ## [0.0.4]
12
+
13
+ ## Added
14
+
15
+ - A `Settings` base class that packages can inherit from for their settings. This is based
16
+ on [Pydantic Settings](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/) and
17
+ provides validation for settings values as well as loading settings from environment variables and
18
+ `.env` files.
19
+
20
+ ## [0.0.3]
21
+
22
+ ## Added
23
+
24
+ - A `deprecated` decorator wrapping `warnings.deprecated` that additionally modifies the
25
+ docstring to include a deprecation notice.
26
+
27
+ ## [0.0.2]
28
+
29
+ ## Removed
30
+
31
+ - The Pandas utility functions
32
+
33
+ ## [0.0.1]
34
+
35
+ - Initial release
36
+
37
+ [0.0.4]: https://github.com/scverse/scverse-misc/releases/tag/v0.0.4
38
+ [0.0.3]: https://github.com/scverse/scverse-misc/releases/tag/v0.0.3
39
+ [0.0.2]: https://github.com/scverse/scverse-misc/releases/tag/v0.0.2
40
+ [0.0.1]: https://github.com/scverse/scverse-misc/releases/tag/v0.0.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scverse-misc
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Miscellaneous utility code used by scverse packages
5
5
  Project-URL: Documentation, https://scverse-misc.readthedocs.io/
6
6
  Project-URL: Homepage, https://github.com/scverse/scverse-misc
@@ -38,13 +38,15 @@ License: BSD 3-Clause License
38
38
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
39
  License-File: LICENSE
40
40
  Classifier: Programming Language :: Python :: 3 :: Only
41
- Classifier: Programming Language :: Python :: 3.11
42
41
  Classifier: Programming Language :: Python :: 3.12
43
42
  Classifier: Programming Language :: Python :: 3.13
44
43
  Classifier: Programming Language :: Python :: 3.14
45
- Requires-Python: >=3.11
46
- Requires-Dist: pandas>=1
44
+ Requires-Python: >=3.12
47
45
  Requires-Dist: session-info2
46
+ Requires-Dist: typing-extensions; python_version < '3.13'
47
+ Provides-Extra: settings
48
+ Requires-Dist: pydantic-settings; extra == 'settings'
49
+ Requires-Dist: python-dotenv; extra == 'settings'
48
50
  Description-Content-Type: text/markdown
49
51
 
50
52
  # scverse-misc
@@ -101,7 +103,7 @@ If you found a bug, please use the [issue tracker][].
101
103
  [scverse discourse]: https://discourse.scverse.org/
102
104
  [issue tracker]: https://github.com/scverse/scverse-misc/issues
103
105
  [tests]: https://github.com/scverse/scverse-misc/actions/workflows/test.yaml
104
- [codecov]: https://codecov.io/gh/bioFAM/mofaflex
106
+ [codecov]: https://codecov.io/gh/scverse/scverse-misc
105
107
  [documentation]: https://scverse-misc.readthedocs.io
106
108
  [changelog]: https://scverse-misc.readthedocs.io/en/latest/changelog.html
107
109
  [api documentation]: https://scverse-misc.readthedocs.io/latest/api.html
@@ -52,7 +52,7 @@ If you found a bug, please use the [issue tracker][].
52
52
  [scverse discourse]: https://discourse.scverse.org/
53
53
  [issue tracker]: https://github.com/scverse/scverse-misc/issues
54
54
  [tests]: https://github.com/scverse/scverse-misc/actions/workflows/test.yaml
55
- [codecov]: https://codecov.io/gh/bioFAM/mofaflex
55
+ [codecov]: https://codecov.io/gh/scverse/scverse-misc
56
56
  [documentation]: https://scverse-misc.readthedocs.io
57
57
  [changelog]: https://scverse-misc.readthedocs.io/en/latest/changelog.html
58
58
  [api documentation]: https://scverse-misc.readthedocs.io/latest/api.html
@@ -0,0 +1,8 @@
1
+ scverse\_misc.Settings
2
+ ======================
3
+
4
+ .. currentmodule:: scverse_misc
5
+
6
+ .. autoclass:: Settings
7
+
8
+ .. automethod:: override
@@ -0,0 +1,44 @@
1
+ # API
2
+
3
+ ```{eval-rst}
4
+ .. currentmodule:: scverse_misc
5
+ .. toctree::
6
+ ```
7
+
8
+ ## Extensions
9
+
10
+ ```{eval-rst}
11
+ .. autosummary::
12
+ :toctree: generated
13
+
14
+ make_register_namespace_decorator
15
+ ```
16
+ Types used by the former:
17
+ ```{eval-rst}
18
+ .. autosummary::
19
+ :toctree: generated
20
+
21
+ ExtensionNamespace
22
+ ```
23
+
24
+ ## Deprecations
25
+ ```{eval-rst}
26
+ .. autosummary::
27
+ :toctree: generated
28
+
29
+ deprecated
30
+ Deprecation
31
+ ```
32
+
33
+ ## Settings
34
+
35
+ ```{eval-rst}
36
+ .. toctree::
37
+ :hidden:
38
+
39
+ api/settings
40
+
41
+ +---------------------------+----------------------------------+
42
+ | :class:`Settings` () | Base class for package settings. |
43
+ +---------------------------+----------------------------------+
44
+ ```
@@ -26,7 +26,7 @@ project = info["Name"]
26
26
  author = info["Author"]
27
27
  copyright = f"{datetime.now():%Y}, {author}."
28
28
  version = info["Version"]
29
- urls = dict(pu.split(", ") for pu in info.get_all("Project-URL"))
29
+ urls = dict(pu.split(", ") for pu in info.get_all("Project-URL") or ())
30
30
  repository_url = urls["Source"]
31
31
 
32
32
  # The full version, including alpha/beta/rc tags
@@ -101,6 +101,7 @@ intersphinx_mapping = {
101
101
  "scipy": ("https://docs.scipy.org/doc/scipy", None),
102
102
  "pandas": ("https://pandas.pydata.org/docs/", None),
103
103
  "scanpy": ("https://scanpy.readthedocs.io/en/stable/", None),
104
+ "pydantic": ("https://pydantic.dev/docs/validation/", None),
104
105
  }
105
106
 
106
107
  # List of patterns, relative to source directory, that match files and
@@ -130,7 +131,7 @@ html_theme_options = {
130
131
  pygments_style = "default"
131
132
  katex_prerender = shutil.which(katex.NODEJS_BINARY) is not None
132
133
 
133
- nitpick_ignore = [
134
+ nitpick_ignore: list[tuple[str, str]] = [
134
135
  # If building the documentation fails because of a missing link that is outside your control,
135
136
  # you can add an exception to this list.
136
137
  # ("py:class", "igraph.Graph"),
@@ -6,7 +6,8 @@ import re
6
6
  from collections.abc import Generator, Iterable
7
7
 
8
8
  from sphinx.application import Sphinx
9
- from sphinx.ext.napoleon import NumpyDocstring
9
+ from sphinx.ext.napoleon.docstring import NumpyDocstring, GoogleDocstring
10
+ from sphinx.util.typing import ExtensionMetadata
10
11
 
11
12
 
12
13
  def _process_return(lines: Iterable[str]) -> Generator[str, None, None]:
@@ -17,7 +18,7 @@ def _process_return(lines: Iterable[str]) -> Generator[str, None, None]:
17
18
  yield line
18
19
 
19
20
 
20
- def _parse_returns_section(self: NumpyDocstring, section: str) -> list[str]:
21
+ def _parse_returns_section(self: GoogleDocstring, section: str) -> list[str]:
21
22
  lines_raw = self._dedent(self._consume_to_next_section())
22
23
  if lines_raw[0] == ":":
23
24
  del lines_raw[0]
@@ -27,6 +28,7 @@ def _parse_returns_section(self: NumpyDocstring, section: str) -> list[str]:
27
28
  return lines
28
29
 
29
30
 
30
- def setup(app: Sphinx):
31
+ def setup(app: Sphinx) -> ExtensionMetadata:
31
32
  """Set app."""
32
- NumpyDocstring._parse_returns_section = _parse_returns_section
33
+ NumpyDocstring._parse_returns_section = _parse_returns_section # type: ignore[method-assign]
34
+ return ExtensionMetadata(parallel_read_safe=True)
@@ -13,20 +13,20 @@ maintainers = [
13
13
  authors = [
14
14
  { name = "Ilia Kats" },
15
15
  ]
16
- requires-python = ">=3.11"
16
+ requires-python = ">=3.12"
17
17
  classifiers = [
18
18
  "Programming Language :: Python :: 3 :: Only",
19
- "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
21
20
  "Programming Language :: Python :: 3.13",
22
21
  "Programming Language :: Python :: 3.14",
23
22
  ]
24
23
  dynamic = [ "version" ]
25
24
  dependencies = [
26
- "pandas>=1",
27
25
  # for debug logging (referenced from the issue template)
28
26
  "session-info2",
27
+ "typing-extensions; python_version<'3.13'",
29
28
  ]
29
+ optional-dependencies.settings = [ "pydantic-settings", "python-dotenv" ]
30
30
  # https://docs.pypi.org/project_metadata/#project-urls
31
31
  urls.Documentation = "https://scverse-misc.readthedocs.io/"
32
32
  urls.Homepage = "https://github.com/scverse/scverse-misc"
@@ -37,12 +37,13 @@ dev = [
37
37
  "pre-commit",
38
38
  "twine>=4.0.2",
39
39
  ]
40
- test = [ "coverage>=7.10", "numpy", "pytest" ]
40
+ test = [ "coverage>=7.10", "numpy", "pytest", "scverse-misc[settings]", "sphinx" ]
41
41
  doc = [
42
42
  "ipykernel",
43
43
  "ipython",
44
44
  "myst-nb>=1.1",
45
45
  "pandas",
46
+ "scverse-misc[settings]",
46
47
  "sphinx>=8.1",
47
48
  "sphinx-autodoc-typehints",
48
49
  "sphinx-book-theme>=1",
@@ -54,7 +55,6 @@ doc = [
54
55
  ]
55
56
 
56
57
  [tool.hatch]
57
- build.hooks.vcs.version-file = "src/scverse_misc/_version.py"
58
58
  envs.default.installer = "uv"
59
59
  envs.default.dependency-groups = [ "dev" ]
60
60
  envs.docs.dependency-groups = [ "doc" ]
@@ -64,7 +64,7 @@ envs.docs.scripts.clean = "git clean -fdX -- {args:docs}"
64
64
  envs.hatch-test.dependency-groups = [ "dev", "test" ]
65
65
  envs.hatch-test.matrix = [
66
66
  # Test the lowest and highest supported Python versions with normal deps
67
- { deps = [ "stable" ], python = [ "3.11", "3.14" ] },
67
+ { deps = [ "stable" ], python = [ "3.12", "3.14" ] },
68
68
  # Test the newest supported Python version also with pre-release deps
69
69
  { deps = [ "pre" ], python = [ "3.14" ] },
70
70
  ]
@@ -95,6 +95,7 @@ lint.select = [
95
95
  ]
96
96
  lint.ignore = [
97
97
  "B008", # Errors from function calls in argument defaults. These are fine when the result is immutable.
98
+ "C408", # `dict()` is sometimes nicer than `{}`
98
99
  "D100", # Missing docstring in public module
99
100
  "D104", # Missing docstring in public package
100
101
  "D105", # __magic__ methods are often self-explanatory, allow missing docstrings
@@ -114,6 +115,11 @@ lint.per-file-ignores."docs/*" = [ "I" ]
114
115
  lint.per-file-ignores."tests/*" = [ "D" ]
115
116
  lint.pydocstyle.convention = "google"
116
117
 
118
+ [tool.mypy]
119
+ strict = true
120
+ explicit_package_bases = true
121
+ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/stubs", "$MYPY_CONFIG_FILE_DIR/src" ]
122
+
117
123
  [tool.pytest]
118
124
  strict = true
119
125
  testpaths = [ "tests" ]
@@ -0,0 +1,11 @@
1
+ from contextlib import suppress
2
+
3
+ from ._deprecated import Deprecation, deprecated
4
+ from ._extensions import ExtensionNamespace, make_register_namespace_decorator
5
+
6
+ __all__ = ["ExtensionNamespace", "make_register_namespace_decorator", "deprecated", "Deprecation"]
7
+
8
+ with suppress(ImportError):
9
+ from ._settings import Settings
10
+
11
+ __all__.append("Settings")
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from inspect import getdoc
5
+ from typing import TYPE_CHECKING, LiteralString
6
+
7
+ if sys.version_info >= (3, 13):
8
+ from warnings import deprecated as _deprecated
9
+ else:
10
+ from typing_extensions import deprecated as _deprecated
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+
16
+ __all__ = ["deprecated", "Deprecation"]
17
+
18
+
19
+ class Deprecation(str):
20
+ """Utility class storing information on deprecated functionality.
21
+
22
+ Args:
23
+ version_deprecated: The version of the package where the functionality was deprecated.
24
+ msg: The deprecation message.
25
+ """
26
+
27
+ version_deprecated: LiteralString
28
+
29
+ def __new__(cls, version_deprecated: LiteralString, msg: LiteralString = "") -> LiteralString: # type: ignore[misc] # typing.Intersection doesn’t exist yet
30
+ if not msg:
31
+ msg = "" # be lenient here, people don’t want to see “None” or “False” here
32
+ obj = super().__new__(cls, msg)
33
+ obj.version_deprecated = version_deprecated
34
+ return obj
35
+
36
+
37
+ def _deprecated_at[F: Callable[..., object]](
38
+ msg: Deprecation, *, category: type[Warning] = FutureWarning, stacklevel: int = 1
39
+ ) -> Callable[[F], F]:
40
+ """Decorator to indicate that a class, function, or overload is deprecated.
41
+
42
+ Wraps :func:`warnings.deprecated` and additionally modifies the docstring to include a deprecation notice.
43
+
44
+ Args:
45
+ msg: The deprecation message.
46
+ category: The category of the warning that will be emitted at runtime.
47
+ stacklevel: The stack level of the warning.
48
+
49
+ Examples:
50
+ >>> @deprecated(Deprecation("0.2", "Use bar() instead."))
51
+ ... def foo(baz):
52
+ ... pass
53
+ """
54
+
55
+ def decorate(func: F) -> F:
56
+ kind = "function" if func.__name__ == func.__qualname__ else "method"
57
+ warnmsg = f"The {kind} {func.__name__} is deprecated and will be removed in the future."
58
+
59
+ doc = getdoc(func)
60
+ docmsg = f".. version-deprecated:: {msg.version_deprecated}"
61
+ if len(msg):
62
+ docmsg += f"\n {msg}"
63
+ warnmsg += f" {msg}"
64
+
65
+ if doc is None:
66
+ doc = docmsg
67
+ else:
68
+ lines = doc.splitlines()
69
+ body = "\n".join(lines[1:])
70
+ doc = f"{lines[0]}\n\n{docmsg}\n{body}"
71
+ func.__doc__ = doc
72
+
73
+ return _deprecated(warnmsg, category=category, stacklevel=stacklevel)(func)
74
+
75
+ return decorate
76
+
77
+
78
+ if TYPE_CHECKING:
79
+ deprecated = _deprecated
80
+ else:
81
+ deprecated = _deprecated_at
@@ -1,14 +1,24 @@
1
+ """System to add extension attributes to classes.
2
+
3
+ Based off of the extension framework in Polars:
4
+ https://github.com/pola-rs/polars/blob/main/py-polars/polars/api.py
5
+ """
6
+
1
7
  from __future__ import annotations
2
8
 
3
9
  import inspect
10
+ import sys
4
11
  import warnings
5
12
  from itertools import islice
6
- from typing import TYPE_CHECKING, Generic, Literal, Protocol, TypeVar, get_type_hints, overload, runtime_checkable
13
+ from typing import TYPE_CHECKING, Literal, Protocol, get_type_hints, overload, runtime_checkable
7
14
 
8
15
  if TYPE_CHECKING:
9
16
  from collections.abc import Callable, Set
10
17
 
11
18
 
19
+ __all__ = ["make_register_namespace_decorator", "ExtensionNamespace"]
20
+
21
+
12
22
  @runtime_checkable
13
23
  class ExtensionNamespace(Protocol):
14
24
  """Protocol for extension namespaces.
@@ -19,21 +29,11 @@ class ExtensionNamespace(Protocol):
19
29
  checking with mypy and IDEs.
20
30
  """
21
31
 
22
- def __init__(self, instance) -> None:
32
+ def __init__(self, instance: object) -> None:
23
33
  """Used to enforce the correct signature for extension namespaces."""
24
34
 
25
35
 
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]):
36
+ class AccessorNameSpace[T, NameSpT: ExtensionNamespace]:
37
37
  """Establish property-like namespace object for user-defined functionality."""
38
38
 
39
39
  def __init__(self, name: str, namespace: type[NameSpT]) -> None:
@@ -50,7 +50,7 @@ class AccessorNameSpace(ExtensionNamespace, Generic[NameSpT]):
50
50
  if instance is None:
51
51
  return self._ns
52
52
 
53
- ns_instance = self._ns(instance) # type: ignore[call-arg]
53
+ ns_instance = self._ns(instance)
54
54
  setattr(instance, self._accessor, ns_instance)
55
55
  return ns_instance
56
56
 
@@ -81,7 +81,7 @@ def _check_namespace_signature(ns_class: type, cls: type, canonical_instance_nam
81
81
  TypeError: If both the name and type annotation of the second parameter are incorrect.
82
82
 
83
83
  """
84
- sig = inspect.signature(ns_class.__init__)
84
+ sig = inspect.signature(ns_class.__init__) # type: ignore[misc] # https://github.com/python/mypy/issues/21236
85
85
  params = sig.parameters
86
86
 
87
87
  # Ensure there are at least two parameters (self and mdata)
@@ -89,9 +89,7 @@ def _check_namespace_signature(ns_class: type, cls: type, canonical_instance_nam
89
89
  raise TypeError(f"Namespace initializer must accept a {cls.__name__} instance as the second parameter.")
90
90
 
91
91
  # Get the second parameter (expected to be `canonical_instance_name`)
92
- param = iter(params.values())
93
- next(param)
94
- param = next(param)
92
+ [_, param, *_] = params.values()
95
93
  if param.annotation is inspect.Parameter.empty:
96
94
  raise AttributeError(
97
95
  f"Namespace initializer's second parameter must be annotated as the {cls.__name__!r} class, got empty annotation."
@@ -101,7 +99,7 @@ def _check_namespace_signature(ns_class: type, cls: type, canonical_instance_nam
101
99
 
102
100
  # Resolve the annotation using get_type_hints to handle forward references and aliases.
103
101
  try:
104
- type_hints = get_type_hints(ns_class.__init__)
102
+ type_hints = get_type_hints(ns_class.__init__) # type: ignore[misc] # https://github.com/python/mypy/issues/21236
105
103
  resolved_type = type_hints.get(param.name, param.annotation)
106
104
  except NameError as e:
107
105
  raise NameError(
@@ -130,7 +128,7 @@ def _check_namespace_signature(ns_class: type, cls: type, canonical_instance_nam
130
128
  )
131
129
 
132
130
 
133
- def _create_namespace(
131
+ def _create_namespace[NameSpT: ExtensionNamespace](
134
132
  name: str, cls: type, reserved_namespaces: Set[str], canonical_instance_name: str
135
133
  ) -> Callable[[type[NameSpT]], type[NameSpT]]:
136
134
  """Register custom namespace against the underlying class."""
@@ -149,14 +147,14 @@ def _create_namespace(
149
147
  return namespace
150
148
 
151
149
 
152
- def _indent_string_lines(string: str, indentation_level: int, skip_lines: int = 0):
153
- minspace = 1e6
150
+ def _indent_string_lines(string: str, indentation_level: int, skip_lines: int = 0) -> str:
151
+ minspace = sys.maxsize
154
152
  for line in islice(string.splitlines(), 1, None):
155
153
  for i, char in enumerate(line):
156
154
  if not char.isspace():
157
155
  minspace = min(minspace, i)
158
156
  break
159
- if minspace == 1e6: # single-line string
157
+ if minspace == sys.maxsize: # single-line string
160
158
  minspace = 0
161
159
  return "\n".join(
162
160
  " " * 4 * indentation_level + sline if i >= skip_lines else sline
@@ -165,7 +163,7 @@ def _indent_string_lines(string: str, indentation_level: int, skip_lines: int =
165
163
  )
166
164
 
167
165
 
168
- def make_register_namespace_decorator(
166
+ def make_register_namespace_decorator[NameSpT: ExtensionNamespace](
169
167
  cls: type, canonical_instance_name: str, decorator_name: str, docstring_style: Literal["google", "numpy"] = "google"
170
168
  ) -> Callable[[str], Callable[[type[NameSpT]], type[NameSpT]]]:
171
169
  """Create a decorator for registering custom functionality with a class.
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+ import warnings
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from types import GenericAlias
8
+ from typing import Literal
9
+
10
+ import dotenv
11
+ from pydantic.fields import FieldInfo
12
+ from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict
13
+
14
+ from ._utils import copy_func
15
+
16
+
17
+ def _type_str(field: FieldInfo) -> str:
18
+ return (
19
+ field.annotation.__name__
20
+ if isinstance(field.annotation, type) and not isinstance(field.annotation, GenericAlias)
21
+ else str(field.annotation)
22
+ )
23
+
24
+
25
+ _docstring_template = """Allows users to customize settings for the `{package}` package.
26
+
27
+ Settings here will generally be for advanced use-cases and should be used with caution.
28
+
29
+ For setting an option use :func:`~{package}.{name}.override` (local) or set the attributes directly (global)
30
+ i.e., `{package}.{name}.my_setting = foo`. For assignment by environment variable, use the variable name in
31
+ all caps with `{env_prefix}` as the prefix before import of `{package}`.
32
+ """
33
+
34
+
35
+ class Settings(BaseSettings):
36
+ '''Base class for package settings.
37
+
38
+ This class can be subclassed by individual packages to get package-specific settings handling.
39
+ Settings will be validated on assignment thanks to Pydantic. The class requires one argument
40
+ `exported_object_name` and one optional argument `docstring_style`, which will be used to construct
41
+ a suitable docstring (see the examples).
42
+
43
+ Both a settings instance and its `override` method should be added to the package documentation.
44
+
45
+ Thanks to Pydantic Settings, settings values will also be loaded from environment variables or `.env`
46
+ files. Environment variables must be prefixex with `$PACKAGE_NAME_` to take effect, where `$PACKAGE_NAME`
47
+ is the name of the package of the subclass. This can be overridden by passing `env_prefix=CUSTOMPREFIX`
48
+ as class argument.
49
+
50
+ Examples:
51
+ >>> from typing import Annotated
52
+ ... from pydantic import Field
53
+ ... from scverse_misc import Settings
54
+ ...
55
+ ...
56
+ ... class MySettings(Settings, exported_object_name="settings", docstring_style="numpy"):
57
+ ... eps: Annotated[float, Field(gt=0, lt=1)] = 1e-8
58
+ ... """Small epsilon for numerical stability."""
59
+ ...
60
+ ... use_optional_feature: bool = False
61
+ ... """Whether to use the optional feature."""
62
+ ...
63
+ ...
64
+ ... settings = MySettings()
65
+ '''
66
+
67
+ @classmethod
68
+ def settings_customise_sources(
69
+ cls,
70
+ settings_cls: type[BaseSettings],
71
+ init_settings: PydanticBaseSettingsSource,
72
+ env_settings: PydanticBaseSettingsSource,
73
+ dotenv_settings: PydanticBaseSettingsSource,
74
+ file_secret_settings: PydanticBaseSettingsSource,
75
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
76
+ return init_settings, env_settings, dotenv_settings
77
+
78
+ @staticmethod
79
+ def _get_packagename(subcls: type[Settings]) -> str:
80
+ package_name = subcls.__module__
81
+ dotidx = package_name.find(".")
82
+ if dotidx > -1:
83
+ package_name = package_name[:dotidx]
84
+ return package_name
85
+
86
+ def __init_subclass__(subcls, *, exported_object_name: str, docstring_style: Literal["google", "numpy"] = "google"):
87
+ if (config := subcls.__dict__.get("model_config")) is not None:
88
+ if not config.get("validate_assignment", True):
89
+ warnings.warn("`validate_assignment=False` is not supported, overriding.", RuntimeWarning, stacklevel=2)
90
+ if not config.get("use_attribute_docstrings", True):
91
+ warnings.warn(
92
+ "`use_attribute_docstrings=False` is not supported, overriding.", RuntimeWarning, stacklevel=2
93
+ )
94
+ if config.get("env_file") is not None:
95
+ warnings.warn(
96
+ "Setting a custom env_file location is not supported, overriding.", RuntimeWarning, stacklevel=2
97
+ )
98
+ else:
99
+ config = SettingsConfigDict()
100
+
101
+ config["validate_assignment"] = True
102
+ config["use_attribute_docstrings"] = True
103
+ config["env_file"] = dotenv.find_dotenv()
104
+
105
+ if not config.get("env_prefix"):
106
+ config["env_prefix"] = f"{__class__._get_packagename(subcls)}_" # type: ignore[name-defined] # https://github.com/python/mypy/issues/4177
107
+ subcls.model_config = config
108
+
109
+ super().__init_subclass__()
110
+
111
+ @contextmanager
112
+ def override(self, **kwargs: object) -> Generator[None]:
113
+ """Context manager for local setting overrides.
114
+
115
+ Subclasses will get a version with a docstring detailing the available parameters.
116
+ """
117
+ oldsettings = {argname: getattr(self, argname) for argname in kwargs.keys()}
118
+ try:
119
+ for argname, argval in kwargs.items():
120
+ setattr(self, argname, argval)
121
+ yield
122
+ finally:
123
+ for argname, argval in reversed(oldsettings.items()):
124
+ setattr(self, argname, argval)
125
+
126
+ @classmethod
127
+ def __pydantic_init_subclass__( # type: ignore[override]
128
+ subcls, *, exported_object_name: str, docstring_style: Literal["google", "numpy"] = "google"
129
+ ) -> None:
130
+ subcls.__doc__ = (
131
+ _docstring_template.format(
132
+ package=__class__._get_packagename(subcls), # type: ignore[name-defined] # https://github.com/python/mypy/issues/4177
133
+ name=exported_object_name,
134
+ env_prefix=subcls.model_config["env_prefix"].upper(),
135
+ )
136
+ + "\n\nThe following options are available:\n"
137
+ )
138
+ override_doc = "Provides local override via keyword arguments as a context manager.\n\n"
139
+ if docstring_style == "google":
140
+ override_doc += "Args:\n"
141
+ else:
142
+ override_doc += "Parameters\n----------\n"
143
+ for fname, field in subcls.model_fields.items():
144
+ subcls.__doc__ += f"""
145
+ .. attribute:: {exported_object_name}.{fname}
146
+ :type: {_type_str(field)}
147
+ :value: {field.default!r}\n"""
148
+
149
+ description = f"(default `{field.default!r}`) "
150
+ if field.description is not None:
151
+ subcls.__doc__ += f"\n{textwrap.indent(field.description, ' ')}\n"
152
+ description += field.description
153
+
154
+ if docstring_style == "google":
155
+ override_doc += f""" {fname} ({_type_str(field)}): {textwrap.indent(description, " ")}\n"""
156
+ else:
157
+ override_doc += f"""
158
+ {fname} : {_type_str(field)}
159
+ {textwrap.indent(description, " ")}\n"""
160
+
161
+ subcls.override = copy_func( # type: ignore[method-assign,type-var]
162
+ subcls.override,
163
+ __doc__=override_doc,
164
+ __module__=subcls.__module__,
165
+ __qualname__=f"{subcls.__qualname__}.override",
166
+ )
@@ -0,0 +1,25 @@
1
+ import functools
2
+ import sys
3
+ from functools import WRAPPER_ASSIGNMENTS
4
+ from types import FunctionType
5
+ from typing import ParamSpec, TypedDict, TypeVar, TypeVarTuple, Unpack, cast
6
+
7
+
8
+ class Overrides(TypedDict, total=False):
9
+ __module__: str
10
+ __name__: str
11
+ __qualname__: str
12
+ __doc__: str
13
+ # ≥3.14: __annotate__, <3.14: __annotations__
14
+ __type_params__: tuple[TypeVar | TypeVarTuple | ParamSpec, ...]
15
+
16
+
17
+ def copy_func[F: FunctionType](func: F, /, **overrides: Unpack[Overrides]) -> F:
18
+ kw = dict(kwdefaults=func.__kwdefaults__) if sys.version_info >= (3, 13) else {}
19
+ new = FunctionType(
20
+ func.__code__, func.__globals__, name=func.__name__, argdefs=func.__defaults__, closure=func.__closure__, **kw
21
+ )
22
+ for key, value in overrides.items():
23
+ setattr(new, key, value)
24
+ copy = set(WRAPPER_ASSIGNMENTS) - overrides.keys()
25
+ return cast("F", functools.update_wrapper(new, func, assigned=copy))
@@ -0,0 +1,2 @@
1
+ # mypy doesn’t understand namespace packages apparently
2
+ from . import katex as katex
@@ -0,0 +1,8 @@
1
+ STARTUP_TIMEOUT: float
2
+ """How long to wait for the render server to start in seconds."""
3
+
4
+ RENDER_TIMEOUT: float
5
+ """Timeout per rendering request in seconds."""
6
+
7
+ NODEJS_BINARY: str
8
+ """nodejs binary to run javascript."""
@@ -0,0 +1,65 @@
1
+ from collections.abc import Callable
2
+ from typing import cast
3
+
4
+ import pytest
5
+
6
+ from scverse_misc import Deprecation, deprecated
7
+
8
+
9
+ @pytest.fixture(params=[pytest.param(None, id="no_message"), pytest.param("Test message.", id="message")])
10
+ def msg(request: pytest.FixtureRequest) -> str | None:
11
+ return cast(str | None, request.param)
12
+
13
+
14
+ @pytest.fixture(
15
+ params=[
16
+ pytest.param(None, id="no_docstring"),
17
+ pytest.param("Test function", id="short"),
18
+ pytest.param(
19
+ """Test function
20
+
21
+ This is a test.
22
+
23
+ Parameters
24
+ ----------
25
+ foo
26
+ bar
27
+ bar
28
+ baz
29
+ """,
30
+ id="long",
31
+ ),
32
+ ]
33
+ )
34
+ def docstring(request: pytest.FixtureRequest) -> str | None:
35
+ return cast(str | None, request.param)
36
+
37
+
38
+ @pytest.fixture
39
+ def deprecated_func(msg: str | None, docstring: str | None) -> Callable[[int, int], int]:
40
+ def func(foo: int, bar: int) -> int:
41
+ return 42
42
+
43
+ func.__doc__ = docstring
44
+ return deprecated(Deprecation("foo", msg or ""))(func)
45
+
46
+
47
+ def test_deprecation_decorator(
48
+ deprecated_func: Callable[[int, int], int], docstring: str | None, msg: str | None
49
+ ) -> None:
50
+ with pytest.warns(FutureWarning, match="deprecated"):
51
+ assert deprecated_func(1, 2) == 42
52
+
53
+ assert deprecated_func.__doc__ is not None
54
+ lines = deprecated_func.__doc__.expandtabs().splitlines()
55
+ if docstring is None:
56
+ assert lines[0].startswith(".. version-deprecated::")
57
+ else:
58
+ lines_orig = docstring.expandtabs().splitlines()
59
+ assert lines[0] == lines_orig[0]
60
+ assert len(lines[1].strip()) == 0
61
+ assert lines[2].startswith(".. version-deprecated")
62
+ if msg is None:
63
+ assert len(lines) == 3 or not lines[3].startswith(" ")
64
+ else:
65
+ assert lines[3] == f" {msg}"
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Protocol
4
4
 
5
5
  import pytest
6
6
 
@@ -11,16 +11,20 @@ if TYPE_CHECKING:
11
11
  from collections.abc import Generator
12
12
 
13
13
 
14
+ class Greeter(Protocol):
15
+ def __init__(self, obj: DummyClass) -> None: ...
16
+ def greet(self) -> str: ...
17
+
18
+
14
19
  class DummyClass:
15
- foo = []
20
+ foo: list[object] = []
16
21
  bar = None
17
22
 
18
23
  @property
19
- def baz(self):
20
- pass
24
+ def baz(self) -> None: ...
25
+ def foobar(self) -> None: ...
21
26
 
22
- def foobar(self):
23
- pass
27
+ dummy: Greeter # available when using `dummy_namespace` fixture
24
28
 
25
29
 
26
30
  register_dummy_namespace = make_register_namespace_decorator(DummyClass, "obj", "register_dummy_namespace")
@@ -72,16 +76,18 @@ def test_accessor_namespace() -> None:
72
76
 
73
77
  # Define a dummy namespace class to be used via the descriptor.
74
78
  class DummyNamespace:
75
- def __init__(self, obj: DummyClass):
79
+ def __init__(self, obj: Dummy):
76
80
  self._obj = obj
77
81
 
78
82
  def foo(self) -> str:
79
83
  return "foo"
80
84
 
81
85
  class Dummy:
82
- pass
86
+ dummy: DummyNamespace # just typing, runtime added below
83
87
 
84
- descriptor = extensions.AccessorNameSpace("dummy", DummyNamespace)
88
+ descriptor: extensions.AccessorNameSpace[Dummy, DummyNamespace] = extensions.AccessorNameSpace(
89
+ "dummy", DummyNamespace
90
+ )
85
91
 
86
92
  # When accessed on the class, it should return the namespace type.
87
93
  ns_class = descriptor.__get__(None, Dummy)
@@ -204,7 +210,7 @@ def test_missing_annotation() -> None:
204
210
 
205
211
  @register_dummy_namespace("missing_annotation")
206
212
  class MissingAnnotationNamespace:
207
- def __init__(self, obj) -> None:
213
+ def __init__(self, obj) -> None: # type: ignore[no-untyped-def]
208
214
  self.obj = obj
209
215
 
210
216
 
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import TYPE_CHECKING, Annotated, Literal, cast
5
+
6
+ import pytest
7
+ from pydantic import Field, ValidationError
8
+ from pydantic.fields import FieldInfo
9
+ from pydantic_settings import SettingsConfigDict
10
+ from sphinx.ext.napoleon import GoogleDocstring, NumpyDocstring # type: ignore[attr-defined]
11
+
12
+ from scverse_misc import Settings
13
+
14
+ if TYPE_CHECKING:
15
+ # Static version of the class returned by the `settings_class` fixture
16
+ class DummySettings(Settings, exported_object_name="settings"):
17
+ field_bool: bool = False
18
+ field_no_docstring: int = 42
19
+ field_int_range: int = 1
20
+
21
+
22
+ @pytest.fixture
23
+ def docstring_style(request: pytest.FixtureRequest) -> Literal["google", "numpy"]:
24
+ return getattr(request, "param", "google")
25
+
26
+
27
+ @pytest.fixture
28
+ def settings_class(docstring_style: Literal["google", "numpy"]) -> type[DummySettings]:
29
+ class _DummySettings(Settings, exported_object_name="settings", docstring_style=docstring_style):
30
+ field_bool: bool = False
31
+ """Boolean field."""
32
+
33
+ field_no_docstring: int = 42
34
+
35
+ field_int_range: Annotated[int, Field(ge=0, le=4)] = 1
36
+ """Integer range field."""
37
+
38
+ return cast("type[DummySettings]", _DummySettings)
39
+
40
+
41
+ @pytest.fixture
42
+ def settings(settings_class: type[DummySettings]) -> DummySettings:
43
+ return settings_class()
44
+
45
+
46
+ def test_defaults_override() -> None:
47
+ with (
48
+ pytest.warns(RuntimeWarning, match="validate_assignment=False"),
49
+ pytest.warns(RuntimeWarning, match="use_attribute_docstrings=False"),
50
+ pytest.warns(RuntimeWarning, match="custom env_file location"),
51
+ ):
52
+
53
+ class WarnSettings(Settings, exported_object_name="settings"):
54
+ model_config = SettingsConfigDict(
55
+ validate_assignment=False, use_attribute_docstrings=False, env_file="mydotenv"
56
+ )
57
+
58
+ field_bool: bool = False
59
+
60
+ settings = WarnSettings()
61
+ with pytest.raises(ValidationError):
62
+ settings.field_bool = 2 # type: ignore[assignment]
63
+
64
+
65
+ @pytest.mark.parametrize("v", [2, 4])
66
+ def test_env_vars(monkeypatch: pytest.MonkeyPatch, settings_class: type[DummySettings], v: int) -> None:
67
+ """Test that the env var prefix is derived from the module name."""
68
+ monkeypatch.setenv("TESTS_FIELD_INT_RANGE", str(v))
69
+ settings = settings_class()
70
+ assert settings.field_int_range == v
71
+
72
+
73
+ def test_validate_assignment(settings: DummySettings) -> None:
74
+ with pytest.raises(ValidationError):
75
+ settings.field_bool = 2 # type: ignore[assignment]
76
+ with pytest.raises(ValidationError):
77
+ settings.field_int_range = -1
78
+
79
+
80
+ def test_override(settings: DummySettings) -> None:
81
+ with settings.override(field_bool=True):
82
+ assert settings.field_bool is True
83
+ assert settings.field_bool is False
84
+
85
+ with pytest.raises(ValidationError):
86
+ with settings.override(field_int_range=3, field_no_docstring=1.1):
87
+ pass
88
+ assert settings.field_no_docstring == 42
89
+ assert settings.field_int_range == 1
90
+
91
+
92
+ @pytest.mark.parametrize("docstring_style", ["google", "numpy"], indirect=True)
93
+ def test_docs(docstring_style: Literal["google", "numpy"], settings: DummySettings) -> None:
94
+ parser = GoogleDocstring if docstring_style == "google" else NumpyDocstring
95
+ lines = parser(inspect.getdoc(settings)).lines() # type: ignore[arg-type]
96
+
97
+ assert lines[0].endswith("`tests` package.")
98
+
99
+ current_field: FieldInfo | None = None
100
+ field_iter = iter(type(settings).model_fields.items())
101
+ for line in lines:
102
+ if line.startswith(".. attribute::"):
103
+ current_field_name, current_field = next(field_iter)
104
+ assert line.endswith(current_field_name)
105
+ elif current_field is not None:
106
+ line = line.strip()
107
+ if line.startswith(":type:"):
108
+ assert current_field.annotation is not None
109
+ assert line.endswith(current_field.annotation.__name__)
110
+ elif line.startswith(":value:"):
111
+ assert line.endswith(repr(current_field.default))
112
+ elif len(line) > 0 and current_field.description is not None:
113
+ assert line == current_field.description
114
+
115
+
116
+ @pytest.mark.parametrize("docstring_style", ["google", "numpy"], indirect=True)
117
+ def test_override_docs(docstring_style: Literal["google", "numpy"], settings: DummySettings) -> None:
118
+ parser = GoogleDocstring if docstring_style == "google" else NumpyDocstring
119
+ lines = parser(inspect.getdoc(settings.override)).lines() # type: ignore[arg-type]
120
+
121
+ current_field: FieldInfo | None = None
122
+ field_iter = iter(type(settings).model_fields.items())
123
+ for line in lines:
124
+ if line.startswith(":param"):
125
+ current_field_name, current_field = next(field_iter)
126
+ description = " " + current_field.description if current_field.description is not None else ""
127
+ assert line.startswith(f":param {current_field_name}: (default `{current_field.default!r}`){description}")
128
+ elif current_field is not None and len(line) > 0:
129
+ assert line == f":type {current_field_name}: {current_field.annotation.__name__}" # type: ignore[union-attr]
@@ -1,15 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog][],
6
- and this project adheres to [Semantic Versioning][].
7
-
8
- [keep a changelog]: https://keepachangelog.com/en/1.0.0/
9
- [semantic versioning]: https://semver.org/spec/v2.0.0.html
10
-
11
- ## [Unreleased]
12
-
13
- ### Added
14
-
15
- - Basic tool, preprocessing and plotting functions
@@ -1,22 +0,0 @@
1
- # API
2
-
3
- ```{eval-rst}
4
- .. currentmodule:: scverse_misc
5
- .. toctree::
6
- ```
7
-
8
- ## Extensions
9
-
10
- ``` {eval-rst}
11
- .. autosummary::
12
- :toctree: generated
13
-
14
- make_register_namespace_decorator
15
- ```
16
- Types used by the former:
17
- ``` {eval-rst}
18
- .. autosummary::
19
- :toctree: generated
20
-
21
- ExtensionNamespace
22
- ```
@@ -1,2 +0,0 @@
1
- from ._extensions import ExtensionNamespace, make_register_namespace_decorator
2
- from ._version import __version__, __version_tuple__
@@ -1,34 +0,0 @@
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.2'
32
- __version_tuple__ = version_tuple = (0, 0, 2)
33
-
34
- __commit_id__ = commit_id = None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes