diff --git a/example/package.mill b/example/package.mill index 1616927e8ca..c1fd451dbbf 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 module extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "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..2fc70becb84 --- /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.runBackground + +*/ 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/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 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..fb5158d0f70 --- /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.runBackground + +*/ 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/src/mill/pythonlib/PythonModule.scala b/pythonlib/src/mill/pythonlib/PythonModule.scala index 9867c0f28e9..c5d247c66a7 100644 --- a/pythonlib/src/mill/pythonlib/PythonModule.scala +++ b/pythonlib/src/mill/pythonlib/PythonModule.scala @@ -117,6 +117,7 @@ trait PythonModule extends PipModule with TaskModule { outer => */ def bundleOptions: T[Seq[String]] = Task { Seq("--scie", "eager") } + // 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 { @@ -167,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" /** @@ -234,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(Task.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 = Task.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) }