Skip to content

Commit

Permalink
Add minimal typecheck infrastructure to CI (has2k1#644)
Browse files Browse the repository at this point in the history
* add mypy.ini, but ignore errors at first

* type hint labels

* type hint options, exceptions

Also fix an import error in options.
It attempted a relative import beyond the top level package.

* Add typecheck job to CI

* resolve feedback
  • Loading branch information
Emerentius authored Nov 8, 2022
1 parent b120af2 commit 0724b8e
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 13 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,40 @@ jobs:
shell: bash -l {0}
run: make lint


# Typecheck
typecheck:
runs-on: ubuntu-latest

# We want to run on external PRs, but not on our own internal PRs as they'll be run
# by the push to the branch.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository

strategy:
matrix:
python-version: [3.8]
steps:
- name: Checkout Code
uses: actions/checkout@v3

- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install Packages
shell: bash -l {0}
run: pip install mypy

- name: Environment Information
shell: bash -l {0}
run: pip list

- name: Run Tests
shell: bash -l {0}
run: make typecheck


# Documentation
documentation:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ clean-test:
lint:
flake8 plotnine

typecheck:
mypy plotnine

test: clean-test
export MATPLOTLIB_BACKEND=agg
pytest
Expand Down
47 changes: 47 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Mypy configuration file
# See docs: https://mypy.readthedocs.io/en/stable/config_file.html
[mypy]

# Specifies the Python version used to parse and check the target program.
# We're using the oldest supported python version. Mypy will warn us if we use
# features or parts of the stdlib that are too new.
python_version = 3.8

# Set some rather strict defaults, but also globally deactivate all errors
# and then activate the typecheck only for certain modules.
# The set of typechcecked modules can be incrementally expanded until everything
# is correctly typed.

# Shows a warning when returning a value with type Any from a function declared with a non- Any return type.
warn_return_any = True

# Warns about per-module sections in the config file that do not match any files processed when invoking mypy.
# Has no effect in incremental mode
warn_unused_configs = True

# Disallows defining functions without type annotations or with incomplete type annotations.
# Once a module is fully typed, this should be activated to avoid regressions.
disallow_untyped_defs = True

# Disallows usage of generic types that do not specify explicit type parameters.
disallow_any_generics = True

# Allows variables to be redefined with an arbitrary type, as long as the redefinition is in the same block and nesting level as the original definition.
# The original variable must have been used before redefinition.
allow_redefinition = True

# Shows a warning when encountering any code inferred to be unreachable or redundant after performing type analysis.
warn_unreachable = True

# Ignores all non-fatal errors.
ignore_errors = True

# per module options
[mypy-plotnine.labels]
ignore_errors = False

[mypy-plotnine.options]
ignore_errors = False

[mypy-plotnine.exceptions]
ignore_errors = False
9 changes: 6 additions & 3 deletions plotnine/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations
from textwrap import dedent
import warnings


# Show the warnings on one line, leaving out any code makes the
# message clear
def warning_format(message, category, filename, lineno, file=None, line=None):
def warning_format( # type: ignore
message, category, filename, lineno, line=None
):
"""
Format for plotnine warnings
"""
Expand All @@ -20,11 +23,11 @@ class PlotnineError(Exception):
Exception for ggplot errors
"""

def __init__(self, *args):
def __init__(self, *args: str) -> None:
args = [dedent(arg) for arg in args]
self.message = " ".join(args)

def __str__(self):
def __str__(self) -> str:
"""
Error Message
"""
Expand Down
17 changes: 11 additions & 6 deletions plotnine/labels.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from __future__ import annotations
from copy import deepcopy

from .mapping.aes import rename_aesthetics, SCALED_AESTHETICS
from .exceptions import PlotnineError

import typing
if typing.TYPE_CHECKING:
import plotnine as p9

__all__ = ['xlab', 'ylab', 'labs', 'ggtitle']
VALID_LABELS = SCALED_AESTHETICS | {'caption', 'title'}

Expand All @@ -17,17 +22,17 @@ class labs:
Aesthetics (with scales) to be renamed. You can also
set the ``title`` and ``caption``.
"""
labels = {}
labels: dict[str, str] = {}

def __init__(self, **kwargs):
def __init__(self, **kwargs: str) -> None:
unknown = kwargs.keys() - VALID_LABELS
if unknown:
raise PlotnineError(
f"Cannot deal with these labels: {unknown}"
)
self.labels = rename_aesthetics(kwargs)

def __radd__(self, gg, inplace=False):
def __radd__(self, gg: p9.ggplot, inplace: bool = False) -> p9.ggplot:
"""
Add labels to ggplot object
"""
Expand All @@ -46,7 +51,7 @@ class xlab(labs):
x-axis label
"""

def __init__(self, xlab):
def __init__(self, xlab: str) -> None:
if xlab is None:
raise PlotnineError(
"Arguments to xlab cannot be None")
Expand All @@ -63,7 +68,7 @@ class ylab(labs):
y-axis label
"""

def __init__(self, ylab):
def __init__(self, ylab: str) -> None:
if ylab is None:
raise PlotnineError(
"Arguments to ylab cannot be None")
Expand All @@ -80,7 +85,7 @@ class ggtitle(labs):
Plot title
"""

def __init__(self, title):
def __init__(self, title: str) -> None:
if title is None:
raise PlotnineError(
"Arguments to ggtitle cannot be None")
Expand Down
10 changes: 6 additions & 4 deletions plotnine/options.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

#: Development flag, e.g. set to ``True`` to prevent
#: the queuing up of figures when errors happen.
close_all_figures = False
Expand Down Expand Up @@ -28,7 +30,7 @@
}


def get_option(name):
def get_option(name: str) -> Any:
"""
Get package option
Expand All @@ -40,13 +42,13 @@ def get_option(name):
d = globals()

if name in {'get_option', 'set_option'} or name not in d:
from ..exceptions import PlotnineError
from .exceptions import PlotnineError
raise PlotnineError(f"Unknown option {name}")

return d[name]


def set_option(name, value):
def set_option(name: str, value: Any) -> Any:
"""
Set package option
Expand All @@ -65,7 +67,7 @@ def set_option(name, value):
d = globals()

if name in {'get_option', 'set_option'} or name not in d:
from ..exceptions import PlotnineError
from .exceptions import PlotnineError
raise PlotnineError(f"Unknown option {name}")

old = d[name]
Expand Down

0 comments on commit 0724b8e

Please sign in to comment.