EpicLaunchX — Decorators Walkthrough
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:
- Install WSL by following the official Microsoft guide: https://learn.microsoft.com/en-us/windows/wsl/
- Install Chocolatey, a package manager for Windows, using the instructions here: https://chocolatey.org/install
- Activate WSL virtualization following the steps outlined here: https://docs.docker.com/desktop/troubleshoot/overview/
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:
- Creating a file named
domain/models.py
to house our domain classes. - Defining a
Movie
class within this file with two attributes:
name
: This attribute will represent the movie title and should be of typestr
.customer_age
: This attribute will store the customer's age and should be of typeint
.
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:
- Defining a function named
movie_factory
within the existingdomain/models.py
file.
2. The function will accept two arguments:
name
: This argument represents the movie title and should be of typestr
.customer_age
: This argument represents the customer's age and should be of typeint
.
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:
- Creating the
age_limit_6plus
Decorator:
- Define a decorator named
age_limit_6plus
within a new file namedutils/decorator.py
. - The decorator will accept a single argument named
movie
which should be of typeMovie
(as defined in the previous tasks). - Utilize the
@wraps
decorator from thefunctools
module to preserve the original function's metadata (like docstring and name). - Inside the decorator function, check the
customer_age
attribute of the providedmovie
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
withinservice/tickets.py
. - This function should also accept a
movie
argument of typeMovie
. - Apply the
@age_limit_6plus
decorator to thebuy_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 theage_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:
- Creating the
entrypoints/cli/main.py
File:
- Establish a new file named
main.py
within theentrypoints/cli
directory. This file will house the main function for our CLI application.
2. Implementing the main
Function:
- Define a function named
main
within themain.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 thebuy_ticket_for_children
function (assuming it's implemented with proper age restriction handling). - If
age_limit
is 13, call thebuy_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.