Skip to content

Commit

Permalink
Parse class folder methods correctly (#213)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joeced authored Sep 13, 2023
1 parent e29d951 commit 2cf37c3
Show file tree
Hide file tree
Showing 22 changed files with 370 additions and 13 deletions.
13 changes: 11 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
12 changes: 8 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down Expand Up @@ -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``
Expand Down
64 changes: 61 additions & 3 deletions sphinxcontrib/mat_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/roots/test_classfolder/@First/First.m
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/roots/test_classfolder/@First/method_in_folder.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function [varargout] = method_in_folder(obj, varargin)
% A method defined in the folder
varargout = varargin;
20 changes: 20 additions & 0 deletions tests/roots/test_classfolder/Makefile
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions tests/roots/test_classfolder/conf.py
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions tests/roots/test_classfolder/index.rst
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions tests/roots/test_classfolder/index_first.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
First
-----
.. currentmodule:: .

.. autoclass:: First
:show-inheritance:
:members:
8 changes: 8 additions & 0 deletions tests/roots/test_classfolder/index_second.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Second
------

.. currentmodule:: src

.. autoclass:: Second
:show-inheritance:
:members:
6 changes: 6 additions & 0 deletions tests/roots/test_classfolder/index_third.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Third
-----

.. autoclass:: pkg.Third
:show-inheritance:
:members:
36 changes: 36 additions & 0 deletions tests/roots/test_classfolder/make.bat
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions tests/roots/test_classfolder/readme.txt
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions tests/roots/test_classfolder/src/+pkg/@Third/Third.m
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function [varargout] = method_in_folder(obj, varargin)
% A method defined in the folder
varargout = varargin;
19 changes: 19 additions & 0 deletions tests/roots/test_classfolder/src/@Second/Second.m
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/roots/test_classfolder/src/@Second/method_in_folder.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function [varargout] = method_in_folder(obj, varargin)
% A method defined in the folder
varargout = varargin;
2 changes: 1 addition & 1 deletion tests/test_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)


Expand Down
2 changes: 1 addition & 1 deletion tests/test_autodoc_short_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)


Expand Down
Loading

0 comments on commit 2cf37c3

Please sign in to comment.