Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Part of #3928 [Web Examples] Add First Class Python Support #4107

Merged
merged 25 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
400cc78
[WIP] First Test Phase
himanshumahajan138 Dec 11, 2024
a3ece20
Added Test Todo-Flask
himanshumahajan138 Dec 11, 2024
61d3fd5
Final Code Updated
himanshumahajan138 Dec 12, 2024
f138ddd
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 12, 2024
764a460
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 13, 2024
b47387a
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 13, 2024
c15f409
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 14, 2024
79f20b5
Merge branch 'main' of https://github.com/com-lihaoyi/mill into issue…
himanshumahajan138 Dec 15, 2024
3555233
testing
himanshumahajan138 Dec 15, 2024
47b87a7
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 16, 2024
032c507
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 16, 2024
14e7057
Final Changes
himanshumahajan138 Dec 16, 2024
8533ff6
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 16, 2024
49c19c3
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 16, 2024
4ad263a
Fixtures
himanshumahajan138 Dec 16, 2024
762026e
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 17, 2024
b0d3dd0
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 17, 2024
4fba1ae
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 18, 2024
4566126
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 20, 2024
6151bf0
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 21, 2024
fb606eb
Flask Updation
himanshumahajan138 Dec 21, 2024
044c761
Update app.py
himanshumahajan138 Dec 22, 2024
8fc2a02
Final Updation
himanshumahajan138 Dec 22, 2024
e033073
Final Updates
himanshumahajan138 Dec 23, 2024
7b63b5e
Merge branch 'main' into issue-3928
himanshumahajan138 Dec 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
*** xref:pythonlib/module-config.adoc[]
*** xref:pythonlib/dependencies.adoc[]
*** xref:pythonlib/publishing.adoc[]
*** xref:pythonlib/web-examples.adoc[]
* xref:comparisons/why-mill.adoc[]
** xref:comparisons/maven.adoc[]
** xref:comparisons/gradle.adoc[]
Expand Down
25 changes: 25 additions & 0 deletions docs/modules/ROOT/pages/pythonlib/web-examples.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
= Python Web Project Examples
:page-aliases: Python_Web_Examples.adoc

include::partial$gtag-config.adoc[]

This page provides examples of using Mill as a build tool for Python web applications.
It includes setting up a basic "Hello, World!" application and developing a fully
functional Todo-MVC app with Flask and Django, showcasing best practices
for project organization, scalability, and maintainability.

== Flask Hello World App

include::partial$example/pythonlib/web/1-hello-flask.adoc[]

== Flask TodoMvc App

include::partial$example/pythonlib/web/2-todo-flask.adoc[]

== Django Hello World App

include::partial$example/pythonlib/web/3-hello-django.adoc[]

== Django TodoMvc App

include::partial$example/pythonlib/web/4-todo-django.adoc[]
1 change: 1 addition & 0 deletions example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ object `package` extends RootModule with Module {
object dependencies extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "dependencies"))
object publishing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "publishing"))
object module extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "module"))
object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web"))
}

object cli extends Module{
Expand Down
37 changes: 37 additions & 0 deletions example/pythonlib/web/1-hello-flask/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// This example uses Mill to manage a Flask app that serves "Hello, Mill!"
// at the root URL (`/`), with Flask installed as a dependency
// and tests enabled using `unittest`.
package build
import mill._, pythonlib._

object foo extends PythonModule {

def mainScript = Task.Source { millSourcePath / "src" / "foo.py" }
himanshumahajan138 marked this conversation as resolved.
Show resolved Hide resolved

def pythonDeps = Seq("flask==3.1.0")

object test extends PythonTests with TestModule.Unittest

}

// Running these commands will test and run the Flask server with desired outputs.

/** Usage

> ./mill foo.test
...
test_hello_flask (test.TestScript...)
Test the '/' endpoint. ... ok
...
Ran 1 test...
OK
...

> ./mill foo.runBackground

> curl http://localhost:5000
...<h1>Hello, Mill!</h1>...

> ./mill clean foo.runBackground

*/
12 changes: 12 additions & 0 deletions example/pythonlib/web/1-hello-flask/foo/src/foo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello_world():
return "<h1>Hello, Mill!</h1>"


if __name__ == "__main__":
app.run(debug=True)
21 changes: 21 additions & 0 deletions example/pythonlib/web/1-hello-flask/foo/test/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import unittest
from foo import app # type: ignore


class TestScript(unittest.TestCase):
def setUp(self):
himanshumahajan138 marked this conversation as resolved.
Show resolved Hide resolved
"""Set up the test client before each test."""
self.app = app.test_client() # Initialize the test client
self.app.testing = True # Enable testing mode for better error handling

def test_hello_flask(self):
"""Test the '/' endpoint."""
response = self.app.get("/") # Simulate a GET request to the root endpoint
self.assertEqual(response.status_code, 200) # Check the HTTP status code
self.assertIn(
b"Hello, Mill!", response.data
) # Check if the response contains the expected text


if __name__ == "__main__":
unittest.main()
62 changes: 62 additions & 0 deletions example/pythonlib/web/2-todo-flask/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// This is a `Flask`-based `TodoMVC` application managed and built using `Mill`.
himanshumahajan138 marked this conversation as resolved.
Show resolved Hide resolved
// It allows users to `add`, `edit`, `delete`, and `view` tasks stored in a Python Data Structure.
// Tasks can be filtered as `all`, `active`, or `completed` based on their state.
// The application demonstrates dynamic rendering using Flask's routing and templating features.
package build
import mill._, pythonlib._

object todo extends PythonModule {

def mainScript = Task.Source { millSourcePath / "src" / "app.py" }
himanshumahajan138 marked this conversation as resolved.
Show resolved Hide resolved

def pythonDeps = Seq("flask==3.1.0", "Flask-SQLAlchemy==3.1.1", "Flask-WTF==1.2.2")

object test extends PythonTests with TestModule.Unittest
object itest extends PythonTests with TestModule.Unittest

}

// Apart from running a web server, this example demonstrates:

// - **Serving HTML templates** using **Jinja2** (Flask's default templating engine).
// - **Managing static files** such as JavaScript, CSS, and images.
// - **Filtering and managing tasks** in-memory using Python data structures.
// - **Unit testing** using **unittest** for testing task operations.
// - **Integration testing** using **unittest** for end-to-end application behavior.

// This example also utilizes **Mill** for managing `dependencies`, `builds`, and `tests`,
// offering an efficient development workflow.

/** Usage

> ./mill todo.test
...
test_add_todo (test.TestTodoApp...) ... ok
test_delete_todo (test.TestTodoApp...) ... ok
test_edit_todo (test.TestTodoApp...) ... ok
test_filter_todos (test.TestTodoApp...) ... ok
test_toggle_all (test.TestTodoApp...) ... ok
test_toggle_todo (test.TestTodoApp...) ... ok
...Ran 6 tests...
OK
...

> ./mill todo.itest
...
test_add_and_list_todos (test.TestTodoAppIntegration...) ... ok
test_delete_todo (test.TestTodoAppIntegration...) ... ok
test_edit_and_list_todos (test.TestTodoAppIntegration...) ... ok
test_toggle_all_todos (test.TestTodoAppIntegration...) ... ok
test_toggle_and_list_todos (test.TestTodoAppIntegration...) ... ok
...Ran 5 tests...
OK
...

> ./mill todo.runBackground

> curl http://localhost:5001
...What needs to be done...

> ./mill clean todo.runBackground

*/
68 changes: 68 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/itest/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import unittest
from app import app, todos


class TestTodoAppIntegration(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Set up the test client for the app
cls.client = app.test_client()

def setUp(self):
# Clear the todos list before each test
global todos
todos.clear()

def test_add_and_list_todos(self):
# Test adding a todo and listing all todos
response = self.client.post("/add/all", data="Test Todo")
self.assertEqual(response.status_code, 200)
# Fetch the todos list and verify the item was added
response = self.client.post("/list/all", data="")
self.assertIn(b"Test Todo", response.data)

def test_toggle_and_list_todos(self):
# Test adding a todo, toggling it, and listing active/completed todos
self.client.post("/add/all", data="Test Todo")
response = self.client.post("/toggle/all/0", data="")
# Check if the todo is toggled
self.assertEqual(response.status_code, 200)
# Now, test filtering todos based on active/completed state
response = self.client.post("/list/active", data="")
self.assertNotIn(b"Test Todo", response.data)
response = self.client.post("/list/completed", data="")
self.assertIn(b"Test Todo", response.data)

def test_edit_and_list_todos(self):
# Test adding a todo, editing it, and then verifying the updated text
self.client.post("/add/all", data="Test Todo")
response = self.client.post("/edit/all/0", data="Updated Todo")
# Check that the todo was updated
response = self.client.post("/list/all", data="")
self.assertIn(b"Updated Todo", response.data)
self.assertNotIn(b"Test Todo", response.data)

def test_delete_todo(self):
# Test adding and deleting a todo
self.client.post("/add/all", data="Test Todo")
response = self.client.post("/delete/all/0", data="")
# Verify that the todo was deleted
response = self.client.post("/list/all", data="")
self.assertNotIn(b"Test Todo", response.data)

def test_toggle_all_todos(self):
# Test toggling all todos
self.client.post("/add/all", data="Todo 1")
self.client.post("/add/all", data="Todo 2")
response = self.client.post("/toggle-all/all", data="")
response = self.client.post("/list/completed", data="")
self.assertIn(b"Todo 1", response.data)
self.assertIn(b"Todo 2", response.data)
response = self.client.post("/toggle-all/all", data="")
response = self.client.post("/list/active", data="")
self.assertIn(b"Todo 1", response.data)
self.assertIn(b"Todo 2", response.data)


if __name__ == "__main__":
unittest.main()
96 changes: 96 additions & 0 deletions example/pythonlib/web/2-todo-flask/todo/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from flask import Flask, render_template, request
from dataclasses import dataclass
from typing import List

app = Flask(__name__, static_folder="../static", template_folder="../templates")


@dataclass
class Todo:
checked: bool
text: str


todos: List[Todo] = []


@app.route("/")
def index():
return render_template("base.html", todos=todos, state="all")


def render_body(state: str):
filtered_todos = {
"all": todos,
"active": [todo for todo in todos if not todo.checked],
"completed": [todo for todo in todos if todo.checked],
}[state]
return render_template("index.html", todos=filtered_todos, state=state)


def filter_todos(state):
"""Filter todos based on the state (all, active, completed)."""
if state == "all":
return todos
elif state == "active":
return [todo for todo in todos if not todo.checked]
elif state == "completed":
return [todo for todo in todos if todo.checked]


@app.route("/edit/<state>/<int:index>", methods=["POST"])
def edit_todo(state, index):
"""Edit the text of a todo."""
global todos
updated_text = request.data.decode("utf-8")
# Update the text attribute of the Todo object
todos[index].text = updated_text
filtered_todos = filter_todos(state)
return render_template("index.html", todos=filtered_todos, state=state)


@app.route("/list/<state>", methods=["POST"])
def list_todos(state):
return render_body(state)


@app.route("/add/<state>", methods=["POST"])
def add_todo(state):
todos.insert(0, Todo(checked=False, text=request.data.decode("utf-8")))
return render_body(state)


@app.route("/delete/<state>/<int:index>", methods=["POST"])
def delete_todo(state, index):
if 0 <= index < len(todos):
todos.pop(index)
return render_body(state)


@app.route("/toggle/<state>/<int:index>", methods=["POST"])
def toggle(state, index):
if 0 <= index < len(todos):
todos[index].checked = not todos[index].checked
return render_body(state)


@app.route("/clear-completed/<state>", methods=["POST"])
def clear_completed(state):
global todos
todos = [todo for todo in todos if not todo.checked]
return render_body(state)


@app.route("/toggle-all/<state>", methods=["POST"])
def toggle_all(state):
global todos

all_checked = all(todo.checked for todo in todos)
for todo in todos:
todo.checked = not all_checked

return render_body(state)


if __name__ == "__main__":
app.run(debug=True, port=5001)
Loading
Loading