From ff8146ef305fe07a5c0dc0a30006d96112af600c Mon Sep 17 00:00:00 2001 From: Alicja Miloszewska Date: Fri, 20 Dec 2024 10:05:45 +0100 Subject: [PATCH] [Py OV] Extend Model to utilize with-expressions (#27191) ### Details: - Implement `__enter__` and `__exit__` to create class-based context manager - Remove inheritance from ModelBase in ie_api.Model and make it an attribute. - add private property __model that stores `_pyopenvino.Model`. In `_pyopenvino` such attribute can be accessed as `_Model__model` because of name mangling - update pyAPI methods that have `std::shared_ptr` in their signatures. - add `test_model_with_statement` and `test_model_tempdir_fails` ### Motivation: On Windows reading `ov.Model` from temporary directory leads to `PermissionError`: ```python mem_model = generate_model_with_memory(input_shape=Shape([2, 1]), data_type=Type.f32) with tempfile.TemporaryDirectory() as model_save_dir: save_model(mem_model, f"{model_save_dir}/model.xml") model = Core().read_model(f"{model_save_dir}/model.xml") ``` ### Tickets: - CVS-106987 --------- Signed-off-by: Alicja Miloszewska --- src/bindings/python/src/openvino/_ov_api.py | 53 +++++++++++++++---- .../src/openvino/properties/_properties.py | 3 ++ .../src/openvino/test_utils/__init__.py | 2 +- .../src/openvino/test_utils/test_api.py | 10 ++++ .../core/offline_transformations.cpp | 37 ++++++++----- .../src/pyopenvino/frontend/frontend.cpp | 22 +++++--- .../pyopenvino/graph/attribute_visitor.cpp | 5 ++ .../python/src/pyopenvino/graph/model.cpp | 4 -- .../python/src/pyopenvino/graph/ops/if.cpp | 39 +++++++++----- .../python/src/pyopenvino/graph/ops/loop.cpp | 4 +- .../pyopenvino/graph/ops/tensor_iterator.cpp | 12 ++++- .../src/pyopenvino/graph/passes/manager.cpp | 12 +++-- .../graph/preprocess/pre_post_process.cpp | 20 ++++++- .../python/src/pyopenvino/pyopenvino.cpp | 34 +++++++----- .../src/pyopenvino/test_utils/CMakeLists.txt | 6 +-- .../python/src/pyopenvino/utils/utils.cpp | 12 +++++ .../python/src/pyopenvino/utils/utils.hpp | 2 + .../python/tests/test_runtime/test_model.py | 44 +++++++++++++-- 18 files changed, 247 insertions(+), 74 deletions(-) create mode 100644 src/bindings/python/src/openvino/test_utils/test_api.py diff --git a/src/bindings/python/src/openvino/_ov_api.py b/src/bindings/python/src/openvino/_ov_api.py index 972ab4a9eb81c0..da31fab4c95d8e 100644 --- a/src/bindings/python/src/openvino/_ov_api.py +++ b/src/bindings/python/src/openvino/_ov_api.py @@ -2,7 +2,8 @@ # Copyright (C) 2018-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Iterable, Union, Optional, Dict +from types import TracebackType +from typing import Any, Iterable, Union, Optional, Dict, Type from pathlib import Path @@ -21,22 +22,30 @@ ) -class Model(ModelBase): +class Model: def __init__(self, *args: Any, **kwargs: Any) -> None: if args and not kwargs: if isinstance(args[0], ModelBase): - super().__init__(args[0]) + self.__model = ModelBase(args[0]) elif isinstance(args[0], Node): - super().__init__(*args) + self.__model = ModelBase(*args) else: - super().__init__(*args) + self.__model = ModelBase(*args) if args and kwargs: - super().__init__(*args, **kwargs) + self.__model = ModelBase(*args, **kwargs) if kwargs and not args: - super().__init__(**kwargs) + self.__model = ModelBase(**kwargs) + + def __getattr__(self, name: str) -> Any: + if self.__model is None: + raise AttributeError(f"'Model' object has no attribute '{name}' or attribute is no longer accessible.") + return getattr(self.__model, name) def clone(self) -> "Model": - return Model(super().clone()) + return Model(self.__model.clone()) + + def __copy__(self) -> "Model": + raise TypeError("Cannot copy 'openvino.runtime.Model'. Please, use deepcopy instead.") def __deepcopy__(self, memo: Dict) -> "Model": """Returns a deepcopy of Model. @@ -44,7 +53,17 @@ def __deepcopy__(self, memo: Dict) -> "Model": :return: A copy of Model. :rtype: openvino.runtime.Model """ - return Model(super().clone()) + return Model(self.__model.clone()) + + def __enter__(self) -> "Model": + return self + + def __exit__(self, exc_type: Type[BaseException], exc_value: BaseException, traceback: TracebackType) -> None: + del self.__model + self.__model = None + + def __repr__(self) -> str: + return self.__model.__repr__() class InferRequest(_InferRequestWrapper): @@ -500,6 +519,8 @@ def read_model( config: Optional[dict] = None ) -> Model: config = {} if config is None else config + if isinstance(model, Model): + model = model._Model__model if isinstance(weights, Tensor): return Model(super().read_model(model, weights)) @@ -543,6 +564,8 @@ def compile_model( :return: A compiled model. :rtype: openvino.runtime.CompiledModel """ + if isinstance(model, Model): + model = model._Model__model if weights is None: if device_name is None: return CompiledModel( @@ -562,6 +585,16 @@ def compile_model( weights=weights, ) + def query_model( + self, + model: Model, + device_name: str, + config: Optional[dict] = None, + ) -> dict: + return super().query_model(model._Model__model, + device_name, + {} if config is None else config, ) + def import_model( self, model_stream: bytes, @@ -637,4 +670,6 @@ def compile_model( """ core = Core() + if isinstance(model, Model): + model = model._Model__model return core.compile_model(model, device_name, {} if config is None else config) diff --git a/src/bindings/python/src/openvino/properties/_properties.py b/src/bindings/python/src/openvino/properties/_properties.py index a3d9e2076ad072..ee0a612583431c 100644 --- a/src/bindings/python/src/openvino/properties/_properties.py +++ b/src/bindings/python/src/openvino/properties/_properties.py @@ -16,6 +16,9 @@ def __new__(cls, prop: Callable[..., Any]): # type: ignore def __call__(self, *args: Any) -> Callable[..., Any]: if args is not None: + from openvino import Model + if args and isinstance(args[0], Model): + return self.prop(args[0]._Model__model) return self.prop(*args) return self.prop() diff --git a/src/bindings/python/src/openvino/test_utils/__init__.py b/src/bindings/python/src/openvino/test_utils/__init__.py index e25fa9e67be800..bca79f8a4e2729 100644 --- a/src/bindings/python/src/openvino/test_utils/__init__.py +++ b/src/bindings/python/src/openvino/test_utils/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2018-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from .test_utils_api import compare_functions +from .test_api import compare_functions diff --git a/src/bindings/python/src/openvino/test_utils/test_api.py b/src/bindings/python/src/openvino/test_utils/test_api.py new file mode 100644 index 00000000000000..ce65eb9dcd820e --- /dev/null +++ b/src/bindings/python/src/openvino/test_utils/test_api.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2018-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .test_utils_api import compare_functions as compare_functions_base +from openvino.runtime import Model + + +def compare_functions(lhs: Model, rhs: Model, compare_tensor_names: bool = True) -> tuple: + return compare_functions_base(lhs._Model__model, rhs._Model__model, compare_tensor_names) diff --git a/src/bindings/python/src/pyopenvino/core/offline_transformations.cpp b/src/bindings/python/src/pyopenvino/core/offline_transformations.cpp index 641893cdd267a2..90aece1803f4b4 100644 --- a/src/bindings/python/src/pyopenvino/core/offline_transformations.cpp +++ b/src/bindings/python/src/pyopenvino/core/offline_transformations.cpp @@ -23,6 +23,7 @@ #include "openvino/pass/low_latency.hpp" #include "openvino/pass/manager.hpp" +#include "pyopenvino/utils/utils.hpp" namespace py = pybind11; @@ -34,7 +35,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_moc_transformations", - [](std::shared_ptr model, bool cf, bool smart_reshape) { + [](py::object& ie_api_model, bool cf, bool smart_reshape) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; if (smart_reshape) manager.register_pass(); @@ -48,7 +50,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_moc_legacy_transformations", - [](std::shared_ptr model, const std::vector& params_with_custom_types) { + [](py::object& ie_api_model, const std::vector& params_with_custom_types) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(params_with_custom_types); manager.run_passes(model); @@ -58,7 +61,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_low_latency_transformation", - [](std::shared_ptr model, bool use_const_initializer = true) { + [](py::object& ie_api_model, bool use_const_initializer = true) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(use_const_initializer); manager.run_passes(model); @@ -68,7 +72,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_pruning_transformation", - [](std::shared_ptr model) { + [](py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(); manager.run_passes(model); @@ -77,7 +82,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_make_stateful_transformation", - [](std::shared_ptr model, const std::map& param_res_names) { + [](py::object& ie_api_model, const std::map& param_res_names) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(param_res_names); manager.run_passes(model); @@ -87,7 +93,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_make_stateful_transformation", - [](std::shared_ptr model, const ov::pass::MakeStateful::ParamResPairs& pairs_to_replace) { + [](py::object& ie_api_model, const ov::pass::MakeStateful::ParamResPairs& pairs_to_replace) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(pairs_to_replace); manager.run_passes(model); @@ -97,7 +104,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "compress_model_transformation", - [](std::shared_ptr model) { + [](py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); bool postponed = false; return ov::pass::compress_model_to_f16(model, postponed); }, @@ -105,7 +113,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "compress_quantize_weights_transformation", - [](std::shared_ptr model) { + [](py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(); manager.run_passes(model); @@ -114,7 +123,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "convert_sequence_to_tensor_iterator_transformation", - [](std::shared_ptr model) { + [](py::object ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(); manager.run_passes(model); @@ -123,7 +133,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "apply_fused_names_cleanup", - [](std::shared_ptr model) { + [](py::object ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(); manager.run_passes(model); @@ -132,7 +143,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "paged_attention_transformation", - [](std::shared_ptr model, bool use_block_indices_inputs, bool use_score_outputs) { + [](py::object& ie_api_model, bool use_block_indices_inputs, bool use_score_outputs) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(use_block_indices_inputs, use_score_outputs); manager.run_passes(model); @@ -143,7 +155,8 @@ void regmodule_offline_transformations(py::module m) { m_offline_transformations.def( "stateful_to_stateless_transformation", - [](std::shared_ptr model) { + [](py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::pass::Manager manager; manager.register_pass(); manager.run_passes(model); diff --git a/src/bindings/python/src/pyopenvino/frontend/frontend.cpp b/src/bindings/python/src/pyopenvino/frontend/frontend.cpp index 758fb505f5f885..52707b0b8248ce 100644 --- a/src/bindings/python/src/pyopenvino/frontend/frontend.cpp +++ b/src/bindings/python/src/pyopenvino/frontend/frontend.cpp @@ -113,10 +113,13 @@ void regclass_frontend_FrontEnd(py::module m) { :rtype: openvino.runtime.Model )"); - fem.def("convert", - static_cast&) const>(&FrontEnd::convert), - py::arg("model"), - R"( + fem.def( + "convert", + [](FrontEnd& self, const py::object& ie_api_model) { + return self.convert(Common::utils::convert_to_model(ie_api_model)); + }, + py::arg("model"), + R"( Completely convert the remaining, not converted part of a function. :param model: Partially converted OpenVINO model. @@ -153,10 +156,13 @@ void regclass_frontend_FrontEnd(py::module m) { :rtype: openvino.runtime.Model )"); - fem.def("normalize", - &FrontEnd::normalize, - py::arg("model"), - R"( + fem.def( + "normalize", + [](FrontEnd& self, const py::object& ie_api_model) { + self.normalize(Common::utils::convert_to_model(ie_api_model)); + }, + py::arg("model"), + R"( Runs normalization passes on function that was loaded with partial conversion. :param model : Partially converted OpenVINO model. diff --git a/src/bindings/python/src/pyopenvino/graph/attribute_visitor.cpp b/src/bindings/python/src/pyopenvino/graph/attribute_visitor.cpp index 587d3906b02607..40a603977159a5 100644 --- a/src/bindings/python/src/pyopenvino/graph/attribute_visitor.cpp +++ b/src/bindings/python/src/pyopenvino/graph/attribute_visitor.cpp @@ -35,6 +35,7 @@ void regclass_graph_AttributeVisitor(py::module m) { "on_attributes", [](ov::AttributeVisitor* self, py::dict& attributes) { py::object float_32_type = py::module_::import("numpy").attr("float32"); + py::object model = py::module_::import("openvino.runtime").attr("Model"); for (const auto& attribute : attributes) { if (py::isinstance(attribute.second)) { visit_attribute(attributes, attribute, self); @@ -48,6 +49,10 @@ void regclass_graph_AttributeVisitor(py::module m) { visit_attribute(attributes, attribute, self); } else if (py::isinstance(attribute.second)) { visit_attribute>(attributes, attribute, self); + } else if (py::isinstance(attribute.second, model)) { + auto attr_casted = attribute.second.attr("_Model__model").cast>(); + self->on_attribute>(attribute.first.cast(), attr_casted); + attributes[attribute.first] = std::move(attr_casted); } else if (py::isinstance(attribute.second)) { visit_attribute(attributes, attribute, self); } else if (py::isinstance(attribute.second)) { diff --git a/src/bindings/python/src/pyopenvino/graph/model.cpp b/src/bindings/python/src/pyopenvino/graph/model.cpp index e3c648c0f4cfcb..a482ba55e46e74 100644 --- a/src/bindings/python/src/pyopenvino/graph/model.cpp +++ b/src/bindings/python/src/pyopenvino/graph/model.cpp @@ -1328,10 +1328,6 @@ void regclass_graph_Model(py::module m) { outputs_str + "\n]>"; }); - model.def("__copy__", [](ov::Model& self) { - throw py::type_error("Cannot copy 'openvino.runtime.Model. Please, use deepcopy instead."); - }); - model.def("get_rt_info", (PyRTMap & (ov::Model::*)()) & ov::Model::get_rt_info, py::return_value_policy::reference_internal, diff --git a/src/bindings/python/src/pyopenvino/graph/ops/if.cpp b/src/bindings/python/src/pyopenvino/graph/ops/if.cpp index c452e2fe4ac849..8cd52099436d2b 100644 --- a/src/bindings/python/src/pyopenvino/graph/ops/if.cpp +++ b/src/bindings/python/src/pyopenvino/graph/ops/if.cpp @@ -12,6 +12,7 @@ #include "pyopenvino/core/common.hpp" #include "pyopenvino/graph/ops/if.hpp" #include "pyopenvino/graph/ops/util/multisubgraph.hpp" +#include "pyopenvino/utils/utils.hpp" namespace py = pybind11; @@ -77,10 +78,14 @@ void regclass_graph_op_If(py::module m) { :rtype: openvino.Model )"); - cls.def("set_then_body", - &ov::op::v8::If::set_then_body, - py::arg("body"), - R"( + cls.def( + "set_then_body", + [](const std::shared_ptr& self, const py::object& ie_api_model) { + const auto body = Common::utils::convert_to_model(ie_api_model); + return self->set_then_body(body); + }, + py::arg("body"), + R"( Sets new Model object as new then_body. :param body: new body for 'then' branch. @@ -89,10 +94,14 @@ void regclass_graph_op_If(py::module m) { :rtype: None )"); - cls.def("set_else_body", - &ov::op::v8::If::set_else_body, - py::arg("body"), - R"( + cls.def( + "set_else_body", + [](const std::shared_ptr& self, const py::object& ie_api_model) { + const auto body = Common::utils::convert_to_model(ie_api_model); + return self->set_else_body(body); + }, + py::arg("body"), + R"( Sets new Model object as new else_body. :param body: new body for 'else' branch. @@ -156,11 +165,15 @@ void regclass_graph_op_If(py::module m) { :rtype: openvino.Model )"); - cls.def("set_function", - &ov::op::util::MultiSubGraphOp::set_function, - py::arg("index"), - py::arg("func"), - R"( + cls.def( + "set_function", + [](const std::shared_ptr& self, int index, const py::object& ie_api_model) { + const auto func = Common::utils::convert_to_model(ie_api_model); + self->set_function(index, func); + }, + py::arg("index"), + py::arg("func"), + R"( Adds sub-graph to MultiSubGraphOp. :param index: index of new sub-graph. diff --git a/src/bindings/python/src/pyopenvino/graph/ops/loop.cpp b/src/bindings/python/src/pyopenvino/graph/ops/loop.cpp index 536d97d17273ab..069a1376eba758 100644 --- a/src/bindings/python/src/pyopenvino/graph/ops/loop.cpp +++ b/src/bindings/python/src/pyopenvino/graph/ops/loop.cpp @@ -11,6 +11,7 @@ #include "openvino/util/log.hpp" #include "pyopenvino/core/common.hpp" #include "pyopenvino/graph/ops/util/multisubgraph.hpp" +#include "pyopenvino/utils/utils.hpp" namespace py = pybind11; @@ -91,7 +92,8 @@ void regclass_graph_op_Loop(py::module m) { cls.def( "set_function", - [](const std::shared_ptr& self, const std::shared_ptr& func) { + [](const std::shared_ptr& self, const py::object& ie_api_model) { + const auto func = Common::utils::convert_to_model(ie_api_model); self->set_function(func); }, py::arg("func")); diff --git a/src/bindings/python/src/pyopenvino/graph/ops/tensor_iterator.cpp b/src/bindings/python/src/pyopenvino/graph/ops/tensor_iterator.cpp index 5932656c3eccb9..3039aa90008f29 100644 --- a/src/bindings/python/src/pyopenvino/graph/ops/tensor_iterator.cpp +++ b/src/bindings/python/src/pyopenvino/graph/ops/tensor_iterator.cpp @@ -9,6 +9,7 @@ #include "openvino/op/util/sub_graph_base.hpp" #include "pyopenvino/core/common.hpp" #include "pyopenvino/graph/ops/util/multisubgraph.hpp" +#include "pyopenvino/utils/utils.hpp" namespace py = pybind11; @@ -18,7 +19,13 @@ void regclass_graph_op_TensorIterator(py::module m) { "tensor_iterator"); cls.doc() = "openvino.impl.op.TensorIterator wraps ov::op::v0::TensorIterator"; cls.def(py::init<>()); - cls.def("set_body", &ov::op::v0::TensorIterator::set_body, py::arg("body")); + cls.def( + "set_body", + [](const std::shared_ptr& self, py::object& ie_api_model) { + const auto body = Common::utils::convert_to_model(ie_api_model); + self->set_body(body); + }, + py::arg("body")); cls.def("set_invariant_input", &ov::op::v0::TensorIterator::set_invariant_input, py::arg("body_parameter"), @@ -68,7 +75,8 @@ void regclass_graph_op_TensorIterator(py::module m) { cls.def( "set_function", - [](const std::shared_ptr& self, const std::shared_ptr& func) { + [](const std::shared_ptr& self, const py::object& ie_api_model) { + const auto func = Common::utils::convert_to_model(ie_api_model); self->set_function(func); }, py::arg("func")); diff --git a/src/bindings/python/src/pyopenvino/graph/passes/manager.cpp b/src/bindings/python/src/pyopenvino/graph/passes/manager.cpp index 9bd2833308db41..5fb4ddb4bd6dc8 100644 --- a/src/bindings/python/src/pyopenvino/graph/passes/manager.cpp +++ b/src/bindings/python/src/pyopenvino/graph/passes/manager.cpp @@ -35,10 +35,14 @@ void regclass_passes_Manager(py::module m) { :type new_state: bool )"); - manager.def("run_passes", - &ov::pass::Manager::run_passes, - py::arg("model"), - R"( + manager.def( + "run_passes", + [](ov::pass::Manager& self, const py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); + self.run_passes(model); + }, + py::arg("model"), + R"( Executes sequence of transformations on given Model. :param model: openvino.runtime.Model to be transformed. diff --git a/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp b/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp index a19f2b2f482337..25fdd7b007a297 100644 --- a/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp +++ b/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp @@ -11,6 +11,7 @@ #include "openvino/core/node.hpp" #include "openvino/core/preprocess/pre_post_process.hpp" #include "pyopenvino/core/common.hpp" +#include "pyopenvino/utils/utils.hpp" namespace py = pybind11; @@ -553,7 +554,14 @@ void regclass_graph_PrePostProcessor(py::module m) { "PrePostProcessor"); proc.doc() = "openvino.runtime.preprocess.PrePostProcessor wraps ov::preprocess::PrePostProcessor"; - proc.def(py::init&>(), py::arg("model")); + proc.def(py::init([](const py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); + return std::make_shared(model); + }), + py::arg("model"), + R"( + It creates PrePostProcessor. + )"); proc.def("input", [](ov::preprocess::PrePostProcessor& self) { return &self.input(); @@ -591,7 +599,15 @@ void regclass_graph_PrePostProcessor(py::module m) { }, py::arg("output_index")); - proc.def("build", &ov::preprocess::PrePostProcessor::build, py::call_guard()); + proc.def("build", [](ov::preprocess::PrePostProcessor& self) { + std::shared_ptr model; + { + py::gil_scoped_release release; + model = self.build(); + } + py::type model_class = py::module_::import("openvino.runtime").attr("Model"); + return model_class(py::cast(model)); + }); proc.def("__str__", [](const ov::preprocess::PrePostProcessor& self) -> std::string { std::stringstream ss; diff --git a/src/bindings/python/src/pyopenvino/pyopenvino.cpp b/src/bindings/python/src/pyopenvino/pyopenvino.cpp index ee3ef1c8b8144e..c385e5467224c0 100644 --- a/src/bindings/python/src/pyopenvino/pyopenvino.cpp +++ b/src/bindings/python/src/pyopenvino/pyopenvino.cpp @@ -98,10 +98,25 @@ PYBIND11_MODULE(_pyopenvino, m) { m.def("get_version", &get_version); m.def("get_batch", &ov::get_batch); - m.def("set_batch", &ov::set_batch); + m.def( + "get_batch", + [](const py::object& ie_api_model) { + const auto model = Common::utils::convert_to_model(ie_api_model); + return ov::get_batch(model); + }, + py::arg("model")); + m.def( + "set_batch", + [](const py::object& ie_api_model, ov::Dimension value) { + auto model = Common::utils::convert_to_model(ie_api_model); + ov::set_batch(model, value); + }, + py::arg("model"), + py::arg("dimension")); m.def( "set_batch", - [](const std::shared_ptr& model, int64_t value) { + [](const py::object& ie_api_model, int64_t value) { + auto model = Common::utils::convert_to_model(ie_api_model); ov::set_batch(model, ov::Dimension(value)); }, py::arg("model"), @@ -109,10 +124,11 @@ PYBIND11_MODULE(_pyopenvino, m) { m.def( "serialize", - [](std::shared_ptr& model, + [](py::object& ie_api_model, const py::object& xml_path, const py::object& bin_path, const std::string& version) { + const auto model = Common::utils::convert_to_model(ie_api_model); ov::serialize(model, Common::utils::convert_path_to_string(xml_path), Common::utils::convert_path_to_string(bin_path), @@ -173,15 +189,9 @@ PYBIND11_MODULE(_pyopenvino, m) { m.def( "save_model", - [](std::shared_ptr& model, - const py::object& xml_path, - bool compress_to_fp16) { - if (model == nullptr) { - throw py::attribute_error("'model' argument is required and cannot be None."); - } - ov::save_model(model, - Common::utils::convert_path_to_string(xml_path), - compress_to_fp16); + [](py::object& ie_api_model, const py::object& xml_path, bool compress_to_fp16) { + const auto model = Common::utils::convert_to_model(ie_api_model); + ov::save_model(model, Common::utils::convert_path_to_string(xml_path), compress_to_fp16); }, py::arg("model"), py::arg("output_model"), diff --git a/src/bindings/python/src/pyopenvino/test_utils/CMakeLists.txt b/src/bindings/python/src/pyopenvino/test_utils/CMakeLists.txt index 94a1e62b7e1809..81d993b93f95a4 100644 --- a/src/bindings/python/src/pyopenvino/test_utils/CMakeLists.txt +++ b/src/bindings/python/src/pyopenvino/test_utils/CMakeLists.txt @@ -39,7 +39,7 @@ endif() # perform copy add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${OpenVINOPython_SOURCE_DIR}/src/openvino/test_utils/__init__.py ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/__init__.py + COMMAND ${CMAKE_COMMAND} -E copy_directory ${OpenVINOPython_SOURCE_DIR}/src/openvino/test_utils ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ) ov_add_clang_format_target(${TARGET_NAME}_clang FOR_TARGETS ${TARGET_NAME} @@ -53,7 +53,7 @@ install(TARGETS ${TARGET_NAME} LIBRARY DESTINATION tests/${OV_CPACK_PYTHONDIR}/openvino/test_utils COMPONENT tests EXCLUDE_FROM_ALL) -install(PROGRAMS ${OpenVINOPython_SOURCE_DIR}/src/openvino/test_utils/__init__.py - DESTINATION tests/${OV_CPACK_PYTHONDIR}/openvino/test_utils +install(DIRECTORY ${OpenVINOPython_SOURCE_DIR}/src/openvino/test_utils + DESTINATION tests/${OV_CPACK_PYTHONDIR}/openvino COMPONENT tests EXCLUDE_FROM_ALL) diff --git a/src/bindings/python/src/pyopenvino/utils/utils.cpp b/src/bindings/python/src/pyopenvino/utils/utils.cpp index c747e2d3b81166..bd1520119bd8a9 100644 --- a/src/bindings/python/src/pyopenvino/utils/utils.cpp +++ b/src/bindings/python/src/pyopenvino/utils/utils.cpp @@ -311,6 +311,18 @@ std::string convert_path_to_string(const py::object& path) { OPENVINO_THROW(str.str()); } +std::shared_ptr convert_to_model(const py::object& obj) { + if (!py::isinstance(obj, py::module_::import("openvino").attr("Model"))) { + throw py::type_error("Incompatible `model` argument. Please provide a valid openvino.Model instance."); + } + auto model = obj.attr("_Model__model").cast>(); + if (model == nullptr) { + throw py::attribute_error("Invalid openvino.Model instance. It cannot be None. " + "Please make sure it is not used outside of its context."); + } + return model; +} + Version convert_to_version(const std::string& version) { if (version == "UNSPECIFIED") return Version::UNSPECIFIED; diff --git a/src/bindings/python/src/pyopenvino/utils/utils.hpp b/src/bindings/python/src/pyopenvino/utils/utils.hpp index 2a7b6505269535..224b70bb1fa176 100644 --- a/src/bindings/python/src/pyopenvino/utils/utils.hpp +++ b/src/bindings/python/src/pyopenvino/utils/utils.hpp @@ -81,6 +81,8 @@ class MemoryBuffer : public std::streambuf { std::string convert_path_to_string(const py::object& path); + std::shared_ptr convert_to_model(const py::object& obj); + void deprecation_warning(const std::string& function_name, const std::string& version = std::string(), const std::string& message = std::string(), int stacklevel=2); void raise_not_implemented(); diff --git a/src/bindings/python/tests/test_runtime/test_model.py b/src/bindings/python/tests/test_runtime/test_model.py index 0ae592b2d1dff5..425cdb97129c69 100644 --- a/src/bindings/python/tests/test_runtime/test_model.py +++ b/src/bindings/python/tests/test_runtime/test_model.py @@ -3,11 +3,13 @@ # SPDX-License-Identifier: Apache-2.0 import os +import sys import numpy as np import pytest import math from contextlib import nullcontext as does_not_raise from copy import copy +import tempfile import openvino.runtime.opset13 as ops from openvino import ( @@ -801,13 +803,49 @@ def test_model_add_remove_variable(): def test_save_model_with_none(): - with pytest.raises(AttributeError) as e: + with pytest.raises(TypeError) as e: save_model(model=None, output_model="model.xml") - assert "'model' argument is required and cannot be None." in str(e.value) + assert "Please provide a valid openvino.Model instance." in str(e.value) def test_copy_failed(): model = generate_add_model() with pytest.raises(TypeError) as e: copy(model) - assert "Cannot copy 'openvino.runtime.Model. Please, use deepcopy instead." in str(e.value) + assert "Cannot copy 'openvino.runtime.Model'. Please, use deepcopy instead." in str(e.value) + + +def test_model_attr_not_found(): + model = generate_add_model() + with pytest.raises(AttributeError) as e: + _ = model.not_found_attr + assert "'openvino._pyopenvino.Model' object has no attribute 'not_found_attr'" in str(e.value) + + +def test_model_with_statement(): + mem_model = generate_model_with_memory(input_shape=Shape([2, 1]), data_type=Type.f32) + with tempfile.TemporaryDirectory() as model_save_dir: + save_model(mem_model, f"{model_save_dir}/model.xml") + + with Core().read_model(f"{model_save_dir}/model.xml") as model: + assert mem_model.friendly_name == model.friendly_name + + with pytest.raises(AttributeError): + save_model(model, f"{model_save_dir}/model.xml") + + # Behavior after exiting the context manager + with mem_model as model: + pass + assert isinstance(mem_model, Model) + with pytest.raises(AttributeError, match="attribute is no longer accessible."): + model.friendly_name + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") +def test_tempdir_save_load_error(): + # Generate a model with stateful components, ensuring the .bin file will be non-empty after saving + mem_model = generate_model_with_memory(input_shape=Shape([2, 1]), data_type=Type.f32) + with pytest.raises((NotADirectoryError, PermissionError)): + with tempfile.TemporaryDirectory() as model_save_dir: + save_model(mem_model, f"{model_save_dir}/model.xml") + _ = Core().read_model(f"{model_save_dir}/model.xml")