88from collections .abc import Callable
99from enum import Enum
1010from pathlib import Path
11+ from typing import TypeVar
1112
1213from astroid import Call , Const , ImportFrom , Name , NodeNG # type: ignore
1314from astroid .exceptions import AstroidSyntaxError # type: ignore
@@ -229,7 +230,8 @@ def is_runnable(self) -> bool:
229230 return True # TODO
230231
231232 def build_dependency_graph (self , graph : DependencyGraph ) -> list [DependencyProblem ]:
232- return PipMagic (self .original_code ).build_dependency_graph (graph )
233+ node = MagicNode (0 , 1 , None , end_lineno = 0 , end_col_offset = len (self .original_code ))
234+ return PipCommand (node , self .original_code ).build_dependency_graph (graph )
233235
234236
235237class CellLanguage (Enum ):
@@ -414,10 +416,10 @@ def build_graph_from_python_source(self, python_code: str) -> list[DependencyPro
414416 import_problems : list [DependencyProblem ]
415417 import_sources , import_problems = ImportSource .extract_from_tree (tree , DependencyProblem .from_node )
416418 problems .extend (import_problems )
417- magic_commands , command_problems = MagicCommand .extract_from_tree (tree , DependencyProblem .from_node )
419+ magic_commands , command_problems = MagicLine .extract_from_tree (tree , DependencyProblem .from_node )
418420 problems .extend (command_problems )
419421 nodes = syspath_changes + run_calls + import_sources + magic_commands
420- # need to execute things in intertwined sequence so concat and sort
422+ # need to execute things in intertwined sequence so concat and sort them
421423 for base_node in sorted (nodes , key = lambda node : (node .node .lineno , node .node .col_offset )):
422424 for problem in self ._process_node (base_node ):
423425 # Astroid line numbers are 1-based.
@@ -437,7 +439,7 @@ def _process_node(self, base_node: NodeBase):
437439 yield from self ._register_notebook (base_node )
438440 elif isinstance (base_node , ImportSource ):
439441 yield from self ._register_import (base_node )
440- elif isinstance (base_node , MagicCommand ):
442+ elif isinstance (base_node , MagicLine ):
441443 yield from base_node .build_dependency_graph (self ._context .parent )
442444 else :
443445 logger .warning (f"Can't process { NodeBase .__name__ } of type { type (base_node ).__name__ } " )
@@ -466,23 +468,20 @@ def _mutate_path_lookup(self, change: SysPathChange):
466468 f"Can't update sys.path from { change .node .as_string ()} because the expression cannot be computed" ,
467469 )
468470 return
469- path = Path (change .path )
470- if not path .is_absolute ():
471- path = self ._context .path_lookup .cwd / path
472- if change .is_append :
473- self ._context .path_lookup .append_path (path )
474- return
475- self ._context .path_lookup .prepend_path (path )
471+ change .apply_to (self ._context .path_lookup )
472+
476473
474+ T = TypeVar ("T" )
477475
478- class MagicCommand (NodeBase ):
476+
477+ class MagicLine (NodeBase ):
479478
480479 @classmethod
481480 def extract_from_tree (
482- cls , tree : Tree , problem_factory : Callable [[str , str , NodeNG ], DependencyProblem ]
483- ) -> tuple [list [MagicCommand ], list [DependencyProblem ]]:
484- problems : list [DependencyProblem ] = []
485- commands : list [MagicCommand ] = []
481+ cls , tree : Tree , problem_factory : Callable [[str , str , NodeNG ], T ]
482+ ) -> tuple [list [MagicLine ], list [T ]]:
483+ problems : list [T ] = []
484+ commands : list [MagicLine ] = []
486485 try :
487486 nodes = tree .locate (Call , [("magic_command" , Name )])
488487 for command in cls ._make_commands_for_magic_command_call_nodes (nodes ):
@@ -498,36 +497,82 @@ def _make_commands_for_magic_command_call_nodes(cls, nodes: list[Call]):
498497 for node in nodes :
499498 arg = node .args [0 ]
500499 if isinstance (arg , Const ):
501- yield MagicCommand (node , arg .value )
500+ yield MagicLine (node , arg .value )
502501
503502 def __init__ (self , node : NodeNG , command : bytes ):
504503 super ().__init__ (node )
505504 self ._command = command .decode ()
506505
507- def build_dependency_graph (self , graph : DependencyGraph ) -> list [ DependencyProblem ] :
506+ def as_magic (self ) -> MagicCommand | None :
508507 if self ._command .startswith ("%pip" ) or self ._command .startswith ("!pip" ):
509- cmd = PipMagic (self ._command )
510- return cmd .build_dependency_graph (graph )
508+ return PipCommand (self .node , self ._command )
509+ if self ._command .startswith ("%run" ):
510+ return RunCommand (self .node , self ._command )
511+ return None
512+
513+ def build_dependency_graph (self , graph : DependencyGraph ) -> list [DependencyProblem ]:
514+ magic = self .as_magic ()
515+ if magic is not None :
516+ return magic .build_dependency_graph (graph )
511517 problem = DependencyProblem .from_node (
512518 code = 'unsupported-magic-line' , message = f"magic line '{ self ._command } ' is not supported yet" , node = self .node
513519 )
514520 return [problem ]
515521
516522
517- class PipMagic :
523+ class MagicNode (NodeNG ):
524+ pass
518525
519- def __init__ (self , code : str ):
526+
527+ class MagicCommand (ABC ):
528+
529+ def __init__ (self , node : NodeNG , code : str ):
530+ self ._node = node
520531 self ._code = code
521532
533+ @abstractmethod
534+ def build_dependency_graph (self , graph : DependencyGraph ) -> list [DependencyProblem ]: ...
535+
536+
537+ class RunCommand (MagicCommand ):
538+
539+ def build_dependency_graph (self , graph : DependencyGraph ) -> list [DependencyProblem ]:
540+ path = self .notebook_path
541+ if path is not None :
542+ problems = graph .register_notebook (path )
543+ return [problem .from_node (problem .code , problem .message , self ._node ) for problem in problems ]
544+ problem = DependencyProblem .from_node ('invalid-run-cell' , "Missing notebook path in %run command" , self ._node )
545+ return [problem ]
546+
547+ @property
548+ def notebook_path (self ) -> Path | None :
549+ start = self ._code .find (' ' )
550+ if start < 0 :
551+ return None
552+ path = self ._code [start + 1 :].strip ().strip ('"' ).strip ("'" )
553+ return Path (path )
554+
555+
556+ class PipCommand (MagicCommand ):
557+
522558 def build_dependency_graph (self , graph : DependencyGraph ) -> list [DependencyProblem ]:
523559 argv = self ._split (self ._code )
524560 if len (argv ) == 1 :
525- return [DependencyProblem ("library-install-failed" , "Missing command after 'pip'" )]
561+ return [DependencyProblem . from_node ("library-install-failed" , "Missing command after 'pip'" , self . _node )]
526562 if argv [1 ] != "install" :
527- return [DependencyProblem ("library-install-failed" , f"Unsupported 'pip' command: { argv [1 ]} " )]
563+ return [
564+ DependencyProblem .from_node (
565+ "library-install-failed" , f"Unsupported 'pip' command: { argv [1 ]} " , self ._node
566+ )
567+ ]
528568 if len (argv ) == 2 :
529- return [DependencyProblem ("library-install-failed" , "Missing arguments after 'pip install'" )]
530- return graph .register_library (* argv [2 :]) # Skipping %pip install
569+ return [
570+ DependencyProblem .from_node (
571+ "library-install-failed" , "Missing arguments after 'pip install'" , self ._node
572+ )
573+ ]
574+ problems = graph .register_library (* argv [2 :]) # Skipping %pip install
575+ return [problem .from_node (problem .code , problem .message , self ._node ) for problem in problems ]
531576
532577 # Cache re-used regex (and ensure issues are raised during class init instead of upon first use).
533578 _splitter = re .compile (r"(?<!\\)\n" )
0 commit comments