Skip to content

project.yaml

Project identity

project.yaml opens with the same descriptive metadata that pyproject.toml's [project] table carries: a name, a version, a short description, and a pointer to the readme file.

name: acme-inference
version: "2.3.0"
description: Inference serving for Acme models
readme: README.rst
A field can be left out of project.yaml and listed under dynamic instead, when its value is computed rather than written by hand, for example a version stamped in from a version-control tag:

dynamic:
  - version

Lucid version

lucid is the required-requires-python: a version specifier for the Lucid toolchain the project needs.

lucid: ">=0.4"
Because lucid names the toolchain that both runs and builds a project, project.yaml has no counterpart to pyproject.toml's [build-system] table. There is no separate build backend to select or pin; the lucid version specifier already pins the one toolchain that builds and runs the project.

License

license is an SPDX license expression, and license-files is a list of glob patterns for the license text to distribute, following the same model as the modern pyproject.toml license fields:

license: Apache-2.0
license-files:
  - LICENSE

authors and maintainers are lists of name/email pairs. keywords and classifiers describe the project for a package index. urls is a mapping of labels to links:

authors:
  - name: Ada Lovelace
    email: ada@example.com
maintainers:
  - name: Grace Hopper
    email: grace@example.com
keywords:
  - inference
  - serving
classifiers:
  - "Intended Audience :: Developers"
urls:
  Homepage: https://example.com/acme-inference
  Documentation: https://example.com/acme-inference/docs
  Repository: https://example.com/acme-inference.git
  Issues: https://example.com/acme-inference/issues

Dependencies

dependencies is a mapping from a required project's name to a version specifier, in place of pyproject.toml's flat list of requirement strings:

dependencies:
  numpy: ">=1.26"
  acme-models: ">=1.0,<2.0"
This is more than a syntax choice: the same names and specifiers are what library.initialize walks to topologically sort library initialization (see Library initialization below), so a project's dependency list and its initialization order come from one declaration instead of two.

optional-dependencies declares installable extras the same way pyproject.toml does: named groups of additional dependencies that a consumer can opt into.

optional-dependencies:
  gpu:
    cupy: ">=13.0"
Dependencies that only a contributor to the project needs, such as test or lint tooling, do not belong here; they are declared in development.yaml's Development dependencies.

Public API

export is a tree mapping the paths a project makes public to where they actually live — the one place a project's externally visible surface is declared, full stop:

export:
  models:
    User: .models.User
  parsing:
    parse_user: .parsing.parse_user
Only a name that is not itself module- or class-private can appear on the right: export promotes something the project already exposes to itself into something the outside world can reach too, but it cannot reach past a leading underscore to expose what the project keeps to itself.

No __module__ or __qualname__

Python spreads a symbol's location across two attributes: __module__ (the dotted module it was defined in) and __qualname__ (its lexical nesting within that module, for anything declared inside a class or function). Two attributes for one concept is two chances for them to disagree, and both encode wherever a symbol happens to be defined — exactly the internal layout Public API already keeps out of everything else a project exposes.

Lucid folds both into one, __path__: !DottedPath. DottedPath is a Sequence[str] of the location's segments — indexing and slicing it work exactly like any other sequence of strings — and its __str__ joins them with dots, so printing a path and inspecting its segments are just two views of the same value. It is frozen (!, see Mutable, read-only, and immutable views) because a symbol's location does not change after it is computed. __name__ stays exactly as it already is, the bare identifier, always equal to path[-1]:

str(slow_query.__path__)  # "acme_inference.queries.slow_query"
slow_query.__path__[-1]   # "slow_query"
slow_query.__name__       # "slow_query"
Every function, class, trait, and module has a __path__. By default it holds the symbol's own defining location — project name, then module path, then lexical nesting — exactly what __module__ and __qualname__ used to spell out between them. Flattening the two into one sequence does mean the boundary between them is gone: for a method nested inside a class, __path__ no longer says which prefix was the module and which was the class nesting, the way __module__ and __qualname__ separately did. Nothing in Lucid needs that boundary — the segments as a whole, or just the trailing name, cover every actual use — and the rare code that wants "which module is this in" asks the containing module directly rather than parsing a path apart. A symbol named in export: gets its __path__ overridden instead, to the path the tree actually declares for it, so a symbol's visible identity matches how the outside world reaches it rather than wherever it happens to live inside the project:

str(User.__path__)  # "acme_inference.models.User" -- the export path,
                     # not wherever .models.User actually lives
Computing the override costs one pass over the manifest, not a search per symbol. The export: tree is already a trie: one path segment per level, an internal dotted location at every leaf. Walking it once builds the reverse of it too — a hash table from each leaf's internal location to the segment list leading there — and that table is exactly the set of overrides applied to produce every exported symbol's __path__.

Local aliases

local-alias declares a tree of aliases that remap paths for local use inside the project. Aliases can flatten an internal source layout, for example mapping an internal ._src package back onto the project root, or promote a deeply nested definition to a short local path:

local-alias:
  _src: .
  matrix: .some.deep.path.matrix
Between export and local-alias, a project has no remaining need for __init__.py-style re-exporting or flattening modules.

Library initialization

library-context names a context manager (see Context managers), holding the setup work that Python would otherwise run as side effects in __init__.py, such as configuring an underlying native library:

library-context: .setup.initialize
Because imports are lazy, that setup does not run just because a name is imported. It runs only when library.initialize enters it:

with library.initialize({"numpy": numpy_params, "acme-inference": project_params}):
    run_entry_point()
Using a library's behavior without its context manager having been entered is an error. Each named library's library-context is entered in dependency order: library.initialize topologically sorts the libraries being initialized using the dependencies declared in each project's project.yaml, so a library is initialized after every library it depends on.

Entry points

entry-points is a mapping of names to paths: the runnable commands a project exposes, for example to the command line, in place of pyproject.toml's [project.scripts] table:

entry-points:
  serve: .cli.serve
  migrate: .cli.migrate
Running an entry point is what opens library.initialize; see Library initialization.