Python Packaging & Publishing
This page will discuss common topics around publishing your Python projects for others to use.
All packaging and publishing functionality is defined in PublishModule.
Start by extending it.
import mill.*, pythonlib.*
object `package` extends PythonModule, 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"
// The distribution uses a different name from its Python import package.
override def publishModuleName = "testpkg"
}
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
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.
|
The version of your package is not included in |
uv_build normally derives the import package name from the distribution
name. Override publishModuleName when those names differ, as they do above.
Building packages locally
You can build a source distribution or wheel by running the following tasks:
> ./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 installed by other projects with uv pip install or, if you’re using Mill, you can
include them in your 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 uv publish
to upload packages. Configure it with uv’s UV_PUBLISH_* environment variables or the
equivalent MILL_UV_PUBLISH_* variables.
> export MILL_UV_PUBLISH_URL=https://test.pypi.org/legacy/
> export MILL_UV_PUBLISH_USERNAME=__token__
> export MILL_UV_PUBLISH_PASSWORD=<apitoken>
> mill __.publish
|
Mill does not transitively upload all your packages, hence we
recommended to use |
Advanced Packaging
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.tomlfile from its own metadata -
use
uv_buildto package the module anduv buildas the frontend -
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 adds a generated build note to the source distribution
with uv_build’s source-include setting.
import mill.*, pythonlib.*
object `package` extends PythonModule, 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"
def buildNote = Task {
os.write(Task.dest / "build-note.txt", "Generated by Mill")
PathRef(Task.dest / "build-note.txt")
}
override def pyproject = Task {
super.pyproject() +
"""|
|source-include=["build-note.txt"]
|""".stripMargin
}
override def buildFiles = Task {
super.buildFiles() ++ Map("build-note.txt" -> buildNote())
}
}
> ./mill sdist
...testpackage-0.0.3.tar.gz
> tar tzf out/sdist.dest/dist/testpackage-0.0.3.tar.gz
...testpackage-0.0.3/build-note.txt