EpicLaunchX — Decorators Walkthrough

Rashad Musayev
7 min readMay 17, 2024

--

EpicLaunchX is currently in beta and looking for enthusiastic users to try out our new Decorators feature. Head over to beta.epiclaunchx.io to get started!

In this blog post, we’ll delve into the exciting world of decorators in Python! We’ll embark on a step-by-step journey through the EpicLaunchX “Decorators” project, building a solid foundation for enhancing your code’s functionality and elegance.

Important Note: Before you begin, please follow our Git workflow for contributing to EpicLaunchX. Here’s a quick rundown:

  • Branching: For every new task you work on, create a separate branch with a descriptive name following our convention. Pattern: launch_{number}_task_{number}.
  • Pushing & Pull Requests: Once you’ve finished your work on the feature, push your local branch to the remote repository. Then, create a pull request from your feature branch to the main branch for code review and merging.

Setting Up the Environment

First, we need to create a virtual environment specifically for your EpicLaunchX projects. This will be a designated workspace where all the necessary tools and libraries are neatly organized, separate from your main system. To create this environment, open your terminal and type the following command:

python3 -m venv .venv

For Linux and MacOS users:

source .venv/bin/activate

For Windows users:

.venv/Scripts/activate

This step is only necessary if you’re using Linux or macOS. EpicLaunchX leverages the power of Flit for package management. To install Flit, simply run the following command within your activated environment:

make install-flit

If you’re a Windows developer, EpicLaunchX offers an alternative approach using the Windows Subsystem for Linux (WSL). This allows you to seamlessly run Linux commands directly within your Windows environment. Here’s what you’ll need to do:

Once you’ve completed these steps, you’ll be able to access a Linux terminal within your Windows environment. From there, you can follow the same steps as Linux and macOS users to activate your virtual environment and install Flit

For an extra layer of security, EpicLaunchX recommends setting an environment variable called FLIT_ROOT_INSTALL. Within your activated environment, simply run the following command:

export FLIT_ROOT_INSTALL=1

Now that everything is set up, we can install the essential development dependencies needed for your EpicLaunchX adventures. Execute the following command within your activated environment:

make install-dev

With these steps completed, you’ve successfully constructed your Python environment, ready to tackle the challenges and conquer the learning curve on EpicLaunchX!

Launching Task 1: Domain Modeling with Movie Class

This first task focuses on setting up the foundation for our “Decorators” project by creating a domain model. We’ll achieve this by:

  1. Creating a file named domain/models.py to house our domain classes.
  2. Defining a Movie class within this file with two attributes:
  • name: This attribute will represent the movie title and should be of type str.
  • customer_age: This attribute will store the customer's age and should be of type int.

domain/models.py:

from dataclasses import dataclass


@dataclass
class Movie:
name: str
customer_age: int

Utilizing @dataclass for Efficiency:

To streamline our code and improve readability, we can leverage the @dataclass decorator from the dataclasses module in Python. This decorator automatically generates boilerplate code like __init__ and methods for comparison and representation, saving us time and effort.

Implementing Unit Tests with pytest:

To ensure the proper functionality of our Movie class, we'll also create unit tests using the pytest framework. These tests will verify that the class attributes are of the correct types and behave as expected.

tests/test_model.py

from src.pytemplate.domain.models import Movie


def test_init_movie():
movie = Movie("The Matrix", 15)
assert movie.name == "The Matrix"
assert movie.customer_age == 15

My Solution PR: https://github.com/EpicLaunchX/rmusayevr-decorators-easy/pull/6

Launching Task 2: Creating the Movie Factory Function

Building upon the domain model established in the previous task, let’s create a factory function to simplify movie object creation. We’ll achieve this by:

  1. Defining a function named movie_factory within the existing domain/models.py file.

2. The function will accept two arguments:

  • name: This argument represents the movie title and should be of type str.
  • customer_age: This argument represents the customer's age and should be of type int.

3. The movie_factory function should return a new instance of the Movie class we created earlier. This ensures the returned object adheres to the defined structure and data types.

domain/models.py:

...

def movie_factory(name: str, customer_age: int) -> Movie:
return Movie(name, customer_age)

Similar to the previous task, we’ll write unit tests using pytest to verify the functionality of the movie_factory function. These tests will confirm that the function:

tests/test_model.py

from src.pytemplate.domain.models import Movie, movie_factory

...

def test_movie_factory():
name, customer_age = "Dune: Part Two", 21
movie = movie_factory(name, customer_age)
assert isinstance(movie, Movie)
assert movie.name == name
assert movie.customer_age == customer_age

My Solution PR: https://github.com/EpicLaunchX/rmusayevr-decorators-easy/pull/7

Launching Task 3: Implementing Age Restriction for Children with Decorators

This task introduces decorators to control access based on age restrictions. We’ll achieve this by:

  1. Creating the age_limit_6plus Decorator:
  • Define a decorator named age_limit_6plus within a new file named utils/decorator.py.
  • The decorator will accept a single argument named movie which should be of type Movie (as defined in the previous tasks).
  • Utilize the @wraps decorator from the functools module to preserve the original function's metadata (like docstring and name).
  • Inside the decorator function, check the customer_age attribute of the provided movie object.

utils/decorator.py

from functools import wraps
from typing import Callable

from pytemplate.domain.models import Movie


def age_limit_6plus(func: Callable[..., str]) -> Callable[..., str]:
@wraps(func)
def wrapper(movie: Movie):
if movie.customer_age >= 6:
return func(movie)
else:
return f"Sorry, you are not old enough to watch {movie.name}!"

return wrapper

2. Conditional Logic and Return Values:

  • If the customer_age is less than 6, return a message indicating the user isn't old enough to watch the movie (e.g., "Sorry, you are not old enough to watch {movie.name}!").
  • If the customer_age is 6 or above, return a message indicating the user is allowed to watch the movie (e.g., "You are allowed to watch {movie.name}!").

3. Service Logic and Decorator Usage:

  • Create a new file named service/tickets.py to house your service logic.
  • Define a function named buy_ticket_for_children within service/tickets.py.
  • This function should also accept a movie argument of type Movie.
  • Apply the @age_limit_6plus decorator to the buy_ticket_for_children function.
  • Inside the buy_ticket_for_children function, return a success message indicating the user can watch the movie (e.g., "You are allowed to watch {movie.name}.").

service/tickets.py

from pytemplate.domain.models import Movie
from pytemplate.utils.decorator import age_limit_6plus


@age_limit_6plus
def buy_ticket_for_children(movie: Movie) -> str:
return f"You are allowed to watch {movie.name}!"

4. Unit Testing with pytest:

  • Write unit tests using pytest to verify the functionality of the age_limit_6plus decorator in various scenarios.
  • Test cases should cover:
  • Age less than 6 (failing scenario).
  • Age 6 or above (passing scenario).
  • Ensure the decorated function (buy_ticket_for_children in this case) behaves as expected.

tests/test_model.py

from pytemplate.service.tickets import buy_ticket_for_children
from pytemplate.utils.decorator import age_limit_6plus
from src.pytemplate.domain.models import Movie, movie_factory

...

def test_allowed_6plus_decorator():
@age_limit_6plus
def check_age_limit(movie: Movie):
return f"You are allowed to watch {movie.name}!"

movie = Movie("Frozen", 9)
assert check_age_limit(movie) == "You are allowed to watch Frozen!"


def test_not_allowed_6plus_decorator():
@age_limit_6plus
def check_age_limit(movie: Movie):
return f"You are allowed to watch {movie.name}!"

movie = Movie("Frozen", 4)
assert check_age_limit(movie) == "Sorry, you are not old enough to watch Frozen!"


def test_buy_ticket_for_children_allowed():
movie = Movie(name="Toy Story", customer_age=9)
result = buy_ticket_for_children(movie)
assert result == "You are allowed to watch Toy Story!"


def test_buy_ticket_for_children_not_allowed():
movie = Movie(name="Toy Story", customer_age=4)
result = buy_ticket_for_children(movie)
assert result == "Sorry, you are not old enough to watch Toy Story!"

My Solution PR: https://github.com/EpicLaunchX/rmusayevr-decorators-easy/pull/8

Launching Task 4: Implementing Age Restriction for Teenagers with Decorators

This task introduces decorators to control access based on age restrictions for teenagers. We’ll achieve this by:

Apply the codes I mentioned above, but use 13 instead of 6 :)

Also change the ages in the tests :D

My Solution PR: https://github.com/EpicLaunchX/rmusayevr-decorators-easy/pull/11

Launching Task 5: Building the Command-Line Interface (CLI)

This final task focuses on creating a user-friendly CLI to interact with the functionalities we’ve developed. We’ll achieve this by:

  1. Creating the entrypoints/cli/main.py File:
  • Establish a new file named main.py within the entrypoints/cli directory. This file will house the main function for our CLI application.

2. Implementing the main Function:

  • Define a function named main within the main.py file. This function will serve as the entry point for our CLI application.

Inside the main function:

Utilize functions like input to prompt the user for input values:

  • movie_name: This will store the name of the movie the user wants to watch.
  • customer_age: This will capture the user's age.
  • age_limit: This will allow the user to specify the desired age restriction (e.g., 6, 13).

Employ conditional logic to determine the appropriate service function based on the provided age_limit:

  • If age_limit is 6, call the buy_ticket_for_children function (assuming it's implemented with proper age restriction handling).
  • If age_limit is 13, call the buy_ticket_for_teens function.
  • For any other age_limit value, display an error message indicating invalid input.

Remember to construct a Movie object using the gathered movie_name and customer_age before calling the service function.

entrypoints/cli/main.py

from pytemplate.domain.models import movie_factory
from pytemplate.service.tickets import buy_ticket_for_children, buy_ticket_for_teens


def main():
movie_name = input("Enter the name of the movie: ")
customer_age = int(input("Enter your age: "))
age_limit = int(input("Enter the age limit of the movie (6/13): "))

movie = movie_factory(movie_name, customer_age)

if age_limit == 6:
result = buy_ticket_for_children(movie)
elif age_limit == 13:
result = buy_ticket_for_teens(movie)
else:
print("Invalid action! Please choose 6/13.")
return

print(result)

3. Write unit tests using pytest to verify the overall functionality of the main function across various user input scenarios.

Test cases should cover:

  • Valid input with different age limits (6, 13).
  • Invalid input for age_limit (non-existent value).
  • Ensure the correct service function is called based on the provided age_limit.

tests/test_model.py

from io import StringIO
from unittest.mock import patch

from pytemplate.entrypoints.cli.main import main
from pytemplate.service.tickets import buy_ticket_for_children, buy_ticket_for_teens
from pytemplate.utils.decorator import age_limit_6plus, age_limit_13plus
from src.pytemplate.domain.models import Movie, movie_factory

...


@patch("builtins.input", side_effect=["Ponyo", 8, 6])
def test_main_allowed_6plus(mock_input):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
main()
assert mock_stdout.getvalue().strip() == "You are allowed to watch Ponyo!"


@patch("builtins.input", side_effect=["Ponyo", 4, 6])
def test_main_not_allowed_6plus(mock_input):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
main()
assert mock_stdout.getvalue().strip() == "Sorry, you are not old enough to watch Ponyo!"


@patch("builtins.input", side_effect=["Monster", 14, 13])
def test_main_allowed_13plus(mock_input):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
main()
assert mock_stdout.getvalue().strip() == "You are allowed to watch Monster!"


@patch("builtins.input", side_effect=["Monster", 12, 13])
def test_main_not_allowed_13plus(mock_input):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
main()
assert mock_stdout.getvalue().strip() == "Sorry, you are not old enough to watch Monster!"


@patch("builtins.input", side_effect=["The Hunt", 21, 0])
def test_main_invalid_action(mock_input):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
main()
assert mock_stdout.getvalue().strip() == "Invalid action! Please choose 6/13."

My Solution PR: https://github.com/EpicLaunchX/rmusayevr-decorators-easy/pull/12

Conclusion

By following these steps, you’ll gain experience with building a practical project using decorators in EpicLaunchX. You’ll learn how to manage project structure, create domain models, enforce age restrictions using decorators, and develop a user-friendly CLI for interaction.

Happy Coding :)

--

--

No responses yet