diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c024b86..fed3f1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,6 @@ jobs: - name: dependencies run: | pip install --upgrade pip wheel - pip install -e .[test,typehints] + pip install .[test,typehints] - name: tests run: pytest --color=yes diff --git a/docs/conf.py b/docs/conf.py index 650081d..e1bf4f9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -import sys from datetime import datetime from importlib.metadata import metadata from pathlib import Path @@ -8,9 +7,6 @@ HERE = Path(__file__).parent -# necessary for rtd_gh_links’ linkcode support -sys.path.insert(0, HERE.parent / "src") - # Clean build env for file in HERE.glob("scanpydoc.*.rst"): file.unlink() @@ -49,6 +45,7 @@ # Generate .rst stubs for modules using autosummary autosummary_generate = True +autosummary_ignore_module_all = False # Don’t add module paths to documented functions’ names add_module_names = False diff --git a/pyproject.toml b/pyproject.toml index 6a617b3..bd62581 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ source = 'vcs' version-file = 'src/scanpydoc/_version.py' [tool.hatch.envs.docs] +python = '3.11' features = ['doc'] [tool.hatch.envs.docs.scripts] build = 'sphinx-build -M html docs docs/_build' diff --git a/src/scanpydoc/elegant_typehints/__init__.py b/src/scanpydoc/elegant_typehints/__init__.py index 49abe5e..fbdf7e7 100644 --- a/src/scanpydoc/elegant_typehints/__init__.py +++ b/src/scanpydoc/elegant_typehints/__init__.py @@ -60,6 +60,9 @@ def x() -> Tuple[int, float]: from .example import example_func +__all__ = ["example_func", "setup"] + + HERE = Path(__file__).parent.resolve() qualname_overrides_default = { @@ -88,9 +91,6 @@ def _init_vars(app: Sphinx, config: Config): config.html_static_path.append(str(HERE / "static")) -example_func.__module__ = "scanpydoc.elegant_typehints" # Make it show up here - - @_setup_sig def setup(app: Sphinx) -> dict[str, Any]: """Patches :mod:`sphinx_autodoc_typehints` for a more elegant display.""" diff --git a/src/scanpydoc/rtd_github_links/__init__.py b/src/scanpydoc/rtd_github_links/__init__.py index c514af0..ab2e06f 100644 --- a/src/scanpydoc/rtd_github_links/__init__.py +++ b/src/scanpydoc/rtd_github_links/__init__.py @@ -8,13 +8,6 @@ .. code:: python - import sys - from pathlib import Path - - HERE = Path(__file__).parent - # make sure modules are import from the right place - sys.path.insert(0, HERE.parent / "src") - extensions = [ "scanpydoc", "sphinx.ext.linkcode", @@ -67,6 +60,7 @@ import inspect import sys +from importlib import import_module from pathlib import Path, PurePosixPath from types import ModuleType from typing import Any @@ -108,14 +102,18 @@ def _get_obj_module(qualname: str) -> tuple[Any, ModuleType]: # retrieve object and find original module name mod = sys.modules[modname] obj = None + del modname for attr_name in attr_path: try: thing = getattr(mod if obj is None else obj, attr_name) - except AttributeError: + except AttributeError as e: if is_dataclass(obj): thing = next(f for f in fields(obj) if f.name == attr_name) else: - raise + try: + thing = import_module(f"{mod.__name__}.{attr_name}") + except ImportError: + raise e from None if isinstance(thing, ModuleType): mod = thing else: @@ -137,12 +135,15 @@ def _get_linenos(obj): return start, start + len(lines) - 1 -def _module_path(module: ModuleType) -> PurePosixPath: - stem = PurePosixPath(*module.__name__.split(".")) - if Path(module.__file__).name == "__init__.py": - return stem / "__init__.py" - else: - return stem.with_suffix(".py") +def _module_path(obj: Any, module: ModuleType) -> PurePosixPath: + """Relative module path to parent directory of toplevel module.""" + try: + file = Path(inspect.getabsfile(obj)) + except TypeError: + file = Path(module.__file__) + offset = -1 if file.name == "__init__.py" else 0 + parts = module.__name__.split(".") + return PurePosixPath(*file.parts[offset - len(parts) :]) def github_url(qualname: str) -> str: @@ -159,7 +160,7 @@ def github_url(qualname: str) -> str: except Exception: print(f"Error in github_url({qualname!r}):", file=sys.stderr) raise - path = rtd_links_prefix / _module_path(module) + path = rtd_links_prefix / _module_path(obj, module) start, end = _get_linenos(obj) fragment = f"#L{start}-L{end}" if start and end else "" return f"{github_base_url}/{path}{fragment}" diff --git a/tests/test_rtd_github_links.py b/tests/test_rtd_github_links.py index 0a5471e..8f4f99d 100644 --- a/tests/test_rtd_github_links.py +++ b/tests/test_rtd_github_links.py @@ -1,4 +1,5 @@ from dataclasses import Field +from importlib import import_module from pathlib import Path, PurePosixPath import pytest @@ -16,17 +17,30 @@ def _env(monkeypatch: MonkeyPatch) -> None: @pytest.fixture(params=[".", "src"]) -def pfx(monkeypatch: MonkeyPatch, _env, request): +def prefix(monkeypatch: MonkeyPatch, _env, request) -> PurePosixPath: pfx = PurePosixPath(request.param) monkeypatch.setattr("scanpydoc.rtd_github_links.rtd_links_prefix", pfx) - return pfx - - -def test_as_function(pfx): - pth = "x" / pfx / "scanpydoc/rtd_github_links/__init__.py" - assert github_url("scanpydoc.rtd_github_links") == str(pth) - s, e = _get_linenos(github_url) - assert github_url("scanpydoc.rtd_github_links.github_url") == f"{pth}#L{s}-L{e}" + return "x" / pfx / "scanpydoc" + + +@pytest.mark.parametrize( + ("module", "name", "obj_path"), + [ + pytest.param( + *("rtd_github_links", "github_url", "rtd_github_links/__init__.py"), + id="basic", + ), + pytest.param( + *("elegant_typehints", "example_func", "elegant_typehints/example.py"), + id="reexport", + ), + ], +) +def test_as_function(prefix, module, name, obj_path): + assert github_url(f"scanpydoc.{module}") == str(prefix / module / "__init__.py") + obj = getattr(import_module(f"scanpydoc.{module}"), name) + s, e = _get_linenos(obj) + assert github_url(f"scanpydoc.{module}.{name}") == f"{prefix}/{obj_path}#L{s}-L{e}" def test_get_obj_module():