|
|
|
|
|
|
|
import glob |
|
import importlib |
|
import inspect |
|
import logging |
|
import os |
|
import re |
|
import sys |
|
from collections.abc import Iterable |
|
from typing import Optional, Union |
|
|
|
|
|
def _transform_changelog(path_in: str, path_out: str) -> None: |
|
"""Adjust changelog titles so not to be duplicated. |
|
|
|
Args: |
|
path_in: input MD file |
|
path_out: output also MD file |
|
|
|
""" |
|
with open(path_in) as fp: |
|
chlog_lines = fp.readlines() |
|
|
|
chlog_ver = "" |
|
for i, ln in enumerate(chlog_lines): |
|
if ln.startswith("## "): |
|
chlog_ver = ln[2:].split("-")[0].strip() |
|
elif ln.startswith("### "): |
|
ln = ln.replace("###", f"### {chlog_ver} -") |
|
chlog_lines[i] = ln |
|
with open(path_out, "w") as fp: |
|
fp.writelines(chlog_lines) |
|
|
|
|
|
def _linkcode_resolve( |
|
domain: str, |
|
info: dict, |
|
github_user: str, |
|
github_repo: str, |
|
main_branch: str = "master", |
|
stable_branch: str = "release/stable", |
|
) -> str: |
|
def find_source() -> tuple[str, int, int]: |
|
|
|
|
|
obj = sys.modules[info["module"]] |
|
for part in info["fullname"].split("."): |
|
obj = getattr(obj, part) |
|
fname = str(inspect.getsourcefile(obj)) |
|
|
|
if any(s in fname for s in ("readthedocs", "rtfd", "checkouts")): |
|
|
|
|
|
path_top = os.path.abspath(os.path.join("..", "..", "..")) |
|
fname = str(os.path.relpath(fname, start=path_top)) |
|
else: |
|
|
|
fname = f"{main_branch}/{os.path.relpath(fname, start=os.path.abspath('..'))}" |
|
source, line_start = inspect.getsourcelines(obj) |
|
return fname, line_start, line_start + len(source) - 1 |
|
|
|
if domain != "py" or not info["module"]: |
|
return "" |
|
try: |
|
filename = "%s#L%d-L%d" % find_source() |
|
except Exception: |
|
filename = info["module"].replace(".", "/") + ".py" |
|
|
|
|
|
|
|
branch = filename.split("/")[0] |
|
|
|
branch = {"latest": main_branch, "stable": stable_branch}.get(branch, branch) |
|
filename = "/".join([branch] + filename.split("/")[1:]) |
|
return f"https://github.com/{github_user}/{github_repo}/blob/{filename}" |
|
|
|
|
|
def _load_pypi_versions(package_name: str) -> list[str]: |
|
"""Load the versions of the package from PyPI. |
|
|
|
>>> _load_pypi_versions("numpy") # doctest: +ELLIPSIS |
|
['0.9.6', '0.9.8', '1.0', ...] |
|
>>> _load_pypi_versions("scikit-learn") # doctest: +ELLIPSIS |
|
['0.9', '0.10', '0.11', '0.12', ...] |
|
|
|
""" |
|
from distutils.version import LooseVersion |
|
|
|
import requests |
|
|
|
url = f"https://pypi.org/pypi/{package_name}/json" |
|
data = requests.get(url, timeout=10).json() |
|
versions = data["releases"].keys() |
|
|
|
versions = {k for k in versions if re.match(r"^\d+(\.\d+)*$", k)} |
|
return sorted(versions, key=LooseVersion) |
|
|
|
|
|
def _update_link_based_imported_package(link: str, pkg_ver: str, version_digits: Optional[int]) -> str: |
|
"""Adjust the linked external docs to be local. |
|
|
|
Args: |
|
link: the source link to be replaced |
|
pkg_ver: the target link to be replaced, if ``{package.version}`` is included it will be replaced accordingly |
|
version_digits: for semantic versioning, how many digits to be considered |
|
|
|
""" |
|
pkg_att = pkg_ver.split(".") |
|
try: |
|
ver = _load_pypi_versions(pkg_att[0])[-1] |
|
except Exception: |
|
|
|
module = importlib.import_module(".".join(pkg_att[:-1])) |
|
|
|
ver = getattr(module, pkg_att[0]) |
|
|
|
ver = ver.split("+")[0] |
|
|
|
ver = ".".join(ver.split(".")[:version_digits]) |
|
|
|
return link.replace(f"{{{pkg_ver}}}", ver) |
|
|
|
|
|
def adjust_linked_external_docs( |
|
source_link: str, |
|
target_link: str, |
|
browse_folder: Union[str, Iterable[str]], |
|
file_extensions: Iterable[str] = (".rst", ".py"), |
|
version_digits: int = 2, |
|
) -> None: |
|
r"""Adjust the linked external docs to be local. |
|
|
|
Args: |
|
source_link: the link to be replaced |
|
target_link: the link to be replaced, if ``{package.version}`` is included it will be replaced accordingly |
|
browse_folder: the location of the browsable folder |
|
file_extensions: what kind of files shall be scanned |
|
version_digits: for semantic versioning, how many digits to be considered |
|
|
|
Examples: |
|
>>> adjust_linked_external_docs( |
|
... "https://numpy.org/doc/stable/", |
|
... "https://numpy.org/doc/{numpy.__version__}/", |
|
... "docs/source", |
|
... ) |
|
|
|
""" |
|
list_files = [] |
|
if isinstance(browse_folder, str): |
|
browse_folder = [browse_folder] |
|
for folder in browse_folder: |
|
for ext in file_extensions: |
|
list_files += glob.glob(os.path.join(folder, "**", f"*{ext}"), recursive=True) |
|
if not list_files: |
|
logging.warning(f'No files were listed in folder "{browse_folder}" and pattern "{file_extensions}"') |
|
return |
|
|
|
|
|
pkg_ver_all = re.findall(r"{(.+)}", target_link) |
|
for pkg_ver in pkg_ver_all: |
|
target_link = _update_link_based_imported_package(target_link, pkg_ver, version_digits) |
|
|
|
|
|
for fpath in set(list_files): |
|
with open(fpath, encoding="UTF-8") as fopen: |
|
lines = fopen.readlines() |
|
found, skip = False, False |
|
for i, ln in enumerate(lines): |
|
|
|
if f"{adjust_linked_external_docs.__name__}(" in ln: |
|
skip = True |
|
if not skip and source_link in ln: |
|
|
|
lines[i] = ln.replace(source_link, target_link) |
|
|
|
found = True |
|
if skip and ")" in ln: |
|
skip = False |
|
if not found: |
|
continue |
|
logging.debug(f'links adjusting in {fpath}: "{source_link}" -> "{target_link}"') |
|
with open(fpath, "w", encoding="UTF-8") as fw: |
|
fw.writelines(lines) |
|
|