Repository: onnx/onnx
Pull request: Expose check_function in Python API
### Description
<!-- - Describe your changes. -->
- Expose check_function in Python API.
- Add a test
- Add more functions in API doc on Web.

### Motivation and Context
<!-- - Why is this change required? What problem does it solve? -->
<!-- - If it fixes an open issue, please link to the issue here. -->
Closes https://github.com/onnx/onnx/issues/4700.



Problem statement:
[Feature request] ONNX checker checks function proto validity 
### System information

_No response_

### What is the problem that this feature solves?

ONNX checker currently checks model proto validity with check_model_proto. I would be very helpful to have a similar API that checks for function proto validity.

This greatly helps testing function libraries built with ONNX script.

### Alternatives considered

Make a model. Not all functions can be directly converted into a Model. 

### Describe the feature

_No response_

### Will this influence the current api (Y/N)?

_No response_

### Feature Area

checker

### Are you willing to contribute it (Y/N)

None

### Notes

_No response_

Hints from issue discussion:
Hi @justinchuby,
Thanks for proposing this. Currently ONNX only provides [check_function](https://github.com/onnx/onnx/blob/main/onnx/checker.cc#L867-L956) in C++. Does the current check meet your requirement? If it is useful, we can also expose a Python API for it.
A Python api would be great/necessary for unit testing the function library.

Commit message: add a test and doc

Signed-off-by: Chun-Wei Chen <jacky82226@gmail.com>

Patch to review:
diff --git a/docs/docsgen/source/api/checker.rst b/docs/docsgen/source/api/checker.rst
index 3fbb08a55ad..95fba77a499 100644
--- a/docs/docsgen/source/api/checker.rst
+++ b/docs/docsgen/source/api/checker.rst
@@ -5,6 +5,36 @@ onnx.checker
 .. contents::
     :local:
 
+check_value_info
++++++++++++
+
+.. autofunction:: onnx.checker.check_value_info
+
+check_tensor
++++++++++++
+
+.. autofunction:: onnx.checker.check_tensor
+
+check_attribute
++++++++++++
+
+.. autofunction:: onnx.checker.check_attribute
+
+check_node
++++++++++++
+
+.. autofunction:: onnx.checker.check_node
+
+check_function
++++++++++++
+
+.. autofunction:: onnx.checker.check_function
+
+check_graph
++++++++++++
+
+.. autofunction:: onnx.checker.check_graph
+
 check_model
 +++++++++++
 
diff --git a/onnx/checker.py b/onnx/checker.py
index b842d7765f4..955ea22adfe 100644
--- a/onnx/checker.py
+++ b/onnx/checker.py
@@ -17,6 +17,7 @@
 from onnx import (
     IR_VERSION,
     AttributeProto,
+    FunctionProto,
     GraphProto,
     ModelProto,
     NodeProto,
@@ -85,6 +86,13 @@ def check_node(node: NodeProto, ctx: C.CheckerContext = DEFAULT_CONTEXT) -> None
     pass
 
 
+@_create_checker(FunctionProto)
+def check_function(
+    graph: FunctionProto, ctx: C.CheckerContext = DEFAULT_CONTEXT
+) -> None:
+    pass
+
+
 @_create_checker(GraphProto)
 def check_graph(graph: GraphProto, ctx: C.CheckerContext = DEFAULT_CONTEXT) -> None:
     pass
diff --git a/onnx/cpp2py_export.cc b/onnx/cpp2py_export.cc
index f6e1738e075..b88e5130771 100644
--- a/onnx/cpp2py_export.cc
+++ b/onnx/cpp2py_export.cc
@@ -372,6 +372,12 @@ PYBIND11_MODULE(onnx_cpp2py_export, onnx_cpp2py_export) {
     checker::check_node(proto, ctx, lex_ctx);
   });
 
+  checker.def("check_function", [](const py::bytes& bytes, const checker::CheckerContext& ctx) -> void {
+    FunctionProto proto{};
+    ParseProtoFromPyBytes(&proto, bytes);
+    checker::check_function(proto, ctx, checker::LexicalScopeContext());
+  });
+
   checker.def("check_graph", [](const py::bytes& bytes, const checker::CheckerContext& ctx) -> void {
     GraphProto proto{};
     ParseProtoFromPyBytes(&proto, bytes);
diff --git a/onnx/onnx_cpp2py_export/checker.pyi b/onnx/onnx_cpp2py_export/checker.pyi
index e866d02f4a7..65e116b0e43 100644
--- a/onnx/onnx_cpp2py_export/checker.pyi
+++ b/onnx/onnx_cpp2py_export/checker.pyi
@@ -11,6 +11,7 @@ def check_tensor(bytes: bytes, checker_context: CheckerContext) -> None: ...
 def check_sparse_tensor(bytes: bytes, checker_context: CheckerContext) -> None: ...
 def check_attribute(bytes: bytes, checker_context: CheckerContext) -> None: ...
 def check_node(bytes: bytes, checker_context: CheckerContext) -> None: ...
+def check_function(bytes: bytes, checker_context: CheckerContext) -> None: ...
 def check_graph(bytes: bytes, checker_context: CheckerContext) -> None: ...
 def check_model(bytes: bytes, full_check: bool) -> None: ...
 def check_model_path(path: str, full_check: bool) -> None: ...
diff --git a/onnx/test/checker_test.py b/onnx/test/checker_test.py
index c4f69bf1e3f..75851c522bc 100644
--- a/onnx/test/checker_test.py
+++ b/onnx/test/checker_test.py
@@ -66,6 +66,31 @@ def test_check_node_input_marked_optional(self) -> None:
         node = helper.make_node("Relu", [""], ["Y"], name="test")
         self.assertRaises(checker.ValidationError, checker.check_node, node)
 
+    def test_check_function_nested(self) -> None:
+        func_domain = "local"
+        func_nested_opset_imports = [
+            onnx.helper.make_opsetid("", 14),
+            onnx.helper.make_opsetid(func_domain, 1),
+        ]
+        # nested identity/add function
+        func_nested_identity_add_name = "func_nested_identity_add"
+        func_nested_identity_add_inputs = ["a", "b"]
+        func_nested_identity_add_outputs = ["c"]
+        func_nested_identity_add_nodes = [
+            onnx.helper.make_node("func_identity", ["a"], ["a1"], domain=func_domain),
+            onnx.helper.make_node("func_identity", ["b"], ["b1"], domain=func_domain),
+            onnx.helper.make_node("func_add", ["a1", "b1"], ["c"], domain=func_domain),
+        ]
+        func_nested_identity_add = onnx.helper.make_function(
+            func_domain,
+            func_nested_identity_add_name,
+            func_nested_identity_add_inputs,
+            func_nested_identity_add_outputs,
+            func_nested_identity_add_nodes,
+            func_nested_opset_imports,
+        )
+        onnx.checker.check_function(func_nested_identity_add)
+
     def test_check_graph_ir_version_3(self) -> None:
         ctx = C.CheckerContext()
         ctx.ir_version = 3


Code review comments:
diff_hunk: @@ -5,6 +5,36 @@ onnx.checker
 .. contents::
     :local:
 
+check_value_info
path: docs/docsgen/source/api/checker.rst
text: @user1:
`C.CheckerContext`  should be exposed as well.

@author:
Just exposes it. Not sure whether the usage is correct though. PTAL. Thanks!

diff_hunk: @@ -5,6 +5,36 @@ onnx.checker
 .. contents::
     :local:
 
+check_value_info
++++++++++++
path: docs/docsgen/source/api/checker.rst
text: @user1:
The underline should have the same length.

@author:
Updated. Thanks!