An open API service indexing awesome lists of open source software.

https://github.com/kitschpatrol/read-pyproject

Parse and normalize pyproject.toml metadata into strictly typed JavaScript objects.
https://github.com/kitschpatrol/read-pyproject

npm-package pep-518 pyproject python toml

Last synced: 4 months ago
JSON representation

Parse and normalize pyproject.toml metadata into strictly typed JavaScript objects.

Awesome Lists containing this project

README

          

# read-pyproject

[![NPM Package read-pyproject](https://img.shields.io/npm/v/read-pyproject.svg)](https://npmjs.com/package/read-pyproject)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Parse and normalize pyproject.toml metadata into strictly typed JavaScript objects.**

## Overview

A typed [`pyproject.toml`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) reader for Node.js.

Highlights:

- **Typed output**\
The returned object is deeply typed via Zod schema inference, giving you autocomplete and type safety for `[project]`, `[build-system]`, `[dependency-groups]`, and 30+ `[tool.*]` sections.

- **Normalization**\
The mess of `kebab-case`, `snake_case`, and `PascalCase` keys are converted to `camelCase` in the output by default, and some other fields (like `license` and `readme` values) are sensibly normalized.

- **Flexibly strict**\
Control how unknown keys are handled with three modes: `'passthrough'` (default, keeps everything), `'strip'` (silently removes unknown keys), or `'error'` (throws on unknown keys).

- **Broad tool coverage**\
Typed schemas for 30+ common `[tool.*]` sections. Unrecognized tools pass through as `unknown` by default.

Note that this library currently only _reads_, it does not write changes back to the `.toml` file.

## Getting started

### Dependencies

[Node](https://nodejs.org/) 20.17.0+

### Installation

```sh
npm install read-pyproject
```

### Quick start

```ts
import { readPyproject } from 'read-pyproject'

const pyproject = await readPyproject('/path/to/project')

console.log(pyproject.project?.name) // Normalized PEP 503 name
console.log(pyproject) // The rest of the object...
```

Or parse a TOML string directly:

```ts
import { parsePyproject } from 'read-pyproject'

const toml = `
[project]
name = "my-package"
version = "1.0.0"
`

const pyproject = parsePyproject(toml)
```

## Usage

### API

#### `readPyproject(pathOrDirectory?, options?)`

Read, parse, validate, and normalize a `pyproject.toml` file.

##### Parameters

- `pathOrDirectory` — A file path or directory. If a directory, appends `/pyproject.toml`. Defaults to `process.cwd()`.
- `options` — Optional configuration object:
- `camelCase` — Convert keys to camelCase (`true` by default). Set to `false` to get raw TOML keys.
- `unknownKeyPolicy` — How to handle unknown keys: `'passthrough'` (default), `'strip'`, or `'error'`.

##### Returns

- `Promise` when `camelCase` is `true` (default)
- `Promise` when `camelCase` is `false`

The return type is inferred from the `camelCase` option via function overloads.

##### Throws

`PyprojectError` on missing files, invalid TOML, or validation failures (in `'error'` mode).

##### Examples

```ts
import { readPyproject } from 'read-pyproject'

// Read from current directory (camelCase keys by default)
await readPyproject()

// Read from a specific file
await readPyproject('/path/to/pyproject.toml')

// Read from a directory
await readPyproject('/path/to/project')

// Get raw TOML keys (no camelCase conversion)
const raw = await readPyproject('/path/to/project', { camelCase: false })
raw['build-system']?.['build-backend'] // Raw kebab-case keys

// Reject unknown keys
await readPyproject('/path/to/project', { unknownKeyPolicy: 'error' })

// Strip unknown keys from the output
await readPyproject('/path/to/project', { unknownKeyPolicy: 'strip' })
```

#### `parsePyproject(content, options?)`

Parse, validate, and normalize a `pyproject.toml` content string. This is the synchronous counterpart to `readPyproject` — it accepts a TOML string instead of reading from the filesystem.

##### Parameters

- `content` — A `pyproject.toml` content string.
- `options` — Optional configuration object:
- `camelCase` — Convert keys to camelCase (`true` by default). Set to `false` to get raw TOML keys.
- `unknownKeyPolicy` — How to handle unknown keys: `'passthrough'` (default), `'strip'`, or `'error'`.

##### Returns

- `PyprojectData` when `camelCase` is `true` (default)
- `RawPyprojectData` when `camelCase` is `false`

The return type is inferred from the `camelCase` option via function overloads.

##### Throws

`PyprojectError` on invalid TOML or validation failures (in `'error'` mode).

##### Examples

```ts
import { parsePyproject } from 'read-pyproject'

// Parse a TOML string (camelCase keys by default)
const pyproject = parsePyproject('[project]\nname = "my-package"')

// Get raw TOML keys (no camelCase conversion)
const raw = parsePyproject(tomlString, { camelCase: false })

// Reject unknown keys
parsePyproject(tomlString, { unknownKeyPolicy: 'error' })
```

#### `PyprojectError`

Custom error class thrown for file read errors, TOML parse errors, and validation failures. Includes a `filePath` property for context.

#### `setLogger(logger?)`

Inject a custom logger. Accepts a [LogLayer](https://github.com/theogravity/loglayer) instance or a `Console`-like object.

### Normalization

- All kebab-case, snake_case, and PascalCase keys in the TOML are converted to camelCase in the output by default. Pass `{ camelCase: false }` to disable this and receive raw TOML keys instead.

- `project.name` is normalized per [PEP 503](https://peps.python.org/pep-0503/#normalized-names) (lowercased, runs of `[-_.]+` collapsed to a single `-`). The original name is available as `project.rawName`. This normalization is always applied regardless of the `camelCase` option.

- `project.readme` is normalized to a string when the readme is file-based (`"README.md"` stays as-is, `{ file: "README.md", content-type: "..." }` collapses to `"README.md"`). Inline-text readmes (`{ text: "...", content-type: "..." }`) are kept as a `{ text, contentType? }` object.

- `project.license` is normalized from an SPDX string (`"MIT"`) to `{ spdx }`, validated and corrected via [spdx-correct](https://github.com/jslicense/spdx-correct.js). Legacy table-form licenses (`{ file }` or `{ text }`) pass through as-is.

### Supported `[tool.*]` sections

The following tools have typed schemas:

autopep8, bandit, black, bumpversion, check-wheel-contents, cibuildwheel, codespell, comfy, commitizen, coverage, dagster, distutils, docformatter, flake8, flit, hatch, isort, jupyter-releaser, mypy, pdm, pixi, poe, poetry, pydocstyle, pylint, pyright, pytest, ruff, setuptools, setuptools_scm, tbump, towncrier, uv, yapf

Unknown tools pass through as `unknown` by default.

## Background

### Motivation

I wanted something like [read-pkg](https://github.com/sindresorhus/read-pkg) or [pkg-types](https://github.com/unjs/pkg-types), but for `pyproject.toml` files.

It's a bit strange to work across language ecosystems like this, but I had occasion to do so for some other Node-based projects related to project metadata extraction, specifically [@kitschpatrol/codemeta](https://github.com/kitschpatrol/codemeta) and [metascope](https://github.com/kitschpatrol/metascope).

### Implementation notes

The project consists of a number of [Zod](https://zod.dev) schemas responsible for validating and normalizing data found in a `pyproject.toml`. Schemas output raw TOML keys; camelCase conversion is handled centrally by a recursive `deepCamelCaseKeys` function that knows which paths contain user-defined record keys (like package names or file paths) and skips those. Type-level camelCase conversion uses [`CamelCasedPropertiesDeep`](https://github.com/sindresorhus/type-fest) from `type-fest`, and function overloads ensure the return type matches the `camelCase` option.

Forcing the rather dynamic and extensible data structure found in `pyproject.toml` into a TypeScript straightjacket is likely futile, but an LLM makes the project at least somewhat tractable.

## Maintainers

@kitschpatrol

## Slop factor

_High._

An initial human-crafted specification was implemented by LLM. The output has been subject to only moderate post-facto human scrutiny.

## Contributing

[Issues](https://github.com/kitschpatrol/read-pyproject/issues) and pull requests are welcome.

## License

[MIT](license.txt) © Eric Mika