From 2cf37c32d14724384f947c8bb6b2252c67e2016a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Cederberg?= Date: Wed, 13 Sep 2023 10:35:41 +0200 Subject: [PATCH] Parse class folder methods correctly (#213) Previously, a class folder definition, `@ClassFolder` was treated like a collection of a module, class with methods and free functions. Module: target.@ClassFolder Class: target.@ClassFolder.ClassFolder Functions: target.@ClassFolder.method_in_file Now, we transform this into a single class definition, that can be referenced with: Class: target.ClassFolder This fixes #56. However, #44 is not solved by this. --- CHANGES.rst | 13 +++- README.rst | 12 ++-- sphinxcontrib/mat_types.py | 64 ++++++++++++++++++- tests/roots/test_classfolder/@First/First.m | 19 ++++++ .../@First/method_in_folder.m | 3 + tests/roots/test_classfolder/Makefile | 20 ++++++ tests/roots/test_classfolder/conf.py | 10 +++ tests/roots/test_classfolder/index.rst | 24 +++++++ tests/roots/test_classfolder/index_first.rst | 7 ++ tests/roots/test_classfolder/index_second.rst | 8 +++ tests/roots/test_classfolder/index_third.rst | 6 ++ tests/roots/test_classfolder/make.bat | 36 +++++++++++ tests/roots/test_classfolder/readme.txt | 10 +++ .../test_classfolder/src/+pkg/@Third/Third.m | 19 ++++++ .../src/+pkg/@Third/method_in_folder.m | 3 + .../test_classfolder/src/@Second/Second.m | 19 ++++++ .../src/@Second/method_in_folder.m | 3 + tests/test_autodoc.py | 2 +- tests/test_autodoc_short_links.py | 2 +- tests/test_classfolder.py | 60 +++++++++++++++++ tests/test_classfolder_class_name.py | 24 +++++++ tests/test_shortest_name.py | 19 +++++- 22 files changed, 370 insertions(+), 13 deletions(-) create mode 100644 tests/roots/test_classfolder/@First/First.m create mode 100644 tests/roots/test_classfolder/@First/method_in_folder.m create mode 100644 tests/roots/test_classfolder/Makefile create mode 100644 tests/roots/test_classfolder/conf.py create mode 100644 tests/roots/test_classfolder/index.rst create mode 100644 tests/roots/test_classfolder/index_first.rst create mode 100644 tests/roots/test_classfolder/index_second.rst create mode 100644 tests/roots/test_classfolder/index_third.rst create mode 100644 tests/roots/test_classfolder/make.bat create mode 100644 tests/roots/test_classfolder/readme.txt create mode 100644 tests/roots/test_classfolder/src/+pkg/@Third/Third.m create mode 100644 tests/roots/test_classfolder/src/+pkg/@Third/method_in_folder.m create mode 100644 tests/roots/test_classfolder/src/@Second/Second.m create mode 100644 tests/roots/test_classfolder/src/@Second/method_in_folder.m create mode 100644 tests/test_classfolder.py create mode 100644 tests/test_classfolder_class_name.py diff --git a/CHANGES.rst b/CHANGES.rst index 25209ee..2dc9299 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -sphinxcontrib-matlabdomain-0.20.0 (2023-MM-DD) +sphinxcontrib-matlabdomain-0.20.0 (2023-09-13) ============================================== * Fixed `Issue 188`_ and `Issue 189`_, which caused the extension to crash if @@ -8,10 +8,19 @@ sphinxcontrib-matlabdomain-0.20.0 (2023-MM-DD) methods) to links! This means that we can write class documentation as `MATLAB Class Help`_ suggests. Including property and methods lists in the class docstring. - +* Fixed `Issue 56`_. `MATLAB Folder Class definitions`_, i.e. prefixed with + ``@``, are now treated as normal classes. All methods defined in the class + defintion and in files in the ``@``-folder are availble. Further, it can + referenced by a shortened name. Before you had to explicity write out the + "module", class and methods. Now you can just write the class name. Only + caveat is that `Issue 44`_ still applies. + +.. _Issue 44: https://github.com/sphinx-contrib/matlabdomain/issues/44 +.. _Issue 56: https://github.com/sphinx-contrib/matlabdomain/issues/56 .. _Issue 188: https://github.com/sphinx-contrib/matlabdomain/issues/188 .. _Issue 189: https://github.com/sphinx-contrib/matlabdomain/issues/189 .. _MATLAB Class Help: https://mathworks.com/help/matlab/matlab_prog/create-help-for-classes.html +.. _MATLAB Folder Class definitions: https://mathworks.com/help/matlab/matlab_oop/organizing-classes-in-folders.html sphinxcontrib-matlabdomain-0.19.1 (2023-05-17) diff --git a/README.rst b/README.rst index 477e3de..f40c467 100644 --- a/README.rst +++ b/README.rst @@ -30,10 +30,10 @@ Usage The Python package must be installed with:: - pip install -U sphinxcontrib-matlabdomain + pip install sphinxcontrib-matlabdomain In general, the usage is the same as for documenting Python code. The package -is tested with Python >= 3.8 and Sphinx >=4.0.0. +is tested with Python >= 3.8 and Sphinx >= 4.5.0. For a Python 2 compatible version the package must be installed with:: @@ -67,17 +67,21 @@ Additional Configuration that everything is in the path as we would expect it in MATLAB. This will resemble a more MATLAB-like presentation. If it is ``True`` is forces ``matlab_keep_package_prefix = False``. Further, it allows for much shorter - and cleaner references. Example, given a path to a class like - ``target.subfolder.ClassFoo``. + and cleaner references. Example, given a path to classes like + ``target.subfolder.ClassFoo`` and ``target.@ClassFolder.Classfolder`` * With ``False``:: :class:`target.subfolder.ClassFoo` + :class:`target.@ClassFolder.Classfolder` + * With ``True``:: :class:`ClassFoo` + :class:`ClassFolder` + Default is ``False``. *Added in Version 0.19.0*. ``matlab_auto_link`` diff --git a/sphinxcontrib/mat_types.py b/sphinxcontrib/mat_types.py index e10dcb3..3ad8209 100644 --- a/sphinxcontrib/mat_types.py +++ b/sphinxcontrib/mat_types.py @@ -119,16 +119,37 @@ def shortest_name(dotted_path): if len(parts) == 1: return parts[0].lstrip("+") + if "@" in dotted_path: + return dotted_path + parts_to_keep = [] for part in parts[:-1]: - if part.startswith("+") or part.startswith("@"): - parts_to_keep.append(part.lstrip("+@")) + if part.startswith("+"): + parts_to_keep.append(part.lstrip("+")) elif len(parts_to_keep) > 0: parts_to_keep = [] - parts_to_keep.append(parts[-1].lstrip("+@")) + parts_to_keep.append(parts[-1].lstrip("+")) return ".".join(parts_to_keep) +def classfolder_class_name(dotted_path): + # Returns a @ClassFolder classname if applicable, otherwise the dotted_path is returned + # + if "@" not in dotted_path: + return dotted_path + + parts = dotted_path.split(".") + if len(parts) == 1: + return dotted_path + + stripped_parts = [part.lstrip("@") for part in parts] + + if stripped_parts[-1] == stripped_parts[-2]: + return ".".join(parts[0:-2] + [stripped_parts[-1]]) + else: + return dotted_path + + def recursive_find_all(obj): # Recursively finds all entities in all "modules" aka directories. for _, o in obj.entities: @@ -204,6 +225,43 @@ def analyze(app): populate_entities_table(root) entities_table["."] = root + # Transform Class Folders classes from + # + # @ClassFolder (Module) + # ClassFolder (Class) + # method1 (Function) + # method2 (Function) + # + # To + # + # ClassFolder (Class) with the method1 and method2 add to the ClassFolder Class. + class_folder_modules = { + k: v for k, v in entities_table.items() if "@" in k and isinstance(v, MatModule) + } + # For each Class Folder module + for cf_name, cf_entity in class_folder_modules.items(): + # Find the class entity class. + class_entities = [e for e in cf_entity.entities if isinstance(e[1], MatClass)] + func_entities = [e for e in cf_entity.entities if isinstance(e[1], MatFunction)] + assert len(class_entities) == 1 + cls = class_entities[0][1] + + # Add functions to class + for func_name, func in func_entities: + func.__class__ = MatMethod + func.cls = cls + # TODO: Find the method attributes defined in classfolder class defintion. + func.attrs = {} + cls.methods[func.name] = func + + # Transform @ClassFolder names. Specifically + class_folder_names = {} + for name, entity in entities_table.items(): + alt_name = classfolder_class_name(name) + if name != alt_name: + class_folder_names[alt_name] = entity + entities_table.update(class_folder_names) + # Find alternative names to entities # target.+package.+sub.Class -> package.sub.Class # folder.subfolder.Class -> Class diff --git a/tests/roots/test_classfolder/@First/First.m b/tests/roots/test_classfolder/@First/First.m new file mode 100644 index 0000000..862f83f --- /dev/null +++ b/tests/roots/test_classfolder/@First/First.m @@ -0,0 +1,19 @@ +classdef First + % The first class + + properties + a % The property + end + + methods + function self = First(a) + % Constructor for First + self.a = a; + end + + function method_inside_classdef(obj, b) + % Method inside class definition + obj.a = b; + end + end +end diff --git a/tests/roots/test_classfolder/@First/method_in_folder.m b/tests/roots/test_classfolder/@First/method_in_folder.m new file mode 100644 index 0000000..9a7725a --- /dev/null +++ b/tests/roots/test_classfolder/@First/method_in_folder.m @@ -0,0 +1,3 @@ +function [varargout] = method_in_folder(obj, varargin) +% A method defined in the folder +varargout = varargin; diff --git a/tests/roots/test_classfolder/Makefile b/tests/roots/test_classfolder/Makefile new file mode 100644 index 0000000..697294a --- /dev/null +++ b/tests/roots/test_classfolder/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = test_classfolder +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/tests/roots/test_classfolder/conf.py b/tests/roots/test_classfolder/conf.py new file mode 100644 index 0000000..4f7437b --- /dev/null +++ b/tests/roots/test_classfolder/conf.py @@ -0,0 +1,10 @@ +import os + +matlab_src_dir = os.path.abspath(".") +matlab_short_links = True +extensions = ["sphinx.ext.autodoc", "sphinxcontrib.matlab"] +primary_domain = "mat" +project = "test_classfolder" +master_doc = "index" +source_suffix = ".rst" +nitpicky = True diff --git a/tests/roots/test_classfolder/index.rst b/tests/roots/test_classfolder/index.rst new file mode 100644 index 0000000..50e7b0f --- /dev/null +++ b/tests/roots/test_classfolder/index.rst @@ -0,0 +1,24 @@ +Description +=========== + +In this directory we test basic autodoc features for class folders. The +folder layout is:: + + test_classfolder + src - A typically folder + @Second + +pkg + @Third + @First + First.m + + +Table of contents +================= + +.. toctree:: + :maxdepth: 2 + + index_first + index_second + index_third diff --git a/tests/roots/test_classfolder/index_first.rst b/tests/roots/test_classfolder/index_first.rst new file mode 100644 index 0000000..c250644 --- /dev/null +++ b/tests/roots/test_classfolder/index_first.rst @@ -0,0 +1,7 @@ +First +----- +.. currentmodule:: . + +.. autoclass:: First + :show-inheritance: + :members: diff --git a/tests/roots/test_classfolder/index_second.rst b/tests/roots/test_classfolder/index_second.rst new file mode 100644 index 0000000..7a704f0 --- /dev/null +++ b/tests/roots/test_classfolder/index_second.rst @@ -0,0 +1,8 @@ +Second +------ + +.. currentmodule:: src + +.. autoclass:: Second + :show-inheritance: + :members: diff --git a/tests/roots/test_classfolder/index_third.rst b/tests/roots/test_classfolder/index_third.rst new file mode 100644 index 0000000..82ba673 --- /dev/null +++ b/tests/roots/test_classfolder/index_third.rst @@ -0,0 +1,6 @@ +Third +----- + +.. autoclass:: pkg.Third + :show-inheritance: + :members: diff --git a/tests/roots/test_classfolder/make.bat b/tests/roots/test_classfolder/make.bat new file mode 100644 index 0000000..be97017 --- /dev/null +++ b/tests/roots/test_classfolder/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=test_classfolder + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/tests/roots/test_classfolder/readme.txt b/tests/roots/test_classfolder/readme.txt new file mode 100644 index 0000000..ba6c304 --- /dev/null +++ b/tests/roots/test_classfolder/readme.txt @@ -0,0 +1,10 @@ +Test of class folder functionality +---------------------------------- + +Regular build.:: + + make html + +Regular build with very verbose settings, piped to file.:: + + sphinx-build -vvv -b html . _build\html > sphinx.log diff --git a/tests/roots/test_classfolder/src/+pkg/@Third/Third.m b/tests/roots/test_classfolder/src/+pkg/@Third/Third.m new file mode 100644 index 0000000..126f770 --- /dev/null +++ b/tests/roots/test_classfolder/src/+pkg/@Third/Third.m @@ -0,0 +1,19 @@ +classdef Third + % The third class + + properties + c % a property of a class folder + end + + methods + function self = Third(c) + % Constructor for Third + self.c = c; + end + + function method_inside_classdef(obj, d) + % Method inside class definition + obj.c = d; + end + end +end diff --git a/tests/roots/test_classfolder/src/+pkg/@Third/method_in_folder.m b/tests/roots/test_classfolder/src/+pkg/@Third/method_in_folder.m new file mode 100644 index 0000000..9a7725a --- /dev/null +++ b/tests/roots/test_classfolder/src/+pkg/@Third/method_in_folder.m @@ -0,0 +1,3 @@ +function [varargout] = method_in_folder(obj, varargin) +% A method defined in the folder +varargout = varargin; diff --git a/tests/roots/test_classfolder/src/@Second/Second.m b/tests/roots/test_classfolder/src/@Second/Second.m new file mode 100644 index 0000000..9de2380 --- /dev/null +++ b/tests/roots/test_classfolder/src/@Second/Second.m @@ -0,0 +1,19 @@ +classdef Second + % The second class + + properties + b % a property of a class folder + end + + methods + function self = Second(b) + % Constructor for Second + self.b = b; + end + + function method_inside_classdef(obj, c) + % Method inside class definition + obj.b = c; + end + end +end diff --git a/tests/roots/test_classfolder/src/@Second/method_in_folder.m b/tests/roots/test_classfolder/src/@Second/method_in_folder.m new file mode 100644 index 0000000..9a7725a --- /dev/null +++ b/tests/roots/test_classfolder/src/@Second/method_in_folder.m @@ -0,0 +1,3 @@ +function [varargout] = method_in_folder(obj, varargin) +% A method defined in the folder +varargout = varargin; diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index f4df8fb..00ebe07 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -137,7 +137,7 @@ def test_classfolder(make_app, rootdir): assert len(content) == 1 assert ( content[0].astext() - == "classfolder\n\n\n\nclass target.@ClassFolder.ClassFolder\n\nA class in a folder\n\nProperty Summary\n\n\n\n\n\np\n\na property of a class folder\n\nMethod Summary\n\n\n\n\n\nmethod_inside_classdef(a, b)\n\nMethod inside class definition" + == "classfolder\n\n\n\nclass target.@ClassFolder.ClassFolder\n\nA class in a folder\n\nProperty Summary\n\n\n\n\n\np\n\na property of a class folder\n\nMethod Summary\n\n\n\n\n\na_static_func(args)\n\nA static method in @ClassFolder\n\n\n\nclassMethod(varargin)\n\nCLASSMETHOD A function within a package\n\nParameters\n\nobj – An instance of this class.\n\nvarargin – Variable input arguments.\n\nReturns\n\nvarargout\n\n\n\nmethod_inside_classdef(a, b)\n\nMethod inside class definition" ) diff --git a/tests/test_autodoc_short_links.py b/tests/test_autodoc_short_links.py index ca35e3c..89e509d 100644 --- a/tests/test_autodoc_short_links.py +++ b/tests/test_autodoc_short_links.py @@ -141,7 +141,7 @@ def test_classfolder(make_app, rootdir): assert len(content) == 1 assert ( content[0].astext() - == "classfolder\n\n\n\nclass ClassFolder\n\nA class in a folder\n\nProperty Summary\n\n\n\n\n\np\n\na property of a class folder\n\nMethod Summary\n\n\n\n\n\nmethod_inside_classdef(a, b)\n\nMethod inside class definition" + == "classfolder\n\n\n\nclass ClassFolder\n\nA class in a folder\n\nProperty Summary\n\n\n\n\n\np\n\na property of a class folder\n\nMethod Summary\n\n\n\n\n\na_static_func(args)\n\nA static method in @ClassFolder\n\n\n\nclassMethod(varargin)\n\nCLASSMETHOD A function within a package\n\nParameters\n\nobj – An instance of this class.\n\nvarargin – Variable input arguments.\n\nReturns\n\nvarargout\n\n\n\nmethod_inside_classdef(a, b)\n\nMethod inside class definition" ) diff --git a/tests/test_classfolder.py b/tests/test_classfolder.py new file mode 100644 index 0000000..e2ba6be --- /dev/null +++ b/tests/test_classfolder.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" + test_autodoc + ~~~~~~~~~~~~ + + Test the autodoc extension. + + :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import pickle +import sys +import pytest +import helper +from sphinx import addnodes +from sphinx.testing.fixtures import make_app, test_params # noqa: F811; + + +@pytest.fixture(scope="module") +def rootdir(): + return helper.rootdir(__file__) + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") +def test_first(make_app, rootdir): + srcdir = rootdir / "roots" / "test_classfolder" + app = make_app(srcdir=srcdir) + app.builder.build_all() + + content = pickle.loads((app.doctreedir / "index_first.doctree").read_bytes()) + assert ( + content[0].astext() + == "First\n\n\n\nclass First\n\nThe first class\n\nConstructor Summary\n\n\n\n\n\nFirst(a)\n\nConstructor for First\n\nProperty Summary\n\n\n\n\n\na\n\nThe property\n\nMethod Summary\n\n\n\n\n\nmethod_in_folder(varargin)\n\nA method defined in the folder\n\n\n\nmethod_inside_classdef(b)\n\nMethod inside class definition" + ) + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") +def test_second(make_app, rootdir): + srcdir = rootdir / "roots" / "test_classfolder" + app = make_app(srcdir=srcdir) + app.builder.build_all + + content = pickle.loads((app.doctreedir / "index_second.doctree").read_bytes()) + assert ( + content[0].astext() + == "Second\n\n\n\nclass Second\n\nThe second class\n\nConstructor Summary\n\n\n\n\n\nSecond(b)\n\nConstructor for Second\n\nProperty Summary\n\n\n\n\n\nb\n\na property of a class folder\n\nMethod Summary\n\n\n\n\n\nmethod_in_folder(varargin)\n\nA method defined in the folder\n\n\n\nmethod_inside_classdef(c)\n\nMethod inside class definition" + ) + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") +def test_third(make_app, rootdir): + srcdir = rootdir / "roots" / "test_classfolder" + app = make_app(srcdir=srcdir) + app.builder.build_all + + content = pickle.loads((app.doctreedir / "index_third.doctree").read_bytes()) + assert ( + content[0].astext() + == "Third\n\n\n\nclass Third\n\nThe third class\n\nConstructor Summary\n\n\n\n\n\nThird(c)\n\nConstructor for Third\n\nProperty Summary\n\n\n\n\n\nc\n\na property of a class folder\n\nMethod Summary\n\n\n\n\n\nmethod_in_folder(varargin)\n\nA method defined in the folder\n\n\n\nmethod_inside_classdef(d)\n\nMethod inside class definition" + ) diff --git a/tests/test_classfolder_class_name.py b/tests/test_classfolder_class_name.py new file mode 100644 index 0000000..d362696 --- /dev/null +++ b/tests/test_classfolder_class_name.py @@ -0,0 +1,24 @@ +from sphinxcontrib.mat_types import classfolder_class_name + + +def test_classfolders(): + name = classfolder_class_name("target.@ClassFolder") + assert name == "target.@ClassFolder" + + name = classfolder_class_name("target.@ClassFolder.Func") + assert name == "target.@ClassFolder.Func" + + name = classfolder_class_name("target.@ClassFolder.ClassFolder") + assert name == "target.ClassFolder" + + name = classfolder_class_name("target.+pkg.@ClassFolder.ClassFolder") + assert name == "target.+pkg.ClassFolder" + + name = classfolder_class_name("@ClassFolder") + assert name == "@ClassFolder" + + name = classfolder_class_name("@ClassFolder.Func") + assert name == "@ClassFolder.Func" + + name = classfolder_class_name("@ClassFolder.ClassFolder") + assert name == "ClassFolder" diff --git a/tests/test_shortest_name.py b/tests/test_shortest_name.py index d883bc2..242f231 100644 --- a/tests/test_shortest_name.py +++ b/tests/test_shortest_name.py @@ -38,7 +38,22 @@ def test_weird(): def test_classfolders(): name = shortest_name("target.@ClassFolder") - assert name == "ClassFolder" + assert name == "target.@ClassFolder" name = shortest_name("target.@ClassFolder.Func") - assert name == "ClassFolder.Func" + assert name == "target.@ClassFolder.Func" + + name = shortest_name("target.@ClassFolder.ClassFolder") + assert name == "target.@ClassFolder.ClassFolder" + + name = shortest_name("target.+pkg.@ClassFolder.ClassFolder") + assert name == "target.+pkg.@ClassFolder.ClassFolder" + + name = shortest_name("@ClassFolder") + assert name == "@ClassFolder" + + name = shortest_name("@ClassFolder.Func") + assert name == "@ClassFolder.Func" + + name = shortest_name("@ClassFolder.ClassFolder") + assert name == "@ClassFolder.ClassFolder"