1111# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
14+ import ast
1415import base64
1516import dataclasses
1617import gzip
4748SINGLE_OUTPUT_NAME = 'Output'
4849
4950
51+ def _detect_kubeflow_imports_in_function (func : Callable ) -> bool :
52+ """Detects if the function imports kubeflow package using AST parsing.
53+
54+ Args:
55+ func: The function to analyze for kubeflow imports.
56+
57+ Returns:
58+ bool: True if any kubeflow import is found, False otherwise.
59+
60+ Detects these import patterns:
61+ - import kubeflow
62+ - import kubeflow.training (any submodule)
63+ - from kubeflow import X
64+ - from kubeflow.submodule import X
65+ """
66+ try :
67+ # Get function source code
68+ source = inspect .getsource (func )
69+
70+ # Remove leading indentation to handle nested/indented functions
71+ source = textwrap .dedent (source )
72+
73+ # Parse the source into an AST
74+ tree = ast .parse (source )
75+
76+ for node in ast .walk (tree ):
77+ if isinstance (node , ast .Import ):
78+ # Handle "import kubeflow" or "import kubeflow.submodule"
79+ for alias in node .names :
80+ if alias .name == 'kubeflow' or alias .name .startswith (
81+ 'kubeflow.' ):
82+ return True
83+ elif isinstance (node , ast .ImportFrom ):
84+ # Handle "from kubeflow import X" or "from kubeflow.submodule import X"
85+ if node .module and (node .module == 'kubeflow' or
86+ node .module .startswith ('kubeflow.' )):
87+ return True
88+
89+ return False
90+
91+ except (OSError , TypeError , SyntaxError , AttributeError ):
92+ # Handle cases where:
93+ # - Source is not available (OSError)
94+ # - Function is built-in or C extension (TypeError)
95+ # - Source has syntax errors (SyntaxError)
96+ # - inspect module issues (AttributeError)
97+ return False
98+
99+
100+ def _parse_package_name (package_spec : str ) -> str :
101+ """Extract base package name from package specification.
102+
103+ Args:
104+ package_spec: Package specification like 'kubeflow==2.0.0', 'kubeflow>=1.5.0',
105+ 'kubeflow[extras]', 'git+https://github.com/kubeflow/sdk.git', etc.
106+
107+ Returns:
108+ str: The base package name without version/extras/operators.
109+
110+ Examples:
111+ 'kubeflow==2.0.0' -> 'kubeflow'
112+ 'kubeflow>=1.5.0' -> 'kubeflow'
113+ 'kubeflow[extras]' -> 'kubeflow'
114+ 'git+https://github.com/kubeflow/sdk.git' -> 'kubeflow' (special case for kubeflow detection)
115+ """
116+ # Handle VCS URLs (git+https://, git+ssh://, etc.)
117+ if '+' in package_spec and '://' in package_spec :
118+ # Special case: if it's a kubeflow URL, return 'kubeflow' for auto-detection logic
119+ if 'kubeflow' in package_spec .lower ():
120+ return 'kubeflow'
121+ # For other VCS URLs, extract package name from URL path
122+ # e.g., 'git+https://github.com/org/package.git' -> 'package'
123+ match = re .search (r'/([^/]+?)(?:\.git)?(?:[#@].*)?$' , package_spec )
124+ if match :
125+ return match .group (1 )
126+ return package_spec # fallback
127+
128+ # Handle standard package specs: package[extras]==version, package>=version, etc.
129+ package_name = package_spec
130+
131+ # Remove extras in brackets: package[extras] -> package
132+ if '[' in package_name :
133+ package_name = package_name .split ('[' )[0 ]
134+
135+ # Remove version operators: package>=1.0 -> package, package==2.0 -> package
136+ package_name = re .split (r'[<>=!~]' , package_name )[0 ]
137+
138+ # Remove any remaining whitespace
139+ return package_name .strip ()
140+
141+
50142@dataclasses .dataclass
51143class ComponentInfo ():
52144 """A dataclass capturing registered components.
@@ -152,15 +244,32 @@ def make_pip_install_command(
152244
153245
154246def _get_packages_to_install_command (
247+ func : Optional [Callable ] = None ,
155248 kfp_package_path : Optional [str ] = None ,
156249 pip_index_urls : Optional [List [str ]] = None ,
157250 packages_to_install : Optional [List [str ]] = None ,
158251 install_kfp_package : bool = True ,
252+ install_kubeflow_package : bool = True ,
159253 target_image : Optional [str ] = None ,
160254 pip_trusted_hosts : Optional [List [str ]] = None ,
161255 use_venv : bool = False ,
162256) -> List [str ]:
163257 packages_to_install = packages_to_install or []
258+
259+ # Auto-detect and add kubeflow if needed
260+ if install_kubeflow_package and func is not None :
261+ detected_kubeflow = _detect_kubeflow_imports_in_function (func )
262+
263+ if detected_kubeflow :
264+ # Parse existing packages to check for kubeflow
265+ existing_package_names = [
266+ _parse_package_name (pkg ) for pkg in packages_to_install
267+ ]
268+
269+ # Only add if not already specified
270+ if 'kubeflow' not in existing_package_names :
271+ packages_to_install .append ('kubeflow' )
272+
164273 kfp_in_user_pkgs = any (pkg .startswith ('kfp' ) for pkg in packages_to_install )
165274 # if the user doesn't say "don't install", they aren't building a
166275 # container component, and they haven't already specified a KFP dep
@@ -645,6 +754,7 @@ def create_notebook_component_from_func(
645754 pip_index_urls : Optional [List [str ]] = None ,
646755 output_component_file : Optional [str ] = None ,
647756 install_kfp_package : bool = True ,
757+ install_kubeflow_package : bool = True ,
648758 kfp_package_path : Optional [str ] = None ,
649759 pip_trusted_hosts : Optional [List [str ]] = None ,
650760 use_venv : bool = False ,
@@ -709,6 +819,7 @@ def create_notebook_component_from_func(
709819 pip_index_urls = pip_index_urls ,
710820 output_component_file = output_component_file ,
711821 install_kfp_package = install_kfp_package ,
822+ install_kubeflow_package = install_kubeflow_package ,
712823 kfp_package_path = kfp_package_path ,
713824 pip_trusted_hosts = pip_trusted_hosts ,
714825 use_venv = use_venv ,
@@ -738,6 +849,7 @@ def create_component_from_func(
738849 pip_index_urls : Optional [List [str ]] = None ,
739850 output_component_file : Optional [str ] = None ,
740851 install_kfp_package : bool = True ,
852+ install_kubeflow_package : bool = True ,
741853 kfp_package_path : Optional [str ] = None ,
742854 pip_trusted_hosts : Optional [List [str ]] = None ,
743855 use_venv : bool = False ,
@@ -752,7 +864,9 @@ def create_component_from_func(
752864 """
753865
754866 packages_to_install_command = _get_packages_to_install_command (
867+ func = func ,
755868 install_kfp_package = install_kfp_package ,
869+ install_kubeflow_package = install_kubeflow_package ,
756870 target_image = target_image ,
757871 kfp_package_path = kfp_package_path ,
758872 packages_to_install = packages_to_install ,
0 commit comments