def _read(self, data):
        """
        Fill in the node/edge features field in data.

        Parameters
        ----------
        data : MiniBatch
            An instance of :class:`MiniBatch`. Even if 'node_feature' or
            'edge_feature' is already filled, it will be overwritten for
            overlapping features.

        Returns
        -------
        MiniBatch
            An instance of :class:`MiniBatch` filled with required features.
        """
        node_features = {}
        num_layers = data.num_layers()
        edge_features = [{} for _ in range(num_layers)]
        is_heterogeneous = isinstance(
            self.node_feature_keys, Dict
        ) or isinstance(self.edge_feature_keys, Dict)
        # Read Node features.
        input_nodes = data.node_ids()

        def read_helper(feature_key, index):
            if self.max_num_stages > 0:
                feature = self.feature_store[feature_key]
                num_stages = feature.read_async_num_stages(index.device)
                if num_stages > 0:
                    return (feature.read_async(index), num_stages)
                else:  # Asynchronicity is not needed, compute in _final_stage.

                    class _Waiter:
                        def __init__(self, feature, index):
                            self.feature = feature
                            self.index = index

                        def wait(self):
                            """Returns the stored value when invoked."""
                            result = self.feature.read(self.index)
                            # Ensure there is no memory leak.
                            self.feature = self.index = None
                            return result

                    return (_Waiter(feature, index), 0)
            else:
                domain, type_name, feature_name = feature_key
                return self.feature_store.read(
                    domain, type_name, feature_name, index
                )

        if self.node_feature_keys and input_nodes is not None:
            if is_heterogeneous:
                for type_name, nodes in input_nodes.items():
                    if type_name not in self.node_feature_keys or nodes is None:
                        continue
                    for feature_name in self.node_feature_keys[type_name]:
                        node_features[(type_name, feature_name)] = read_helper(
                            ("node", type_name, feature_name), nodes
                        )
            else:
                for feature_name in self.node_feature_keys:
                    node_features[feature_name] = read_helper(
                        ("node", None, feature_name), input_nodes
                    )
        # Read Edge features.
        if self.edge_feature_keys and num_layers > 0:
            for i in range(num_layers):
                original_edge_ids = data.edge_ids(i)
                if is_heterogeneous:
                    # Convert edge type to string.
                    original_edge_ids = {
                        (
                            etype_tuple_to_str(key)
                            if isinstance(key, tuple)
                            else key
                        ): value
                        for key, value in original_edge_ids.items()
                    }
                    for type_name, edges in original_edge_ids.items():
                        if (
                            type_name not in self.edge_feature_keys
                            or edges is None
                        ):
                            continue
                        for feature_name in self.edge_feature_keys[type_name]:
                            edge_features[i][
                                (type_name, feature_name)
                            ] = read_helper(
                                ("edge", type_name, feature_name), edges
                            )
                else:
                    for feature_name in self.edge_feature_keys:
                        edge_features[i][feature_name] = read_helper(
                            ("edge", None, feature_name), original_edge_ids
                        )
        data.set_node_features(node_features)
        data.set_edge_features(edge_features)
        return data

        Fill in the node/edge features field in data.

        Parameters
        ----------
        data : MiniBatch
            An instance of :class:`MiniBatch`. Even if 'node_feature' or
            'edge_feature' is already filled, it will be overwritten for
            overlapping features.

        Returns
        -------
        MiniBatch
            An instance of :class:`MiniBatch` filled with required features.
        
python/dgl/graphbolt/feature_fetcher.py