sqlnbfmt 0.3.0__tar.gz → 0.3.1__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.1}/PKG-INFO +1 -1
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/pyproject.toml +1 -1
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt/formatter.py +125 -174
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1/sqlnbfmt.egg-info}/PKG-INFO +1 -1
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/tests/test_formatter.py +127 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/LICENSE +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/README.md +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/setup.cfg +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt/__init__.py +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt.egg-info/SOURCES.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt.egg-info/dependency_links.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt.egg-info/entry_points.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt.egg-info/requires.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/sqlnbfmt.egg-info/top_level.txt +0 -0
- {sqlnbfmt-0.3.0 → sqlnbfmt-0.3.1}/tests/test_eval.py +0 -0
|
@@ -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
|
),
|
|
@@ -372,7 +378,7 @@ class SQLStringFormatter(ast.NodeVisitor):
|
|
|
372
378
|
"""Constructs the replacement string literal text."""
|
|
373
379
|
prefix = "f" if is_fstring else ""
|
|
374
380
|
if in_function and "\n" in formatted_sql:
|
|
375
|
-
indent = " " *
|
|
381
|
+
indent = " " * self.config.indent_width
|
|
376
382
|
lines = formatted_sql.split("\n")
|
|
377
383
|
indented_lines = [indent + line if line.strip() else line for line in lines]
|
|
378
384
|
indented_sql = "\n".join(indented_lines)
|
|
@@ -427,188 +433,75 @@ class SQLStringFormatter(ast.NodeVisitor):
|
|
|
427
433
|
return ".".join(reversed(parts))
|
|
428
434
|
|
|
429
435
|
|
|
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.
|
|
438
|
-
|
|
439
|
-
Args:
|
|
440
|
-
check_only: If True, detect changes but don't write the file.
|
|
436
|
+
def _has_skip_hint(source: str) -> bool:
|
|
437
|
+
"""Check if any comment line contains the sqlnbfmt: skip directive.
|
|
441
438
|
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
Only dedicated comment lines are recognised (the line must start with ``#``
|
|
440
|
+
after stripping whitespace). Inline trailing comments like
|
|
441
|
+
``x = 1 # sqlnbfmt: skip`` are intentionally ignored so that the directive
|
|
442
|
+
cannot be triggered accidentally from within an expression or string.
|
|
444
443
|
"""
|
|
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
|
|
444
|
+
return any(
|
|
445
|
+
line.strip().startswith("#") and "sqlnbfmt: skip" in line
|
|
446
|
+
for line in source.split("\n")
|
|
447
|
+
)
|
|
567
448
|
|
|
568
449
|
|
|
569
|
-
def
|
|
570
|
-
|
|
450
|
+
def _format_cells(
|
|
451
|
+
notebook: nbformat.NotebookNode,
|
|
571
452
|
config: FormattingConfig,
|
|
572
453
|
dialect: Optional[str],
|
|
573
454
|
logger: logging.Logger,
|
|
574
|
-
) ->
|
|
575
|
-
"""
|
|
576
|
-
|
|
577
|
-
original_nb = nbformat.read(notebook_path, as_version=4)
|
|
578
|
-
formatted_nb = copy.deepcopy(original_nb)
|
|
455
|
+
) -> bool:
|
|
456
|
+
"""Format cells in-place. Returns True if any cell was modified."""
|
|
457
|
+
changed = False
|
|
579
458
|
|
|
580
|
-
|
|
581
|
-
for cell_index, cell in enumerate(formatted_nb.cells):
|
|
459
|
+
for cell_index, cell in enumerate(notebook.cells):
|
|
582
460
|
if cell.cell_type != "code":
|
|
583
461
|
continue
|
|
462
|
+
|
|
584
463
|
original_code = cell.source
|
|
585
464
|
if not original_code.strip():
|
|
586
465
|
continue
|
|
587
|
-
|
|
466
|
+
|
|
467
|
+
if _has_skip_hint(original_code):
|
|
468
|
+
logger.debug(f"[Cell {cell_index}] Skipping cell (sqlnbfmt: skip)")
|
|
588
469
|
continue
|
|
589
470
|
|
|
590
471
|
lines = original_code.split("\n")
|
|
472
|
+
|
|
473
|
+
# Initialize variables to track magic commands
|
|
591
474
|
magic_cmd = None
|
|
592
475
|
magic_cmd_index = None
|
|
476
|
+
|
|
477
|
+
# Iterate through lines to find the first non-comment magic command
|
|
593
478
|
for idx, line in enumerate(lines):
|
|
594
479
|
stripped = line.strip()
|
|
480
|
+
|
|
595
481
|
if not stripped:
|
|
596
|
-
continue
|
|
482
|
+
continue # Skip empty lines
|
|
483
|
+
|
|
597
484
|
if stripped.startswith("#"):
|
|
598
|
-
continue
|
|
485
|
+
continue # Skip comment lines
|
|
486
|
+
|
|
599
487
|
if stripped.startswith("%%sql") or stripped.startswith("%sql"):
|
|
600
488
|
magic_cmd = stripped.split()[0]
|
|
601
489
|
magic_cmd_index = idx
|
|
602
|
-
break
|
|
490
|
+
break # Magic command found
|
|
491
|
+
|
|
603
492
|
else:
|
|
604
|
-
break
|
|
493
|
+
break # Non-magic, non-comment line found
|
|
605
494
|
|
|
606
495
|
if magic_cmd:
|
|
607
496
|
is_cell_magic = magic_cmd.startswith("%%sql")
|
|
497
|
+
|
|
608
498
|
if is_cell_magic:
|
|
499
|
+
# Cell magic: SQL code starts from the next line
|
|
609
500
|
sql_code = "\n".join(lines[magic_cmd_index + 1 :]).strip()
|
|
610
501
|
else:
|
|
502
|
+
# Line magic: SQL code is on the same line after the magic command
|
|
611
503
|
sql_code = lines[magic_cmd_index][len(magic_cmd) :].strip()
|
|
504
|
+
|
|
612
505
|
try:
|
|
613
506
|
formatted_sql = format_sql_code(
|
|
614
507
|
sql_code,
|
|
@@ -617,23 +510,33 @@ def diff_notebook(
|
|
|
617
510
|
is_magic_command=True,
|
|
618
511
|
is_cell_magic=is_cell_magic,
|
|
619
512
|
)
|
|
513
|
+
|
|
514
|
+
# Reconstruct the cell content
|
|
620
515
|
if is_cell_magic:
|
|
516
|
+
# Preserve comments before the magic command
|
|
621
517
|
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
622
518
|
if pre_magic:
|
|
623
519
|
new_content = f"{pre_magic}\n{magic_cmd}\n{formatted_sql}"
|
|
624
520
|
else:
|
|
625
521
|
new_content = f"{magic_cmd}\n{formatted_sql}"
|
|
626
522
|
else:
|
|
523
|
+
# Preserve comments before the magic command
|
|
627
524
|
pre_magic = "\n".join(lines[:magic_cmd_index])
|
|
628
525
|
if pre_magic:
|
|
629
526
|
new_content = f"{pre_magic}\n{magic_cmd} {formatted_sql}"
|
|
630
527
|
else:
|
|
631
528
|
new_content = f"{magic_cmd} {formatted_sql}"
|
|
632
|
-
cell.source = new_content
|
|
633
|
-
except SQLFormattingError:
|
|
634
|
-
pass
|
|
635
|
-
continue
|
|
636
529
|
|
|
530
|
+
if new_content != original_code:
|
|
531
|
+
cell.source = new_content
|
|
532
|
+
changed = True
|
|
533
|
+
|
|
534
|
+
except SQLFormattingError as e:
|
|
535
|
+
logger.warning(f"[Cell {cell_index}] SQL magic formatting skipped: {e}")
|
|
536
|
+
|
|
537
|
+
continue # Move to the next cell after handling magic command
|
|
538
|
+
|
|
539
|
+
# Handle regular Python code with surgical string replacement
|
|
637
540
|
try:
|
|
638
541
|
tree = ast.parse(original_code)
|
|
639
542
|
line_offsets = build_line_offsets(original_code)
|
|
@@ -651,10 +554,61 @@ def diff_notebook(
|
|
|
651
554
|
+ rep.new_text
|
|
652
555
|
+ result[rep.end_offset :]
|
|
653
556
|
)
|
|
654
|
-
|
|
557
|
+
if result != original_code:
|
|
558
|
+
cell.source = result
|
|
559
|
+
changed = True
|
|
655
560
|
except SyntaxError:
|
|
561
|
+
logger.warning(
|
|
562
|
+
f"[Cell {cell_index}] Failed to parse cell:\n{original_code}"
|
|
563
|
+
)
|
|
656
564
|
continue
|
|
657
565
|
|
|
566
|
+
return changed
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def process_notebook(
|
|
570
|
+
notebook_path: Union[str, Path],
|
|
571
|
+
config: FormattingConfig,
|
|
572
|
+
dialect: Optional[str],
|
|
573
|
+
logger: logging.Logger,
|
|
574
|
+
check_only: bool = False,
|
|
575
|
+
) -> bool:
|
|
576
|
+
"""Processes a Jupyter notebook.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
check_only: If True, detect changes but don't write the file.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
True if changes were made (or would be made in check mode).
|
|
583
|
+
"""
|
|
584
|
+
try:
|
|
585
|
+
notebook = nbformat.read(notebook_path, as_version=4)
|
|
586
|
+
changed = _format_cells(notebook, config, dialect, logger)
|
|
587
|
+
|
|
588
|
+
if changed and not check_only:
|
|
589
|
+
nbformat.write(notebook, notebook_path)
|
|
590
|
+
logger.info(f"Updated notebook: {notebook_path}")
|
|
591
|
+
|
|
592
|
+
return changed
|
|
593
|
+
|
|
594
|
+
except Exception as e:
|
|
595
|
+
logger.error(f"Failed to process notebook {notebook_path}: {e}", exc_info=True)
|
|
596
|
+
raise
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def diff_notebook(
|
|
600
|
+
notebook_path: Union[str, Path],
|
|
601
|
+
config: FormattingConfig,
|
|
602
|
+
dialect: Optional[str],
|
|
603
|
+
logger: logging.Logger,
|
|
604
|
+
) -> str:
|
|
605
|
+
"""Returns a unified diff of formatting changes for a notebook."""
|
|
606
|
+
notebook_path = Path(notebook_path)
|
|
607
|
+
original_nb = nbformat.read(notebook_path, as_version=4)
|
|
608
|
+
formatted_nb = copy.deepcopy(original_nb)
|
|
609
|
+
|
|
610
|
+
_format_cells(formatted_nb, config, dialect, logger)
|
|
611
|
+
|
|
658
612
|
# Build unified diff cell-by-cell
|
|
659
613
|
diff_lines = []
|
|
660
614
|
for i, (orig_cell, fmt_cell) in enumerate(
|
|
@@ -665,8 +619,8 @@ def diff_notebook(
|
|
|
665
619
|
difflib.unified_diff(
|
|
666
620
|
orig_cell.source.splitlines(keepends=True),
|
|
667
621
|
fmt_cell.source.splitlines(keepends=True),
|
|
668
|
-
fromfile=f"Cell [{i}] original",
|
|
669
|
-
tofile=f"Cell [{i}] formatted",
|
|
622
|
+
fromfile=f"{notebook_path} Cell [{i}] original",
|
|
623
|
+
tofile=f"{notebook_path} Cell [{i}] formatted",
|
|
670
624
|
)
|
|
671
625
|
)
|
|
672
626
|
return "".join(diff_lines)
|
|
@@ -715,30 +669,27 @@ def main():
|
|
|
715
669
|
sys.exit(1)
|
|
716
670
|
|
|
717
671
|
try:
|
|
718
|
-
changed = False
|
|
719
672
|
changed_files = []
|
|
720
673
|
|
|
721
674
|
for notebook in args.notebooks:
|
|
722
|
-
if args.diff:
|
|
675
|
+
if args.check or args.diff:
|
|
676
|
+
# In check/diff mode, use diff_notebook to get both the
|
|
677
|
+
# diff output and whether changes are needed (single pass).
|
|
723
678
|
diff_output = diff_notebook(notebook, config, args.dialect, logger)
|
|
724
|
-
|
|
679
|
+
needs_formatting = bool(diff_output)
|
|
680
|
+
|
|
681
|
+
if args.diff and diff_output:
|
|
725
682
|
print(diff_output, end="")
|
|
726
|
-
changed = True
|
|
727
|
-
changed_files.append(notebook)
|
|
728
683
|
|
|
729
|
-
|
|
730
|
-
if process_notebook(
|
|
731
|
-
notebook, config, args.dialect, logger, check_only=True
|
|
732
|
-
):
|
|
733
|
-
changed = True
|
|
684
|
+
if needs_formatting:
|
|
734
685
|
changed_files.append(notebook)
|
|
735
|
-
|
|
686
|
+
else:
|
|
687
|
+
# Normal mode: format in place
|
|
736
688
|
if process_notebook(notebook, config, args.dialect, logger):
|
|
737
|
-
changed = True
|
|
738
689
|
changed_files.append(notebook)
|
|
739
690
|
|
|
740
691
|
if args.check:
|
|
741
|
-
if
|
|
692
|
+
if changed_files:
|
|
742
693
|
logger.error(f"{len(changed_files)} notebook(s) would be reformatted:")
|
|
743
694
|
for f in changed_files:
|
|
744
695
|
logger.error(f" - {f}")
|
|
@@ -749,7 +700,7 @@ def main():
|
|
|
749
700
|
|
|
750
701
|
# Print summary for normal mode
|
|
751
702
|
if not args.diff:
|
|
752
|
-
if
|
|
703
|
+
if changed_files:
|
|
753
704
|
logger.info("Changes made to the following notebooks:")
|
|
754
705
|
for file in changed_files:
|
|
755
706
|
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
|