diff --git a/docs/advanced/cast/stl.rst b/docs/advanced/cast/stl.rst index 23e67516b5..ecd889ffb0 100644 --- a/docs/advanced/cast/stl.rst +++ b/docs/advanced/cast/stl.rst @@ -150,10 +150,10 @@ the declaration before any binding code (e.g. invocations to ``class_::def()``, etc.). This macro must be specified at the top level (and outside of any namespaces), since it instantiates a partial template overload. If your binding code consists of -multiple compilation units, it must be present in every file preceding any -usage of ``std::vector``. Opaque types must also have a corresponding -``class_`` declaration to associate them with a name in Python, and to define a -set of available operations, e.g.: +multiple compilation units, it must be present in every file (typically via a +common header) preceding any usage of ``std::vector``. Opaque types must +also have a corresponding ``class_`` declaration to associate them with a name +in Python, and to define a set of available operations, e.g.: .. code-block:: cpp @@ -167,6 +167,20 @@ set of available operations, e.g.: }, py::keep_alive<0, 1>()) /* Keep vector alive while iterator is used */ // .... +Please take a look at the :ref:`macro_notes` before using the +``PYBIND11_MAKE_OPAQUE`` macro. + +.. seealso:: + + The file :file:`tests/test_opaque_types.cpp` contains a complete + example that demonstrates how to create and expose opaque types using + pybind11 in more detail. + +.. _stl_bind: + +Binding STL containers +====================== + The ability to expose STL containers as native Python objects is a fairly common request, hence pybind11 also provides an optional header file named :file:`pybind11/stl_bind.h` that does exactly this. The mapped containers try @@ -188,14 +202,34 @@ The following example showcases usage of :file:`pybind11/stl_bind.h`: py::bind_vector>(m, "VectorInt"); py::bind_map>(m, "MapStringDouble"); -Please take a look at the :ref:`macro_notes` before using the -``PYBIND11_MAKE_OPAQUE`` macro. +When binding STL containers pybind11 considers the types of the container's +elements to decide whether the container should be confined to the local module +(via the :ref:`module_local` feature). If the container element types are +anything other than already-bound custom types bound without +``py::module_local()`` the container binding will have ``py::module_local()`` +applied. This includes converting types such as numeric types, strings, Eigen +types; and types that have not yet been bound at the time of the stl container +binding. This module-local binding is designed to avoid potential conflicts +between module bindings (for example, from two separate modules each attempting +to bind ``std::vector`` as a python type). + +It is possible to override this behavior to force a definition to be either +module-local or global. To do so, you can pass the attributes +``py::module_local()`` (to make the binding module-local) or +``py::module_local(false)`` (to make the binding global) into the +``py::bind_vector`` or ``py::bind_map`` arguments: -.. seealso:: +.. code-block:: cpp - The file :file:`tests/test_opaque_types.cpp` contains a complete - example that demonstrates how to create and expose opaque types using - pybind11 in more detail. + py::bind_vector>(m, "VectorInt", py::module_local(false)); + +Note, however, that such a global binding would make it impossible to load this +module at the same time as any other pybind module that also attempts to bind +the same container type (``std::vector`` in the above example). + +See :ref:`module_local` for more details on module-local bindings. + +.. seealso:: The file :file:`tests/test_stl_binders.cpp` shows how to use the convenience STL container wrappers. diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 87bbe2427f..71a7a88590 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -635,3 +635,139 @@ inheritance, which can lead to undefined behavior. In such cases, add the tag The tag is redundant and does not need to be specified when multiple base types are listed. + +.. _module_local: + +Module-local class bindings +=========================== + +When creating a binding for a class, pybind by default makes that binding +"global" across modules. What this means is that a type defined in one module +can be passed to functions of other modules that expect the same C++ type. For +example, this allows the following: + +.. code-block:: cpp + + // In the module1.cpp binding code for module1: + py::class_(m, "Pet") + .def(py::init()); + +.. code-block:: cpp + + // In the module2.cpp binding code for module2: + m.def("pet_name", [](Pet &p) { return p.name(); }); + +.. code-block:: pycon + + >>> from module1 import Pet + >>> from module2 import pet_name + >>> mypet = Pet("Kitty") + >>> pet_name(mypet) + 'Kitty' + +When writing binding code for a library, this is usually desirable: this +allows, for example, splitting up a complex library into multiple Python +modules. + +In some cases, however, this can cause conflicts. For example, suppose two +unrelated modules make use of an external C++ library and each provide custom +bindings for one of that library's classes. This will result in an error when +a Python program attempts to import both modules (directly or indirectly) +because of conflicting definitions on the external type: + +.. code-block:: cpp + + // dogs.cpp + + // Binding for external library class: + py::class(m, "Pet") + .def("name", &pets::Pet::name); + + // Binding for local extension class: + py::class(m, "Dog") + .def(py::init()); + +.. code-block:: cpp + + // cats.cpp, in a completely separate project from the above dogs.cpp. + + // Binding for external library class: + py::class(m, "Pet") + .def("get_name", &pets::Pet::name); + + // Binding for local extending class: + py::class(m, "Cat") + .def(py::init()); + +.. code-block:: pycon + + >>> import cats + >>> import dogs + Traceback (most recent call last): + File "", line 1, in + ImportError: generic_type: type "Pet" is already registered! + +To get around this, you can tell pybind11 to keep the external class binding +localized to the module by passing the ``py::module_local()`` attribute into +the ``py::class_`` constructor: + +.. code-block:: cpp + + // Pet binding in dogs.cpp: + py::class(m, "Pet", py::module_local()) + .def("name", &pets::Pet::name); + +.. code-block:: cpp + + // Pet binding in cats.cpp: + py::class(m, "Pet", py::module_local()) + .def("get_name", &pets::Pet::name); + +This makes the Python-side ``dogs.Pet`` and ``cats.Pet`` into distinct classes +that can only be accepted as ``Pet`` arguments within those classes. This +avoids the conflict and allows both modules to be loaded. + +One limitation of this approach is that because ``py::module_local`` types are +distinct on the Python side, it is not possible to pass such a module-local +type as a C++ ``Pet``-taking function outside that module. For example, if the +above ``cats`` and ``dogs`` module are each extended with a function: + +.. code-block:: cpp + + m.def("petname", [](pets::Pet &p) { return p.name(); }); + +you will only be able to call the function with the local module's class: + +.. code-block:: pycon + + >>> import cats, dogs # No error because of the added py::module_local() + >>> mycat, mydog = cats.Cat("Fluffy"), dogs.Dog("Rover") + >>> (cats.petname(mycat), dogs.petname(mydog)) + ('Fluffy', 'Rover') + >>> cats.petname(mydog) + Traceback (most recent call last): + File "", line 1, in + TypeError: petname(): incompatible function arguments. The following argument types are supported: + 1. (arg0: cats.Pet) -> str + + Invoked with: + +.. note:: + + STL bindings (as provided via the optional :file:`pybind11/stl_bind.h` + header) apply ``py::module_local`` by default when the bound type might + conflict with other modules; see :ref:`stl_bind` for details. + +.. note:: + + The localization of the bound types is actually tied to the shared object + or binary generated by the compiler/linker. For typical modules created + with ``PYBIND11_MODULE()``, this distinction is not significant. It is + possible, however, when :ref:`embedding` to embed multiple modules in the + same binary (see :ref:`embedding_modules`). In such a case, the + localization will apply across all embedded modules within the same binary. + +.. seealso:: + + The file :file:`tests/test_local_bindings.cpp` contains additional examples + that demonstrate how ``py::module_local()`` works. diff --git a/docs/advanced/embedding.rst b/docs/advanced/embedding.rst index 5354eee9d2..bdfc75e0de 100644 --- a/docs/advanced/embedding.rst +++ b/docs/advanced/embedding.rst @@ -1,3 +1,5 @@ +.. _embedding: + Embedding the interpreter ######################### @@ -131,6 +133,7 @@ embedding the interpreter. This makes it easy to import local Python files: int n = result.cast(); assert(n == 3); +.. _embedding_modules: Adding embedded modules ======================= diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index b4137cb2bd..44a33458da 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -64,6 +64,9 @@ struct metaclass { explicit metaclass(handle value) : value(value) { } }; +/// Annotation that marks a class as local to the module: +struct module_local { const bool value; constexpr module_local(bool v = true) : value(v) { } }; + /// Annotation to mark enums as an arithmetic type struct arithmetic { }; @@ -196,7 +199,7 @@ struct function_record { /// Special data structure which (temporarily) holds metadata about a bound class struct type_record { PYBIND11_NOINLINE type_record() - : multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false) { } + : multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false), module_local(false) { } /// Handle to the parent scope handle scope; @@ -243,6 +246,9 @@ struct type_record { /// Is the default (unique_ptr) holder type used? bool default_holder : 1; + /// Is the class definition local to the module shared object? + bool module_local : 1; + PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *)) { auto base_info = detail::get_type_info(base, false); if (!base_info) { @@ -408,6 +414,10 @@ struct process_attribute : process_attribute_default { static void init(const metaclass &m, type_record *r) { r->metaclass = m.value; } }; +template <> +struct process_attribute : process_attribute_default { + static void init(const module_local &l, type_record *r) { r->module_local = l.value; } +}; /// Process an 'arithmetic' attribute for enums (does nothing here) template <> diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 5db03e2f7b..d7dcb6ef54 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -58,14 +58,14 @@ struct type_info { bool simple_ancestors : 1; /* for base vs derived holder_type checks */ bool default_holder : 1; + /* true if this is a type registered with py::module_local */ + bool module_local : 1; }; -// Store the static internals pointer in a version-specific function so that we're guaranteed it -// will be distinct for modules compiled for different pybind11 versions. Without this, some -// compilers (i.e. gcc) can use the same static pointer storage location across different .so's, -// even though the `get_internals()` function itself is local to each shared object. -template -internals *&get_internals_ptr() { static internals *internals_ptr = nullptr; return internals_ptr; } +PYBIND11_UNSHARED_STATIC_LOCALS PYBIND11_NOINLINE inline internals *&get_internals_ptr() { + static internals *internals_ptr = nullptr; + return internals_ptr; +} PYBIND11_NOINLINE inline internals &get_internals() { internals *&internals_ptr = get_internals_ptr(); @@ -75,6 +75,23 @@ PYBIND11_NOINLINE inline internals &get_internals() { const char *id = PYBIND11_INTERNALS_ID; if (builtins.contains(id) && isinstance(builtins[id])) { internals_ptr = *static_cast(capsule(builtins[id])); + + // We loaded builtins through python's builtins, which means that our error_already_set and + // builtin_exception may be different local classes than the ones set up in the initial + // exception translator, below, so add another for our local exception classes. + // + // stdlibc++ doesn't require this (types there are identified only by name) + #if !defined(__GLIBCXX__) + internals_ptr->registered_exception_translators.push_front( + [](std::exception_ptr p) -> void { + try { + if (p) std::rethrow_exception(p); + } catch (error_already_set &e) { e.restore(); return; + } catch (const builtin_exception &e) { e.set_error(); return; + } + } + ); + #endif } else { internals_ptr = new internals(); #if defined(WITH_THREAD) @@ -111,6 +128,12 @@ PYBIND11_NOINLINE inline internals &get_internals() { return *internals_ptr; } +// Works like internals.registered_types_cpp, but for module-local registered types: +PYBIND11_NOINLINE PYBIND11_UNSHARED_STATIC_LOCALS inline type_map ®istered_local_types_cpp() { + static type_map locals{}; + return locals; +} + /// A life support system for temporary objects created by `type_caster::load()`. /// Adding a patient will keep it alive up until the enclosing function returns. class loader_life_support { @@ -198,7 +221,7 @@ PYBIND11_NOINLINE inline void all_type_info_populate(PyTypeObject *t, std::vecto // registered types if (i + 1 == check.size()) { // When we're at the end, we can pop off the current element to avoid growing - // `check` when adding just one base (which is typical--.e. when there is no + // `check` when adding just one base (which is typical--i.e. when there is no // multiple inheritance) check.pop_back(); i--; @@ -242,13 +265,18 @@ PYBIND11_NOINLINE inline detail::type_info* get_type_info(PyTypeObject *type) { return bases.front(); } -PYBIND11_NOINLINE inline detail::type_info *get_type_info(const std::type_info &tp, +/// Return the type info for a given C++ type; on lookup failure can either throw or return nullptr. +PYBIND11_NOINLINE inline detail::type_info *get_type_info(const std::type_index &tp, bool throw_if_missing = false) { + std::type_index type_idx(tp); auto &types = get_internals().registered_types_cpp; - - auto it = types.find(std::type_index(tp)); + auto it = types.find(type_idx); if (it != types.end()) return (detail::type_info *) it->second; + auto &locals = registered_local_types_cpp(); + it = locals.find(type_idx); + if (it != locals.end()) + return (detail::type_info *) it->second; if (throw_if_missing) { std::string tname = tp.name(); detail::clean_type_id(tname); @@ -716,10 +744,8 @@ class type_caster_generic { // with .second = nullptr. (p.first = nullptr is not an error: it becomes None). PYBIND11_NOINLINE static std::pair src_and_type( const void *src, const std::type_info &cast_type, const std::type_info *rtti_type = nullptr) { - auto &internals = get_internals(); - auto it = internals.registered_types_cpp.find(std::type_index(cast_type)); - if (it != internals.registered_types_cpp.end()) - return {src, (const type_info *) it->second}; + if (auto *tpi = get_type_info(cast_type)) + return {src, const_cast(tpi)}; // Not found, set error: std::string tname = rtti_type ? rtti_type->name() : cast_type.name(); @@ -804,7 +830,6 @@ template class type_caster_base : public type_caster_generic { template ::value, int> = 0> static std::pair src_and_type(const itype *src) { const void *vsrc = src; - auto &internals = get_internals(); auto &cast_type = typeid(itype); const std::type_info *instance_type = nullptr; if (vsrc) { @@ -813,9 +838,8 @@ template class type_caster_base : public type_caster_generic { // This is a base pointer to a derived type; if it is a pybind11-registered type, we // can get the correct derived pointer (which may be != base pointer) by a // dynamic_cast to most derived type: - auto it = internals.registered_types_cpp.find(std::type_index(*instance_type)); - if (it != internals.registered_types_cpp.end()) - return {dynamic_cast(src), (const type_info *) it->second}; + if (auto *tpi = get_type_info(*instance_type)) + return {dynamic_cast(src), const_cast(tpi)}; } } // Otherwise we have either a nullptr, an `itype` pointer, or an unknown derived pointer, so diff --git a/include/pybind11/common.h b/include/pybind11/common.h index 240f6d8e58..518e245634 100644 --- a/include/pybind11/common.h +++ b/include/pybind11/common.h @@ -68,6 +68,16 @@ # endif #endif +// Attribute macro for a function containing one or more static local variables that mustn't share +// the variable across shared objects (for example, because the value might be incompatible for +// modules compiled under different pybind versions). This is required under g++ (depending on the +// specific compiler and linker options), and won't hurt under gcc-compatible compilers: +#if defined(__GNUG__) +# define PYBIND11_UNSHARED_STATIC_LOCALS __attribute__ ((visibility("hidden"))) +#else +# define PYBIND11_UNSHARED_STATIC_LOCALS +#endif + #if defined(_MSC_VER) # define PYBIND11_NOINLINE __declspec(noinline) #else diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index d3f34ee6e8..96dea65519 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -839,12 +839,16 @@ class generic_type : public object { tinfo->dealloc = rec.dealloc; tinfo->simple_type = true; tinfo->simple_ancestors = true; + tinfo->default_holder = rec.default_holder; + tinfo->module_local = rec.module_local; auto &internals = get_internals(); auto tindex = std::type_index(*rec.type); tinfo->direct_conversions = &internals.direct_conversions[tindex]; - tinfo->default_holder = rec.default_holder; - internals.registered_types_cpp[tindex] = tinfo; + if (rec.module_local) + registered_local_types_cpp()[tindex] = tinfo; + else + internals.registered_types_cpp[tindex] = tinfo; internals.registered_types_py[(PyTypeObject *) m_ptr] = { tinfo }; if (rec.bases.size() > 1 || rec.multiple_inheritance) { @@ -986,7 +990,7 @@ class class_ : public detail::generic_type { generic_type::initialize(record); if (has_alias) { - auto &instances = get_internals().registered_types_cpp; + auto &instances = record.module_local ? registered_local_types_cpp() : get_internals().registered_types_cpp; instances[std::type_index(typeid(type_alias))] = instances[std::type_index(typeid(type))]; } } @@ -1442,7 +1446,7 @@ iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) { typedef detail::iterator_state state; if (!detail::get_type_info(typeid(state), false)) { - class_(handle(), "iterator") + class_(handle(), "iterator", pybind11::module_local()) .def("__iter__", [](state &s) -> state& { return s; }) .def("__next__", [](state &s) -> ValueType { if (!s.first_or_done) @@ -1471,7 +1475,7 @@ iterator make_key_iterator(Iterator first, Sentinel last, Extra &&... extra) { typedef detail::iterator_state state; if (!detail::get_type_info(typeid(state), false)) { - class_(handle(), "iterator") + class_(handle(), "iterator", pybind11::module_local()) .def("__iter__", [](state &s) -> state& { return s; }) .def("__next__", [](state &s) -> KeyType { if (!s.first_or_done) diff --git a/include/pybind11/stl_bind.h b/include/pybind11/stl_bind.h index f16e9d22bd..6263f9926c 100644 --- a/include/pybind11/stl_bind.h +++ b/include/pybind11/stl_bind.h @@ -373,10 +373,16 @@ NAMESPACE_END(detail) // std::vector // template , typename... Args> -class_ bind_vector(module &m, std::string const &name, Args&&... args) { +class_ bind_vector(handle scope, std::string const &name, Args&&... args) { using Class_ = class_; - Class_ cl(m, name.c_str(), std::forward(args)...); + // If the value_type is unregistered (e.g. a converting type) or is itself registered + // module-local then make the vector binding module-local as well: + using vtype = typename Vector::value_type; + auto vtype_info = detail::get_type_info(typeid(vtype)); + bool local = !vtype_info || vtype_info->module_local; + + Class_ cl(scope, name.c_str(), pybind11::module_local(local), std::forward(args)...); // Declare the buffer interface if a buffer_protocol() is passed in detail::vector_buffer(cl); @@ -528,12 +534,22 @@ template auto map_if_insertion_operator(Class_ & NAMESPACE_END(detail) template , typename... Args> -class_ bind_map(module &m, const std::string &name, Args&&... args) { +class_ bind_map(handle scope, const std::string &name, Args&&... args) { using KeyType = typename Map::key_type; using MappedType = typename Map::mapped_type; using Class_ = class_; - Class_ cl(m, name.c_str(), std::forward(args)...); + // If either type is a non-module-local bound type then make the map binding non-local as well; + // otherwise (e.g. both types are either module-local or converting) the map will be + // module-local. + auto tinfo = detail::get_type_info(typeid(MappedType)); + bool local = !tinfo || tinfo->module_local; + if (local) { + tinfo = detail::get_type_info(typeid(KeyType)); + local = !tinfo || tinfo->module_local; + } + + Class_ cl(scope, name.c_str(), pybind11::module_local(local), std::forward(args)...); cl.def(init<>()); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 945753f0e7..aa2704b29f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(PYBIND11_TEST_FILES test_eval.cpp test_exceptions.cpp test_kwargs_and_defaults.cpp + test_local_bindings.cpp test_methods_and_attributes.cpp test_modules.cpp test_multiple_inheritance.cpp @@ -67,6 +68,14 @@ endif() string(REPLACE ".cpp" ".py" PYBIND11_PYTEST_FILES "${PYBIND11_TEST_FILES}") +# Contains the set of test files that require pybind11_cross_module_tests to be +# built; if none of these are built (i.e. because TEST_OVERRIDE is used and +# doesn't include them) the second module doesn't get built. +set(PYBIND11_CROSS_MODULE_TESTS + test_exceptions.py + test_local_bindings.py +) + # Check if Eigen is available; if not, remove from PYBIND11_TEST_FILES (but # keep it in PYBIND11_PYTEST_FILES, so that we get the "eigen is not installed" # skip message). @@ -120,36 +129,52 @@ function(pybind11_enable_warnings target_name) endif() endfunction() +set(test_targets pybind11_tests) -# Create the binding library -pybind11_add_module(pybind11_tests THIN_LTO pybind11_tests.cpp - ${PYBIND11_TEST_FILES} ${PYBIND11_HEADERS}) +# Build pybind11_cross_module_tests if any test_whatever.py are being built that require it +foreach(t ${PYBIND11_CROSS_MODULE_TESTS}) + list(FIND PYBIND11_PYTEST_FILES ${t} i) + if (i GREATER -1) + list(APPEND test_targets pybind11_cross_module_tests) + break() + endif() +endforeach() -pybind11_enable_warnings(pybind11_tests) +set(testdir ${CMAKE_CURRENT_SOURCE_DIR}) +foreach(tgt ${test_targets}) + set(test_files ${PYBIND11_TEST_FILES}) + if(NOT tgt STREQUAL "pybind11_tests") + set(test_files "") + endif() -if(MSVC) - target_compile_options(pybind11_tests PRIVATE /utf-8) -endif() + # Create the binding library + pybind11_add_module(${tgt} THIN_LTO ${tgt}.cpp + ${test_files} ${PYBIND11_HEADERS}) -if(EIGEN3_FOUND) - if (PYBIND11_EIGEN_VIA_TARGET) - target_link_libraries(pybind11_tests PRIVATE Eigen3::Eigen) - else() - target_include_directories(pybind11_tests PRIVATE ${EIGEN3_INCLUDE_DIR}) + pybind11_enable_warnings(${tgt}) + + if(MSVC) + target_compile_options(${tgt} PRIVATE /utf-8) endif() - target_compile_definitions(pybind11_tests PRIVATE -DPYBIND11_TEST_EIGEN) -endif() -set(testdir ${CMAKE_CURRENT_SOURCE_DIR}) + if(EIGEN3_FOUND) + if (PYBIND11_EIGEN_VIA_TARGET) + target_link_libraries(${tgt} PRIVATE Eigen3::Eigen) + else() + target_include_directories(${tgt} PRIVATE ${EIGEN3_INCLUDE_DIR}) + endif() + target_compile_definitions(${tgt} PRIVATE -DPYBIND11_TEST_EIGEN) + endif() -# Always write the output file directly into the 'tests' directory (even on MSVC) -if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) - set_target_properties(pybind11_tests PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${testdir}) - foreach(config ${CMAKE_CONFIGURATION_TYPES}) - string(TOUPPER ${config} config) - set_target_properties(pybind11_tests PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${config} ${testdir}) - endforeach() -endif() + # Always write the output file directly into the 'tests' directory (even on MSVC) + if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set_target_properties(${tgt} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${testdir}) + foreach(config ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${config} config) + set_target_properties(${tgt} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${config} ${testdir}) + endforeach() + endif() +endforeach() # Make sure pytest is found or produce a fatal error if(NOT PYBIND11_PYTEST_FOUND) @@ -173,7 +198,7 @@ endif() # A single command to compile and run the tests add_custom_target(pytest COMMAND ${PYTHON_EXECUTABLE} -m pytest ${PYBIND11_PYTEST_FILES} - DEPENDS pybind11_tests WORKING_DIRECTORY ${testdir} ${PYBIND11_USES_TERMINAL}) + DEPENDS ${test_targets} WORKING_DIRECTORY ${testdir} ${PYBIND11_USES_TERMINAL}) if(PYBIND11_TEST_OVERRIDE) add_custom_command(TARGET pytest POST_BUILD @@ -189,7 +214,7 @@ if (NOT PROJECT_NAME STREQUAL "pybind11") return() endif() -# Add a post-build comment to show the .so size and, if a previous size, compare it: +# Add a post-build comment to show the primary test suite .so size and, if a previous size, compare it: add_custom_command(TARGET pybind11_tests POST_BUILD COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/tools/libsize.py $ ${CMAKE_CURRENT_BINARY_DIR}/sosize-$.txt) diff --git a/tests/local_bindings.h b/tests/local_bindings.h new file mode 100644 index 0000000000..0c53369306 --- /dev/null +++ b/tests/local_bindings.h @@ -0,0 +1,26 @@ +#pragma once +#include "pybind11_tests.h" + +/// Simple class used to test py::local: +template class LocalBase { +public: + LocalBase(int i) : i(i) { } + int i = -1; +}; + +/// Registered with py::local in both main and secondary modules: +using LocalType = LocalBase<0>; +/// Registered without py::local in both modules: +using NonLocalType = LocalBase<1>; +/// A second non-local type (for stl_bind tests): +using NonLocal2 = LocalBase<2>; +/// Tests within-module, different-compilation-unit local definition conflict: +using LocalExternal = LocalBase<3>; + +// Simple bindings (used with the above): +template +py::class_ bind_local(Args && ...args) { + return py::class_(std::forward(args)...) + .def(py::init()) + .def("get", [](T &i) { return i.i + Adjust; }); +}; diff --git a/tests/pybind11_cross_module_tests.cpp b/tests/pybind11_cross_module_tests.cpp new file mode 100644 index 0000000000..f417a8944f --- /dev/null +++ b/tests/pybind11_cross_module_tests.cpp @@ -0,0 +1,70 @@ +/* + tests/pybind11_cross_module_tests.cpp -- contains tests that require multiple modules + + Copyright (c) 2017 Jason Rhinelander + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include "pybind11_tests.h" +#include "local_bindings.h" +#include + +PYBIND11_MODULE(pybind11_cross_module_tests, m) { + m.doc() = "pybind11 cross-module test module"; + + // test_local_bindings.py tests: + // + // Definitions here are tested by importing both this module and the + // relevant pybind11_tests submodule from a test_whatever.py + + // test_exceptions.py + m.def("raise_runtime_error", []() { PyErr_SetString(PyExc_RuntimeError, "My runtime error"); throw py::error_already_set(); }); + m.def("raise_value_error", []() { PyErr_SetString(PyExc_ValueError, "My value error"); throw py::error_already_set(); }); + m.def("throw_pybind_value_error", []() { throw py::value_error("pybind11 value error"); }); + m.def("throw_pybind_type_error", []() { throw py::type_error("pybind11 type error"); }); + m.def("throw_stop_iteration", []() { throw py::stop_iteration(); }); + + // test_local_bindings.py + // Local to both: + bind_local(m, "LocalType", py::module_local()) + .def("get2", [](LocalType &t) { return t.i + 2; }) + ; + + // Can only be called with our python type: + m.def("local_value", [](LocalType &l) { return l.i; }); + + // test_nonlocal_failure + // This registration will fail (global registration when LocalFail is already registered + // globally in the main test module): + m.def("register_nonlocal", [m]() { + bind_local(m, "NonLocalType"); + }); + + // test_stl_bind_local + // stl_bind.h binders defaults to py::module_local if the types are local or converting: + py::bind_vector>(m, "LocalVec"); + py::bind_map>(m, "LocalMap"); + // and global if the type (or one of the types, for the map) is global (so these will fail, + // assuming pybind11_tests is already loaded): + m.def("register_nonlocal_vec", [m]() { + py::bind_vector>(m, "NonLocalVec"); + }); + m.def("register_nonlocal_map", [m]() { + py::bind_map>(m, "NonLocalMap"); + }); + + // test_stl_bind_global + // The default can, however, be overridden to global using `py::module_local()` or + // `py::module_local(false)`. + // Explicitly made local: + py::bind_vector>(m, "NonLocalVec2", py::module_local()); + // Explicitly made global (and so will fail to bind): + m.def("register_nonlocal_map2", [m]() { + py::bind_map>(m, "NonLocalMap2", py::module_local(false)); + }); + + // test_internal_locals_differ + m.def("local_cpp_types_addr", []() { return (uintptr_t) &py::detail::registered_local_types_cpp(); }); +} diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 8761f26504..5860b741eb 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -9,6 +9,7 @@ #include "pybind11_tests.h" #include "constructor_stats.h" +#include "local_bindings.h" TEST_SUBMODULE(class_, m) { // test_instance @@ -224,6 +225,10 @@ TEST_SUBMODULE(class_, m) { aliased.def(py::init<>()); aliased.attr("size_noalias") = py::int_(sizeof(AliasedHasOpNewDelSize)); aliased.attr("size_alias") = py::int_(sizeof(PyAliasedHasOpNewDelSize)); + + // This test is actually part of test_local_bindings (test_duplicate_local), but we need a + // definition in a different compilation unit within the same module: + bind_local(m, "LocalExternal", py::module_local()); } template class BreaksBase {}; diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 06d442e67a..8d37c09b89 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,6 +1,7 @@ import pytest from pybind11_tests import exceptions as m +import pybind11_cross_module_tests as cm def test_std_exception(msg): @@ -19,6 +20,27 @@ def test_error_already_set(msg): assert msg(excinfo.value) == "foo" +def test_cross_module_exceptions(): + with pytest.raises(RuntimeError) as excinfo: + cm.raise_runtime_error() + assert str(excinfo.value) == "My runtime error" + + with pytest.raises(ValueError) as excinfo: + cm.raise_value_error() + assert str(excinfo.value) == "My value error" + + with pytest.raises(ValueError) as excinfo: + cm.throw_pybind_value_error() + assert str(excinfo.value) == "pybind11 value error" + + with pytest.raises(TypeError) as excinfo: + cm.throw_pybind_type_error() + assert str(excinfo.value) == "pybind11 type error" + + with pytest.raises(StopIteration) as excinfo: + cm.throw_stop_iteration() + + def test_python_call_in_catch(): d = {} assert m.python_call_in_destructor(d) is True diff --git a/tests/test_local_bindings.cpp b/tests/test_local_bindings.cpp new file mode 100644 index 0000000000..d98840f628 --- /dev/null +++ b/tests/test_local_bindings.cpp @@ -0,0 +1,62 @@ +/* + tests/test_local_bindings.cpp -- tests the py::module_local class feature which makes a class + binding local to the module in which it is defined. + + Copyright (c) 2017 Jason Rhinelander + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include "pybind11_tests.h" +#include "local_bindings.h" +#include + +TEST_SUBMODULE(local_bindings, m) { + + // test_local_bindings + // Register a class with py::module_local: + bind_local(m, "LocalType", py::module_local()) + .def("get3", [](LocalType &t) { return t.i + 3; }) + ; + + m.def("local_value", [](LocalType &l) { return l.i; }); + + // test_nonlocal_failure + // The main pybind11 test module is loaded first, so this registration will succeed (the second + // one, in pybind11_cross_module_tests.cpp, is designed to fail): + bind_local(m, "NonLocalType") + .def(py::init()) + .def("get", [](LocalType &i) { return i.i; }) + ; + + // test_duplicate_local + // py::module_local declarations should be visible across compilation units that get linked together; + // this tries to register a duplicate local. It depends on a definition in test_class.cpp and + // should raise a runtime error from the duplicate definition attempt. If test_class isn't + // available it *also* throws a runtime error (with "test_class not enabled" as value). + m.def("register_local_external", [m]() { + auto main = py::module::import("pybind11_tests"); + if (py::hasattr(main, "class_")) { + bind_local(m, "LocalExternal", py::module_local()); + } + else throw std::runtime_error("test_class not enabled"); + }); + + // test_stl_bind_local + // stl_bind.h binders defaults to py::module_local if the types are local or converting: + py::bind_vector>(m, "LocalVec"); + py::bind_map>(m, "LocalMap"); + // and global if the type (or one of the types, for the map) is global: + py::bind_vector>(m, "NonLocalVec"); + py::bind_map>(m, "NonLocalMap"); + + // test_stl_bind_global + // They can, however, be overridden to global using `py::module_local(false)`: + bind_local(m, "NonLocal2"); + py::bind_vector>(m, "LocalVec2", py::module_local()); + py::bind_map>(m, "NonLocalMap2", py::module_local(false)); + + // test_internal_locals_differ + m.def("local_cpp_types_addr", []() { return (uintptr_t) &py::detail::registered_local_types_cpp(); }); +} diff --git a/tests/test_local_bindings.py b/tests/test_local_bindings.py new file mode 100644 index 0000000000..4c5a874096 --- /dev/null +++ b/tests/test_local_bindings.py @@ -0,0 +1,109 @@ +import pytest + +from pybind11_tests import local_bindings as m + + +def test_local_bindings(): + """Tests that duplicate py::local class bindings work across modules""" + + # Make sure we can load the second module with the conflicting (but local) definition: + import pybind11_cross_module_tests as cm + + i1 = m.LocalType(5) + + assert i1.get() == 4 + assert i1.get3() == 8 + + i2 = cm.LocalType(10) + assert i2.get() == 11 + assert i2.get2() == 12 + + assert not hasattr(i1, 'get2') + assert not hasattr(i2, 'get3') + + assert m.local_value(i1) == 5 + assert cm.local_value(i2) == 10 + + with pytest.raises(TypeError) as excinfo: + m.local_value(i2) + assert "incompatible function arguments" in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + cm.local_value(i1) + assert "incompatible function arguments" in str(excinfo.value) + + +def test_nonlocal_failure(): + """Tests that attempting to register a non-local type in multiple modules fails""" + import pybind11_cross_module_tests as cm + + with pytest.raises(RuntimeError) as excinfo: + cm.register_nonlocal() + assert str(excinfo.value) == 'generic_type: type "NonLocalType" is already registered!' + + +def test_duplicate_local(): + """Tests expected failure when registering a class twice with py::local in the same module""" + with pytest.raises(RuntimeError) as excinfo: + m.register_local_external() + import pybind11_tests + assert str(excinfo.value) == ( + 'generic_type: type "LocalExternal" is already registered!' + if hasattr(pybind11_tests, 'class_') else 'test_class not enabled') + + +def test_stl_bind_local(): + import pybind11_cross_module_tests as cm + + v1, v2 = m.LocalVec(), cm.LocalVec() + v1.append(m.LocalType(1)) + v1.append(m.LocalType(2)) + v2.append(cm.LocalType(1)) + v2.append(cm.LocalType(2)) + + with pytest.raises(TypeError): + v1.append(cm.LocalType(3)) + with pytest.raises(TypeError): + v2.append(m.LocalType(3)) + + assert [i.get() for i in v1] == [0, 1] + assert [i.get() for i in v2] == [2, 3] + + v3, v4 = m.NonLocalVec(), cm.NonLocalVec2() + v3.append(m.NonLocalType(1)) + v3.append(m.NonLocalType(2)) + v4.append(m.NonLocal2(3)) + v4.append(m.NonLocal2(4)) + + assert [i.get() for i in v3] == [1, 2] + assert [i.get() for i in v4] == [13, 14] + + d1, d2 = m.LocalMap(), cm.LocalMap() + d1["a"] = v1[0] + d1["b"] = v1[1] + d2["c"] = v2[0] + d2["d"] = v2[1] + assert {i: d1[i].get() for i in d1} == {'a': 0, 'b': 1} + assert {i: d2[i].get() for i in d2} == {'c': 2, 'd': 3} + + +def test_stl_bind_global(): + import pybind11_cross_module_tests as cm + + with pytest.raises(RuntimeError) as excinfo: + cm.register_nonlocal_map() + assert str(excinfo.value) == 'generic_type: type "NonLocalMap" is already registered!' + + with pytest.raises(RuntimeError) as excinfo: + cm.register_nonlocal_vec() + assert str(excinfo.value) == 'generic_type: type "NonLocalVec" is already registered!' + + with pytest.raises(RuntimeError) as excinfo: + cm.register_nonlocal_map2() + assert str(excinfo.value) == 'generic_type: type "NonLocalMap2" is already registered!' + + +def test_internal_locals_differ(): + """Makes sure the internal local type map differs across the two modules""" + import pybind11_cross_module_tests as cm + assert m.local_cpp_types_addr() != cm.local_cpp_types_addr()