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

pythonlib: publish modules #4032

Merged
merged 1 commit into from
Dec 4, 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
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
** xref:android/kotlin.adoc[]
** xref:pythonlib/intro.adoc[]
*** xref:pythonlib/dependencies.adoc[]
*** xref:pythonlib/publishing.adoc[]
* xref:comparisons/why-mill.adoc[]
** xref:comparisons/maven.adoc[]
** xref:comparisons/gradle.adoc[]
Expand Down
12 changes: 12 additions & 0 deletions docs/modules/ROOT/pages/pythonlib/publishing.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
= Python Packaging & Publishing
:page-aliases: Publishing_Python_Projects.adoc

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

This page will discuss common topics around publishing your Python projects for others to use.

include::partial$example/pythonlib/publishing/1-publish-module.adoc[]

== Advanced Packaging

include::partial$example/pythonlib/publishing/2-publish-module-advanced.adoc[]
1 change: 1 addition & 0 deletions example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ object `package` extends RootModule with Module {
object pythonlib extends 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 cli extends Module{
Expand Down
10 changes: 10 additions & 0 deletions example/pythonlib/publishing/1-publish-module/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This is an example package.

```
pip install testpkg-mill
```

```
>>> import testpkg.greet
testpkg.greet.hello("world")
```
90 changes: 90 additions & 0 deletions example/pythonlib/publishing/1-publish-module/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// All packaging and publishing functionality is defined in `PublishModule`.
// Start by extending it.

import mill._, pythonlib._

object `package` extends RootModule with PythonModule with PublishModule {

// information about dependencies will be included in the published package
def pythonDeps = Seq("jinja2==3.1.4")

def publishMeta = PublishMeta(
name = "testpkg-mill",
description = "an example package",
requiresPython = ">= 3.12",
license = License.MIT,
authors = Seq(Developer("John Doe", "jdoe@example.org"))
)

// the version under which the package will be published
def publishVersion = "0.0.2"

}

// You'll need to define some metadata in the `publishMeta` and `publishVersion`
// tasks. This metadata is roughly equivalent to what you'd define in a
// https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#basic-information[`pyproject.toml` file].
//
// You'll also need to create a `readme` file, which will be bundled in the
// final package and serves as the landing page seen on PyPI. By default, Mill
// assumes a file starting with the string `readme` (in any capitalization), but
// you can override it to whatever you please.

// [NOTE]
// ====
// The version of your package is not included in `publishMeta`, but
// rather in its own `publishVersion` task. This is done so that you can easily
// override the task to automate the version, such as deriving it from source
// control.
// ====

// == Building packages locally ==
//
// You can build a source distribution or wheel by running the following tasks:

/** Usage
> mill show sdist
".../out/sdist.dest/dist/testpkg_mill-0.0.2.tar.gz"

> mill show wheel
".../out/wheel.dest/dist/testpkg_mill-0.0.2-py3-none-any.whl"
*/

// These files can then be `pip-installed` by other projects, or, if you're using Mill, you can
// include them in your xref:pythonlib/dependencies.adoc#_unmanaged_wheels[unmanagedWheels] task.
// Usually however, you'd want to publish them to a package index such as PyPI or your
// organization's internal package repository.

// == Uploading your packages to PyPI (or other repository)
//
// Uploading your packages to PyPI can be done by running `mill __.publish`.
//
// Mill uses https://twine.readthedocs.io/en/stable/[`twine`] to upload packages, and respects its
// configuration. You can also configure it with environment variables, prefixed with `MILL_`.
//
// [source,bash]
// ----
// export MILL_TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/
// export MILL_TWINE_USERNAME=<username, not necessary for PyPI>
// export MILL_TWINE_PASSWORD=<apitoken>
// mill __.publish
// ----
//
// [NOTE]
// ====
// Mill does not transitively upload all your packages, hence we
// recommended to use `mill __.publish`, instead of `mill <module>.publish`.
// While it's technically possible to upload packages of individual Mill modules
// by calling their `publish` tasks separately, you'd usually want to ensure all
// your dependencies are also published.
// ====
//
// === Check before uploading
//
// Twine has a nice feature to check your artifacts before uploading them. You
// can also do this with Mill, by running:

/** Usage
> mill __.checkPublish
... PASSED
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import jinja2

def hello(name: str):
environment = jinja2.Environment()
template = environment.from_string("Hello, {{ name }}!")
return template.render(name=name)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is an example package.
73 changes: 73 additions & 0 deletions example/pythonlib/publishing/2-publish-module-advanced/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Behind the scenes, Mill delegates most Python packaging tasks to other tools,
// and only takes care of configuring them with information it has on your build.
//
// By default, it will:
//
// * create a synthetic `pyproject.toml` file from its own metadata
//
// * use `setuptools` to package the module
//
// * first create a source distribution and then use that to build a wheel (instead of building a wheel directly)
//
// While this should be sufficient for most projects, sometimes you need a little
// customization.
//
// === Customizing the `pyproject.toml` and other build files
//
// If you're happy to use a PEP-518-compliant `pyproject.toml` to describe how to
// package your published project, but would like some customization, you can amend
// or override the `pyproject` task with your own metadata.
//
// You can also include additional files in the packaging process by adding them to
// `buildFiles`. You can then reference these in your `pyproject.toml` file.
//
// The following example shows how to override the packaging process by providing a
// custom `setup.py` file.

import mill._, pythonlib._

object `package` extends RootModule with PythonModule with PublishModule {

def publishMeta = PublishMeta(
name = "testpackage",
description = "an example package",
requiresPython = ">= 3.12",
license = License.MIT,
authors = Seq(Developer("John Doe", "jdoe@example.org"))
)

def publishVersion = "0.0.3"

// you could also reference an existing setup.py file directly, e.g.
// `def setup = Task.Source { millSourcePath / "setup.py" }`
def setup = Task {
val str =
s"""#from setuptools import setup
#
#print("hello from custom setup.py!")
#
## empty setup, defers to using values in pyproject.toml
#setup()
#""".stripMargin('#')
os.write(Task.dest / "setup.py", str)
PathRef(Task.dest / "setup.py")
}

override def buildFiles = Task {
super.buildFiles() ++ Map("setup.py" -> setup())
}

}

/** Usage
> mill sdist
...
hello from custom setup.py!
...
*/

// === Changing the packaging process entirely
//
// In case customizing of `pyproject` is too cumbersome, or you cannot use it for
// some reason, you can always override the `sdist` and `wheel` tasks with your own
// packaging implementation. Publishing with `__.publish` will still work as usual.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from setuptools import setup

print("hello from custom setup.py!")

# empty setup, defers to using values in pyproject.toml
setup()
4 changes: 3 additions & 1 deletion pythonlib/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ import mill._
// TODO change MillPublishScalaModule to MillStableScalaModule after mill version with pythonlib is released,
// because currently there is no previous artifact version
object `package` extends RootModule with build.MillPublishScalaModule {
def moduleDeps = Seq(build.main)
// 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)
}
Loading
Loading