Repository: pyg-team/pytorch_geometric
Pull request #8754 (2024-01-12 16:29:06)

Type Representation <> Type conversion

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8754?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) Report
Attention: `1 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`13a1fa8`)](https://app.codecov.io/gh/pyg-team/pytorch_geometric/commit/13a1fa87a61a303c8d8c557c16f3e4f4464a77e1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) 89.35% compared to head [(`fab9f88`)](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8754?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) 89.36%.

| [Files](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8754?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) | Patch % | Lines |
|---|---|---|
| [torch\_geometric/nn/conv/utils/inspector.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8754?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL25uL2NvbnYvdXRpbHMvaW5zcGVjdG9yLnB5) | 97.22% | [1 Missing :warning: ](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8754?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) |

<details><summary>Additional details and impacted files</summary>


```diff
@@                  Coverage Diff                   @@
##           module_from_template2    #8754   +/-   ##
======================================================
  Coverage                  89.35%   89.36%           
======================================================
  Files                        482      482           
  Lines                      31203    31239   +36     
======================================================
+ Hits                       27882    27917   +35     
- Misses                      3321     3322    +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8754?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team).


Patch:
diff --git a/torch_geometric/nn/conv/fa_conv.py b/torch_geometric/nn/conv/fa_conv.py
index b844ec982..8fd2f1791 100644
--- a/torch_geometric/nn/conv/fa_conv.py
+++ b/torch_geometric/nn/conv/fa_conv.py
@@ -7,6 +7,7 @@ from torch import Tensor
 from torch_geometric.nn.conv import MessagePassing
 from torch_geometric.nn.conv.gcn_conv import gcn_norm
 from torch_geometric.nn.dense.linear import Linear
+from torch_geometric.typing import PairTensor  # noqa
 from torch_geometric.typing import (
     Adj,
     NoneType,
diff --git a/torch_geometric/nn/conv/gravnet_conv.py b/torch_geometric/nn/conv/gravnet_conv.py
index 6f025225a..e7afac36f 100644
--- a/torch_geometric/nn/conv/gravnet_conv.py
+++ b/torch_geometric/nn/conv/gravnet_conv.py
@@ -6,6 +6,7 @@ from torch import Tensor
 
 from torch_geometric.nn.conv import MessagePassing
 from torch_geometric.nn.dense.linear import Linear
+from torch_geometric.typing import OptPairTensor  # noqa
 from torch_geometric.typing import OptTensor, PairOptTensor, PairTensor
 
 try:
diff --git a/torch_geometric/nn/conv/message_passing.py b/torch_geometric/nn/conv/message_passing.py
index 1db2b4651..0a2a3213b 100644
--- a/torch_geometric/nn/conv/message_passing.py
+++ b/torch_geometric/nn/conv/message_passing.py
@@ -13,7 +13,6 @@ from typing import (
     OrderedDict,
     Set,
     Tuple,
-    Type,
     Union,
 )
 
@@ -22,11 +21,8 @@ from torch import Tensor
 from torch.utils.hooks import RemovableHandle
 
 from torch_geometric.nn.aggr import Aggregation
-from torch_geometric.nn.conv.propagate import (
-    module_from_template,
-    type_hint_to_str,
-)
-from torch_geometric.nn.conv.utils.inspector import Inspector
+from torch_geometric.nn.conv.propagate import module_from_template
+from torch_geometric.nn.conv.utils.inspector import Inspector, Signature
 from torch_geometric.nn.conv.utils.jit import class_from_module_repr
 from torch_geometric.nn.resolver import aggregation_resolver as aggr_resolver
 from torch_geometric.typing import Adj, Size, SparseTensor
@@ -178,16 +174,15 @@ class MessagePassing(torch.nn.Module):
         # Test code for performing on-the-fly TorchScript support:
         if getattr(self, 'jit_on_init', False):
             root_dir = osp.dirname(osp.realpath(__file__))
-            prop_types, prop_return_type = self._get_propagate_types()
+            signature = self._get_propagate_signature()
             prop_types = {
-                param.name: type_hint_to_str(param.type)
-                for param in prop_types.values()
+                param.name: param.type_repr
+                for param in signature.param_dict.values()
             }
-            prop_return_type = type_hint_to_str(prop_return_type)
             collect_types = self.inspector.get_flat_params(
                 ['message', 'aggregate', 'update'])
             collect_types = {
-                param.name: type_hint_to_str(param.type)
+                param.name: param.type_repr
                 for param in collect_types
             }
             module = module_from_template(
@@ -195,7 +190,7 @@ class MessagePassing(torch.nn.Module):
                 module=self.__module__,
                 template_path=osp.join(root_dir, 'propagate.jinja'),
                 propagate_types=prop_types,
-                propagate_return_type=prop_return_type,
+                propagate_return_type=signature.return_type_repr,
                 collect_types=collect_types,
                 message_args=self.inspector.get_param_names('message'),
                 aggregate_args=self.inspector.get_param_names('aggregate'),
@@ -868,17 +863,27 @@ class MessagePassing(torch.nn.Module):
 
     # TorchScript Support #####################################################
 
-    def _get_propagate_types(self) -> Tuple[Dict[str, Parameter], Type]:
+    def _get_propagate_signature(self) -> Signature:
         param_dict = self.inspector.get_params_from_method_call(
             'propagate', exclude=[0, 'edge_index', 'size'])
-        return_type = self.inspector.get_signature('update').return_type
-        return param_dict, return_type
+        update_signature = self.inspector.get_signature('update')
+
+        return Signature(
+            param_dict=param_dict,
+            return_type=update_signature.return_type,
+            return_type_repr=update_signature.return_type_repr,
+        )
 
-    def _get_edge_updater_types(self) -> Tuple[Dict[str, Parameter], Type]:
+    def _get_edge_updater_signature(self) -> Signature:
         param_dict = self.inspector.get_params_from_method_call(
             'edge_updater', exclude=[0, 'edge_index'])
-        return_type = self.inspector.get_signature('edge_update').return_type
-        return param_dict, return_type
+        edge_update_signature = self.inspector.get_signature('edge_update')
+
+        return Signature(
+            param_dict=param_dict,
+            return_type=edge_update_signature.return_type,
+            return_type_repr=edge_update_signature.return_type_repr,
+        )
 
     @torch.jit.unused
     def jittable(self, typing: Optional[str] = None) -> 'MessagePassing':
@@ -898,33 +903,31 @@ class MessagePassing(torch.nn.Module):
         source = self.inspector.source
 
         # Find and parse `propagate()` types to format `{arg1: type1, ...}`.
-        prop_types, prop_return_type = self._get_propagate_types()
+        prop_signature = self._get_propagate_signature()
         prop_types = {
-            param.name: type_hint_to_str(param.type)
-            for param in prop_types.values()
+            param.name: param.type_repr
+            for param in prop_signature.param_dict.values()
         }
-        prop_return_type = type_hint_to_str(prop_return_type)
 
         # Find and parse `edge_updater` types to format `{arg1: type1, ...}`.
-        edge_types, edge_return_type = self._get_edge_updater_types()
+        edge_signature = self._get_edge_updater_signature()
         edge_types = {
-            param.name: type_hint_to_str(param.type)
-            for param in edge_types.values()
+            param.name: param.type_repr
+            for param in edge_signature.param_dict.values()
         }
-        edge_return_type = type_hint_to_str(edge_return_type)
 
         # Parse `_collect()` types to format `{arg:1, type1, ...}`.
         collect_types = self.inspector.get_flat_params(
             ['message', 'aggregate', 'update'])
         collect_types = {
-            param.name: type_hint_to_str(param.type)
+            param.name: param.type_repr
             for param in collect_types
         }
 
         # Parse `_edge_collect()` types to format `{arg:1, type1, ...}`.
         edge_collect_types = self.inspector.get_params('edge_update')
         edge_collect_types = {
-            param.name: type_hint_to_str(param.type)
+            param.name: param.type_repr
             for param in edge_collect_types
         }
 
@@ -951,7 +954,7 @@ class MessagePassing(torch.nn.Module):
             cls_name=cls_name,
             parent_cls_name=self.__class__.__name__,
             prop_types=prop_types,
-            prop_return_type=prop_return_type,
+            prop_return_type=prop_signature.return_type_repr,
             fuse=self.fuse,
             collect_types=collect_types,
             user_args=self._user_args,
@@ -964,7 +967,7 @@ class MessagePassing(torch.nn.Module):
             edge_collect_types=edge_collect_types,
             edge_update_args=self.inspector.get_param_names('edge_update'),
             edge_updater_types=edge_types,
-            edge_updater_return_type=edge_return_type,
+            edge_updater_return_type=edge_signature.return_type_repr,
             forward_repr=forward_repr,
         )
         # Instantiate a class from the rendered JIT module representation.
diff --git a/torch_geometric/nn/conv/utils/inspector.py b/torch_geometric/nn/conv/utils/inspector.py
index 176510434..b6d78ac0c 100644
--- a/torch_geometric/nn/conv/utils/inspector.py
+++ b/torch_geometric/nn/conv/utils/inspector.py
@@ -1,5 +1,7 @@
 import inspect
 import re
+import sys
+import typing
 from typing import Any, Callable, Dict, List, NamedTuple, Optional, Type, Union
 
 from torch import Tensor
@@ -7,13 +9,15 @@ from torch import Tensor
 
 class Parameter(NamedTuple):
     name: str
-    type: Union[Type, str]
+    type: Type
+    type_repr: str
     default: Any
 
 
 class Signature(NamedTuple):
     param_dict: Dict[str, Parameter]
     return_type: Type
+    return_type_repr: str
 
 
 class Inspector:
@@ -28,9 +32,64 @@ class Inspector:
         self._signature_dict: Dict[str, Signature] = {}
         self._source: Optional[str] = None
 
+    @property
+    def _globals(self) -> Dict[str, Any]:
+        return sys.modules[self._cls.__module__].__dict__
+
     def __repr__(self) -> str:
         return f'{self.__class__.__name__}({self._cls.__name__})'
 
+    def eval_type(self, value: Union[str, Type]) -> Type:
+        r"""Returns the type hint of a string."""
+        if isinstance(value, str):
+            value = typing.ForwardRef(value)
+        return typing._eval_type(value, self._globals, None)
+
+    def type_repr(self, obj: Union[Type, str]) -> str:
+        r"""Returns the type hint representation of an object."""
+        def _get_name(name: str, module: str) -> str:
+            return name if name in self._globals else f'{module}.{name}'
+
+        if isinstance(obj, str):
+            return obj
+
+        if obj is type(None):
+            return 'None'
+
+        if obj is ...:
+            return '...'
+
+        if obj.__module__ == 'typing':  # Special logic for `typing.*` types:
+            name = obj._name
+            if name is None:  # In some cases, `_name` is not populated.
+                name = str(obj.__origin__).split('.')[-1]
+
+            args = getattr(obj, '__args__', None)
+            if args is None or len(args) == 0:
+                return _get_name(name, obj.__module__)
+            if all(isinstance(arg, typing.TypeVar) for arg in args):
+                return _get_name(name, obj.__module__)
+
+            # Convert `Union[*, None]` to `Optional[*]`.
+            # This is only necessary for old Python versions, e.g. 3.8.
+            # Only convert to `Optional` if `Optional` is actually importable:
+            if (name == 'Union' and len(args) == 2
+                    and any([arg is type(None) for arg in args])
+                    and ('Optional' in self._globals
+                         or obj.__module__ in self._globals or True)):  # TODO
+                name = 'Optional'
+
+            if name == 'Optional':  # Remove `None` from `Optional` arguments:
+                args = [arg for arg in obj.__args__ if arg is not type(None)]
+
+            args_repr = ', '.join([self.type_repr(arg) for arg in args])
+            return f'{_get_name(name, obj.__module__)}[{args_repr}]'
+
+        if obj.__module__ == 'builtins':
+            return obj.__qualname__
+
+        return _get_name(obj.__qualname__, obj.__module__)
+
     def implements(self, func_name: str) -> bool:
         r"""Returns :obj:`True` in case the inspected class implements the
         :obj:`func_name` method.
@@ -73,18 +132,25 @@ class Inspector:
                 continue
 
             param_type = param.annotation
+            # Mimic TorchScript to auto-infer `Tensor` on non-present types:
+            param_type = Tensor if param_type is inspect._empty else param_type
+
             param_dict[param.name] = Parameter(
                 name=param.name,
-                # Mimic TorchScript which auto-infers `Tensor` on empty types:
-                type=Tensor if param_type is inspect._empty else param_type,
+                type=self.eval_type(param_type),
+                type_repr=self.type_repr(param_type),
                 default=param.default,
             )
 
         return_type = signature.return_annotation
+        # Mimic TorchScript to auto-infer `Tensor` on non-present types:
         return_type = Tensor if return_type is inspect._empty else return_type
 
         self._signature_dict[func.__name__] = Signature(
-            param_dict, return_type)
+            param_dict=param_dict,
+            return_type=self.eval_type(return_type),
+            return_type_repr=self.type_repr(return_type),
+        )
 
         return self._signature_dict[func.__name__]
 
@@ -110,9 +176,16 @@ class Inspector:
         if exclude is None:
             return signature
 
-        param_dict = signature.param_dict
-        param_dict = {k: v for k, v in param_dict.items() if k not in exclude}
-        return Signature(param_dict, signature.return_type)
+        param_dict = {
+            name: param
+            for name, param in signature.param_dict.items()
+            if name not in exclude
+        }
+        return Signature(
+            param_dict=param_dict,
+            return_type=signature.return_type,
+            return_type_repr=signature.return_type_repr,
+        )
 
     def get_params(
         self,
@@ -165,8 +238,12 @@ class Inspector:
                     if default is inspect._empty:
                         default = param.default
 
-                    out_dict[param.name] = Parameter(  #
-                        param.name, param.type, default)
+                    out_dict[param.name] = Parameter(
+                        name=param.name,
+                        type=param.type,
+                        type_repr=param.type_repr,
+                        default=default,
+                    )
 
                 if expected is None:
                     out_dict[param.name] = param
@@ -264,41 +341,55 @@ class Inspector:
             if not isinstance(type_dict, dict):
                 raise ValueError(f"'{func_name}_type' is expected to be a "
                                  f"dictionary (got '{type(type_dict)}')")
-            return {
-                name: Parameter(name, param_type, inspect._empty)
-                for name, param_type in type_dict.items()
-            }
+
+            param_dict: Dict[str, Parameter] = {}
+            for name, param_type in type_dict.items():
+                param_dict[name] = Parameter(
+                    name=name,
+                    type=self.eval_type(param_type),
+                    type_repr=self.type_repr(param_type),
+                    default=inspect._empty,
+                )
+            return param_dict
 
         # (2) Find type annotation:
         match = find_parenthesis_content(self.source, f'{func_name}_type:')
         if match is not None:
             param_dict: Dict[str, Parameter] = {}
-            for type_repr in split(match, sep=','):
-                name_and_type = re.split(r'\s*:\s*', type_repr)
-                if len(name_and_type) != 2:
-                    raise ValueError(f"Could not parse the argument "
-                                     f"'{type_repr}' of the "
-                                     f"'{func_name}_type' annotation")
-                name, param_type = name_and_type[0], name_and_type[1]
-                param_dict[name] = Parameter(name, param_type, inspect._empty)
+            for arg in split(match, sep=','):
+                name_and_type_repr = re.split(r'\s*:\s*', arg)
+                if len(name_and_type_repr) != 2:
+                    raise ValueError(f"Could not parse the argument '{arg}' "
+                                     f"of the '{func_name}_type' annoitation")
+                name, type_repr = name_and_type_repr
+                param_dict[name] = Parameter(
+                    name=name,
+                    type=self.eval_type(type_repr),
+                    type_repr=type_repr,
+                    default=inspect._empty,
+                )
             return param_dict
 
         # (3) Parse the function call:
         match = find_parenthesis_content(self.source, f'self.{func_name}')
         if match is not None:
             param_dict: Dict[str, Parameter] = {}
-            for i, type_repr in enumerate(split(match, sep=',')):
+            for i, kwarg in enumerate(split(match, sep=',')):
                 if exclude is not None and i in exclude:
                     continue
-                name_and_content = re.split(r'\s*=\s*', type_repr)
+                name_and_content = re.split(r'\s*=\s*', kwarg)
                 if len(name_and_content) != 2:
                     raise ValueError(f"Could not parse the keyword argument "
-                                     f"'{type_repr}' of the "
-                                     f"'self.{func_name}' call")
-                name = name_and_content[0]
+                                     f"'{kwarg}' in 'self.{func_name}(...)'")
+                name, _ = name_and_content
                 if exclude is not None and name in exclude:
                     continue
-                param_dict[name] = Parameter(name, Tensor, inspect._empty)
+                param_dict[name] = Parameter(
+                    name=name,
+                    type=Tensor,
+                    type_repr=self.type_repr(Tensor),
+                    default=inspect._empty,
+                )
             return param_dict
 
         return {}  # (4) No function call found:


Test patch:
diff --git a/test/nn/conv/utils/test_inspector.py b/test/nn/conv/utils/test_inspector.py
index 55bd66523..943190343 100644
--- a/test/nn/conv/utils/test_inspector.py
+++ b/test/nn/conv/utils/test_inspector.py
@@ -1,4 +1,5 @@
 import inspect
+from typing import Any, Dict, Final, List, Optional, Set, Tuple, Union
 
 import torch
 from torch import Tensor
@@ -9,6 +10,39 @@ from torch_geometric.nn.conv.utils.inspector import (
     Parameter,
     Signature,
 )
+from torch_geometric.typing import OptPairTensor
+
+
+def test_eval_type() -> None:
+    inspector = Inspector(SAGEConv)
+
+    assert inspector.eval_type('Tensor') == Tensor
+    assert inspector.eval_type('List[Tensor]') == List[Tensor]
+    assert inspector.eval_type('Tuple[Tensor, int]') == Tuple[Tensor, int]
+    assert inspector.eval_type('Tuple[int, ...]') == Tuple[int, ...]
+
+
+def test_type_repr() -> None:
+    inspector = Inspector(SAGEConv)
+
+    assert inspector.type_repr(Any) == 'typing.Any'
+    assert inspector.type_repr(Final) == 'typing.Final'
+    assert inspector.type_repr(OptPairTensor) == (
+        'Tuple[Tensor, Optional[Tensor]]')
+    assert inspector.type_repr(
+        Final[Optional[Tensor]]) == ('typing.Final[Optional[Tensor]]')
+    assert inspector.type_repr(Union[None, Tensor]) == 'Optional[Tensor]'
+    assert inspector.type_repr(Optional[Tensor]) == 'Optional[Tensor]'
+    assert inspector.type_repr(Set[Tensor]) == 'typing.Set[Tensor]'
+    assert inspector.type_repr(List) == 'List'
+    assert inspector.type_repr(Tuple) == 'Tuple'
+    assert inspector.type_repr(Set) == 'typing.Set'
+    assert inspector.type_repr(Dict) == 'typing.Dict'
+    assert inspector.type_repr(Dict[str, Tuple[Tensor, Tensor]]) == (  #
+        'typing.Dict[str, Tuple[Tensor, Tensor]]')
+    assert inspector.type_repr(Tuple[int, ...]) == 'Tuple[int, ...]'
+    assert inspector.type_repr(Union[int, str, None]) == (  #
+        'Union[int, str, None]')
 
 
 def test_inspector_sage_conv() -> None:
@@ -19,10 +53,12 @@ def test_inspector_sage_conv() -> None:
 
     out = inspector.inspect_signature(SAGEConv.message)
     assert isinstance(out, Signature)
-    assert out.param_dict == {'x_j': Parameter('x_j', Tensor, inspect._empty)}
+    assert out.param_dict == {
+        'x_j': Parameter('x_j', Tensor, 'Tensor', inspect._empty)
+    }
     assert out.return_type == Tensor
     assert inspector.get_flat_params(['message', 'message']) == [
-        Parameter('x_j', Tensor, inspect._empty),
+        Parameter('x_j', Tensor, 'Tensor', inspect._empty),
     ]
     assert inspector.get_flat_param_names(['message']) == ['x_j']
 
@@ -32,7 +68,7 @@ def test_inspector_sage_conv() -> None:
     assert torch.allclose(data['x_j'], kwargs['x_j'])
 
     assert inspector.get_params_from_method_call(SAGEConv.propagate) == {
-        'x': Parameter('x', 'OptPairTensor', inspect._empty),
+        'x': Parameter('x', OptPairTensor, 'OptPairTensor', inspect._empty),
     }
 
 
@@ -45,13 +81,13 @@ def test_inspector_gat_conv() -> None:
     out = inspector.inspect_signature(GATConv.message)
     assert isinstance(out, Signature)
     assert out.param_dict == {
-        'x_j': Parameter('x_j', Tensor, inspect._empty),
-        'alpha': Parameter('alpha', Tensor, inspect._empty),
+        'x_j': Parameter('x_j', Tensor, 'Tensor', inspect._empty),
+        'alpha': Parameter('alpha', Tensor, 'Tensor', inspect._empty),
     }
     assert out.return_type == Tensor
     assert inspector.get_flat_params(['message', 'message']) == [
-        Parameter('x_j', Tensor, inspect._empty),
-        Parameter('alpha', Tensor, inspect._empty),
+        Parameter('x_j', Tensor, 'Tensor', inspect._empty),
+        Parameter('alpha', Tensor, 'Tensor', inspect._empty),
     ]
     assert inspector.get_flat_param_names(['message']) == ['x_j', 'alpha']
 
@@ -62,8 +98,8 @@ def test_inspector_gat_conv() -> None:
     assert torch.allclose(data['alpha'], kwargs['alpha'])
 
     assert inspector.get_params_from_method_call(SAGEConv.propagate) == {
-        'x': Parameter('x', 'OptPairTensor', inspect._empty),
-        'alpha': Parameter('alpha', 'Tensor', inspect._empty),
+        'x': Parameter('x', OptPairTensor, 'OptPairTensor', inspect._empty),
+        'alpha': Parameter('alpha', Tensor, 'Tensor', inspect._empty),
     }
 
 
@@ -73,7 +109,7 @@ def test_get_params_from_method_call() -> None:
 
     inspector = Inspector(FromMethodCall1)
     assert inspector.get_params_from_method_call('propagate') == {
-        'x': Parameter('x', Tensor, inspect._empty),
+        'x': Parameter('x', Tensor, 'Tensor', inspect._empty),
     }
 
     class FromMethodCall2:
@@ -82,7 +118,7 @@ def test_get_params_from_method_call() -> None:
 
     inspector = Inspector(FromMethodCall2)
     assert inspector.get_params_from_method_call('propagate') == {
-        'x': Parameter('x', 'Tensor', inspect._empty),
+        'x': Parameter('x', Tensor, 'Tensor', inspect._empty),
     }
 
     class FromMethodCall3:
@@ -96,7 +132,7 @@ def test_get_params_from_method_call() -> None:
     inspector = Inspector(FromMethodCall3)
     exclude = [0, 'size']
     assert inspector.get_params_from_method_call('propagate', exclude) == {
-        'x': Parameter('x', Tensor, inspect._empty),
+        'x': Parameter('x', Tensor, 'Tensor', inspect._empty),
     }
 
     class FromMethodCall4: