Repository: pytest-dev/pytest-django

Issue:
Provide a context manager/fixture to disable DB access explicitly
I think the following code looks weird ,because it reads like it would disable the DB.

```
@pytest.fixture
def myfixture(django_db_blocker):
    with django_db_blocker:
        ...  # modify something in the database
```

But that's a manager/fixture I would like to have.

What I came up with is this, meant to work with pytest-django master and 2.9.1:

``` python
@pytest.yield_fixture
def disable_db(request):
    from _pytest.python import FixtureLookupError
    try:
        manager = request.getfuncargvalue('django_db_blocker')
        manager.disable_database_access()
        yield
        manager.restore_previous_access()
    except FixtureLookupError:
        manager = request.getfuncargvalue('_django_cursor_wrapper')
        manager.disable()
        yield
        manager.restore()
```



Discussion:
Hmm, maybe

```
with django_db_blocker:
```

should not be supported, instead we should have:

```
with django_db_blocker.enable_database_access():
```

and

```
with django_db_blocker.disable_database_access():
```

?
enable_database_access / disable_database_access would work both as plain functions and context manager.

That would be nice to change before 3.0 is released to avoid having to support the weird API.

@blueyed thoughts?

@pelme 
Makes sense!


Resolving patch:
diff --git a/pytest_django/compat.py b/pytest_django/compat.py
--- a/pytest_django/compat.py
+++ b/pytest_django/compat.py
@@ -9,3 +9,9 @@ def teardown_databases(db_cfg, verbosity):
         (_DiscoverRunner(verbosity=verbosity,
                          interactive=False)
          .teardown_databases(db_cfg))
+
+try:
+    from django.db.backends.base.base import BaseDatabaseWrapper  # noqa
+except ImportError:
+    # Django 1.7.
+    from django.db.backends import BaseDatabaseWrapper  # noqa
diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py
--- a/pytest_django/fixtures.py
+++ b/pytest_django/fixtures.py
@@ -86,10 +86,10 @@ def django_db_setup(
             # Django 1.7 compatibility
             from .db_reuse import monkey_patch_creation_for_db_reuse
 
-            with django_db_blocker:
+            with django_db_blocker.unblock():
                 monkey_patch_creation_for_db_reuse()
 
-    with django_db_blocker:
+    with django_db_blocker.unblock():
         db_cfg = setup_databases(
             verbosity=pytest.config.option.verbose,
             interactive=False,
@@ -97,7 +97,7 @@ def django_db_setup(
         )
 
     def teardown_database():
-        with django_db_blocker:
+        with django_db_blocker.unblock():
             teardown_databases(
                 db_cfg,
                 verbosity=pytest.config.option.verbose,
@@ -115,8 +115,8 @@ def _django_db_fixture_helper(transactional, request, django_db_blocker):
         # Do nothing, we get called with transactional=True, too.
         return
 
-    django_db_blocker.enable_database_access()
-    request.addfinalizer(django_db_blocker.restore_previous_access)
+    django_db_blocker.unblock()
+    request.addfinalizer(django_db_blocker.restore)
 
     if transactional:
         from django.test import TransactionTestCase as django_case
diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py
--- a/pytest_django/plugin.py
+++ b/pytest_django/plugin.py
@@ -146,7 +146,7 @@ def _setup_django():
         return
 
     django.setup()
-    _blocking_manager.disable_database_access()
+    _blocking_manager.block()
 
 
 def _get_boolean_value(x, name, default=None):
@@ -348,8 +348,7 @@ def django_db_blocker():
     special database handling.
 
     The object is a context manager and provides the methods
-    .enable_database_access()/.disable_database_access() and
-    .restore_database_access() to temporarily enable database access.
+    .unblock()/.block() and .restore() to temporarily enable database access.
 
     This is an advanced feature that is meant to be used to implement database
     fixtures.
@@ -383,7 +382,7 @@ def _django_setup_unittest(request, django_db_blocker):
         request.getfuncargvalue('django_test_environment')
         request.getfuncargvalue('django_db_setup')
 
-        django_db_blocker.enable_database_access()
+        django_db_blocker.unblock()
 
         cls = request.node.cls
 
@@ -394,7 +393,7 @@ def _django_setup_unittest(request, django_db_blocker):
         def teardown():
             _restore_class_methods(cls)
             cls.tearDownClass()
-            django_db_blocker.restore_previous_access()
+            django_db_blocker.restore()
 
         request.addfinalizer(teardown)
 
@@ -518,6 +517,17 @@ def _template_string_if_invalid_marker(request):
 # ############### Helper Functions ################
 
 
+class _DatabaseBlockerContextManager(object):
+    def __init__(self, db_blocker):
+        self._db_blocker = db_blocker
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self._db_blocker.restore()
+
+
 class _DatabaseBlocker(object):
     """Manager for django.db.backends.base.base.BaseDatabaseWrapper.
 
@@ -530,11 +540,7 @@ def __init__(self):
 
     @property
     def _dj_db_wrapper(self):
-        try:
-            from django.db.backends.base.base import BaseDatabaseWrapper
-        except ImportError:
-            # Django 1.7.
-            from django.db.backends import BaseDatabaseWrapper
+        from .compat import BaseDatabaseWrapper
 
         # The first time the _dj_db_wrapper is accessed, we will save a
         # reference to the real implementation.
@@ -552,25 +558,21 @@ def _blocking_wrapper(*args, **kwargs):
         pytest.fail('Database access not allowed, '
                     'use the "django_db" mark to enable it.')
 
-    def enable_database_access(self):
+    def unblock(self):
         """Enable access to the Django database."""
         self._save_active_wrapper()
         self._dj_db_wrapper.ensure_connection = self._real_ensure_connection
+        return _DatabaseBlockerContextManager(self)
 
-    def disable_database_access(self):
+    def block(self):
         """Disable access to the Django database."""
         self._save_active_wrapper()
         self._dj_db_wrapper.ensure_connection = self._blocking_wrapper
+        return _DatabaseBlockerContextManager(self)
 
-    def restore_previous_access(self):
+    def restore(self):
         self._dj_db_wrapper.ensure_connection = self._history.pop()
 
-    def __enter__(self):
-        self.enable_database_access()
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        self.restore_previous_access()
-
 
 _blocking_manager = _DatabaseBlocker()
 


Test patch:
diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py
--- a/tests/test_fixtures.py
+++ b/tests/test_fixtures.py
@@ -304,3 +304,31 @@ class Migration(migrations.Migration):
     result = django_testdir.runpytest_subprocess('-s')
     result.stdout.fnmatch_lines(['*1 passed*'])
     assert result.ret == 0
+
+
+class Test_django_db_blocker:
+    @pytest.mark.django_db
+    def test_block_manually(self, django_db_blocker):
+        try:
+            django_db_blocker.block()
+            with pytest.raises(pytest.fail.Exception):
+                Item.objects.exists()
+        finally:
+            django_db_blocker.restore()
+
+    @pytest.mark.django_db
+    def test_block_with_block(self, django_db_blocker):
+        with django_db_blocker.block():
+            with pytest.raises(pytest.fail.Exception):
+                Item.objects.exists()
+
+    def test_unblock_manually(self, django_db_blocker):
+        try:
+            django_db_blocker.unblock()
+            Item.objects.exists()
+        finally:
+            django_db_blocker.restore()
+
+    def test_unblock_with_block(self, django_db_blocker):
+        with django_db_blocker.unblock():
+            Item.objects.exists()