Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.4] - 2026-03-20

### Added

- `Style::Plain` — new style variant returned by `detect_style` for docstrings
that contain no NumPy section underlines or Google section headers (e.g.
summary-only docstrings, Sphinx-style docstrings).
- `SyntaxKind::PLAIN_DOCSTRING` — root node kind for plain-style parse trees.
- `parse_plain(input)` — lightweight parser that extracts only a `SUMMARY` and
an optional `EXTENDED_SUMMARY` token from the input, without attempting
section detection.
- `parse(input)` — unified entry point that calls `detect_style` and dispatches
to `parse_google`, `parse_numpy`, or `parse_plain` automatically.
- `PlainDocstring` typed wrapper with `summary()` and `extended_summary()`
accessors (mirrors the existing `GoogleDocstring` / `NumPyDocstring` API).
- Python bindings: `Style.PLAIN`, `SyntaxKind.PLAIN_DOCSTRING`, `PlainDocstring`
class, and `parse_plain(input)` function.
- Google parser: zero-length `DESCRIPTION` token emitted when a colon is
present but no description text follows (e.g. `a (int):`, `a:`), and
zero-length `TYPE` token emitted for empty brackets `()`.
- NumPy parser: zero-length `TYPE` token emitted when a colon is present but
type text is absent (e.g. `a :`); zero-length `DEFAULT_VALUE` token emitted
when a default separator is present but no value follows (e.g. `default =`).
Callers can use `find_missing(KIND)` to detect these absent-but-declared
slots without inspecting surrounding tokens.
- `examples/parse_auto.rs` — demonstrates the unified `parse()` entry point
with Google, NumPy, and plain-style inputs.

### Changed

- `detect_style` rewritten as a single O(n) pass; returns `Style::Plain` as the
fallback instead of `Style::Google`.

## [0.1.3] - 2026-03-19

### Added
Expand Down Expand Up @@ -94,6 +127,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Zero external crate dependencies
- Python bindings via PyO3 (`pydocstring-rs`)

[0.1.4]: https://github.com/qraqras/pydocstring/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/qraqras/pydocstring/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/qraqras/pydocstring/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/qraqras/pydocstring/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/qraqras/pydocstring/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pydocstring"
version = "0.1.3"
version = "0.1.4"
edition = "2024"
authors = ["Ryuma Asai"]
description = "A zero-dependency Rust parser for Python docstrings (Google and NumPy styles) with a unified syntax tree and byte-precise source locations"
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ Python bindings are also available as [`pydocstring-rs`](https://pypi.org/projec
- **Byte-precise source locations** — every token carries its exact byte range for pinpoint diagnostics
- **Zero dependencies** — pure Rust, no external crates, no regex
- **Error-resilient** — never panics; malformed input still yields a best-effort tree
- **Style auto-detection** — hand it a docstring, it tells you the convention
- **Style auto-detection** — hand it a docstring, get back `Style::Google`, `Style::NumPy`, or `Style::Plain`

## Installation

```toml
[dependencies]
pydocstring = "0.1.3"
pydocstring = "0.1.4"
```

## Usage
Expand Down Expand Up @@ -60,6 +60,25 @@ use pydocstring::parse::{detect_style, Style};

assert_eq!(detect_style("Summary.\n\nArgs:\n x: Desc."), Style::Google);
assert_eq!(detect_style("Summary.\n\nParameters\n----------\nx : int"), Style::NumPy);
assert_eq!(detect_style("Just a summary."), Style::Plain);
```

`Style::Plain` covers docstrings with no recognised section markers: summary-only,
summary + extended summary, and unrecognised styles such as Sphinx.

### Unified Auto-Detecting Parser

Use `parse()` to let the library detect the style and parse in one step:

```rust
use pydocstring::parse::parse;
use pydocstring::syntax::SyntaxKind;

let result = parse("Summary.\n\nArgs:\n x: Desc.");
assert_eq!(result.root().kind(), SyntaxKind::GOOGLE_DOCSTRING);

let result = parse("Just a summary.");
assert_eq!(result.root().kind(), SyntaxKind::PLAIN_DOCSTRING);
```

### Source Locations
Expand Down Expand Up @@ -215,13 +234,14 @@ Both styles support the following section categories. Typed accessor methods are
| Methods | `methods()` → `GoogleMethod` | `methods()` → `NumPyMethod` |
| Free text (Notes, Examples, etc.) | `body_text()` | `body_text()` |

Root-level accessors: `summary()`, `extended_summary()` (NumPy also has `deprecation()`).
Root-level accessors: `summary()`, `extended_summary()` (NumPy also has `deprecation()`). `PlainDocstring` exposes only `summary()` and `extended_summary()`.

## Development

```bash
cargo build
cargo test
cargo run --example parse_auto
cargo run --example parse_google
cargo run --example parse_numpy
```
4 changes: 2 additions & 2 deletions bindings/python/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions bindings/python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pydocstring-python"
version = "0.1.3"
version = "0.1.4"
edition = "2024"
authors = ["Ryuma Asai"]
description = "Python bindings for pydocstring — a fast docstring parser for Google and NumPy styles"
Expand All @@ -12,5 +12,5 @@ name = "pydocstring"
crate-type = ["cdylib"]

[dependencies]
pydocstring_core = { package = "pydocstring", version = "0.1.3", path = "../.." }
pydocstring_core = { package = "pydocstring", version = "0.1.4", path = "../.." }
pyo3 = { version = "0.24", features = ["extension-module"] }
80 changes: 69 additions & 11 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Produces a **unified syntax tree** with **byte-precise source locations** on eve
- **Byte-precise source locations** — every token carries its exact byte range for pinpoint diagnostics
- **Powered by Rust** — native extension with no Python runtime overhead
- **Error-resilient** — never raises exceptions; malformed input still yields a best-effort tree
- **Style auto-detection** — hand it a docstring, get back `Style.GOOGLE` or `Style.NUMPY`
- **Style auto-detection** — hand it a docstring, get back `Style.GOOGLE`, `Style.NUMPY`, or `Style.PLAIN`

## Installation

Expand All @@ -26,15 +26,70 @@ pip install pydocstring-rs

## Usage

### Unified Parse (auto-detect)

Use `parse()` when you don't know the style in advance.
The returned object has a `.style` property so you can dispatch without `isinstance` checks:

```python
from pydocstring import parse, Style

doc = parse(source)

match doc.style:
case Style.GOOGLE:
for arg in doc.sections[0].args:
print(arg.name.text, arg.description.text)
case Style.NUMPY:
for param in doc.sections[0].parameters:
print([n.text for n in param.names], param.description.text)
case Style.PLAIN:
print(doc.summary.text)
```

When you only need the style-independent model, no dispatch is necessary:

```python
model = parse(source).to_model() # works for all three styles
```

If you already know the style, prefer the explicit functions `parse_google()`,
`parse_numpy()`, or `parse_plain()` — they return a concrete type and are
slightly more efficient.

### Style Detection

```python
from pydocstring import detect_style, Style

detect_style("Summary.\n\nArgs:\n x: Desc.") # Style.GOOGLE
detect_style("Summary.\n\nParameters\n----------\n") # Style.NUMPY
detect_style("Just a summary.") # Style.PLAIN
```

`Style.PLAIN` covers docstrings with no recognised section markers:
summary-only, summary + extended, and unrecognised styles such as Sphinx.

### Plain Style

Docstrings with no NumPy or Google section markers are parsed as plain:

```python
from pydocstring import parse_plain

doc = parse_plain("""Brief summary.

More detail here.
Spanning multiple lines.
""")

print(doc.summary.text) # "Brief summary."
print(doc.extended_summary.text) # "More detail here.\nSpanning multiple lines."
```

Unrecognised styles such as Sphinx are also treated as plain: the `:param:`
lines are preserved verbatim in `extended_summary`.

### Google Style

```python
Expand Down Expand Up @@ -230,25 +285,28 @@ print(numpy_text) # Contains "Parameters\n----------"

### Functions

| Function | Returns | Description |
|----------------------|-------------------|------------------------------------------------|
| `parse_google(text)` | `GoogleDocstring` | Parse a Google-style docstring |
| `parse_numpy(text)` | `NumPyDocstring` | Parse a NumPy-style docstring |
| `detect_style(text)` | `Style` | Detect style: `Style.GOOGLE` or `Style.NUMPY` |
| `emit_google(doc)` | `str` | Emit a `Docstring` model as Google-style text |
| `emit_numpy(doc)` | `str` | Emit a `Docstring` model as NumPy-style text |
| Function | Returns | Description |
|----------------------|-------------------------------------------------|---------------------------------------------------------------|
| `parse(text)` | `GoogleDocstring \| NumPyDocstring \| PlainDocstring` | Auto-detect style and parse |
| `parse_google(text)` | `GoogleDocstring` | Parse a Google-style docstring |
| `parse_numpy(text)` | `NumPyDocstring` | Parse a NumPy-style docstring |
| `parse_plain(text)` | `PlainDocstring` | Parse a plain docstring (no section markers) |
| `detect_style(text)` | `Style` | Detect style: `Style.GOOGLE`, `Style.NUMPY`, or `Style.PLAIN` |
| `emit_google(doc)` | `str` | Emit a `Docstring` model as Google-style text |
| `emit_numpy(doc)` | `str` | Emit a `Docstring` model as NumPy-style text |

### Objects

| Class | Key Properties |
|-------------------|------------------------------------------------------------------------------------------------------------------|
| `Style` | `GOOGLE`, `NUMPY` (enum) |
| `GoogleDocstring` | `summary`, `extended_summary`, `sections`, `node`, `source`, `pretty_print()`, `to_model()` |
| `Style` | `GOOGLE`, `NUMPY`, `PLAIN` (enum) |
| `GoogleDocstring` | `style`, `summary`, `extended_summary`, `sections`, `node`, `source`, `pretty_print()`, `to_model()` |
| `GoogleSection` | `kind`, `args`, `returns`, `exceptions`, `body_text`, `node` |
| `GoogleArg` | `name`, `type`, `description`, `optional` |
| `GoogleReturns` | `return_type`, `description` |
| `GoogleException` | `type`, `description` |
| `NumPyDocstring` | `summary`, `extended_summary`, `sections`, `node`, `source`, `pretty_print()`, `to_model()` |
| `PlainDocstring` | `style`, `summary`, `extended_summary`, `node`, `source`, `pretty_print()`, `to_model()` |
| `NumPyDocstring` | `style`, `summary`, `extended_summary`, `sections`, `node`, `source`, `pretty_print()`, `to_model()` |
| `NumPySection` | `kind`, `parameters`, `returns`, `exceptions`, `body_text`, `node` |
| `NumPyParameter` | `names`, `type`, `description`, `optional`, `default_value` |
| `NumPyReturns` | `name`, `return_type`, `description` |
Expand Down
32 changes: 32 additions & 0 deletions bindings/python/pydocstring.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ from typing import Generator, Union
class Style(enum.IntEnum):
GOOGLE = ...
NUMPY = ...
PLAIN = ...

class SyntaxKind(enum.IntEnum):
# Common tokens
Expand Down Expand Up @@ -56,6 +57,8 @@ class SyntaxKind(enum.IntEnum):
NUMPY_REFERENCE = ...
NUMPY_ATTRIBUTE = ...
NUMPY_METHOD = ...
# Plain node
PLAIN_DOCSTRING = ...

class TextRange:
@property
Expand Down Expand Up @@ -129,6 +132,8 @@ class GoogleSection:
def node(self) -> Node: ...

class GoogleDocstring:
@property
def style(self) -> Style: ...
@property
def summary(self) -> Token | None: ...
@property
Expand Down Expand Up @@ -192,6 +197,8 @@ class NumPySection:
def node(self) -> Node: ...

class NumPyDocstring:
@property
def style(self) -> Style: ...
@property
def summary(self) -> Token | None: ...
@property
Expand All @@ -212,6 +219,29 @@ class NumPyDocstring:
"""
...

# ─── Plain ───────────────────────────────────────────────────────────────────

class PlainDocstring:
@property
def style(self) -> Style: ...
@property
def summary(self) -> Token | None: ...
@property
def extended_summary(self) -> Token | None: ...
@property
def node(self) -> Node: ...
@property
def source(self) -> str: ...
def pretty_print(self) -> str: ...
def to_model(self) -> Docstring: ...
def line_col(self, offset: int) -> LineColumn:
"""Convert a byte offset to a LineColumn.

The offset is typically ``token.range.start`` or ``token.range.end``.
``lineno`` is 1-based; ``col`` is 0-based Unicode codepoints.
"""
...

# ─── Model IR ────────────────────────────────────────────────────────────────

class Deprecation:
Expand Down Expand Up @@ -361,8 +391,10 @@ class Docstring:

# ─── Functions ───────────────────────────────────────────────────────────────

def parse(input: str) -> GoogleDocstring | NumPyDocstring | PlainDocstring: ...
def parse_google(input: str) -> GoogleDocstring: ...
def parse_numpy(input: str) -> NumPyDocstring: ...
def parse_plain(input: str) -> PlainDocstring: ...
def detect_style(input: str) -> Style: ...
def emit_google(doc: Docstring, base_indent: int = 0) -> str: ...
def emit_numpy(doc: Docstring, base_indent: int = 0) -> str: ...
Expand Down
2 changes: 1 addition & 1 deletion bindings/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "pydocstring-rs"
version = "0.1.3"
version = "0.1.4"
description = "Python bindings for pydocstring — a zero-dependency Rust parser for Python docstrings (Google and NumPy styles) with a unified syntax tree and byte-precise source locations"
license = {text = "MIT"}
authors = [{name = "Ryuma Asai"}]
Expand Down
1 change: 1 addition & 0 deletions bindings/python/rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
max_width = 120
Loading
Loading