Skip to content
Merged
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
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,20 @@ jobs:
python -m pip install --upgrade pip
pip install pre-commit

- name: Run pre-commit (black + ruff + mypy)
- name: Run pre-commit (black + ruff)
run: pre-commit run --all-files

# mypy is deliberately NOT run through the pre-commit step above.
# M-TYPECHECK TYPE.2: the mypy hook is pinned `stages: [pre-push]` (kept
# off every commit — it's slow) and `pre-commit run --all-files` only
# runs default-stage hooks, so it silently skipped mypy here for months
# while this job's name and the branch-protection check both claimed
# type checking was happening. A `--hook-stage pre-push` flag would fix
# today's instance but not the class of bug: the next hook to acquire an
# unusual `stages:` pin would vanish from CI the same way, silently. An
# explicit step that runs mypy directly has no stage-filtering machinery
# to hide behind.
- name: Type check (mypy)
run: |
pip install "mypy~=1.10.0" numpy types-requests
mypy --ignore-missing-imports quantui/
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,11 @@ dev = [
"pytest-cov>=4.0.0",
"pytest-mock>=3.10.0",
"pytest-xdist>=3.0.0", # parallel test execution (-n=auto in addopts)
"mypy>=1.0.0",
"mypy~=1.10.0", # pinned, not an open floor — same reasoning as
# black/ruff below: it must agree with .pre-commit-config.yaml's rev,
# which is what CI enforces (M-TYPECHECK TYPE.2). A newer mypy silently
# disagrees: 2.x drops support for python_version = "3.9" (this repo's
# floor) and reports a different error set than the pinned check.
"types-requests>=2.28.0",
# Formatter/linter versions are pinned to a compatible range, NOT an open
# floor: they must agree with the revs in .pre-commit-config.yaml, which is
Expand Down
44 changes: 28 additions & 16 deletions quantui/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from typing import Optional, cast

from quantui.calc_log import _log_dir, get_perf_history, get_prediction_history

Expand Down Expand Up @@ -280,11 +280,17 @@ def _bar_chart_html(
margin=dict(l=40, r=20, t=10, b=40),
plot_bgcolor="#ffffff",
)
return pio.to_html(
fig,
include_plotlyjs="inline" if include_plotlyjs else False,
full_html=False,
config={"displayModeBar": False},
# plotly has no bundled type stubs; --ignore-missing-imports leaves
# pio.to_html untyped. It genuinely returns str (verified: this
# function's other returns are explicit None for the no-data case).
return cast(
str,
pio.to_html(
fig,
include_plotlyjs="inline" if include_plotlyjs else False,
full_html=False,
config={"displayModeBar": False},
),
)


Expand Down Expand Up @@ -344,11 +350,14 @@ def _timeline_html(records: list[dict], *, include_plotlyjs: bool) -> Optional[s
plot_bgcolor="#ffffff",
legend=dict(orientation="h", x=0, y=1.05),
)
return pio.to_html(
fig,
include_plotlyjs="inline" if include_plotlyjs else False,
full_html=False,
config={"displayModeBar": False},
return cast(
str,
pio.to_html(
fig,
include_plotlyjs="inline" if include_plotlyjs else False,
full_html=False,
config={"displayModeBar": False},
),
)


Expand Down Expand Up @@ -452,11 +461,14 @@ def _prediction_scatter_html(
plot_bgcolor="#ffffff",
legend=dict(orientation="h", x=0, y=1.05),
)
return pio.to_html(
fig,
include_plotlyjs="inline" if include_plotlyjs else False,
full_html=False,
config={"displayModeBar": False},
return cast(
str,
pio.to_html(
fig,
include_plotlyjs="inline" if include_plotlyjs else False,
full_html=False,
config={"displayModeBar": False},
),
)


Expand Down
56 changes: 50 additions & 6 deletions quantui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import uuid as _uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Literal, Optional
from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Literal, Optional, cast

import ipywidgets as widgets
from IPython import get_ipython
Expand Down Expand Up @@ -1025,6 +1025,8 @@ class QuantUIApp:
_clear_log_cache_confirm_btn: Any
_exit_btn: Any
_exit_output: Any
_exit_cancel_btn: Any
_exit_warn_html: Any
_help_btn: Any
_issue_btn: Any
_issue_cancel_btn: Any
Expand All @@ -1039,6 +1041,7 @@ class QuantUIApp:
_cal_run_btn: Any
_cal_step_label: Any
_cal_stop_btn: Any
_cal_skip_btn: Any
_log_clear_btn: Any
_log_output_html: Any
_log_source_lbl: Any
Expand All @@ -1062,6 +1065,7 @@ class QuantUIApp:
_status_tab_panel: Any
_theme_style: Any
_welcome_html: Any
_welcome_header: Any
_activity_btn: Any
advanced_accordion: Any
calc_setup_panel: Any
Expand Down Expand Up @@ -1102,6 +1106,8 @@ class QuantUIApp:
history_basis_dd: Any
history_date_from: Any
history_date_to: Any
_history_calc_chips: Any
_history_status_chips: Any
lib_category_dd: Any
lib_search_txt: Any
lib_results_dd: Any
Expand All @@ -1115,6 +1121,7 @@ class QuantUIApp:
results_path_lbl: Any
run_btn: Any
cancel_btn: Any
basis_fix_btn: Any
run_output: Any
run_panel: Any
run_status: Any
Expand Down Expand Up @@ -1145,6 +1152,7 @@ class QuantUIApp:
_freq_seed_dd: Any
_freq_seed_note: Any
_freq_seed_refresh_btn: Any
_tddft_seed_dd: Any
_go_analysis_btn: Any
_go_results_btn: Any
_ir_export_btn: Any
Expand All @@ -1154,8 +1162,15 @@ class QuantUIApp:
_ir_fwhm_slider: Any
_ir_mode_toggle: Any
_ir_accordion: Any
_ir_copy_data_btn: Any
_iso_accordion: Any
_iso_generate_btn: Any
_iso_cancel_btn: Any
_iso_colors_dd: Any
_iso_export_cube_btn: Any
_iso_isovalue_slider: Any
_iso_opacity_slider: Any
_iso_resolution_dd: Any
_last_result_dir: Any
_nmr_accordion: Any
_nmr_output: Any
Expand All @@ -1165,17 +1180,25 @@ class QuantUIApp:
_orb_export_btn: Any
_orb_export_fmt_dd: Any
_orb_export_status: Any
_orb_copy_data_btn: Any
_orb_iso_controls: Any
_orb_iso_output: Any
_orb_n_orb_input: Any
_orb_index_input: Any
_orb_png_inbox: Any
_orb_toggle: Any
_orb_ymax_input: Any
_orb_ymin_input: Any
_pes_export_btn: Any
_pes_export_fmt_dd: Any
_pes_export_status: Any
_pes_copy_data_btn: Any
_pes_plot_html: Any
_pes_scan_accordion: Any
_reorg_view_toggle: Any
_reorg_overlay_pair: Any
_reorg_exaggerate: Any
_reorg_mode_dd: Any
_result_dir_label: Any
_result_log_accordion: Any
_result_log_output: Any
Expand All @@ -1194,6 +1217,7 @@ class QuantUIApp:
_uv_export_btn: Any
_uv_export_fmt_dd: Any
_uv_export_status: Any
_uv_copy_data_btn: Any
_uv_fwhm_slider: Any
_uv_mode_toggle: Any
_to_analysis_btn: Any
Expand Down Expand Up @@ -1222,6 +1246,7 @@ class QuantUIApp:
export_pdb_btn: Any
export_status: Any
export_xyz_btn: Any
_export_bundle_btn: Any
fmax_fi: Any
log_clear_btn: Any
max_steps_si: Any
Expand All @@ -1234,6 +1259,13 @@ class QuantUIApp:
_basis_card_html: Any
_descriptor_cards_box: Any
_open_shell_hint: Any
spin_metal_dd: Any
spin_ox_si: Any
spin_geom_dd: Any
spin_suggest_btn: Any
spin_helper_output: Any
spin_apply_btns: Any
spin_helper_box: Any
nstates_si: Any
perf_estimate_html: Any
post_calc_panel: Any
Expand All @@ -1252,6 +1284,11 @@ class QuantUIApp:
vib_accordion: Any
vib_mode_dd: Any
vib_output: Any
vib_prev_btn: Any
vib_next_btn: Any
_vib_export_btn: Any
_vib_export_status: Any
_last_vib_molecule: Any

def __init__(self) -> None:
# ── Instance state ────────────────────────────────────────────────
Expand Down Expand Up @@ -2610,15 +2647,15 @@ def _preview_file_path(self, path: Path) -> None:

if suffix in {".html", ".htm"}:
try:
raw = path.read_text(encoding="utf-8", errors="replace")
if len(raw) <= 1_000_000:
html_text = path.read_text(encoding="utf-8", errors="replace")
if len(html_text) <= 1_000_000:
# Sandboxed iframe via srcdoc — embedded JS can't
# reach the parent app.
iframe_html = (
'<iframe sandbox="allow-scripts" '
f'style="width:100%;height:400px;border:1px solid {_theme.BORDER};'
'border-radius:4px" '
f'srcdoc="{_html.escape(raw, quote=True)}"></iframe>'
f'srcdoc="{_html.escape(html_text, quote=True)}"></iframe>'
)
with self._files_preview_output:
display(HTML(iframe_html))
Expand Down Expand Up @@ -3090,7 +3127,11 @@ def _rerender_3d_views(self) -> None:
# toggled backends on the Analysis tab.
html = _render_molecule_html(
self._analysis_displayed_molecule,
backend=str(chosen),
# VizBackend is a StrEnum whose only members are
# "py3dmol"/"plotlymol", a subset of render_molecule_html's
# accepted Literal — cast documents that, str() alone widens
# to plain str for mypy.
backend=cast(Literal["auto", "py3dmol", "plotlymol"], str(chosen)),
style=self._viz_style,
lighting=self._viz_lighting,
bgcolor=self._plotly_theme_colors()["scene_bgcolor"],
Expand Down Expand Up @@ -3151,7 +3192,10 @@ def _on_gpu_enabled_changed(self, change) -> None:
try:
from quantui.gpu_offload import is_gpu_available, probe_gpu

is_gpu_available.cache_clear()
# cache_clear is forwarded from _probe_gpu's lru_cache onto this
# function at definition time (gpu_offload.py); mypy can't see a
# monkey-patched attribute across the module boundary.
is_gpu_available.cache_clear() # type: ignore[attr-defined]
state = probe_gpu()
except Exception: # noqa: BLE001 — a probe failure must not break the UI
state = (False, None, "")
Expand Down
2 changes: 1 addition & 1 deletion quantui/app_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ def build_shared_widgets(
)
for _ in range(2)
)
app._spin_suggested_mults: list = []
app._spin_suggested_mults = []
app.spin_helper_box = widgets.Accordion(
children=[
widgets.VBox(
Expand Down
6 changes: 3 additions & 3 deletions quantui/app_runflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import threading
import time
from typing import Any, Optional
from typing import Any, Dict, Optional

import ipywidgets as widgets
from IPython.display import HTML, Javascript, display
Expand Down Expand Up @@ -1563,7 +1563,7 @@ def _update_open_shell_hint(app: Any) -> None:
#: Calculate-tab dropdown label → the key used in perf records, saved
#: results, and checkpoint identities. One mapping, so the estimator and the
#: checkpoint layer can never disagree about what calculation is configured.
_CALC_TYPE_KEYS: dict = {
_CALC_TYPE_KEYS: Dict[str, str] = {
"Single Point": "single_point",
"Geometry Opt": "geometry_opt",
"Frequency": "frequency",
Expand Down Expand Up @@ -1708,7 +1708,7 @@ def _hide() -> None:
#: Inverse of ``_CALC_TYPE_KEYS`` — a stored calc-type key back to the label
#: the Calculate-tab dropdown actually uses. Derived rather than written out
#: twice, so the two can never drift apart.
_CALC_TYPE_LABELS: dict = {v: k for k, v in _CALC_TYPE_KEYS.items()}
_CALC_TYPE_LABELS: Dict[str, str] = {v: k for k, v in _CALC_TYPE_KEYS.items()}


def _age_phrase(updated_at: Any) -> str:
Expand Down
6 changes: 4 additions & 2 deletions quantui/app_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Any, List
from typing import Any, List, cast

import ipywidgets as widgets
from IPython.display import HTML, display
Expand Down Expand Up @@ -2541,7 +2541,9 @@ def _xyz(g: dict) -> str:

view.setBackgroundColor(bgcolor)
view.zoomTo()
view_html = view._make_html()
# py3Dmol has no type stubs (ignore_missing_imports); _make_html()
# genuinely returns str.
view_html = cast(str, view._make_html())
if re.search(r"3dmolviewer_(\w+)", view_html) is None:
return view_html

Expand Down
Loading