Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add docstrings mypy check #1448

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/check_docstrings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Check docstrings

on:
pull_request:
push:
branches: [main]

jobs:
check_docstrings:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: local-install
run: uv pip install -e . --system
- name: install-reqs
run: uv pip install mypy -r requirements-dev.txt --system
- name: Run docstring check
run: python utils/check_docstrings.py
72 changes: 72 additions & 0 deletions utils/check_docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

import ast
import doctest
import os
import sys
from pathlib import Path

from mypy import api


def validate_results(my_py_results: list[tuple[Path, str, str]]) -> None:
if len(my_py_results) == 0:
print("No issues found.") # noqa: T201
sys.exit(0)
else:
for file, name, result in my_py_results:
print(f"File: {file}\nFunc or class name: {name}") # noqa: T201
print(result) # noqa: T201
print() # noqa: T201
sys.exit(1)


def check_with_mypy(
examples: list[tuple[Path, str, str]],
) -> list[tuple[Path, str, str]]:
results: list[tuple[Path, str, str]] = []
for file, name, example in examples:
print(f"Checking {file} {name}") # noqa: T201
result = api.run(["-c", example, "--ignore-missing-imports"])
if "Success" in result[0]:
print("Success") # noqa: T201
continue
print(f"{result[0]}") # noqa: T201
results.append((file, name, result[0]))
return results


def get_docstrings_examples(files: list[Path]) -> list[tuple[Path, str, str]]:
docstrings: list[tuple[Path, str, str]] = []
for file in files:
with open(file) as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
docstring = ast.get_docstring(node)
if docstring:
parsed_examples = doctest.DocTestParser().get_examples(docstring)
# Treat all examples as one code block, because there are cases
# where explainer text separates code examples
example_code = "\n".join(
example.source for example in parsed_examples
)
docstrings.append((file, node.name, example_code))
return docstrings


def get_python_files() -> list[Path]:
package_path = Path("./narwhals")
return [
Path(path) / name
for path, _subdir, files in os.walk(package_path)
for name in files
if name.endswith(".py")
]


if __name__ == "__main__":
python_files = get_python_files()
docstring_examples = get_docstrings_examples(python_files)
my_py_results = check_with_mypy(docstring_examples)
validate_results(my_py_results)
Loading