Skip to content

Commit d533e0a

Browse files
committed
BUG#29928485 PYTHON PLUGINS: FAILS LOADING PLUGINS HAVING INNER PACKAGE WITH THE SAME NAME
This bug describes an unexpected behavior when two plugins contain an inner package with the same name, it results in the second plugin not being loaded because the definitions on the affected package are not available. This is due to a name clashing on package names caused because the plugin logic adds both the plugin path (and in the case of plugin groups, the parent plugin path) into sys path, allowing the inner packages to be imported using import <package_name>. This patch fixes this problem by replacing the paths to the plugin/plugin group with the plugins folder from which the plugins are being loaded. As a consecuense the following restrictions are added when defining plugins with inner packages or shared code: - The main plugin folder must be a valid package name according to python standards (PEP-8) - Each folder to be considered a package must contain a file named __init__.py The imports will have to be done with fully specified package names, i.e. from <plugin_name>[.<package_name>]* import <target_import> This resolves the package name clashing and so the reported bug. Change-Id: I9bcc29a237839556d2a03527fe23c657250c30c1
1 parent 233dfea commit d533e0a

File tree

2 files changed

+40
-42
lines changed

2 files changed

+40
-42
lines changed

mysqlshdk/scripting/python_context.cc

+9-20
Original file line numberDiff line numberDiff line change
@@ -1145,13 +1145,19 @@ bool Python_context::load_plugin(const Plugin_definition &plugin) {
11451145

11461146
PyObject *plugin_file = nullptr;
11471147
PyObject *plugin_path = nullptr;
1148-
PyObject *parent_plugin_path = nullptr;
11491148
PyObject *result = nullptr;
11501149
PyObject *globals = nullptr;
11511150
PyObject *sys_path_backup = nullptr;
11521151
FILE *file = nullptr;
11531152
bool executed = false;
11541153
std::string file_name = plugin.file;
1154+
1155+
// The path to the plugins folder needs to be in sys.path while the plugin is
1156+
// being loaded
1157+
std::string plugin_dir = shcore::path::dirname(file_name);
1158+
if (!plugin.main) plugin_dir = shcore::path::dirname(plugin_dir);
1159+
plugin_dir = shcore::path::dirname(plugin_dir);
1160+
11551161
try {
11561162
file = fopen(file_name.c_str(), "r");
11571163
if (nullptr == file) {
@@ -1164,21 +1170,11 @@ bool Python_context::load_plugin(const Plugin_definition &plugin) {
11641170
throw std::runtime_error("Unable to get plugin file");
11651171
}
11661172

1167-
std::string plugin_dir = shcore::path::dirname(file_name);
11681173
plugin_path = PyString_FromString(plugin_dir.c_str());
11691174
if (nullptr == plugin_path) {
11701175
throw std::runtime_error("Unable to get plugin path");
11711176
}
11721177

1173-
std::string parent_plugin_dir;
1174-
if (!plugin.main) {
1175-
parent_plugin_dir = shcore::path::dirname(plugin_dir);
1176-
parent_plugin_path = PyString_FromString(parent_plugin_dir.c_str());
1177-
if (nullptr == parent_plugin_path) {
1178-
throw std::runtime_error("Unable to get the parent plugin path");
1179-
}
1180-
}
1181-
11821178
PyObject *sys_path = PySys_GetObject(const_cast<char *>("path"));
11831179
if (nullptr == sys_path) {
11841180
throw std::runtime_error("Failed getting sys.path");
@@ -1205,13 +1201,6 @@ bool Python_context::load_plugin(const Plugin_definition &plugin) {
12051201
throw std::runtime_error("Failed adding plugin path to sys.path");
12061202
}
12071203

1208-
if (!plugin.main) {
1209-
if (PyList_Insert(sys_path, 0, parent_plugin_path)) {
1210-
throw std::runtime_error(
1211-
"Failed adding parent plugin path to sys.path");
1212-
}
1213-
}
1214-
12151204
// PyRun_FileEx() will close the file
12161205
// plugins are loaded in sandboxed environment, we're not using
12171206
// the compiler flags here
@@ -1220,8 +1209,8 @@ bool Python_context::load_plugin(const Plugin_definition &plugin) {
12201209

12211210
executed = true;
12221211

1223-
// In case of an error, gets the description and clears the error condition
1224-
// in python to be able to restore the sys.path
1212+
// In case of an error, gets the description and clears the error
1213+
// condition in python to be able to restore the sys.path
12251214
std::string error;
12261215
if (nullptr == result) {
12271216
error = fetch_and_clear_exception();

unittest/mysqlsh_plugins_t.cc

+31-22
Original file line numberDiff line numberDiff line change
@@ -720,15 +720,15 @@ shell.register_report('first_py', 'print', report);
720720
// in subfolders, while only the function registration resides
721721
// in the init.py file
722722
TEST_F(Mysqlsh_plugin_test, py_plugin_with_imports) {
723-
// Creates complex-py plugin initialization file
724-
write_user_plugin("complex-py", R"(
723+
// Creates complex_py plugin initialization file
724+
write_user_plugin("complex_py", R"(
725725
print("Plugin File Path: {0}".format(__file__))
726726
727727
# loads a function definition from src package
728-
from src import definition
728+
from complex_py.src import definition
729729
730730
# loads a sibling script
731-
import sibling
731+
from complex_py import sibling
732732
733733
734734
# Executes the plugin registration
@@ -739,23 +739,28 @@ shell.register_global('pyObject', obj);
739739
)", ".py");
740740

741741
auto user_plugins = get_user_plugin_folder();
742-
auto src_package = join_path(user_plugins, "complex-py", "src");
742+
auto main_package = join_path(user_plugins, "complex_py");
743+
auto src_package = join_path(main_package, "src");
744+
745+
// Creates __init__.py to make the main plugin a package
746+
shcore::create_file(join_path(main_package, "__init__.py"), "");
747+
743748
// Creates a sibling script with a function definition
744-
shcore::create_file(join_path(user_plugins, "complex-py", "sibling.py"),
749+
shcore::create_file(join_path(main_package, "sibling.py"),
745750
R"(def my_function():
746-
print("Function at complex-py/sibling.py"))");
751+
print("Function at complex_py/sibling.py"))");
747752

748753
// Creates the src sub-package with a function definition
749754
shcore::create_directory(src_package);
750755
shcore::create_file(join_path(src_package, "__init__.py"), "");
751756
shcore::create_file(join_path(src_package, "definition.py"),
752757
R"(def my_function():
753-
print("Function at complex-py/src/definition.py"))");
758+
print("Function at complex_py/src/definition.py"))");
754759

755760
add_py_test("pyObject.test_package()",
756-
"Function at complex-py/src/definition.py");
761+
"Function at complex_py/src/definition.py");
757762

758-
add_py_test("pyObject.test_sibling()", "Function at complex-py/sibling.py");
763+
add_py_test("pyObject.test_sibling()", "Function at complex_py/sibling.py");
759764

760765
add_expected_py_log(
761766
"The 'test_package' function has been registered into an unregistered "
@@ -774,36 +779,40 @@ shell.register_global('pyObject', obj);
774779
// check the output
775780
MY_EXPECT_CMD_OUTPUT_NOT_CONTAINS("WARNING: Found errors loading plugins");
776781
std::string expected("Plugin File Path: ");
777-
expected += join_path(get_user_plugin_folder(), "complex-py", "init.py");
782+
expected += join_path(get_user_plugin_folder(), "complex_py", "init.py");
778783
MY_EXPECT_CMD_OUTPUT_CONTAINS(expected.c_str());
779784
MY_EXPECT_CMD_OUTPUT_CONTAINS(expected_output().c_str());
780785

781786
validate_log();
782787

783788
wipe_out();
784789

785-
delete_user_plugin("complex-py");
790+
delete_user_plugin("complex_py");
786791
}
787792

788793
// This test emulates a folder in plugins that contains subfolders
789794
// with plugins
790795
TEST_F(Mysqlsh_plugin_test, py_multi_plugins) {
791-
// Creates complex-py plugin initialization file
796+
// Creates multi_plugins plugin initialization file
792797
auto user_plugins = get_user_plugin_folder();
793-
auto multi_plugins = join_path(user_plugins, "multi-plugins");
794-
shcore::create_directory(multi_plugins);
795-
shcore::create_file(join_path(multi_plugins, "plugin_common.py"),
798+
auto main_package = join_path(user_plugins, "multi_plugins");
799+
800+
// Creates __init__.py to make the main plugin a package
801+
shcore::create_directory(main_package);
802+
shcore::create_file(join_path(main_package, "__init__.py"), "");
803+
804+
shcore::create_file(join_path(main_package, "plugin_common.py"),
796805
R"(import mysqlsh
797806
798807
def global_function(caller):
799808
print("Global function called from {0}".format(caller)))");
800809

801-
auto create_plugin = [multi_plugins](const std::string &name,
802-
const std::string &greeting) {
803-
auto plugin = join_path(multi_plugins, name);
810+
auto create_plugin = [main_package](const std::string &name,
811+
const std::string &greeting) {
812+
auto plugin = join_path(main_package, name);
804813
shcore::create_directory(plugin);
805814
std::string code =
806-
R"(import plugin_common
815+
R"(from multi_plugins import plugin_common
807816
808817
# The implementation of the function to be added to demo
809818
def hello_function():
@@ -893,13 +902,13 @@ shell.add_extension_object_member(global_obj, "common_%s", global_function)
893902

894903
wipe_out();
895904

896-
delete_user_plugin("multi-plugins");
905+
delete_user_plugin("multi_plugins");
897906
}
898907

899908
#ifdef WITH_OCI
900909
// This test emulates a plugins that uses the oci
901910
TEST_F(Mysqlsh_plugin_test, oci_and_paramiko_plugin) {
902-
// Creates complex-py plugin initialization file
911+
// Creates oci-paramiko plugin initialization file
903912
write_user_plugin("oci-paramiko", R"(import oci
904913
import paramiko
905914

0 commit comments

Comments
 (0)