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

Fixed: #3928 [Basic] Re-Work for First Class Python Support #4000

Merged
merged 19 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
** xref:kotlinlib/publishing.adoc[]
// ** xref:kotlinlib/build-examples.adoc[]
** xref:kotlinlib/web-examples.adoc[]
* xref:pythonlib/intro.adoc[]

// Future Additions
// ** xref:pythonlib/module-config.adoc[]
// ** xref:pythonlib/dependencies.adoc[]
// ** xref:pythonlib/testing.adoc[]
// ** xref:pythonlib/linting.adoc[]
// ** xref:pythonlib/publishing.adoc[]
// ** xref:pythonlib/build-examples.adoc[]
// ** xref:pythonlib/web-examples.adoc[]

* (Experimental) Android with Mill
** xref:android/java.adoc[]
** xref:android/kotlin.adoc[]
Expand Down
14 changes: 12 additions & 2 deletions docs/modules/ROOT/pages/pythonlib/intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
include::partial$gtag-config.adoc[]


:language: Java
:language-small: java
:language: python
:language-small: python

include::partial$Intro_Header.adoc[]

== Simple Python Module

include::partial$example/pythonlib/basic/1-simple.adoc[]

== Custom Build Logic

include::partial$example/pythonlib/basic/2-custom-build-logic.adoc[]

== Multi-Module Project

include::partial$example/pythonlib/basic/3-multi-module.adoc[]
36 changes: 35 additions & 1 deletion example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ object `package` extends RootModule with Module {
object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic"))
}
object pythonlib extends Module {
object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic"))
object basic extends Cross[ExampleCrossModulePython](build.listIn(millSourcePath / "basic"))
}

object cli extends Module{
Expand Down Expand Up @@ -99,6 +99,40 @@ object `package` extends RootModule with Module {
object typescript extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "typescript"))
}

trait ExampleCrossModulePython extends ExampleCrossModuleJava {
override def lineTransform(line: String) ={
this.millModuleSegments.parts.last match {
case "1-simple" =>
val updatedLine = line
.replace("xref:{language-small}lib/web-examples.adoc", "link:") // Need updated link
.replace("xref:{language-small}lib/build-examples.adoc", "link:") // Need updated link
.replace("compile", "typeCheck")
.replace("Scala console", "Python console")
.replace("Ammonite Scala", "Python")
.replace("assembly", "typeCheck")
.replace(s"// $$ mill jar # bundle the classfiles into a jar suitable for publishing", "")
.replace("foo.scalaVersion", "foo.typeCheck")
updatedLine
case "2-custom-build-logic" =>
val updatedLine = line
.replace("17", "10") // it's just the change for page count
.replace("`allSourceFiles` (an existing task)", "`allSourceFiles`")
updatedLine

case "3-multi-module" =>
val updatedLine = line
.replace("compiled", "typeChecked")
.replace("compile", "typeCheck")
.replace("...bar.BarTests...simple...", "test_escaping (...test.TestScript...) ... ok")
.replace("...bar.BarTests...escaping...", "test_simple (...test.TestScript...) ... ok")
updatedLine

case _ => line

}
}
}

trait ExampleCrossModuleKotlin extends ExampleCrossModuleJava {

override def lineTransform(line: String) = this.millModuleSegments.parts.last match {
Expand Down
99 changes: 80 additions & 19 deletions example/pythonlib/basic/1-simple/build.mill
Original file line number Diff line number Diff line change
@@ -1,34 +1,95 @@
//// SNIPPET:BUILD
package build
import mill._, pythonlib._

// Docs for PythonLib
object foo extends PythonModule {
object bar extends PythonModule {
def pythonDeps = Seq("pandas==2.2.3", "numpy==2.1.3")
}
def pythonDeps = Seq("numpy==2.1.3")
}

object qux extends PythonModule { q =>
def moduleDeps = Seq(foo, foo.bar)
def mainScript = Task.Source { millSourcePath / "src" / "foo.py" }

def pythonDeps = Agg("Jinja2==3.1.4")

object test extends PythonTests with TestModule.Unittest
object test2 extends PythonTests with TestModule.Pytest {
override def sources = T {
q.test.sources()
}
}

}

// This is a basic Mill build for a single `PythonModule`, with one
// dependency and a test suite using the `Unittest` Library.
//// SNIPPET:TREE
//
// ----
// build.mill
// foo/
// src/
// foo/foo.py
// resources/
// ...
// test/
// src/
// foo/test.py
// out/foo/
// run.json
// run.dest/
// ...
// test/
// run.json
// run.dest/
// ...
// ----
//// SNIPPET:DEPENDENCIES
//
// This example project uses one dependency - https://pypi.org/project/Jinja2/[Jinja2]
// for HTML rendering and uses it to wrap a given input string in HTML templates with proper escaping.
//
// Typical usage of a `PythonModule` is shown below:

/** Usage

> ./mill qux.run
Numpy : Sum: 150 | Pandas: Mean: 30.0, Max: 50
> ./mill resolve foo._ # List what tasks are available to run
foo.bundle
...
foo.console
...
foo.run
...
foo.test
...
foo.typeCheck

> ./mill inspect foo.typeCheck # Show documentation and inputs of a task
...
foo.typeCheck(PythonModule...)
Run a typechecker on this module.
Inputs:
foo.pythonExe
foo.transitivePythonPath
foo.sources
...

> ./mill foo.typeCheck # TypeCheck the Python Files and notify errors
Success: no issues found in 1 source file

> ./mill foo.run --text "Hello Mill" # run the main method with arguments
<h1>Hello Mill</h1>

> ./mill foo.test
...
test_escaping (test.TestScript...) ... ok
test_simple (test.TestScript...) ... ok
...
----------------------------------------------------------------------
Ran 2 tests...
OK
...

> ./mill show foo.bundle # Creates Bundle for the python file
".../out/foo/bundle.dest/bundle.pex"

> out/foo/bundle.dest/bundle.pex --text "Hello Mill" # running the PEX binary outside of Mill
<h1>Hello Mill</h1>

> ./mill show qux.bundle
".../out/qux/bundle.dest/bundle.pex"
> sed -i.bak 's/print(main())/print(maaain())/g' foo/src/foo.py

> out/qux/bundle.dest/bundle.pex # running the PEX binary outside of Mill
Numpy : Sum: 150 | Pandas: Mean: 30.0, Max: 50
> ./mill foo.typeCheck # if we make a typo in a method name, mypy flags it
error: ...Name "maaain" is not defined...

*/
5 changes: 0 additions & 5 deletions example/pythonlib/basic/1-simple/foo/bar/src/bar.py

This file was deleted.

23 changes: 21 additions & 2 deletions example/pythonlib/basic/1-simple/foo/src/foo.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
import numpy as np
data = np.array([10, 20, 30, 40, 50])
import argparse
from argparse import Namespace
from jinja2 import Template


def generate_html(text: str) -> str:
template = Template("<h1>{{ text }}</h1>")
return template.render(text=text)


def main() -> str:
parser = argparse.ArgumentParser(description="Inserts text into an HTML template")
parser.add_argument("-t", "--text", required=True, help="Text to insert")

args: Namespace = parser.parse_args()
html: str = generate_html(args.text)
return html


if __name__ == "__main__":
print(main())
16 changes: 16 additions & 0 deletions example/pythonlib/basic/1-simple/foo/test/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import unittest
from markupsafe import escape
from foo import generate_html # type: ignore


class TestScript(unittest.TestCase):
def test_simple(self):
self.assertEqual(generate_html("hello"), "<h1>hello</h1>")

def test_escaping(self):
escaped_text = escape("<hello>")
self.assertEqual(generate_html("&lt;hello&gt;"), f"<h1>{escaped_text}</h1>")


if __name__ == "__main__":
unittest.main()
11 changes: 0 additions & 11 deletions example/pythonlib/basic/1-simple/qux/src/main.py

This file was deleted.

17 changes: 0 additions & 17 deletions example/pythonlib/basic/1-simple/qux/test/src/test_dummy.py

This file was deleted.

28 changes: 28 additions & 0 deletions example/pythonlib/basic/2-custom-build-logic/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// SNIPPET:BUILD
package build
import mill._, pythonlib._

object foo extends PythonModule {

def mainScript = Task.Source { millSourcePath / "src" / "foo.py" }

/** All Python source files in this module, recursively from the source directories.*/
def allSourceFiles: T[Seq[PathRef]] = Task {
sources().flatMap(src => os.walk(src.path).filter(_.ext == "py").map(PathRef(_)))
}

/** Total number of lines in module source files */
def lineCount = Task {
allSourceFiles().map(f => os.read.lines(f.path).size).sum
}

/** Generate resources using lineCount of sources */
override def resources = Task {
val resourcesDir = Task.dest / "resources"
os.makeDir.all(resourcesDir)
os.write(resourcesDir / "line-count.txt", "" + lineCount())
super.resources() ++ Seq(PathRef(Task.dest))
}

object test extends PythonTests with TestModule.Unittest
}
10 changes: 10 additions & 0 deletions example/pythonlib/basic/2-custom-build-logic/foo/src/foo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import importlib.resources


def line_count() -> int:
with importlib.resources.open_text("resources", "line-count.txt") as file:
return int(file.readline().strip())


if __name__ == "__main__":
print(f"Line Count: {line_count()}")
13 changes: 13 additions & 0 deletions example/pythonlib/basic/2-custom-build-logic/foo/test/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import unittest
from foo import line_count # type: ignore


class TestScript(unittest.TestCase):
def test_line_count(self) -> None:
expected_line_count = 10
# Check if the line count matches the expected value
self.assertEqual(line_count(), expected_line_count)


if __name__ == "__main__":
unittest.main()
15 changes: 15 additions & 0 deletions example/pythonlib/basic/3-multi-module/bar/src/bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sys
from jinja2 import Template
from markupsafe import escape


def generate_html(bar_text: str) -> str:
escaped_text = escape(bar_text)
template = Template("<h1>{{ text }}</h1>")
return template.render(text=escaped_text)


if __name__ == "__main__":
# Get the argument from command line
text = sys.argv[1]
print(f"Bar.value: {generate_html(text)}")
16 changes: 16 additions & 0 deletions example/pythonlib/basic/3-multi-module/bar/test/src/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import unittest
from bar import generate_html # type: ignore


class TestScript(unittest.TestCase):
def test_simple(self) -> None:
expected_line = "<h1>world</h1>"
self.assertEqual(generate_html("world"), expected_line)

def test_escaping(self) -> None:
expected_line = "<h1>&lt;world&gt;</h1>"
self.assertEqual(generate_html("<world>"), expected_line)


if __name__ == "__main__":
unittest.main()
Loading
Loading