From 45cd514805e236f393e13ce7ee53b7c527513f79 Mon Sep 17 00:00:00 2001 From: Chuck McCallum Date: Wed, 20 Nov 2024 21:41:40 -0500 Subject: [PATCH] Type checking with pyright (#164) * lower and upper more consistently * one more * handle bounds/bins/counts the same way * lots of reactive dicts, but the UI has not changed * data dump on the results page * add a pragma: no cover * reset widget values after checkbox change * do not clean up values * put tooltips in labels * pull warning up to analysis panel. TODO: conditional * move warning to bottom of list * analysis definition JSON * stubs for python * stub a script on results page * include column info in generated script * closer to a runable notebook * stuck on split_by_weight... maybe a library bug? * margin stubs * format python identifiers correctly * script has gotten longer: does not make sense to check for exact equality * fix syntactic problems in generated code * fill in columns, but still WIP * fix column names; tests pass * move confidence * simplify download panel * add markdown cells * tidy up * switch requirements from mypy to pyright * run pyright; currently fails * fix pyright errors * type input, output, session * rm unused session * more typing * enable strict type checking; lots of errors! * more typing. Add "finish()" for Templates (Alternative is to implement __buffer__.) * add a lot of ignores, but type checks pass locally * I think Optional is needed under 3.9 * Remove strict, and most of the ignores * tests in code generation --- .mypy.ini | 5 --- dp_wizard/app/__init__.py | 13 +++--- dp_wizard/app/analysis_panel.py | 31 +++++++------- dp_wizard/app/components/column_module.py | 46 ++++++++++----------- dp_wizard/app/components/inputs.py | 2 +- dp_wizard/app/components/outputs.py | 4 +- dp_wizard/app/dataset_panel.py | 13 +++--- dp_wizard/app/feedback_panel.py | 8 ++-- dp_wizard/app/results_panel.py | 22 +++++----- dp_wizard/utils/argparse_helpers.py | 19 +++++---- dp_wizard/utils/code_generators/__init__.py | 24 ++++++----- dp_wizard/utils/converters.py | 2 +- dp_wizard/utils/csv_helper.py | 10 ++--- dp_wizard/utils/dp_helper.py | 18 +++++--- dp_wizard/utils/mock_data.py | 11 +++-- dp_wizard/utils/shared.py | 11 +++-- pyproject.toml | 4 ++ requirements-dev.in | 2 +- requirements-dev.txt | 14 +++---- tests/utils/test_misc.py | 4 +- 20 files changed, 145 insertions(+), 118 deletions(-) delete mode 100644 .mypy.ini diff --git a/.mypy.ini b/.mypy.ini deleted file mode 100644 index 16faf59..0000000 --- a/.mypy.ini +++ /dev/null @@ -1,5 +0,0 @@ -[mypy] -exclude = '\.venv' - -# TODO: Ignore undefined names only in templates. -disable_error_code = name-defined diff --git a/dp_wizard/app/__init__.py b/dp_wizard/app/__init__.py index 435e38d..43e78ff 100644 --- a/dp_wizard/app/__init__.py +++ b/dp_wizard/app/__init__.py @@ -1,9 +1,9 @@ from pathlib import Path import logging -from shiny import App, ui, reactive +from shiny import App, ui, reactive, Inputs, Outputs, Session -from dp_wizard.utils.argparse_helpers import get_cli_info +from dp_wizard.utils.argparse_helpers import get_cli_info, CLIInfo from dp_wizard.app import analysis_panel, dataset_panel, results_panel, feedback_panel @@ -26,16 +26,17 @@ def ctrl_c_reminder(): # pragma: no cover print("Session ended (Press CTRL+C to quit)") -def make_server_from_cli_info(cli_info): - def server(input, output, session): # pragma: no cover - csv_path = reactive.value(cli_info.csv_path) +def make_server_from_cli_info(cli_info: CLIInfo): + def server(input: Inputs, output: Outputs, session: Session): # pragma: no cover + cli_csv_path = cli_info.csv_path + csv_path = reactive.value("" if cli_csv_path is None else cli_csv_path) contributions = reactive.value(cli_info.contributions) lower_bounds = reactive.value({}) upper_bounds = reactive.value({}) bin_counts = reactive.value({}) weights = reactive.value({}) - epsilon = reactive.value(1) + epsilon = reactive.value(1.0) dataset_panel.dataset_server( input, diff --git a/dp_wizard/app/analysis_panel.py b/dp_wizard/app/analysis_panel.py index 8465c1b..a0833e5 100644 --- a/dp_wizard/app/analysis_panel.py +++ b/dp_wizard/app/analysis_panel.py @@ -1,6 +1,7 @@ from math import pow +from typing import Iterable, Any -from shiny import ui, reactive, render, req +from shiny import ui, reactive, render, req, Inputs, Outputs, Session from dp_wizard.app.components.inputs import log_slider from dp_wizard.app.components.column_module import column_ui, column_server @@ -39,7 +40,9 @@ def analysis_ui(): ) -def _cleanup_reactive_dict(reactive_dict, keys_to_keep): # pragma: no cover +def _cleanup_reactive_dict( + reactive_dict: reactive.Value[dict[str, Any]], keys_to_keep: Iterable[str] +): # pragma: no cover reactive_dict_copy = {**reactive_dict()} keys_to_del = set(reactive_dict_copy.keys()) - set(keys_to_keep) for key in keys_to_del: @@ -48,17 +51,17 @@ def _cleanup_reactive_dict(reactive_dict, keys_to_keep): # pragma: no cover def analysis_server( - input, - output, - session, - csv_path, - contributions, - is_demo, - lower_bounds, - upper_bounds, - bin_counts, - weights, - epsilon, + input: Inputs, + output: Outputs, + session: Session, + csv_path: reactive.Value[str], + contributions: reactive.Value[int], + is_demo: bool, + lower_bounds: reactive.Value[dict[str, float]], + upper_bounds: reactive.Value[dict[str, float]], + bin_counts: reactive.Value[dict[str, int]], + weights: reactive.Value[dict[str, str]], + epsilon: reactive.Value[float], ): # pragma: no cover @reactive.calc def button_enabled(): @@ -133,7 +136,7 @@ def columns_ui(): """ ) ], - col_widths=col_widths, + col_widths=col_widths, # type: ignore ) if column_ids else [] diff --git a/dp_wizard/app/components/column_module.py b/dp_wizard/app/components/column_module.py index 7296943..4578887 100644 --- a/dp_wizard/app/components/column_module.py +++ b/dp_wizard/app/components/column_module.py @@ -1,6 +1,6 @@ from logging import info -from shiny import ui, render, module, reactive +from shiny import ui, render, module, reactive, Inputs, Outputs, Session from dp_wizard.utils.dp_helper import make_confidence_accuracy_histogram from dp_wizard.utils.shared import plot_histogram @@ -8,14 +8,14 @@ from dp_wizard.app.components.outputs import output_code_sample, demo_tooltip -default_weight = 2 +default_weight = "2" col_widths = { # Controls stay roughly a constant width; # Graph expands to fill space. - "sm": (4, 8), - "md": (3, 9), - "lg": (2, 10), + "sm": [4, 8], + "md": [3, 9], + "lg": [2, 10], } @@ -37,9 +37,9 @@ def column_ui(): # pragma: no cover "weight", ["Weight", ui.output_ui("weight_tooltip_ui")], choices={ - 1: "Less accurate", + "1": "Less accurate", default_weight: "Default", - 4: "More accurate", + "4": "More accurate", }, selected=default_weight, width=width, @@ -50,23 +50,23 @@ def column_ui(): # pragma: no cover # Make plot smaller than default: about the same size as the other column. output_code_sample("Column Definition", "column_code"), ], - col_widths=col_widths, + col_widths=col_widths, # type: ignore ) @module.server def column_server( - input, - output, - session, - name, - contributions, - epsilon, - lower_bounds, - upper_bounds, - bin_counts, - weights, - is_demo, + input: Inputs, + output: Outputs, + session: Session, + name: str, + contributions: int, + epsilon: float, + lower_bounds: reactive.Value[dict[str, float]], + upper_bounds: reactive.Value[dict[str, float]], + bin_counts: reactive.Value[dict[str, int]], + weights: reactive.Value[dict[str, str]], + is_demo: bool, ): # pragma: no cover @reactive.effect def _set_all_inputs(): @@ -74,7 +74,7 @@ def _set_all_inputs(): ui.update_numeric("lower", value=lower_bounds().get(name, 0)) ui.update_numeric("upper", value=upper_bounds().get(name, 10)) ui.update_numeric("bins", value=bin_counts().get(name, 10)) - ui.update_numeric("weight", value=weights().get(name, default_weight)) + ui.update_numeric("weight", value=int(weights().get(name, default_weight))) @reactive.effect @reactive.event(input.lower) @@ -89,12 +89,12 @@ def _set_upper(): @reactive.effect @reactive.event(input.bins) def _set_bins(): - bin_counts.set({**bin_counts(), name: float(input.bins())}) + bin_counts.set({**bin_counts(), name: int(input.bins())}) @reactive.effect @reactive.event(input.weight) def _set_weight(): - weights.set({**weights(), name: float(input.weight())}) + weights.set({**weights(), name: input.weight()}) @render.ui def bounds_tooltip_ui(): @@ -149,7 +149,7 @@ def column_plot(): upper_x = float(input.upper()) bin_count = int(input.bins()) weight = float(input.weight()) - weights_sum = sum(weights().values()) + weights_sum = sum(float(weight) for weight in weights().values()) info(f"Weight ratio for {name}: {weight}/{weights_sum}") if weights_sum == 0: # This function is triggered when column is removed; diff --git a/dp_wizard/app/components/inputs.py b/dp_wizard/app/components/inputs.py index bb95c96..4ef2b49 100644 --- a/dp_wizard/app/components/inputs.py +++ b/dp_wizard/app/components/inputs.py @@ -2,7 +2,7 @@ from shiny import ui -def log_slider(id, lower, upper): +def log_slider(id: str, lower: float, upper: float): # Rather than engineer a new widget, hide the numbers we don't want. # The rendered widget doesn't have a unique ID, but the following # element does, so we can use some fancy CSS to get the preceding element. diff --git a/dp_wizard/app/components/outputs.py b/dp_wizard/app/components/outputs.py index a66e2e2..96d9d9f 100644 --- a/dp_wizard/app/components/outputs.py +++ b/dp_wizard/app/components/outputs.py @@ -3,14 +3,14 @@ from faicons import icon_svg -def output_code_sample(title, name_of_render_function): +def output_code_sample(title: str, name_of_render_function: str): return details( summary(f"Code sample: {title}"), ui.output_code(name_of_render_function), ) -def demo_tooltip(is_demo, text): # pragma: no cover +def demo_tooltip(is_demo: bool, text: str): # pragma: no cover if is_demo: return ui.tooltip( icon_svg("circle-question"), diff --git a/dp_wizard/app/dataset_panel.py b/dp_wizard/app/dataset_panel.py index 9cd9836..cdfc9ae 100644 --- a/dp_wizard/app/dataset_panel.py +++ b/dp_wizard/app/dataset_panel.py @@ -1,6 +1,6 @@ from pathlib import Path -from shiny import ui, reactive, render +from shiny import ui, reactive, render, Inputs, Outputs, Session from dp_wizard.utils.argparse_helpers import get_cli_info from dp_wizard.app.components.outputs import output_code_sample, demo_tooltip @@ -9,9 +9,7 @@ def dataset_ui(): cli_info = get_cli_info() - csv_placeholder = ( - None if cli_info.csv_path is None else Path(cli_info.csv_path).name - ) + csv_placeholder = "" if cli_info.csv_path is None else Path(cli_info.csv_path).name return ui.nav_panel( "Select Dataset", @@ -41,7 +39,12 @@ def dataset_ui(): def dataset_server( - input, output, session, csv_path=None, contributions=None, is_demo=None + input: Inputs, + output: Outputs, + session: Session, + csv_path: reactive.Value[str], + contributions: reactive.Value[int], + is_demo: bool, ): # pragma: no cover @reactive.effect @reactive.event(input.csv_path) diff --git a/dp_wizard/app/feedback_panel.py b/dp_wizard/app/feedback_panel.py index 4084d44..75c1722 100644 --- a/dp_wizard/app/feedback_panel.py +++ b/dp_wizard/app/feedback_panel.py @@ -1,4 +1,4 @@ -from shiny import ui +from shiny import ui, Inputs, Outputs, Session from htmltools import HTML @@ -25,8 +25,8 @@ def feedback_ui(): def feedback_server( - input, - output, - session, + input: Inputs, + output: Outputs, + session: Session, ): # pragma: no cover pass diff --git a/dp_wizard/app/results_panel.py b/dp_wizard/app/results_panel.py index 8d448c5..3f69950 100644 --- a/dp_wizard/app/results_panel.py +++ b/dp_wizard/app/results_panel.py @@ -1,4 +1,4 @@ -from shiny import ui, render, reactive +from shiny import ui, render, reactive, Inputs, Outputs, Session from dp_wizard.utils.code_generators import ( NotebookGenerator, @@ -26,16 +26,16 @@ def results_ui(): def results_server( - input, - output, - session, - csv_path, - contributions, - lower_bounds, - upper_bounds, - bin_counts, - weights, - epsilon, + input: Inputs, + output: Outputs, + session: Session, + csv_path: reactive.Value[str], + contributions: reactive.Value[int], + lower_bounds: reactive.Value[dict[str, float]], + upper_bounds: reactive.Value[dict[str, float]], + bin_counts: reactive.Value[dict[str, int]], + weights: reactive.Value[dict[str, str]], + epsilon: reactive.Value[float], ): # pragma: no cover @reactive.calc def analysis_plan() -> AnalysisPlan: diff --git a/dp_wizard/utils/argparse_helpers.py b/dp_wizard/utils/argparse_helpers.py index 36bf603..149b05b 100644 --- a/dp_wizard/utils/argparse_helpers.py +++ b/dp_wizard/utils/argparse_helpers.py @@ -4,10 +4,10 @@ import csv import random from warnings import warn -from collections import namedtuple +from typing import NamedTuple, Optional -def _existing_csv_type(arg): +def _existing_csv_type(arg: str) -> Path: path = Path(arg) if not path.exists(): raise ArgumentTypeError(f"No such file: {arg}") @@ -54,7 +54,7 @@ def _get_args(): return arg_parser.parse_args() # pragma: no cover -def _clip(n, lower, upper): +def _clip(n: float, lower: float, upper: float) -> float: """ >>> _clip(-5, 0, 10) 0 @@ -66,7 +66,13 @@ def _clip(n, lower, upper): return max(min(n, upper), lower) -def _get_demo_csv_contrib(): +class CLIInfo(NamedTuple): + csv_path: Optional[str] + contributions: int + is_demo: bool + + +def _get_demo_csv_contrib() -> CLIInfo: """ >>> csv_path, contributions, is_demo = _get_demo_csv_contrib() >>> with open(csv_path, newline="") as csv_handle: @@ -103,10 +109,7 @@ def _get_demo_csv_contrib(): } ) - return CLIInfo(csv_path=csv_path, contributions=contributions, is_demo=True) - - -CLIInfo = namedtuple("CLIInfo", ["csv_path", "contributions", "is_demo"]) + return CLIInfo(csv_path=str(csv_path), contributions=contributions, is_demo=True) def get_cli_info(): # pragma: no cover diff --git a/dp_wizard/utils/code_generators/__init__.py b/dp_wizard/utils/code_generators/__init__.py index 521c4a4..a38478e 100644 --- a/dp_wizard/utils/code_generators/__init__.py +++ b/dp_wizard/utils/code_generators/__init__.py @@ -1,4 +1,4 @@ -from typing import NamedTuple +from typing import NamedTuple, Optional, Iterable from abc import ABC, abstractmethod from pathlib import Path import re @@ -14,21 +14,23 @@ class AnalysisPlanColumn(NamedTuple): class AnalysisPlan(NamedTuple): - csv_path: str + csv_path: Optional[str] contributions: int epsilon: float columns: dict[str, AnalysisPlanColumn] class _CodeGenerator(ABC): - def __init__(self, analysis_plan): + root_template = "placeholder" + + def __init__(self, analysis_plan: AnalysisPlan): self.csv_path = analysis_plan.csv_path self.contributions = analysis_plan.contributions self.epsilon = analysis_plan.epsilon self.columns = analysis_plan.columns @abstractmethod - def _make_context(self): ... # pragma: no cover + def _make_context(self) -> str: ... # pragma: no cover def make_py(self): return str( @@ -40,7 +42,7 @@ def make_py(self): ) ) - def _make_margins_dict(self, bin_names): + def _make_margins_dict(self, bin_names: Iterable[str]): # TODO: Don't worry too much about the formatting here. # Plan to run the output through black for consistency. # https://github.com/opendp/dp-creator-ii/issues/50 @@ -63,7 +65,7 @@ def _make_margins_dict(self, bin_names): margins_dict = "{" + "".join(margins) + "\n }" return margins_dict - def _make_columns(self, columns): + def _make_columns(self, columns: dict[str, AnalysisPlanColumn]): return "\n".join( make_column_config_block( name=name, @@ -74,7 +76,7 @@ def _make_columns(self, columns): for name, col in columns.items() ) - def _make_queries(self, column_names): + def _make_queries(self, column_names: Iterable[str]): return "confidence = 0.95\n\n" + "\n".join( _make_query(column_name) for column_name in column_names ) @@ -120,15 +122,17 @@ def _make_context(self): # These do not require an entire analysis plan, so they stand on their own. -def make_privacy_unit_block(contributions): +def make_privacy_unit_block(contributions: int): return str(Template("privacy_unit").fill_values(CONTRIBUTIONS=contributions)) -def make_privacy_loss_block(epsilon): +def make_privacy_loss_block(epsilon: float): return str(Template("privacy_loss").fill_values(EPSILON=epsilon)) -def make_column_config_block(name, lower_bound, upper_bound, bin_count): +def make_column_config_block( + name: str, lower_bound: float, upper_bound: float, bin_count: int +): """ >>> print(make_column_config_block( ... name="HW GRADE", diff --git a/dp_wizard/utils/converters.py b/dp_wizard/utils/converters.py index 7feaae1..4c6f004 100644 --- a/dp_wizard/utils/converters.py +++ b/dp_wizard/utils/converters.py @@ -3,7 +3,7 @@ import subprocess -def convert_py_to_nb(python_str, execute=False): +def convert_py_to_nb(python_str: str, execute: bool = False): """ Given Python code as a string, returns a notebook as a string. Calls jupytext as a subprocess: diff --git a/dp_wizard/utils/csv_helper.py b/dp_wizard/utils/csv_helper.py index b7b92dd..edc822a 100644 --- a/dp_wizard/utils/csv_helper.py +++ b/dp_wizard/utils/csv_helper.py @@ -10,7 +10,7 @@ import polars as pl -def read_csv_names(csv_path): +def read_csv_names(csv_path: str): # Polars is overkill, but it is more robust against # variations in encoding than Python stdlib csv. # However, it could be slow: @@ -21,22 +21,22 @@ def read_csv_names(csv_path): return lf.collect_schema().names() -def read_csv_ids_labels(csv_path): +def read_csv_ids_labels(csv_path: str): return { name_to_id(name): f"{i+1}: {name or '[blank]'}" for i, name in enumerate(read_csv_names(csv_path)) } -def read_csv_ids_names(csv_path): +def read_csv_ids_names(csv_path: str): return {name_to_id(name): name for name in read_csv_names(csv_path)} -def name_to_id(name): +def name_to_id(name: str): # Shiny is fussy about module IDs, # but we don't need them to be human readable. return str(hash(name)).replace("-", "_") -def name_to_identifier(name): +def name_to_identifier(name: str): return re.sub(r"\W+", "_", name).lower() diff --git a/dp_wizard/utils/dp_helper.py b/dp_wizard/utils/dp_helper.py index f7ec1d9..25fae1e 100644 --- a/dp_wizard/utils/dp_helper.py +++ b/dp_wizard/utils/dp_helper.py @@ -1,3 +1,5 @@ +from typing import Any + import polars as pl import opendp.prelude as dp @@ -8,8 +10,12 @@ def make_confidence_accuracy_histogram( - lower=None, upper=None, bin_count=None, contributions=None, weighted_epsilon=None -): + lower: float, + upper: float, + bin_count: int, + contributions: int, + weighted_epsilon: float, +) -> tuple[float, float, Any]: """ Creates fake data between lower and upper, and then returns a DP histogram from it. >>> confidence, accuracy, histogram = make_confidence_accuracy_histogram( @@ -60,15 +66,17 @@ def make_confidence_accuracy_histogram( ), split_by_weights=[1], margins={ - ("bin",): dp.polars.Margin( + ("bin",): dp.polars.Margin( # type: ignore max_partition_length=row_count, public_info="keys", ), }, ) - query = context.query().group_by("bin").agg(pl.len().dp.noise()) + query = context.query().group_by("bin").agg(pl.len().dp.noise()) # type: ignore confidence = 0.95 - accuracy = query.summarize(alpha=1 - confidence)["accuracy"].item() + accuracy = query.summarize(alpha=1 - confidence)["accuracy"].item() # type: ignore + # The sort is alphabetical. df_to_columns needs to be used + # downstream to parse interval and sort by numeric value. histogram = query.release().collect().sort("bin") return (confidence, accuracy, histogram) diff --git a/dp_wizard/utils/mock_data.py b/dp_wizard/utils/mock_data.py index bfdd52c..3345c5f 100644 --- a/dp_wizard/utils/mock_data.py +++ b/dp_wizard/utils/mock_data.py @@ -1,11 +1,14 @@ -from collections import namedtuple +from typing import NamedTuple import polars as pl -from scipy.stats import norm # type: ignore +from scipy.stats import norm -ColumnDef = namedtuple("ColumnDef", ["lower", "upper"]) +class ColumnDef(NamedTuple): + lower: float + upper: float -def mock_data(column_defs, row_count=1000): + +def mock_data(column_defs: dict[str, ColumnDef], row_count: int = 1000): """ Return values from the inverse CDF of a normal distribution, so in the preview the only noise is from DP, diff --git a/dp_wizard/utils/shared.py b/dp_wizard/utils/shared.py index 75719ff..b6cc03a 100644 --- a/dp_wizard/utils/shared.py +++ b/dp_wizard/utils/shared.py @@ -1,7 +1,8 @@ # These functions are used both in the application and in generated notebooks. +from polars import DataFrame -def make_cut_points(lower_bound, upper_bound, bin_count): +def make_cut_points(lower_bound: float, upper_bound: float, bin_count: int): """ Returns one more cut point than the bin_count. (There are actually two more bins, extending to @@ -14,7 +15,7 @@ def make_cut_points(lower_bound, upper_bound, bin_count): return [round(lower_bound + i * bin_width, 2) for i in range(bin_count + 1)] -def interval_bottom(interval): +def interval_bottom(interval: str): """ >>> interval_bottom("(10, 20]") 10.0 @@ -22,7 +23,7 @@ def interval_bottom(interval): return float(interval.split(",")[0][1:]) -def df_to_columns(df): +def df_to_columns(df: DataFrame): """ Transform a Dataframe into a format that is easier to plot, parsing the interval strings to sort them as numbers. @@ -38,7 +39,9 @@ def df_to_columns(df): return tuple(zip(*sorted_rows)) -def plot_histogram(histogram_df, error, cutoff): # pragma: no cover +def plot_histogram( + histogram_df: DataFrame, error: float, cutoff: float +): # pragma: no cover """ Given a Dataframe for a histogram, plot the data. """ diff --git a/pyproject.toml b/pyproject.toml index bfcef5b..02a60db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,3 +25,7 @@ dp-wizard = "dp_wizard:main" [project.urls] Home = "https://github.com/opendp/dp-wizard" + +[tool.pyright] +include = ["dp_wizard"] +ignore = ["**/no-tests/"] diff --git a/requirements-dev.in b/requirements-dev.in index a0ffe05..11b7872 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -12,7 +12,7 @@ pre-commit # Testing: pytest pytest-playwright -mypy +pyright coverage diff --git a/requirements-dev.txt b/requirements-dev.txt index 7bbe11f..6eb4a52 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -154,12 +154,8 @@ mdurl==0.1.2 # via markdown-it-py mistune==3.0.2 # via nbconvert -mypy==1.12.0 - # via -r requirements-dev.in mypy-extensions==1.0.0 - # via - # black - # mypy + # via black nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -172,7 +168,9 @@ nbformat==5.10.4 nest-asyncio==1.6.0 # via ipykernel nodeenv==1.9.1 - # via pre-commit + # via + # pre-commit + # pyright numpy==1.26.4 # via # contourpy @@ -249,6 +247,8 @@ pyproject-hooks==1.2.0 # via # build # pip-tools +pyright==1.1.389 + # via -r requirements-dev.in pytest==8.3.3 # via # -r requirements-dev.in @@ -337,8 +337,8 @@ traitlets==5.14.3 typing-extensions==4.12.2 # via # htmltools - # mypy # pyee + # pyright # shiny uc-micro-py==1.0.3 # via linkify-it-py diff --git a/tests/utils/test_misc.py b/tests/utils/test_misc.py index d7cbede..dee4ba9 100644 --- a/tests/utils/test_misc.py +++ b/tests/utils/test_misc.py @@ -4,11 +4,11 @@ tests = { "flake8 linting": "flake8 . --count --show-source --statistics", - "mypy type checking": "mypy .", + "pyright type checking": "pyright", } @pytest.mark.parametrize("cmd", tests.values(), ids=tests.keys()) -def test_subprocess(cmd): +def test_subprocess(cmd: str): result = subprocess.run(cmd, shell=True) assert result.returncode == 0, f'"{cmd}" failed'