From 1f30a487f7650e363f752089c1842a015010698c Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 09:29:37 -0600 Subject: [PATCH 1/7] Adding follow_symlinks option. --- autoapi/_mapper.py | 7 ++++--- autoapi/extension.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/autoapi/_mapper.py b/autoapi/_mapper.py index 872c8b41..2b322294 100644 --- a/autoapi/_mapper.py +++ b/autoapi/_mapper.py @@ -312,9 +312,10 @@ def _wrapped_prepare(value): self._use_implicit_namespace = ( self.app.config.autoapi_python_use_implicit_namespaces ) + self._follow_symlinks = self.app.config.autoapi_follow_symlinks @staticmethod - def find_files(patterns, dirs, ignore): + def find_files(patterns, dirs, ignore, follow_symlinks: bool): if not ignore: ignore = [] @@ -324,7 +325,7 @@ def find_files(patterns, dirs, ignore): pattern_regexes.append((pattern, regex)) for _dir in dirs: # iterate autoapi_dirs - for root, subdirectories, filenames in os.walk(_dir): + for root, subdirectories, filenames in os.walk(_dir, followlinks=follow_symlinks): # skip directories if needed for sub_dir in subdirectories.copy(): # iterate copy as we adapt subdirectories during loop @@ -429,7 +430,7 @@ def _find_files(self, patterns, dirs, ignore): ): dir_root = os.path.abspath(os.path.join(dir_, os.pardir)) - for path in self.find_files(patterns=patterns, dirs=[dir_], ignore=ignore): + for path in self.find_files(patterns=patterns, dirs=[dir_], ignore=ignore, follow_symlinks=self._follow_symlinks): yield dir_root, path def load(self, patterns, dirs, ignore=None): diff --git a/autoapi/extension.py b/autoapi/extension.py index 218f28e8..e4f1a748 100644 --- a/autoapi/extension.py +++ b/autoapi/extension.py @@ -256,6 +256,7 @@ def setup(app): app.add_config_value("autoapi_add_toctree_entry", True, "html") app.add_config_value("autoapi_template_dir", None, "html") app.add_config_value("autoapi_include_summaries", None, "html") + app.add_config_value("autoapi_follow_symlinks", False, "html") app.add_config_value("autoapi_python_use_implicit_namespaces", False, "html") app.add_config_value("autoapi_python_class_content", "class", "html") app.add_config_value("autoapi_generate_api_docs", True, "html") From 674e4984f91ccf463613744b956c27884f0ff0ec Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 09:33:56 -0600 Subject: [PATCH 2/7] Adding documentation for autoapi_follow_symlinks. --- docs/reference/config.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 2f527c5e..b4186af5 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -92,6 +92,14 @@ Customisation Options A list of patterns to ignore when finding directories and files to document. +.. confval:: autoapi_follow_symlinks + + Default: ``False`` + + If `True`, then follow symlinks when walking ``autoapi_dirs`` to generate the API + documentation. If `False`, then do not follow symlinks when when walking + ``autoapi_dirs`` to generate the API documentation. + .. confval:: autoapi_root Default: ``autoapi`` From ee9bba813791d6606d2efd1412b1c69e437dfe88 Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 09:56:18 -0600 Subject: [PATCH 3/7] Adding unit tests for symlinks. --- tests/test_integration.py | 24 ++++++++++++++++++--- tests/toctreeexample/example/example_2 | 1 + tests/toctreeexample/example_2/example_2.py | 9 ++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 120000 tests/toctreeexample/example/example_2 create mode 100644 tests/toctreeexample/example_2/example_2.py diff --git a/tests/test_integration.py b/tests/test_integration.py index 413cb279..a71df1a9 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -27,11 +27,14 @@ def sphinx_build(test_dir, confoverrides=None): class LanguageIntegrationTests: - def _run_test(self, test_dir, test_file, test_string): - with sphinx_build(test_dir): + def _run_test(self, test_dir, test_file, test_string, confoverrides={}, test_missing:bool=False): + with sphinx_build(test_dir, confoverrides=confoverrides): with open(test_file, encoding="utf8") as fin: text = fin.read().strip() - assert test_string in text + if test_missing: + assert test_string not in text + else: + assert test_string in text class TestIntegration(LanguageIntegrationTests): @@ -54,3 +57,18 @@ def test_toctree_domain_insertion(self): self._run_test( "toctreeexample", "_build/text/index.txt", '* "example_function()"' ) + + def test_symlink(self): + """ + Test that the example_function_2 gets added to the TOC Tree when running with symlinks + and that it does not get added when running without them. + """ + # Without symlinks, should not contain example_function_2 + self._run_test( + "toctreeexample", "_build/text/index.txt", '* "example_function_2()"', test_missing=True + ) + + # With symlinks, should contain example_function_2 + self._run_test( + "toctreeexample", "_build/text/index.txt", '* "example_function_2()"', confoverrides={"autoapi_follow_symlinks": True} + ) diff --git a/tests/toctreeexample/example/example_2 b/tests/toctreeexample/example/example_2 new file mode 120000 index 00000000..f2aaf432 --- /dev/null +++ b/tests/toctreeexample/example/example_2 @@ -0,0 +1 @@ +../example_2 \ No newline at end of file diff --git a/tests/toctreeexample/example_2/example_2.py b/tests/toctreeexample/example_2/example_2.py new file mode 100644 index 00000000..70704c9d --- /dev/null +++ b/tests/toctreeexample/example_2/example_2.py @@ -0,0 +1,9 @@ +__author__ = "leake" + +import math + + +def example_function_2(x): + """Compute the square of x and return it.""" + return x**2 + From 19c95fefd34a702e8909bc655d70d0ae6e0df139 Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 09:56:54 -0600 Subject: [PATCH 4/7] Formatting with ruff. --- autoapi/_mapper.py | 11 +++++++++-- tests/test_integration.py | 19 ++++++++++++++++--- tests/toctreeexample/example_2/example_2.py | 3 +-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/autoapi/_mapper.py b/autoapi/_mapper.py index 2b322294..0c9a0b66 100644 --- a/autoapi/_mapper.py +++ b/autoapi/_mapper.py @@ -325,7 +325,9 @@ def find_files(patterns, dirs, ignore, follow_symlinks: bool): pattern_regexes.append((pattern, regex)) for _dir in dirs: # iterate autoapi_dirs - for root, subdirectories, filenames in os.walk(_dir, followlinks=follow_symlinks): + for root, subdirectories, filenames in os.walk( + _dir, followlinks=follow_symlinks + ): # skip directories if needed for sub_dir in subdirectories.copy(): # iterate copy as we adapt subdirectories during loop @@ -430,7 +432,12 @@ def _find_files(self, patterns, dirs, ignore): ): dir_root = os.path.abspath(os.path.join(dir_, os.pardir)) - for path in self.find_files(patterns=patterns, dirs=[dir_], ignore=ignore, follow_symlinks=self._follow_symlinks): + for path in self.find_files( + patterns=patterns, + dirs=[dir_], + ignore=ignore, + follow_symlinks=self._follow_symlinks, + ): yield dir_root, path def load(self, patterns, dirs, ignore=None): diff --git a/tests/test_integration.py b/tests/test_integration.py index a71df1a9..902d3dbb 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -27,7 +27,14 @@ def sphinx_build(test_dir, confoverrides=None): class LanguageIntegrationTests: - def _run_test(self, test_dir, test_file, test_string, confoverrides={}, test_missing:bool=False): + def _run_test( + self, + test_dir, + test_file, + test_string, + confoverrides={}, + test_missing: bool = False, + ): with sphinx_build(test_dir, confoverrides=confoverrides): with open(test_file, encoding="utf8") as fin: text = fin.read().strip() @@ -65,10 +72,16 @@ def test_symlink(self): """ # Without symlinks, should not contain example_function_2 self._run_test( - "toctreeexample", "_build/text/index.txt", '* "example_function_2()"', test_missing=True + "toctreeexample", + "_build/text/index.txt", + '* "example_function_2()"', + test_missing=True, ) # With symlinks, should contain example_function_2 self._run_test( - "toctreeexample", "_build/text/index.txt", '* "example_function_2()"', confoverrides={"autoapi_follow_symlinks": True} + "toctreeexample", + "_build/text/index.txt", + '* "example_function_2()"', + confoverrides={"autoapi_follow_symlinks": True}, ) diff --git a/tests/toctreeexample/example_2/example_2.py b/tests/toctreeexample/example_2/example_2.py index 70704c9d..08a3d193 100644 --- a/tests/toctreeexample/example_2/example_2.py +++ b/tests/toctreeexample/example_2/example_2.py @@ -5,5 +5,4 @@ def example_function_2(x): """Compute the square of x and return it.""" - return x**2 - + return x**2 From ccc4e124150d1e7b4ff3aab8ca6175c48265caf8 Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 10:04:50 -0600 Subject: [PATCH 5/7] Adding release note. --- docs/changes/+d379912c.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/+d379912c.feature.rst diff --git a/docs/changes/+d379912c.feature.rst b/docs/changes/+d379912c.feature.rst new file mode 100644 index 00000000..267459da --- /dev/null +++ b/docs/changes/+d379912c.feature.rst @@ -0,0 +1 @@ +Adding `autoapi_follow_symlinks`, which allows api to traverse into symlinked directories when generating the API documentation. From b55a1aaabb895cdc7553908ba237b6367215f7d8 Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 10:10:18 -0600 Subject: [PATCH 6/7] Handling case where match returns None. This was caught by mypy. --- autoapi/_mapper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/autoapi/_mapper.py b/autoapi/_mapper.py index 0c9a0b66..0463fb68 100644 --- a/autoapi/_mapper.py +++ b/autoapi/_mapper.py @@ -345,6 +345,10 @@ def find_files(patterns, dirs, ignore, follow_symlinks: bool): for pattern, pattern_re in pattern_regexes: for filename in fnmatch.filter(filenames, pattern): match = re.match(pattern_re, filename) + if match is None: + raise ValueError( + f'Could not match pattern "{pattern_re}" to filename "{filename}".' + ) norm_name = match.groups() if norm_name in seen: continue From 56eaddb05fe743c39ee07ede36c922a4493b3960 Mon Sep 17 00:00:00 2001 From: leake Date: Tue, 21 Oct 2025 10:11:28 -0600 Subject: [PATCH 7/7] Adding release note for mypy code fix. --- docs/changes/+7eaa921c.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/+7eaa921c.misc.rst diff --git a/docs/changes/+7eaa921c.misc.rst b/docs/changes/+7eaa921c.misc.rst new file mode 100644 index 00000000..85f26cc6 --- /dev/null +++ b/docs/changes/+7eaa921c.misc.rst @@ -0,0 +1 @@ +Handling case where match returns None to fix mypy unit test.