1717from databricks .labs .ucx .source_code .files import LocalFile
1818from databricks .labs .ucx .source_code .graph import Dependency
1919from databricks .labs .ucx .source_code .known import KnownDependency
20- from databricks .labs .ucx .source_code .linters .base import PythonLinter
20+ from databricks .labs .ucx .source_code .linters .base import PythonFixer , PythonLinter
2121from databricks .labs .ucx .source_code .linters .context import LinterContext
2222from databricks .labs .ucx .source_code .linters .imports import SysPathChange , UnresolvedPath
2323from databricks .labs .ucx .source_code .notebooks .cells import (
2626 RunCell ,
2727 RunCommand ,
2828)
29- from databricks .labs .ucx .source_code .notebooks .loaders import NotebookLoader
3029from databricks .labs .ucx .source_code .notebooks .magic import MagicLine
3130from databricks .labs .ucx .source_code .notebooks .sources import Notebook
3231from databricks .labs .ucx .source_code .path_lookup import PathLookup
@@ -42,7 +41,11 @@ class NotebookLinter:
4241 """
4342
4443 def __init__ (
45- self , notebook : Notebook , path_lookup : PathLookup , context : LinterContext , parent_tree : Tree | None = None
44+ self ,
45+ notebook : Notebook ,
46+ path_lookup : PathLookup ,
47+ context : LinterContext ,
48+ parent_tree : Tree | None = None ,
4649 ):
4750 self ._context : LinterContext = context
4851 self ._path_lookup = path_lookup
@@ -76,6 +79,37 @@ def lint(self) -> Iterable[Advice]:
7679 )
7780 return
7881
82+ def apply (self ) -> None :
83+ """Apply changes to the notebook."""
84+ maybe_tree = self ._parse_notebook (self ._notebook , parent_tree = self ._parent_tree )
85+ if maybe_tree and maybe_tree .failure :
86+ logger .warning ("Failed to parse the notebook, run linter for more details." )
87+ return
88+ for cell in self ._notebook .cells :
89+ try :
90+ linter = self ._context .linter (cell .language .language )
91+ except ValueError : # Language is not supported (yet)
92+ continue
93+ fixed_code = cell .original_code # For default fixing
94+ tree = self ._python_tree_cache .get ((self ._notebook .path , cell )) # For Python fixing
95+ is_python_cell = isinstance (cell , PythonCell )
96+ if is_python_cell and tree :
97+ advices = cast (PythonLinter , linter ).lint_tree (tree )
98+ else :
99+ advices = linter .lint (cell .original_code )
100+ for advice in advices :
101+ fixer = self ._context .fixer (cell .language .language , advice .code )
102+ if not fixer :
103+ continue
104+ if is_python_cell and tree :
105+ # By calling `apply_tree` instead of `apply`, we chain fixes on the same tree
106+ tree = cast (PythonFixer , fixer ).apply_tree (tree )
107+ else :
108+ fixed_code = fixer .apply (fixed_code )
109+ cell .migrated_code = tree .node .as_string () if tree else fixed_code
110+ self ._notebook .back_up_original_and_flush_migrated_code ()
111+ return
112+
79113 def _parse_notebook (self , notebook : Notebook , * , parent_tree : Tree ) -> MaybeTree | None :
80114 """Parse a notebook by parsing its cells.
81115
@@ -264,50 +298,16 @@ def apply(self) -> None:
264298 source_container = self ._dependency .load (self ._path_lookup )
265299 if isinstance (source_container , LocalFile ):
266300 self ._apply_file (source_container )
301+ elif isinstance (source_container , Notebook ):
302+ self ._apply_notebook (source_container )
267303
268304 def _apply_file (self , local_file : LocalFile ) -> None :
269305 """Apply changes to a local file."""
270306 fixed_code = self ._context .apply_fixes (local_file .language , local_file .original_code )
271307 local_file .migrated_code = fixed_code
272308 local_file .back_up_original_and_flush_migrated_code ()
273309
274-
275- class NotebookMigrator :
276- def __init__ (self , languages : LinterContext ):
277- # TODO: move languages to `apply`
278- self ._languages = languages
279-
280- def revert (self , path : Path ) -> bool :
281- backup_path = path .with_suffix (".bak" )
282- if not backup_path .exists ():
283- return False
284- return path .write_text (backup_path .read_text ()) > 0
285-
286- def apply (self , path : Path ) -> bool :
287- if not path .exists ():
288- return False
289- dependency = Dependency (NotebookLoader (), path )
290- # TODO: the interface for this method has to be changed
291- lookup = PathLookup .from_sys_path (Path .cwd ())
292- container = dependency .load (lookup )
293- assert isinstance (container , Notebook )
294- return self ._apply (container )
295-
296- def _apply (self , notebook : Notebook ) -> bool :
297- changed = False
298- for cell in notebook .cells :
299- # %run is not a supported language, so this needs to come first
300- if isinstance (cell , RunCell ):
301- # TODO migration data, see https://github.com/databrickslabs/ucx/issues/1327
302- continue
303- if not self ._languages .is_supported (cell .language .language ):
304- continue
305- migrated_code = self ._languages .apply_fixes (cell .language .language , cell .original_code )
306- if migrated_code != cell .original_code :
307- cell .migrated_code = migrated_code
308- changed = True
309- if changed :
310- # TODO https://github.com/databrickslabs/ucx/issues/1327 store 'migrated' status
311- notebook .path .replace (notebook .path .with_suffix (".bak" ))
312- notebook .path .write_text (notebook .to_migrated_code ())
313- return changed
310+ def _apply_notebook (self , notebook : Notebook ) -> None :
311+ """Apply changes to a notebook."""
312+ notebook_linter = NotebookLinter (notebook , self ._path_lookup , self ._context , self ._inherited_tree )
313+ notebook_linter .apply ()
0 commit comments