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.
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/workflows/test.yaml +4 -4
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.pre-commit-config.yaml +13 -3
- scverse_misc-0.0.4/CHANGELOG.md +40 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/PKG-INFO +7 -5
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/README.md +1 -1
- scverse_misc-0.0.4/docs/api/settings.rst +8 -0
- scverse_misc-0.0.4/docs/api.md +44 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/conf.py +3 -2
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/extensions/typed_returns.py +6 -4
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/pyproject.toml +12 -6
- scverse_misc-0.0.4/src/scverse_misc/__init__.py +11 -0
- scverse_misc-0.0.4/src/scverse_misc/_deprecated.py +81 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/src/scverse_misc/_extensions.py +22 -24
- scverse_misc-0.0.4/src/scverse_misc/_settings.py +166 -0
- scverse_misc-0.0.4/src/scverse_misc/_utils.py +25 -0
- scverse_misc-0.0.4/stubs/sphinxcontrib/__init__.pyi +2 -0
- scverse_misc-0.0.4/stubs/sphinxcontrib/katex.pyi +8 -0
- scverse_misc-0.0.4/tests/test_deprecation_decorator.py +65 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/tests/test_extensions.py +16 -10
- scverse_misc-0.0.4/tests/test_settings.py +129 -0
- scverse_misc-0.0.2/CHANGELOG.md +0 -15
- scverse_misc-0.0.2/docs/api.md +0 -22
- scverse_misc-0.0.2/src/scverse_misc/__init__.py +0 -2
- scverse_misc-0.0.2/src/scverse_misc/_version.py +0 -34
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.codecov.yaml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.cruft.json +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.editorconfig +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/workflows/build.yaml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.github/workflows/release.yaml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.gitignore +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/.readthedocs.yaml +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/LICENSE +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/biome.jsonc +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/Makefile +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_static/.gitkeep +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_static/css/custom.css +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_templates/.gitkeep +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/_templates/autosummary/class.rst +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/changelog.md +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/contributing.md +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/index.md +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/references.bib +0 -0
- {scverse_misc-0.0.2 → scverse_misc-0.0.4}/docs/references.md +0 -0
- {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@
|
|
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@
|
|
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@
|
|
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@
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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/
|
|
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,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:
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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 =
|
|
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 ==
|
|
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,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
|
-
|
|
24
|
+
def baz(self) -> None: ...
|
|
25
|
+
def foobar(self) -> None: ...
|
|
21
26
|
|
|
22
|
-
|
|
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:
|
|
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
|
-
|
|
86
|
+
dummy: DummyNamespace # just typing, runtime added below
|
|
83
87
|
|
|
84
|
-
descriptor = extensions.AccessorNameSpace(
|
|
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]
|
scverse_misc-0.0.2/CHANGELOG.md
DELETED
|
@@ -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
|
scverse_misc-0.0.2/docs/api.md
DELETED
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|