Path: blob/main/scripts/content_checks/nb_vale.py
3855 views
"""This script runs 'prose linting' checks on the notebooks.1This includes spell checking, and writing best practices.2Requires Vale: https://vale.sh/docs/vale-cli/installation/3"""4import sys5import os6import shutil7import nbformat8import subprocess9import json10from pathlib import Path11from tools import parse_args, style, indent121314NB_PATHS = './scripts/content_checks/notebook_paths.txt'15TEMP_DIR = './scripts/temp/md'16STYLE_DIR = './scripts/content_checks/style'171819def lint_notebook(filepath, CI=False):20"""Perform Vale prose linting checks on21notebook at `filepath`. If `CI` then exit22early with code 1 on lint error."""23print(style('bold', filepath))24outdir = (25Path(TEMP_DIR)26/ Path(filepath).parent.stem27/ Path(filepath).stem28)29extract_markdown(filepath, outdir)30lint_markdown(outdir, CI)313233def extract_markdown(filepath, outdir):34"""Extracts the markdown from the notebook35at `filepath` and saves each cell as a separate36file in the temp folder for Vale to lint."""37nb = nbformat.read(filepath, as_version=4)38if os.path.exists(outdir):39shutil.rmtree(outdir)40Path(outdir).mkdir(parents=True)41for idx, cell in enumerate(nb.cells):42if cell.cell_type == 'markdown':43# outpath e.g.: ./scripts/temp/intro/grover-intro/0002.md44outpath = Path(outdir) / (str(idx).rjust(4, '0') + '.md')45with open(outpath, 'w+') as f:46f.write(cell.source)474849def lint_markdown(md_dir, CI=False):50"""Lints the markdown files in `md_dir`51using Vale linter. If `CI`, then will exit with52code 1 on any Vale error."""5354# Run Vale on folder55files = os.listdir(md_dir)56path = Path(md_dir)57vale_result = subprocess.run(58['vale', path, '--output', 'JSON'],59capture_output=True)60vale_output = json.loads(vale_result.stdout)6162# Parse output and print nicely63fail = False64notebook_msg = ''65for file, suggestions in vale_output.items():66notebook_msg += f"cell {int(Path(file).stem)}\n"67cell_msg = ''68for s in suggestions:69severity = s['Severity']70if severity == 'error':71fail = True72cell_msg += style(severity, severity.capitalize())73cell_msg += f": {s['Message']}\n"74if s['Match'] != '':75cell_msg += style('faint', f'"…{s["Match"]}…" ')76cell_msg += style('faint',77f"@l{s['Line']};c{s['Span'][0]} ({s['Check']})")78cell_msg += '\n'79notebook_msg += indent(cell_msg) + '\n'80if not CI and (notebook_msg!=''):81print(indent(notebook_msg))82if fail and CI:83print(indent(notebook_msg))84print(style('error', 'Prose linting error encountered; test failed.'))85sys.exit(1)868788if __name__ == '__main__':89# usage: python3 nb_vale.py --CI notebook1.ipynb path/to/notebook2.ipynb90switches, filepaths = parse_args(sys.argv)9192CI = '--CI' in switches9394for path in filepaths:95lint_notebook(path, CI)969798