mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
15fff62d5d
The docs build process was failing on djangoproject.com since it uses Python 3.8 and the dict union operator was added in Python 3.9.
150 lines
4.6 KiB
Python
150 lines
4.6 KiB
Python
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 "/" explicitely 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}"
|