From a103989fd80fd0e2c36568a713a80ae35fb1ff12 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 21 Apr 2026 12:20:31 -0400 Subject: [PATCH 1/5] Support auto_include and no_auto_exclude config --- great_docs/config.py | 12 ++++++++++++ great_docs/core.py | 26 +++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/great_docs/config.py b/great_docs/config.py index 306da2ba..c1dfaea9 100644 --- a/great_docs/config.py +++ b/great_docs/config.py @@ -19,6 +19,8 @@ "jupyter": "python3", # Default kernel for Quarto computations # API discovery settings "exclude": [], + "auto_include": [], # Names to force-include even if they match AUTO_EXCLUDE + "no_auto_exclude": False, # Bypass the built-in AUTO_EXCLUDE list entirely # GitHub integration "repo": None, # GitHub repository URL override (e.g., "https://github.com/owner/repo") "github_style": "widget", # "widget" (shows stars) or "icon" @@ -454,6 +456,16 @@ def exclude(self) -> list[str]: """Get the list of items to exclude.""" return self.get("exclude", []) + @property + def auto_include(self) -> list[str]: + """Get names to force-include even if they match AUTO_EXCLUDE.""" + return self.get("auto_include", []) + + @property + def no_auto_exclude(self) -> bool: + """Check if the built-in AUTO_EXCLUDE list should be bypassed.""" + return self.get("no_auto_exclude", False) + @property def repo(self) -> str | None: """Get the GitHub repository URL override.""" diff --git a/great_docs/core.py b/great_docs/core.py index 59995dc2..d76a749f 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -1335,6 +1335,8 @@ def _get_package_metadata(self) -> dict: # Map config properties to metadata dict for backward compatibility metadata["rich_authors"] = self._config.authors metadata["exclude"] = self._config.exclude + metadata["auto_include"] = self._config.auto_include + metadata["no_auto_exclude"] = self._config.no_auto_exclude # Source link configuration metadata["source_link_enabled"] = self._config.source_enabled @@ -5487,9 +5489,27 @@ def _discover_package_exports(self, package_name: str) -> list | None: # Get config from great-docs.yml metadata = self._get_package_metadata() config_exclude = set(metadata.get("exclude", [])) + auto_include = set(metadata.get("auto_include", [])) + no_auto_exclude = metadata.get("no_auto_exclude", False) + + # Determine effective auto-exclude set + if no_auto_exclude: + effective_auto_exclude: set[str] = set() + print("Auto-exclude list bypassed (no_auto_exclude: true)") + else: + effective_auto_exclude = self.AUTO_EXCLUDE - auto_include + if auto_include: + forced = auto_include & self.AUTO_EXCLUDE + if forced: + print( + f"Force-including {len(forced)} auto-excluded name(s): " + f"{', '.join(sorted(forced))}" + ) # Apply auto-exclusions - auto_excluded_found = [name for name in public_members if name in self.AUTO_EXCLUDE] + auto_excluded_found = [ + name for name in public_members if name in effective_auto_exclude + ] if auto_excluded_found: print( f"Auto-excluding {len(auto_excluded_found)} item(s): " @@ -5497,7 +5517,7 @@ def _discover_package_exports(self, package_name: str) -> list | None: ) # Combine all exclusions (auto + user-specified) - all_exclude = self.AUTO_EXCLUDE | config_exclude + all_exclude = effective_auto_exclude | config_exclude # Filter out excluded items filtered = [name for name in public_members if name not in all_exclude] @@ -5507,7 +5527,7 @@ def _discover_package_exports(self, package_name: str) -> list | None: user_excluded_found = [ name for name in public_members - if name in config_exclude and name not in self.AUTO_EXCLUDE + if name in config_exclude and name not in effective_auto_exclude ] if user_excluded_found: print( From 2df9046230753cc1eae6e4f850f3894582cdd409 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 21 Apr 2026 12:21:36 -0400 Subject: [PATCH 2/5] Document auto_include and no_auto_exclude options --- user_guide/03-configuration.qmd | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/user_guide/03-configuration.qmd b/user_guide/03-configuration.qmd index ce79bfa6..3fe7f1b3 100644 --- a/user_guide/03-configuration.qmd +++ b/user_guide/03-configuration.qmd @@ -93,6 +93,28 @@ Great Docs automatically excludes these common internal names during discovery: | Standard library | `PackageNotFoundError`, `typing`, `annotations`, `TYPE_CHECKING` | | Logging | `logger`, `log`, `logging` | +### Force-Including Auto-Excluded Names + +Some packages intentionally export names that match the auto-exclude list (e.g., a `config` or `logging` module that is part of the public API). Use `auto_include` to force specific names back into discovery: + +```{.yaml filename="great-docs.yml"} +auto_include: + - config + - logging +``` + +Names listed in `auto_include` are removed from the auto-exclude filter while all other auto-excluded names remain filtered as usual. + +### Disabling Auto-Exclude Entirely + +If the auto-exclude list doesn't suit your package at all, you can bypass it completely: + +```{.yaml filename="great-docs.yml"} +no_auto_exclude: true +``` + +With this setting, no names are automatically excluded. You can still use `exclude` to manually remove specific items. + ## Docstring Parser Different projects use different docstring conventions, so Great Docs automatically detects your docstring style during initialization. From 77006fd4dffb0f428e8b151845ab52947d9dfa1f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 21 Apr 2026 12:22:03 -0400 Subject: [PATCH 3/5] Add tests for package export discovery options --- tests/test_great_docs.py | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/test_great_docs.py b/tests/test_great_docs.py index 138f580f..a98f1e5b 100644 --- a/tests/test_great_docs.py +++ b/tests/test_great_docs.py @@ -24082,6 +24082,112 @@ def test_discover_package_exports_load_failure(): assert result is None +def test_discover_package_exports_auto_include(): + """Test auto_include forces names back through AUTO_EXCLUDE.""" + + with tempfile.TemporaryDirectory() as tmp_dir: + pkg_dir = Path(tmp_dir) / "autoincpkg" + pkg_dir.mkdir() + (pkg_dir / "__init__.py").write_text( + '__all__ = ["MyClass", "config", "logger"]\n' + "class MyClass: pass\n" + "class config: pass\n" + "import logging\n" + "logger = logging.getLogger()\n", + encoding="utf-8", + ) + + # Write a great-docs.yml that force-includes "config" + gd_yml = Path(tmp_dir) / "great-docs.yml" + gd_yml.write_text("auto_include:\n - config\n", encoding="utf-8") + (Path(tmp_dir) / "pyproject.toml").write_text( + '[project]\nname = "autoincpkg"\n', encoding="utf-8" + ) + + sys.path.insert(0, tmp_dir) + try: + docs = GreatDocs(project_path=tmp_dir) + result = docs._discover_package_exports("autoincpkg") + + assert result is not None + assert "MyClass" in result + # "config" should be force-included + assert "config" in result + # "logger" should still be auto-excluded + assert "logger" not in result + finally: + sys.path.remove(tmp_dir) + + +def test_discover_package_exports_no_auto_exclude(): + """Test no_auto_exclude bypasses the entire AUTO_EXCLUDE list.""" + + with tempfile.TemporaryDirectory() as tmp_dir: + pkg_dir = Path(tmp_dir) / "noautoexclpkg" + pkg_dir.mkdir() + (pkg_dir / "__init__.py").write_text( + '__all__ = ["MyClass", "main", "config", "logger"]\n' + "class MyClass: pass\n" + "def main(): pass\n" + "class config: pass\n" + "import logging\n" + "logger = logging.getLogger()\n", + encoding="utf-8", + ) + + # Write a great-docs.yml that disables auto-exclude + gd_yml = Path(tmp_dir) / "great-docs.yml" + gd_yml.write_text("no_auto_exclude: true\n", encoding="utf-8") + (Path(tmp_dir) / "pyproject.toml").write_text( + '[project]\nname = "noautoexclpkg"\n', encoding="utf-8" + ) + + sys.path.insert(0, tmp_dir) + try: + docs = GreatDocs(project_path=tmp_dir) + result = docs._discover_package_exports("noautoexclpkg") + + assert result is not None + # All names should be present since auto-exclude is bypassed + assert "MyClass" in result + assert "main" in result + assert "config" in result + assert "logger" in result + finally: + sys.path.remove(tmp_dir) + + +def test_discover_package_exports_auto_include_no_overlap(): + """Test auto_include with names not in AUTO_EXCLUDE has no effect.""" + + with tempfile.TemporaryDirectory() as tmp_dir: + pkg_dir = Path(tmp_dir) / "nooverlappkg" + pkg_dir.mkdir() + (pkg_dir / "__init__.py").write_text( + '__all__ = ["MyClass", "main"]\nclass MyClass: pass\ndef main(): pass\n', + encoding="utf-8", + ) + + # auto_include names that are NOT in AUTO_EXCLUDE + gd_yml = Path(tmp_dir) / "great-docs.yml" + gd_yml.write_text("auto_include:\n - MyClass\n", encoding="utf-8") + (Path(tmp_dir) / "pyproject.toml").write_text( + '[project]\nname = "nooverlappkg"\n', encoding="utf-8" + ) + + sys.path.insert(0, tmp_dir) + try: + docs = GreatDocs(project_path=tmp_dir) + result = docs._discover_package_exports("nooverlappkg") + + assert result is not None + assert "MyClass" in result + # "main" should still be auto-excluded + assert "main" not in result + finally: + sys.path.remove(tmp_dir) + + def test_extract_all_directives_basic(): """Test _extract_all_directives finds directives in docstrings.""" From 331ace0d036b67de8441c61aa7b1686ad9eeefb1 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 21 Apr 2026 12:22:31 -0400 Subject: [PATCH 4/5] Add auto-include and no-auto-exclude test packages --- test-packages/synthetic/catalog.py | 15 ++ .../synthetic/specs/gdtest_auto_include.py | 157 ++++++++++++++++++ .../synthetic/specs/gdtest_no_auto_exclude.py | 146 ++++++++++++++++ tests/test_gdg_rendered.py | 32 ++++ 4 files changed, 350 insertions(+) create mode 100644 test-packages/synthetic/specs/gdtest_auto_include.py create mode 100644 test-packages/synthetic/specs/gdtest_no_auto_exclude.py diff --git a/test-packages/synthetic/catalog.py b/test-packages/synthetic/catalog.py index 46a6d2b7..54fb73fa 100644 --- a/test-packages/synthetic/catalog.py +++ b/test-packages/synthetic/catalog.py @@ -354,6 +354,9 @@ "gdtest_sec_dir_titles", # 174 # 175: Namespace package with src/ layout and dotted module name "gdtest_namespace_src", # 175 + # 176–177: Auto-include / no-auto-exclude discovery overrides + "gdtest_auto_include", # 176 + "gdtest_no_auto_exclude", # 177 ] @@ -1990,6 +1993,18 @@ "namespace packages in src/ layout via griffe search_paths and " "dotted path resolution in _find_package_init." ), + "gdtest_auto_include": ( + "Module exports names that match AUTO_EXCLUDE (config, logging, main) " + "alongside real API (Widget, process). The auto_include config option " + "forces config and logging back into documentation while main remains " + "excluded. Tests selective override of AUTO_EXCLUDE." + ), + "gdtest_no_auto_exclude": ( + "Module exports names that match AUTO_EXCLUDE (main, config, logger) " + "alongside real API (Adapter, run). The no_auto_exclude config option " + "is set to true, so ALL names pass through — none are automatically " + "excluded. Tests complete bypass of the AUTO_EXCLUDE filter." + ), } diff --git a/test-packages/synthetic/specs/gdtest_auto_include.py b/test-packages/synthetic/specs/gdtest_auto_include.py new file mode 100644 index 00000000..32a662af --- /dev/null +++ b/test-packages/synthetic/specs/gdtest_auto_include.py @@ -0,0 +1,157 @@ +""" +gdtest_auto_include — Force-include names that match AUTO_EXCLUDE. + +Dimensions: A1, B7, C4, D1, E6, F6, G1, H7 +Focus: __all__ includes names like "config", "logging", and "main" that are + in the AUTO_EXCLUDE set. The great-docs.yml ``auto_include`` option + forces "config" and "logging" back into the documentation while "main" + remains excluded. Validates that auto_include selectively overrides + AUTO_EXCLUDE without disabling it entirely. +""" + +SPEC = { + "name": "gdtest_auto_include", + "description": "Force-include AUTO_EXCLUDE names via auto_include config", + "dimensions": ["A1", "B7", "C4", "D1", "E6", "F6", "G1", "H7"], + "pyproject_toml": { + "project": { + "name": "gdtest-auto-include", + "version": "0.1.0", + "description": "A synthetic test package testing auto_include override of AUTO_EXCLUDE", + }, + "build-system": { + "requires": ["setuptools"], + "build-backend": "setuptools.build_meta", + }, + }, + "config": { + "auto_include": ["config", "logging"], + }, + "files": { + "gdtest_auto_include/__init__.py": '''\ + """A test package with auto_include overriding AUTO_EXCLUDE.""" + + __version__ = "0.1.0" + __all__ = ["Widget", "process", "config", "logging", "main"] + + + class Widget: + """ + A public widget class. + + Parameters + ---------- + label + Widget label. + """ + + def __init__(self, label: str): + self.label = label + + def render(self) -> str: + """ + Render the widget. + + Returns + ------- + str + Rendered HTML. + """ + return f"{self.label}" + + + def process(data: str) -> str: + """ + Process input data. + + Parameters + ---------- + data + Raw input data. + + Returns + ------- + str + Processed data. + """ + return data.strip() + + + class config: + """ + Configuration manager for the package. + + This is a real public API class that happens to be named ``config`` + — a name normally in AUTO_EXCLUDE. The ``auto_include`` option + forces it back into documentation. + + Parameters + ---------- + path + Configuration file path. + """ + + def __init__(self, path: str = "config.ini"): + self.path = path + + def load(self) -> dict: + """ + Load configuration from file. + + Returns + ------- + dict + Loaded configuration values. + """ + return {} + + + class logging: + """ + Logging facade for the package. + + This is a real public API class that happens to be named ``logging`` + — a name normally in AUTO_EXCLUDE. The ``auto_include`` option + forces it back into documentation. + + Parameters + ---------- + level + Default log level. + """ + + def __init__(self, level: str = "INFO"): + self.level = level + + def info(self, msg: str) -> None: + """ + Log an informational message. + + Parameters + ---------- + msg + The message to log. + """ + pass + + + def main(): + """CLI entry point — should still be auto-excluded.""" + pass + ''', + "README.md": """\ + # gdtest-auto-include + + A synthetic test package testing auto_include override of AUTO_EXCLUDE. + """, + }, + "expected": { + "detected_name": "gdtest-auto-include", + "detected_module": "gdtest_auto_include", + "detected_parser": "numpy", + "export_names": ["Widget", "process", "config", "logging"], + "auto_excluded": ["main"], + "force_included": ["config", "logging"], + "has_user_guide": False, + }, +} diff --git a/test-packages/synthetic/specs/gdtest_no_auto_exclude.py b/test-packages/synthetic/specs/gdtest_no_auto_exclude.py new file mode 100644 index 00000000..4e764e59 --- /dev/null +++ b/test-packages/synthetic/specs/gdtest_no_auto_exclude.py @@ -0,0 +1,146 @@ +""" +gdtest_no_auto_exclude — Bypass the AUTO_EXCLUDE list entirely. + +Dimensions: A1, B7, C4, D1, E6, F6, G1, H7 +Focus: __all__ includes names like "main", "cli", "config", "utils", "logger" + that are in the AUTO_EXCLUDE set. The great-docs.yml ``no_auto_exclude`` + option is set to true, so ALL names pass through — none are + automatically excluded. Validates that the entire AUTO_EXCLUDE filter + can be disabled. +""" + +SPEC = { + "name": "gdtest_no_auto_exclude", + "description": "Bypass AUTO_EXCLUDE entirely via no_auto_exclude config", + "dimensions": ["A1", "B7", "C4", "D1", "E6", "F6", "G1", "H7"], + "pyproject_toml": { + "project": { + "name": "gdtest-no-auto-exclude", + "version": "0.1.0", + "description": "A synthetic test package testing no_auto_exclude bypass", + }, + "build-system": { + "requires": ["setuptools"], + "build-backend": "setuptools.build_meta", + }, + }, + "config": { + "no_auto_exclude": True, + }, + "files": { + "gdtest_no_auto_exclude/__init__.py": '''\ + """A test package with no_auto_exclude: true.""" + + __version__ = "0.1.0" + __all__ = ["Adapter", "run", "main", "config", "logger"] + + + class Adapter: + """ + A public adapter class. + + Parameters + ---------- + backend + Backend identifier. + """ + + def __init__(self, backend: str): + self.backend = backend + + def connect(self) -> bool: + """ + Connect to the backend. + + Returns + ------- + bool + Whether connection succeeded. + """ + return True + + + def run(data: str) -> str: + """ + Run a processing pipeline. + + Parameters + ---------- + data + Input data string. + + Returns + ------- + str + Processed output. + """ + return data.upper() + + + def main(): + """ + CLI entry point. + + Normally auto-excluded, but present because no_auto_exclude is true. + + Returns + ------- + None + """ + pass + + + class config: + """ + Configuration manager. + + Normally auto-excluded, but present because no_auto_exclude is true. + + Parameters + ---------- + path + Config file path. + """ + + def __init__(self, path: str = "settings.ini"): + self.path = path + + def read(self) -> dict: + """ + Read configuration. + + Returns + ------- + dict + Configuration values. + """ + return {} + + + def logger(): + """ + Create a logger instance. + + Normally auto-excluded, but present because no_auto_exclude is true. + + Returns + ------- + None + """ + pass + ''', + "README.md": """\ + # gdtest-no-auto-exclude + + A synthetic test package testing no_auto_exclude bypass. + """, + }, + "expected": { + "detected_name": "gdtest-no-auto-exclude", + "detected_module": "gdtest_no_auto_exclude", + "detected_parser": "numpy", + "export_names": ["Adapter", "run", "main", "config", "logger"], + "auto_excluded": [], + "has_user_guide": False, + }, +} diff --git a/tests/test_gdg_rendered.py b/tests/test_gdg_rendered.py index 012e5cb0..1e1fba74 100644 --- a/tests/test_gdg_rendered.py +++ b/tests/test_gdg_rendered.py @@ -5962,6 +5962,38 @@ def test_DED_auto_exclude_ref_pages(): assert (ref / "real_func.html").exists(), "real_func page missing" +def test_DED_auto_include_ref_pages(): + """gdtest_auto_include: auto_include forces config/logging through, main still excluded.""" + pkg = "gdtest_auto_include" + if not _has_rendered_site(pkg): + pytest.skip(f"{pkg} not rendered") + + ref = _ref_dir(pkg) + # Real exports present + assert (ref / "Widget.html").exists(), "Widget page missing" + assert (ref / "process.html").exists(), "process page missing" + # Force-included names present (overriding AUTO_EXCLUDE) + assert (ref / "config.html").exists(), "config page missing (should be force-included)" + assert (ref / "logging.html").exists(), "logging page missing (should be force-included)" + # main should still be auto-excluded + assert not (ref / "main.html").exists(), "main page exists but should be auto-excluded" + + +def test_DED_no_auto_exclude_ref_pages(): + """gdtest_no_auto_exclude: no_auto_exclude bypasses filter, all exports present.""" + pkg = "gdtest_no_auto_exclude" + if not _has_rendered_site(pkg): + pytest.skip(f"{pkg} not rendered") + + ref = _ref_dir(pkg) + # All exports present — no auto-exclusion + assert (ref / "Adapter.html").exists(), "Adapter page missing" + assert (ref / "run.html").exists(), "run page missing" + assert (ref / "main.html").exists(), "main page missing (no_auto_exclude should keep it)" + assert (ref / "config.html").exists(), "config page missing (no_auto_exclude should keep it)" + assert (ref / "logger.html").exists(), "logger page missing (no_auto_exclude should keep it)" + + def test_DED_duplicate_all_ref_pages(): """gdtest_duplicate_all: duplicate __all__ entries deduplicated, pages exist.""" pkg = "gdtest_duplicate_all" From d39b6ddb20b74df796868b452d2f43ccae4700d9 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 21 Apr 2026 12:22:58 -0400 Subject: [PATCH 5/5] Document auto-exclude override options --- skills/great-docs/references/config-reference.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skills/great-docs/references/config-reference.md b/skills/great-docs/references/config-reference.md index 37736e1a..f9439355 100644 --- a/skills/great-docs/references/config-reference.md +++ b/skills/great-docs/references/config-reference.md @@ -21,6 +21,8 @@ All keys are optional. Defaults are auto-detected from `pyproject.toml`. | `parser` | `str` | `"numpy"` | Docstring style: `"numpy"`, `"google"`, `"sphinx"` | | `dynamic` | `bool` | `true` | `true`: runtime introspection; `false`: static (griffe) | | `exclude` | `list[str]` | `[]` | Items to hide from API docs | +| `auto_include` | `list[str]` | `[]` | Force-include names that match the auto-exclude list | +| `no_auto_exclude` | `bool` | `false` | Bypass the built-in auto-exclude list entirely | ## GitHub and source links