|
| 1 | +# Databricks CLI |
| 2 | +# Copyright 2017 Databricks, Inc. |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"), except |
| 5 | +# that the use of services to which certain application programming |
| 6 | +# interfaces (each, an "API") connect requires that the user first obtain |
| 7 | +# a license for the use of the APIs from Databricks, Inc. ("Databricks"), |
| 8 | +# by creating an account at www.databricks.com and agreeing to either (a) |
| 9 | +# the Community Edition Terms of Service, (b) the Databricks Terms of |
| 10 | +# Service, or (c) another written agreement between Licensee and Databricks |
| 11 | +# for the use of the APIs. |
| 12 | +# |
| 13 | +# You may not use this file except in compliance with the License. |
| 14 | +# You may obtain a copy of the License at |
| 15 | +# |
| 16 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 17 | +# |
| 18 | +# Unless required by applicable law or agreed to in writing, software |
| 19 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 20 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 21 | +# See the License for the specific language governing permissions and |
| 22 | +# limitations under the License. |
| 23 | + |
| 24 | +import importlib |
| 25 | +import inspect |
| 26 | +import json |
| 27 | +import os |
| 28 | +import re |
| 29 | +import sys |
| 30 | +import tempfile |
| 31 | +import unittest |
| 32 | + |
| 33 | +import six |
| 34 | + |
| 35 | + |
| 36 | +def normalize_module_name(name): |
| 37 | + """ |
| 38 | + Replace `databricks_cli_main` with `databricks_cli` to make objects comparable. |
| 39 | + """ |
| 40 | + return re.sub(r"^(databricks_cli)_main", r"\1", name) |
| 41 | + |
| 42 | + |
| 43 | +def func_key(func): |
| 44 | + """ |
| 45 | + Returns key of the full package path to the specified function. |
| 46 | + """ |
| 47 | + return normalize_module_name(func.__module__) + "." + func.__qualname__ |
| 48 | + |
| 49 | + |
| 50 | +def collect_argspecs(modules): |
| 51 | + return { |
| 52 | + func_key(func): inspect.getfullargspec(func).args |
| 53 | + for module in modules |
| 54 | + for (_, clazz) in inspect.getmembers(module, predicate=inspect.isclass) |
| 55 | + for (_, func) in inspect.getmembers(clazz, predicate=inspect.isfunction) |
| 56 | + # Ignore functions that are defined outside the specified module. |
| 57 | + if module.__name__ == func.__module__ |
| 58 | + } |
| 59 | + |
| 60 | + |
| 61 | +# pylint: disable=superfluous-parens |
| 62 | +def _test_compatibility(current_argspecs, existing_argspecs): |
| 63 | + removed_functions = [] |
| 64 | + incompatible_functions = [] |
| 65 | + for existing_name, existing_args in existing_argspecs.items(): |
| 66 | + if existing_name not in current_argspecs: |
| 67 | + removed_functions.append(existing_name) |
| 68 | + continue |
| 69 | + |
| 70 | + # New arguments may only be appended to ensure compatibility |
| 71 | + # when functions are called with positional parameters. |
| 72 | + current_args = current_argspecs[existing_name] |
| 73 | + if ( |
| 74 | + len(current_args) < len(existing_args) |
| 75 | + or current_args[0 : len(existing_args)] != existing_args |
| 76 | + ): |
| 77 | + incompatible_functions.append(existing_name) |
| 78 | + continue |
| 79 | + |
| 80 | + if removed_functions: |
| 81 | + print("Removed functions:") |
| 82 | + for fn in removed_functions: |
| 83 | + print(" * " + fn) |
| 84 | + assert len(removed_functions) == 0 |
| 85 | + |
| 86 | + if incompatible_functions: |
| 87 | + print("Incompatible functions:") |
| 88 | + for fn in incompatible_functions: |
| 89 | + current_args = current_argspecs[fn] |
| 90 | + existing_args = existing_argspecs[fn] |
| 91 | + print(" * " + fn + ": " + str(existing_args) + " -> " + str(current_args)) |
| 92 | + assert len(incompatible_functions) == 0 |
| 93 | + |
| 94 | + |
| 95 | +def import_databricks_modules(root): |
| 96 | + return [ |
| 97 | + # Generated code. |
| 98 | + importlib.import_module(".sdk.service", root), |
| 99 | + # Functionality under the API package is used as an SDK by some. |
| 100 | + importlib.import_module(".cluster_policies.api", root), |
| 101 | + importlib.import_module(".clusters.api", root), |
| 102 | + importlib.import_module(".dbfs.api", root), |
| 103 | + importlib.import_module(".groups.api", root), |
| 104 | + importlib.import_module(".instance_pools.api", root), |
| 105 | + importlib.import_module(".jobs.api", root), |
| 106 | + importlib.import_module(".libraries.api", root), |
| 107 | + importlib.import_module(".pipelines.api", root), |
| 108 | + importlib.import_module(".repos.api", root), |
| 109 | + importlib.import_module(".runs.api", root), |
| 110 | + importlib.import_module(".secrets.api", root), |
| 111 | + importlib.import_module(".tokens.api", root), |
| 112 | + importlib.import_module(".unity_catalog.api", root), |
| 113 | + importlib.import_module(".workspace.api", root), |
| 114 | + ] |
| 115 | + |
| 116 | + |
| 117 | +def databricks_cli_main_checkout_path(): |
| 118 | + return os.getenv('DATABRICKS_CLI_MAIN_CHECKOUT') |
| 119 | + |
| 120 | + |
| 121 | +def can_perform_compatibility_check(): |
| 122 | + """ |
| 123 | + Checks if the user has specified a path to a parallel checkout |
| 124 | + of the main branch of the databricks-cli repository. |
| 125 | +
|
| 126 | + The files in this checkout will be imported under an aliased |
| 127 | + package name such that we have access to the argument specs |
| 128 | + of the functions in both the main branch and this repository. |
| 129 | + """ |
| 130 | + path = databricks_cli_main_checkout_path() |
| 131 | + return path and os.path.isdir(path) |
| 132 | + |
| 133 | + |
| 134 | +@unittest.skipIf(six.PY2 or not can_perform_compatibility_check(), reason=None) |
| 135 | +def test_compatibility(): |
| 136 | + """ |
| 137 | + To run this test, checkout a reference copy of this repository and set |
| 138 | + the DATABRICKS_CLI_MAIN_CHECKOUT environment variable to its path. |
| 139 | +
|
| 140 | + See `.github/workflows/` for the configuration of the GitHub action that runs it. |
| 141 | + """ |
| 142 | + |
| 143 | + # Make the specified path importable by symlinking its `databricks_cli` |
| 144 | + # directory |
| 145 | + tmpdir = tempfile.mkdtemp() |
| 146 | + sys.path.append(tmpdir) |
| 147 | + os.symlink( |
| 148 | + os.path.join(databricks_cli_main_checkout_path(), "databricks_cli"), |
| 149 | + os.path.join(tmpdir, "databricks_cli_main"), |
| 150 | + ) |
| 151 | + |
| 152 | + current_argspecs = collect_argspecs(import_databricks_modules("databricks_cli")) |
| 153 | + existing_argspecs = collect_argspecs(import_databricks_modules("databricks_cli_main")) |
| 154 | + _test_compatibility(current_argspecs, existing_argspecs) |
| 155 | + |
| 156 | + |
| 157 | +if __name__ == '__main__': |
| 158 | + # If run directly, write the argspecs in this repository to stdout. |
| 159 | + json.dump( |
| 160 | + obj=collect_argspecs(import_databricks_modules("databricks_cli")), |
| 161 | + fp=sys.stdout, |
| 162 | + indent=2, |
| 163 | + ) |
0 commit comments