@@ -1676,6 +1676,7 @@ def load_flow_from_entrypoint(
1676
1676
if ":" in entrypoint :
1677
1677
# split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
1678
1678
path , func_name = entrypoint .rsplit (":" , maxsplit = 1 )
1679
+
1679
1680
else :
1680
1681
path , func_name = entrypoint .rsplit ("." , maxsplit = 1 )
1681
1682
try :
@@ -1684,15 +1685,13 @@ def load_flow_from_entrypoint(
1684
1685
raise MissingFlowError (
1685
1686
f"Flow function with name { func_name !r} not found in { path !r} . "
1686
1687
) from exc
1687
- except ScriptError as exc :
1688
+ except ScriptError :
1688
1689
# If the flow has dependencies that are not installed in the current
1689
- # environment, fallback to loading the flow via AST parsing. The
1690
- # drawback of this approach is that we're unable to actually load the
1691
- # function, so we create a placeholder flow that will re-raise this
1692
- # exception when called.
1693
-
1690
+ # environment, fallback to loading the flow via AST parsing.
1694
1691
if use_placeholder_flow :
1695
- flow = load_placeholder_flow (entrypoint = entrypoint , raises = exc )
1692
+ flow = safe_load_flow_from_entrypoint (entrypoint )
1693
+ if flow is None :
1694
+ raise
1696
1695
else :
1697
1696
raise
1698
1697
@@ -1855,6 +1854,147 @@ async def async_placeholder_flow(*args, **kwargs):
1855
1854
return Flow (** arguments )
1856
1855
1857
1856
1857
+ def safe_load_flow_from_entrypoint (entrypoint : str ) -> Optional [Flow ]:
1858
+ """
1859
+ Load a flow from an entrypoint and return None if an exception is raised.
1860
+
1861
+ Args:
1862
+ entrypoint: a string in the format `<path_to_script>:<flow_func_name>`
1863
+ or a module path to a flow function
1864
+ """
1865
+ func_def , source_code = _entrypoint_definition_and_source (entrypoint )
1866
+ path = None
1867
+ if ":" in entrypoint :
1868
+ path = entrypoint .rsplit (":" )[0 ]
1869
+ namespace = safe_load_namespace (source_code , filepath = path )
1870
+ if func_def .name in namespace :
1871
+ return namespace [func_def .name ]
1872
+ else :
1873
+ # If the function is not in the namespace, if may be due to missing dependencies
1874
+ # for the function. We will attempt to compile each annotation and default value
1875
+ # and remove them from the function definition to see if the function can be
1876
+ # compiled without them.
1877
+
1878
+ return _sanitize_and_load_flow (func_def , namespace )
1879
+
1880
+
1881
+ def _sanitize_and_load_flow (
1882
+ func_def : Union [ast .FunctionDef , ast .AsyncFunctionDef ], namespace : Dict [str , Any ]
1883
+ ) -> Optional [Flow ]:
1884
+ """
1885
+ Attempt to load a flow from the function definition after sanitizing the annotations
1886
+ and defaults that can't be compiled.
1887
+
1888
+ Args:
1889
+ func_def: the function definition
1890
+ namespace: the namespace to load the function into
1891
+
1892
+ Returns:
1893
+ The loaded function or None if the function can't be loaded
1894
+ after sanitizing the annotations and defaults.
1895
+ """
1896
+ args = func_def .args .posonlyargs + func_def .args .args + func_def .args .kwonlyargs
1897
+ if func_def .args .vararg :
1898
+ args .append (func_def .args .vararg )
1899
+ if func_def .args .kwarg :
1900
+ args .append (func_def .args .kwarg )
1901
+ # Remove annotations that can't be compiled
1902
+ for arg in args :
1903
+ if arg .annotation is not None :
1904
+ try :
1905
+ code = compile (
1906
+ ast .Expression (arg .annotation ),
1907
+ filename = "<ast>" ,
1908
+ mode = "eval" ,
1909
+ )
1910
+ exec (code , namespace )
1911
+ except Exception as e :
1912
+ logger .debug (
1913
+ "Failed to evaluate annotation for argument %s due to the following error. Ignoring annotation." ,
1914
+ arg .arg ,
1915
+ exc_info = e ,
1916
+ )
1917
+ arg .annotation = None
1918
+
1919
+ # Remove defaults that can't be compiled
1920
+ new_defaults = []
1921
+ for default in func_def .args .defaults :
1922
+ try :
1923
+ code = compile (ast .Expression (default ), "<ast>" , "eval" )
1924
+ exec (code , namespace )
1925
+ new_defaults .append (default )
1926
+ except Exception as e :
1927
+ logger .debug (
1928
+ "Failed to evaluate default value %s due to the following error. Ignoring default." ,
1929
+ default ,
1930
+ exc_info = e ,
1931
+ )
1932
+ new_defaults .append (
1933
+ ast .Constant (
1934
+ value = None , lineno = default .lineno , col_offset = default .col_offset
1935
+ )
1936
+ )
1937
+ func_def .args .defaults = new_defaults
1938
+
1939
+ # Remove kw_defaults that can't be compiled
1940
+ new_kw_defaults = []
1941
+ for default in func_def .args .kw_defaults :
1942
+ if default is not None :
1943
+ try :
1944
+ code = compile (ast .Expression (default ), "<ast>" , "eval" )
1945
+ exec (code , namespace )
1946
+ new_kw_defaults .append (default )
1947
+ except Exception as e :
1948
+ logger .debug (
1949
+ "Failed to evaluate default value %s due to the following error. Ignoring default." ,
1950
+ default ,
1951
+ exc_info = e ,
1952
+ )
1953
+ new_kw_defaults .append (
1954
+ ast .Constant (
1955
+ value = None ,
1956
+ lineno = default .lineno ,
1957
+ col_offset = default .col_offset ,
1958
+ )
1959
+ )
1960
+ else :
1961
+ new_kw_defaults .append (
1962
+ ast .Constant (
1963
+ value = None ,
1964
+ lineno = func_def .lineno ,
1965
+ col_offset = func_def .col_offset ,
1966
+ )
1967
+ )
1968
+ func_def .args .kw_defaults = new_kw_defaults
1969
+
1970
+ if func_def .returns is not None :
1971
+ try :
1972
+ code = compile (
1973
+ ast .Expression (func_def .returns ), filename = "<ast>" , mode = "eval"
1974
+ )
1975
+ exec (code , namespace )
1976
+ except Exception as e :
1977
+ logger .debug (
1978
+ "Failed to evaluate return annotation due to the following error. Ignoring annotation." ,
1979
+ exc_info = e ,
1980
+ )
1981
+ func_def .returns = None
1982
+
1983
+ # Attempt to compile the function without annotations and defaults that
1984
+ # can't be compiled
1985
+ try :
1986
+ code = compile (
1987
+ ast .Module (body = [func_def ], type_ignores = []),
1988
+ filename = "<ast>" ,
1989
+ mode = "exec" ,
1990
+ )
1991
+ exec (code , namespace )
1992
+ except Exception as e :
1993
+ logger .debug ("Failed to compile: %s" , e )
1994
+ else :
1995
+ return namespace .get (func_def .name )
1996
+
1997
+
1858
1998
def load_flow_arguments_from_entrypoint (
1859
1999
entrypoint : str , arguments : Optional [Union [List [str ], Set [str ]]] = None
1860
2000
) -> Dict [str , Any ]:
@@ -1870,6 +2010,9 @@ def load_flow_arguments_from_entrypoint(
1870
2010
"""
1871
2011
1872
2012
func_def , source_code = _entrypoint_definition_and_source (entrypoint )
2013
+ path = None
2014
+ if ":" in entrypoint :
2015
+ path = entrypoint .rsplit (":" )[0 ]
1873
2016
1874
2017
if arguments is None :
1875
2018
# If no arguments are provided default to known arguments that are of
@@ -1905,7 +2048,7 @@ def load_flow_arguments_from_entrypoint(
1905
2048
1906
2049
# if the arg value is not a raw str (i.e. a variable or expression),
1907
2050
# then attempt to evaluate it
1908
- namespace = safe_load_namespace (source_code )
2051
+ namespace = safe_load_namespace (source_code , filepath = path )
1909
2052
literal_arg_value = ast .get_source_segment (source_code , keyword .value )
1910
2053
cleaned_value = (
1911
2054
literal_arg_value .replace ("\n " , "" ) if literal_arg_value else ""
0 commit comments