From a6012dcce9484ae77abeed8716ae991c0d3559c8 Mon Sep 17 00:00:00 2001 From: "Jason H. Nicholson" <1058191+jasonnicholson@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:24:02 -0500 Subject: [PATCH 1/7] test: adding arguments block variant testing The main point is to maintain interface to the user so that these types of blocks continue to work. --- .../f_with_output_repeating_argument_block.m | 10 ++++ .../f_with_repeating_argument_block.m | 14 +++++ tests/test_matlabify.py | 2 + tests/test_parse_mfile.py | 58 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 tests/test_data/f_with_output_repeating_argument_block.m create mode 100644 tests/test_data/f_with_repeating_argument_block.m diff --git a/tests/test_data/f_with_output_repeating_argument_block.m b/tests/test_data/f_with_output_repeating_argument_block.m new file mode 100644 index 00000000..d526c291 --- /dev/null +++ b/tests/test_data/f_with_output_repeating_argument_block.m @@ -0,0 +1,10 @@ +function varargout = f_with_output_repeating_argument_block(a1) + arguments (Input) + a1 (1,1) double {mustBePositive} % Positive scalar input + end + arguments (Output,Repeating) + varargout (1,1) double {mustBePositive} % Repeating outputs + end + + varargout = {a1, a1 + 1}; +end diff --git a/tests/test_data/f_with_repeating_argument_block.m b/tests/test_data/f_with_repeating_argument_block.m new file mode 100644 index 00000000..009a2eeb --- /dev/null +++ b/tests/test_data/f_with_repeating_argument_block.m @@ -0,0 +1,14 @@ +function s = f_with_repeating_argument_block(a, varargin) + arguments (Input) + a (1,1) double {mustBePositive} % Positive scalar input + end + arguments (Repeating) + varargin (1,1) double {mustBePositive} % Repeating positive scalar + end + + arguments (Output) + s (1,1) double + end + + s = sum([a, varargin{:}]); +end diff --git a/tests/test_matlabify.py b/tests/test_matlabify.py index 577105ce..49a900e1 100644 --- a/tests/test_matlabify.py +++ b/tests/test_matlabify.py @@ -94,6 +94,8 @@ def test_module(mod): "f_with_function_variable", "f_with_input_argument_block", "f_with_output_argument_block", + "f_with_repeating_argument_block", + "f_with_output_repeating_argument_block", "ClassWithUndocumentedMembers", "ClassWithGetterSetter", "ClassWithDoubleQuotedString", diff --git a/tests/test_parse_mfile.py b/tests/test_parse_mfile.py index 261b4c21..3f41239f 100644 --- a/tests/test_parse_mfile.py +++ b/tests/test_parse_mfile.py @@ -942,5 +942,63 @@ def test_f_with_output_argument_block(): assert obj.retv["o3"]["validators"] == ["mustBePositive"] +def test_f_with_repeating_argument_block(): + mfile = os.path.join(DIRNAME, "test_data", "f_with_repeating_argument_block.m") + obj = mat_types.MatObject.parse_mfile( + mfile, "f_with_repeating_argument_block", "test_data" + ) + + assert obj.name == "f_with_repeating_argument_block" + assert list(obj.retv.keys()) == ["s"] + assert list(obj.args.keys()) == ["a", "varargin"] + + input_arg = obj.args["a"] + assert input_arg["attrs"] == {"Input": None} + assert input_arg["size"] == ("1", "1") + assert input_arg["type"] == "double" + assert input_arg["validators"] == ["mustBePositive"] + assert input_arg["docstring"] == "Positive scalar input" + + repeating_arg = obj.args["varargin"] + assert repeating_arg["attrs"] == {"Repeating": None} + assert repeating_arg["size"] == ("1", "1") + assert repeating_arg["type"] == "double" + assert repeating_arg["validators"] == ["mustBePositive"] + assert repeating_arg["docstring"] == "Repeating positive scalar" + + s_out = obj.retv["s"] + assert s_out["attrs"] == {"Output": None} + assert s_out["size"] == ("1", "1") + assert s_out["type"] == "double" + assert s_out["docstring"] is None + + +def test_f_with_output_repeating_argument_block(): + mfile = os.path.join( + DIRNAME, "test_data", "f_with_output_repeating_argument_block.m" + ) + obj = mat_types.MatObject.parse_mfile( + mfile, "f_with_output_repeating_argument_block", "test_data" + ) + + assert obj.name == "f_with_output_repeating_argument_block" + assert list(obj.retv.keys()) == ["varargout"] + assert list(obj.args.keys()) == ["a1"] + + input_arg = obj.args["a1"] + assert input_arg["attrs"] == {"Input": None} + assert input_arg["size"] == ("1", "1") + assert input_arg["type"] == "double" + assert input_arg["validators"] == ["mustBePositive"] + assert input_arg["docstring"] == "Positive scalar input" + + output_arg = obj.retv["varargout"] + assert output_arg["attrs"] == {"Output": None, "Repeating": None} + assert output_arg["size"] == ("1", "1") + assert output_arg["type"] == "double" + assert output_arg["validators"] == ["mustBePositive"] + assert output_arg["docstring"] == "Repeating outputs" + + if __name__ == "__main__": pytest.main([os.path.abspath(__file__)]) From 7e627bc726bdab16cd43638be72c3c1223889180 Mon Sep 17 00:00:00 2001 From: "Jason H. Nicholson" <1058191+jasonnicholson@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:49:53 -0500 Subject: [PATCH 2/7] fix: m-files in root of matlab_src_dir now work more consistently --- sphinxcontrib/mat_tree_sitter_parser.py | 24 ++++++ sphinxcontrib/mat_types.py | 97 +++++++++++++++++++------ 2 files changed, 97 insertions(+), 24 deletions(-) diff --git a/sphinxcontrib/mat_tree_sitter_parser.py b/sphinxcontrib/mat_tree_sitter_parser.py index acfe212d..d5250d99 100644 --- a/sphinxcontrib/mat_tree_sitter_parser.py +++ b/sphinxcontrib/mat_tree_sitter_parser.py @@ -2,8 +2,11 @@ from importlib.metadata import version import tree_sitter_matlab as tsml +from sphinx.util.logging import getLogger from tree_sitter import Language +logger = getLogger("matlab-domain") + # Attribute default dictionary used to give default values for e.g. `Abstract` or `Static` when used without # a right hand side i.e. `classdef (Abstract)` vs `classdef (Abstract=true)` # From: @@ -367,6 +370,10 @@ def _parse_argument_section(self, argblock_node): arguments = argblock_match.get("args") + # Bug fix: Check if arguments is None before iterating + if arguments is None: + return + # TODO this is almost identical to property parsing. # might be a good idea to extract common code here. for arg in arguments: @@ -547,6 +554,23 @@ def __init__(self, root_node, encoding): # Parse class basics class_matches = q_classdef.matches(root_node) + + # Bug fix: Handle empty matches gracefully + if not class_matches: + logger.warning( + "[sphinxcontrib-matlabdomain] No class definition found in file, skipping" + ) + # Set minimal attributes to avoid crashes + self.cls = None + self.name = None + self.attrs = {} + self.supers = [] + self.docstring = "" + self.properties = {} + self.methods = {} + self.enumerations = {} + return + _, class_match = class_matches[0] self.cls = class_match.get("class") self.name = class_match.get("name") diff --git a/sphinxcontrib/mat_types.py b/sphinxcontrib/mat_types.py index cea55957..d64d4405 100644 --- a/sphinxcontrib/mat_types.py +++ b/sphinxcontrib/mat_types.py @@ -152,6 +152,10 @@ def classfolder_class_name(dotted_path): def recursive_find_all(obj): # Recursively finds all entities in all "modules" aka directories. + # Bug fix: Check if obj.entities exists and is not None + if not hasattr(obj, "entities") or obj.entities is None: + return + for _, o in obj.entities: if isinstance(o, MatModule): o.safe_getmembers() @@ -161,6 +165,10 @@ def recursive_find_all(obj): def recursive_log_debug(obj, indent=""): # Traverse the object hierarchy and log to debug + # Bug fix: Check if obj.entities exists and is not None + if not hasattr(obj, "entities") or obj.entities is None: + return + for n, o in obj.entities: logger.debug( "[sphinxcontrib-matlabdomain] %s Name=%s, Entity=%s", indent, n, str(o) @@ -185,6 +193,10 @@ def recursive_log_debug(obj, indent=""): def populate_entities_table(obj, path=""): # Recursively scan the hiearachy of entities and populate the entities_table. + # Bug fix: Check if obj.entities exists and is not None + if not hasattr(obj, "entities") or obj.entities is None: + return + for _n, o in obj.entities: fullpath = path + "." + o.name fullpath = fullpath.lstrip(".") @@ -207,37 +219,70 @@ def analyze(app): # `matlab_src_dir` is recursively scanned for MATLAB objects only once. # All entities found are stored in globally available `entities_table` - if app.env.config.matlab_src_dir is None: - logger.debug( - "[sphinxcontrib-matlabdomain] matlab_src_dir is None, skipping parsing." + try: + if app.env.config.matlab_src_dir is None: + logger.debug( + "[sphinxcontrib-matlabdomain] matlab_src_dir is None, skipping parsing." + ) + return + + # Interpret `matlab_src_dir` relative to the sphinx source directory. + basedir = os.path.normpath( + os.path.join(app.env.srcdir, app.env.config.matlab_src_dir) ) - return + MatObject.basedir = basedir # set MatObject base directory + MatObject.sphinx_env = app.env # pass env to MatObject cls + MatObject.sphinx_app = app # pass app to MatObject cls + + entities_table.clear() + entities_name_map.clear() + + # Set the root object and get root members. + logger.debug("[sphinxcontrib-matlabdomain] Starting matlabify") + root = MatObject.matlabify("") + if not root: + logger.debug("[sphinxcontrib-matlabdomain] root is None, returning") + return - # Interpret `matlab_src_dir` relative to the sphinx source directory. - basedir = os.path.normpath( - os.path.join(app.env.srcdir, app.env.config.matlab_src_dir) - ) - MatObject.basedir = basedir # set MatObject base directory - MatObject.sphinx_env = app.env # pass env to MatObject cls - MatObject.sphinx_app = app # pass app to MatObject cls + logger.debug( + f"[sphinxcontrib-matlabdomain] root={root}, root.entities={getattr(root, 'entities', 'NO ENTITIES ATTR')}" + ) + root.safe_getmembers() + logger.debug( + f"[sphinxcontrib-matlabdomain] After safe_getmembers, root.entities={getattr(root, 'entities', 'NO ENTITIES ATTR')}" + ) - entities_table.clear() - entities_name_map.clear() + logger.debug("[sphinxcontrib-matlabdomain] Starting recursive_find_all") + recursive_find_all(root) + logger.debug("[sphinxcontrib-matlabdomain] Finished recursive_find_all") - # Set the root object and get root members. - root = MatObject.matlabify("") - if not root: - return - root.safe_getmembers() - recursive_find_all(root) + # Print the hierarchy of entities to the log. + logger.debug("[sphinxcontrib-matlabdomain] Found the following entities:") + recursive_log_debug(root) - # Print the hierarchy of entities to the log. - logger.debug("[sphinxcontrib-matlabdomain] Found the following entities:") - recursive_log_debug(root) + logger.debug("[sphinxcontrib-matlabdomain] Starting populate_entities_table") + populate_entities_table(root) + logger.debug("[sphinxcontrib-matlabdomain] Finished populate_entities_table") + entities_table["."] = root + except Exception as e: + import traceback - populate_entities_table(root) - entities_table["."] = root + logger.error(f"[sphinxcontrib-matlabdomain] ERROR in analyze: {e}") + logger.error( + f"[sphinxcontrib-matlabdomain] Traceback: {traceback.format_exc()}" + ) + raise + # Transform Class Folders classes from + # + # @ClassFolder (Module) + # ClassFolder (Class) # noqa: ERA001 + # method1 (Function) # noqa: ERA001 + # method2 (Function) # noqa: ERA001 + # + # To + # + # ClassFolder (Class) with the method1 and method2 add to the ClassFolder Class. def isClassFolderModule(name, entity): if not isinstance(entity, MatModule): return False @@ -250,6 +295,10 @@ def isClassFolderModule(name, entity): } # For each Class Folder module for cf_entity in class_folder_modules.values(): + # Bug fix: Check if cf_entity has entities and they're not None + if not hasattr(cf_entity, "entities") or cf_entity.entities is None: + continue + # 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)] From 4643ada6f74548e2a4dd09a8bae9fcb55cf897a2 Mon Sep 17 00:00:00 2001 From: "Jason H. Nicholson" <1058191+jasonnicholson@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:51:49 -0500 Subject: [PATCH 3/7] fix: Fixed a spelling mistake in tests/test_data/ClassWithSeparatedComments.m --- ...thSeperatedComments.m => ClassWithSeparatedComments.m} | 2 +- tests/test_matlabify.py | 2 +- tests/test_parse_mfile.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename tests/test_data/{ClassWithSeperatedComments.m => ClassWithSeparatedComments.m} (71%) diff --git a/tests/test_data/ClassWithSeperatedComments.m b/tests/test_data/ClassWithSeparatedComments.m similarity index 71% rename from tests/test_data/ClassWithSeperatedComments.m rename to tests/test_data/ClassWithSeparatedComments.m index 90e6fa16..13850831 100644 --- a/tests/test_data/ClassWithSeperatedComments.m +++ b/tests/test_data/ClassWithSeparatedComments.m @@ -1,4 +1,4 @@ -classdef ClassWithSeperatedComments +classdef ClassWithSeparatedComments properties % some comment diff --git a/tests/test_matlabify.py b/tests/test_matlabify.py index 49a900e1..eb05cd5b 100644 --- a/tests/test_matlabify.py +++ b/tests/test_matlabify.py @@ -112,7 +112,7 @@ def test_module(mod): "ClassWithPropertyValidators", "ClassWithTrailingCommentAfterBases", "ClassWithTrailingSemicolons", - "ClassWithSeperatedComments", + "ClassWithSeparatedComments", "ClassWithKeywordsAsFieldnames", "ClassWithPropertyCellValues", "ClassWithTests", diff --git a/tests/test_parse_mfile.py b/tests/test_parse_mfile.py index 3f41239f..21222572 100644 --- a/tests/test_parse_mfile.py +++ b/tests/test_parse_mfile.py @@ -839,12 +839,12 @@ def test_ClassWithTrailingSemicolons(): ] -def test_ClassWithSeperatedComments(): - mfile = os.path.join(TESTDATA_ROOT, "ClassWithSeperatedComments.m") +def test_ClassWithSeparatedComments(): + mfile = os.path.join(TESTDATA_ROOT, "ClassWithSeparatedComments.m") obj = mat_types.MatObject.parse_mfile( - mfile, "ClassWithSeperatedComments", "test_data" + mfile, "ClassWithSeparatedComments", "test_data" ) - assert obj.name == "ClassWithSeperatedComments" + assert obj.name == "ClassWithSeparatedComments" assert obj.bases == [] assert "prop" in obj.properties prop = obj.properties["prop"] From 3ee9ebf3d4eb104ba0e8620cfeb39f28af5d551d Mon Sep 17 00:00:00 2001 From: "Jason H. Nicholson" <1058191+jasonnicholson@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:56:27 -0500 Subject: [PATCH 4/7] fix: files in root of the matlab_src_dir automatically resolve. No ".. mat:currentmodule :: ." needed. All unit tests are still passing. --- .gitignore | 1 + sphinxcontrib/mat_documenters.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f423051b..fe1c5c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ tests/test_docs/_build/ _build htmlcov .coverage +.coverage.* coverage.xml build/ diff --git a/sphinxcontrib/mat_documenters.py b/sphinxcontrib/mat_documenters.py index 9b4ad9b6..8d9509fe 100644 --- a/sphinxcontrib/mat_documenters.py +++ b/sphinxcontrib/mat_documenters.py @@ -111,7 +111,7 @@ def parse_name(self): self.modname, self.objpath = self.resolve_name(modname, parents, path, base) - if not self.modname: + if self.modname is None: return False self.args = args @@ -857,7 +857,9 @@ def resolve_name(self, modname, parents, path, base): # ... or in the scope of a module directive if not modname: modname = self.env.temp_data.get("mat:module") - # ... else, it stays None, which means invalid + # ... else, default to root module (empty string) + if not modname: + modname = "" return modname, [*parents, base] @@ -889,7 +891,9 @@ def resolve_name(self, modname, parents, path, base): modname = self.env.temp_data.get("autodoc:module") if not modname: modname = self.env.temp_data.get("mat:module") - # ... else, it stays None, which means invalid + # ... else, default to root module (empty string) + if not modname: + modname = "" return modname, [*parents, base] From 62f1e718101324651fca28fbca166626d4e5bc0b Mon Sep 17 00:00:00 2001 From: "Jason H. Nicholson" <1058191+jasonnicholson@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:04:23 -0500 Subject: [PATCH 5/7] fix: `.. mat:automodule:: .` now works --- sphinxcontrib/mat_documenters.py | 91 +++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/sphinxcontrib/mat_documenters.py b/sphinxcontrib/mat_documenters.py index 8d9509fe..b0bf8b5a 100644 --- a/sphinxcontrib/mat_documenters.py +++ b/sphinxcontrib/mat_documenters.py @@ -59,7 +59,7 @@ mat_ext_sig_re = re.compile( r"""^ ([+@]?[+@\w.]+::)? # explicit module name ([+@]?[+@\w.]+\.)? # module and/or class name(s) - ([+@]?\w+) \s* # thing name + (\.|[+@]?\w+) \s* # thing name or dot for global namespace (?: \((.*)\) # optional: arguments (?:\s* -> \s* (.*))? # return annotation )? $ # and nothing more @@ -101,15 +101,25 @@ def parse_name(self): ) return False - # support explicit module and class name separation via :: - if explicit_modname is not None: - modname = explicit_modname[:-2] - parents = (path and path.rstrip(".").split(".")) or [] + # Handle special case where base is "." (global namespace) + # This only applies when there's no explicit module name and no path + if base == "." and explicit_modname is None and path is None: + logger.info( + "[sphinxcontrib-matlabdomain] parse_name: Special case for root module detected" + ) + # "." means document the global namespace (root module) + self.modname = "" + self.objpath = [] else: - modname = None - parents = [] + # support explicit module and class name separation via :: + if explicit_modname is not None: + modname = explicit_modname[:-2] + parents = (path and path.rstrip(".").split(".")) or [] + else: + modname = None + parents = [] - self.modname, self.objpath = self.resolve_name(modname, parents, path, base) + self.modname, self.objpath = self.resolve_name(modname, parents, path, base) if self.modname is None: return False @@ -132,6 +142,19 @@ def import_object(self): try: msg = f"[sphinxcontrib-matlabdomain] MatlabDocumenter.import_object {self.modname=}, {self.objpath=}, {self.fullname=}." logger.debug(msg) + + # Handle special case: documenting the root/global namespace + if self.modname == "" and len(self.objpath) == 0: + # Look up the root module in entities_table + root_obj = entities_table.get(".") + logger.info( + f"[sphinxcontrib-matlabdomain] import_object: Found root module: {root_obj is not None}, type: {type(root_obj).__name__ if root_obj else 'None'}" + ) + self.object = root_obj + if self.object is None: + raise KeyError("Root module '.' not found in entities_table") + return True + if len(self.objpath) > 1: lookup_name = ".".join([self.modname, self.objpath[0]]) lookup_name = lookup_name.lstrip(".") @@ -674,7 +697,12 @@ def document_members(self, all_members=False): classes.sort(key=lambda cls: cls.priority) # give explicitly separated module name, so that members # of inner classes can be documented - full_mname = self.modname + "::" + ".".join([*self.objpath, mname]) + # Special handling for root module (empty modname) + if self.modname == "" and len(self.objpath) == 0: + # For root module members, just use the member name + full_mname = mname + else: + full_mname = self.modname + "::" + ".".join([*self.objpath, mname]) documenter = classes[-1](self.directive, full_mname, self.indent) memberdocumenters.append((documenter, isattr)) member_order = self.options.member_order or self.env.config.autodoc_member_order @@ -740,22 +768,29 @@ def generate( self.real_modname = real_modname or self.get_real_modname() # try to also get a source code analyzer for attribute docs - try: - self.analyzer = MatModuleAnalyzer.for_module(self.real_modname) - # parse right now, to get PycodeErrors on parsing (results will - # be cached anyway) - self.analyzer.find_attr_docs() - except PycodeError as err: - self.env.app.debug( - "[sphinxcontrib-matlabdomain] module analyzer failed: %s", err - ) - # no source file -- e.g. for builtin and C modules + # For the root module (empty modname), skip the analyzer + if self.real_modname: + try: + self.analyzer = MatModuleAnalyzer.for_module(self.real_modname) + # parse right now, to get PycodeErrors on parsing (results will + # be cached anyway) + self.analyzer.find_attr_docs() + except PycodeError as err: + self.env.app.debug( + "[sphinxcontrib-matlabdomain] module analyzer failed: %s", err + ) + # no source file -- e.g. for builtin and C modules + self.analyzer = None + # at least add the module.__file__ as a dependency + if hasattr(self.module, "__file__") and self.module.__file__: + self.directive.record_dependencies.add(self.module.__file__) + else: + self.directive.record_dependencies.add(self.analyzer.srcname) + else: + # Root module has no real module name, so no analyzer needed self.analyzer = None - # at least add the module.__file__ as a dependency if hasattr(self.module, "__file__") and self.module.__file__: self.directive.record_dependencies.add(self.module.__file__) - else: - self.directive.record_dependencies.add(self.analyzer.srcname) # check __module__ of object (for members not given explicitly) if check_module: @@ -795,7 +830,14 @@ def parse_name(self): return ret def add_directive_header(self, sig): - MatlabDocumenter.add_directive_header(self, sig) + # Special case for root module (empty modname and objpath) + if self.modname == "" and len(self.objpath) == 0: + # Don't add the directive header for root module + # Just add the signature if there is one + if sig: + self.add_line(sig, self.get_sourcename()) + else: + MatlabDocumenter.add_directive_header(self, sig) # add some module-specific options if self.options.synopsis: @@ -810,7 +852,8 @@ def get_object_members(self, want_all): if not hasattr(self.object, "__all__"): # for implicit module members, check __module__ to avoid # documenting imported objects - return True, self.object.safe_getmembers() + members = self.object.safe_getmembers() + return True, members else: memberlist = [name for name, obj in self.object.__all__] else: From e4b57a7f7727e380619f679adf42adb2291b9b12 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 4 Jan 2026 07:12:27 +0100 Subject: [PATCH 6/7] apply precommit fixes --- sphinxcontrib/mat_tree_sitter_parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinxcontrib/mat_tree_sitter_parser.py b/sphinxcontrib/mat_tree_sitter_parser.py index 73bc564b..989cea2a 100644 --- a/sphinxcontrib/mat_tree_sitter_parser.py +++ b/sphinxcontrib/mat_tree_sitter_parser.py @@ -5,7 +5,6 @@ from sphinx.util.logging import getLogger from tree_sitter import Language - logger = getLogger("matlab-domain") # Attribute default dictionary used to give default values From 28f6c63e15221d97a498c18a3822aa52fa72a1be Mon Sep 17 00:00:00 2001 From: "Jason H. Nicholson" <1058191+jasonnicholson@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:23:46 -0500 Subject: [PATCH 7/7] refactor: update sphinxcontrib/mat_types.py Co-authored-by: Remi Gau --- sphinxcontrib/mat_types.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sphinxcontrib/mat_types.py b/sphinxcontrib/mat_types.py index 020d4197..d0014629 100644 --- a/sphinxcontrib/mat_types.py +++ b/sphinxcontrib/mat_types.py @@ -155,7 +155,7 @@ def classfolder_class_name(dotted_path): def recursive_find_all(obj): # Recursively finds all entities in all "modules" aka directories. # Bug fix: Check if obj.entities exists and is not None - if not hasattr(obj, "entities") or obj.entities is None: + if getattr(obj, "entities", None) is None: return for _, o in obj.entities: @@ -168,7 +168,7 @@ def recursive_find_all(obj): def recursive_log_debug(obj, indent=""): # Traverse the object hierarchy and log to debug # Bug fix: Check if obj.entities exists and is not None - if not hasattr(obj, "entities") or obj.entities is None: + if getattr(obj, "entities", None) is None: return for n, o in obj.entities: @@ -192,8 +192,7 @@ def recursive_log_debug(obj, indent=""): def populate_entities_table(obj, path=""): # Recursively scan the hiearachy of entities and populate the entities_table. - # Bug fix: Check if obj.entities exists and is not None - if not hasattr(obj, "entities") or obj.entities is None: + if getattr(obj, "entities", None) is None: return for _n, o in obj.entities: