diff --git a/changelog.d/1519.change.md b/changelog.d/1519.change.md new file mode 100644 index 000000000..7f3a94d3f --- /dev/null +++ b/changelog.d/1519.change.md @@ -0,0 +1,9 @@ +Added a Sphinx extension to show cached properties on slots classes. + +To use it, append `'attrs.sphinx_cached_property'` to the `extensions` in your Sphinx configuration module: + +```python +# docs/conf.py + +extensions += ['attrs.sphinx_cached_property'] +``` diff --git a/docs/conf.py b/docs/conf.py index 2262f2c77..7912ba396 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,6 +59,7 @@ "sphinx.ext.todo", "notfound.extension", "sphinxcontrib.towncrier", + "attrs.sphinx_cached_property", ] myst_enable_extensions = [ diff --git a/docs/how-does-it-work.md b/docs/how-does-it-work.md index 96dc5c303..4bf334096 100644 --- a/docs/how-does-it-work.md +++ b/docs/how-does-it-work.md @@ -114,6 +114,9 @@ Getting this working is achieved by: * Adding a `__getattr__` method to set values on the wrapped methods. For most users, this should mean that it works transparently. +However, the docstring for the wrapped function is inaccessible. +If you need it for your documentation, you can use the bundled Sphinx extension. +Add `"attrs.sphinx_cached_property"` to the `extensions` list in your Sphinx `conf.py`. :::{note} The implementation does not guarantee that the wrapped method is called only once in multi-threaded usage. diff --git a/pyproject.toml b/pyproject.toml index a4f255ac0..7b0c52cec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ tests = [ "pympler", "pytest", "pytest-xdist[psutil]", + { include-group = "docs" } ] cov = [{ include-group = "tests" }, "coverage[toml]"] pyright = ["pyright", { include-group = "tests" }] diff --git a/src/attr/_make.py b/src/attr/_make.py index a23bf8a06..af3c4a557 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -14,7 +14,7 @@ import unicodedata import weakref -from collections.abc import Callable, Mapping +from collections.abc import Callable, Mapping, Sequence from functools import cached_property from typing import Any, NamedTuple, TypeVar @@ -103,6 +103,35 @@ def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 return _none_constructor, _args +class _TupleProxy(Sequence): + """A wrapper for a tuple that makes it not type-check as a tuple + + This is a hack to make Sphinx document all cached properties on slots + classes as if they were regular properties. + + """ + + __slots__ = ("_tup",) + + def __init__(self, tup: tuple): + self._tup = tup + + def __iter__(self): + return iter(self._tup) + + def __len__(self): + return len(self._tup) + + def __getitem__(self, item): + return self._tup[item] + + def __eq__(self, other): + return self._tup == other + + def __hash__(self): + return hash(self._tup) + + def attrib( default=NOTHING, validator=None, @@ -911,7 +940,7 @@ def _create_slots_class(self): names += ("__weakref__",) cached_properties = { - name: cached_prop.func + name: cached_prop for name, cached_prop in cd.items() if isinstance(cached_prop, cached_property) } @@ -920,8 +949,11 @@ def _create_slots_class(self): # To know to update them. additional_closure_functions_to_update = [] if cached_properties: + # Store cached property functions for the autodoc extension to read + cd["__attrs_cached_properties__"] = cached_properties class_annotations = _get_annotations(self._cls) - for name, func in cached_properties.items(): + for name, prop in cached_properties.items(): + func = prop.func # Add cached properties to names for slotting. names += (name,) # Clear out function from class to avoid clashing. @@ -936,7 +968,12 @@ def _create_slots_class(self): additional_closure_functions_to_update.append(original_getattr) cd["__getattr__"] = _make_cached_property_getattr( - cached_properties, original_getattr, self._cls + { + name: prop.func + for (name, prop) in cached_properties.items() + }, + original_getattr, + self._cls, ) # We only add the names of attributes that aren't inherited. @@ -957,7 +994,7 @@ def _create_slots_class(self): if self._cache_hash: slot_names.append(_HASH_CACHE_FIELD) - cd["__slots__"] = tuple(slot_names) + cd["__slots__"] = _TupleProxy(tuple(slot_names)) cd["__qualname__"] = self._cls.__qualname__ diff --git a/src/attrs/sphinx_cached_property.py b/src/attrs/sphinx_cached_property.py new file mode 100644 index 000000000..18511f539 --- /dev/null +++ b/src/attrs/sphinx_cached_property.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: MIT +"""A Sphinx extension to document cached properties on slots classes + +Add ``"attrs.sphinx_cached_property"`` to the ``extensions`` list in Sphinx's +conf.py to use this. Otherwise, cached properties of ``@define(slots=True)`` +classes will be inaccessible. + +""" + +from __future__ import annotations + +from functools import cached_property +from typing import Any + +from sphinx.application import Sphinx + +from . import __version__ + + +def get_cached_property_for_member_descriptor( + cls: type, name: str, default=None +) -> cached_property | Any: + """If the attribute is for a cached property, return the ``cached_property`` + + Otherwise, delegate to normal ``getattr`` + + """ + props = getattr(cls, "__attrs_cached_properties__", None) + if props is None or name not in props: + return getattr(cls, name, default) + return props[name] + + +def setup(app: Sphinx) -> dict[str, str | bool]: + """Install the special attribute getter for cached properties of slotted classes""" + app.add_autodoc_attrgetter( + object, get_cached_property_for_member_descriptor + ) + return { + "version": __version__, + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/tests/doctest_data/explicit-autoproperty-cached.rst b/tests/doctest_data/explicit-autoproperty-cached.rst new file mode 100644 index 000000000..15087659f --- /dev/null +++ b/tests/doctest_data/explicit-autoproperty-cached.rst @@ -0,0 +1,3 @@ +.. autoclass:: tests.test_slots.SphinxDocTest + + .. autoproperty:: documented diff --git a/tests/doctest_data/index.txt b/tests/doctest_data/index.txt new file mode 100644 index 000000000..1a2289622 --- /dev/null +++ b/tests/doctest_data/index.txt @@ -0,0 +1,7 @@ +class tests.test_slots.SphinxDocTest + + Test that slotted cached_property shows up in Sphinx docs + + property documented + + A very well documented function diff --git a/tests/doctest_data/members-cached-property.rst b/tests/doctest_data/members-cached-property.rst new file mode 100644 index 000000000..546f83021 --- /dev/null +++ b/tests/doctest_data/members-cached-property.rst @@ -0,0 +1,2 @@ +.. autoclass:: tests.test_slots.SphinxDocTest + :members: diff --git a/tests/test_slots.py b/tests/test_slots.py index a74c32b03..de2502f91 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -6,16 +6,24 @@ import functools import pickle +import shutil import weakref +from itertools import zip_longest +from pathlib import Path from unittest import mock +import hypothesis.strategies as st import pytest +from hypothesis import given +from sphinx.application import Sphinx + import attr import attrs from attr._compat import PY_3_14_PLUS, PYPY +from attr._make import _TupleProxy # Pympler doesn't work on PyPy. @@ -736,6 +744,75 @@ def f(self): assert B(17).f == 289 +@given(st.tuples(st.integers() | st.text() | st.floats())) +def test_tuple_proxy(t): + """ + The `_TupleProxy` class acts just like a normal tuple, but isn't one + + It's not a tuple for the purposes of :func:`isinstance` and that's about it + """ + prox = _TupleProxy(t) + assert len(t) == len(prox) + for a, b in zip_longest(t, prox): + assert a is b + for i in range(len(prox)): + assert t[i] is prox[i] + assert t == prox + assert hash(t) == hash(prox) + assert not isinstance(prox, tuple) + + +@attr.s(slots=True) +class SphinxDocTest: + """Test that slotted cached_property shows up in Sphinx docs""" + + @functools.cached_property + def documented(self): + """A very well documented function""" + return True + + +def test_sphinx_autodocuments_cached_property(tmp_path): + """ + Sphinx can generate autodocs for cached properties in slots classes + """ + here = Path(__file__).parent / "doctest_data" + rst = here.joinpath("explicit-autoproperty-cached.rst") + index = tmp_path.joinpath("index.rst") + shutil.copy(rst, index) + outdir = tmp_path.joinpath("docs") + outdir.mkdir() + app = Sphinx( + tmp_path, here.parent.parent / "docs", outdir, tmp_path, "text" + ) + app.build(force_all=True) + assert ( + outdir.joinpath("index.txt").read_text() + == here.joinpath("index.txt").read_text() + ) + + +def test_sphinx_automembers_cached_property(tmp_path): + """ + Sphinx can find cached properties in the :members: of slots classes + """ + here = Path(__file__).parent / "doctest_data" + rst = here.joinpath("members-cached-property.rst") + index = tmp_path.joinpath("index.rst") + shutil.copy(rst, index) + outdir = tmp_path.joinpath("docs") + outdir.mkdir() + app = Sphinx( + tmp_path, here.parent.parent / "docs", outdir, tmp_path, "text" + ) + app.build(force_all=True) + with ( + outdir.joinpath("index.txt").open("r") as written, + here.joinpath("index.txt").open("r") as good, + ): + assert written.read() == good.read() + + def test_slots_cached_property_allows_call(): """ cached_property in slotted class allows call. diff --git a/uv.lock b/uv.lock index 851226f84..8da3f9749 100644 --- a/uv.lock +++ b/uv.lock @@ -73,34 +73,70 @@ source = { editable = "." } [package.dev-dependencies] benchmark = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-codspeed" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] cov = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.13.4", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] dev = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-xdist", extra = ["psutil"] }, { name = "ruff" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] docs = [ { name = "cogapp" }, @@ -133,51 +169,111 @@ docs-watch = [ ] mypy = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-mypy-plugins", marker = "python_full_version >= '3.10' and platform_python_implementation == 'CPython'" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] pyrefly = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pyrefly" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] pyright = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pyright" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] tests = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] ty = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "hypothesis", version = "6.151.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pympler" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, { name = "ty" }, ] @@ -186,27 +282,48 @@ ty = [ [package.metadata.requires-dev] benchmark = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pytest" }, { name = "pytest-codspeed" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] cov = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, { name = "coverage", extras = ["toml"] }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] dev = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, { name = "ruff" }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] docs = [ { name = "cogapp" }, @@ -229,41 +346,76 @@ docs-watch = [ ] mypy = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pytest" }, { name = "pytest-mypy-plugins", marker = "python_full_version >= '3.10' and platform_python_implementation == 'CPython'" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] pyrefly = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pyrefly" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] pyright = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] tests = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, ] ty = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "cogapp" }, + { name = "furo" }, { name = "hypothesis" }, + { name = "myst-parser" }, { name = "pympler" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, + { name = "sphinx" }, + { name = "sphinx-notfound-page" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, { name = "ty" }, ]