Inheritance and Decorators in Python

Source Code

Classes

Admin is a child of User: it inherits the instance properties name and is_logged_in.

class User:
    def __init__(self, name):
        self.name = name
        self.is_logged_in = False


class Admin(User):
    def __init__(self, name):
        super().__init__(name)

Post in reality would never have a randomnly generated id, but would receive its ID from a posts table in the database. I have yet to learn the Django equivalent of a Model in Ruby on Rails.

class Post:
    def __init__(self, id=None):
        self.id = id or random.randint(0, 1000)

Decorator Functions

is_authenticated checks the is_logged_in instance attribute of the object.

def is_authenticated(function):
    def wrapper(*args):
        if args[0].is_logged_in is True:
            return function(*args)
        else:
            raise Exception("401")

    return wrapper

is_admin checks whether the object itself isinstance of Admin.

def is_admin(function):
    def wrapper(*args, **kwargs):
        if isinstance(args[0], Admin):
            return function(*args, **kwargs)
        else:
            raise Exception("401")

    return wrapper

Functions with Class Instance Objects

create_blog_post can be performed by either User or Admin providing he is_authenticated, i.e. is_logged_in:

@is_authenticated
def create_blog_post(user):
    print(f"This is {user.name}'s new blog post.")
    return {"status": "ok"}

delete_blog_post can be performed only by an Admin`` who is_logged_in.

@is_admin
@is_authenticated
def delete_blog_post(user, post):
    print(f"{user.name} deleted post {post.id}")
    return {"status": "ok"}

Testing

Fixtures

Testing requires setup of Class Instance objects in a certain state – logged in or not, admin or not.

This is trickier than using primitives are objects with a fixed state, both in setup and execution of the tests.

Test scenarios require user_with_login, user_sans_login, admin_with_login. Admin not logged in is identical to a User not logged in, so both cases are covered by user_sans_login. An instance of Post is also required to test a successful execution of delete_blog_post(user, post).

@pytest.fixture
def user_with_login():
    u = d.User("Foo")
    u.is_logged_in = True
    yield u


@pytest.fixture
def user_sans_login():
    u = d.User("Bar")
    print(u.name)
    print(u.is_logged_in)
    yield u


@pytest.fixture
def admin_with_login():
    u = d.Admin("MyAdmin")
    u.is_logged_in = True
    yield u


@pytest.fixture
def my_post():
    post = d.Post(42)
    yield post

Tests

Tests are filtered by a custom tag filter and the command pytest -k filter or ptw -- -k filter.

@pytest.mark.filter
def test_authenticated(user_with_login):
    assert user_with_login.is_logged_in is True
    assert d.create_blog_post(user_with_login) == {"status": "ok"}


@pytest.mark.filter
def test_unauthenticated(user_sans_login):
    assert user_sans_login.is_logged_in is False
    with pytest.raises(Exception) as e:
        d.create_blog_post(user_sans_login)
        assert e.value == "401"


@pytest.mark.filter
def test_authenticated_admin(admin_with_login, my_post):
    assert admin_with_login.is_logged_in is True
    assert isinstance(admin_with_login, d.Admin) is True
    assert my_post.id == 42
    assert d.delete_blog_post(admin_with_login, my_post) == {"status": "ok"}


@pytest.mark.filter
def test_authenticated_not_admin(user_with_login, my_post):
    assert user_with_login.is_logged_in is True
    assert isinstance(user_with_login, d.User) is True
    assert my_post.id == 42
    with pytest.raises(Exception) as e:
        d.delete_blog_post(user_with_login, my_post)
        assert e.value == "401"



all tags