Skip to content

Commit

Permalink
Add decorators and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jayvdb committed Sep 17, 2020
1 parent 5386849 commit c5ad6aa
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 10 deletions.
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language: python
python:
- 3.8
- 3.6
- 2.7
install: pip install tox-travis
script: tox
4 changes: 2 additions & 2 deletions readonly/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ def _last_executed(self):


class PatchedCursorWrapper(utils.CursorWrapper):
def __init__(self, cursor, db):
self.cursor = ReadOnlyCursorWrapper(cursor, db)
def __init__(self, cursor, db,read_only=None):
self.cursor = ReadOnlyCursorWrapper(cursor, db, read_only=read_only)
self.db = db


Expand Down
49 changes: 49 additions & 0 deletions readonly/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from contextlib import contextmanager

from django.db.backends import utils

from readonly.cursor import (
PatchedCursorWrapper,
PatchedCursorDebugWrapper,
)

_orig_CursorWrapper = utils.CursorWrapper
_orig_CursorDebugWrapper = utils.CursorDebugWrapper


class ForcedPatchedCursorWrapper(PatchedCursorWrapper):
def __init__(self, cursor, db):
super(ForcedPatchedCursorWrapper, self).__init__(cursor, db, read_only=True)


class ForcedPatchedCursorDebugWrapper(PatchedCursorDebugWrapper):
def __init__(self, cursor, db):
super(ForcedPatchedCursorDebugWrapper, self).__init__(
cursor, db, read_only=True
)


@contextmanager
def readonly():
old_CursorWrapper = utils.CursorWrapper
old_CursorDebugWrapper = utils.CursorDebugWrapper
utils.CursorWrapper = ForcedPatchedCursorWrapper
utils.CursorDebugWrapper = ForcedPatchedCursorDebugWrapper
try:
yield
finally:
utils.CursorWrapper = old_CursorWrapper
utils.CursorDebugWrapper = old_CursorDebugWrapper


@contextmanager
def write_enabled():
old_CursorWrapper = utils.CursorWrapper
old_CursorDebugWrapper = utils.CursorDebugWrapper
utils.CursorWrapper = _orig_CursorWrapper
utils.CursorDebugWrapper = _orig_CursorDebugWrapper
try:
yield
finally:
utils.CursorWrapper = old_CursorWrapper
utils.CursorDebugWrapper = old_CursorDebugWrapper
5 changes: 5 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.db import models


class Widget(models.Model):
name = models.CharField(max_length=100)
16 changes: 8 additions & 8 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

DB_READ_ONLY_MIDDLEWARE_MESSAGE = False
SITE_READ_ONLY = False
DB_READ_ONLY_DATABASES = False
DB_READ_ONLY_DATABASES = []

DATABASE_ENGINE = "sqlite3"

# Uncomment below to run tests with mysql
# DATABASE_ENGINE = "django.db.backends.mysql"
# DATABASE_NAME = "readonly_test"
# DATABASE_USER = "readonly_test"
# DATABASE_HOST = "/var/mysql/mysql.sock"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
},
}

INSTALLED_APPS = [
"readonly",
"tests",
]

MIDDLEWARE = [
Expand Down
88 changes: 88 additions & 0 deletions tests/test_context_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from django.db import transaction
from django.db.transaction import TransactionManagementError

from django.test import TestCase

from readonly.decorators import readonly, write_enabled
from readonly.exceptions import DatabaseWriteDenied

from tests.models import Widget


class ContextManagerTestCase(TestCase):
def _create_obj(self):
with transaction.atomic():
obj = Widget.objects.create()
obj.save()

def test_normal(self):
Widget.objects.count()
obj = Widget.objects.create()
obj.save()

def test_readonly_transaction(self):
before = Widget.objects.count()

with readonly():
with self.assertRaises(DatabaseWriteDenied):
with transaction.atomic():
obj = Widget.objects.create()
obj.save()

after = Widget.objects.count()
assert after == before

obj = Widget.objects.create()
obj.save()

after = Widget.objects.count()
assert after == before + 1

def test_readonly(self):
Widget.objects.count()

with readonly():
with self.assertRaises(DatabaseWriteDenied):
obj = Widget.objects.create()
obj.save()

# TODO: Automatic cancellation of the transaction would simplify
# developer use of readonly & DatabaseWriteDenied with foreign code
with self.assertRaises(TransactionManagementError):
Widget.objects.count()

def test_nested_readonly_disabled(self):
with readonly():
with self.assertRaises(DatabaseWriteDenied):
self._create_obj()
with readonly():
with self.assertRaises(DatabaseWriteDenied):
self._create_obj()
with readonly():
with self.assertRaises(DatabaseWriteDenied):
self._create_obj()

Widget.objects.create()

def test_readonly_enabled(self):
with readonly():
with write_enabled():
self._create_obj()

def test_nested_readonly_enabled(self):
with readonly():
with readonly():
with write_enabled():
with readonly():
with write_enabled():
with readonly():
with self.assertRaises(DatabaseWriteDenied):
self._create_obj()

with self.assertRaises(DatabaseWriteDenied):
self._create_obj()

with self.assertRaises(DatabaseWriteDenied):
self._create_obj()

self._create_obj()
18 changes: 18 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[tox]
envlist = py27-django{18,19,110,111}, py{36,37,38}-django{111,20,21,22,30,31}
skip_missing_interpreters = True

[testenv]
commands = python -m django test {posargs}
setenv =
DJANGO_SETTINGS_MODULE = tests.settings
deps =
django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10
django110: Django>=1.10,<1.11
django111: Django>=1.11,<1.12
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2,<2.3
django30: Django>=3.0,<3.1
django31: Django>=3.1,<3.2

0 comments on commit c5ad6aa

Please sign in to comment.