From 988cc69646b28cd5042d4af189d57d2367873b2d Mon Sep 17 00:00:00 2001 From: Julian Gilbey Date: Mon, 13 Jan 2025 20:25:49 +0000 Subject: [PATCH 1/2] Handle automatic docstring dedenting in Python 3.13 --- jupyter_client/client.py | 52 ++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/jupyter_client/client.py b/jupyter_client/client.py index 67c44600..e0eaf3cc 100644 --- a/jupyter_client/client.py +++ b/jupyter_client/client.py @@ -3,7 +3,9 @@ # Distributed under the terms of the Modified BSD License. import asyncio import inspect +import re import sys +import textwrap import time import typing as t from functools import partial @@ -35,6 +37,23 @@ def validate_string_dict(dct: t.Dict[str, str]) -> None: raise ValueError("value %r in dict must be a string" % v) +def get_docstring_indent(doc: str) -> str: + # Python 3.13 dedents docstrings automatically. + # In this module, the docstring indent is the indent of the + # first non-blank line after the initial line, which is returned + # as a whitespace string. + # This is not a general method for determining the indent of a docstring! + # See the source code of textwrap.dedent() for that. + doclines = doc.split("\n") + for line in doclines[1:]: + if re.match(r"\s*$", line): + continue + linematch = re.match(r"(\s*)\S", line) + return linematch.group(1) + # If there was no content in the docstring beyond the initial line + return "" + + def reqrep(wrapped: t.Callable, meth: t.Callable, channel: str = "shell") -> t.Callable: wrapped = wrapped(meth, channel) if not meth.__doc__: @@ -42,31 +61,34 @@ def reqrep(wrapped: t.Callable, meth: t.Callable, channel: str = "shell") -> t.C # so don't bother building the wrapped docstring return wrapped - basedoc, _ = meth.__doc__.split("Returns\n", 1) - parts = [basedoc.strip()] - if "Parameters" not in basedoc: - parts.append( - """ + params_header = """\ Parameters ---------- """ - ) - parts.append( - """ - reply: bool (default: False) + returns_doc = """\ + reply : bool (default: False) Whether to wait for and return reply - timeout: float or None (default: None) + + timeout : float or None (default: None) Timeout to use when waiting for a reply Returns ------- - msg_id: str + msg_id : str The msg_id of the request sent, if reply=False (default) - reply: dict + + reply : dict The reply message for this request, if reply=True - """ - ) - wrapped.__doc__ = "\n".join(parts) + """ + + basedoc, _ = meth.__doc__.split("Returns\n", 1) + parts = [basedoc.strip()] + indent = get_docstring_indent(basedoc) + if "Parameters" not in basedoc: + parts.append(textwrap.indent(textwrap.dedent(params_header), indent)) + parts.append(textwrap.indent(textwrap.dedent(returns_doc), indent)) + + wrapped.__doc__ = "\n\n".join(parts) return wrapped From c53d48deb5ace708b54a382af688ab88d93e3cc5 Mon Sep 17 00:00:00 2001 From: Julian Gilbey Date: Mon, 13 Jan 2025 20:37:56 +0000 Subject: [PATCH 2/2] Assert match object is not None, making linter happy (and if it None, something is very wrong) --- jupyter_client/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jupyter_client/client.py b/jupyter_client/client.py index e0eaf3cc..5e11914c 100644 --- a/jupyter_client/client.py +++ b/jupyter_client/client.py @@ -49,6 +49,7 @@ def get_docstring_indent(doc: str) -> str: if re.match(r"\s*$", line): continue linematch = re.match(r"(\s*)\S", line) + assert linematch is not None return linematch.group(1) # If there was no content in the docstring beyond the initial line return ""