forked from django/django
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgithub_links.py
149 lines (119 loc) · 4.62 KB
/
github_links.py
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}"