From 7df8094d243f00f716a7186d8581de6a1da84cac Mon Sep 17 00:00:00 2001 From: Qinyi Ding Date: Fri, 2 Oct 2026 21:11:53 -0700 Subject: [PATCH 1/3] DOC-5942: Make API source links independent of the build directory Resolve source paths relative to the repository and test decorated methods and properties without importing Sphinx configuration. Generated with [Snowflake CoCo](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-authored-by: Snowflake CoCo --- docs/source/_linkcode.py | 41 +++++++++++ docs/source/conf.py | 48 +++---------- tests/unit/test_doc_linkcode.py | 124 ++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 40 deletions(-) create mode 100644 docs/source/_linkcode.py create mode 100644 tests/unit/test_doc_linkcode.py diff --git a/docs/source/_linkcode.py b/docs/source/_linkcode.py new file mode 100644 index 0000000000..43f49aeacd --- /dev/null +++ b/docs/source/_linkcode.py @@ -0,0 +1,41 @@ +"""Resolve API source links without importing Sphinx or the Snowpark runtime.""" + +import inspect +import sys +from pathlib import Path + + +def resolve_linkcode(domain, info, release, repository_root): + if domain != "py" or not info.get("module") or not info.get("fullname"): + return None + + obj = sys.modules.get(info["module"]) + if obj is None: + return None + + try: + for part in info["fullname"].split("."): + obj = getattr(obj, part) + if isinstance(obj, property): + obj = obj.fget + obj = inspect.unwrap(obj) + filename = inspect.getsourcefile(obj) + if filename is None: + return None + # External dependencies have no source in the Snowpark repository. + source_path = ( + Path(filename).resolve().relative_to(Path(repository_root).resolve()) + ) + except (AttributeError, OSError, TypeError, ValueError): + return None + + try: + source, first_line = inspect.getsourcelines(obj) + linespec = f"#L{first_line}-L{first_line + len(source) - 1}" + except (OSError, TypeError): + linespec = "" + + return ( + "https://github.com/snowflakedb/snowpark-python/blob/" + f"v{release}/{source_path.as_posix()}{linespec}" + ) diff --git a/docs/source/conf.py b/docs/source/conf.py index 983d2136d8..8d7f80e0e8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,6 +13,12 @@ import os import sys +DOCS_SOURCE_DIR = os.path.dirname(os.path.abspath(__file__)) +REPOSITORY_ROOT = os.path.abspath(os.path.join(DOCS_SOURCE_DIR, "../..")) +sys.path.insert(0, DOCS_SOURCE_DIR) + +from _linkcode import resolve_linkcode + # -- Project information ----------------------------------------------------- @@ -21,7 +27,7 @@ author = "Snowflake Inc." # The full version, including alpha/beta/rc tags -SRC_DIR = "../../src" +SRC_DIR = os.path.join(REPOSITORY_ROOT, "src") sys.path.insert(0, os.path.abspath(SRC_DIR)) SNOWPARK_SRC_DIR = os.path.join(SRC_DIR, "snowflake", "snowpark") VERSION = (1, 1, 1, None) # Default, needed so code will compile @@ -333,42 +339,4 @@ def setup(app): # Construct URL to the corresponding section in the snowpark-python repo def linkcode_resolve(domain, info): - import inspect - - if domain != "py": - return None - - mod_name = info["module"] - full_name = info["fullname"] - - obj = sys.modules.get(mod_name) - if obj is None: - return None - - for part in full_name.split("."): - try: - obj = getattr(obj, part) - except AttributeError: - return None - - try: - if isinstance(obj, property): - fn = inspect.getsourcefile(inspect.unwrap(obj.fget)) - else: - fn = inspect.getsourcefile(inspect.unwrap(obj)) - except TypeError as e: - return None - - try: - if isinstance(obj, property): - source, lineno = inspect.getsourcelines(obj.fget) - else: - source, lineno = inspect.getsourcelines(obj) - linespec = f"#L{lineno}-L{lineno + len(source) - 1}" - except TypeError: - linespec = "" - return ( - f"https://github.com/snowflakedb/snowpark-python/blob/" - f"v{release}/{os.path.relpath(fn)}{linespec}" - ) - + return resolve_linkcode(domain, info, release, REPOSITORY_ROOT) diff --git a/tests/unit/test_doc_linkcode.py b/tests/unit/test_doc_linkcode.py new file mode 100644 index 0000000000..bf8a626efa --- /dev/null +++ b/tests/unit/test_doc_linkcode.py @@ -0,0 +1,124 @@ +"""Source-link tests that don't import Sphinx or its configuration.""" + +import functools +import importlib.util +import inspect +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +SPEC = importlib.util.spec_from_file_location( + "_linkcode", REPOSITORY_ROOT / "docs/source/_linkcode.py" +) +LINKCODE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(LINKCODE) + + +def decorated(function): + @functools.wraps(function) + def wrapper(*args, **kwargs): + return function(*args, **kwargs) + + return wrapper + + +class Example: + @decorated + def method(self): + return "example" + + @property + @decorated + def value(self): + return "example" + + +class LinkcodeTests(unittest.TestCase): + def resolve(self, name, **kwargs): + return LINKCODE.resolve_linkcode( + "py", + {"module": __name__, "fullname": name}, + "1.55.0", + kwargs.get("repository_root", REPOSITORY_ROOT), + ) + + def expected(self, obj): + source, line = inspect.getsourcelines(inspect.unwrap(obj)) + return ( + "https://github.com/snowflakedb/snowpark-python/blob/" + f"v1.55.0/tests/unit/test_doc_linkcode.py#L{line}-L{line + len(source) - 1}" + ) + + def test_decorated_method(self): + self.assertEqual(self.resolve("Example.method"), self.expected(Example.method)) + + def test_decorated_property(self): + self.assertEqual( + self.resolve("Example.value"), self.expected(Example.value.fget) + ) + + def test_independent_of_working_directory(self): + original = Path.cwd() + try: + with tempfile.TemporaryDirectory() as directory: + for cwd in ( + REPOSITORY_ROOT, + REPOSITORY_ROOT / "docs/source", + directory, + ): + with self.subTest(cwd=str(cwd)): + os.chdir(cwd) + self.assertEqual( + self.resolve("Example.method"), + self.expected(Example.method), + ) + finally: + os.chdir(original) + + def test_missing_object(self): + self.assertIsNone(self.resolve("Example.missing")) + + def test_unsupported_domain_and_missing_metadata(self): + for domain, info in ( + ("js", {"module": __name__, "fullname": "Example"}), + ("py", {}), + ("py", {"module": "not_a_loaded_module", "fullname": "Example"}), + ): + with self.subTest(domain=domain, info=info): + self.assertIsNone( + LINKCODE.resolve_linkcode(domain, info, "1.55.0", REPOSITORY_ROOT) + ) + + def test_builtin(self): + self.assertIsNone( + LINKCODE.resolve_linkcode( + "py", + {"module": "builtins", "fullname": "len"}, + "1.55.0", + REPOSITORY_ROOT, + ) + ) + + def test_source_outside_repository(self): + with tempfile.TemporaryDirectory() as directory: + self.assertIsNone(self.resolve("Example.method", repository_root=directory)) + + def test_missing_source_file(self): + with patch.object(LINKCODE.inspect, "getsourcefile", return_value=None): + self.assertIsNone(self.resolve("Example.method")) + + def test_missing_source_lines(self): + with patch.object(LINKCODE.inspect, "getsourcelines", side_effect=OSError): + self.assertEqual( + self.resolve("Example.method"), + "https://github.com/snowflakedb/snowpark-python/blob/" + "v1.55.0/tests/unit/test_doc_linkcode.py", + ) + + +if __name__ == "__main__": + unittest.main() From d5f46246bab72bbf228262ead2a7b377096032c9 Mon Sep 17 00:00:00 2001 From: Qinyi Ding Date: Tue, 6 Oct 2026 17:05:52 -0700 Subject: [PATCH 2/3] DOC-5942: Align source-link regression tests with repository lint Generated with [Snowflake CoCo](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-authored-by: Snowflake CoCo --- tests/unit/test_doc_linkcode.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_doc_linkcode.py b/tests/unit/test_doc_linkcode.py index bf8a626efa..8832e877c7 100644 --- a/tests/unit/test_doc_linkcode.py +++ b/tests/unit/test_doc_linkcode.py @@ -1,15 +1,19 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved. +# + """Source-link tests that don't import Sphinx or its configuration.""" import functools import importlib.util import inspect import os -from pathlib import Path import tempfile import unittest +from pathlib import Path from unittest.mock import patch - REPOSITORY_ROOT = Path(__file__).resolve().parents[2] SPEC = importlib.util.spec_from_file_location( "_linkcode", REPOSITORY_ROOT / "docs/source/_linkcode.py" From f632e019f57273533b2c617231eb9e7d02ff78f0 Mon Sep 17 00:00:00 2001 From: Qinyi Ding Date: Thu, 8 Oct 2026 21:11:53 -0700 Subject: [PATCH 3/3] DOC-5942: Restore working directory before temporary directory cleanup Avoid Windows sharing violations in the source-link regression test by leaving the temporary directory before removing it, including when assertions fail. Generated with [Snowflake CoCo](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-authored-by: Snowflake CoCo --- tests/unit/test_doc_linkcode.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_doc_linkcode.py b/tests/unit/test_doc_linkcode.py index 8832e877c7..b11e1b428f 100644 --- a/tests/unit/test_doc_linkcode.py +++ b/tests/unit/test_doc_linkcode.py @@ -67,8 +67,8 @@ def test_decorated_property(self): def test_independent_of_working_directory(self): original = Path.cwd() - try: - with tempfile.TemporaryDirectory() as directory: + with tempfile.TemporaryDirectory() as directory: + try: for cwd in ( REPOSITORY_ROOT, REPOSITORY_ROOT / "docs/source", @@ -80,8 +80,9 @@ def test_independent_of_working_directory(self): self.resolve("Example.method"), self.expected(Example.method), ) - finally: - os.chdir(original) + finally: + # Windows cannot remove a directory while it is the current directory. + os.chdir(original) def test_missing_object(self): self.assertIsNone(self.resolve("Example.missing"))