1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
import ast
import functools
import importlib.util
import pathlib
class CodeLocator(ast.NodeVisitor):
def __init__(self):
super().__init__()
self.current_path = []
self.node_line_numbers = {}
self.import_locations = {}
@classmethod
def from_code(cls, code):
tree = ast.parse(code)
locator = cls()
locator.visit(tree)
return locator
def visit_node(self, node):
self.current_path.append(node.name)
self.node_line_numbers[".".join(self.current_path)] = node.lineno
self.generic_visit(node)
self.current_path.pop()
def visit_FunctionDef(self, node):
self.visit_node(node)
def visit_ClassDef(self, node):
self.visit_node(node)
def visit_ImportFrom(self, node):
for alias in node.names:
if alias.asname:
# Exclude linking aliases (`import x as y`) to avoid confusion
# when clicking a source link to a differently named entity.
continue
if alias.name == "*":
# Resolve wildcard imports.
file = module_name_to_file_path(node.module)
file_contents = file.read_text(encoding="utf-8")
locator = CodeLocator.from_code(file_contents)
self.import_locations.update(locator.import_locations)
self.import_locations.update(
{n: node.module for n in locator.node_line_numbers if "." not in n}
)
else:
self.import_locations[alias.name] = ("." * node.level) + (
node.module or ""
)
@functools.lru_cache(maxsize=1024)
def get_locator(file):
file_contents = file.read_text(encoding="utf-8")
return CodeLocator.from_code(file_contents)
class CodeNotFound(Exception):
pass
def module_name_to_file_path(module_name):
# Avoid importlib machinery as locating a module involves importing its
# parent, which would trigger import side effects.
for suffix in [".py", "/__init__.py"]:
file_path = pathlib.Path(__file__).parents[2] / (
module_name.replace(".", "/") + suffix
)
if file_path.exists():
return file_path
raise CodeNotFound
def get_path_and_line(module, fullname):
path = module_name_to_file_path(module_name=module)
locator = get_locator(path)
lineno = locator.node_line_numbers.get(fullname)
if lineno is not None:
return path, lineno
imported_object = fullname.split(".", maxsplit=1)[0]
try:
imported_path = locator.import_locations[imported_object]
except KeyError:
raise CodeNotFound
# From a statement such as:
# from . import y.z
# - either y.z might be an object in the parent module
# - or y might be a module, and z be an object in y
# also:
# - either the current file is x/__init__.py, and z would be in x.y
# - or the current file is x/a.py, and z would be in x.a.y
if path.name != "__init__.py":
# Look in parent module
module = module.rsplit(".", maxsplit=1)[0]
try:
imported_module = importlib.util.resolve_name(
name=imported_path, package=module
)
except ImportError as error:
raise ImportError(
f"Could not import '{imported_path}' in '{module}'."
) from error
try:
return get_path_and_line(module=imported_module, fullname=fullname)
except CodeNotFound:
if "." not in fullname:
raise
first_element, remainder = fullname.rsplit(".", maxsplit=1)
# Retrying, assuming the first element of the fullname is a module.
return get_path_and_line(
module=f"{imported_module}.{first_element}", fullname=remainder
)
def get_branch(version, next_version):
if version == next_version:
return "main"
else:
return f"stable/{version}.x"
def github_linkcode_resolve(domain, info, *, version, next_version):
if domain != "py":
return None
if not (module := info["module"]):
return None
try:
path, lineno = get_path_and_line(module=module, fullname=info["fullname"])
except CodeNotFound:
return None
branch = get_branch(version=version, next_version=next_version)
relative_path = path.relative_to(pathlib.Path(__file__).parents[2])
# Use "/" explicitly to join the path parts since str(file), on Windows,
# uses the Windows path separator which is incorrect for URLs.
url_path = "/".join(relative_path.parts)
return f"https://github.com/django/django/blob/{branch}/{url_path}#L{lineno}"
|