diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 40053f086cc..a33d5590cec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -58,6 +58,9 @@ jobs: fail-fast: false matrix: include: + - os: ubuntu-latest + python: '3.13' + kind: pip - os: ubuntu-latest python: '3.12' kind: pip-pre diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ca4177174f..40e214102e1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -171,7 +171,8 @@ stages: python -m pip install --progress-bar off --upgrade pip python -m pip install --progress-bar off --upgrade --pre --only-binary=\"numpy,scipy,matplotlib,vtk\" numpy scipy matplotlib vtk python -c "import vtk" - python -m pip install --progress-bar off --upgrade -ve .[full,test_extra] + # Bug on 2.5.13 https://github.com/openmeeg/openmeeg/issues/700 + python -m pip install --progress-bar off --upgrade -ve .[full,test_extra] "openmeeg==2.5.12" displayName: 'Install dependencies with pip' - bash: | set -e diff --git a/doc/changes/devel/13021.dependency.rst b/doc/changes/devel/13021.dependency.rst new file mode 100644 index 00000000000..55681cc06e4 --- /dev/null +++ b/doc/changes/devel/13021.dependency.rst @@ -0,0 +1 @@ +Compatibility improved for Python 3.13, by `Eric Larson`_. \ No newline at end of file diff --git a/mne/evoked.py b/mne/evoked.py index a985fc30ad7..5fb09db9d1b 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -615,7 +615,8 @@ def plot_topo( exclude="bads", show=True, ): - """ + """. + Notes ----- .. versionadded:: 0.10.0 diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index d32e62a454e..c94da5e5ab8 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -76,6 +76,7 @@ def _func_name(func, cls=None): error_ignores = { # These we do not live by: "GL01", # Docstring should start in the line immediately after the quotes + "GL02", # Closing quotes on own line (doesn't work on Python 3.13 anyway) "EX01", "EX02", # examples failed (we test them separately) "ES01", # no extended summary diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 5ff7b480ddc..082a50feb35 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -5121,6 +5121,8 @@ def copy_doc(source): This is useful when inheriting from a class and overloading a method. This decorator can be used to copy the docstring of the original method. + Docstrings are processed by :func:`python:inspect.cleandoc` before being used. + Parameters ---------- source : function @@ -5143,7 +5145,8 @@ def copy_doc(source): ... ''' this gets appended''' ... pass >>> print(B.m1.__doc__) - Docstring for m1 this gets appended + Docstring for m1 + this gets appended """ def wrapper(func): @@ -5151,7 +5154,7 @@ def wrapper(func): raise ValueError("Cannot copy docstring: docstring was empty.") doc = source.__doc__ if func.__doc__ is not None: - doc += func.__doc__ + doc += f"\n{inspect.cleandoc(func.__doc__)}" func.__doc__ = doc return func @@ -5170,6 +5173,10 @@ def copy_function_doc_to_method_doc(source): function. This pattern is prevalent in for example the plotting functions of MNE. + Docstrings are parsed by :func:`python:inspect.cleandoc` before being used. + If indentation and newlines are important, make the first line ``.``, and the dot + will be removed and all following lines dedented jointly. + Parameters ---------- source : function @@ -5205,7 +5212,8 @@ def copy_function_doc_to_method_doc(source): >>> class A: ... @copy_function_doc_to_method_doc(plot_function) ... def plot(self, a, b): - ... ''' + ... '''. + ... ... Notes ... ----- ... .. versionadded:: 0.13.0 @@ -5214,26 +5222,31 @@ def copy_function_doc_to_method_doc(source): >>> print(A.plot.__doc__) Docstring for plotting function. - Parameters - ---------- - a : int - Some parameter - b : int - Some parameter - - Notes - ----- - .. versionadded:: 0.13.0 + Parameters + ---------- + a : int + Some parameter + b : int + Some parameter + Notes + ----- + .. versionadded:: 0.13.0 """ # noqa: D410, D411, D214, D215 def wrapper(func): - doc = source.__doc__.split("\n") + # Work with cleandoc'ed sources (py3.13-compat) + doc = inspect.cleandoc(source.__doc__).split("\n") + if func.__doc__ is not None: + func_doc = inspect.cleandoc(func.__doc__) + if func_doc[:2] == ".\n": + func_doc = func_doc[2:] + func_doc = f"\n{func_doc}" + else: + func_doc = "" + if len(doc) == 1: - doc = doc[0] - if func.__doc__ is not None: - doc += func.__doc__ - func.__doc__ = doc + func.__doc__ = f"{doc[0]}{func_doc}" return func # Find parameter block @@ -5281,7 +5294,7 @@ def wrapper(func): break else: # End of docstring reached - first_parameter_end = line + first_parameter_end = line + 1 first_parameter = parameter_block # Copy the docstring, but remove the first parameter @@ -5290,9 +5303,7 @@ def wrapper(func): + "\n" + "\n".join(doc[first_parameter_end:]) ) - if func.__doc__ is not None: - doc += func.__doc__ - func.__doc__ = doc + func.__doc__ = f"{doc}{func_doc}" return func return wrapper diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py index e7611081b55..4426eae0fc7 100644 --- a/mne/utils/tests/test_config.py +++ b/mne/utils/tests/test_config.py @@ -129,7 +129,7 @@ def test_sys_info_complete(): pyproject = tomllib.loads(pyproject.read_text("utf-8")) deps = pyproject["project"]["optional-dependencies"]["test_extra"] for dep in deps: - dep = dep.split("[")[0].split(">")[0] + dep = dep.split("[")[0].split(">")[0].strip() assert f" {dep}" in out, f"Missing in dev config: {dep}" diff --git a/mne/utils/tests/test_docs.py b/mne/utils/tests/test_docs.py index 64ebc2c6916..253317af760 100644 --- a/mne/utils/tests/test_docs.py +++ b/mne/utils/tests/test_docs.py @@ -195,28 +195,29 @@ def method_f3(self): assert ( A.method_f1.__doc__ - == """Docstring for f1. - - Parameters - ---------- - a : int - Parameter a - b : int - Parameter b - """ + == """\ +Docstring for f1. + +Parameters +---------- +a : int + Parameter a +b : int + Parameter b""" ) assert ( A.method_f2.__doc__ - == """Docstring for f2. + == """\ +Docstring for f2. - Returns - ------- - nothing. - method_f3 own docstring""" +Returns +------- +nothing. +method_f3 own docstring""" ) - assert A.method_f3.__doc__ == "Docstring for f3.\n\n " + assert A.method_f3.__doc__ == "Docstring for f3.\n\n" pytest.raises(ValueError, copy_function_doc_to_method_doc(f5), A.method_f1) diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 28d0b16f91c..17b5381d72f 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -5,7 +5,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) STD_ARGS="--progress-bar off --upgrade" python -m pip install $STD_ARGS pip setuptools wheel if [ "${TEST_MODE}" == "pip" ]; then - python -m pip install $STD_ARGS --only-binary="numba,llvmlite,numpy,scipy,vtk,dipy" -e .[test,full] + python -m pip install $STD_ARGS --only-binary="numba,llvmlite,numpy,scipy,vtk,dipy,openmeeg" -e .[test,full] "openmeeg==2.5.12" elif [ "${TEST_MODE}" == "pip-pre" ]; then ${SCRIPT_DIR}/install_pre_requirements.sh python -m pip install $STD_ARGS --pre -e .[test_extra] diff --git a/tools/circleci_dependencies.sh b/tools/circleci_dependencies.sh index bd9ec823871..9c32b008a28 100755 --- a/tools/circleci_dependencies.sh +++ b/tools/circleci_dependencies.sh @@ -19,4 +19,4 @@ python -m pip install --upgrade --progress-bar off \ mne-icalabel mne-lsl mne-microstates mne-nirs mne-rsa \ neurodsp neurokit2 niseq nitime openneuro-py pactools \ plotly pycrostates pyprep pyriemann python-picard sesameeg \ - sleepecg tensorpac yasa meegkit + sleepecg tensorpac yasa meegkit eeg_positions diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 1b74ce14e99..149f5a194da 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -20,6 +20,11 @@ if [ ! -z "$CONDA_ENV" ]; then INSTALL_KIND="test" STD_ARGS="--progress-bar off" fi +elif [[ "${MNE_CI_KIND}" == "pip" ]]; then + # Only used for 3.13 at the moment, just get test deps plus a few extras + # that we know are available + INSTALL_ARGS="nibabel scikit-learn numpydoc PySide6 mne-qt-browser" + INSTALL_KIND="test" else test "${MNE_CI_KIND}" == "pip-pre" STD_ARGS="$STD_ARGS --pre" diff --git a/tools/github_actions_env_vars.sh b/tools/github_actions_env_vars.sh index 8b9fc8560e6..3f9322d14c3 100755 --- a/tools/github_actions_env_vars.sh +++ b/tools/github_actions_env_vars.sh @@ -4,9 +4,13 @@ set -eo pipefail -x # old and minimal use conda if [[ "$MNE_CI_KIND" == "pip"* ]]; then echo "Setting pip env vars for $MNE_CI_KIND" - echo "MNE_QT_BACKEND=PyQt6" >> $GITHUB_ENV - # We should test an eager import somewhere, might as well be here - echo "EAGER_IMPORT=true" >> $GITHUB_ENV + if [[ "$MNE_CI_KIND" == "pip-pre" ]]; then + echo "MNE_QT_BACKEND=PyQt6" >> $GITHUB_ENV + # We should test an eager import somewhere, might as well be here + echo "EAGER_IMPORT=true" >> $GITHUB_ENV + else + echo "MNE_QT_BACKEND=PySide6" >> $GITHUB_ENV + fi else # conda-like echo "Setting conda env vars for $MNE_CI_KIND" if [[ "$MNE_CI_KIND" == "old" ]]; then