def replace_emoji(app: Sphinx, exception: Optional[Exception] = None): if exception: return if app.builder.name.lower() != "latex": return output_file = PathPlus( app.builder.outdir) / f"{app.builder.titles[0][1]}.tex" output_content = output_file.read_text() # Documentation summary emoji output_content = output_content.replace(" 🐍 🛠️", '') output_content = output_content.replace('🐍', '') output_content = output_content.replace('🛠', '') output_content = output_content.replace('️', '') # Variation Selector-16 output_content = output_content.replace( '≈', r" $\approx$ ") # coming in sphinx-toolbox 2.12 output_content = output_content.replace( 'μ', r"\textmu ") # fixed in sphinx-toolbox 2.12 output_content = output_content.replace( r"\textmum", r"\textmu m") # fixed in sphinx-toolbox 2.12 output_content = output_content.replace( '\u205f', r"\medspace ") # medium mathematical space # in words.py output_content = output_content.replace(r'A\sphinxhyphen{}Ω', r"A\sphinxhyphen{}\textOmega") output_content = output_content.replace( r'α\sphinxhyphen{}ϖ', r"\textalpha\sphinxhyphen{}\textomega") output_file.write_clean(output_content)
def rewrite_readme(repo_path: pathlib.Path, templates: Environment) -> List[str]: """ Update blocks in the ``README.rst`` file. :param repo_path: Path to the repository root. :param templates: """ # TODO: link to documentation below installation readme_file = PathPlus(repo_path / "README.rst") shields_block = ShieldsBlock( username=templates.globals["username"], repo_name=templates.globals["repo_name"], version=templates.globals["version"], conda=templates.globals["enable_conda"], tests=templates.globals["enable_tests"] and not templates.globals["stubs_package"], docs=templates.globals["enable_docs"], pypi_name=templates.globals["pypi_name"], docker_shields=templates.globals["docker_shields"], docker_name=templates.globals["docker_name"], platforms=templates.globals["platforms"], pre_commit=templates.globals["enable_pre_commit"], on_pypi=templates.globals["on_pypi"], docs_url=templates.globals["docs_url"], primary_conda_channel=templates.globals["primary_conda_channel"], ).make() if templates.globals["on_pypi"]: install_block = create_readme_install_block( templates.globals["modname"], templates.globals["username"], templates.globals["enable_conda"], templates.globals["on_pypi"], templates.globals["pypi_name"], templates.globals["conda_channels"], ) else: install_block = get_readme_installation_block_no_pypi_template( ).render( modname=templates.globals["modname"], username=templates.globals["username"], repo_name=templates.globals["repo_name"], ) readme = readme_file.read_text(encoding="UTF-8") readme = shields_regex.sub(str(shields_block), readme) readme = installation_regex.sub(install_block + '\n', readme) short_desc_block = create_short_desc_block( templates.globals["short_desc"], ) readme = short_desc_regex.sub(short_desc_block, readme) readme_file.write_clean(readme) return [readme_file.name]
def replace_unknown_unicode(app: Sphinx, exception: Optional[Exception] = None): r""" Replaces certain unknown unicode characters in the Sphinx LaTeX output with the best equivalents. .. only:: html The mapping is as follows: * ♠ -- \spadesuit * ♥ -- \heartsuit * ♦ -- \diamondsuit * ♣ -- \clubsuit * Zero width space -- \hspace{0pt} * μ -- \textmu * ≡ -- \equiv (new in version 2.11.0) * ≈ -- \approx (new in version 2.12.0) * ≥ -- \geq (new in version 2.13.0) * ≤ -- \leq (new in version 2.13.0) This function can be hooked into the :event:`build-finished` event as follows: .. code-block:: python app.connect("build-finished", replace_unknown_unicode) .. versionadded:: 2.9.0 :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.name.lower() != "latex": return builder = cast(LaTeXBuilder, app.builder) output_file = PathPlus( builder.outdir) / f"{builder.titles[0][1].lower()}.tex" output_content = output_file.read_text() output_content = output_content.replace('♠', r' $\spadesuit$ ') output_content = output_content.replace('♥', r' $\heartsuit$ ') output_content = output_content.replace('♦', r' $\diamondsuit$ ') output_content = output_content.replace('♣', r' $\clubsuit$ ') output_content = output_content.replace( '\u200b', r'\hspace{0pt}') # Zero width space output_content = output_content.replace('μ', r"\textmu{}") output_content = output_content.replace('≡', r" $\equiv$ ") output_content = output_content.replace('≈', r" $\approx$ ") output_content = output_content.replace('≥', r" $\geq$ ") output_content = output_content.replace('≤', r" $\leq$ ") output_file.write_clean(output_content)
def replace_geq(app: Sphinx, exception: Optional[Exception] = None): if exception: return if app.builder.name.lower() != "latex": return output_file = PathPlus( app.builder.outdir) / f"{app.builder.titles[0][1]}.tex" output_content = output_file.read_text() output_content = output_content.replace('≥', r" $\geq$ ") output_file.write_clean(output_content)
def replace_unicode(app, exception): if exception: return if app.builder.name.lower() != "latex": return output_file = PathPlus( app.builder.outdir) / f"{app.builder.titles[0][1]}.tex" output_content = output_file.read_text() output_content = output_content.replace('➞', r" $\rightarrow$ ") output_file.write_clean(output_content)
def check_file(filename: PathPlus, extension: Optional[str] = None): data = filename.read_text(encoding="UTF-8") assert expected_version in data extension = extension or filename.suffix if extension == ".py": extension = "._py_" return check_file_regression(data, advanced_file_regression, extension)
def check_fn(obtained_filename, expected_filename): print(obtained_filename, expected_filename) expected_filename = PathPlus(expected_filename) template = Template(expected_filename.read_text()) expected_filename.write_text( template.render( sphinx_version=sphinx.version_info, python_version=sys.version_info, )) return check_text_files(obtained_filename, expected_filename, encoding="UTF-8")
def check_fn(obtained_filename, expected_filename): __tracebackhide__ = True expected_filename = PathPlus(expected_filename) template = Template(expected_filename.read_text()) expected_filename.write_text( template.render( sphinx_version=sphinx.version_info, python_version=sys.version_info, docutils_version=docutils_version, **jinja2_namespace or {}, )) return check_text_files(obtained_filename, expected_filename, encoding="UTF-8")
def replace_environment_variables_header(app: Sphinx, exception: Optional[Exception] = None): if exception: return if app.builder.name.lower() != "latex": return output_file = PathPlus(app.builder.outdir) / f"{app.builder.titles[0][1]}.tex" output_content = output_file.read_text() output_content = output_content.replace( r"\subsubsection*{Environment variables}", r"\vspace{20px}{\textcolor{TitleColor}{\sffamily\bfseries Environment variables}}", ) output_file.write_clean(output_content)
def replace_latex(app: Sphinx, exception: Optional[Exception] = None): if exception: return if app.builder.name.lower() != "latex": return output_file = PathPlus(app.builder.outdir) / f"{app.builder.titles[0][1]}.tex" output_content = output_file.read_text() output_content = output_content.replace('≡', r"$\equiv$") output_content = output_content.replace( r"@\spxentry{verify}\spxextra{SlumberURL attribute}}\needspace{5\baselineskip}", r"@\spxentry{verify}\spxextra{SlumberURL attribute}}", ) output_file.write_clean(output_content)
def demo_environment(tmp_pathplus): example_formate_toml = PathPlus(__file__).parent / "example_formate.toml" (tmp_pathplus / "formate.toml").write_text( example_formate_toml.read_text()) code = [ "class F:", "\tfrom collections import (", "Iterable,", "\tCounter,", "\t\t)", '', "\tdef foo(self):", "\t\tpass", '', "print('hello world')", ] (tmp_pathplus / "code.py").write_lines(code, trailing_whitespace=True)
def replace_emoji(app: Sphinx, exception: Optional[Exception] = None): if exception: return if app.builder.name.lower() != "latex": return output_file = PathPlus( app.builder.outdir) / f"{app.builder.titles[0][1]}.tex" output_content = output_file.read_text() output_content = output_content.replace('🧰', '') output_content = output_content.replace('📔', '') output_content = output_content.replace( r"\sphinxcode{\sphinxupquote{\textbackslash{}vspace\{\}}}", r"\mbox{\sphinxcode{\sphinxupquote{\textbackslash{}vspace\{\}}}}", ) output_file.write_clean(output_content)
class Reformatter: """ Reformat a Python source file. :param filename: The filename to reformat. :param config: The ``formate`` configuration, parsed from a TOML file (or similar). .. autosummary-widths:: 5/16 11/16 """ #: The filename being reformatted. filename: str #: The filename being reformatted, as a POSIX-style path. file_to_format: PathPlus #: The ``formate`` configuration, parsed from a TOML file (or similar). config: FormateConfigDict def __init__(self, filename: PathLike, config: FormateConfigDict): self.file_to_format = PathPlus(filename) self.filename = self.file_to_format.as_posix() self.config = config self._unformatted_source = self.file_to_format.read_text() self._reformatted_source: Optional[str] = None def run(self) -> bool: """ Run the reformatter. :return: Whether the file was changed. """ hooks = parse_hooks(self.config) reformatted_source = StringList(call_hooks(hooks, self._unformatted_source, self.filename)) reformatted_source.blankline(ensure_single=True) self._reformatted_source = str(reformatted_source) return self._reformatted_source != self._unformatted_source def get_diff(self) -> str: """ Returns the diff between the original and reformatted file content. """ # Based on yapf # Apache 2.0 License after = self.to_string().split('\n') before = self._unformatted_source.split('\n') return coloured_diff( before, after, self.filename, self.filename, "(original)", "(reformatted)", lineterm='', ) def to_string(self) -> str: """ Return the reformatted file as a string. :rtype: .. latex:clearpage:: """ if self._reformatted_source is None: raise ValueError("'Reformatter.run()' must be called first!") return self._reformatted_source def to_file(self) -> None: """ Write the reformatted source to the original file. """ self.file_to_format.write_text(self.to_string())
def test_append_pathplus(self): file = PathPlus("paths_append_test_file.txt") file.write_text("initial content\n") file.append_text("appended content") assert file.read_text() == "initial content\nappended content" file.unlink()
def rewrite_docs_index(repo_path: pathlib.Path, templates: Environment) -> List[str]: """ Update blocks in the documentation ``index.rst`` file. :param repo_path: Path to the repository root. :param templates: """ index_rst_file = PathPlus(repo_path / templates.globals["docs_dir"] / "index.rst") index_rst_file.parent.maybe_make() # Set up the blocks sb = ShieldsBlock( username=templates.globals["username"], repo_name=templates.globals["repo_name"], version=templates.globals["version"], conda=templates.globals["enable_conda"], tests=templates.globals["enable_tests"] and not templates.globals["stubs_package"], docs=templates.globals["enable_docs"], pypi_name=templates.globals["pypi_name"], docker_shields=templates.globals["docker_shields"], docker_name=templates.globals["docker_name"], platforms=templates.globals["platforms"], pre_commit=templates.globals["enable_pre_commit"], on_pypi=templates.globals["on_pypi"], primary_conda_channel=templates.globals["primary_conda_channel"], ) sb.set_docs_mode() make_out = sb.make() shield_block_list = StringList([*make_out[0:2], ".. only:: html"]) with shield_block_list.with_indent_size(1): shield_block_list.extend(make_out[1:-1]) shield_block_list.append(make_out[-1]) shields_block = str(shield_block_list) if templates.globals["license"] == "GNU General Public License v2 (GPLv2)": source = f"https://img.shields.io/github/license/{templates.globals['username']}/{templates.globals['repo_name']}" shields_block.replace( source, "https://img.shields.io/badge/license-GPLv2-orange") # .. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg install_block = create_docs_install_block( templates.globals["repo_name"], templates.globals["username"], templates.globals["enable_conda"], templates.globals["on_pypi"], templates.globals["pypi_name"], templates.globals["conda_channels"], ) + '\n' links_block = create_docs_links_block( templates.globals["username"], templates.globals["repo_name"], ) # Do the replacement index_rst = index_rst_file.read_text(encoding="UTF-8") index_rst = shields_regex.sub(shields_block, index_rst) index_rst = installation_regex.sub(install_block, index_rst) index_rst = links_regex.sub(links_block, index_rst) index_rst = short_desc_regex.sub( ".. start short_desc\n\n.. documentation-summary::\n\t:meta:\n\n.. end short_desc", index_rst, ) if ":caption: Links" not in index_rst and not templates.globals[ "preserve_custom_theme"]: index_rst = index_rst.replace( ".. start links", '\n'.join([ ".. sidebar-links::", "\t:caption: Links", "\t:github:", (f" :pypi: {templates.globals['pypi_name']}" if templates.globals["on_pypi"] else ''), '', '', ".. start links", ])) index_rst_file.write_clean(index_rst) return [index_rst_file.relative_to(repo_path).as_posix()]
def make_pre_commit(repo_path: pathlib.Path, templates: Environment) -> List[str]: """ Add configuration for ``pre-commit``. https://github.com/pre-commit/pre-commit # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks :param repo_path: Path to the repository root. :param templates: """ docs_dir = templates.globals["docs_dir"] import_name = templates.globals["import_name"] stubs_package = templates.globals["stubs_package"] non_source_files = [ posixpath.join(docs_dir, "conf"), "__pkginfo__", "setup" ] domdfcoding_hooks = Repo( repo=make_github_url("domdfcoding", "pre-commit-hooks"), rev="v0.3.0", hooks=[ { "id": "requirements-txt-sorter", "args": ["--allow-git"] }, { "id": "check-docstring-first", "exclude": fr"^({'|'.join(non_source_files)}|{templates.globals['tests_dir']}/.*)\.py$" }, "bind-requirements", ]) flake8_dunder_all = Repo( repo=make_github_url("domdfcoding", "flake8-dunder-all"), rev="v0.1.8", hooks=[{ "id": "ensure-dunder-all", "files": fr"^{import_name}{'-stubs' if stubs_package else ''}/.*\.py$" }]) snippet_fmt = Repo( repo=make_github_url("python-formate", "snippet-fmt"), rev="v0.1.4", hooks=["snippet-fmt"], ) formate_excludes = fr"^({'|'.join([*templates.globals['yapf_exclude'], *non_source_files])})\.(_)?py$" formate = Repo( repo=make_github_url("python-formate", "formate"), rev="v0.4.9", hooks=[{ "id": "formate", "exclude": formate_excludes }], ) dep_checker_args = [templates.globals["import_name"].replace('.', '/')] if templates.globals["source_dir"]: dep_checker_args.extend( ["--work-dir", templates.globals["source_dir"]]) dep_checker = Repo(repo=make_github_url("domdfcoding", "dep_checker"), rev="v0.6.2", hooks=[{ "id": "dep_checker", "args": dep_checker_args }]) pre_commit_file = PathPlus(repo_path / ".pre-commit-config.yaml") if not pre_commit_file.is_file(): pre_commit_file.touch() dumper = ruamel.yaml.YAML() dumper.indent(mapping=2, sequence=3, offset=1) output = StringList([ f"# {templates.globals['managed_message']}", "---", '', f"exclude: {templates.globals['pre_commit_exclude']}", '', "repos:", ]) indent_re = re.compile("^ {3}") managed_hooks = [ pyproject_parser, pre_commit_hooks, domdfcoding_hooks, flake8_dunder_all, flake2lint, pygrep_hooks, pyupgrade, lucas_c_hooks, snippet_fmt, formate, ] if not templates.globals["stubs_package"]: managed_hooks.append(dep_checker) managed_hooks_urls = [str(hook.repo) for hook in managed_hooks] custom_hooks_comment = "# Custom hooks can be added below this comment" for hook in managed_hooks: buf = StringIO() dumper.dump(hook.to_dict(), buf) output.append(indent_re.sub(" - ", indent(buf.getvalue(), " "))) output.blankline(ensure_single=True) output.append(custom_hooks_comment) output.blankline(ensure_single=True) raw_yaml = pre_commit_file.read_text() if custom_hooks_comment in raw_yaml: custom_hooks_yaml = pre_commit_file.read_text().split( custom_hooks_comment)[1] custom_hooks = [] local_hooks = [] for repo in yaml_safe_loader.load(custom_hooks_yaml) or []: if repo["repo"] == "local": local_hooks.append(repo) elif repo["repo"] not in managed_hooks_urls: custom_hooks.append(Repo(**repo)) for hook in custom_hooks: buf = StringIO() dumper.dump(hook.to_dict(), buf) output.append(indent_re.sub(" - ", indent(buf.getvalue(), " "))) output.blankline(ensure_single=True) for hook in local_hooks: buf = StringIO() dumper.dump(hook, buf) output.append(indent_re.sub(" - ", indent(buf.getvalue(), " "))) output.blankline(ensure_single=True) pre_commit_file.write_lines(output) return [pre_commit_file.name]
class Reformatter: """ Reformat a Python source file. :param filename: :param yapf_style: The name of the yapf style, or the path to the yapf style file. :param isort_config: The filename of the isort configuration file. """ def __init__(self, filename: PathLike, yapf_style: str, isort_config: Config): self.file_to_format = PathPlus(filename) self.filename = self.file_to_format.as_posix() self.yapf_style = yapf_style self.isort_config = isort_config self._unformatted_source = self.file_to_format.read_text() self._reformatted_source: Optional[str] = None def run(self) -> bool: """ Run the reformatter. :return: Whether the file was changed. """ quote_formatted_code = reformat_quotes(self._unformatted_source) yapfed_code = FormatCode(quote_formatted_code, style_config=self.yapf_style)[0] generic_formatted_code = reformat_generics(yapfed_code) # TODO: support spaces try: isorted_code = StringList( isort.code(generic_formatted_code, config=self.isort_config)) except FileSkipComment: isorted_code = StringList(generic_formatted_code) isorted_code.blankline(ensure_single=True) self._reformatted_source = str(isorted_code) # Fix for noqa comments being pushed to new line self._reformatted_source = noqa_reformat(self._reformatted_source) return self._reformatted_source != self._unformatted_source def get_diff(self) -> str: """ Returns the diff between the original and reformatted file content. """ # Based on yapf # Apache 2.0 License if self._reformatted_source is None: raise ValueError("'Reformatter.run()' must be called first!") before = self._unformatted_source.splitlines() after = self._reformatted_source.splitlines() return coloured_diff( before, after, self.filename, self.filename, "(original)", "(reformatted)", lineterm='', ) def to_string(self) -> str: """ Return the reformatted file as a string. """ if self._reformatted_source is None: raise ValueError("'Reformatter.run()' must be called first!") return self._reformatted_source def to_file(self) -> None: """ Write the reformatted source to the original file. """ if self._reformatted_source is None: raise ValueError("'Reformatter.run()' must be called first!") self.file_to_format.write_text(self._reformatted_source)
def demo_pyproject_environment(demo_environment, tmp_pathplus): example_formate_toml = PathPlus(__file__).parent / "example_pyproject.toml" (tmp_pathplus / "pyproject.toml").write_text( example_formate_toml.read_text())