Skip to content

Commit 96ad252

Browse files
update and reorganise docs
1 parent c6288e8 commit 96ad252

21 files changed

Lines changed: 1126 additions & 15 deletions

CONTRIBUTING.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# contributing
2+
3+
## prerequisites
4+
5+
- **Rust** (stable toolchain)
6+
- **Python 3.10+** (for running transpiled output)
7+
- **uv** (recommended for Python dependency management)
8+
9+
## building
10+
11+
basedpython is a Rust project managed with Cargo:
12+
13+
```sh
14+
cargo build
15+
```
16+
17+
the binary is built to `target/debug/by`. for a release build:
18+
19+
```sh
20+
cargo build --release
21+
```
22+
23+
## running tests
24+
25+
```sh
26+
cargo test
27+
```
28+
29+
- **inline tests** — most transform files in `src/transforms/` have `#[cfg(test)]` modules at the bottom with unit tests that call `transpile()` on a snippet and assert the output
30+
- **end-to-end tests**`tests/e2e.rs` runs larger integration scenarios
31+
32+
## project structure
33+
34+
```
35+
src/
36+
├── main.rs # CLI entry point (clap argument parsing)
37+
├── lib.rs # transpile() — orchestrates the full pipeline
38+
├── args.rs # CLI argument definitions
39+
├── config.rs # Config struct and PythonVersion enum
40+
├── source_map.rs # source map generation
41+
├── symbol_table.rs # module-level symbol collection
42+
├── transforms/ # forward transforms (basedpython → Python)
43+
│ ├── mod.rs
44+
│ ├── subscript.rs
45+
│ ├── mutable_defaults.rs
46+
│ ├── generics.rs
47+
│ ├── modifiers.rs
48+
│ ├── coalesce.rs
49+
│ ├── none_chain.rs
50+
│ └── ...
51+
└── reverse_transforms/ # reverse transforms (Python → basedpython)
52+
├── mod.rs
53+
├── empty_class.rs
54+
├── literal_types.rs
55+
└── subscript.rs
56+
crates/ # forked ruff/ty crates
57+
docs/ # documentation pages
58+
tests/ # end-to-end tests
59+
```
60+
61+
## adding a new transform
62+
63+
1. **create the transform file** in `src/transforms/`, e.g. `my_feature.rs`
64+
65+
2. **define a struct** that holds the source text and a `Vec<(TextRange, String)>` for edits:
66+
67+
```rust
68+
pub struct MyFeature<'src> {
69+
source: &'src str,
70+
pub edits: Vec<(ruff_text_size::TextRange, String)>,
71+
}
72+
```
73+
74+
3. **implement `Visitor`** from `ruff_python_ast::visitor` — walk the AST nodes you care about and push edits:
75+
76+
```rust
77+
impl Visitor<'_> for MyFeature<'_> {
78+
fn visit_stmt(&mut self, stmt: &Stmt) {
79+
// detect your pattern, push to self.edits
80+
walk_stmt(self, stmt);
81+
}
82+
}
83+
```
84+
85+
4. **register the transform** in `src/transforms/mod.rs` and wire it into the pipeline in `src/lib.rs`:
86+
- instantiate it alongside the other transforms
87+
- call `visitor.visit_stmt(stmt)` in the main loop
88+
- extend the edits vec with your transform's edits
89+
- if your transform needs generated imports (e.g. `from typing import TypeVar`), integrate with the **preamble system** — after all edits are applied, `lib.rs` collects import requests from transforms and prepends them to the output file. see how `generics.needed_imports` or `literal_types.needs_literal_import` for examples
90+
91+
5. **add tests** in a `#[cfg(test)]` module at the bottom of your file:
92+
93+
```rust
94+
#[cfg(test)]
95+
mod tests {
96+
use crate::{Config, transpile};
97+
98+
fn t(input: &str) -> String {
99+
transpile(input, &Config::default()).unwrap()
100+
}
101+
102+
#[test]
103+
fn basic() {
104+
assert_eq!(t("input code"), "expected output");
105+
}
106+
}
107+
```
108+
109+
6. **add a doc page** in `docs/` describing the syntax and showing examples
110+
111+
## running the docs locally
112+
113+
the documentation site is built with [zensical](https://github.com/kotlinisland/zensical). to view it locally, use:
114+
115+
```sh
116+
uv run zensical serve
117+
```
118+
119+
this starts a local dev server (usually at `http://localhost:8000`) with live reload. pages are in `docs/` and the site configuration is in `zensical.toml`
120+
121+
## adding a reverse transform
122+
123+
reverse transforms live in `src/reverse_transforms/` and follow the same pattern. they detect standard Python patterns that correspond to basedpython idioms and rewrite them back. register new reverse transforms in `src/reverse_transforms/mod.rs`
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ TODO: more top types like `Callable[Ts, None]`?
5454
`Concatenate` will be replaced with a new unpack notation:
5555
`***` means unpack positional and keyword items, a combination of `*` and `**`
5656
```bython
57-
def f[P: Parameters](fn: Callable[P, None] -> Callable[[int, ***P], None]:
57+
def f[P: Parameters](fn: Callable[P, None]) -> Callable[[int, ***P], None]:
5858
```
5959

60-
`Callable` will expose it's type via "attributes as types" by forwarding to it's `Parameters` type parameter
60+
`Callable` will expose its type via "attributes as types" by forwarding to it's `Parameters` type parameter
6161
```bython
6262
class Callable[P: Parameters, R]:
6363
@type_check_only
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# how transpilation works
2+
3+
basedpython transpiles `.by` source files into standard Python
4+
5+
## pipeline
6+
7+
```
8+
source (.by)
9+
10+
├─ 1. parse (ruff_python_parser)
11+
│ └─ produces a Python AST
12+
13+
├─ 2. build symbol table
14+
│ └─ collects module-level names for use by transforms
15+
16+
├─ 3. run transforms
17+
│ └─ each transform walks the AST and emits text edits (range → replacement)
18+
19+
├─ 4. collect & deduplicate edits
20+
│ └─ overlapping edits are resolved
21+
22+
├─ 5. apply edits to source text
23+
│ └─ produces the output Python string
24+
25+
├─ 6. append preamble
26+
│ └─ generated imports (typing, typing_extensions, etc.) are prepended
27+
28+
└─ 7. build source map
29+
└─ maps output byte offsets back to input byte offsets
30+
```
31+
32+
## transforms
33+
34+
each transform is a struct that implements ruff's `Visitor` trait. it walks the AST looking for patterns it handles and records text edits as `(TextRange, String)` pairs. transforms do not modify the AST — they only produce edits against the original source text
35+
36+
all transforms run independently over the same AST in a single pass:
37+
38+
| transform | file | what it does |
39+
|---|---|---|
40+
| subscription normalization | `subscript.rs` | normalizes tuple subscript keys |
41+
| mutable defaults | `mutable_defaults.rs` | rewrites mutable default arguments to sentinel pattern |
42+
| typing redirect | `typing_redirect.rs` | redirects `typing` imports to `typing_extensions` |
43+
| generics polyfill | `generics.rs` | desugars PEP 695 generics to TypeVar/Generic |
44+
| compat rewrites | `compat.rs` | rewrites expressions for older Python versions |
45+
| tuple literal types | `annotation.rs` | `(int, str)``tuple[int, str]` in annotations |
46+
| literal types | `literal_types.rs` | string/number literals in type positions → `Literal[...]` |
47+
| auto-quoting | `auto_quote.rs` | quotes forward self-references in class definitions |
48+
| intersection types | `intersection.rs` | `A & B``Intersection[A, B]` |
49+
| callable syntax | `callable.rs` | `(int) -> int``Callable[[int], int]` |
50+
| unpack syntax | `unpack.rs` | `*tuple[int, ...]``Unpack[tuple[int, ...]]` |
51+
| empty declarations | `empty_declarations.rs` | `class A``class A: ...` |
52+
| modifiers | `modifiers.rs` | keyword modifiers → decorators/annotations |
53+
| overload | `overload.rs` | stacked signatures → `@overload` |
54+
| none-coalescing | `coalesce.rs` | `a ?? b` → conditional expression |
55+
| none-chaining | `none_chain.rs` | `a?.b` → guarded access |
56+
| multiline dedent | `dedent_string.rs` | strips common indentation from triple-quoted strings |
57+
| typed lambda | `typed_lambda.rs` | `lambda (a: int): ...``lambda a: ...` |
58+
59+
after all transforms run, their edits are merged, deduplicated, and applied to produce the final output
60+
61+
## source maps
62+
63+
`transpile_with_map` returns a `SourceMap` alongside the output string. the source map records how byte offsets in the generated Python correspond to byte offsets in the original `.by` source. this enables error messages and debuggers to point back to the correct line in the original file
64+
65+
## reverse transforms
66+
67+
basedpython also supports reverse transpilation — converting standard Python back into basedpython syntax. see the [reverse transforms](reverse-transforms.md) page for details
68+
69+
## preamble generation
70+
71+
transforms that require new imports (e.g. `from typing import TypeVar`) register them during the walk. after all edits are applied, the transpiler collects these imports and prepends them as a preamble to the output file
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# reverse transforms
2+
3+
basedpython can convert standard Python source back into basedpython syntax. this is the inverse of the normal transpilation pipeline
4+
5+
## usage
6+
7+
```sh
8+
by transpile --reverse file.py
9+
```
10+
11+
## what it does
12+
13+
reverse transforms detect patterns in standard Python that correspond to basedpython idioms and rewrite them back. this enables round-tripping: a Python file run through `--reverse` and then transpiled forward should produce code with the same AST as the original
14+
15+
16+
## implemented reverse transforms
17+
18+
### empty class bodies
19+
20+
detects `class A: ...` (with a `pass` or `...` body) and rewrites it to the basedpython shorthand:
21+
22+
```python
23+
# standard Python
24+
class A: ...
25+
26+
# basedpython
27+
class A
28+
```
29+
30+
### literal types
31+
32+
detects `Literal["foo", 5]` in annotations and rewrites to basedpython's inline literal syntax:
33+
34+
```python
35+
# standard Python
36+
from typing import Literal
37+
a: Literal["foo", 5]
38+
39+
# basedpython
40+
a: "foo" | 5
41+
```
42+
43+
### subscription normalization
44+
45+
detects the trailing-comma 1-tuple form `x[(a, b),]` and rewrites it back to the natural parenthesized form:
46+
47+
```python
48+
# standard Python (transpiled)
49+
x[(a, b),]
50+
51+
# basedpython
52+
x[(a, b)]
53+
```
54+
55+
## design
56+
57+
each reverse transform lives in `src/reverse_transforms/` and mirrors the structure of the forward transforms in `src/transforms/`. they use the same visitor-based approach: walk the AST, detect the polyfill pattern, and emit text edits to rewrite it back to basedpython syntax
File renamed without changes.

docs/features/auto-quoting.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# auto-quoting
2+
3+
basedpython automatically quotes forward self-references in class definitions, so you don't need `from __future__ import annotations` or manual string literals
4+
5+
## transformation
6+
7+
```python
8+
# basedpython
9+
class A(list[A]):
10+
children: list[A]
11+
```
12+
```python
13+
# generated Python
14+
class A(list["A"]):
15+
children: list["A"]
16+
```
17+
18+
## when it applies
19+
20+
the transform applies when the class's own name appears as a subscript slice argument in:
21+
22+
- base class expressions (e.g. `list[A]` in `class A(list[A])`)
23+
- the class body (e.g. `list[A]` in an annotation)
24+
25+
direct base class references (`class A(A):`) are left alone — that is a runtime error regardless of quoting
26+
27+
## motivation
28+
29+
in Python, a class name is not yet bound when its body is being evaluated. using the class name in a generic base class like `class Tree(list[Tree])` raises a `NameError` at runtime. the standard workaround is either `from __future__ import annotations` (which defers all annotation evaluation) or manually writing `list["Tree"]`
30+
31+
basedpython handles this automatically — you write the natural syntax, and the transpiler inserts the quotes where needed

docs/features/callable-syntax.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# callable syntax
2+
3+
basedpython lets you write callable types using arrow syntax instead of `typing.Callable`
4+
5+
## transformation rules
6+
7+
| basedpython | Python output |
8+
|---|---|
9+
| `(int) -> int` | `Callable[[int], int]` |
10+
| `(int, str) -> bool` | `Callable[[int, str], bool]` |
11+
| `() -> None` | `Callable[[], None]` |
12+
| `(int) -> (str) -> bool` | `Callable[[int], Callable[[str], bool]]` |
13+
14+
the `Callable` import from `typing` is added automatically when needed
15+
16+
## examples
17+
18+
basic callable annotation:
19+
20+
```python
21+
# basedpython
22+
a: (int) -> int
23+
```
24+
```python
25+
# generated Python
26+
from typing import Callable
27+
a: Callable[[int], int]
28+
```
29+
30+
multiple parameters:
31+
32+
```python
33+
# basedpython
34+
a: (int, str) -> bool
35+
```
36+
```python
37+
# generated Python
38+
from typing import Callable
39+
a: Callable[[int, str], bool]
40+
```
41+
42+
no parameters:
43+
44+
```python
45+
# basedpython
46+
a: () -> None
47+
```
48+
```python
49+
# generated Python
50+
from typing import Callable
51+
a: Callable[[], None]
52+
```
53+
54+
nested callables:
55+
56+
```python
57+
# basedpython
58+
a: (int) -> (str) -> bool
59+
```
60+
```python
61+
# generated Python
62+
from typing import Callable
63+
a: Callable[[int], Callable[[str], bool]]
64+
```
65+
66+
works inside generic types:
67+
68+
```python
69+
# basedpython
70+
a: list[(int) -> int]
71+
```
72+
```python
73+
# generated Python
74+
from typing import Callable
75+
a: list[Callable[[int], int]]
76+
```
77+
78+
## annotation context only
79+
80+
the arrow syntax is only rewritten in type annotation positions. in value expressions, parenthesized tuples are left unchanged:
81+
82+
```python
83+
x = (int) # not rewritten — this is a value expression
84+
```

0 commit comments

Comments
 (0)