sqlnbfmt 0.3.0__tar.gz → 0.3.2__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.0/sqlnbfmt.egg-info → sqlnbfmt-0.3.2}/PKG-INFO +1 -1
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/pyproject.toml +1 -1
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt/formatter.py +138 -179
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2/sqlnbfmt.egg-info}/PKG-INFO +1 -1
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/tests/test_formatter.py +127 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/LICENSE +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/README.md +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/setup.cfg +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt/__init__.py +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt.egg-info/SOURCES.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt.egg-info/dependency_links.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt.egg-info/entry_points.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt.egg-info/requires.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/sqlnbfmt.egg-info/top_level.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.2}/tests/test_eval.py +0 -0
|
@@ -10,7 +10,7 @@ from typing import Set, Optional, Dict, Union, Tuple
|
|
|
10
10
|
|
|
11
11
|
import nbformat
|
|
12
12
|
import yaml
|
|
13
|
-
from sqlglot import
|
|
13
|
+
from sqlglot import parse, errors
|
|
14
14
|
|
|
15
15
|
DEFAULT_SQL_KEYWORDS = frozenset(
|
|
16
16
|
{
|
|
@@ -116,10 +116,16 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> FormattingCon
|
|
|
116
116
|
try:
|
|
117
117
|
with open(config_file) as file:
|
|
118
118
|
config = yaml.safe_load(file)
|
|
119
|
+
if config is None:
|
|
120
|
+
return FormattingConfig()
|
|
119
121
|
return FormattingConfig(
|
|
120
|
-
sql_keywords=set(config.get("sql_keywords",
|
|
121
|
-
function_names=set(
|
|
122
|
-
|
|
122
|
+
sql_keywords=set(config.get("sql_keywords", DEFAULT_SQL_KEYWORDS)),
|
|
123
|
+
function_names=set(
|
|
124
|
+
config.get("function_names", DEFAULT_FUNCTION_NAMES)
|
|
125
|
+
),
|
|
126
|
+
sql_decorators=set(
|
|
127
|
+
config.get("sql_decorators", DEFAULT_SQL_DECORATORS)
|
|
128
|
+
),
|
|
123
129
|
single_line_threshold=config.get("formatting_options", {}).get(
|
|
124
130
|
"single_line_threshold", 80
|
|
125
131
|
),
|
|
@@ -200,10 +206,18 @@ def format_sql_code(
|
|
|
200
206
|
temp_sql = temp_sql.strip()
|
|
201
207
|
|
|
202
208
|
# Parse and format SQL
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
statements = parse(temp_sql, read=dialect)
|
|
210
|
+
formatted_parts = []
|
|
211
|
+
for stmt in statements:
|
|
212
|
+
formatted_parts.append(
|
|
213
|
+
stmt.sql(
|
|
214
|
+
pretty=not force_single_line,
|
|
215
|
+
indent=config.indent_width,
|
|
216
|
+
dialect=dialect,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
separator = "; " if force_single_line else ";\n"
|
|
220
|
+
formatted_sql = separator.join(formatted_parts)
|
|
207
221
|
|
|
208
222
|
# Apply formatting based on context
|
|
209
223
|
if is_magic_command and not is_cell_magic:
|
|
@@ -372,7 +386,7 @@ class SQLStringFormatter(ast.NodeVisitor):
|
|
|
372
386
|
"""Constructs the replacement string literal text."""
|
|
373
387
|
prefix = "f" if is_fstring else ""
|
|
374
388
|
if in_function and "\n" in formatted_sql:
|
|
375
|
-
indent = " " *
|
|
389
|
+
indent = " " * self.config.indent_width
|
|
376
390
|
lines = formatted_sql.split("\n")
|
|
377
391
|
indented_lines = [indent + line if line.strip() else line for line in lines]
|
|
378
392
|
indented_sql = "\n".join(indented_lines)
|
|
@@ -427,188 +441,75 @@ class SQLStringFormatter(ast.NodeVisitor):
|
|
|
427
441
|
return ".".join(reversed(parts))
|
|
428
442
|
|
|
429
443
|
|
|
430
|
-
def
|
|
431
|
-
|
|
432
|
-
config: FormattingConfig,
|
|
433
|
-
dialect: Optional[str],
|
|
434
|
-
logger: logging.Logger,
|
|
435
|
-
check_only: bool = False,
|
|
436
|
-
) -> bool:
|
|
437
|
-
"""Processes a Jupyter notebook.
|
|
444
|
+
def _has_skip_hint(source: str) -> bool:
|
|
445
|
+
"""Check if any comment line contains the sqlnbfmt: skip directive.
|
|
438
446
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
True if changes were made (or would be made in check mode).
|
|
447
|
+
Only dedicated comment lines are recognised (the line must start with ``#``
|
|
448
|
+
after stripping whitespace). Inline trailing comments like
|
|
449
|
+
``x = 1 # sqlnbfmt: skip`` are intentionally ignored so that the directive
|
|
450
|
+
cannot be triggered accidentally from within an expression or string.
|
|
444
451
|
"""
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
for cell_index, cell in enumerate(notebook.cells):
|
|
450
|
-
if cell.cell_type != "code":
|
|
451
|
-
continue
|
|
452
|
-
|
|
453
|
-
original_code = cell.source
|
|
454
|
-
if not original_code.strip():
|
|
455
|
-
continue
|
|
456
|
-
|
|
457
|
-
if "# sqlnbfmt: skip" in original_code:
|
|
458
|
-
logger.debug(f"[Cell {cell_index}] Skipping cell (sqlnbfmt: skip)")
|
|
459
|
-
continue
|
|
460
|
-
|
|
461
|
-
lines = original_code.split("\n")
|
|
462
|
-
|
|
463
|
-
# Initialize variables to track magic commands
|
|
464
|
-
magic_cmd = None
|
|
465
|
-
magic_cmd_index = None
|
|
466
|
-
|
|
467
|
-
# Iterate through lines to find the first non-comment magic command
|
|
468
|
-
for idx, line in enumerate(lines):
|
|
469
|
-
stripped = line.strip()
|
|
470
|
-
|
|
471
|
-
if not stripped:
|
|
472
|
-
continue # Skip empty lines
|
|
473
|
-
|
|
474
|
-
if stripped.startswith("#"):
|
|
475
|
-
continue # Skip comment lines
|
|
476
|
-
|
|
477
|
-
if stripped.startswith("%%sql") or stripped.startswith("%sql"):
|
|
478
|
-
magic_cmd = stripped.split()[0]
|
|
479
|
-
magic_cmd_index = idx
|
|
480
|
-
break # Magic command found
|
|
481
|
-
|
|
482
|
-
else:
|
|
483
|
-
break # Non-magic, non-comment line found
|
|
484
|
-
|
|
485
|
-
if magic_cmd:
|
|
486
|
-
is_cell_magic = magic_cmd.startswith("%%sql")
|
|
487
|
-
|
|
488
|
-
if is_cell_magic:
|
|
489
|
-
# Cell magic: SQL code starts from the next line
|
|
490
|
-
sql_code = "\n".join(lines[magic_cmd_index + 1 :]).strip()
|
|
491
|
-
else:
|
|
492
|
-
# Line magic: SQL code is on the same line after the magic command
|
|
493
|
-
sql_code = lines[magic_cmd_index][len(magic_cmd) :].strip()
|
|
494
|
-
|
|
495
|
-
try:
|
|
496
|
-
formatted_sql = format_sql_code(
|
|
497
|
-
sql_code,
|
|
498
|
-
dialect,
|
|
499
|
-
config,
|
|
500
|
-
is_magic_command=True,
|
|
501
|
-
is_cell_magic=is_cell_magic,
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
# Reconstruct the cell content
|
|
505
|
-
if is_cell_magic:
|
|
506
|
-
# Preserve comments before the magic command
|
|
507
|
-
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
508
|
-
if pre_magic:
|
|
509
|
-
new_content = f"{pre_magic}\n{magic_cmd}\n{formatted_sql}"
|
|
510
|
-
else:
|
|
511
|
-
new_content = f"{magic_cmd}\n{formatted_sql}"
|
|
512
|
-
else:
|
|
513
|
-
# Preserve comments before the magic command
|
|
514
|
-
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
515
|
-
if pre_magic:
|
|
516
|
-
new_content = f"{pre_magic}\n{magic_cmd} {formatted_sql}"
|
|
517
|
-
else:
|
|
518
|
-
new_content = f"{magic_cmd} {formatted_sql}"
|
|
519
|
-
|
|
520
|
-
if new_content != original_code:
|
|
521
|
-
cell.source = new_content
|
|
522
|
-
changed = True
|
|
523
|
-
|
|
524
|
-
except SQLFormattingError as e:
|
|
525
|
-
logger.warning(
|
|
526
|
-
f"[Cell {cell_index}] SQL magic formatting skipped: {e}"
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
continue # Move to the next cell after handling magic command
|
|
530
|
-
|
|
531
|
-
# Handle regular Python code with surgical string replacement
|
|
532
|
-
try:
|
|
533
|
-
tree = ast.parse(original_code)
|
|
534
|
-
line_offsets = build_line_offsets(original_code)
|
|
535
|
-
formatter = SQLStringFormatter(config, dialect, logger, line_offsets)
|
|
536
|
-
formatter.visit(tree)
|
|
537
|
-
if formatter.replacements:
|
|
538
|
-
result = original_code
|
|
539
|
-
for rep in sorted(
|
|
540
|
-
formatter.replacements,
|
|
541
|
-
key=lambda r: r.start_offset,
|
|
542
|
-
reverse=True,
|
|
543
|
-
):
|
|
544
|
-
result = (
|
|
545
|
-
result[: rep.start_offset]
|
|
546
|
-
+ rep.new_text
|
|
547
|
-
+ result[rep.end_offset :]
|
|
548
|
-
)
|
|
549
|
-
if result != original_code:
|
|
550
|
-
cell.source = result
|
|
551
|
-
changed = True
|
|
552
|
-
except SyntaxError:
|
|
553
|
-
logger.warning(
|
|
554
|
-
f"[Cell {cell_index}] Failed to parse cell:\n{original_code}"
|
|
555
|
-
)
|
|
556
|
-
continue
|
|
557
|
-
|
|
558
|
-
if changed and not check_only:
|
|
559
|
-
nbformat.write(notebook, notebook_path)
|
|
560
|
-
logger.info(f"Updated notebook: {notebook_path}")
|
|
561
|
-
|
|
562
|
-
return changed
|
|
563
|
-
|
|
564
|
-
except Exception as e:
|
|
565
|
-
logger.error(f"Failed to process notebook {notebook_path}: {e}", exc_info=True)
|
|
566
|
-
raise
|
|
452
|
+
return any(
|
|
453
|
+
line.strip().startswith("#") and "sqlnbfmt: skip" in line
|
|
454
|
+
for line in source.split("\n")
|
|
455
|
+
)
|
|
567
456
|
|
|
568
457
|
|
|
569
|
-
def
|
|
570
|
-
|
|
458
|
+
def _format_cells(
|
|
459
|
+
notebook: nbformat.NotebookNode,
|
|
571
460
|
config: FormattingConfig,
|
|
572
461
|
dialect: Optional[str],
|
|
573
462
|
logger: logging.Logger,
|
|
574
|
-
) ->
|
|
575
|
-
"""
|
|
576
|
-
|
|
577
|
-
original_nb = nbformat.read(notebook_path, as_version=4)
|
|
578
|
-
formatted_nb = copy.deepcopy(original_nb)
|
|
463
|
+
) -> bool:
|
|
464
|
+
"""Format cells in-place. Returns True if any cell was modified."""
|
|
465
|
+
changed = False
|
|
579
466
|
|
|
580
|
-
|
|
581
|
-
for cell_index, cell in enumerate(formatted_nb.cells):
|
|
467
|
+
for cell_index, cell in enumerate(notebook.cells):
|
|
582
468
|
if cell.cell_type != "code":
|
|
583
469
|
continue
|
|
470
|
+
|
|
584
471
|
original_code = cell.source
|
|
585
472
|
if not original_code.strip():
|
|
586
473
|
continue
|
|
587
|
-
|
|
474
|
+
|
|
475
|
+
if _has_skip_hint(original_code):
|
|
476
|
+
logger.debug(f"[Cell {cell_index}] Skipping cell (sqlnbfmt: skip)")
|
|
588
477
|
continue
|
|
589
478
|
|
|
590
479
|
lines = original_code.split("\n")
|
|
480
|
+
|
|
481
|
+
# Initialize variables to track magic commands
|
|
591
482
|
magic_cmd = None
|
|
592
483
|
magic_cmd_index = None
|
|
484
|
+
|
|
485
|
+
# Iterate through lines to find the first non-comment magic command
|
|
593
486
|
for idx, line in enumerate(lines):
|
|
594
487
|
stripped = line.strip()
|
|
488
|
+
|
|
595
489
|
if not stripped:
|
|
596
|
-
continue
|
|
490
|
+
continue # Skip empty lines
|
|
491
|
+
|
|
597
492
|
if stripped.startswith("#"):
|
|
598
|
-
continue
|
|
493
|
+
continue # Skip comment lines
|
|
494
|
+
|
|
599
495
|
if stripped.startswith("%%sql") or stripped.startswith("%sql"):
|
|
600
496
|
magic_cmd = stripped.split()[0]
|
|
601
497
|
magic_cmd_index = idx
|
|
602
|
-
break
|
|
498
|
+
break # Magic command found
|
|
499
|
+
|
|
603
500
|
else:
|
|
604
|
-
break
|
|
501
|
+
break # Non-magic, non-comment line found
|
|
605
502
|
|
|
606
503
|
if magic_cmd:
|
|
607
504
|
is_cell_magic = magic_cmd.startswith("%%sql")
|
|
505
|
+
|
|
608
506
|
if is_cell_magic:
|
|
507
|
+
# Cell magic: SQL code starts from the next line
|
|
609
508
|
sql_code = "\n".join(lines[magic_cmd_index + 1 :]).strip()
|
|
610
509
|
else:
|
|
510
|
+
# Line magic: SQL code is on the same line after the magic command
|
|
611
511
|
sql_code = lines[magic_cmd_index][len(magic_cmd) :].strip()
|
|
512
|
+
|
|
612
513
|
try:
|
|
613
514
|
formatted_sql = format_sql_code(
|
|
614
515
|
sql_code,
|
|
@@ -617,23 +518,33 @@ def diff_notebook(
|
|
|
617
518
|
is_magic_command=True,
|
|
618
519
|
is_cell_magic=is_cell_magic,
|
|
619
520
|
)
|
|
521
|
+
|
|
522
|
+
# Reconstruct the cell content
|
|
620
523
|
if is_cell_magic:
|
|
524
|
+
# Preserve comments before the magic command
|
|
621
525
|
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
622
526
|
if pre_magic:
|
|
623
527
|
new_content = f"{pre_magic}\n{magic_cmd}\n{formatted_sql}"
|
|
624
528
|
else:
|
|
625
529
|
new_content = f"{magic_cmd}\n{formatted_sql}"
|
|
626
530
|
else:
|
|
531
|
+
# Preserve comments before the magic command
|
|
627
532
|
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
628
533
|
if pre_magic:
|
|
629
534
|
new_content = f"{pre_magic}\n{magic_cmd} {formatted_sql}"
|
|
630
535
|
else:
|
|
631
536
|
new_content = f"{magic_cmd} {formatted_sql}"
|
|
632
|
-
cell.source = new_content
|
|
633
|
-
except SQLFormattingError:
|
|
634
|
-
pass
|
|
635
|
-
continue
|
|
636
537
|
|
|
538
|
+
if new_content != original_code:
|
|
539
|
+
cell.source = new_content
|
|
540
|
+
changed = True
|
|
541
|
+
|
|
542
|
+
except SQLFormattingError as e:
|
|
543
|
+
logger.warning(f"[Cell {cell_index}] SQL magic formatting skipped: {e}")
|
|
544
|
+
|
|
545
|
+
continue # Move to the next cell after handling magic command
|
|
546
|
+
|
|
547
|
+
# Handle regular Python code with surgical string replacement
|
|
637
548
|
try:
|
|
638
549
|
tree = ast.parse(original_code)
|
|
639
550
|
line_offsets = build_line_offsets(original_code)
|
|
@@ -651,10 +562,61 @@ def diff_notebook(
|
|
|
651
562
|
+ rep.new_text
|
|
652
563
|
+ result[rep.end_offset :]
|
|
653
564
|
)
|
|
654
|
-
|
|
565
|
+
if result != original_code:
|
|
566
|
+
cell.source = result
|
|
567
|
+
changed = True
|
|
655
568
|
except SyntaxError:
|
|
569
|
+
logger.warning(
|
|
570
|
+
f"[Cell {cell_index}] Failed to parse cell:\n{original_code}"
|
|
571
|
+
)
|
|
656
572
|
continue
|
|
657
573
|
|
|
574
|
+
return changed
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def process_notebook(
|
|
578
|
+
notebook_path: Union[str, Path],
|
|
579
|
+
config: FormattingConfig,
|
|
580
|
+
dialect: Optional[str],
|
|
581
|
+
logger: logging.Logger,
|
|
582
|
+
check_only: bool = False,
|
|
583
|
+
) -> bool:
|
|
584
|
+
"""Processes a Jupyter notebook.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
check_only: If True, detect changes but don't write the file.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
True if changes were made (or would be made in check mode).
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
notebook = nbformat.read(notebook_path, as_version=4)
|
|
594
|
+
changed = _format_cells(notebook, config, dialect, logger)
|
|
595
|
+
|
|
596
|
+
if changed and not check_only:
|
|
597
|
+
nbformat.write(notebook, notebook_path)
|
|
598
|
+
logger.info(f"Updated notebook: {notebook_path}")
|
|
599
|
+
|
|
600
|
+
return changed
|
|
601
|
+
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.error(f"Failed to process notebook {notebook_path}: {e}", exc_info=True)
|
|
604
|
+
raise
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def diff_notebook(
|
|
608
|
+
notebook_path: Union[str, Path],
|
|
609
|
+
config: FormattingConfig,
|
|
610
|
+
dialect: Optional[str],
|
|
611
|
+
logger: logging.Logger,
|
|
612
|
+
) -> str:
|
|
613
|
+
"""Returns a unified diff of formatting changes for a notebook."""
|
|
614
|
+
notebook_path = Path(notebook_path)
|
|
615
|
+
original_nb = nbformat.read(notebook_path, as_version=4)
|
|
616
|
+
formatted_nb = copy.deepcopy(original_nb)
|
|
617
|
+
|
|
618
|
+
_format_cells(formatted_nb, config, dialect, logger)
|
|
619
|
+
|
|
658
620
|
# Build unified diff cell-by-cell
|
|
659
621
|
diff_lines = []
|
|
660
622
|
for i, (orig_cell, fmt_cell) in enumerate(
|
|
@@ -665,8 +627,8 @@ def diff_notebook(
|
|
|
665
627
|
difflib.unified_diff(
|
|
666
628
|
orig_cell.source.splitlines(keepends=True),
|
|
667
629
|
fmt_cell.source.splitlines(keepends=True),
|
|
668
|
-
fromfile=f"Cell [{i}] original",
|
|
669
|
-
tofile=f"Cell [{i}] formatted",
|
|
630
|
+
fromfile=f"{notebook_path} Cell [{i}] original",
|
|
631
|
+
tofile=f"{notebook_path} Cell [{i}] formatted",
|
|
670
632
|
)
|
|
671
633
|
)
|
|
672
634
|
return "".join(diff_lines)
|
|
@@ -715,30 +677,27 @@ def main():
|
|
|
715
677
|
sys.exit(1)
|
|
716
678
|
|
|
717
679
|
try:
|
|
718
|
-
changed = False
|
|
719
680
|
changed_files = []
|
|
720
681
|
|
|
721
682
|
for notebook in args.notebooks:
|
|
722
|
-
if args.diff:
|
|
683
|
+
if args.check or args.diff:
|
|
684
|
+
# In check/diff mode, use diff_notebook to get both the
|
|
685
|
+
# diff output and whether changes are needed (single pass).
|
|
723
686
|
diff_output = diff_notebook(notebook, config, args.dialect, logger)
|
|
724
|
-
|
|
687
|
+
needs_formatting = bool(diff_output)
|
|
688
|
+
|
|
689
|
+
if args.diff and diff_output:
|
|
725
690
|
print(diff_output, end="")
|
|
726
|
-
changed = True
|
|
727
|
-
changed_files.append(notebook)
|
|
728
691
|
|
|
729
|
-
|
|
730
|
-
if process_notebook(
|
|
731
|
-
notebook, config, args.dialect, logger, check_only=True
|
|
732
|
-
):
|
|
733
|
-
changed = True
|
|
692
|
+
if needs_formatting:
|
|
734
693
|
changed_files.append(notebook)
|
|
735
|
-
|
|
694
|
+
else:
|
|
695
|
+
# Normal mode: format in place
|
|
736
696
|
if process_notebook(notebook, config, args.dialect, logger):
|
|
737
|
-
changed = True
|
|
738
697
|
changed_files.append(notebook)
|
|
739
698
|
|
|
740
699
|
if args.check:
|
|
741
|
-
if
|
|
700
|
+
if changed_files:
|
|
742
701
|
logger.error(f"{len(changed_files)} notebook(s) would be reformatted:")
|
|
743
702
|
for f in changed_files:
|
|
744
703
|
logger.error(f" - {f}")
|
|
@@ -749,7 +708,7 @@ def main():
|
|
|
749
708
|
|
|
750
709
|
# Print summary for normal mode
|
|
751
710
|
if not args.diff:
|
|
752
|
-
if
|
|
711
|
+
if changed_files:
|
|
753
712
|
logger.info("Changes made to the following notebooks:")
|
|
754
713
|
for file in changed_files:
|
|
755
714
|
logger.info(f" - {file}")
|
|
@@ -334,3 +334,130 @@ def test_load_config_missing_file():
|
|
|
334
334
|
|
|
335
335
|
with pytest.raises(FileNotFoundError):
|
|
336
336
|
load_config("/nonexistent/config.yaml")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_load_config_empty_file(tmp_path):
|
|
340
|
+
"""load_config with empty YAML file returns defaults (not AttributeError)."""
|
|
341
|
+
empty_config = tmp_path / "empty.yaml"
|
|
342
|
+
empty_config.write_text("")
|
|
343
|
+
config = load_config(empty_config)
|
|
344
|
+
assert "SELECT" in config.sql_keywords
|
|
345
|
+
assert "read_sql" in config.function_names
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_load_config_partial_override(tmp_path):
|
|
349
|
+
"""Config file with only indent_width still gets default keywords."""
|
|
350
|
+
partial_config = tmp_path / "partial.yaml"
|
|
351
|
+
partial_config.write_text("formatting_options:\n indent_width: 2\n")
|
|
352
|
+
config = load_config(partial_config)
|
|
353
|
+
assert config.indent_width == 2
|
|
354
|
+
assert "SELECT" in config.sql_keywords
|
|
355
|
+
assert "read_sql" in config.function_names
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_skip_hint_in_string_not_triggered(tmp_path, logger):
|
|
359
|
+
"""Skip hint inside a string literal should NOT skip the cell."""
|
|
360
|
+
nb = nbf.new_notebook()
|
|
361
|
+
original_source = (
|
|
362
|
+
'msg = "Use # sqlnbfmt: skip to disable"\n'
|
|
363
|
+
'query = "select id, name from users where active = 1"'
|
|
364
|
+
)
|
|
365
|
+
nb.cells = [nbf.new_code_cell(original_source)]
|
|
366
|
+
nb_path = tmp_path / "skip_in_string.ipynb"
|
|
367
|
+
nbformat.write(nb, nb_path)
|
|
368
|
+
|
|
369
|
+
config = load_config()
|
|
370
|
+
changed = process_notebook(nb_path, config, "mysql", logger)
|
|
371
|
+
assert changed # SQL should still be formatted
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_skip_hint_magic_cell(tmp_path, logger):
|
|
375
|
+
"""Skip hint in a magic command cell should skip formatting."""
|
|
376
|
+
nb = nbf.new_notebook()
|
|
377
|
+
original_source = "# sqlnbfmt: skip\n%%sql\nselect * from users where active = 1"
|
|
378
|
+
nb.cells = [nbf.new_code_cell(original_source)]
|
|
379
|
+
nb_path = tmp_path / "skip_magic.ipynb"
|
|
380
|
+
nbformat.write(nb, nb_path)
|
|
381
|
+
|
|
382
|
+
config = load_config()
|
|
383
|
+
changed = process_notebook(nb_path, config, "mysql", logger)
|
|
384
|
+
assert not changed
|
|
385
|
+
|
|
386
|
+
result_nb = nbformat.read(nb_path, as_version=4)
|
|
387
|
+
assert result_nb.cells[0].source == original_source
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_check_diff_combined(tmp_path, logger):
|
|
391
|
+
"""--check --diff combined: no duplicate files, diff is printed."""
|
|
392
|
+
nb = nbf.new_notebook()
|
|
393
|
+
nb.cells = [
|
|
394
|
+
nbf.new_code_cell('query = "select id, name from users where active = 1"')
|
|
395
|
+
]
|
|
396
|
+
nb_path = tmp_path / "combined.ipynb"
|
|
397
|
+
nbformat.write(nb, nb_path)
|
|
398
|
+
|
|
399
|
+
config = load_config()
|
|
400
|
+
# Simulate the combined logic from main()
|
|
401
|
+
diff_output = diff_notebook(nb_path, config, "mysql", logger)
|
|
402
|
+
assert diff_output # has diff
|
|
403
|
+
# In the refactored main(), diff_notebook is used as the single source
|
|
404
|
+
# of truth for both --check and --diff, so no duplication possible.
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_main_diff_only_exits_zero(tmp_path):
|
|
408
|
+
"""--diff alone always exits 0 even when changes are needed."""
|
|
409
|
+
import subprocess
|
|
410
|
+
|
|
411
|
+
nb = nbf.new_notebook()
|
|
412
|
+
nb.cells = [
|
|
413
|
+
nbf.new_code_cell('query = "select id, name from users where active = 1"')
|
|
414
|
+
]
|
|
415
|
+
nb_path = tmp_path / "diff_exit.ipynb"
|
|
416
|
+
nbformat.write(nb, nb_path)
|
|
417
|
+
|
|
418
|
+
result = subprocess.run(
|
|
419
|
+
["sqlnbfmt", "--diff", str(nb_path)],
|
|
420
|
+
capture_output=True,
|
|
421
|
+
text=True,
|
|
422
|
+
)
|
|
423
|
+
assert result.returncode == 0
|
|
424
|
+
assert "---" in result.stdout # diff was printed
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_main_check_exits_one(tmp_path):
|
|
428
|
+
"""--check exits 1 when formatting is needed."""
|
|
429
|
+
import subprocess
|
|
430
|
+
|
|
431
|
+
nb = nbf.new_notebook()
|
|
432
|
+
nb.cells = [
|
|
433
|
+
nbf.new_code_cell('query = "select id, name from users where active = 1"')
|
|
434
|
+
]
|
|
435
|
+
nb_path = tmp_path / "check_exit.ipynb"
|
|
436
|
+
nbformat.write(nb, nb_path)
|
|
437
|
+
|
|
438
|
+
result = subprocess.run(
|
|
439
|
+
["sqlnbfmt", "--check", str(nb_path)],
|
|
440
|
+
capture_output=True,
|
|
441
|
+
text=True,
|
|
442
|
+
)
|
|
443
|
+
assert result.returncode == 1
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_main_check_diff_exits_one(tmp_path):
|
|
447
|
+
"""--check --diff combined exits 1 and prints diff."""
|
|
448
|
+
import subprocess
|
|
449
|
+
|
|
450
|
+
nb = nbf.new_notebook()
|
|
451
|
+
nb.cells = [
|
|
452
|
+
nbf.new_code_cell('query = "select id, name from users where active = 1"')
|
|
453
|
+
]
|
|
454
|
+
nb_path = tmp_path / "check_diff_exit.ipynb"
|
|
455
|
+
nbformat.write(nb, nb_path)
|
|
456
|
+
|
|
457
|
+
result = subprocess.run(
|
|
458
|
+
["sqlnbfmt", "--check", "--diff", str(nb_path)],
|
|
459
|
+
capture_output=True,
|
|
460
|
+
text=True,
|
|
461
|
+
)
|
|
462
|
+
assert result.returncode == 1
|
|
463
|
+
assert "---" in result.stdout # diff was printed
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|