Skip to content

Commit 6ca15bd

Browse files
committed
0.1.5
1 parent f4573d8 commit 6ca15bd

File tree

9 files changed

+285
-6
lines changed

9 files changed

+285
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ htmlcov/
4141
.cache
4242
nosetests.xml
4343
coverage.xml
44+
.codspeed
4445

4546
# Translations
4647
*.mo

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fast-walk"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
edition = "2024"
55
license = "MIT"
66
readme = "README.md"
@@ -13,6 +13,8 @@ crate-type = ["cdylib"]
1313

1414
[profile.release]
1515
debug = false
16+
lto = true # Link-time optimization.
17+
codegen-units = 1 # Slower compilation but faster code.
1618

1719
[dependencies]
1820
pyo3 = "0.27.1"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pip install fast-walk
1212

1313
```python
1414
from fast_walk import walk
15-
import parse
15+
import ast
1616

1717
code = """
1818
def hello( x,y, z ):

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ build-backend = "maturin"
44

55
[project]
66
name = "fast-walk"
7+
description = "A fast reimplementation of ast.walk()"
78
requires-python = ">=3.13"
89
classifiers = [
910
"Programming Language :: Rust",
1011
"Programming Language :: Python :: Implementation :: CPython",
1112
"Programming Language :: Python :: Implementation :: PyPy",
13+
"Programming Language :: Python :: 3",
14+
"Programming Language :: Python :: 3.13",
15+
"Programming Language :: Python :: 3.14",
16+
"Programming Language :: Python :: 3 :: Only",
1217
]
18+
license = { text = "MIT" }
19+
readme = "README.md"
1320
dynamic = ["version"]
1421

1522
[tool.maturin]
@@ -23,3 +30,6 @@ cache-keys = [
2330
{ file = "fast_walk.pyi" },
2431
{ file = "src/**/*.rs" },
2532
]
33+
34+
[dependency-groups]
35+
dev = ["pytest>=8.4.2", "pytest-codspeed>=4.2.0"]

src/lib.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
use pyo3::types::{PyList, PyModule, PyTuple};
22
use pyo3::{intern, prelude::*};
33

4+
fn getattr<'py>(
5+
obj: &Bound<'py, PyAny>,
6+
attr_name: &Bound<'py, PyAny>,
7+
) -> Option<Bound<'py, PyAny>> {
8+
let py = obj.py();
9+
10+
let mut resp_ptr: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
11+
let attr_ptr = unsafe {
12+
pyo3::ffi::PyObject_GetOptionalAttr(obj.as_ptr(), attr_name.as_ptr(), &mut resp_ptr)
13+
};
14+
15+
if attr_ptr == 1 {
16+
Some(unsafe { Bound::from_owned_ptr(py, resp_ptr) })
17+
} else {
18+
None
19+
}
20+
}
21+
422
fn walk_node<'py>(
523
node: Bound<'py, PyAny>,
624
field_names: Bound<'py, PyTuple>,
@@ -10,18 +28,18 @@ fn walk_node<'py>(
1028

1129
// Recursively walk through child nodes
1230
for field in field_names {
13-
if let Ok(Some(child)) = node.getattr_opt(unsafe { field.cast_unchecked() }) {
31+
if let Some(child) = getattr(&node, &field) {
1432
if child.is_exact_instance_of::<PyList>() {
1533
for item in unsafe { child.cast_unchecked::<PyList>() } {
16-
if let Ok(Some(subfields)) = item.getattr_opt(intern!(item.py(), "_fields")) {
34+
if let Some(subfields) = getattr(&item, intern!(item.py(), "_fields")) {
1735
walk_node(
1836
item,
1937
unsafe { subfields.cast_into_unchecked::<PyTuple>() },
2038
result_list,
2139
)?;
2240
}
2341
}
24-
} else if let Ok(Some(subfields)) = child.getattr_opt(intern!(child.py(), "_fields")) {
42+
} else if let Some(subfields) = getattr(&child, intern!(child.py(), "_fields")) {
2543
walk_node(
2644
child,
2745
unsafe { subfields.cast_into_unchecked::<PyTuple>() },

tests/benchmarks.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from ast import AST, parse
2+
from ast import walk as ast_walk
3+
from collections.abc import Callable, Iterable
4+
from pathlib import Path
5+
6+
from pytest_codspeed import BenchmarkFixture
7+
from fast_walk import walk as fast_walk
8+
import pytest
9+
10+
11+
def python_walk_helper(node: AST, fields: tuple[str, ...], nodes: list[AST]):
12+
nodes.append(node)
13+
for field in fields:
14+
value = getattr(node, field, None)
15+
if type(value) is list:
16+
for item in value:
17+
if subfields := getattr(item, "_fields", None):
18+
python_walk_helper(item, subfields, nodes)
19+
elif subfields := getattr(value, "_fields", None):
20+
python_walk_helper(value, subfields, nodes) # pyright: ignore[reportArgumentType]
21+
22+
23+
def python_walk(node: AST) -> list[AST]:
24+
nodes: list[AST] = []
25+
python_walk_helper(node, node._fields, nodes)
26+
return nodes
27+
28+
29+
@pytest.mark.parametrize(
30+
"algorithm",
31+
[
32+
ast_walk,
33+
fast_walk,
34+
python_walk,
35+
],
36+
)
37+
def test_walk(benchmark: BenchmarkFixture, algorithm: Callable[[AST], Iterable[AST]]):
38+
import difflib
39+
40+
source_code = Path(difflib.__file__).read_text()
41+
node = parse(source_code)
42+
43+
def run():
44+
for _ in algorithm(node):
45+
pass
46+
47+
benchmark(run)

tests/manual.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
if __name__ == "__main__":
2+
from ast import parse
3+
from pathlib import Path
4+
from fast_walk import walk
5+
import difflib
6+
7+
source_code = Path(difflib.__file__).read_text()
8+
node = parse(source_code)
9+
10+
for _ in range(10_000):
11+
walk(node)

0 commit comments

Comments
 (0)