-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Change directory structure for Python packaging - Split `algoesup.py` into modules to separate IPython code - Add README for PyPI - Use `poetry` for building and development - replace `requirements.txt` with `pyproject.toml` - make linters dev dependencies - Update doc config for Google-style docstrings --------- Co-authored-by: Michel Wermelinger <[email protected]>
- Loading branch information
1 parent
26da24e
commit c06c3dd
Showing
11 changed files
with
3,241 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
[tool.poetry] | ||
name = "algoesup" | ||
version = "0.1.0" | ||
description = "Algorithmic essay support library" | ||
authors = [ | ||
"Michel Wermelinger <[email protected]>", | ||
"Michael Snowden" | ||
] | ||
packages = [ | ||
{ include = "algoesup", from = "src" }, | ||
] | ||
readme = "src/README.md" | ||
license = "BSD-3-Clause" | ||
repository = "https://github.com/dsa-ou/algoesup" | ||
homepage = "https://dsa-ou.github.io/algoesup/" | ||
keywords = ["education"] | ||
classifiers = [ | ||
"Intended Audience :: Education", | ||
"License :: OSI Approved :: BSD License", | ||
"Programming Language :: Python :: 3.10", | ||
"Topic :: Education", | ||
"Operating System :: OS Independent", | ||
] | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.10" | ||
ipython = "^8.13.1" | ||
matplotlib = "^3.4.2" | ||
|
||
[tool.poetry.group.dev.dependencies] | ||
ruff = "^0.1.11" | ||
allowed = "^1.3" | ||
pytype = {version = ">=2023.04.27", markers = "platform_system != 'Windows'"} | ||
mkdocs = "^1.5.3" | ||
mkdocs-material = "^9.5.11" | ||
mkdocstrings = { version = "^0.24.0", extras = ["python"] } | ||
mkdocs-jupyter = "^0.24.6" | ||
mkdocs-open-in-new-tab = "^1.0.3" | ||
|
||
[build-system] | ||
requires = ["poetry-core"] | ||
build-backend = "poetry.core.masonry.api" |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Algoesup | ||
|
||
This small library was written to support the writing of | ||
[algorithmic essays](https://dsa-ou.github.io/algoesup), | ||
but can be used independently of that purpose. | ||
The library provides auxiliary functions that make it easier to: | ||
- write unit tests for functions | ||
- measure and plot run-times for best, average and worst case inputs | ||
- use linters within Jupyter Notebook environments. | ||
|
||
Guidance on how to use the library is [here](https://dsa-ou.github.io/algoesup/writing/#code). | ||
|
||
To install in the global Python environment, open a terminal or PowerShell, and enter | ||
```bash | ||
pip install algoesup | ||
``` | ||
To install in a virtual environment, activate it before entering the command above. | ||
|
||
The library supports the [ruff](https://docs.astral.sh/ruff) and | ||
[allowed](https://dsa-ou.github.io/allowed) linters, and the | ||
[pytype](https://google.github.io/pytype) type checker. | ||
You have to install them explicitly if you want to use them from within a notebook: | ||
```bash | ||
pip install ruff allowed pytype | ||
``` | ||
Note that `pytype` is not available for Windows. | ||
|
||
## Licence | ||
|
||
`algoesup` is Copyright © 2023–2024 by The Open University, UK. | ||
The code is licensed under a | ||
[BSD 3-clause licence](https://github.com/dsa-ou/algoesup/blob/main/LICENSE). | ||
The documentation is licensed under a | ||
[Creative Commons Attribution 4.0 International Licence](https://creativecommons.org/licenses/by/4.0/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from .test import test | ||
from .time import time_cases, time_functions, time_functions_int |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
"""Linting tools for Jupyter Notebook environments""" | ||
|
||
import json | ||
import os | ||
import re | ||
import subprocess | ||
import tempfile | ||
from typing import Callable | ||
|
||
from IPython.core.inputtransformer2 import TransformerManager | ||
from IPython.core.magic import register_line_magic | ||
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring | ||
from IPython.display import display_markdown | ||
|
||
|
||
def show_errors(checker: str, output: str, filename: str) -> None: | ||
"""Print the errors for the given file in the checker's output.""" | ||
md = [f"**{checker}** found issues:"] | ||
for line in output.split("\n"): | ||
if "syntax" in line.lower() and "error" in line.lower(): | ||
continue # syntax errors already reported when running the cell | ||
if m := re.match(rf".*{filename}[^\d]*(\d+[^:]*:.*)", line): | ||
md.append(f"- {m.group(1)}") | ||
if len(md) > 1: | ||
display_markdown("\n".join(md), raw=True) | ||
|
||
|
||
def show_ruff_json(checker: str, output: str, filename: str) -> None: | ||
"""Print the errors in ruff's JSON output for the given file.""" | ||
if errors := json.loads(output): | ||
md = [f"**{checker}** found issues:"] | ||
# the following assumes errors come in line order | ||
for error in errors: | ||
line = error["location"]["row"] | ||
code = error["code"] | ||
url = error["url"] | ||
msg = error["message"] | ||
if error["fix"]: | ||
msg += f". Suggested fix: {error['fix']['message']}" | ||
md.append(f"- {line}: \[[{code}]({url})\] {msg}") | ||
display_markdown("\n".join(md), raw=True) | ||
|
||
|
||
def show_pytype_errors(checker: str, output: str, filename: str) -> None: | ||
"""Print the errors in pytype's output for the given file.""" | ||
md = [f"**{checker}** found issues:"] | ||
for error in output.split("\n"): | ||
if "syntax" in error.lower() and "error" in error.lower(): | ||
continue # syntax errors already reported when running the cell | ||
if m := re.match(rf".*{filename}[^\d]*(\d+)[^:]*:(.*)\[(.*)\]", error): | ||
line = m.group(1) | ||
msg = m.group(2) | ||
code = m.group(3) | ||
md.append( | ||
f"- {line}:{msg}\[[{code}](https://google.github.io/pytype/errors.html#{code})\]" | ||
) | ||
if len(md) > 1: | ||
display_markdown("\n".join(md), raw=True) | ||
|
||
|
||
# register the supported checkers, their commands and the output processor | ||
checkers: dict[str, tuple[str, Callable]] = { | ||
"pytype": ("pytype", show_pytype_errors), | ||
"allowed": ("allowed", show_errors), | ||
"ruff": ("ruff check --output-format json", show_ruff_json), | ||
} | ||
# initially no checker is active | ||
active: set[str] = set() | ||
|
||
|
||
def process_status(name: str, status: str) -> None: | ||
"""Process the status of a checker.""" | ||
if status is None: | ||
print(name, "is", "active" if name in active else "inactive") | ||
elif status == "on": | ||
active.add(name) | ||
print(name, "was activated") | ||
elif status == "off": | ||
active.discard(name) | ||
print(name, "was deactivated") | ||
|
||
|
||
@magic_arguments() | ||
@argument( | ||
"status", | ||
choices=["on", "off"], | ||
type=str.lower, | ||
help="Activate or deactivate the linter. If omitted, show the current status.", | ||
nargs="?", | ||
default=None, | ||
) | ||
@register_line_magic | ||
def pytype(line: str) -> None: | ||
"""Activate/deactivate the `pytype` linter. | ||
This ipython magic command controls the activation state of the `pytype` linter within | ||
the ipython environment. It can be toggled on or off, or queried for its | ||
current state. | ||
Examples: | ||
``` | ||
%pytype on | ||
pytype was activated | ||
%pytype off | ||
pytype was deactivated | ||
%pytype | ||
pytype is inactive | ||
``` | ||
""" | ||
args = parse_argstring(pytype, line) | ||
process_status("pytype", args.status) | ||
|
||
|
||
@magic_arguments() | ||
@argument( | ||
"-c", | ||
"--config", | ||
default=None, | ||
help="Use configuration file CONFIG (default: m269.json).", | ||
) | ||
@argument( | ||
"status", | ||
choices=["on", "off"], | ||
type=str.lower, | ||
help="Activate or deactivate the linter. If omitted, show the current status.", | ||
nargs="?", | ||
default=None, | ||
) | ||
@register_line_magic | ||
def allowed(line: str) -> None: | ||
"""Activate/deactivate the `pytype` linter. | ||
This magic command controls the activation state of the `pytype` linter within | ||
the ipython environment. It can be toggled on or off, or queried for its | ||
current state. | ||
Examples: | ||
``` | ||
%pytype on | ||
pytype was activated | ||
%pytype off | ||
pytype was deactivated | ||
%pytype | ||
pytype is inactive | ||
``` | ||
""" | ||
args = parse_argstring(allowed, line) | ||
config = f"-c {args.config}" if args.config else "" | ||
checkers["allowed"] = (f"allowed {config}", show_errors) | ||
process_status("allowed", args.status) | ||
|
||
|
||
@magic_arguments() | ||
@argument( | ||
"status", | ||
choices=["on", "off"], | ||
type=str.lower, | ||
help="Activate or deactivate the linter. If omitted, show the current status.", | ||
nargs="?", | ||
default=None, | ||
) | ||
@register_line_magic | ||
def ruff(line: str) -> None: | ||
"""Activate/deactivate the `ruff` linter. | ||
This ipython magic command controls the activation state of the `ruff` linter within | ||
the interactive environment. It can be toggled on or off, or queried for its | ||
current state. | ||
Examples: | ||
`%ruff on` | ||
`ruff was activated` | ||
`%ruff off` | ||
`ruff was deactivated` | ||
`%ruff` | ||
`ruff is inactive` | ||
""" | ||
args = parse_argstring(ruff, line) | ||
process_status("ruff", args.status) | ||
|
||
|
||
# TODO: add an option to set the output processor function | ||
@register_line_magic | ||
def checker(line: str) -> None: | ||
"""Define or turn on/off a given checker.""" | ||
global checkers, active | ||
|
||
args = line.split() | ||
if len(args) == 0: | ||
print("Active checkers:", *active) | ||
print("Inactive checkers:", *(set(checkers) - active)) | ||
return | ||
|
||
name = args[0] | ||
if len(args) == 1: | ||
if name not in checkers: | ||
print(f"Checker {name} isn't defined.") | ||
else: | ||
status = "active" if name in active else "inactive" | ||
print(f"Checker {name} is {status} and defined as '{checkers[name]}'.") | ||
elif args[1].lower() == "on": | ||
if name not in checkers: | ||
print(f"Error: checker {name} isn't defined.") | ||
else: | ||
active.add(name) | ||
print(f"Checker {name} has been activated.") | ||
elif args[1].lower() == "off": | ||
if name not in checkers: | ||
print(f"Error: checker {name} isn't defined.") | ||
else: | ||
active.discard(name) | ||
print(f"Checker {name} has been deactivated.") | ||
else: | ||
command = " ".join(args[1:]) | ||
status = "redefined" if name in checkers else "defined" | ||
checkers[name] = command | ||
active.add(name) | ||
print(f"Checker {name} has been {status} and activated.") | ||
|
||
|
||
def run_checkers(result) -> None: | ||
"""Run all active checkers after a cell is executed.""" | ||
if not active: | ||
return | ||
try: | ||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp: | ||
# transform IPython to pure Python to avoid linters reporting syntax errors | ||
temp.write(TransformerManager().transform_cell(result.info.raw_cell)) | ||
temp_name = temp.name | ||
for checker in active: | ||
command, display = checkers[checker] | ||
command += " " + temp_name | ||
try: | ||
output = subprocess.run( | ||
command, capture_output=True, text=True, check=False, shell=True | ||
).stdout | ||
display(checker, output, temp_name) | ||
except Exception as e: | ||
print(f"Error on executing {command}:\n{e}") | ||
except Exception as e: | ||
print(f"Error on writing cell to a temporary file:\n{e}") | ||
finally: | ||
os.remove(temp_name) | ||
|
||
|
||
def load_ipython_extension(ipython): | ||
ipython.events.register("post_run_cell", run_checkers) # type: ignore[name-defined] |
Oops, something went wrong.