Source code for pyquickhelper.helpgen.sphinx_main_helper

# -*- coding: utf-8 -*-
"""
Contains helpers for the main function :func:`generate_help_sphinx <pyquickhelper.helpgen.sphinx_main.generate_help_sphinx>`.



:githublink:`%|py|7`
"""
import os
import sys
import datetime
import shutil
import subprocess
import re

from ..loghelper import run_cmd, RunCmdException, fLOG
from ..loghelper.run_cmd import parse_exception_message
from ..loghelper.pyrepo_helper import SourceRepository
from ..pandashelper.tblformat import df2rst
from ..filehelper import explore_folder_iterfile
from .utils_sphinx_doc_helpers import HelpGenException
from .post_process import post_process_latex_output
from .process_notebooks import find_pdflatex


template_examples = """

List of programs
++++++++++++++++

.. toctree::
   :maxdepth: 2

.. autosummary:: __init__.py
   :toctree: %s/
   :template: modules.rst

Another list
++++++++++++

"""


[docs]def setup_environment_for_help(fLOG=fLOG): """ Modifies environment variables to be able to use external tools such as :epkg:`Inkscape`. :githublink:`%|py|46` """ if sys.platform.startswith("win"): prog = os.environ["ProgramFiles"] inkscape = os.path.join(prog, "Inkscape") if not os.path.exists(inkscape): raise FileNotFoundError( "Inkscape is not installed, expected at: {0}".format(inkscape)) path = os.environ["PATH"] if inkscape not in path: fLOG("SETUP: add path to %path%", inkscape) os.environ["PATH"] = path + ";" + inkscape else: pass
[docs]def get_executables_path(): """ Returns the paths to :epkg:`Python`, :epkg:`Python` Scripts. :return: a list of paths :githublink:`%|py|67` """ res = [os.path.split(sys.executable)[0]] res.extend([os.path.join(res[-1], "Scripts"), os.path.join(res[-1], "bin")]) return res
[docs]def my_date_conversion(sdate): """ Converts a date into a datetime. :param sdate: string :return: date :githublink:`%|py|80` """ first = sdate.split(" ")[0] trois = first.replace(".", "-").replace("/", "-").split("-") return datetime.datetime(int(trois[0]), int(trois[1]), int(trois[2]))
[docs]def produce_code_graph_changes(df): """ Returns the code for a graph which counts the number of changes per week over the last year. :param df: dataframe (has a column date with format ``YYYY-MM-DD``) :return: graph The call to :epkg:`datetime.datetime.strptime` introduced exceptions:: File "<frozen importlib._bootstrap>", line 2212, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 321, in _call_with_frames_removed File "<frozen importlib._bootstrap>", line 2254, in _gcd_import File "<frozen importlib._bootstrap>", line 2237, in _find_and_load File "<frozen importlib._bootstrap>", line 2224, in _find_and_load_unlocked when generating the documentation for another project. The reason is still unclear. It was replaced by a custom function. :githublink:`%|py|105` """ def year_week(x): dt = datetime.datetime(x.year, x.month, x.day) return dt.isocalendar()[:2] def to_str(x): year, week = year_week(x) return "%d-w%02d" % (year, week) df = df.copy() df["dt"] = df.apply(lambda r: my_date_conversion(r["date"]), axis=1) df = df[["dt"]] now = datetime.datetime.now() last = now - datetime.timedelta(365) df = df[df.dt >= last] df["week"] = df['dt'].apply(to_str) df["commits"] = 1 val = [] for alldays in range(0, 365): a = now - datetime.timedelta(alldays) val.append({"dt": a, "week": to_str(a), "commits": 0}) # we move pandas here because it imports matplotlib # which is not always wise when you need to modify the backend import pandas df = pandas.concat([df, pandas.DataFrame(val)], sort=True) gr = df[["week", "commits"]].groupby("week", as_index=False).sum() xl = list(gr["week"]) x = list(range(len(xl))) y = list(gr["commits"]) typstr = str code = """ import matplotlib.pyplot as plt x = __X__ y = __Y__ xl = __XL__ plt.close('all') plt.style.use('ggplot') fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 4)) ax.bar(x, y) tig = ax.get_xticks() labs = [] for t in tig: if t in x: labs.append(xl[x.index(t)]) else: labs.append("") ax.set_xticklabels(labs) ax.grid(True) ax.set_title("commits") plt.show() """.replace(" ", "") \ .replace("__X__", typstr(x)) \ .replace("__XL__", typstr(xl)) \ .replace("__Y__", typstr(y)) return code
[docs]def generate_changes_repo(chan, source, exception_if_empty=True, filter_commit=lambda c: c.strip() != "documentation", fLOG=fLOG, modify_commit=None): """ Generates a :epkg:`RST` tables containing the changes stored by a :epkg:`SVN` or :epkg:`GIT` repository, the outcome is stored in a file. The log comment must start with ``*`` to be taken into account. :param chan: filename to write (or None if you don't need to) :param source: source folder to get changes for :param exception_if_empty: raises an exception if empty :param filter_commit: function which accepts a commit to show on the documentation (based on the comment) :param fLOG: logging function :param modify_commit: function which rewrite the commit text (see below) :return: string (rst tables with the changes) :epkg:`pandas` is not imported in the function itself but at the beginning of the module. It seemed to cause soe weird exceptions when generating the documentation for another module:: File "<frozen importlib._bootstrap>", line 2212, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 321, in _call_with_frames_removed File "<frozen importlib._bootstrap>", line 2254, in _gcd_import File "<frozen importlib._bootstrap>", line 2237, in _find_and_load File "<frozen importlib._bootstrap>", line 2224, in _find_and_load_unlocked Doing that helps. The cause still remains obscure. If not None, function *modify_commit* is called the following way (see below). *nbch* is the commit number. *date* can be returned as a datetime or a string. :: nbch, date, author, comment = modify_commit(nbch, date, author, comment) :githublink:`%|py|201` """ # builds the changes files try: src = SourceRepository(commandline=True) logs = src.log(path=source) except Exception as eee: if exception_if_empty: fLOG("[sphinxerror]-9 unable to retrieve log from " + source) raise HelpGenException( "unable to retrieve log in " + source + "\n" + str(eee)) from eee logs = [("none", 0, datetime.datetime.now(), "-")] fLOG("[sphinxerror]-8", eee) if len(logs) == 0: fLOG("[sphinxerror]-7 unable to retrieve log from " + source) if exception_if_empty: raise HelpGenException("retrieved logs are empty in " + source) else: fLOG("info, retrieved ", len(logs), " commits") rows = [] rows.append( """\n.. _l-changes:\n\n\nChanges\n=======\n\n__CODEGRAPH__\n\nList of recent changes:\n""") typstr = str values = [] for i, row in enumerate(logs): n = len(logs) - i author, nbch, date, comment = row[:4] last = row[-1] if last.startswith("http"): nbch = "`%s <%s>`_" % (typstr(nbch), last) if filter_commit(comment): if modify_commit is not None: nbch, date, author, comment = modify_commit( nbch, date, author, comment) if isinstance(date, datetime.datetime): ds = "%04d-%02d-%02d" % (date.year, date.month, date.day) else: ds = date if isinstance(nbch, int): values.append( ["%d" % n, "%04d" % nbch, "%s" % ds, author, comment.strip("*")]) else: values.append( ["%d" % n, "%s" % nbch, "%s" % ds, author, comment.strip("*")]) if len(values) == 0 and exception_if_empty: raise HelpGenException( "Logs were not empty but there was no comment starting with '*' from '{0}'\n".format(source) + "\n".join([typstr(_) for _ in logs])) if len(values) > 0: import pandas tbl = pandas.DataFrame( columns=["#", "change number", "date", "author", "comment"], data=values) rows.append( "\n\n" + df2rst(tbl, list_table=True) + "\n\n") final = "\n".join(rows) if len(values) > 0: code = produce_code_graph_changes(tbl) code = code.split("\n") code = [" " + _ for _ in code] code = "\n".join(code) code = ".. plot::\n" + code + "\n" final = final.replace("__CODEGRAPH__", code) if chan is not None: with open(chan, "w", encoding="utf8") as f: f.write(final) return final
[docs]def compile_latex_output_final(root, latex_path, doall, afile=None, latex_book=False, fLOG=fLOG, custom_latex_processing=None, remove_unicode=False): """ Compiles the :epkg:`latex` documents. :param root: root :param latex_path: path to the compiler :param doall: do more transformation of the latex file before compiling it :param afile: process a specific file :param latex_book: do some customized transformation for a book :param fLOG: logging function :param custom_latex_processing: function which does some post processing of the full latex file :param remove_unicode: remove unicode characters before compiling it .. faqreq: :title: The PDF is corrupted, SVG are not there :epkg:`SVG` graphs are not well processed by the latex compilation. It usually goes through the following instruction: :: \\sphinxincludegraphics{{seance4_projection_population_correction_51_0}.svg} And produces the following error: :: ! LaTeX Error: Unknown graphics extension: .svg. This function does not stop if the latex compilation but if the PDF is corrupted, the log should be checked to see the errors. :githublink:`%|py|310` """ latex_exe = find_pdflatex(latex_path) processed = 0 tried = [] for subfolder in ['latex', 'elatex']: build = os.path.join(root, "_doc", "sphinxdoc", "build", subfolder) if not os.path.exists(build): build = root tried.append(build) for tex in os.listdir(build): if tex.endswith(".tex") and (afile is None or afile in tex): processed += 1 file = os.path.join(build, tex) if doall: # -interaction=batchmode c = '"{0}" "{1}" -max-print-line=900 -buf-size=10000000 -output-directory="{2}"'.format( latex_exe, file, build) else: c = '"{0}" "{1}" -max-print-line=900 -buf-size=10000000 -interaction=nonstopmode -output-directory="{2}"'.format( latex_exe, file, build) fLOG("[compile_latex_output_final] LATEX compilation (c)", c) post_process_latex_output(file, doall, latex_book=latex_book, fLOG=fLOG, custom_latex_processing=custom_latex_processing, remove_unicode=remove_unicode) if sys.platform.startswith("win"): change_path = None else: # On Linux the parameter --output-directory is sometimes ignored. # And it only works from the current directory. change_path = os.path.split(file)[0] try: out, err = run_cmd(c, wait=True, log_error=False, catch_exit=True, communicate=False, tell_if_no_output=120, fLOG=fLOG, prefix_log="[latex] ", change_path=change_path) except Exception as e: # An exception is raised when the return code is an error. We # check that PDF file was written. out, err = parse_exception_message(e) if err is not None and len(err) == 0 and out is not None and "Output written" in out: # The output was produced. We ignore the return code. fLOG("WARNINGS: Latex compilation had warnings:", c) else: raise OSError( "Unable to execute\n{0}".format(c)) from e if len(err) > 0 and "Output written on " not in out: raise HelpGenException( "CMD:\n{0}\n[sphinxerror]-6\n{1}\n---OUT:---\n{2}".format(c, err, out)) # second compilation fLOG("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") fLOG("~~~~ LATEX compilation (d)", c) fLOG("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") try: out, err = run_cmd( c, wait=True, log_error=False, communicate=False, fLOG=fLOG, tell_if_no_output=600, prefix_log="[latex] ", change_path=change_path) except (subprocess.CalledProcessError, RunCmdException): fLOG("[sphinxerror]-5 LATEX ERROR: check the logs") err = "" out = "" fLOG("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") fLOG("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") if len(err) > 0 and "Output written on " not in out: raise HelpGenException(err) if processed == 0: raise FileNotFoundError("Unable to find any latex file in folders\n{0}".format( "\n".join(tried)))
[docs]def replace_placeholder_by_recent_blogpost(all_tocs, plist, placeholder, nb_post=5, fLOG=fLOG): """ Replaces a place holder by a list of blog post. :param all_tocs: list of files to look into :param plist: list of blog post :param placeholder: place holder to replace :param nb_post: number of blog post to display :param fLOG: logging function :githublink:`%|py|388` """ def make_link(post): name = os.path.splitext(os.path.split(post.FileName)[-1])[0] s = """<a href="{{ pathto('',1) }}/blog/%s/%s.html">%s - %s</a>""" % ( post.Year, name, post.Date, post.Title) return s end = min(nb_post, len(plist)) for toc in all_tocs: with open(toc, "r", encoding="utf8") as f: content = f.read() if placeholder in content: fLOG(" *** update", toc) links = [make_link(post) for post in plist[:end]] content = content.replace(placeholder, "\n<br />".join(links)) with open(toc, "w", encoding="utf8") as f: f.write(content)
_pattern_images = ".*(([.]png)|([.]gif])|([.]jpeg])|([.]jpg])|([.]svg]))$"
[docs]def enumerate_copy_images_for_slides(src, dest, pattern=_pattern_images): """ Copies images, initial intent was for slides, once converted into html, link to images are relative to the folder which contains them, we copy the images from ``_images`` to ``_downloads``. :param src: sources :param dest: destination :param pattern: see :func:`explore_folder_iterfile <pyquickhelper.filehelper.synchelper.explore_folder_iterfile>` :return: enumerator of copied files :githublink:`%|py|421` """ iter = explore_folder_iterfile(src, pattern=pattern) for img in iter: d = os.path.join(dest, os.path.split(img)[-1]) if os.path.exists(d): os.remove(d) shutil.copy(img, dest) yield d
[docs]def format_history(src, dest, format="basic"): """ Formats history based on module `releases <https://github.com/bitprophet/releases>`_. :param src: source history (file) :param dest: destination (file) .. versionchanged:: 1.7 Parameter *format* was added. :epkg:`Sphinx` extension *release* is no longer used but the formatting is still available. :githublink:`%|py|442` """ with open(src, "r", encoding="utf-8") as f: lines = f.readlines() new_lines = [] if format == "release": tag = None for i in range(0, len(lines)): line = lines[i].rstrip("\r\t\n ") if line.startswith("===") and i > 0: rel = lines[i - 1].rstrip("\r\t\n ") if "." in rel: del new_lines[-1] res = "* :release:`{0}`".format(rel) res = res.replace("(", "<").replace(")", ">") if new_lines[-1].startswith("==="): new_lines.append("") new_lines.append(res) tag = None else: new_lines.append(line) elif len(line) > 0: if line.startswith("**"): ll = line.lower().strip("*") if ll in ('bug', 'bugfix', 'bugfixes'): tag = "bug" elif ll in ('features', 'feature'): tag = "feature" elif ll in ('support', 'support'): tag = "support" else: raise ValueError( "Line {0}, unable to infer tag from '{1}'".format(i, line)) else: nline = line.lstrip("* ") if nline.startswith("`"): if tag is None: tag = 'issue' res = "* :{0}:{1}".format(tag, nline) if new_lines[-1].startswith("==="): new_lines.append("") new_lines.append(res) else: new_lines.append(line) if line.startswith(".. _"): new_lines.append("") elif format == "basic": reg = re.compile("(.*?)`([0-9]+)`:(.*?)[(]([-0-9]{10})[)]") for line in lines: match = reg.search(line) if match: gr = match.groups() new_line = "{0}:issue:`{1}`:{2}({3})".format(*gr) new_lines.append(new_line) else: new_lines.append(line.strip("\n\r")) else: raise ValueError("Unexpected value for format '{0}'".format(format)) with open(dest, "w", encoding="utf-8") as f: f.write("\n".join(new_lines))