add python toolchain for datamodel-code-generator

This commit is contained in:
sdcoffey
2026-03-09 19:50:07 -07:00
parent 5c38a595c7
commit 216013b8fb
12 changed files with 12540 additions and 11805 deletions

View File

@@ -15,6 +15,10 @@ struct Args {
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
/// Optional Ruff executable path to format generated Python files
#[arg(long = "ruff", value_name = "RUFF_BIN")]
ruff: Option<PathBuf>,
/// Include experimental API methods and fields in generated output.
#[arg(long = "experimental")]
experimental: bool,
@@ -34,5 +38,12 @@ fn main() -> Result<()> {
},
)?;
codex_app_server_protocol::generate_json_with_experimental(&json_out_dir, args.experimental)?;
codex_app_server_protocol::generate_python_with_experimental(&python_out_dir, args.experimental)
codex_app_server_protocol::generate_python_with_options(
&python_out_dir,
args.ruff.as_deref(),
codex_app_server_protocol::GeneratePythonOptions {
experimental_api: args.experimental,
..codex_app_server_protocol::GeneratePythonOptions::default()
},
)
}

View File

@@ -14,6 +14,10 @@ struct Args {
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
/// Optional path to the Ruff executable to format generated Python files.
#[arg(long = "ruff", value_name = "RUFF_BIN")]
ruff: Option<PathBuf>,
/// Include experimental API methods and fields in generated fixtures.
#[arg(long = "experimental")]
experimental: bool,
@@ -29,6 +33,7 @@ fn main() -> Result<()> {
codex_app_server_protocol::write_schema_fixtures_with_options(
&schema_root,
args.prettier.as_deref(),
args.ruff.as_deref(),
codex_app_server_protocol::SchemaFixtureOptions {
experimental_api: args.experimental,
},

View File

@@ -102,6 +102,21 @@ impl Default for GenerateTsOptions {
}
}
#[derive(Clone, Copy, Debug)]
pub struct GeneratePythonOptions {
pub run_ruff: bool,
pub experimental_api: bool,
}
impl Default for GeneratePythonOptions {
fn default() -> Self {
Self {
run_ruff: true,
experimental_api: false,
}
}
}
pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
generate_ts_with_options(out_dir, prettier, GenerateTsOptions::default())
}
@@ -244,10 +259,25 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -
}
pub fn generate_python(out_dir: &Path) -> Result<()> {
generate_python_with_experimental(out_dir, false)
generate_python_with_options(out_dir, None, GeneratePythonOptions::default())
}
pub fn generate_python_with_experimental(out_dir: &Path, experimental_api: bool) -> Result<()> {
generate_python_with_options(
out_dir,
None,
GeneratePythonOptions {
experimental_api,
..GeneratePythonOptions::default()
},
)
}
pub fn generate_python_with_options(
out_dir: &Path,
ruff: Option<&Path>,
options: GeneratePythonOptions,
) -> Result<()> {
ensure_dir(out_dir)?;
let temp_json_dir = std::env::temp_dir().join(format!(
@@ -261,7 +291,7 @@ pub fn generate_python_with_experimental(out_dir: &Path, experimental_api: bool)
)
})?;
generate_json_with_experimental(&temp_json_dir, experimental_api)?;
generate_json_with_experimental(&temp_json_dir, options.experimental_api)?;
let bundle_path = temp_json_dir.join("codex_app_server_protocol.schemas.json");
let bundle = read_json_value(&bundle_path)?;
let root_schema_path = temp_json_dir.join("codex_app_server_protocol.python.schemas.json");
@@ -295,6 +325,10 @@ pub fn generate_python_with_experimental(out_dir: &Path, experimental_api: bool)
fs::write(module_dir.join("py.typed"), "")
.with_context(|| format!("Failed to write {}", module_dir.join("py.typed").display()))?;
if options.run_ruff {
run_ruff_on_python_files(ruff, &module_dir)?;
}
let _ = fs::remove_dir_all(&temp_json_dir);
Ok(())
}
@@ -325,8 +359,7 @@ fn generate_python_models(schema_path: &Path, output_path: &Path) -> Result<()>
.arg("--use-generic-base-class")
.arg("--disable-timestamp")
.arg("--formatters")
.arg("black")
.arg("isort");
.arg("ruff-format");
let status = command.status().with_context(|| {
format!(
@@ -353,28 +386,25 @@ Install `uv`/`uvx` or `datamodel-codegen` to regenerate app-server Python bindin
}
fn datamodel_codegen_command() -> Command {
if command_exists("uvx") {
let mut command = Command::new("uvx");
if command_exists("uv") {
// Keep the uv-managed tool environment next to the protocol crate source,
// not under schema/python/, which is regenerated as a fixture artifact.
let python_codegen_project = Path::new(env!("CARGO_MANIFEST_DIR")).join("python");
let mut command = Command::new("uv");
command
.arg("--with")
.arg("black")
.arg("--with")
.arg("isort")
.arg("--from")
.arg("datamodel-code-generator")
.arg("datamodel-codegen");
.arg("run")
.arg("--project")
.arg(python_codegen_project)
.arg("--locked")
.arg("python")
.arg("-m")
.arg("datamodel_code_generator");
return command;
}
if command_exists("uv") {
let mut command = Command::new("uv");
if command_exists("uvx") {
let mut command = Command::new("uvx");
command
.arg("tool")
.arg("run")
.arg("--with")
.arg("black")
.arg("--with")
.arg("isort")
.arg("--from")
.arg("datamodel-code-generator")
.arg("datamodel-codegen");
@@ -384,6 +414,52 @@ fn datamodel_codegen_command() -> Command {
Command::new("datamodel-codegen")
}
fn run_ruff_on_python_files(ruff: Option<&Path>, out_dir: &Path) -> Result<()> {
let py_files = py_files_in_recursive(out_dir)?;
if py_files.is_empty() {
return Ok(());
}
let mut format_command = ruff_command(ruff)?;
let format_status = format_command
.arg("format")
.args(py_files.iter().map(|p| p.as_os_str()))
.status()
.context("Failed to invoke Ruff format")?;
if !format_status.success() {
return Err(anyhow!("Ruff format failed with status {format_status}"));
}
Ok(())
}
fn ruff_command(ruff: Option<&Path>) -> Result<Command> {
if let Some(ruff_bin) = ruff {
return Ok(Command::new(ruff_bin));
}
if command_exists("uv") {
let python_codegen_project = Path::new(env!("CARGO_MANIFEST_DIR")).join("python");
let mut command = Command::new("uv");
command
.arg("run")
.arg("--project")
.arg(python_codegen_project)
.arg("--locked")
.arg("ruff");
return Ok(command);
}
if command_exists("ruff") {
return Ok(Command::new("ruff"));
}
Err(anyhow!(
"Ruff was requested for Python generation, but no Ruff command is available. \
Install `uv` for the vendored Python toolchain or pass `--ruff /path/to/ruff`."
))
}
fn command_exists(command: &str) -> bool {
std::env::var_os("PATH").is_some_and(|paths| {
std::env::split_paths(&paths).any(|path| {
@@ -2289,6 +2365,26 @@ fn ts_files_in_recursive(dir: &Path) -> Result<Vec<PathBuf>> {
Ok(files)
}
fn py_files_in_recursive(dir: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for entry in
fs::read_dir(&d).with_context(|| format!("Failed to read dir {}", d.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.is_file() && path.extension() == Some(OsStr::new("py")) {
files.push(path);
}
}
}
files.sort();
Ok(files)
}
/// Generate an index.ts file that re-exports all generated types.
/// This allows consumers to import all types from a single file.
fn generate_index_ts(out_dir: &Path) -> Result<PathBuf> {

View File

@@ -5,11 +5,13 @@ mod protocol;
mod schema_fixtures;
pub use experimental_api::*;
pub use export::GeneratePythonOptions;
pub use export::GenerateTsOptions;
pub use export::generate_json;
pub use export::generate_json_with_experimental;
pub use export::generate_python;
pub use export::generate_python_with_experimental;
pub use export::generate_python_with_options;
pub use export::generate_ts;
pub use export::generate_ts_with_options;
pub use export::generate_types;

View File

@@ -85,14 +85,19 @@ pub fn generate_typescript_schema_fixture_subtree_for_tests() -> Result<BTreeMap
///
/// This is intended to be used by tooling (e.g., `just write-app-server-schema`).
/// It deletes any previously generated files so stale artifacts are removed.
pub fn write_schema_fixtures(schema_root: &Path, prettier: Option<&Path>) -> Result<()> {
write_schema_fixtures_with_options(schema_root, prettier, SchemaFixtureOptions::default())
pub fn write_schema_fixtures(
schema_root: &Path,
prettier: Option<&Path>,
ruff: Option<&Path>,
) -> Result<()> {
write_schema_fixtures_with_options(schema_root, prettier, ruff, SchemaFixtureOptions::default())
}
/// Regenerates schema fixtures with configurable options.
pub fn write_schema_fixtures_with_options(
schema_root: &Path,
prettier: Option<&Path>,
ruff: Option<&Path>,
options: SchemaFixtureOptions,
) -> Result<()> {
let typescript_out_dir = schema_root.join("typescript");
@@ -112,7 +117,14 @@ pub fn write_schema_fixtures_with_options(
},
)?;
crate::generate_json_with_experimental(&json_out_dir, options.experimental_api)?;
crate::generate_python_with_experimental(&python_out_dir, options.experimental_api)?;
crate::generate_python_with_options(
&python_out_dir,
ruff,
crate::GeneratePythonOptions {
experimental_api: options.experimental_api,
..crate::GeneratePythonOptions::default()
},
)?;
Ok(())
}