1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
|
"""Context manager for controlling global state variables."""
from __future__ import annotations
from abc import ABC
from abc import abstractmethod
import contextlib
from typing import TYPE_CHECKING
from typing import Generic
from typing import Literal
from typing import TypeVar
from typing import cast
from typing import final
from typing import get_args
from typing import overload
from pyvista.core import _vtk_core as _vtk
if TYPE_CHECKING:
from typing_extensions import Self
T = TypeVar('T')
class _StateManager(contextlib.AbstractContextManager[None], ABC, Generic[T]):
"""Abstract base class for managing a global state variable.
Subclasses must:
- Specify a `Literal` as the subclass' type argument. The literal's
arguments must specify all allowable options for the state variable.
- Define a getter and setter for the state. Input validation is not
required - the input is automatically validated when setting the state.
Examples
--------
>>> from pyvista.core.utilities.state_manager import _StateManager
>>> from typing import Literal
Define the available options as a ``Literal`` and initialize a global state variable.
>>> _StateOptions = Literal['on', 'off']
>>> _GLOBAL_STATE = ['off'] # Init global state. Use list to make it mutable.
Define the class and its state property.
>>> class MyState(_StateManager[_StateOptions]):
... @property
... def _state(self) -> _StateOptions:
... return _GLOBAL_STATE[0]
...
... @_state.setter
... def _state(self, state: _StateOptions) -> None:
... _GLOBAL_STATE[0] = state
Finally, create an instance of the state manager.
>>> my_state = MyState()
Get the state.
>>> my_state()
'off'
Set the state.
>>> _ = my_state('on')
>>> my_state()
'on'
Use it as a context manager to set the state temporarily:
>>> with my_state('off'):
... pass
"""
@classmethod
def _get_state_options_from_literal(cls) -> tuple[str | int | bool]:
state_manager_fullname = f'{_StateManager.__module__}.{_StateManager.__name__}'
for base in getattr(cls, '__orig_bases__', ()):
if str(base).startswith(state_manager_fullname):
# Get StateManager's typing args
state_manager_args = get_args(base)
if len(state_manager_args) == 1:
# There must only be one arg and it must be a non-empty Literal
literal = state_manager_args[0]
if str(literal).startswith('typing.Literal'):
args = get_args(literal)
if len(args) >= 1:
return args
msg = (
'Type argument for subclasses must be a single non-empty Literal with all state '
'options provided.'
)
raise TypeError(msg)
def __init__(self) -> None:
"""Initialize context manager."""
self._valid_states = self._get_state_options_from_literal()
self._original_state: T | None = None
@property
@abstractmethod
def _state(self) -> T:
"""Get the current global state."""
@_state.setter
@abstractmethod
def _state(self, state: T) -> None:
"""Set the global state."""
@final
def _validate_state(self, state: T) -> T:
from pyvista import _validation # noqa: PLC0415
_validation.check_contains(self._valid_states, must_contain=state, name='state')
return state
def __enter__(self) -> None:
"""Enter context manager."""
if self._original_state is None:
msg = 'State must be set before using it as a context manager.'
raise ValueError(msg)
def __exit__(self, exc_type, exc_value, traceback): # noqa: ANN001, ANN204
"""Exit context manager and restore original state."""
self._state = cast('T', self._original_state)
self._original_state = None # Reset
@overload
def __call__(self: Self, state: None) -> T: ...
@overload
def __call__(self: Self, state: T) -> Self: ...
def __call__(self: Self, state: T | None = None) -> Self | T:
"""Call the context manager."""
if state is None:
return self._state
self._validate_state(state)
# Create new instance and store the local state to be restored when exiting
output = self.__class__()
output._original_state = self._state
output._state = state
return output
_VerbosityOptions = Literal[
'off',
'error',
'warning',
'info',
'max',
]
class _VTKVerbosity(_StateManager[_VerbosityOptions]):
"""Context manager to set VTK verbosity level.
.. versionadded:: 0.45
Parameters
----------
verbosity : str
Verbosity of the :vtk:`vtkLogger` to set.
- ``'off'``: No output.
- ``'error'``: Only error messages.
- ``'warning'``: Errors and warnings.
- ``'info'``: Errors, warnings, and info messages.
- ``'max'``: All messages, including debug info.
Examples
--------
Get the current vtk verbosity.
>>> import pyvista as pv
>>> pv.vtk_verbosity()
'info'
Set verbosity to max.
>>> _ = pv.vtk_verbosity('max')
>>> pv.vtk_verbosity()
'max'
Create a :func:`~pyvista.Sphere`. Note how many VTK debugging messages are now
generated as the sphere is created.
>>> mesh = pv.Sphere()
Use it as a context manager to temporarily turn it off.
>>> with pv.vtk_verbosity('off'):
... mesh = mesh.cell_quality('volume')
The state is restored to its previous value outside the context.
>>> pv.vtk_verbosity()
'max'
Note that the verbosity state is global and will persist between function
calls. If the context manager isn't used, the state needs to be reset explicitly.
Here, we set it back to its default value.
>>> _ = pv.vtk_verbosity('info')
"""
@property
def _state(self) -> _VerbosityOptions:
int_to_string: dict[int, _VerbosityOptions] = {
-9: 'off',
-2: 'error',
-1: 'warning',
0: 'info',
9: 'max',
}
state = _vtk.vtkLogger.GetCurrentVerbosityCutoff()
try:
return int_to_string[state]
except KeyError:
# Unsupported state, raise error using validation method
self._validate_state(state) # type: ignore[arg-type]
msg = 'This line should not be reachable.' # pragma: no cover
raise RuntimeWarning(msg) # pragma: no cover
@_state.setter
def _state(self, state: _VerbosityOptions) -> None:
verbosity_int = _vtk.vtkLogger.ConvertToVerbosity(state.upper())
_vtk.vtkLogger.SetStderrVerbosity(verbosity_int)
vtk_verbosity = _VTKVerbosity()
_VtkSnakeCaseOptions = Literal['allow', 'warning', 'error']
class _vtkSnakeCase(_StateManager[_VtkSnakeCaseOptions]): # noqa: N801
"""Context manager to control access to VTK's pythonic snake_case API.
VTK 9.4 introduced pythonic snake_case attributes, e.g. `output_port` instead
of `GetOutputPort`. These can easily be confused for PyVista attributes
which also use a snake_case convention. This class controls access to vtk's
new interface.
.. versionadded:: 0.45
Parameters
----------
state : 'allow' | 'warning' | 'error'
Allow or disallow the use of VTK's pythonic snake_case API with
PyVista-wrapped VTK classes.
- 'allow': Allow accessing VTK-defined snake_case attributes.
- 'warning': Print a RuntimeWarning when accessing VTK-defined snake_case
attributes.
- 'error': Raise a ``PyVistaAttributeError`` when accessing
VTK-defined snake_case attributes.
Examples
--------
Get the current access state for VTK's snake_case api.
>>> import pyvista as pv
>>> pv.vtk_snake_case()
'error'
The following will raise an error because the `information` property is defined
by :vtk:`vtkDataObject` and is not part of PyVista's API.
>>> # pv.PolyData().information
Allow use of VTK's snake_case attributes. No warning or error is raised.
>>> _ = pv.vtk_snake_case('allow')
>>> pv.PolyData().information
<vtkmodules.vtkCommonCore.vtkInformation...
Note that this state is global and will persist between function calls. Set it
back to its original state explicitly.
>>> _ = pv.vtk_snake_case('error')
Use it as a context manager instead. This way, the state is only temporarily
modified and is automatically restored.
>>> with pv.vtk_snake_case('allow'):
... _ = pv.PolyData().information
>>> pv.vtk_snake_case()
'error'
"""
@property
def _state(self) -> _VtkSnakeCaseOptions:
import pyvista as pv # noqa: PLC0415
return pv._VTK_SNAKE_CASE_STATE
@_state.setter
def _state(self, state: _VtkSnakeCaseOptions) -> None:
import pyvista as pv # noqa: PLC0415
pv._VTK_SNAKE_CASE_STATE = state
vtk_snake_case = _vtkSnakeCase()
|