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.
- Host: GitHub
- URL: https://github.com/kitschpatrol/read-pyproject
- Owner: kitschpatrol
- License: mit
- Created: 2026-02-27T07:57:54.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-02-27T21:08:43.000Z (4 months ago)
- Last Synced: 2026-02-27T23:47:59.034Z (4 months ago)
- Topics: npm-package, pep-518, pyproject, python, toml
- Language: TypeScript
- Homepage:
- Size: 590 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- License: license.txt
Awesome Lists containing this project
README
# read-pyproject
[](https://npmjs.com/package/read-pyproject)
[](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