Skip to content

Commit

Permalink
Add "no_run_validators()" context manager (#859)
Browse files Browse the repository at this point in the history
* Add "no_run_validators()" context manager

* Move functions to validators module

* Update changelog entry

* Add a few docstring improvements and fixes

* Update tests/test_validators.py

* Minor polish

Signed-off-by: Hynek Schlawack <hs@ox.cx>

Co-authored-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
sscherfke and hynek committed Nov 17, 2021
1 parent c4587c0 commit c6143d5
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 3 deletions.
4 changes: 4 additions & 0 deletions changelog.d/859.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added new context manager ``attr.validators.disabled()`` and functions ``attr.validators.(set|get)_disabled()``.
They deprecate ``attr.(set|get)_run_validators()``.
All functions are interoperable and modify the same internal state.
They are not – and never were – thread-safe, though.
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,14 @@ Validators
...
TypeError: ("'x' must be <class 'str'> (got 7 that is a <class 'int'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping <instance_of validator for type <class 'str'>> to <instance_of validator for type <class 'int'>>>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'str'>, 7)

Validators can be both globally and locally disabled:

.. autofunction:: attr.validators.set_disabled

.. autofunction:: attr.validators.get_disabled

.. autofunction:: attr.validators.disabled


Converters
----------
Expand Down
14 changes: 12 additions & 2 deletions docs/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,20 @@ If you define validators both ways for an attribute, they are both ran:

And finally you can disable validators globally:

>>> attr.set_run_validators(False)
>>> attr.validators.set_disabled(True)
>>> C("128")
C(x='128')
>>> attr.set_run_validators(True)
>>> attr.validators.set_disabled(False)
>>> C("128")
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128')

You can achieve the same by using the context manager:

>>> with attr.validators.disabled():
... C("128")
C(x='128')
>>> C("128")
Traceback (most recent call last):
...
Expand Down
8 changes: 8 additions & 0 deletions src/attr/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
def set_run_validators(run):
"""
Set whether or not validators are run. By default, they are run.
.. deprecated:: 21.3.0 It will not be removed, but it also will not be
moved to new ``attrs`` namespace. Use `attr.validators.set_disabled()`
instead.
"""
if not isinstance(run, bool):
raise TypeError("'run' must be bool.")
Expand All @@ -19,5 +23,9 @@ def set_run_validators(run):
def get_run_validators():
"""
Return whether or not validators are run.
.. deprecated:: 21.3.0 It will not be removed, but it also will not be
moved to new ``attrs`` namespace. Use `attr.validators.get_disabled()`
instead.
"""
return _run_validators
54 changes: 54 additions & 0 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import operator
import re

from contextlib import contextmanager

from ._config import get_run_validators, set_run_validators
from ._make import _AndValidator, and_, attrib, attrs
from .exceptions import NotCallableError

Expand All @@ -15,7 +18,9 @@
"and_",
"deep_iterable",
"deep_mapping",
"disabled",
"ge",
"get_disabled",
"gt",
"in_",
"instance_of",
Expand All @@ -26,9 +31,58 @@
"max_len",
"optional",
"provides",
"set_disabled",
]


def set_disabled(disabled):
"""
Globally disable or enable running validators.
By default, they are run.
:param disabled: If ``True``, disable running all validators.
:type disabled: bool
.. warning::
This function is not thread-safe!
.. versionadded:: 21.3.0
"""
set_run_validators(not disabled)


def get_disabled():
"""
Return a bool indicating whether validators are currently disabled or not.
:return: ``True`` if validators are currently disabled.
:rtype: bool
.. versionadded:: 21.3.0
"""
return not get_run_validators()


@contextmanager
def disabled():
"""
Context manager that disables running validators within its context.
.. warning::
This context manager is not thread-safe!
.. versionadded:: 21.3.0
"""
set_run_validators(False)
try:
yield
finally:
set_run_validators(True)


@attrs(repr=False, slots=True, hash=True)
class _InstanceOfValidator(object):
type = attrib()
Expand Down
5 changes: 5 additions & 0 deletions src/attr/validators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ from typing import (
AnyStr,
Callable,
Container,
ContextManager,
Iterable,
List,
Mapping,
Expand All @@ -26,6 +27,10 @@ _K = TypeVar("_K")
_V = TypeVar("_V")
_M = TypeVar("_M", bound=Mapping)

def set_disabled(run: bool) -> None: ...
def get_disabled() -> bool: ...
def disabled() -> ContextManager[None]: ...

# To be more precise on instance_of use some overloads.
# If there are more than 3 items in the tuple then we fall back to Any
@overload
Expand Down
62 changes: 61 additions & 1 deletion tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import attr

from attr import fields, has
from attr import _config, fields, has
from attr import validators as validator_module
from attr._compat import PY2, TYPE
from attr.validators import (
Expand Down Expand Up @@ -46,6 +46,66 @@ def zope_interface():
return zope.interface


class TestDisableValidators(object):
@pytest.fixture(autouse=True)
def reset_default(self):
"""
Make sure validators are always enabled after a test.
"""
yield
_config._run_validators = True

def test_default(self):
"""
Run validators by default.
"""
assert _config._run_validators is True

@pytest.mark.parametrize("value, expected", [(True, False), (False, True)])
def test_set_validators_diabled(self, value, expected):
"""
Sets `_run_validators`.
"""
validator_module.set_disabled(value)

assert _config._run_validators is expected

@pytest.mark.parametrize("value, expected", [(True, False), (False, True)])
def test_disabled(self, value, expected):
"""
Returns `_run_validators`.
"""
_config._run_validators = value

assert validator_module.get_disabled() is expected

def test_disabled_ctx(self):
"""
The `disabled` context manager disables running validators,
but only within its context.
"""
assert _config._run_validators is True

with validator_module.disabled():
assert _config._run_validators is False

assert _config._run_validators is True

def test_disabled_ctx_with_errors(self):
"""
Running validators is re-enabled even if an error is raised.
"""
assert _config._run_validators is True

with pytest.raises(ValueError):
with validator_module.disabled():
assert _config._run_validators is False

raise ValueError("haha!")

assert _config._run_validators is True


class TestInstanceOf(object):
"""
Tests for `instance_of`.
Expand Down
15 changes: 15 additions & 0 deletions tests/typing_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,21 @@ class Validated:
)


@attr.define
class Validated2:
num: int = attr.field(validator=attr.validators.ge(0))


with attr.validators.disabled():
Validated2(num=-1)

try:
attr.validators.set_disabled(True)
Validated2(num=-1)
finally:
attr.validators.set_disabled(False)


# Custom repr()
@attr.s
class WithCustomRepr:
Expand Down

0 comments on commit c6143d5

Please sign in to comment.