# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Various ways of selecting operations and tensors in a graph."""

from __future__ import annotations

import re

from six import iteritems
from six import string_types

from . import util
import tensorflow as tf
from tensorflow.python.framework import ops as tf_ops


__all__ = [
    "can_be_regex",
    "make_regex",
    "filter_ts",
    "filter_ts_from_regex",
    "filter_ops",
    "filter_ops_from_regex",
    "get_name_scope_ops",
    "check_cios",
    "get_ops_ios",
    "compute_boundary_ts",
    "get_within_boundary_ops",
    "get_forward_walk_ops",
    "get_backward_walk_ops",
    "get_walks_intersection_ops",
    "get_walks_union_ops",
    "select_ops",
    "select_ts",
    "select_ops_and_ts",
]

_RE_TYPE = type(re.compile(""))


def can_be_regex(obj):
    """Return True if obj can be turned into a regular expression."""
    return isinstance(obj, string_types + (_RE_TYPE,))


def make_regex(obj):
    """Return a compiled regular expression.

    Args:
      obj: a string or a regular expression.
    Returns:
      A compiled regular expression.
    Raises:
      ValueError: if obj could not be converted to a regular expression.
    """
    if not can_be_regex(obj):
        raise ValueError("Expected a string or a regex, got: {}".format(type(obj)))

    if isinstance(obj, string_types):
        return re.compile(obj)
    else:
        return obj


def _get_input_ts(ops):
    """Compute the list of unique input tensors of all the op in ops.

    Args:
      ops: an object convertible to a list of `tf.Operation`.
    Returns:
      The list of unique input tensors of all the op in ops.
    Raises:
      TypeError: if ops cannot be converted to a list of `tf.Operation`.
    """
    ops = util.make_list_of_op(ops)
    ts = []
    ts_set = set()
    for op in ops:
        for t in op.inputs:
            if t not in ts_set:
                ts.append(t)
                ts_set.add(t)
    return ts


def _get_output_ts(ops):
    """Compute the list of unique output tensors of all the op in ops.

    Args:
      ops: an object convertible to a list of tf.Operation.
    Returns:
      The list of unique output tensors of all the op in ops.
    Raises:
      TypeError: if ops cannot be converted to a list of tf.Operation.
    """
    ops = util.make_list_of_op(ops)
    ts = []
    for op in ops:
        ts += op.outputs
    return ts


def filter_ts(ops, positive_filter):
    """Get all the tensors which are input or output of an op in ops.

    Args:
      ops: an object convertible to a list of `tf.Operation`.
      positive_filter: a function deciding whether to keep a tensor or not.
        If `True`, all the tensors are returned.
    Returns:
      A list of `tf.Tensor`.
    Raises:
      TypeError: if ops cannot be converted to a list of `tf.Operation`.
    """
    ops = util.make_list_of_op(ops)
    ts = _get_input_ts(ops)
    util.concatenate_unique(ts, _get_output_ts(ops))
    if positive_filter is not True:
        ts = [t for t in ts if positive_filter(t)]
    return ts


def filter_ts_from_regex(ops, regex):
    r"""Get all the tensors linked to ops that match the given regex.

    Args:
      ops: an object convertible to a list of tf.Operation.
      regex: a regular expression matching the tensors' name.
        For example, "^foo(/.*)?:\d+$" will match all the tensors in the "foo"
        scope.
    Returns:
      A list of tf.Tensor.
    Raises:
      TypeError: if ops cannot be converted to a list of tf.Operation.
    """
    ops = util.make_list_of_op(ops)
    regex_obj = make_regex(regex)
    return filter_ts(ops, positive_filter=lambda op: regex_obj.search(op.name))


def filter_ops(ops, positive_filter):
    """Get the ops passing the given filter.

    Args:
      ops: an object convertible to a list of tf.Operation.
      positive_filter: a function deciding where to keep an operation or not.
        If True, all the operations are returned.
    Returns:
      A list of selected tf.Operation.
    Raises:
      TypeError: if ops cannot be converted to a list of tf.Operation.
    """
    ops = util.make_list_of_op(ops)
    if positive_filter is not True:  # pylint: disable=g-explicit-bool-comparison
        ops = [op for op in ops if positive_filter(op)]
    return ops


def filter_ops_from_regex(ops, regex):
    """Get all the operations that match the given regex.

    Args:
      ops: an object convertible to a list of `tf.Operation`.
      regex: a regular expression matching the operation's name.
        For example, `"^foo(/.*)?$"` will match all the operations in the "foo"
        scope.
    Returns:
      A list of `tf.Operation`.
    Raises:
      TypeError: if ops cannot be converted to a list of `tf.Operation`.
    """
    ops = util.make_list_of_op(ops)
    regex_obj = make_regex(regex)
    return filter_ops(ops, lambda op: regex_obj.search(op.name))


def get_name_scope_ops(ops, scope):
    """Get all the operations under the given scope path.

    Args:
      ops: an object convertible to a list of tf.Operation.
      scope: a scope path.
    Returns:
      A list of tf.Operation.
    Raises:
      TypeError: if ops cannot be converted to a list of tf.Operation.
    """
    if scope and scope[-1] == "/":
        scope = scope[:-1]
    return filter_ops_from_regex(ops, "^{}(/.*)?$".format(scope))


def check_cios(control_inputs=False, control_outputs=None, control_ios=None):
    """Do various check on control_inputs and control_outputs.

    Args:
      control_inputs: A boolean indicating whether control inputs are enabled.
      control_outputs: An instance of util.ControlOutputs or None. If not None,
        control outputs are enabled.
      control_ios:  An instance of util.ControlOutputs or None. If not None, both
        control inputs and control outputs are enabled. This is equivalent to set
        control_inputs to True and control_outputs to the util.ControlOutputs
        instance.
    Returns:
      A tuple `(control_inputs, control_outputs)` where:
        `control_inputs` is a boolean indicating whether to use control inputs.
        `control_outputs` is an instance of util.ControlOutputs or None
    Raises:
      ValueError: if control_inputs is an instance of util.ControlOutputs but
        control_outputs is not None
      TypeError: if control_outputs is not None and is not a util.ControlOutputs.
    """
    if control_ios is not None:
        if not isinstance(control_ios, util.ControlOutputs):
            raise TypeError("Expected a util.ControlOutputs, got: {}".format(type(control_ios)))
        if control_outputs is not None:
            raise ValueError("control_outputs should be None when using control_ios.")
        control_inputs = True
        control_outputs = control_ios
    elif control_outputs is not None:
        if not isinstance(control_outputs, util.ControlOutputs):
            raise TypeError("Expected a util.ControlOutputs, got: {}".format(type(control_outputs)))

    if control_outputs is not None:
        control_outputs.update()
    return control_inputs, control_outputs


def get_ops_ios(ops, control_inputs=False, control_outputs=None, control_ios=None):
    """Return all the `tf.Operation` which are connected to an op in ops.

    Args:
      ops: an object convertible to a list of `tf.Operation`.
      control_inputs: A boolean indicating whether control inputs are enabled.
      control_outputs: An instance of `util.ControlOutputs` or `None`. If not
        `None`, control outputs are enabled.
      control_ios:  An instance of `util.ControlOutputs` or `None`. If not `None`,
        both control inputs and control outputs are enabled. This is equivalent to
        set `control_inputs` to `True` and `control_outputs` to the
        `util.ControlOutputs` instance.
    Returns:
      All the `tf.Operation` surrounding the given ops.
    Raises:
      TypeError: if `ops` cannot be converted to a list of `tf.Operation`.
    """
    control_inputs, control_outputs = check_cios(control_inputs, control_outputs, control_ios)
    ops = util.make_list_of_op(ops)
    res = []
    for op in ops:
        util.concatenate_unique(res, [t.op for t in op.inputs])
        for t in op.outputs:
            util.concatenate_unique(res, t.consumers())
        if control_outputs is not None:
            util.concatenate_unique(res, control_outputs.get(op))
        if control_inputs:
            util.concatenate_unique(res, op.control_inputs)
    return res


def compute_boundary_ts(ops):
    """Compute the tensors at the boundary of a set of ops.

    This function looks at all the tensors connected to the given ops (in/out)
    and classify them into three categories:
    1) input tensors: tensors whose generating operation is not in ops.
    2) output tensors: tensors whose consumer operations are not in ops
    3) inside tensors: tensors which are neither input nor output tensors.

    Note that a tensor can be both an inside tensor and an output tensor if it is
    consumed by operations both outside and inside of `ops`.

    Args:
      ops: an object convertible to a list of tf.Operation.
    Returns:
      A tuple `(outside_input_ts, outside_output_ts, inside_ts)` where:
        `outside_input_ts` is a Python list of input tensors;
        `outside_output_ts` is a python list of output tensors;
        `inside_ts` is a python list of inside tensors.
      Since a tensor can be both an inside tensor and an output tensor,
      `outside_output_ts` and `inside_ts` might intersect.
    Raises:
      TypeError: if ops cannot be converted to a list of tf.Operation.
    """
    ops = util.make_list_of_op(ops)
    input_ts = _get_input_ts(ops)
    output_ts = _get_output_ts(ops)
    output_ts_set = frozenset(output_ts)
    ops_set = frozenset(ops)

    # Compute inside tensors.
    inside_ts = []
    only_inside_ts = []
    for t in input_ts:
        # Skip if the input tensor is not also an output tensor.
        if t not in output_ts_set:
            continue
        # Mark as "inside".
        inside_ts.append(t)
        # Mark as "only inside" if the tensor is not both inside and output.
        consumers = frozenset(t.consumers())
        if consumers - ops_set:
            continue
        only_inside_ts.append(t)

    inside_ts_set = frozenset(inside_ts)
    only_inside_ts_set = frozenset(only_inside_ts)
    outside_output_ts = [t for t in output_ts if t not in only_inside_ts_set]
    outside_input_ts = [t for t in input_ts if t not in inside_ts_set]
    return outside_input_ts, outside_output_ts, inside_ts


def get_within_boundary_ops(
    ops, seed_ops, boundary_ops=(), inclusive=True, control_inputs=False, control_outputs=None, control_ios=None
):
    """Return all the `tf.Operation` within the given boundary.

    Args:
      ops: an object convertible to a list of `tf.Operation`. those ops define the
        set in which to perform the operation (if a `tf.Graph` is given, it
        will be converted to the list of all its operations).
      seed_ops: the operations from which to start expanding.
      boundary_ops: the ops forming the boundary.
      inclusive: if `True`, the result will also include the boundary ops.
      control_inputs: A boolean indicating whether control inputs are enabled.
      control_outputs: An instance of `util.ControlOutputs` or `None`. If not
        `None`, control outputs are enabled.
      control_ios:  An instance of `util.ControlOutputs` or `None`. If not
        `None`, both control inputs and control outputs are enabled. This is
        equivalent to set control_inputs to True and control_outputs to
        the `util.ControlOutputs` instance.
    Returns:
      All the `tf.Operation` surrounding the given ops.
    Raises:
      TypeError: if `ops` or `seed_ops` cannot be converted to a list of
        `tf.Operation`.
      ValueError: if the boundary is intersecting with the seeds.
    """
    control_inputs, control_outputs = check_cios(control_inputs, control_outputs, control_ios)
    ops = util.make_list_of_op(ops)
    seed_ops = util.make_list_of_op(seed_ops, allow_graph=False)
    boundary_ops = set(util.make_list_of_op(boundary_ops))
    res = set(seed_ops)
    if boundary_ops & res:
        raise ValueError("Boundary is intersecting with the seeds.")
    wave = set(seed_ops)
    while wave:
        new_wave = set()
        ops_io = get_ops_ios(wave, control_inputs, control_outputs)
        for op in ops_io:
            if op in res:
                continue
            if op in boundary_ops:
                if inclusive:
                    res.add(op)
            else:
                new_wave.add(op)
        res.update(new_wave)
        wave = new_wave
    return [op for op in ops if op in res]


def get_forward_walk_ops(
    seed_ops, inclusive=True, within_ops=None, within_ops_fn=None, stop_at_ts=(), control_outputs=None
):
    """Do a forward graph walk and return all the visited ops.

    Args:
      seed_ops: an iterable of operations from which the forward graph
        walk starts. If a list of tensors is given instead, the seed_ops are set
        to be the consumers of those tensors.
      inclusive: if True the given seed_ops are also part of the resulting set.
      within_ops: an iterable of `tf.Operation` within which the search is
        restricted. If `within_ops` is `None`, the search is performed within
        the whole graph.
      within_ops_fn: if provided, a function on ops that should return True iff
        the op is within the graph traversal. This can be used along within_ops,
        in which case an op is within if it is also in within_ops.
      stop_at_ts: an iterable of tensors at which the graph walk stops.
      control_outputs: a `util.ControlOutputs` instance or None.
        If not `None`, it will be used while walking the graph forward.
    Returns:
      A Python set of all the `tf.Operation` ahead of `seed_ops`.
    Raises:
      TypeError: if `seed_ops` or `within_ops` cannot be converted to a list of
        `tf.Operation`.
    """
    _, control_outputs = check_cios(False, control_outputs)
    if not util.is_iterable(seed_ops):
        seed_ops = [seed_ops]
    if not seed_ops:
        return []
    if isinstance(seed_ops[0], tf.Tensor):
        ts = util.make_list_of_t(seed_ops, allow_graph=False)
        seed_ops = util.get_consuming_ops(ts)
    else:
        seed_ops = util.make_list_of_op(seed_ops, allow_graph=False)

    seed_ops = frozenset(seed_ops)
    stop_at_ts = frozenset(util.make_list_of_t(stop_at_ts))
    if within_ops:
        within_ops = util.make_list_of_op(within_ops, allow_graph=False)
        within_ops = frozenset(within_ops)
        seed_ops &= within_ops

    # noinspection PyShadowingNames
    def is_within(op):
        """
        :param tf.Operation op:
        :rtype: bool
        """
        return (within_ops is None or op in within_ops) and (within_ops_fn is None or within_ops_fn(op))

    result = list(seed_ops)
    wave = set(seed_ops)
    while wave:
        new_wave = set()
        for op in wave:
            for new_t in op.outputs:
                if new_t in stop_at_ts:
                    continue
                for new_op in new_t.consumers():
                    if new_op not in result and is_within(new_op):
                        new_wave.add(new_op)
            if control_outputs is not None:
                for new_op in control_outputs.get(op):
                    if new_op not in result and is_within(new_op):
                        new_wave.add(new_op)
        util.concatenate_unique(result, new_wave)
        wave = new_wave
    if not inclusive:
        result = [op for op in result if op not in seed_ops]
    return result


def get_backward_walk_ops(
    seed_ops, inclusive=True, within_ops=None, within_ops_fn=None, stop_at_ts=(), control_inputs=False
):
    """Do a backward graph walk and return all the visited ops.

    Args:
      seed_ops: an iterable of operations from which the backward graph
        walk starts. If a list of tensors is given instead, the seed_ops are set
        to be the generators of those tensors.
      inclusive: if True the given seed_ops are also part of the resulting set.
      within_ops: an iterable of `tf.Operation` within which the search is
        restricted. If `within_ops` is `None`, the search is performed within
        the whole graph.
      within_ops_fn: if provided, a function on ops that should return True iff
        the op is within the graph traversal. This can be used along within_ops,
        in which case an op is within if it is also in within_ops.
      stop_at_ts: an iterable of tensors at which the graph walk stops.
      control_inputs: if True, control inputs will be used while moving backward.
    Returns:
      A Python set of all the `tf.Operation` behind `seed_ops`.
    Raises:
      TypeError: if `seed_ops` or `within_ops` cannot be converted to a list of
        `tf.Operation`.
    """
    try:
        from tensorflow.contrib.graph_editor import select as op_selector
    except ImportError:
        from tensorflow.python.ops import op_selector
    return op_selector.get_backward_walk_ops(
        seed_ops,
        inclusive=inclusive,
        within_ops=within_ops,
        within_ops_fn=within_ops_fn,
        stop_at_ts=stop_at_ts,
        control_inputs=control_inputs,
    )


def get_walks_intersection_ops(
    forward_seed_ops,
    backward_seed_ops,
    forward_inclusive=True,
    backward_inclusive=True,
    within_ops=None,
    within_ops_fn=None,
    control_inputs=False,
    control_outputs=None,
    control_ios=None,
):
    """Return the intersection of a forward and a backward walk.

    Args:
      forward_seed_ops: an iterable of operations from which the forward graph
        walk starts. If a list of tensors is given instead, the seed_ops are set
        to be the consumers of those tensors.
      backward_seed_ops: an iterable of operations from which the backward graph
        walk starts. If a list of tensors is given instead, the seed_ops are set
        to be the generators of those tensors.
      forward_inclusive: if True the given forward_seed_ops are also part of the
        resulting set.
      backward_inclusive: if True the given backward_seed_ops are also part of the
        resulting set.
      within_ops: an iterable of tf.Operation within which the search is
        restricted. If within_ops is None, the search is performed within
        the whole graph.
      within_ops_fn: if provided, a function on ops that should return True iff
        the op is within the graph traversal. This can be used along within_ops,
        in which case an op is within if it is also in within_ops.
      control_inputs: A boolean indicating whether control inputs are enabled.
      control_outputs: An instance of util.ControlOutputs or None. If not None,
        control outputs are enabled.
      control_ios:  An instance of util.ControlOutputs or None. If not None, both
        control inputs and control outputs are enabled. This is equivalent to set
        control_inputs to True and control_outputs to the util.ControlOutputs
        instance.
    Returns:
      A Python set of all the tf.Operation in the intersection of a forward and a
        backward walk.
    Raises:
      TypeError: if `forward_seed_ops` or `backward_seed_ops` or `within_ops`
        cannot be converted to a list of `tf.Operation`.
    """
    control_inputs, control_outputs = check_cios(control_inputs, control_outputs, control_ios)
    forward_ops = get_forward_walk_ops(
        forward_seed_ops,
        inclusive=forward_inclusive,
        within_ops=within_ops,
        within_ops_fn=within_ops_fn,
        control_outputs=control_outputs,
    )
    backward_ops = get_backward_walk_ops(
        backward_seed_ops,
        inclusive=backward_inclusive,
        within_ops=within_ops,
        within_ops_fn=within_ops_fn,
        control_inputs=control_inputs,
    )
    return [op for op in forward_ops if op in backward_ops]


def get_walks_union_ops(
    forward_seed_ops,
    backward_seed_ops,
    forward_inclusive=True,
    backward_inclusive=True,
    within_ops=None,
    within_ops_fn=None,
    control_inputs=False,
    control_outputs=None,
    control_ios=None,
):
    """Return the union of a forward and a backward walk.

    Args:
      forward_seed_ops: an iterable of operations from which the forward graph
        walk starts. If a list of tensors is given instead, the seed_ops are set
        to be the consumers of those tensors.
      backward_seed_ops: an iterable of operations from which the backward graph
        walk starts. If a list of tensors is given instead, the seed_ops are set
        to be the generators of those tensors.
      forward_inclusive: if True the given forward_seed_ops are also part of the
        resulting set.
      backward_inclusive: if True the given backward_seed_ops are also part of the
        resulting set.
      within_ops: restrict the search within those operations. If within_ops is
        None, the search is done within the whole graph.
      within_ops_fn: if provided, a function on ops that should return True iff
        the op is within the graph traversal. This can be used along within_ops,
        in which case an op is within if it is also in within_ops.
      control_inputs: A boolean indicating whether control inputs are enabled.
      control_outputs: An instance of util.ControlOutputs or None. If not None,
        control outputs are enabled.
      control_ios:  An instance of util.ControlOutputs or None. If not None, both
        control inputs and control outputs are enabled. This is equivalent to set
        control_inputs to True and control_outputs to the util.ControlOutputs
        instance.
    Returns:
      A Python set of all the tf.Operation in the union of a forward and a
        backward walk.
    Raises:
      TypeError: if forward_seed_ops or backward_seed_ops or within_ops cannot be
        converted to a list of tf.Operation.
    """
    control_inputs, control_outputs = check_cios(control_inputs, control_outputs, control_ios)
    forward_ops = get_forward_walk_ops(
        forward_seed_ops,
        inclusive=forward_inclusive,
        within_ops=within_ops,
        within_ops_fn=within_ops_fn,
        control_outputs=control_outputs,
    )
    backward_ops = get_backward_walk_ops(
        backward_seed_ops,
        inclusive=backward_inclusive,
        within_ops=within_ops,
        within_ops_fn=within_ops_fn,
        control_inputs=control_inputs,
    )
    return util.concatenate_unique(forward_ops, backward_ops)


def select_ops(*args, **kwargs):
    """Helper to select operations.

    Args:
      *args: list of 1) regular expressions (compiled or not) or 2) (array of)
        `tf.Operation`. `tf.Tensor` instances are silently ignored.
      **kwargs: 'graph': `tf.Graph` in which to perform the regex query.This is
        required when using regex.
        'positive_filter': an elem if selected only if `positive_filter(elem)` is
          `True`. This is optional.
        'restrict_ops_regex': a regular expression is ignored if it doesn't start
          with the substring "(?#ops)".
    Returns:
      A list of `tf.Operation`.
    Raises:
      TypeError: if the optional keyword argument graph is not a `tf.Graph`
        or if an argument in args is not an (array of) `tf.Operation`
        or an (array of) `tf.Tensor` (silently ignored) or a string
        or a regular expression.
      ValueError: if one of the keyword arguments is unexpected or if a regular
        expression is used without passing a graph as a keyword argument.
    """
    # get keywords arguments
    graph = None
    positive_filter = None
    restrict_ops_regex = False
    for k, v in iteritems(kwargs):
        if k == "graph":
            graph = v
            if graph is not None and not isinstance(graph, tf_ops.Graph):
                raise TypeError("Expected a tf.Graph, got: {}".format(type(graph)))
        elif k == "positive_filter":
            positive_filter = v
        elif k == "restrict_ops_regex":
            restrict_ops_regex = v
        elif k == "restrict_ts_regex":
            pass
        else:
            raise ValueError("Wrong keywords argument: {}.".format(k))

    ops = []

    for arg in args:
        if can_be_regex(arg):
            if graph is None:
                raise ValueError("Use the keyword argument 'graph' to use regex.")
            regex = make_regex(arg)
            if regex.pattern.startswith("(?#ts)"):
                continue
            if restrict_ops_regex and not regex.pattern.startswith("(?#ops)"):
                continue
            ops_ = filter_ops_from_regex(graph, regex)
            for op_ in ops_:
                if op_ not in ops:
                    if positive_filter is None or positive_filter(op_):
                        ops.append(op_)
        else:
            ops_aux = util.make_list_of_op(arg, ignore_ts=True)
            if positive_filter is not None:
                ops_aux = [op for op in ops_aux if positive_filter(op)]
            ops_aux = [op for op in ops_aux if op not in ops]
            ops += ops_aux

    return ops


def select_ts(*args, **kwargs):
    """Helper to select tensors.

    Args:
      *args: list of 1) regular expressions (compiled or not) or 2) (array of)
        `tf.Tensor`. `tf.Operation` instances are silently ignored.
      **kwargs: 'graph': `tf.Graph` in which to perform the regex query.This is
        required when using regex.
        'positive_filter': an elem if selected only if `positive_filter(elem)` is
          `True`. This is optional.
        'restrict_ts_regex': a regular expression is ignored if it doesn't start
          with the substring "(?#ts)".
    Returns:
      A list of `tf.Tensor`.
    Raises:
      TypeError: if the optional keyword argument graph is not a `tf.Graph`
        or if an argument in args is not an (array of) `tf.Tensor`
        or an (array of) `tf.Operation` (silently ignored) or a string
        or a regular expression.
      ValueError: if one of the keyword arguments is unexpected or if a regular
        expression is used without passing a graph as a keyword argument.
    """
    # get keywords arguments
    graph = None
    positive_filter = None
    restrict_ts_regex = False
    for k, v in iteritems(kwargs):
        if k == "graph":
            graph = v
            if graph is not None and not isinstance(graph, tf_ops.Graph):
                raise TypeError("Expected a tf.Graph, got {}".format(type(graph)))
        elif k == "positive_filter":
            positive_filter = v
        elif k == "restrict_ts_regex":
            restrict_ts_regex = v
        elif k == "restrict_ops_regex":
            pass
        else:
            raise ValueError("Wrong keywords argument: {}.".format(k))

    ts = []

    for arg in args:
        if can_be_regex(arg):
            if graph is None:
                raise ValueError("Use the keyword argument 'graph' to use regex.")
            regex = make_regex(arg)
            if regex.pattern.startswith("(?#ops)"):
                continue
            if restrict_ts_regex and not regex.pattern.startswith("(?#ts)"):
                continue
            ts_ = filter_ts_from_regex(graph, regex)
            for t_ in ts_:
                if t_ not in ts:
                    if positive_filter is None or positive_filter(t_):
                        ts.append(t_)
        else:
            ts_aux = util.make_list_of_t(arg, ignore_ops=True)
            if positive_filter is not None:
                ts_aux = [t for t in ts_aux if positive_filter(t)]
            ts_aux = [t for t in ts_aux if t not in ts]
            ts += ts_aux

    return ts


def select_ops_and_ts(*args, **kwargs):
    """Helper to select operations and tensors.

    Args:
      *args: list of 1) regular expressions (compiled or not) or 2) (array of)
        `tf.Operation` 3) (array of) tf.Tensor. Regular expressions matching
        tensors must start with the comment `"(?#ts)"`, for instance:
        `"(?#ts)^foo/.*"`.
      **kwargs: 'graph': `tf.Graph` in which to perform the regex query.This is
        required when using regex.
        'positive_filter': an elem if selected only if `positive_filter(elem)` is
          `True`. This is optional.
    Returns:
      A tuple `(ops, ts)` where:
        `ops` is a list of `tf.Operation`, and
        `ts` is a list of `tf.Tensor`
    Raises:
      TypeError: if the optional keyword argument graph is not a `tf.Graph`
        or if an argument in args is not an (array of) `tf.Tensor`
        or an (array of) `tf.Operation` or a string or a regular expression.
      ValueError: if one of the keyword arguments is unexpected or if a regular
        expression is used without passing a graph as a keyword argument.
    """
    ops = select_ops(*args, restrict_ops_regex=False, **kwargs)
    ts = select_ts(*args, restrict_ts_regex=True, **kwargs)
    return ops, ts
