-
Notifications
You must be signed in to change notification settings - Fork 45
[FIX] fixes root m file processing #315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a6012dc
7e627bc
4643ada
3ee9ebf
62f1e71
5bdcd8f
b30844a
e4b57a7
28f6c63
781caea
3038614
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,5 +12,6 @@ tests/test_docs/_build/ | |
| _build | ||
| htmlcov | ||
| .coverage | ||
| .coverage.* | ||
| coverage.xml | ||
| build/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,17 +101,28 @@ 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 not self.modname: | ||
| if self.modname is None: | ||
| return False | ||
|
|
||
| self.args = args | ||
|
|
@@ -136,6 +147,21 @@ def import_object(self): | |
| f"{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( | ||
| "[sphinxcontrib-matlabdomain] import_object: " | ||
| f"Found root module: {root_obj is not None}, " | ||
| f"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(".") | ||
|
|
@@ -684,7 +710,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 = f"{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 = f"{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 | ||
|
|
@@ -751,22 +782,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 and not self.check_module(): | ||
|
|
@@ -808,7 +846,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: | ||
|
|
@@ -823,7 +868,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, _ in self.object.__all__] | ||
| else: | ||
|
|
@@ -867,7 +913,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] | ||
|
|
||
|
|
||
|
|
@@ -899,7 +947,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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, is this always a sane default? I suspect that this can lead to subtle bugs where things that were considered invalid before will now get dumbed in the "root" module. |
||
| if not modname: | ||
| modname = "" | ||
| return modname, [*parents, base] | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)` | ||
|
|
@@ -370,6 +373,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: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be necessary? The Either way this shouldn't hurt to add but would be good to add a test for this.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also probably no need to have |
||
| return | ||
|
|
||
| # TODO this is almost identical to property parsing. | ||
| # might be a good idea to extract common code here. | ||
| for arg in arguments: | ||
|
|
@@ -557,6 +564,24 @@ 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") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| classdef ClassWithSeperatedComments | ||
| classdef ClassWithSeparatedComments | ||
| properties | ||
| % some comment | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to declare the local variable here.