From 3eb12e1e75947dadb0823884a397bcfae75e37d3 Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Sun, 8 Dec 2024 19:54:47 +0530 Subject: [PATCH 1/4] Web Examples --- .gitignore | 4 +- example/package.mill | 1 + .../pythonlib/web/1-hello-flask/build.mill | 32 +++++ .../web/1-hello-flask/foo/src/foo.py | 10 ++ .../web/1-hello-flask/foo/test/src/test.py | 17 +++ .../pythonlib/web/3-hello-django/build.mill | 31 +++++ .../3-hello-django/foo/src/app/__init__.py | 0 .../web/3-hello-django/foo/src/app/asgi.py | 16 +++ .../3-hello-django/foo/src/app/settings.py | 124 ++++++++++++++++++ .../web/3-hello-django/foo/src/app/urls.py | 24 ++++ .../web/3-hello-django/foo/src/app/wsgi.py | 16 +++ .../3-hello-django/foo/src/main/__init__.py | 0 .../web/3-hello-django/foo/src/main/admin.py | 3 + .../web/3-hello-django/foo/src/main/apps.py | 6 + .../foo/src/main/migrations/__init__.py | 0 .../web/3-hello-django/foo/src/main/models.py | 3 + .../web/3-hello-django/foo/src/main/tests.py | 12 ++ .../web/3-hello-django/foo/src/main/views.py | 5 + .../web/3-hello-django/foo/src/manage.py | 22 ++++ pythonlib/package.mill | 1 + .../src/mill/pythonlib/PythonModule.scala | 76 +++++++++-- .../MillBackgroundWrapper.java | 30 ++++- scalalib/src/mill/scalalib/RunModule.scala | 12 +- 23 files changed, 424 insertions(+), 21 deletions(-) create mode 100644 example/pythonlib/web/1-hello-flask/build.mill create mode 100644 example/pythonlib/web/1-hello-flask/foo/src/foo.py create mode 100644 example/pythonlib/web/1-hello-flask/foo/test/src/test.py create mode 100644 example/pythonlib/web/3-hello-django/build.mill create mode 100644 example/pythonlib/web/3-hello-django/foo/src/app/__init__.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/app/asgi.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/app/settings.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/app/urls.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/app/wsgi.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/__init__.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/admin.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/apps.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/migrations/__init__.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/models.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/tests.py create mode 100644 example/pythonlib/web/3-hello-django/foo/src/main/views.py create mode 100755 example/pythonlib/web/3-hello-django/foo/src/manage.py diff --git a/.gitignore b/.gitignore index 70b9505b86a..e98cf1c7b51 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ lowered.hnir node_modules/ dist/ build/ -*.bak \ No newline at end of file +*.bak +db.sqlite3 +__pycache__ \ No newline at end of file diff --git a/example/package.mill b/example/package.mill index 1f214fe09b2..f6437bf8534 100644 --- a/example/package.mill +++ b/example/package.mill @@ -68,6 +68,7 @@ object `package` extends RootModule with Module { object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic")) object dependencies extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "dependencies")) object publishing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "publishing")) + object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web")) } object cli extends Module{ diff --git a/example/pythonlib/web/1-hello-flask/build.mill b/example/pythonlib/web/1-hello-flask/build.mill new file mode 100644 index 00000000000..1405b60501b --- /dev/null +++ b/example/pythonlib/web/1-hello-flask/build.mill @@ -0,0 +1,32 @@ +package build +import mill._, pythonlib._ + +object foo extends PythonModule { + + def mainScript = Task.Source { millSourcePath / "src" / "foo.py" } + + def pythonDeps = Seq("flask==3.1.0") + + object test extends PythonTests with TestModule.Unittest + +} + +/** Usage + +> ./mill foo.test +... +test_hello_flask (test.TestScript...) +Test the '/' endpoint. ... ok +... +Ran 1 test... +OK +... + +> ./mill foo.runBackground + +> curl http://localhost:5000 +...

Hello, Mill!

... + +> ./mill clean foo.runBackgrouund + +*/ diff --git a/example/pythonlib/web/1-hello-flask/foo/src/foo.py b/example/pythonlib/web/1-hello-flask/foo/src/foo.py new file mode 100644 index 00000000000..ca92ec70e52 --- /dev/null +++ b/example/pythonlib/web/1-hello-flask/foo/src/foo.py @@ -0,0 +1,10 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello_world(): + return "

Hello, Mill!

" + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/example/pythonlib/web/1-hello-flask/foo/test/src/test.py b/example/pythonlib/web/1-hello-flask/foo/test/src/test.py new file mode 100644 index 00000000000..982e222e474 --- /dev/null +++ b/example/pythonlib/web/1-hello-flask/foo/test/src/test.py @@ -0,0 +1,17 @@ +import unittest +from foo import app # type: ignore + +class TestScript(unittest.TestCase): + def setUp(self): + """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() diff --git a/example/pythonlib/web/3-hello-django/build.mill b/example/pythonlib/web/3-hello-django/build.mill new file mode 100644 index 00000000000..205dda49b0d --- /dev/null +++ b/example/pythonlib/web/3-hello-django/build.mill @@ -0,0 +1,31 @@ +package build +import mill._, pythonlib._ + +object foo extends PythonModule { + + def mainScript = Task.Source { millSourcePath / "src" / "manage.py" } + + def pythonDeps = Seq("django==5.1.4") + +} + +/** Usage + +> ./mill foo.run test main -v 2 # using inbuilt `django test`, `main` is the app name, `-v 2` is verbosity level 2 +... +System check identified no issues (0 silenced). +test_index_view (main.tests.TestScript...) +Test that the index view returns a 200 status code ... ok +... +Ran 1 test... +OK +... + +> ./mill foo.runBackground runserver + +> curl http://localhost:8000 +...

Hello, Mill!

... + +> ./mill clean foo.runBackgrouund + +*/ diff --git a/example/pythonlib/web/3-hello-django/foo/src/app/__init__.py b/example/pythonlib/web/3-hello-django/foo/src/app/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/example/pythonlib/web/3-hello-django/foo/src/app/asgi.py b/example/pythonlib/web/3-hello-django/foo/src/app/asgi.py new file mode 100644 index 00000000000..df7e978063c --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/app/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for app project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + +application = get_asgi_application() diff --git a/example/pythonlib/web/3-hello-django/foo/src/app/settings.py b/example/pythonlib/web/3-hello-django/foo/src/app/settings.py new file mode 100644 index 00000000000..3c40a0f9649 --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/app/settings.py @@ -0,0 +1,124 @@ +""" +Django settings for app project. + +Generated by 'django-admin startproject' using Django 5.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-7@1hg!(c00%z)t82=^_mu02sxa$nlex_xc!7j++18z5w4dc(iu' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'main', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'app.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'app.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/example/pythonlib/web/3-hello-django/foo/src/app/urls.py b/example/pythonlib/web/3-hello-django/foo/src/app/urls.py new file mode 100644 index 00000000000..879e54d05df --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/app/urls.py @@ -0,0 +1,24 @@ +""" +URL configuration for app project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from main import views + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', views.index, name="homepage") +] diff --git a/example/pythonlib/web/3-hello-django/foo/src/app/wsgi.py b/example/pythonlib/web/3-hello-django/foo/src/app/wsgi.py new file mode 100644 index 00000000000..829fcc707bb --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/app/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for app project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + +application = get_wsgi_application() diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/__init__.py b/example/pythonlib/web/3-hello-django/foo/src/main/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/admin.py b/example/pythonlib/web/3-hello-django/foo/src/main/admin.py new file mode 100644 index 00000000000..8c38f3f3dad --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/main/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/apps.py b/example/pythonlib/web/3-hello-django/foo/src/main/apps.py new file mode 100644 index 00000000000..167f04426e4 --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/main/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MainConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'main' diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/migrations/__init__.py b/example/pythonlib/web/3-hello-django/foo/src/main/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/models.py b/example/pythonlib/web/3-hello-django/foo/src/main/models.py new file mode 100644 index 00000000000..71a83623907 --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/main/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/tests.py b/example/pythonlib/web/3-hello-django/foo/src/main/tests.py new file mode 100644 index 00000000000..aa6ec79bd6a --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/main/tests.py @@ -0,0 +1,12 @@ +from django.test import TestCase +from django.urls import reverse + +class TestScript(TestCase): + def test_index_view(self): + """ + Test that the index view returns a 200 status code + and the expected HTML content. + """ + response = self.client.get(reverse('homepage')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, '

Hello, Mill!

') diff --git a/example/pythonlib/web/3-hello-django/foo/src/main/views.py b/example/pythonlib/web/3-hello-django/foo/src/main/views.py new file mode 100644 index 00000000000..05b8dc7a8f6 --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/main/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render +from django.http import HttpResponse + +def index(request): + return HttpResponse('

Hello, Mill!

') \ No newline at end of file diff --git a/example/pythonlib/web/3-hello-django/foo/src/manage.py b/example/pythonlib/web/3-hello-django/foo/src/manage.py new file mode 100755 index 00000000000..49313893cbd --- /dev/null +++ b/example/pythonlib/web/3-hello-django/foo/src/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/pythonlib/package.mill b/pythonlib/package.mill index cbc3a6a73f2..8e9f58da976 100644 --- a/pythonlib/package.mill +++ b/pythonlib/package.mill @@ -9,4 +9,5 @@ object `package` extends RootModule with build.MillPublishScalaModule { // we depend on scalalib for re-using some common infrastructure (e.g. License // management of projects), NOT for reusing build logic def moduleDeps = Seq(build.main, build.scalalib) + def testTransitiveDeps = super.testTransitiveDeps() ++ Seq(build.scalalib.backgroundwrapper.testDep()) } diff --git a/pythonlib/src/mill/pythonlib/PythonModule.scala b/pythonlib/src/mill/pythonlib/PythonModule.scala index 219673a3254..3de68c7dfb9 100644 --- a/pythonlib/src/mill/pythonlib/PythonModule.scala +++ b/pythonlib/src/mill/pythonlib/PythonModule.scala @@ -75,6 +75,13 @@ trait PythonModule extends PipModule with TaskModule { outer => */ def unmanagedPythonPath: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + /** + * Folders containing source files that are generated rather than + * handwritten; these files can be generated in this target itself, + * or can refer to files generated from other targets + */ + def generatedSources: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + /** * The directories used to construct the PYTHONPATH for this module, used for * execution, excluding upstream modules. @@ -83,7 +90,7 @@ trait PythonModule extends PipModule with TaskModule { outer => * directories. */ def localPythonPath: T[Seq[PathRef]] = Task { - sources() ++ resources() ++ unmanagedPythonPath() + sources() ++ resources() ++ generatedSources() ++ unmanagedPythonPath() } /** @@ -95,21 +102,41 @@ trait PythonModule extends PipModule with TaskModule { outer => localPythonPath() ++ upstream } + /** + * Any environment variables you want to pass to the forked Env + */ + def forkEnv: T[Map[String, String]] = Task { Map.empty[String, String] } + + /** + * Command-line options to pass to the Python Interpreter defined by the user. + */ + def pythonOptions: T[Seq[String]] = Task { Seq.empty[String] } + + /** + * Command-line options to pass as bundle configuration defined by the user. + */ + def bundleOptions: T[Seq[String]] = Task { Seq.empty[String] } + // TODO: right now, any task that calls this helper will have its own python // cache. This is slow. Look into sharing the cache between tasks. def runner: Task[PythonModule.Runner] = Task.Anon { new PythonModule.RunnerImpl( command0 = pythonExe().path.toString, - env0 = Map( - "PYTHONPATH" -> transitivePythonPath().map(_.path).mkString(java.io.File.pathSeparator), - "PYTHONPYCACHEPREFIX" -> (T.dest / "cache").toString, - if (Task.log.colored) { "FORCE_COLOR" -> "1" } - else { "NO_COLOR" -> "1" } - ), + options = pythonOptions(), + env0 = runnerEnvTask() ++ forkEnv(), workingDir0 = Task.workspace ) } + private def runnerEnvTask = Task.Anon { + Map( + "PYTHONPATH" -> transitivePythonPath().map(_.path).mkString(java.io.File.pathSeparator), + "PYTHONPYCACHEPREFIX" -> (T.dest / "cache").toString, + if (Task.log.colored) { "FORCE_COLOR" -> "1" } + else { "NO_COLOR" -> "1" } + ) + } + /** * Run a typechecker on this module. */ @@ -169,6 +196,7 @@ trait PythonModule extends PipModule with TaskModule { outer => "--exe", mainScript().path, "-o", pexFile, "--scie", "eager", + bundleOptions() // format: on ), workingDir = T.dest @@ -176,6 +204,37 @@ trait PythonModule extends PipModule with TaskModule { outer => PathRef(pexFile) } + /** + * Run the main python script of this module. + * + * @see [[mainScript]] + */ + def runBackground(args: mill.define.Args) = Task.Command { + val (procUuidPath, procLockfile, procUuid) = mill.scalalib.RunModule.backgroundSetup(T.dest) + + Jvm.runSubprocess( + mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", + classPath = mill.scalalib.ZincWorkerModule.backgroundWrapperClasspath().map(_.path).toSeq, + jvmArgs = Nil, + envArgs = runnerEnvTask(), + mainArgs = Seq( + procUuidPath.toString, + procLockfile.toString, + procUuid, + "500", + "", + pythonExe().path.toString, + mainScript().path.toString + ) ++ args.value, + workingDir = T.workspace, + background = true, + useCpPassingJar = false, + runBackgroundLogToConsole = true, + javaHome = mill.scalalib.ZincWorkerModule.javaHome().map(_.path) + ) + () + } + trait PythonTests extends PythonModule { override def moduleDeps: Seq[PythonModule] = Seq(outer) } @@ -194,6 +253,7 @@ object PythonModule { private class RunnerImpl( command0: String, + options: Seq[String], env0: Map[String, String], workingDir0: os.Path ) extends Runner { @@ -204,7 +264,7 @@ object PythonModule { workingDir: os.Path = null )(implicit ctx: Ctx): Unit = Jvm.runSubprocess( - commandArgs = Seq(Option(command).getOrElse(command0)) ++ args.value, + commandArgs = Seq(Option(command).getOrElse(command0)) ++ options ++ args.value, envArgs = Option(env).getOrElse(env0), workingDir = Option(workingDir).getOrElse(workingDir0) ) diff --git a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java index ebdd96f84d9..59b134eb1c9 100644 --- a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java +++ b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java @@ -2,10 +2,7 @@ import java.io.RandomAccessFile; import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; +import java.nio.file.*; public class MillBackgroundWrapper { public static void main(String[] args) throws Exception { @@ -55,6 +52,27 @@ public static void main(String[] args) throws Exception { // Actually start the Java main method we wanted to run in the background String realMain = args[4]; String[] realArgs = java.util.Arrays.copyOfRange(args, 5, args.length); - Class.forName(realMain).getMethod("main", String[].class).invoke(null, (Object) realArgs); + if (!realMain.equals("")) { + Class.forName(realMain).getMethod("main", String[].class).invoke(null, (Object) realArgs); + } else { + Process subprocess = new ProcessBuilder().command(realArgs).inheritIO().start(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + subprocess.destroy(); + + long now = System.currentTimeMillis(); + + while (subprocess.isAlive() && System.currentTimeMillis() - now < 100) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + if (subprocess.isAlive()) { + subprocess.destroyForcibly(); + } + } + })); + System.exit(subprocess.waitFor()); + } } -} +} \ No newline at end of file diff --git a/scalalib/src/mill/scalalib/RunModule.scala b/scalalib/src/mill/scalalib/RunModule.scala index a371830e1e7..04cd5f41505 100644 --- a/scalalib/src/mill/scalalib/RunModule.scala +++ b/scalalib/src/mill/scalalib/RunModule.scala @@ -166,7 +166,7 @@ trait RunModule extends WithZincWorker { def runBackgroundTask(mainClass: Task[String], args: Task[Args] = Task.Anon(Args())): Task[Unit] = Task.Anon { - val (procUuidPath, procLockfile, procUuid) = backgroundSetup(T.dest) + val (procUuidPath, procLockfile, procUuid) = RunModule.backgroundSetup(T.dest) runner().run( args = Seq( procUuidPath.toString, @@ -207,7 +207,7 @@ trait RunModule extends WithZincWorker { runUseArgsFile: Boolean, backgroundOutputs: Option[Tuple2[ProcessOutput, ProcessOutput]] )(args: String*): Ctx => Result[Unit] = ctx => { - val (procUuidPath, procLockfile, procUuid) = backgroundSetup(taskDest) + val (procUuidPath, procLockfile, procUuid) = RunModule.backgroundSetup(taskDest) try Result.Success( Jvm.runSubprocessWithBackgroundOutputs( "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", @@ -231,16 +231,16 @@ trait RunModule extends WithZincWorker { Result.Failure("subprocess failed") } } +} + +object RunModule { - private[this] def backgroundSetup(dest: os.Path): (Path, Path, String) = { + private[mill] def backgroundSetup(dest: os.Path): (Path, Path, String) = { val procUuid = java.util.UUID.randomUUID().toString val procUuidPath = dest / ".mill-background-process-uuid" val procLockfile = dest / ".mill-background-process-lock" (procUuidPath, procLockfile, procUuid) } -} - -object RunModule { trait Runner { def run( args: os.Shellable, From 042c4274151ddcd1d5401f8e0df1a774ddacd72c Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Sun, 8 Dec 2024 20:14:49 +0530 Subject: [PATCH 2/4] Fix Typos --- example/pythonlib/web/1-hello-flask/build.mill | 2 +- example/pythonlib/web/3-hello-django/build.mill | 2 +- .../mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/pythonlib/web/1-hello-flask/build.mill b/example/pythonlib/web/1-hello-flask/build.mill index 1405b60501b..2fc70becb84 100644 --- a/example/pythonlib/web/1-hello-flask/build.mill +++ b/example/pythonlib/web/1-hello-flask/build.mill @@ -27,6 +27,6 @@ OK > curl http://localhost:5000 ...

Hello, Mill!

... -> ./mill clean foo.runBackgrouund +> ./mill clean foo.runBackground */ diff --git a/example/pythonlib/web/3-hello-django/build.mill b/example/pythonlib/web/3-hello-django/build.mill index 205dda49b0d..fb5158d0f70 100644 --- a/example/pythonlib/web/3-hello-django/build.mill +++ b/example/pythonlib/web/3-hello-django/build.mill @@ -26,6 +26,6 @@ OK > curl http://localhost:8000 ...

Hello, Mill!

... -> ./mill clean foo.runBackgrouund +> ./mill clean foo.runBackground */ diff --git a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java index 59b134eb1c9..c7d9947c884 100644 --- a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java +++ b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java @@ -75,4 +75,4 @@ public static void main(String[] args) throws Exception { System.exit(subprocess.waitFor()); } } -} \ No newline at end of file +} From b9f93088db9e567e172748c446ddb003cf2e6714 Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Mon, 9 Dec 2024 18:01:25 +0530 Subject: [PATCH 3/4] todo-flask[wip] --- .gitignore | 4 +- example/pythonlib/web/2-todo-flask/build.mill | 24 ++++++ .../web/2-todo-flask/todo/src/app.py | 74 +++++++++++++++++++ .../web/2-todo-flask/todo/src/forms.py | 10 +++ .../web/2-todo-flask/todo/src/models.py | 14 ++++ .../web/2-todo-flask/todo/static/style.css | 21 ++++++ .../web/2-todo-flask/todo/templates/base.html | 31 ++++++++ .../2-todo-flask/todo/templates/index.html | 30 ++++++++ .../web/2-todo-flask/todo/templates/task.html | 24 ++++++ .../web/2-todo-flask/todo/test/src/test.py | 1 + 10 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 example/pythonlib/web/2-todo-flask/build.mill create mode 100644 example/pythonlib/web/2-todo-flask/todo/src/app.py create mode 100644 example/pythonlib/web/2-todo-flask/todo/src/forms.py create mode 100644 example/pythonlib/web/2-todo-flask/todo/src/models.py create mode 100644 example/pythonlib/web/2-todo-flask/todo/static/style.css create mode 100644 example/pythonlib/web/2-todo-flask/todo/templates/base.html create mode 100644 example/pythonlib/web/2-todo-flask/todo/templates/index.html create mode 100644 example/pythonlib/web/2-todo-flask/todo/templates/task.html create mode 100644 example/pythonlib/web/2-todo-flask/todo/test/src/test.py diff --git a/.gitignore b/.gitignore index e98cf1c7b51..70b9505b86a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,4 @@ lowered.hnir node_modules/ dist/ build/ -*.bak -db.sqlite3 -__pycache__ \ No newline at end of file +*.bak \ No newline at end of file diff --git a/example/pythonlib/web/2-todo-flask/build.mill b/example/pythonlib/web/2-todo-flask/build.mill new file mode 100644 index 00000000000..c3523519099 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/build.mill @@ -0,0 +1,24 @@ +package build +import mill._, pythonlib._ + +object todo extends PythonModule { + + def mainScript = Task.Source { millSourcePath / "src" / "app.py" } + + def pythonDeps = Seq("flask==3.1.0", "Flask-SQLAlchemy==3.1.1", "Flask-WTF==1.2.2") + + object test extends PythonTests with TestModule.Unittest + +} + +// TODO: Testing will be added soon... + +/** Usage + +> ./mill todo.runBackground + +> curl http://localhost:5001 + +> ./mill clean todo.runBackground + +*/ diff --git a/example/pythonlib/web/2-todo-flask/todo/src/app.py b/example/pythonlib/web/2-todo-flask/todo/src/app.py new file mode 100644 index 00000000000..0cebf1afb94 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/src/app.py @@ -0,0 +1,74 @@ +from flask import Flask, render_template, redirect, url_for, flash, request +from flask_sqlalchemy import SQLAlchemy +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SelectField, DateField, SubmitField +from wtforms.validators import DataRequired, Length + +# Initialize Flask App and Database +app = Flask(__name__, static_folder="../static", template_folder="../templates") +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///todo.db" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +app.config["SECRET_KEY"] = "your_secret_key" + +# Import models +from models import Task, db + +# Import forms +from forms import TaskForm + +db.init_app(app) + + +# Routes +@app.route("/") +def index(): + tasks = Task.query.all() + return render_template("index.html", tasks=tasks) + + +@app.route("/add", methods=["GET", "POST"]) +def add_task(): + form = TaskForm() + if form.validate_on_submit(): + new_task = Task( + title=form.title.data, + description=form.description.data, + status=form.status.data, + deadline=form.deadline.data, + ) + db.session.add(new_task) + db.session.commit() + flash("Task added successfully!", "success") + return redirect(url_for("index")) + return render_template("task.html", form=form, title="Add Task") + + +@app.route("/edit/", methods=["GET", "POST"]) +def edit_task(task_id): + task = Task.query.get_or_404(task_id) + form = TaskForm(obj=task) + if form.validate_on_submit(): + task.title = form.title.data + task.description = form.description.data + task.status = form.status.data + task.deadline = form.deadline.data + db.session.commit() + flash("Task updated successfully!", "success") + return redirect(url_for("index")) + return render_template("task.html", form=form, title="Edit Task") + + +@app.route("/delete/") +def delete_task(task_id): + task = Task.query.get_or_404(task_id) + db.session.delete(task) + db.session.commit() + flash("Task deleted successfully!", "success") + return redirect(url_for("index")) + + +# Create database tables and run the app +if __name__ == "__main__": + with app.app_context(): + db.create_all() + app.run(debug=True, port=5001) diff --git a/example/pythonlib/web/2-todo-flask/todo/src/forms.py b/example/pythonlib/web/2-todo-flask/todo/src/forms.py new file mode 100644 index 00000000000..be10adcff0f --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/src/forms.py @@ -0,0 +1,10 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SelectField, DateField, SubmitField +from wtforms.validators import DataRequired, Length + +class TaskForm(FlaskForm): + title = StringField('Title', validators=[DataRequired(), Length(max=100)]) + description = TextAreaField('Description') + status = SelectField('Status', choices=[('Pending', 'Pending'), ('Completed', 'Completed')]) + deadline = DateField('Deadline', format='%Y-%m-%d', validators=[DataRequired()]) + submit = SubmitField('Save') diff --git a/example/pythonlib/web/2-todo-flask/todo/src/models.py b/example/pythonlib/web/2-todo-flask/todo/src/models.py new file mode 100644 index 00000000000..93bd7c82bf4 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/src/models.py @@ -0,0 +1,14 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class Task(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text, nullable=True) + status = db.Column(db.String(20), default='Pending') # Options: Pending, Completed + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + deadline = db.Column(db.Date) + + def __repr__(self): + return f'' diff --git a/example/pythonlib/web/2-todo-flask/todo/static/style.css b/example/pythonlib/web/2-todo-flask/todo/static/style.css new file mode 100644 index 00000000000..54f07a91f48 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/static/style.css @@ -0,0 +1,21 @@ +body { + background-color: #f8f9fa; +} + +.navbar-brand { + font-weight: bold; +} + +.table th, +.table td { + text-align: center; + vertical-align: middle; +} + +.btn { + margin-right: 5px; +} + +.container { + max-width: 900px; +} diff --git a/example/pythonlib/web/2-todo-flask/todo/templates/base.html b/example/pythonlib/web/2-todo-flask/todo/templates/base.html new file mode 100644 index 00000000000..7beef670609 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/templates/base.html @@ -0,0 +1,31 @@ + + + + + + Flask To-Do App + + + + + +
+ {% with messages = get_flashed_messages(with_categories=True) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + diff --git a/example/pythonlib/web/2-todo-flask/todo/templates/index.html b/example/pythonlib/web/2-todo-flask/todo/templates/index.html new file mode 100644 index 00000000000..8d1619a4758 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/templates/index.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} +{% block content %} +

Task List

+Add Task + + + + + + + + + + + + {% for task in tasks %} + + + + + + + + {% endfor %} + +
#TitleStatusDeadlineActions
{{ loop.index }}{{ task.title }}{{ task.status }}{{ task.deadline }} + Edit + Delete +
+{% endblock %} diff --git a/example/pythonlib/web/2-todo-flask/todo/templates/task.html b/example/pythonlib/web/2-todo-flask/todo/templates/task.html new file mode 100644 index 00000000000..672696454a9 --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/templates/task.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} +

{{ title }}

+
+ {{ form.hidden_tag() }} +
+ {{ form.title.label }}
+ {{ form.title(class="form-control") }} +
+
+ {{ form.description.label }}
+ {{ form.description(class="form-control") }} +
+
+ {{ form.status.label }}
+ {{ form.status(class="form-control") }} +
+
+ {{ form.deadline.label }}
+ {{ form.deadline(class="form-control") }} +
+ +
+{% endblock %} diff --git a/example/pythonlib/web/2-todo-flask/todo/test/src/test.py b/example/pythonlib/web/2-todo-flask/todo/test/src/test.py new file mode 100644 index 00000000000..ab5258cae2a --- /dev/null +++ b/example/pythonlib/web/2-todo-flask/todo/test/src/test.py @@ -0,0 +1 @@ +# Work in Progress... \ No newline at end of file From 52b4b8fbb6a6b10d0f1714c0ac590a47b07cf042 Mon Sep 17 00:00:00 2001 From: Himanshu Mahajan <83700343+himanshumahajan138@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:54:25 +0530 Subject: [PATCH 4/4] Update PythonModule.scala --- .../src/mill/pythonlib/PythonModule.scala | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/pythonlib/src/mill/pythonlib/PythonModule.scala b/pythonlib/src/mill/pythonlib/PythonModule.scala index 75f453befa5..c5d247c66a7 100644 --- a/pythonlib/src/mill/pythonlib/PythonModule.scala +++ b/pythonlib/src/mill/pythonlib/PythonModule.scala @@ -168,37 +168,6 @@ trait PythonModule extends PipModule with TaskModule { outer => ) } - /** - * Run the main python script of this module. - * - * @see [[mainScript]] - */ - def runBackground(args: mill.define.Args) = Task.Command { - val (procUuidPath, procLockfile, procUuid) = mill.scalalib.RunModule.backgroundSetup(T.dest) - - Jvm.runSubprocess( - mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", - classPath = mill.scalalib.ZincWorkerModule.backgroundWrapperClasspath().map(_.path).toSeq, - jvmArgs = Nil, - envArgs = runnerEnvTask(), - mainArgs = Seq( - procUuidPath.toString, - procLockfile.toString, - procUuid, - "500", - "", - pythonExe().path.toString, - mainScript().path.toString - ) ++ args.value, - workingDir = T.workspace, - background = true, - useCpPassingJar = false, - runBackgroundLogToConsole = true, - javaHome = mill.scalalib.ZincWorkerModule.javaHome().map(_.path) - ) - () - } - override def defaultCommandName(): String = "run" /** @@ -241,7 +210,7 @@ trait PythonModule extends PipModule with TaskModule { outer => * @see [[mainScript]] */ def runBackground(args: mill.define.Args) = Task.Command { - val (procUuidPath, procLockfile, procUuid) = mill.scalalib.RunModule.backgroundSetup(T.dest) + val (procUuidPath, procLockfile, procUuid) = mill.scalalib.RunModule.backgroundSetup(Task.dest) Jvm.runSubprocess( mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", @@ -257,7 +226,7 @@ trait PythonModule extends PipModule with TaskModule { outer => pythonExe().path.toString, mainScript().path.toString ) ++ args.value, - workingDir = T.workspace, + workingDir = Task.workspace, background = true, useCpPassingJar = false, runBackgroundLogToConsole = true,