MolSysMT Warning System — Developer Guide#
This document explains how to emit, categorize, filter, and test warnings in MolSysMT. It covers the recommended categories, parametrized warnings, helpers, configuration, deprecations, and testing patterns.
Target Python: 3.10+
1) Why custom warnings?#
We avoid noisy prefixes like
"[MolSysMT Warning]"
.The warning category itself communicates origin and intent (and is filterable by users).
2) Categories#
All categories inherit from UserMolSysMTWarning
:
# molsysmt/warnings.py (excerpt)
import warnings
from typing import Type, Iterable
class UserMolSysMTWarning(Warning):
# Base class for user-facing warnings in MolSysMT.
pass
class FileFormatWarning(UserMolSysMTWarning):
# Parsing/metadata/IO-related warnings.
pass
class SelectionWarning(UserMolSysMTWarning):
# Selection string / subset resolution warnings.
pass
class PerformanceWarning(UserMolSysMTWarning):
# Potentially expensive operations / degraded performance.
pass
class MolSysMTDeprecationWarning(DeprecationWarning):
# API deprecations for MolSysMT.
pass
3) Emitting warnings (two patterns)#
A. Simple message (most cases)#
Use warnings.warn
with an appropriate category:
import warnings
from molsysmt.warnings import FileFormatWarning
warnings.warn(
"Missing CRYST1 line; no box parsed.",
FileFormatWarning,
stacklevel=2, # points at the caller line
)
When to use: the message is a one-off string; you don’t need to format complex parameters repeatedly.
B. Parametrized warning class (centralized formatting)#
Define a warning that builds its message in __init__
:
from typing import Iterable
class TopologyWarning(UserMolSysMTWarning):
def __init__(
self,
n_bonds: int,
chains: Iterable[str] | None = None,
*,
source: str = "struct_conn",
context: str | None = None,
):
self.n_bonds = n_bonds
self.chains = tuple(chains) if chains else None
self.source = source
self.context = context
msg = f"{n_bonds} covalent bond(s) reported by '{source}'"
if self.chains:
msg += f" between chains {list(self.chains)}"
msg += " were added."
if self.context:
msg += f" (context: {self.context})"
super().__init__(msg)
Usage:
import warnings
from molsysmt.warnings import TopologyWarning
warnings.warn(TopologyWarning(n_bonds=3, chains=["A", "B"], context="read_cif()"), stacklevel=2)
When to use: the same warning is emitted in multiple places with varying parameters; centralizing formatting reduces duplication and keeps messages consistent.
4) Helpers: warn
and warn_once
(optional convenience)#
We provide thin wrappers to standardize stacklevel
and suppress duplication.
from typing import Type
__WARNED_ONCE_CACHE__: set[tuple[Type[Warning], str]] = set()
def warn(message_or_warning: str | Warning,
category: Type[Warning] | None = None,
*,
stacklevel: int = 2) -> None:
import warnings
if isinstance(message_or_warning, Warning):
warnings.warn(message_or_warning, stacklevel=stacklevel)
else:
warnings.warn(message_or_warning, category or UserMolSysMTWarning, stacklevel=stacklevel)
def warn_once(message_or_warning: str | Warning,
category: Type[Warning] | None = None,
*,
stacklevel: int = 2) -> None:
import warnings
if isinstance(message_or_warning, Warning):
msg, cat = str(message_or_warning), type(message_or_warning)
else:
msg, cat = message_or_warning, category or UserMolSysMTWarning
key = (cat, msg)
if key in __WARNED_ONCE_CACHE__:
return
__WARNED_ONCE_CACHE__.add(key)
warnings.warn(message_or_warning, cat, stacklevel=stacklevel)
Examples
from molsysmt.warnings import warn, warn_once, SelectionWarning
warn("Unknown tokens in selection; ignoring.", SelectionWarning)
for _ in range(100):
warn_once("Falling back to slow path; consider pre-alignment.", PerformanceWarning)
5) Choosing stacklevel
#
Use
stacklevel=2
so the traceback points to the caller (your API’s surface), not the internal utility emitting the warning.If you wrap warnings in deeper helpers, you may need
stacklevel=3+
. Verify with a quick run to ensure the location shown is helpful to end users.
6) Filtering and configuration (package & user)#
We don’t force filters in warnings.py
. Suggested defaults (opt-in) in molsysmt/__init__.py
:
# molsysmt/__init__.py
import warnings
from .warnings import UserMolSysMTWarning, MolSysMTDeprecationWarning
warnings.simplefilter("default", UserMolSysMTWarning) # show user warnings at least once
warnings.simplefilter("default", MolSysMTDeprecationWarning) # make deprecations visible
User-side control (in scripts or notebooks):
import warnings
from molsysmt.warnings import PerformanceWarning, TopologyWarning
warnings.simplefilter("ignore", PerformanceWarning) # silence performance warnings
warnings.simplefilter("always", TopologyWarning) # always show topology warnings
warnings.simplefilter("error", FileFormatWarning) # treat IO issues as errors
Common actions:
"ignore"
: hide a category"default"
: show first occurrence per location (Python default behavior)"once"
: show only once per process"always"
: show every time"error"
: convert to exceptions (useful in CI)
7) Deprecation policy#
Use MolSysMTDeprecationWarning
for API removals/renames. Provide guidance and a horizon:
import warnings
from molsysmt.warnings import MolSysMTDeprecationWarning
warnings.warn(
"Function `old_api()` is deprecated and will be removed in v2.0; use `new_api()` instead.",
MolSysMTDeprecationWarning,
stacklevel=2,
)
Guidelines
Include the alternative API.
Include a timeline (e.g., removal in vX.Y).
Consider gating with an environment variable for early adopters (optional).
8) Message style guide#
Actionable: say what happened and what to do, if applicable.
Compact: one sentence when possible; link or reference docs for details.
Context: include small context (e.g., file, function) when it helps.
No prefixes like
[MolSysMT Warning]
— the category is the origin.
Examples:
Good: “Missing CRYST1 line; no box parsed. (file: my.pdb)”
Good: “3 covalent bond(s) reported by ‘struct_conn’ between chains [‘A’, ‘B’] were added.”
Avoid: “[MolSysMT Warning] Something happened.”
9) Testing warnings (pytest)#
A. Expect a warning#
import pytest
import warnings
from molsysmt.warnings import FileFormatWarning
def test_missing_cryst1_emits_warning():
with pytest.warns(FileFormatWarning, match="Missing CRYST1"):
# call the function that should warn
parse_pdb("tests/data/no_cryst1.pdb")
B. No warning expected#
def test_clean_file_no_warning(recwarn):
parse_pdb("tests/data/clean.pdb")
assert not recwarn # no warnings captured
C. Convert to errors in CI (optional)#
In pytest.ini
:
[pytest]
filterwarnings =
error::molsysmt.warnings.FileFormatWarning
default::molsysmt.warnings.UserMolSysMTWarning
default::molsysmt.warnings.MolSysMTDeprecationWarning
Or in a specific test:
def test_treat_file_issues_as_errors(monkeypatch):
import warnings
from molsysmt.warnings import FileFormatWarning
warnings.simplefilter("error", FileFormatWarning)
with pytest.raises(FileFormatWarning):
parse_pdb("tests/data/bad_format.pdb")
10) Migration note (from UserWarning
+ text prefix)#
Before
warnings.warn("[MolSysMT Warning] Added covalent bonds from struct_conn", UserWarning)
After
from molsysmt.warnings import TopologyWarning
import warnings
warnings.warn(TopologyWarning(n_bonds=1, chains=["A", "B"]))
No textual prefix is needed; the category is now TopologyWarning
.
11) Quick reference (cheat sheet)#
Pick a category:
FileFormatWarning
,SelectionWarning
,PerformanceWarning
,TopologyWarning
,MolSysMTDeprecationWarning
.Emit:
warnings.warn("message", Category, stacklevel=2)
orwarnings.warn(CategoryClass(...), stacklevel=2)
.Once only:
warn_once("message", Category)
orwarn_once(CategoryClass(...))
.Filter:
warnings.simplefilter("ignore"|"default"|"once"|"always"|"error", Category)
.Test:
with pytest.warns(Category, match="..."):
.