sqlnbfmt 0.2.0__tar.gz → 0.3.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.2.0/sqlnbfmt.egg-info → sqlnbfmt-0.3.0}/PKG-INFO +71 -17
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/README.md +65 -16
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/pyproject.toml +11 -4
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt/formatter.py +245 -23
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0/sqlnbfmt.egg-info}/PKG-INFO +71 -17
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/tests/test_eval.py +1 -2
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/tests/test_formatter.py +101 -2
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/LICENSE +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/setup.cfg +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt/__init__.py +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt.egg-info/SOURCES.txt +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt.egg-info/dependency_links.txt +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt.egg-info/entry_points.txt +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt.egg-info/requires.txt +0 -0
- {sqlnbfmt-0.2.0 → sqlnbfmt-0.3.0}/sqlnbfmt.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlnbfmt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A tool to format SQL code in Jupyter notebooks.
|
|
5
5
|
Author-email: Qing <qingye779@gmail.com>
|
|
6
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
|
|
7
12
|
Classifier: Programming Language :: Python :: 3
|
|
8
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
9
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -21,16 +26,20 @@ Dynamic: license-file
|
|
|
21
26
|
[](https://github.com/flyersworder/sqlnbfmt/blob/main/LICENSE)
|
|
22
27
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
23
28
|
|
|
29
|
+
> **Quick start:** `pip install sqlnbfmt && sqlnbfmt notebook.ipynb`
|
|
30
|
+
|
|
24
31
|
A SQL formatter designed specifically for Jupyter Notebooks. `sqlnbfmt` automatically formats SQL queries embedded in code cells, including both Python strings and SQL magic cells (`%%sql`), helping you maintain clean and consistent code.
|
|
25
32
|
|
|
26
33
|
## Features
|
|
27
34
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
35
|
+
- **Zero-config**: Works out of the box with sensible defaults — no config file needed
|
|
36
|
+
- **Smart SQL Detection**: Automatically identifies and formats SQL queries in code cells and magic SQL cells
|
|
37
|
+
- **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
|
|
39
|
+
- **CI-Friendly**: `--check` mode exits non-zero when formatting is needed; `--diff` shows what would change
|
|
40
|
+
- **Skip Hints**: Add `# sqlnbfmt: skip` to any cell to exclude it from formatting
|
|
41
|
+
- **Pre-commit Ready**: Seamlessly integrates with pre-commit hooks
|
|
42
|
+
- **Lightweight**: Only three runtime dependencies (sqlglot, nbformat, pyyaml)
|
|
34
43
|
|
|
35
44
|
## Installation
|
|
36
45
|
|
|
@@ -47,9 +56,23 @@ Format a single notebook:
|
|
|
47
56
|
sqlnbfmt path/to/your_notebook.ipynb
|
|
48
57
|
```
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
Check formatting without modifying files (useful in CI):
|
|
60
|
+
```bash
|
|
61
|
+
sqlnbfmt --check path/to/your_notebook.ipynb
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Show a diff of what would change:
|
|
51
65
|
```bash
|
|
52
|
-
sqlnbfmt path/to/
|
|
66
|
+
sqlnbfmt --diff path/to/your_notebook.ipynb
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Skipping Cells
|
|
70
|
+
|
|
71
|
+
Add a `# sqlnbfmt: skip` comment anywhere in a cell to skip formatting:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# sqlnbfmt: skip
|
|
75
|
+
query = "select * from my_special_table where id = 1"
|
|
53
76
|
```
|
|
54
77
|
|
|
55
78
|
### Pre-commit Integration
|
|
@@ -63,17 +86,15 @@ pip install pre-commit
|
|
|
63
86
|
```yaml
|
|
64
87
|
repos:
|
|
65
88
|
- repo: https://github.com/flyersworder/sqlnbfmt
|
|
66
|
-
rev: v0.
|
|
89
|
+
rev: v0.3.0
|
|
67
90
|
hooks:
|
|
68
91
|
- id: sqlnbfmt
|
|
69
|
-
name: sqlnbfmt
|
|
70
92
|
types: [jupyter]
|
|
71
|
-
args: [--config, config.yaml, --dialect, postgres]
|
|
72
93
|
```
|
|
73
|
-
Please run the following command for help:
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
All arguments are optional. To specify a dialect or custom config:
|
|
96
|
+
```yaml
|
|
97
|
+
args: [--dialect, postgres, --config, config.yaml]
|
|
77
98
|
```
|
|
78
99
|
|
|
79
100
|
3. Install the hook:
|
|
@@ -86,8 +107,21 @@ pre-commit install
|
|
|
86
107
|
pre-commit run --all-files
|
|
87
108
|
```
|
|
88
109
|
|
|
110
|
+
### CI Usage
|
|
111
|
+
|
|
112
|
+
Use `--check` in GitHub Actions to enforce formatting:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
- name: Check SQL notebook formatting
|
|
116
|
+
run: |
|
|
117
|
+
pip install sqlnbfmt
|
|
118
|
+
sqlnbfmt --check **/*.ipynb
|
|
119
|
+
```
|
|
120
|
+
|
|
89
121
|
## Configuration
|
|
90
122
|
|
|
123
|
+
`sqlnbfmt` works without any configuration file. A `config.yaml` is only needed to override defaults.
|
|
124
|
+
|
|
91
125
|
Create a `config.yaml` file to customize formatting behavior. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template.
|
|
92
126
|
|
|
93
127
|
### Configuration Options
|
|
@@ -95,8 +129,8 @@ Create a `config.yaml` file to customize formatting behavior. [Here](https://git
|
|
|
95
129
|
| Option | Description | Default |
|
|
96
130
|
|--------|-------------|---------|
|
|
97
131
|
| `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
|
|
98
|
-
| `function_names` | Python functions containing SQL code | `
|
|
99
|
-
| `sql_decorators` | Decorators indicating SQL code | `
|
|
132
|
+
| `function_names` | Python functions containing SQL code | `read_sql`, `execute`, etc. |
|
|
133
|
+
| `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
|
|
100
134
|
| `single_line_threshold` | Maximum length before splitting SQL | 80 |
|
|
101
135
|
| `indent_width` | Number of spaces for indentation | 4 |
|
|
102
136
|
|
|
@@ -125,6 +159,19 @@ ORDER BY
|
|
|
125
159
|
""")
|
|
126
160
|
```
|
|
127
161
|
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
**SQL not being formatted?**
|
|
165
|
+
- Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
|
|
166
|
+
- Check that the function name is in the recognized list (use `--config` to add custom ones)
|
|
167
|
+
|
|
168
|
+
**Comments being modified?**
|
|
169
|
+
- Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
|
|
170
|
+
|
|
171
|
+
**Pre-commit hook fails?**
|
|
172
|
+
- Make sure the `rev` matches the installed version
|
|
173
|
+
- Run `pre-commit autoupdate` to get the latest version
|
|
174
|
+
|
|
128
175
|
## Contributing
|
|
129
176
|
|
|
130
177
|
We welcome contributions! Here's how to get started:
|
|
@@ -146,6 +193,13 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
|
146
193
|
pytest
|
|
147
194
|
```
|
|
148
195
|
|
|
196
|
+
4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.
|
|
197
|
+
|
|
198
|
+
5. Install dev pre-commit hooks:
|
|
199
|
+
```bash
|
|
200
|
+
pre-commit install
|
|
201
|
+
```
|
|
202
|
+
|
|
149
203
|
## License
|
|
150
204
|
|
|
151
205
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
[](https://github.com/flyersworder/sqlnbfmt/blob/main/LICENSE)
|
|
5
5
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
6
6
|
|
|
7
|
+
> **Quick start:** `pip install sqlnbfmt && sqlnbfmt notebook.ipynb`
|
|
8
|
+
|
|
7
9
|
A SQL formatter designed specifically for Jupyter Notebooks. `sqlnbfmt` automatically formats SQL queries embedded in code cells, including both Python strings and SQL magic cells (`%%sql`), helping you maintain clean and consistent code.
|
|
8
10
|
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
- **Zero-config**: Works out of the box with sensible defaults — no config file needed
|
|
14
|
+
- **Smart SQL Detection**: Automatically identifies and formats SQL queries in code cells and magic SQL cells
|
|
15
|
+
- **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
|
|
17
|
+
- **CI-Friendly**: `--check` mode exits non-zero when formatting is needed; `--diff` shows what would change
|
|
18
|
+
- **Skip Hints**: Add `# sqlnbfmt: skip` to any cell to exclude it from formatting
|
|
19
|
+
- **Pre-commit Ready**: Seamlessly integrates with pre-commit hooks
|
|
20
|
+
- **Lightweight**: Only three runtime dependencies (sqlglot, nbformat, pyyaml)
|
|
17
21
|
|
|
18
22
|
## Installation
|
|
19
23
|
|
|
@@ -30,9 +34,23 @@ Format a single notebook:
|
|
|
30
34
|
sqlnbfmt path/to/your_notebook.ipynb
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
Check formatting without modifying files (useful in CI):
|
|
38
|
+
```bash
|
|
39
|
+
sqlnbfmt --check path/to/your_notebook.ipynb
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Show a diff of what would change:
|
|
34
43
|
```bash
|
|
35
|
-
sqlnbfmt path/to/
|
|
44
|
+
sqlnbfmt --diff path/to/your_notebook.ipynb
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Skipping Cells
|
|
48
|
+
|
|
49
|
+
Add a `# sqlnbfmt: skip` comment anywhere in a cell to skip formatting:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# sqlnbfmt: skip
|
|
53
|
+
query = "select * from my_special_table where id = 1"
|
|
36
54
|
```
|
|
37
55
|
|
|
38
56
|
### Pre-commit Integration
|
|
@@ -46,17 +64,15 @@ pip install pre-commit
|
|
|
46
64
|
```yaml
|
|
47
65
|
repos:
|
|
48
66
|
- repo: https://github.com/flyersworder/sqlnbfmt
|
|
49
|
-
rev: v0.
|
|
67
|
+
rev: v0.3.0
|
|
50
68
|
hooks:
|
|
51
69
|
- id: sqlnbfmt
|
|
52
|
-
name: sqlnbfmt
|
|
53
70
|
types: [jupyter]
|
|
54
|
-
args: [--config, config.yaml, --dialect, postgres]
|
|
55
71
|
```
|
|
56
|
-
Please run the following command for help:
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
All arguments are optional. To specify a dialect or custom config:
|
|
74
|
+
```yaml
|
|
75
|
+
args: [--dialect, postgres, --config, config.yaml]
|
|
60
76
|
```
|
|
61
77
|
|
|
62
78
|
3. Install the hook:
|
|
@@ -69,8 +85,21 @@ pre-commit install
|
|
|
69
85
|
pre-commit run --all-files
|
|
70
86
|
```
|
|
71
87
|
|
|
88
|
+
### CI Usage
|
|
89
|
+
|
|
90
|
+
Use `--check` in GitHub Actions to enforce formatting:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
- name: Check SQL notebook formatting
|
|
94
|
+
run: |
|
|
95
|
+
pip install sqlnbfmt
|
|
96
|
+
sqlnbfmt --check **/*.ipynb
|
|
97
|
+
```
|
|
98
|
+
|
|
72
99
|
## Configuration
|
|
73
100
|
|
|
101
|
+
`sqlnbfmt` works without any configuration file. A `config.yaml` is only needed to override defaults.
|
|
102
|
+
|
|
74
103
|
Create a `config.yaml` file to customize formatting behavior. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template.
|
|
75
104
|
|
|
76
105
|
### Configuration Options
|
|
@@ -78,8 +107,8 @@ Create a `config.yaml` file to customize formatting behavior. [Here](https://git
|
|
|
78
107
|
| Option | Description | Default |
|
|
79
108
|
|--------|-------------|---------|
|
|
80
109
|
| `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
|
|
81
|
-
| `function_names` | Python functions containing SQL code | `
|
|
82
|
-
| `sql_decorators` | Decorators indicating SQL code | `
|
|
110
|
+
| `function_names` | Python functions containing SQL code | `read_sql`, `execute`, etc. |
|
|
111
|
+
| `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
|
|
83
112
|
| `single_line_threshold` | Maximum length before splitting SQL | 80 |
|
|
84
113
|
| `indent_width` | Number of spaces for indentation | 4 |
|
|
85
114
|
|
|
@@ -108,6 +137,19 @@ ORDER BY
|
|
|
108
137
|
""")
|
|
109
138
|
```
|
|
110
139
|
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
**SQL not being formatted?**
|
|
143
|
+
- Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
|
|
144
|
+
- Check that the function name is in the recognized list (use `--config` to add custom ones)
|
|
145
|
+
|
|
146
|
+
**Comments being modified?**
|
|
147
|
+
- Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
|
|
148
|
+
|
|
149
|
+
**Pre-commit hook fails?**
|
|
150
|
+
- Make sure the `rev` matches the installed version
|
|
151
|
+
- Run `pre-commit autoupdate` to get the latest version
|
|
152
|
+
|
|
111
153
|
## Contributing
|
|
112
154
|
|
|
113
155
|
We welcome contributions! Here's how to get started:
|
|
@@ -129,6 +171,13 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
|
129
171
|
pytest
|
|
130
172
|
```
|
|
131
173
|
|
|
174
|
+
4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.
|
|
175
|
+
|
|
176
|
+
5. Install dev pre-commit hooks:
|
|
177
|
+
```bash
|
|
178
|
+
pre-commit install
|
|
179
|
+
```
|
|
180
|
+
|
|
132
181
|
## License
|
|
133
182
|
|
|
134
183
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlnbfmt"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A tool to format SQL code in Jupyter notebooks."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Qing", email = "qingye779@gmail.com" }
|
|
@@ -8,6 +8,7 @@ authors = [
|
|
|
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
12
|
classifiers = [
|
|
12
13
|
"Programming Language :: Python :: 3",
|
|
13
14
|
"Programming Language :: Python :: 3.12",
|
|
@@ -19,6 +20,15 @@ dependencies = [
|
|
|
19
20
|
"sqlglot>=25.27.0",
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/flyersworder/sqlnbfmt"
|
|
25
|
+
Repository = "https://github.com/flyersworder/sqlnbfmt"
|
|
26
|
+
Changelog = "https://github.com/flyersworder/sqlnbfmt/blob/main/CHANGELOG.md"
|
|
27
|
+
Issues = "https://github.com/flyersworder/sqlnbfmt/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
sqlnbfmt = "sqlnbfmt.formatter:main"
|
|
31
|
+
|
|
22
32
|
[dependency-groups]
|
|
23
33
|
dev = [
|
|
24
34
|
"pytest-cov>=5.0.0",
|
|
@@ -30,9 +40,6 @@ dev = [
|
|
|
30
40
|
requires = ["setuptools>=61.0", "wheel"]
|
|
31
41
|
build-backend = "setuptools.build_meta"
|
|
32
42
|
|
|
33
|
-
[project.scripts]
|
|
34
|
-
sqlnbfmt = "sqlnbfmt.formatter:main"
|
|
35
|
-
|
|
36
43
|
[tool.pytest.ini_options]
|
|
37
44
|
markers = [
|
|
38
45
|
"integration: mark a test as an integration test."
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import ast
|
|
2
|
+
import copy
|
|
3
|
+
import difflib
|
|
2
4
|
import logging
|
|
3
5
|
import re
|
|
4
6
|
import sys
|
|
5
|
-
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import dataclass, field
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Set, Optional, Dict, Union, Tuple
|
|
8
10
|
|
|
@@ -10,6 +12,55 @@ import nbformat
|
|
|
10
12
|
import yaml
|
|
11
13
|
from sqlglot import parse_one, errors
|
|
12
14
|
|
|
15
|
+
DEFAULT_SQL_KEYWORDS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"SELECT",
|
|
18
|
+
"FROM",
|
|
19
|
+
"WHERE",
|
|
20
|
+
"INSERT",
|
|
21
|
+
"UPDATE",
|
|
22
|
+
"DELETE",
|
|
23
|
+
"WITH",
|
|
24
|
+
"JOIN",
|
|
25
|
+
"CREATE",
|
|
26
|
+
"DROP",
|
|
27
|
+
"ALTER",
|
|
28
|
+
"TRUNCATE",
|
|
29
|
+
"UNION",
|
|
30
|
+
"EXCEPT",
|
|
31
|
+
"INTERSECT",
|
|
32
|
+
"GROUP BY",
|
|
33
|
+
"ORDER BY",
|
|
34
|
+
"HAVING",
|
|
35
|
+
"LIMIT",
|
|
36
|
+
"OFFSET",
|
|
37
|
+
"DISTINCT",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
DEFAULT_FUNCTION_NAMES = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"read_sql",
|
|
44
|
+
"read_sql_query",
|
|
45
|
+
"read_sql_table",
|
|
46
|
+
"execute",
|
|
47
|
+
"executescript",
|
|
48
|
+
"fetchall",
|
|
49
|
+
"fetchone",
|
|
50
|
+
"fetchmany",
|
|
51
|
+
"execute_query",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
DEFAULT_SQL_DECORATORS = frozenset(
|
|
56
|
+
{
|
|
57
|
+
"sql_decorator",
|
|
58
|
+
"query",
|
|
59
|
+
"sql_query",
|
|
60
|
+
"db_query",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
13
64
|
|
|
14
65
|
@dataclass
|
|
15
66
|
class SQLReplacement:
|
|
@@ -24,9 +75,13 @@ class SQLReplacement:
|
|
|
24
75
|
class FormattingConfig:
|
|
25
76
|
"""Configuration for SQL formatting."""
|
|
26
77
|
|
|
27
|
-
sql_keywords: Set[str]
|
|
28
|
-
function_names: Set[str]
|
|
29
|
-
|
|
78
|
+
sql_keywords: Set[str] = field(default_factory=lambda: set(DEFAULT_SQL_KEYWORDS))
|
|
79
|
+
function_names: Set[str] = field(
|
|
80
|
+
default_factory=lambda: set(DEFAULT_FUNCTION_NAMES)
|
|
81
|
+
)
|
|
82
|
+
sql_decorators: Set[str] = field(
|
|
83
|
+
default_factory=lambda: set(DEFAULT_SQL_DECORATORS)
|
|
84
|
+
)
|
|
30
85
|
single_line_threshold: int = 80
|
|
31
86
|
indent_width: int = 4
|
|
32
87
|
|
|
@@ -49,8 +104,11 @@ def setup_logging(level: str = "INFO") -> logging.Logger:
|
|
|
49
104
|
return logger
|
|
50
105
|
|
|
51
106
|
|
|
52
|
-
def load_config(config_path: Union[str, Path] =
|
|
53
|
-
"""Loads configuration from a YAML file."""
|
|
107
|
+
def load_config(config_path: Optional[Union[str, Path]] = None) -> FormattingConfig:
|
|
108
|
+
"""Loads configuration from a YAML file, or returns defaults if no path given."""
|
|
109
|
+
if config_path is None:
|
|
110
|
+
return FormattingConfig()
|
|
111
|
+
|
|
54
112
|
config_file = Path(config_path)
|
|
55
113
|
if not config_file.is_file():
|
|
56
114
|
raise FileNotFoundError(f"Configuration file '{config_file}' not found.")
|
|
@@ -374,13 +432,21 @@ def process_notebook(
|
|
|
374
432
|
config: FormattingConfig,
|
|
375
433
|
dialect: Optional[str],
|
|
376
434
|
logger: logging.Logger,
|
|
435
|
+
check_only: bool = False,
|
|
377
436
|
) -> bool:
|
|
378
|
-
"""Processes a Jupyter notebook.
|
|
437
|
+
"""Processes a Jupyter notebook.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
check_only: If True, detect changes but don't write the file.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
True if changes were made (or would be made in check mode).
|
|
444
|
+
"""
|
|
379
445
|
try:
|
|
380
446
|
notebook = nbformat.read(notebook_path, as_version=4)
|
|
381
447
|
changed = False
|
|
382
448
|
|
|
383
|
-
for cell in notebook.cells:
|
|
449
|
+
for cell_index, cell in enumerate(notebook.cells):
|
|
384
450
|
if cell.cell_type != "code":
|
|
385
451
|
continue
|
|
386
452
|
|
|
@@ -388,6 +454,10 @@ def process_notebook(
|
|
|
388
454
|
if not original_code.strip():
|
|
389
455
|
continue
|
|
390
456
|
|
|
457
|
+
if "# sqlnbfmt: skip" in original_code:
|
|
458
|
+
logger.debug(f"[Cell {cell_index}] Skipping cell (sqlnbfmt: skip)")
|
|
459
|
+
continue
|
|
460
|
+
|
|
391
461
|
lines = original_code.split("\n")
|
|
392
462
|
|
|
393
463
|
# Initialize variables to track magic commands
|
|
@@ -452,7 +522,9 @@ def process_notebook(
|
|
|
452
522
|
changed = True
|
|
453
523
|
|
|
454
524
|
except SQLFormattingError as e:
|
|
455
|
-
logger.warning(
|
|
525
|
+
logger.warning(
|
|
526
|
+
f"[Cell {cell_index}] SQL magic formatting skipped: {e}"
|
|
527
|
+
)
|
|
456
528
|
|
|
457
529
|
continue # Move to the next cell after handling magic command
|
|
458
530
|
|
|
@@ -478,10 +550,12 @@ def process_notebook(
|
|
|
478
550
|
cell.source = result
|
|
479
551
|
changed = True
|
|
480
552
|
except SyntaxError:
|
|
481
|
-
logger.warning(
|
|
553
|
+
logger.warning(
|
|
554
|
+
f"[Cell {cell_index}] Failed to parse cell:\n{original_code}"
|
|
555
|
+
)
|
|
482
556
|
continue
|
|
483
557
|
|
|
484
|
-
if changed:
|
|
558
|
+
if changed and not check_only:
|
|
485
559
|
nbformat.write(notebook, notebook_path)
|
|
486
560
|
logger.info(f"Updated notebook: {notebook_path}")
|
|
487
561
|
|
|
@@ -492,6 +566,112 @@ def process_notebook(
|
|
|
492
566
|
raise
|
|
493
567
|
|
|
494
568
|
|
|
569
|
+
def diff_notebook(
|
|
570
|
+
notebook_path: Union[str, Path],
|
|
571
|
+
config: FormattingConfig,
|
|
572
|
+
dialect: Optional[str],
|
|
573
|
+
logger: logging.Logger,
|
|
574
|
+
) -> str:
|
|
575
|
+
"""Returns a unified diff of formatting changes for a notebook."""
|
|
576
|
+
notebook_path = Path(notebook_path)
|
|
577
|
+
original_nb = nbformat.read(notebook_path, as_version=4)
|
|
578
|
+
formatted_nb = copy.deepcopy(original_nb)
|
|
579
|
+
|
|
580
|
+
# Format the copy in-memory by running the same cell logic
|
|
581
|
+
for cell_index, cell in enumerate(formatted_nb.cells):
|
|
582
|
+
if cell.cell_type != "code":
|
|
583
|
+
continue
|
|
584
|
+
original_code = cell.source
|
|
585
|
+
if not original_code.strip():
|
|
586
|
+
continue
|
|
587
|
+
if "# sqlnbfmt: skip" in original_code:
|
|
588
|
+
continue
|
|
589
|
+
|
|
590
|
+
lines = original_code.split("\n")
|
|
591
|
+
magic_cmd = None
|
|
592
|
+
magic_cmd_index = None
|
|
593
|
+
for idx, line in enumerate(lines):
|
|
594
|
+
stripped = line.strip()
|
|
595
|
+
if not stripped:
|
|
596
|
+
continue
|
|
597
|
+
if stripped.startswith("#"):
|
|
598
|
+
continue
|
|
599
|
+
if stripped.startswith("%%sql") or stripped.startswith("%sql"):
|
|
600
|
+
magic_cmd = stripped.split()[0]
|
|
601
|
+
magic_cmd_index = idx
|
|
602
|
+
break
|
|
603
|
+
else:
|
|
604
|
+
break
|
|
605
|
+
|
|
606
|
+
if magic_cmd:
|
|
607
|
+
is_cell_magic = magic_cmd.startswith("%%sql")
|
|
608
|
+
if is_cell_magic:
|
|
609
|
+
sql_code = "\n".join(lines[magic_cmd_index + 1 :]).strip()
|
|
610
|
+
else:
|
|
611
|
+
sql_code = lines[magic_cmd_index][len(magic_cmd) :].strip()
|
|
612
|
+
try:
|
|
613
|
+
formatted_sql = format_sql_code(
|
|
614
|
+
sql_code,
|
|
615
|
+
dialect,
|
|
616
|
+
config,
|
|
617
|
+
is_magic_command=True,
|
|
618
|
+
is_cell_magic=is_cell_magic,
|
|
619
|
+
)
|
|
620
|
+
if is_cell_magic:
|
|
621
|
+
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
622
|
+
if pre_magic:
|
|
623
|
+
new_content = f"{pre_magic}\n{magic_cmd}\n{formatted_sql}"
|
|
624
|
+
else:
|
|
625
|
+
new_content = f"{magic_cmd}\n{formatted_sql}"
|
|
626
|
+
else:
|
|
627
|
+
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
628
|
+
if pre_magic:
|
|
629
|
+
new_content = f"{pre_magic}\n{magic_cmd} {formatted_sql}"
|
|
630
|
+
else:
|
|
631
|
+
new_content = f"{magic_cmd} {formatted_sql}"
|
|
632
|
+
cell.source = new_content
|
|
633
|
+
except SQLFormattingError:
|
|
634
|
+
pass
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
tree = ast.parse(original_code)
|
|
639
|
+
line_offsets = build_line_offsets(original_code)
|
|
640
|
+
formatter = SQLStringFormatter(config, dialect, logger, line_offsets)
|
|
641
|
+
formatter.visit(tree)
|
|
642
|
+
if formatter.replacements:
|
|
643
|
+
result = original_code
|
|
644
|
+
for rep in sorted(
|
|
645
|
+
formatter.replacements,
|
|
646
|
+
key=lambda r: r.start_offset,
|
|
647
|
+
reverse=True,
|
|
648
|
+
):
|
|
649
|
+
result = (
|
|
650
|
+
result[: rep.start_offset]
|
|
651
|
+
+ rep.new_text
|
|
652
|
+
+ result[rep.end_offset :]
|
|
653
|
+
)
|
|
654
|
+
cell.source = result
|
|
655
|
+
except SyntaxError:
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
# Build unified diff cell-by-cell
|
|
659
|
+
diff_lines = []
|
|
660
|
+
for i, (orig_cell, fmt_cell) in enumerate(
|
|
661
|
+
zip(original_nb.cells, formatted_nb.cells)
|
|
662
|
+
):
|
|
663
|
+
if orig_cell.source != fmt_cell.source:
|
|
664
|
+
diff_lines.extend(
|
|
665
|
+
difflib.unified_diff(
|
|
666
|
+
orig_cell.source.splitlines(keepends=True),
|
|
667
|
+
fmt_cell.source.splitlines(keepends=True),
|
|
668
|
+
fromfile=f"Cell [{i}] original",
|
|
669
|
+
tofile=f"Cell [{i}] formatted",
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
return "".join(diff_lines)
|
|
673
|
+
|
|
674
|
+
|
|
495
675
|
def main():
|
|
496
676
|
"""Main entry point for the SQL formatter."""
|
|
497
677
|
import argparse
|
|
@@ -501,7 +681,7 @@ def main():
|
|
|
501
681
|
"notebooks", nargs="+", type=Path, help="Notebook paths to process"
|
|
502
682
|
)
|
|
503
683
|
parser.add_argument(
|
|
504
|
-
"--config", type=Path, default=
|
|
684
|
+
"--config", type=Path, default=None, help="Configuration file path (optional)"
|
|
505
685
|
)
|
|
506
686
|
parser.add_argument(
|
|
507
687
|
"--dialect", type=str, help="SQL dialect (e.g., mysql, postgres)"
|
|
@@ -512,27 +692,69 @@ def main():
|
|
|
512
692
|
default="INFO",
|
|
513
693
|
help="Logging level",
|
|
514
694
|
)
|
|
695
|
+
parser.add_argument(
|
|
696
|
+
"--check",
|
|
697
|
+
action="store_true",
|
|
698
|
+
help="Check formatting without modifying files (exit code 1 if changes needed)",
|
|
699
|
+
)
|
|
700
|
+
parser.add_argument(
|
|
701
|
+
"--diff",
|
|
702
|
+
action="store_true",
|
|
703
|
+
help="Print a unified diff of formatting changes",
|
|
704
|
+
)
|
|
515
705
|
|
|
516
706
|
args = parser.parse_args()
|
|
517
707
|
logger = setup_logging(args.log_level)
|
|
518
708
|
|
|
519
709
|
try:
|
|
520
710
|
config = load_config(args.config)
|
|
711
|
+
except FileNotFoundError as e:
|
|
712
|
+
logger.error(
|
|
713
|
+
f"{e}\nHint: --config is optional; omit it to use built-in defaults."
|
|
714
|
+
)
|
|
715
|
+
sys.exit(1)
|
|
716
|
+
|
|
717
|
+
try:
|
|
521
718
|
changed = False
|
|
522
719
|
changed_files = []
|
|
523
720
|
|
|
524
721
|
for notebook in args.notebooks:
|
|
525
|
-
if
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
722
|
+
if args.diff:
|
|
723
|
+
diff_output = diff_notebook(notebook, config, args.dialect, logger)
|
|
724
|
+
if diff_output:
|
|
725
|
+
print(diff_output, end="")
|
|
726
|
+
changed = True
|
|
727
|
+
changed_files.append(notebook)
|
|
728
|
+
|
|
729
|
+
if args.check:
|
|
730
|
+
if process_notebook(
|
|
731
|
+
notebook, config, args.dialect, logger, check_only=True
|
|
732
|
+
):
|
|
733
|
+
changed = True
|
|
734
|
+
changed_files.append(notebook)
|
|
735
|
+
elif not args.diff:
|
|
736
|
+
if process_notebook(notebook, config, args.dialect, logger):
|
|
737
|
+
changed = True
|
|
738
|
+
changed_files.append(notebook)
|
|
739
|
+
|
|
740
|
+
if args.check:
|
|
741
|
+
if changed:
|
|
742
|
+
logger.error(f"{len(changed_files)} notebook(s) would be reformatted:")
|
|
743
|
+
for f in changed_files:
|
|
744
|
+
logger.error(f" - {f}")
|
|
745
|
+
sys.exit(1)
|
|
746
|
+
else:
|
|
747
|
+
logger.info("All notebooks are properly formatted.")
|
|
748
|
+
sys.exit(0)
|
|
749
|
+
|
|
750
|
+
# Print summary for normal mode
|
|
751
|
+
if not args.diff:
|
|
752
|
+
if changed:
|
|
753
|
+
logger.info("Changes made to the following notebooks:")
|
|
754
|
+
for file in changed_files:
|
|
755
|
+
logger.info(f" - {file}")
|
|
756
|
+
else:
|
|
757
|
+
logger.info("No changes needed. All notebooks are properly formatted.")
|
|
536
758
|
|
|
537
759
|
sys.exit(0)
|
|
538
760
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlnbfmt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A tool to format SQL code in Jupyter notebooks.
|
|
5
5
|
Author-email: Qing <qingye779@gmail.com>
|
|
6
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
|
|
7
12
|
Classifier: Programming Language :: Python :: 3
|
|
8
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
9
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -21,16 +26,20 @@ Dynamic: license-file
|
|
|
21
26
|
[](https://github.com/flyersworder/sqlnbfmt/blob/main/LICENSE)
|
|
22
27
|
[](https://pypi.org/project/sqlnbfmt/)
|
|
23
28
|
|
|
29
|
+
> **Quick start:** `pip install sqlnbfmt && sqlnbfmt notebook.ipynb`
|
|
30
|
+
|
|
24
31
|
A SQL formatter designed specifically for Jupyter Notebooks. `sqlnbfmt` automatically formats SQL queries embedded in code cells, including both Python strings and SQL magic cells (`%%sql`), helping you maintain clean and consistent code.
|
|
25
32
|
|
|
26
33
|
## Features
|
|
27
34
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
35
|
+
- **Zero-config**: Works out of the box with sensible defaults — no config file needed
|
|
36
|
+
- **Smart SQL Detection**: Automatically identifies and formats SQL queries in code cells and magic SQL cells
|
|
37
|
+
- **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
|
|
39
|
+
- **CI-Friendly**: `--check` mode exits non-zero when formatting is needed; `--diff` shows what would change
|
|
40
|
+
- **Skip Hints**: Add `# sqlnbfmt: skip` to any cell to exclude it from formatting
|
|
41
|
+
- **Pre-commit Ready**: Seamlessly integrates with pre-commit hooks
|
|
42
|
+
- **Lightweight**: Only three runtime dependencies (sqlglot, nbformat, pyyaml)
|
|
34
43
|
|
|
35
44
|
## Installation
|
|
36
45
|
|
|
@@ -47,9 +56,23 @@ Format a single notebook:
|
|
|
47
56
|
sqlnbfmt path/to/your_notebook.ipynb
|
|
48
57
|
```
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
Check formatting without modifying files (useful in CI):
|
|
60
|
+
```bash
|
|
61
|
+
sqlnbfmt --check path/to/your_notebook.ipynb
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Show a diff of what would change:
|
|
51
65
|
```bash
|
|
52
|
-
sqlnbfmt path/to/
|
|
66
|
+
sqlnbfmt --diff path/to/your_notebook.ipynb
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Skipping Cells
|
|
70
|
+
|
|
71
|
+
Add a `# sqlnbfmt: skip` comment anywhere in a cell to skip formatting:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# sqlnbfmt: skip
|
|
75
|
+
query = "select * from my_special_table where id = 1"
|
|
53
76
|
```
|
|
54
77
|
|
|
55
78
|
### Pre-commit Integration
|
|
@@ -63,17 +86,15 @@ pip install pre-commit
|
|
|
63
86
|
```yaml
|
|
64
87
|
repos:
|
|
65
88
|
- repo: https://github.com/flyersworder/sqlnbfmt
|
|
66
|
-
rev: v0.
|
|
89
|
+
rev: v0.3.0
|
|
67
90
|
hooks:
|
|
68
91
|
- id: sqlnbfmt
|
|
69
|
-
name: sqlnbfmt
|
|
70
92
|
types: [jupyter]
|
|
71
|
-
args: [--config, config.yaml, --dialect, postgres]
|
|
72
93
|
```
|
|
73
|
-
Please run the following command for help:
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
All arguments are optional. To specify a dialect or custom config:
|
|
96
|
+
```yaml
|
|
97
|
+
args: [--dialect, postgres, --config, config.yaml]
|
|
77
98
|
```
|
|
78
99
|
|
|
79
100
|
3. Install the hook:
|
|
@@ -86,8 +107,21 @@ pre-commit install
|
|
|
86
107
|
pre-commit run --all-files
|
|
87
108
|
```
|
|
88
109
|
|
|
110
|
+
### CI Usage
|
|
111
|
+
|
|
112
|
+
Use `--check` in GitHub Actions to enforce formatting:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
- name: Check SQL notebook formatting
|
|
116
|
+
run: |
|
|
117
|
+
pip install sqlnbfmt
|
|
118
|
+
sqlnbfmt --check **/*.ipynb
|
|
119
|
+
```
|
|
120
|
+
|
|
89
121
|
## Configuration
|
|
90
122
|
|
|
123
|
+
`sqlnbfmt` works without any configuration file. A `config.yaml` is only needed to override defaults.
|
|
124
|
+
|
|
91
125
|
Create a `config.yaml` file to customize formatting behavior. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template.
|
|
92
126
|
|
|
93
127
|
### Configuration Options
|
|
@@ -95,8 +129,8 @@ Create a `config.yaml` file to customize formatting behavior. [Here](https://git
|
|
|
95
129
|
| Option | Description | Default |
|
|
96
130
|
|--------|-------------|---------|
|
|
97
131
|
| `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
|
|
98
|
-
| `function_names` | Python functions containing SQL code | `
|
|
99
|
-
| `sql_decorators` | Decorators indicating SQL code | `
|
|
132
|
+
| `function_names` | Python functions containing SQL code | `read_sql`, `execute`, etc. |
|
|
133
|
+
| `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
|
|
100
134
|
| `single_line_threshold` | Maximum length before splitting SQL | 80 |
|
|
101
135
|
| `indent_width` | Number of spaces for indentation | 4 |
|
|
102
136
|
|
|
@@ -125,6 +159,19 @@ ORDER BY
|
|
|
125
159
|
""")
|
|
126
160
|
```
|
|
127
161
|
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
**SQL not being formatted?**
|
|
165
|
+
- Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
|
|
166
|
+
- Check that the function name is in the recognized list (use `--config` to add custom ones)
|
|
167
|
+
|
|
168
|
+
**Comments being modified?**
|
|
169
|
+
- Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
|
|
170
|
+
|
|
171
|
+
**Pre-commit hook fails?**
|
|
172
|
+
- Make sure the `rev` matches the installed version
|
|
173
|
+
- Run `pre-commit autoupdate` to get the latest version
|
|
174
|
+
|
|
128
175
|
## Contributing
|
|
129
176
|
|
|
130
177
|
We welcome contributions! Here's how to get started:
|
|
@@ -146,6 +193,13 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
|
146
193
|
pytest
|
|
147
194
|
```
|
|
148
195
|
|
|
196
|
+
4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.
|
|
197
|
+
|
|
198
|
+
5. Install dev pre-commit hooks:
|
|
199
|
+
```bash
|
|
200
|
+
pre-commit install
|
|
201
|
+
```
|
|
202
|
+
|
|
149
203
|
## License
|
|
150
204
|
|
|
151
205
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -14,7 +14,6 @@ import pytest
|
|
|
14
14
|
from sqlnbfmt.formatter import load_config, process_notebook
|
|
15
15
|
|
|
16
16
|
EVAL_DIR = Path(__file__).resolve().parent / "eval"
|
|
17
|
-
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.yaml"
|
|
18
17
|
DIALECT = "mysql"
|
|
19
18
|
|
|
20
19
|
|
|
@@ -54,7 +53,7 @@ def test_eval(case_name, logger, tmp_path):
|
|
|
54
53
|
shutil.copy2(case_dir / "input.ipynb", work_path)
|
|
55
54
|
|
|
56
55
|
# Load config and run formatter
|
|
57
|
-
config = load_config(
|
|
56
|
+
config = load_config()
|
|
58
57
|
process_notebook(work_path, config, DIALECT, logger)
|
|
59
58
|
|
|
60
59
|
# Read formatted and expected
|
|
@@ -3,7 +3,7 @@ import nbformat
|
|
|
3
3
|
from nbformat import v4 as nbf
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
|
-
from sqlnbfmt.formatter import process_notebook, load_config
|
|
6
|
+
from sqlnbfmt.formatter import process_notebook, load_config, diff_notebook
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
# Fixture for temporary notebook path
|
|
@@ -216,7 +216,7 @@ def test_sql_formatter(temp_nb_path, logger, input_cells, expected_cells):
|
|
|
216
216
|
nbformat.write(nb, temp_nb_path)
|
|
217
217
|
|
|
218
218
|
# Load configuration
|
|
219
|
-
config = load_config(
|
|
219
|
+
config = load_config()
|
|
220
220
|
dialect = "mysql" # or your preferred SQL dialect
|
|
221
221
|
|
|
222
222
|
# Run the formatter on the notebook
|
|
@@ -235,3 +235,102 @@ def test_sql_formatter(temp_nb_path, logger, input_cells, expected_cells):
|
|
|
235
235
|
|
|
236
236
|
# Clean up
|
|
237
237
|
temp_nb_path.unlink()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_skip_hint(tmp_path, logger):
|
|
241
|
+
"""Cell with # sqlnbfmt: skip should remain untouched."""
|
|
242
|
+
nb = nbf.new_notebook()
|
|
243
|
+
original_source = (
|
|
244
|
+
'# sqlnbfmt: skip\nquery = "select id, name from users where active = 1"'
|
|
245
|
+
)
|
|
246
|
+
nb.cells = [nbf.new_code_cell(original_source)]
|
|
247
|
+
nb_path = tmp_path / "skip_test.ipynb"
|
|
248
|
+
nbformat.write(nb, nb_path)
|
|
249
|
+
|
|
250
|
+
config = load_config()
|
|
251
|
+
changed = process_notebook(nb_path, config, "mysql", logger)
|
|
252
|
+
assert not changed
|
|
253
|
+
|
|
254
|
+
result_nb = nbformat.read(nb_path, as_version=4)
|
|
255
|
+
assert result_nb.cells[0].source == original_source
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_check_only_unformatted(tmp_path, logger):
|
|
259
|
+
"""--check on unformatted notebook returns True but file unchanged."""
|
|
260
|
+
nb = nbf.new_notebook()
|
|
261
|
+
original_source = 'query = "select id, name from users where active = 1"'
|
|
262
|
+
nb.cells = [nbf.new_code_cell(original_source)]
|
|
263
|
+
nb_path = tmp_path / "check_test.ipynb"
|
|
264
|
+
nbformat.write(nb, nb_path)
|
|
265
|
+
|
|
266
|
+
config = load_config()
|
|
267
|
+
changed = process_notebook(nb_path, config, "mysql", logger, check_only=True)
|
|
268
|
+
assert changed
|
|
269
|
+
|
|
270
|
+
# File should be unchanged
|
|
271
|
+
result_nb = nbformat.read(nb_path, as_version=4)
|
|
272
|
+
assert result_nb.cells[0].source == original_source
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_check_only_formatted(tmp_path, logger):
|
|
276
|
+
"""--check on already-formatted notebook returns False."""
|
|
277
|
+
nb = nbf.new_notebook()
|
|
278
|
+
nb.cells = [
|
|
279
|
+
nbf.new_code_cell(
|
|
280
|
+
'query = """\nSELECT\n id,\n name\nFROM users\nWHERE\n active = 1\n"""'
|
|
281
|
+
)
|
|
282
|
+
]
|
|
283
|
+
nb_path = tmp_path / "check_formatted.ipynb"
|
|
284
|
+
nbformat.write(nb, nb_path)
|
|
285
|
+
|
|
286
|
+
config = load_config()
|
|
287
|
+
changed = process_notebook(nb_path, config, "mysql", logger, check_only=True)
|
|
288
|
+
assert not changed
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_diff_notebook_unformatted(tmp_path, logger):
|
|
292
|
+
"""diff_notebook returns non-empty diff for unformatted notebook."""
|
|
293
|
+
nb = nbf.new_notebook()
|
|
294
|
+
nb.cells = [
|
|
295
|
+
nbf.new_code_cell('query = "select id, name from users where active = 1"')
|
|
296
|
+
]
|
|
297
|
+
nb_path = tmp_path / "diff_test.ipynb"
|
|
298
|
+
nbformat.write(nb, nb_path)
|
|
299
|
+
|
|
300
|
+
config = load_config()
|
|
301
|
+
diff_output = diff_notebook(nb_path, config, "mysql", logger)
|
|
302
|
+
assert diff_output # non-empty
|
|
303
|
+
assert "---" in diff_output
|
|
304
|
+
assert "+++" in diff_output
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_diff_notebook_formatted(tmp_path, logger):
|
|
308
|
+
"""diff_notebook returns empty string for already-formatted notebook."""
|
|
309
|
+
nb = nbf.new_notebook()
|
|
310
|
+
nb.cells = [
|
|
311
|
+
nbf.new_code_cell(
|
|
312
|
+
'query = """\nSELECT\n id,\n name\nFROM users\nWHERE\n active = 1\n"""'
|
|
313
|
+
)
|
|
314
|
+
]
|
|
315
|
+
nb_path = tmp_path / "diff_formatted.ipynb"
|
|
316
|
+
nbformat.write(nb, nb_path)
|
|
317
|
+
|
|
318
|
+
config = load_config()
|
|
319
|
+
diff_output = diff_notebook(nb_path, config, "mysql", logger)
|
|
320
|
+
assert diff_output == ""
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_load_config_defaults():
|
|
324
|
+
"""load_config() without args returns defaults."""
|
|
325
|
+
config = load_config()
|
|
326
|
+
assert "SELECT" in config.sql_keywords
|
|
327
|
+
assert "read_sql" in config.function_names
|
|
328
|
+
assert config.indent_width == 4
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_load_config_missing_file():
|
|
332
|
+
"""load_config with missing path raises FileNotFoundError."""
|
|
333
|
+
import pytest
|
|
334
|
+
|
|
335
|
+
with pytest.raises(FileNotFoundError):
|
|
336
|
+
load_config("/nonexistent/config.yaml")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|