sqlnbfmt 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/PKG-INFO +68 -11
- sqlnbfmt-0.3.2/sqlnbfmt.egg-info/PKG-INFO → sqlnbfmt-0.4.0/README.md +58 -30
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/pyproject.toml +11 -4
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt/formatter.py +124 -15
- sqlnbfmt-0.3.2/README.md → sqlnbfmt-0.4.0/sqlnbfmt.egg-info/PKG-INFO +87 -8
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt.egg-info/SOURCES.txt +2 -1
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/tests/test_eval.py +35 -2
- sqlnbfmt-0.4.0/tests/test_marimo.py +278 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/LICENSE +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/setup.cfg +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt/__init__.py +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt.egg-info/dependency_links.txt +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt.egg-info/entry_points.txt +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt.egg-info/requires.txt +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/sqlnbfmt.egg-info/top_level.txt +0 -0
- {sqlnbfmt-0.3.2 → sqlnbfmt-0.4.0}/tests/test_formatter.py +0 -0
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlnbfmt
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A tool to format SQL code in Jupyter notebooks.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A tool to format SQL code in Jupyter notebooks and Marimo notebooks.
|
|
5
5
|
Author-email: Qing <qingye779@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/flyersworder/sqlnbfmt
|
|
8
8
|
Project-URL: Repository, https://github.com/flyersworder/sqlnbfmt
|
|
9
9
|
Project-URL: Changelog, https://github.com/flyersworder/sqlnbfmt/blob/main/CHANGELOG.md
|
|
10
10
|
Project-URL: Issues, https://github.com/flyersworder/sqlnbfmt/issues
|
|
11
|
-
Keywords: sql,jupyter,notebook,formatter,sqlglot,pre-commit
|
|
11
|
+
Keywords: sql,jupyter,marimo,notebook,formatter,sqlglot,pre-commit
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
16
|
Classifier: Programming Language :: Python :: 3
|
|
13
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Framework :: Jupyter
|
|
15
22
|
Requires-Python: >=3.12
|
|
16
23
|
Description-Content-Type: text/markdown
|
|
17
24
|
License-File: LICENSE
|
|
@@ -23,19 +30,21 @@ Dynamic: license-file
|
|
|
23
30
|
# sqlnbfmt
|
|
24
31
|
|
|
25
32
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
33
|
+
[](https://github.com/flyersworder/sqlnbfmt/actions/workflows/ci-and-publish.yml)
|
|
26
34
|
[](https://github.com/flyersworder/sqlnbfmt/blob/main/LICENSE)
|
|
27
35
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
28
36
|
|
|
29
37
|
> **Quick start:** `pip install sqlnbfmt && sqlnbfmt notebook.ipynb`
|
|
30
38
|
|
|
31
|
-
A SQL formatter
|
|
39
|
+
A SQL formatter for Jupyter Notebooks and Marimo notebooks. `sqlnbfmt` automatically formats SQL queries embedded in notebook cells — Python strings, SQL magic cells (`%%sql`), and Marimo `mo.sql()` calls — helping you maintain clean and consistent code.
|
|
32
40
|
|
|
33
41
|
## Features
|
|
34
42
|
|
|
35
43
|
- **Zero-config**: Works out of the box with sensible defaults — no config file needed
|
|
36
|
-
- **
|
|
44
|
+
- **Jupyter + Marimo**: Formats SQL in `.ipynb` notebooks and Marimo `.py` notebooks
|
|
45
|
+
- **Smart SQL Detection**: Automatically identifies and formats SQL queries in code cells, magic SQL cells, and `mo.sql()` calls
|
|
37
46
|
- **AST-Powered**: Uses Abstract Syntax Tree parsing for accurate SQL string identification
|
|
38
|
-
- **Safe Formatting**: Preserves Python comments, query parameters (e.g., `%s`, `?`), and SQL comments
|
|
47
|
+
- **Safe Formatting**: Preserves Python comments, query parameters (e.g., `%s`, `?`), f-string placeholders, and SQL comments
|
|
39
48
|
- **CI-Friendly**: `--check` mode exits non-zero when formatting is needed; `--diff` shows what would change
|
|
40
49
|
- **Skip Hints**: Add `# sqlnbfmt: skip` to any cell to exclude it from formatting
|
|
41
50
|
- **Pre-commit Ready**: Seamlessly integrates with pre-commit hooks
|
|
@@ -51,14 +60,24 @@ pip install sqlnbfmt
|
|
|
51
60
|
|
|
52
61
|
### Command Line
|
|
53
62
|
|
|
54
|
-
Format
|
|
63
|
+
Format Jupyter notebooks:
|
|
55
64
|
```bash
|
|
56
65
|
sqlnbfmt path/to/your_notebook.ipynb
|
|
57
66
|
```
|
|
58
67
|
|
|
68
|
+
Format Marimo notebooks (or any Python file with SQL strings):
|
|
69
|
+
```bash
|
|
70
|
+
sqlnbfmt path/to/your_notebook.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Mix both in a single invocation:
|
|
74
|
+
```bash
|
|
75
|
+
sqlnbfmt *.ipynb marimo_app.py
|
|
76
|
+
```
|
|
77
|
+
|
|
59
78
|
Check formatting without modifying files (useful in CI):
|
|
60
79
|
```bash
|
|
61
|
-
sqlnbfmt --check path/to/your_notebook.ipynb
|
|
80
|
+
sqlnbfmt --check path/to/your_notebook.ipynb path/to/marimo_app.py
|
|
62
81
|
```
|
|
63
82
|
|
|
64
83
|
Show a diff of what would change:
|
|
@@ -68,7 +87,7 @@ sqlnbfmt --diff path/to/your_notebook.ipynb
|
|
|
68
87
|
|
|
69
88
|
### Skipping Cells
|
|
70
89
|
|
|
71
|
-
Add a `# sqlnbfmt: skip` comment
|
|
90
|
+
Add a `# sqlnbfmt: skip` comment to skip formatting. In Jupyter notebooks this applies per-cell; in Python files it applies to the entire file.
|
|
72
91
|
|
|
73
92
|
```python
|
|
74
93
|
# sqlnbfmt: skip
|
|
@@ -86,12 +105,18 @@ pip install pre-commit
|
|
|
86
105
|
```yaml
|
|
87
106
|
repos:
|
|
88
107
|
- repo: https://github.com/flyersworder/sqlnbfmt
|
|
89
|
-
rev: v0.
|
|
108
|
+
rev: v0.4.0
|
|
90
109
|
hooks:
|
|
91
110
|
- id: sqlnbfmt
|
|
92
111
|
types: [jupyter]
|
|
93
112
|
```
|
|
94
113
|
|
|
114
|
+
To also format Marimo notebooks (Python files), add the Python hook:
|
|
115
|
+
```yaml
|
|
116
|
+
- id: sqlnbfmt-py
|
|
117
|
+
files: '\.py$' # optionally narrow with: files: 'marimo_.*\.py$'
|
|
118
|
+
```
|
|
119
|
+
|
|
95
120
|
All arguments are optional. To specify a dialect or custom config:
|
|
96
121
|
```yaml
|
|
97
122
|
args: [--dialect, postgres, --config, config.yaml]
|
|
@@ -116,6 +141,7 @@ Use `--check` in GitHub Actions to enforce formatting:
|
|
|
116
141
|
run: |
|
|
117
142
|
pip install sqlnbfmt
|
|
118
143
|
sqlnbfmt --check **/*.ipynb
|
|
144
|
+
sqlnbfmt --check marimo_*.py # if using Marimo
|
|
119
145
|
```
|
|
120
146
|
|
|
121
147
|
## Configuration
|
|
@@ -134,7 +160,9 @@ Create a `config.yaml` file to customize formatting behavior. [Here](https://git
|
|
|
134
160
|
| `single_line_threshold` | Maximum length before splitting SQL | 80 |
|
|
135
161
|
| `indent_width` | Number of spaces for indentation | 4 |
|
|
136
162
|
|
|
137
|
-
##
|
|
163
|
+
## Examples
|
|
164
|
+
|
|
165
|
+
### Jupyter Notebook
|
|
138
166
|
|
|
139
167
|
Before formatting:
|
|
140
168
|
```python
|
|
@@ -159,6 +187,35 @@ ORDER BY
|
|
|
159
187
|
""")
|
|
160
188
|
```
|
|
161
189
|
|
|
190
|
+
### Marimo Notebook
|
|
191
|
+
|
|
192
|
+
Before formatting:
|
|
193
|
+
```python
|
|
194
|
+
@app.cell
|
|
195
|
+
def _(mo):
|
|
196
|
+
_df = mo.sql(f"select id, name from users where active = 1 order by name")
|
|
197
|
+
return
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
After formatting:
|
|
201
|
+
```python
|
|
202
|
+
@app.cell
|
|
203
|
+
def _(mo):
|
|
204
|
+
_df = mo.sql(
|
|
205
|
+
f"""
|
|
206
|
+
SELECT
|
|
207
|
+
id,
|
|
208
|
+
name
|
|
209
|
+
FROM users
|
|
210
|
+
WHERE
|
|
211
|
+
active = 1
|
|
212
|
+
ORDER BY
|
|
213
|
+
name
|
|
214
|
+
"""
|
|
215
|
+
)
|
|
216
|
+
return
|
|
217
|
+
```
|
|
218
|
+
|
|
162
219
|
## Troubleshooting
|
|
163
220
|
|
|
164
221
|
**SQL not being formatted?**
|
|
@@ -1,41 +1,21 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqlnbfmt
|
|
3
|
-
Version: 0.3.2
|
|
4
|
-
Summary: A tool to format SQL code in Jupyter notebooks.
|
|
5
|
-
Author-email: Qing <qingye779@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/flyersworder/sqlnbfmt
|
|
8
|
-
Project-URL: Repository, https://github.com/flyersworder/sqlnbfmt
|
|
9
|
-
Project-URL: Changelog, https://github.com/flyersworder/sqlnbfmt/blob/main/CHANGELOG.md
|
|
10
|
-
Project-URL: Issues, https://github.com/flyersworder/sqlnbfmt/issues
|
|
11
|
-
Keywords: sql,jupyter,notebook,formatter,sqlglot,pre-commit
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Requires-Python: >=3.12
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Requires-Dist: nbformat>=5.10.4
|
|
19
|
-
Requires-Dist: pyyaml>=6.0.2
|
|
20
|
-
Requires-Dist: sqlglot>=25.27.0
|
|
21
|
-
Dynamic: license-file
|
|
22
|
-
|
|
23
1
|
# sqlnbfmt
|
|
24
2
|
|
|
25
3
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
4
|
+
[](https://github.com/flyersworder/sqlnbfmt/actions/workflows/ci-and-publish.yml)
|
|
26
5
|
[](https://github.com/flyersworder/sqlnbfmt/blob/main/LICENSE)
|
|
27
6
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
28
7
|
|
|
29
8
|
> **Quick start:** `pip install sqlnbfmt && sqlnbfmt notebook.ipynb`
|
|
30
9
|
|
|
31
|
-
A SQL formatter
|
|
10
|
+
A SQL formatter for Jupyter Notebooks and Marimo notebooks. `sqlnbfmt` automatically formats SQL queries embedded in notebook cells — Python strings, SQL magic cells (`%%sql`), and Marimo `mo.sql()` calls — helping you maintain clean and consistent code.
|
|
32
11
|
|
|
33
12
|
## Features
|
|
34
13
|
|
|
35
14
|
- **Zero-config**: Works out of the box with sensible defaults — no config file needed
|
|
36
|
-
- **
|
|
15
|
+
- **Jupyter + Marimo**: Formats SQL in `.ipynb` notebooks and Marimo `.py` notebooks
|
|
16
|
+
- **Smart SQL Detection**: Automatically identifies and formats SQL queries in code cells, magic SQL cells, and `mo.sql()` calls
|
|
37
17
|
- **AST-Powered**: Uses Abstract Syntax Tree parsing for accurate SQL string identification
|
|
38
|
-
- **Safe Formatting**: Preserves Python comments, query parameters (e.g., `%s`, `?`), and SQL comments
|
|
18
|
+
- **Safe Formatting**: Preserves Python comments, query parameters (e.g., `%s`, `?`), f-string placeholders, and SQL comments
|
|
39
19
|
- **CI-Friendly**: `--check` mode exits non-zero when formatting is needed; `--diff` shows what would change
|
|
40
20
|
- **Skip Hints**: Add `# sqlnbfmt: skip` to any cell to exclude it from formatting
|
|
41
21
|
- **Pre-commit Ready**: Seamlessly integrates with pre-commit hooks
|
|
@@ -51,14 +31,24 @@ pip install sqlnbfmt
|
|
|
51
31
|
|
|
52
32
|
### Command Line
|
|
53
33
|
|
|
54
|
-
Format
|
|
34
|
+
Format Jupyter notebooks:
|
|
55
35
|
```bash
|
|
56
36
|
sqlnbfmt path/to/your_notebook.ipynb
|
|
57
37
|
```
|
|
58
38
|
|
|
39
|
+
Format Marimo notebooks (or any Python file with SQL strings):
|
|
40
|
+
```bash
|
|
41
|
+
sqlnbfmt path/to/your_notebook.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Mix both in a single invocation:
|
|
45
|
+
```bash
|
|
46
|
+
sqlnbfmt *.ipynb marimo_app.py
|
|
47
|
+
```
|
|
48
|
+
|
|
59
49
|
Check formatting without modifying files (useful in CI):
|
|
60
50
|
```bash
|
|
61
|
-
sqlnbfmt --check path/to/your_notebook.ipynb
|
|
51
|
+
sqlnbfmt --check path/to/your_notebook.ipynb path/to/marimo_app.py
|
|
62
52
|
```
|
|
63
53
|
|
|
64
54
|
Show a diff of what would change:
|
|
@@ -68,7 +58,7 @@ sqlnbfmt --diff path/to/your_notebook.ipynb
|
|
|
68
58
|
|
|
69
59
|
### Skipping Cells
|
|
70
60
|
|
|
71
|
-
Add a `# sqlnbfmt: skip` comment
|
|
61
|
+
Add a `# sqlnbfmt: skip` comment to skip formatting. In Jupyter notebooks this applies per-cell; in Python files it applies to the entire file.
|
|
72
62
|
|
|
73
63
|
```python
|
|
74
64
|
# sqlnbfmt: skip
|
|
@@ -86,12 +76,18 @@ pip install pre-commit
|
|
|
86
76
|
```yaml
|
|
87
77
|
repos:
|
|
88
78
|
- repo: https://github.com/flyersworder/sqlnbfmt
|
|
89
|
-
rev: v0.
|
|
79
|
+
rev: v0.4.0
|
|
90
80
|
hooks:
|
|
91
81
|
- id: sqlnbfmt
|
|
92
82
|
types: [jupyter]
|
|
93
83
|
```
|
|
94
84
|
|
|
85
|
+
To also format Marimo notebooks (Python files), add the Python hook:
|
|
86
|
+
```yaml
|
|
87
|
+
- id: sqlnbfmt-py
|
|
88
|
+
files: '\.py$' # optionally narrow with: files: 'marimo_.*\.py$'
|
|
89
|
+
```
|
|
90
|
+
|
|
95
91
|
All arguments are optional. To specify a dialect or custom config:
|
|
96
92
|
```yaml
|
|
97
93
|
args: [--dialect, postgres, --config, config.yaml]
|
|
@@ -116,6 +112,7 @@ Use `--check` in GitHub Actions to enforce formatting:
|
|
|
116
112
|
run: |
|
|
117
113
|
pip install sqlnbfmt
|
|
118
114
|
sqlnbfmt --check **/*.ipynb
|
|
115
|
+
sqlnbfmt --check marimo_*.py # if using Marimo
|
|
119
116
|
```
|
|
120
117
|
|
|
121
118
|
## Configuration
|
|
@@ -134,7 +131,9 @@ Create a `config.yaml` file to customize formatting behavior. [Here](https://git
|
|
|
134
131
|
| `single_line_threshold` | Maximum length before splitting SQL | 80 |
|
|
135
132
|
| `indent_width` | Number of spaces for indentation | 4 |
|
|
136
133
|
|
|
137
|
-
##
|
|
134
|
+
## Examples
|
|
135
|
+
|
|
136
|
+
### Jupyter Notebook
|
|
138
137
|
|
|
139
138
|
Before formatting:
|
|
140
139
|
```python
|
|
@@ -159,6 +158,35 @@ ORDER BY
|
|
|
159
158
|
""")
|
|
160
159
|
```
|
|
161
160
|
|
|
161
|
+
### Marimo Notebook
|
|
162
|
+
|
|
163
|
+
Before formatting:
|
|
164
|
+
```python
|
|
165
|
+
@app.cell
|
|
166
|
+
def _(mo):
|
|
167
|
+
_df = mo.sql(f"select id, name from users where active = 1 order by name")
|
|
168
|
+
return
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
After formatting:
|
|
172
|
+
```python
|
|
173
|
+
@app.cell
|
|
174
|
+
def _(mo):
|
|
175
|
+
_df = mo.sql(
|
|
176
|
+
f"""
|
|
177
|
+
SELECT
|
|
178
|
+
id,
|
|
179
|
+
name
|
|
180
|
+
FROM users
|
|
181
|
+
WHERE
|
|
182
|
+
active = 1
|
|
183
|
+
ORDER BY
|
|
184
|
+
name
|
|
185
|
+
"""
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
```
|
|
189
|
+
|
|
162
190
|
## Troubleshooting
|
|
163
191
|
|
|
164
192
|
**SQL not being formatted?**
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlnbfmt"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "A tool to format SQL code in Jupyter notebooks."
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "A tool to format SQL code in Jupyter notebooks and Marimo notebooks."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Qing", email = "qingye779@gmail.com" }
|
|
7
7
|
]
|
|
8
8
|
license = { text = "MIT" }
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
|
-
keywords = ["sql", "jupyter", "notebook", "formatter", "sqlglot", "pre-commit"]
|
|
11
|
+
keywords = ["sql", "jupyter", "marimo", "notebook", "formatter", "sqlglot", "pre-commit"]
|
|
12
12
|
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
13
17
|
"Programming Language :: Python :: 3",
|
|
14
18
|
"Programming Language :: Python :: 3.12",
|
|
15
|
-
"Programming Language :: Python :: 3.13"
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Framework :: Jupyter",
|
|
16
23
|
]
|
|
17
24
|
dependencies = [
|
|
18
25
|
"nbformat>=5.10.4",
|
|
@@ -4,6 +4,7 @@ import difflib
|
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
|
+
import textwrap
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Set, Optional, Dict, Union, Tuple
|
|
@@ -49,6 +50,7 @@ DEFAULT_FUNCTION_NAMES = frozenset(
|
|
|
49
50
|
"fetchone",
|
|
50
51
|
"fetchmany",
|
|
51
52
|
"execute_query",
|
|
53
|
+
"sql",
|
|
52
54
|
}
|
|
53
55
|
)
|
|
54
56
|
|
|
@@ -359,7 +361,7 @@ class SQLStringFormatter(ast.NodeVisitor):
|
|
|
359
361
|
except SQLFormattingError:
|
|
360
362
|
return None
|
|
361
363
|
|
|
362
|
-
if formatted_sql == sql_str:
|
|
364
|
+
if formatted_sql == textwrap.dedent(sql_str).strip():
|
|
363
365
|
return None
|
|
364
366
|
|
|
365
367
|
# Restore placeholders as f-string expressions
|
|
@@ -574,6 +576,47 @@ def _format_cells(
|
|
|
574
576
|
return changed
|
|
575
577
|
|
|
576
578
|
|
|
579
|
+
def _format_python_source(
|
|
580
|
+
source: str,
|
|
581
|
+
config: FormattingConfig,
|
|
582
|
+
dialect: Optional[str],
|
|
583
|
+
logger: logging.Logger,
|
|
584
|
+
) -> Tuple[str, bool]:
|
|
585
|
+
"""Format SQL strings in Python source code.
|
|
586
|
+
|
|
587
|
+
Returns (formatted_source, changed) tuple.
|
|
588
|
+
"""
|
|
589
|
+
if not source.strip():
|
|
590
|
+
return source, False
|
|
591
|
+
|
|
592
|
+
if _has_skip_hint(source):
|
|
593
|
+
return source, False
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
tree = ast.parse(source)
|
|
597
|
+
except SyntaxError:
|
|
598
|
+
logger.warning("Failed to parse Python source")
|
|
599
|
+
return source, False
|
|
600
|
+
|
|
601
|
+
line_offsets = build_line_offsets(source)
|
|
602
|
+
formatter = SQLStringFormatter(config, dialect, logger, line_offsets)
|
|
603
|
+
formatter.visit(tree)
|
|
604
|
+
|
|
605
|
+
if not formatter.replacements:
|
|
606
|
+
return source, False
|
|
607
|
+
|
|
608
|
+
result = source
|
|
609
|
+
for rep in sorted(
|
|
610
|
+
formatter.replacements,
|
|
611
|
+
key=lambda r: r.start_offset,
|
|
612
|
+
reverse=True,
|
|
613
|
+
):
|
|
614
|
+
result = result[: rep.start_offset] + rep.new_text + result[rep.end_offset :]
|
|
615
|
+
|
|
616
|
+
changed = result != source
|
|
617
|
+
return result, changed
|
|
618
|
+
|
|
619
|
+
|
|
577
620
|
def process_notebook(
|
|
578
621
|
notebook_path: Union[str, Path],
|
|
579
622
|
config: FormattingConfig,
|
|
@@ -634,13 +677,69 @@ def diff_notebook(
|
|
|
634
677
|
return "".join(diff_lines)
|
|
635
678
|
|
|
636
679
|
|
|
680
|
+
def process_python_file(
|
|
681
|
+
file_path: Union[str, Path],
|
|
682
|
+
config: FormattingConfig,
|
|
683
|
+
dialect: Optional[str],
|
|
684
|
+
logger: logging.Logger,
|
|
685
|
+
check_only: bool = False,
|
|
686
|
+
) -> bool:
|
|
687
|
+
"""Processes a Python file (e.g. Marimo notebook).
|
|
688
|
+
|
|
689
|
+
Returns True if changes were made (or would be made in check mode).
|
|
690
|
+
"""
|
|
691
|
+
try:
|
|
692
|
+
file_path = Path(file_path)
|
|
693
|
+
source = file_path.read_text(encoding="utf-8")
|
|
694
|
+
formatted, changed = _format_python_source(source, config, dialect, logger)
|
|
695
|
+
|
|
696
|
+
if changed and not check_only:
|
|
697
|
+
file_path.write_text(formatted, encoding="utf-8")
|
|
698
|
+
logger.info(f"Updated file: {file_path}")
|
|
699
|
+
|
|
700
|
+
return changed
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
logger.error(f"Failed to process file {file_path}: {e}", exc_info=True)
|
|
704
|
+
raise
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def diff_python_file(
|
|
708
|
+
file_path: Union[str, Path],
|
|
709
|
+
config: FormattingConfig,
|
|
710
|
+
dialect: Optional[str],
|
|
711
|
+
logger: logging.Logger,
|
|
712
|
+
) -> str:
|
|
713
|
+
"""Returns a unified diff of formatting changes for a Python file."""
|
|
714
|
+
file_path = Path(file_path)
|
|
715
|
+
source = file_path.read_text(encoding="utf-8")
|
|
716
|
+
formatted, _ = _format_python_source(source, config, dialect, logger)
|
|
717
|
+
|
|
718
|
+
if source == formatted:
|
|
719
|
+
return ""
|
|
720
|
+
|
|
721
|
+
return "".join(
|
|
722
|
+
difflib.unified_diff(
|
|
723
|
+
source.splitlines(keepends=True),
|
|
724
|
+
formatted.splitlines(keepends=True),
|
|
725
|
+
fromfile=f"{file_path} original",
|
|
726
|
+
tofile=f"{file_path} formatted",
|
|
727
|
+
)
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
637
731
|
def main():
|
|
638
732
|
"""Main entry point for the SQL formatter."""
|
|
639
733
|
import argparse
|
|
640
734
|
|
|
641
|
-
parser = argparse.ArgumentParser(
|
|
735
|
+
parser = argparse.ArgumentParser(
|
|
736
|
+
description="Format SQL code in Jupyter notebooks and Python files"
|
|
737
|
+
)
|
|
642
738
|
parser.add_argument(
|
|
643
|
-
"
|
|
739
|
+
"files",
|
|
740
|
+
nargs="+",
|
|
741
|
+
type=Path,
|
|
742
|
+
help="Notebook (.ipynb) or Python (.py) file paths to process",
|
|
644
743
|
)
|
|
645
744
|
parser.add_argument(
|
|
646
745
|
"--config", type=Path, default=None, help="Configuration file path (optional)"
|
|
@@ -679,41 +778,51 @@ def main():
|
|
|
679
778
|
try:
|
|
680
779
|
changed_files = []
|
|
681
780
|
|
|
682
|
-
for
|
|
781
|
+
for file_path in args.files:
|
|
782
|
+
is_python = file_path.suffix == ".py"
|
|
783
|
+
|
|
683
784
|
if args.check or args.diff:
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
785
|
+
if is_python:
|
|
786
|
+
diff_output = diff_python_file(
|
|
787
|
+
file_path, config, args.dialect, logger
|
|
788
|
+
)
|
|
789
|
+
else:
|
|
790
|
+
diff_output = diff_notebook(file_path, config, args.dialect, logger)
|
|
687
791
|
needs_formatting = bool(diff_output)
|
|
688
792
|
|
|
689
793
|
if args.diff and diff_output:
|
|
690
794
|
print(diff_output, end="")
|
|
691
795
|
|
|
692
796
|
if needs_formatting:
|
|
693
|
-
changed_files.append(
|
|
797
|
+
changed_files.append(file_path)
|
|
694
798
|
else:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
799
|
+
if is_python:
|
|
800
|
+
changed = process_python_file(
|
|
801
|
+
file_path, config, args.dialect, logger
|
|
802
|
+
)
|
|
803
|
+
else:
|
|
804
|
+
changed = process_notebook(file_path, config, args.dialect, logger)
|
|
805
|
+
if changed:
|
|
806
|
+
changed_files.append(file_path)
|
|
698
807
|
|
|
699
808
|
if args.check:
|
|
700
809
|
if changed_files:
|
|
701
|
-
logger.error(f"{len(changed_files)}
|
|
810
|
+
logger.error(f"{len(changed_files)} file(s) would be reformatted:")
|
|
702
811
|
for f in changed_files:
|
|
703
812
|
logger.error(f" - {f}")
|
|
704
813
|
sys.exit(1)
|
|
705
814
|
else:
|
|
706
|
-
logger.info("All
|
|
815
|
+
logger.info("All files are properly formatted.")
|
|
707
816
|
sys.exit(0)
|
|
708
817
|
|
|
709
818
|
# Print summary for normal mode
|
|
710
819
|
if not args.diff:
|
|
711
820
|
if changed_files:
|
|
712
|
-
logger.info("Changes made to the following
|
|
821
|
+
logger.info("Changes made to the following files:")
|
|
713
822
|
for file in changed_files:
|
|
714
823
|
logger.info(f" - {file}")
|
|
715
824
|
else:
|
|
716
|
-
logger.info("No changes needed. All
|
|
825
|
+
logger.info("No changes needed. All files are properly formatted.")
|
|
717
826
|
|
|
718
827
|
sys.exit(0)
|
|
719
828
|
|
|
@@ -1,19 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlnbfmt
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A tool to format SQL code in Jupyter notebooks and Marimo notebooks.
|
|
5
|
+
Author-email: Qing <qingye779@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/flyersworder/sqlnbfmt
|
|
8
|
+
Project-URL: Repository, https://github.com/flyersworder/sqlnbfmt
|
|
9
|
+
Project-URL: Changelog, https://github.com/flyersworder/sqlnbfmt/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/flyersworder/sqlnbfmt/issues
|
|
11
|
+
Keywords: sql,jupyter,marimo,notebook,formatter,sqlglot,pre-commit
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Framework :: Jupyter
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: nbformat>=5.10.4
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
27
|
+
Requires-Dist: sqlglot>=25.27.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
1
30
|
# sqlnbfmt
|
|
2
31
|
|
|
3
32
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
33
|
+
[](https://github.com/flyersworder/sqlnbfmt/actions/workflows/ci-and-publish.yml)
|
|
4
34
|
[](https://github.com/flyersworder/sqlnbfmt/blob/main/LICENSE)
|
|
5
35
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
6
36
|
|
|
7
37
|
> **Quick start:** `pip install sqlnbfmt && sqlnbfmt notebook.ipynb`
|
|
8
38
|
|
|
9
|
-
A SQL formatter
|
|
39
|
+
A SQL formatter for Jupyter Notebooks and Marimo notebooks. `sqlnbfmt` automatically formats SQL queries embedded in notebook cells — Python strings, SQL magic cells (`%%sql`), and Marimo `mo.sql()` calls — helping you maintain clean and consistent code.
|
|
10
40
|
|
|
11
41
|
## Features
|
|
12
42
|
|
|
13
43
|
- **Zero-config**: Works out of the box with sensible defaults — no config file needed
|
|
14
|
-
- **
|
|
44
|
+
- **Jupyter + Marimo**: Formats SQL in `.ipynb` notebooks and Marimo `.py` notebooks
|
|
45
|
+
- **Smart SQL Detection**: Automatically identifies and formats SQL queries in code cells, magic SQL cells, and `mo.sql()` calls
|
|
15
46
|
- **AST-Powered**: Uses Abstract Syntax Tree parsing for accurate SQL string identification
|
|
16
|
-
- **Safe Formatting**: Preserves Python comments, query parameters (e.g., `%s`, `?`), and SQL comments
|
|
47
|
+
- **Safe Formatting**: Preserves Python comments, query parameters (e.g., `%s`, `?`), f-string placeholders, and SQL comments
|
|
17
48
|
- **CI-Friendly**: `--check` mode exits non-zero when formatting is needed; `--diff` shows what would change
|
|
18
49
|
- **Skip Hints**: Add `# sqlnbfmt: skip` to any cell to exclude it from formatting
|
|
19
50
|
- **Pre-commit Ready**: Seamlessly integrates with pre-commit hooks
|
|
@@ -29,14 +60,24 @@ pip install sqlnbfmt
|
|
|
29
60
|
|
|
30
61
|
### Command Line
|
|
31
62
|
|
|
32
|
-
Format
|
|
63
|
+
Format Jupyter notebooks:
|
|
33
64
|
```bash
|
|
34
65
|
sqlnbfmt path/to/your_notebook.ipynb
|
|
35
66
|
```
|
|
36
67
|
|
|
68
|
+
Format Marimo notebooks (or any Python file with SQL strings):
|
|
69
|
+
```bash
|
|
70
|
+
sqlnbfmt path/to/your_notebook.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Mix both in a single invocation:
|
|
74
|
+
```bash
|
|
75
|
+
sqlnbfmt *.ipynb marimo_app.py
|
|
76
|
+
```
|
|
77
|
+
|
|
37
78
|
Check formatting without modifying files (useful in CI):
|
|
38
79
|
```bash
|
|
39
|
-
sqlnbfmt --check path/to/your_notebook.ipynb
|
|
80
|
+
sqlnbfmt --check path/to/your_notebook.ipynb path/to/marimo_app.py
|
|
40
81
|
```
|
|
41
82
|
|
|
42
83
|
Show a diff of what would change:
|
|
@@ -46,7 +87,7 @@ sqlnbfmt --diff path/to/your_notebook.ipynb
|
|
|
46
87
|
|
|
47
88
|
### Skipping Cells
|
|
48
89
|
|
|
49
|
-
Add a `# sqlnbfmt: skip` comment
|
|
90
|
+
Add a `# sqlnbfmt: skip` comment to skip formatting. In Jupyter notebooks this applies per-cell; in Python files it applies to the entire file.
|
|
50
91
|
|
|
51
92
|
```python
|
|
52
93
|
# sqlnbfmt: skip
|
|
@@ -64,12 +105,18 @@ pip install pre-commit
|
|
|
64
105
|
```yaml
|
|
65
106
|
repos:
|
|
66
107
|
- repo: https://github.com/flyersworder/sqlnbfmt
|
|
67
|
-
rev: v0.
|
|
108
|
+
rev: v0.4.0
|
|
68
109
|
hooks:
|
|
69
110
|
- id: sqlnbfmt
|
|
70
111
|
types: [jupyter]
|
|
71
112
|
```
|
|
72
113
|
|
|
114
|
+
To also format Marimo notebooks (Python files), add the Python hook:
|
|
115
|
+
```yaml
|
|
116
|
+
- id: sqlnbfmt-py
|
|
117
|
+
files: '\.py$' # optionally narrow with: files: 'marimo_.*\.py$'
|
|
118
|
+
```
|
|
119
|
+
|
|
73
120
|
All arguments are optional. To specify a dialect or custom config:
|
|
74
121
|
```yaml
|
|
75
122
|
args: [--dialect, postgres, --config, config.yaml]
|
|
@@ -94,6 +141,7 @@ Use `--check` in GitHub Actions to enforce formatting:
|
|
|
94
141
|
run: |
|
|
95
142
|
pip install sqlnbfmt
|
|
96
143
|
sqlnbfmt --check **/*.ipynb
|
|
144
|
+
sqlnbfmt --check marimo_*.py # if using Marimo
|
|
97
145
|
```
|
|
98
146
|
|
|
99
147
|
## Configuration
|
|
@@ -112,7 +160,9 @@ Create a `config.yaml` file to customize formatting behavior. [Here](https://git
|
|
|
112
160
|
| `single_line_threshold` | Maximum length before splitting SQL | 80 |
|
|
113
161
|
| `indent_width` | Number of spaces for indentation | 4 |
|
|
114
162
|
|
|
115
|
-
##
|
|
163
|
+
## Examples
|
|
164
|
+
|
|
165
|
+
### Jupyter Notebook
|
|
116
166
|
|
|
117
167
|
Before formatting:
|
|
118
168
|
```python
|
|
@@ -137,6 +187,35 @@ ORDER BY
|
|
|
137
187
|
""")
|
|
138
188
|
```
|
|
139
189
|
|
|
190
|
+
### Marimo Notebook
|
|
191
|
+
|
|
192
|
+
Before formatting:
|
|
193
|
+
```python
|
|
194
|
+
@app.cell
|
|
195
|
+
def _(mo):
|
|
196
|
+
_df = mo.sql(f"select id, name from users where active = 1 order by name")
|
|
197
|
+
return
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
After formatting:
|
|
201
|
+
```python
|
|
202
|
+
@app.cell
|
|
203
|
+
def _(mo):
|
|
204
|
+
_df = mo.sql(
|
|
205
|
+
f"""
|
|
206
|
+
SELECT
|
|
207
|
+
id,
|
|
208
|
+
name
|
|
209
|
+
FROM users
|
|
210
|
+
WHERE
|
|
211
|
+
active = 1
|
|
212
|
+
ORDER BY
|
|
213
|
+
name
|
|
214
|
+
"""
|
|
215
|
+
)
|
|
216
|
+
return
|
|
217
|
+
```
|
|
218
|
+
|
|
140
219
|
## Troubleshooting
|
|
141
220
|
|
|
142
221
|
**SQL not being formatted?**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Fixture-based eval test suite for sqlnbfmt.
|
|
2
2
|
|
|
3
3
|
Auto-discovers test cases from tests/eval/*/ directories.
|
|
4
|
-
|
|
4
|
+
Jupyter cases: input.ipynb + expected.ipynb.
|
|
5
|
+
Marimo/Python cases: input.py + expected.py.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
import logging
|
|
@@ -11,7 +12,7 @@ from pathlib import Path
|
|
|
11
12
|
import nbformat
|
|
12
13
|
import pytest
|
|
13
14
|
|
|
14
|
-
from sqlnbfmt.formatter import load_config, process_notebook
|
|
15
|
+
from sqlnbfmt.formatter import load_config, process_notebook, process_python_file
|
|
15
16
|
|
|
16
17
|
EVAL_DIR = Path(__file__).resolve().parent / "eval"
|
|
17
18
|
DIALECT = "mysql"
|
|
@@ -78,3 +79,35 @@ def test_eval(case_name, logger, tmp_path):
|
|
|
78
79
|
f"Expected:\n{exp_cell.source}\n"
|
|
79
80
|
f"Actual:\n{fmt_cell.source}"
|
|
80
81
|
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def discover_marimo_eval_cases():
|
|
85
|
+
"""Find eval case directories containing input.py + expected.py."""
|
|
86
|
+
if not EVAL_DIR.is_dir():
|
|
87
|
+
return []
|
|
88
|
+
cases = []
|
|
89
|
+
for case_dir in sorted(EVAL_DIR.iterdir()):
|
|
90
|
+
if (
|
|
91
|
+
case_dir.is_dir()
|
|
92
|
+
and (case_dir / "input.py").exists()
|
|
93
|
+
and (case_dir / "expected.py").exists()
|
|
94
|
+
):
|
|
95
|
+
cases.append(case_dir.name)
|
|
96
|
+
return cases
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize("case_name", discover_marimo_eval_cases(), ids=lambda c: c)
|
|
100
|
+
def test_marimo_eval(case_name, logger, tmp_path):
|
|
101
|
+
"""Run formatter on input.py and compare with expected.py."""
|
|
102
|
+
case_dir = EVAL_DIR / case_name
|
|
103
|
+
work_path = tmp_path / "notebook.py"
|
|
104
|
+
shutil.copy2(case_dir / "input.py", work_path)
|
|
105
|
+
|
|
106
|
+
config = load_config()
|
|
107
|
+
process_python_file(work_path, config, DIALECT, logger)
|
|
108
|
+
|
|
109
|
+
formatted = work_path.read_text()
|
|
110
|
+
expected = (case_dir / "expected.py").read_text()
|
|
111
|
+
assert formatted == expected, (
|
|
112
|
+
f"[{case_name}] Content mismatch.\nExpected:\n{expected}\nActual:\n{formatted}"
|
|
113
|
+
)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from sqlnbfmt.formatter import (
|
|
7
|
+
_format_python_source,
|
|
8
|
+
diff_python_file,
|
|
9
|
+
load_config,
|
|
10
|
+
process_python_file,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def logger():
|
|
16
|
+
log = logging.getLogger("test_marimo")
|
|
17
|
+
log.setLevel(logging.DEBUG)
|
|
18
|
+
if not log.handlers:
|
|
19
|
+
handler = logging.StreamHandler()
|
|
20
|
+
handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
|
21
|
+
log.addHandler(handler)
|
|
22
|
+
return log
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# _format_python_source unit tests
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_format_mo_sql_basic(logger):
|
|
31
|
+
"""mo.sql() with a simple SELECT is formatted."""
|
|
32
|
+
source = """import marimo as mo
|
|
33
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
34
|
+
"""
|
|
35
|
+
config = load_config()
|
|
36
|
+
result, changed = _format_python_source(source, config, "mysql", logger)
|
|
37
|
+
assert changed
|
|
38
|
+
assert "SELECT" in result
|
|
39
|
+
assert "FROM users" in result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_format_mo_sql_fstring_placeholder(logger):
|
|
43
|
+
"""mo.sql() with f-string {expr} placeholders preserved."""
|
|
44
|
+
source = """import marimo as mo
|
|
45
|
+
table = "users"
|
|
46
|
+
_df = mo.sql(f"select * from {table} where active = 1")
|
|
47
|
+
"""
|
|
48
|
+
config = load_config()
|
|
49
|
+
result, changed = _format_python_source(source, config, "mysql", logger)
|
|
50
|
+
assert changed
|
|
51
|
+
assert "{table}" in result
|
|
52
|
+
assert "SELECT" in result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_format_non_sql_unchanged(logger):
|
|
56
|
+
"""Non-SQL Python code is not modified."""
|
|
57
|
+
source = """x = 1
|
|
58
|
+
y = "hello world"
|
|
59
|
+
print(x + y)
|
|
60
|
+
"""
|
|
61
|
+
config = load_config()
|
|
62
|
+
result, changed = _format_python_source(source, config, "mysql", logger)
|
|
63
|
+
assert not changed
|
|
64
|
+
assert result == source
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_format_idempotent(logger):
|
|
68
|
+
"""Formatting already-formatted output produces no changes."""
|
|
69
|
+
source = """import marimo as mo
|
|
70
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
71
|
+
"""
|
|
72
|
+
config = load_config()
|
|
73
|
+
result1, changed1 = _format_python_source(source, config, "mysql", logger)
|
|
74
|
+
assert changed1
|
|
75
|
+
result2, changed2 = _format_python_source(result1, config, "mysql", logger)
|
|
76
|
+
assert not changed2
|
|
77
|
+
assert result2 == result1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_format_idempotent_fstring(logger):
|
|
81
|
+
"""Idempotency holds for f-strings with placeholders."""
|
|
82
|
+
source = """import marimo as mo
|
|
83
|
+
table = "users"
|
|
84
|
+
_df = mo.sql(f"select * from {table} where active = 1")
|
|
85
|
+
"""
|
|
86
|
+
config = load_config()
|
|
87
|
+
result1, _ = _format_python_source(source, config, "mysql", logger)
|
|
88
|
+
result2, changed2 = _format_python_source(result1, config, "mysql", logger)
|
|
89
|
+
assert not changed2
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_format_skip_hint(logger):
|
|
93
|
+
"""File with skip hint is not modified."""
|
|
94
|
+
source = """# sqlnbfmt: skip
|
|
95
|
+
import marimo as mo
|
|
96
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
97
|
+
"""
|
|
98
|
+
config = load_config()
|
|
99
|
+
result, changed = _format_python_source(source, config, "mysql", logger)
|
|
100
|
+
assert not changed
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_format_empty_source(logger):
|
|
104
|
+
"""Empty source returns unchanged."""
|
|
105
|
+
config = load_config()
|
|
106
|
+
result, changed = _format_python_source("", config, "mysql", logger)
|
|
107
|
+
assert not changed
|
|
108
|
+
assert result == ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_format_syntax_error(logger):
|
|
112
|
+
"""Invalid Python source returns unchanged."""
|
|
113
|
+
source = "def foo(:\n"
|
|
114
|
+
config = load_config()
|
|
115
|
+
result, changed = _format_python_source(source, config, "mysql", logger)
|
|
116
|
+
assert not changed
|
|
117
|
+
assert result == source
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_format_plain_python_with_sql(logger):
|
|
121
|
+
"""Non-Marimo Python file with cursor.execute() is also formatted."""
|
|
122
|
+
source = """import sqlite3
|
|
123
|
+
conn = sqlite3.connect("db.sqlite")
|
|
124
|
+
cursor = conn.cursor()
|
|
125
|
+
cursor.execute("select id, name from users where active = 1")
|
|
126
|
+
"""
|
|
127
|
+
config = load_config()
|
|
128
|
+
result, changed = _format_python_source(source, config, "mysql", logger)
|
|
129
|
+
assert changed
|
|
130
|
+
assert "SELECT" in result
|
|
131
|
+
assert "FROM users" in result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# process_python_file / diff_python_file tests
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_process_python_file(tmp_path, logger):
|
|
140
|
+
"""process_python_file formats and writes back."""
|
|
141
|
+
source = """import marimo as mo
|
|
142
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
143
|
+
"""
|
|
144
|
+
py_path = tmp_path / "notebook.py"
|
|
145
|
+
py_path.write_text(source)
|
|
146
|
+
|
|
147
|
+
config = load_config()
|
|
148
|
+
changed = process_python_file(py_path, config, "mysql", logger)
|
|
149
|
+
assert changed
|
|
150
|
+
|
|
151
|
+
result = py_path.read_text()
|
|
152
|
+
assert "SELECT" in result
|
|
153
|
+
assert "FROM users" in result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_process_python_file_check_only(tmp_path, logger):
|
|
157
|
+
"""check_only=True detects changes without writing."""
|
|
158
|
+
source = """import marimo as mo
|
|
159
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
160
|
+
"""
|
|
161
|
+
py_path = tmp_path / "notebook.py"
|
|
162
|
+
py_path.write_text(source)
|
|
163
|
+
|
|
164
|
+
config = load_config()
|
|
165
|
+
changed = process_python_file(py_path, config, "mysql", logger, check_only=True)
|
|
166
|
+
assert changed
|
|
167
|
+
# File unchanged
|
|
168
|
+
assert py_path.read_text() == source
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_diff_python_file(tmp_path, logger):
|
|
172
|
+
"""diff_python_file returns unified diff."""
|
|
173
|
+
source = """import marimo as mo
|
|
174
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
175
|
+
"""
|
|
176
|
+
py_path = tmp_path / "notebook.py"
|
|
177
|
+
py_path.write_text(source)
|
|
178
|
+
|
|
179
|
+
config = load_config()
|
|
180
|
+
diff = diff_python_file(py_path, config, "mysql", logger)
|
|
181
|
+
assert diff
|
|
182
|
+
assert "---" in diff
|
|
183
|
+
assert "+++" in diff
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_diff_python_file_no_changes(tmp_path, logger):
|
|
187
|
+
"""diff_python_file returns empty string when no changes needed."""
|
|
188
|
+
source = """x = 1
|
|
189
|
+
y = "hello"
|
|
190
|
+
"""
|
|
191
|
+
py_path = tmp_path / "notebook.py"
|
|
192
|
+
py_path.write_text(source)
|
|
193
|
+
|
|
194
|
+
config = load_config()
|
|
195
|
+
diff = diff_python_file(py_path, config, "mysql", logger)
|
|
196
|
+
assert diff == ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# CLI integration tests
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_cli_marimo_format(tmp_path):
|
|
205
|
+
"""CLI formats a .py file."""
|
|
206
|
+
source = """import marimo as mo
|
|
207
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
208
|
+
"""
|
|
209
|
+
py_path = tmp_path / "notebook.py"
|
|
210
|
+
py_path.write_text(source)
|
|
211
|
+
|
|
212
|
+
result = subprocess.run(
|
|
213
|
+
["sqlnbfmt", str(py_path)],
|
|
214
|
+
capture_output=True,
|
|
215
|
+
text=True,
|
|
216
|
+
)
|
|
217
|
+
assert result.returncode == 0
|
|
218
|
+
formatted = py_path.read_text()
|
|
219
|
+
assert "SELECT" in formatted
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_cli_marimo_check(tmp_path):
|
|
223
|
+
"""CLI --check on unformatted .py exits 1."""
|
|
224
|
+
source = """import marimo as mo
|
|
225
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
226
|
+
"""
|
|
227
|
+
py_path = tmp_path / "notebook.py"
|
|
228
|
+
py_path.write_text(source)
|
|
229
|
+
|
|
230
|
+
result = subprocess.run(
|
|
231
|
+
["sqlnbfmt", "--check", str(py_path)],
|
|
232
|
+
capture_output=True,
|
|
233
|
+
text=True,
|
|
234
|
+
)
|
|
235
|
+
assert result.returncode == 1
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_cli_marimo_diff(tmp_path):
|
|
239
|
+
"""CLI --diff on .py prints diff and exits 0."""
|
|
240
|
+
source = """import marimo as mo
|
|
241
|
+
_df = mo.sql(f"select id, name from users where active = 1")
|
|
242
|
+
"""
|
|
243
|
+
py_path = tmp_path / "notebook.py"
|
|
244
|
+
py_path.write_text(source)
|
|
245
|
+
|
|
246
|
+
result = subprocess.run(
|
|
247
|
+
["sqlnbfmt", "--diff", str(py_path)],
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True,
|
|
250
|
+
)
|
|
251
|
+
assert result.returncode == 0
|
|
252
|
+
assert "---" in result.stdout
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_cli_mixed_files(tmp_path):
|
|
256
|
+
"""CLI handles .ipynb and .py files in the same invocation."""
|
|
257
|
+
import nbformat
|
|
258
|
+
from nbformat import v4 as nbf
|
|
259
|
+
|
|
260
|
+
# Create .ipynb
|
|
261
|
+
nb = nbf.new_notebook()
|
|
262
|
+
nb.cells = [nbf.new_code_cell('query = "select id from users where active = 1"')]
|
|
263
|
+
nb_path = tmp_path / "notebook.ipynb"
|
|
264
|
+
nbformat.write(nb, nb_path)
|
|
265
|
+
|
|
266
|
+
# Create .py
|
|
267
|
+
py_source = """import marimo as mo
|
|
268
|
+
_df = mo.sql(f"select id from users where active = 1")
|
|
269
|
+
"""
|
|
270
|
+
py_path = tmp_path / "notebook.py"
|
|
271
|
+
py_path.write_text(py_source)
|
|
272
|
+
|
|
273
|
+
result = subprocess.run(
|
|
274
|
+
["sqlnbfmt", str(nb_path), str(py_path)],
|
|
275
|
+
capture_output=True,
|
|
276
|
+
text=True,
|
|
277
|
+
)
|
|
278
|
+
assert result.returncode == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|