Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/source/_linkcode.py
Original file line number Diff line number Diff line change
@@ -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}"
)
48 changes: 8 additions & 40 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----------------------------------------------------

Expand All @@ -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
Expand Down Expand Up @@ -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)
128 changes: 128 additions & 0 deletions tests/unit/test_doc_linkcode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/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
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"
)
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()
Loading