text: from typing import List
  from typing import Union
  
  import syft as sy
  from syft.generic.frameworks.hook import hook_args
  from syft.generic.pointers.object_pointer import ObjectPointer
  from syft.generic.frameworks.types import FrameworkTensor
  from syft.messaging.message import ForceObjectDeleteMessage
  from syft.workers.abstract import AbstractWorker
  
  
  class PointerPlan(ObjectPointer):
      """
      The PointerPlan keeps a reference to a remote Plan.
  
      It allows to:
      - __call__ an evaluation of the remote plan
      - get the remote plan
  
      It's a simplification compared to the current hybrid state of
      Plans which can be seen as pointers, which is ambiguous.
      """
  
      def __init__(
          self,
          location: "AbstractWorker" = None,
          id_at_location: Union[str, int] = None,
          owner: "AbstractWorker" = None,
          garbage_collect_data: bool = True,
          id: Union[str, int] = None,
          tags: List[str] = None,
          description: str = None,
      ):
          if owner is None:
              owner = sy.framework.hook.local_worker
  
          self._locations = []
          self._ids_at_location = []
  
          super().__init__(
              location=location,
              id_at_location=id_at_location,
              owner=owner,
              garbage_collect_data=garbage_collect_data,
              id=id,
              tags=tags,
              description=description,
          )
  
      # Make PointerPlan compatible with multi pointers
      @property
      def location(self):
          n_locations = len(self._locations)
          if n_locations != 1:
              return self._locations
          else:
              return self._locations[0]
  
      @location.setter
      def location(self, new_location: Union[AbstractWorker, List[AbstractWorker]]):
          if isinstance(new_location, (list, tuple)):
              self._locations = new_location
          else:
              self._locations = [new_location]
  
      @property
      def id_at_location(self):
          n_ids = len(self._ids_at_location)
          if n_ids != 1:
              return self._ids_at_location
          else:
              return self._ids_at_location[0]
  
      @id_at_location.setter
      def id_at_location(self, new_id_at_location):
          if isinstance(new_id_at_location, (list, tuple)):
              self._ids_at_location = new_id_at_location
          else:
              self._ids_at_location = [new_id_at_location]
  
      def __call__(self, *args, **kwargs):
          """
          Transform the call on the pointer in a request to evaluate the
          remote plan
          """
          if len(self._locations) == 1:
              location = self.location
          else:
              location = args[0].location
  
          result_ids = [sy.ID_PROVIDER.pop()]
          response = self.request_run_plan(location, result_ids, *args)
  
          return response
  
      def request_run_plan(
          self,
          location: "sy.workers.BaseWorker",
          response_ids: List[Union[str, int]],
          *args,
          **kwargs,
      ) -> object:
          """Requests plan execution.
  
          Send a request to execute the plan on the remote location.
  
          Args:
              location: to which worker the request should be sent
              response_ids: where the result should be stored
              args: arguments used as input data for the plan
              kwargs: named arguments used as input data for the plan
  
          Returns:
              Execution response
          """
          plan_name = f"plan{self.id}"
  
          args = [args, response_ids]
  
          if location not in self._locations:
              raise RuntimeError(
                  f"Requested to run a plan on {location.id} but pointer location(s) is/are",
                  self._locations,
              )
  
          # look for the relevant id in the list of ids
          id_at_location = None
          for loc, id_at_loc in zip(self._locations, self._ids_at_location):
              if loc == location:
                  id_at_location = id_at_loc
                  break
  
          command = ("run", id_at_location, args, kwargs)
  
          response = self.owner.send_command(
              message=command, recipient=location, return_ids=response_ids
          )
          response = hook_args.hook_response(plan_name, response, wrap_type=FrameworkTensor[0])
          response.garbage_collect_data = False
          return response
  
      def get(self, deregister_ptr: bool = True):
          """
          This is an alias to fetch_plan, to behave like a pointer
          """
          copy = not deregister_ptr
          plan = self.owner.fetch_plan(self.id_at_location, self.location, copy=copy)
          return plan
  
      @staticmethod
      def simplify(worker: AbstractWorker, ptr: "PointerPlan") -> tuple:
  
          return (ptr.id, ptr.id_at_location, ptr.location.id, ptr.garbage_collect_data)
  
      @staticmethod
      def detail(worker: AbstractWorker, tensor_tuple: tuple) -> "PointerPlan":
          # TODO: fix comment for this and simplifier
          obj_id, id_at_location, worker_id, garbage_collect_data = tensor_tuple
  
          if isinstance(worker_id, bytes):
              worker_id = worker_id.decode()
  
          # If the pointer received is pointing at the current worker, we load the tensor instead
          if worker_id == worker.id:
              plan = worker.get_obj(id_at_location)
  
              return plan
          # Else we keep the same Pointer
          else:
              location = sy.hook.local_worker.get_worker(worker_id)
  
              ptr = PointerPlan(
                  location=location,
                  id_at_location=id_at_location,
                  owner=worker,
                  garbage_collect_data=garbage_collect_data,
                  id=obj_id,
              )
  
              return ptr
  
      def wrap(self):
          return self
  
      def __str__(self):
          """Returns a string version of this pointer.
  
          Example:
              For single pointers:
              > [PointerPlan | me:33873097403 -> dan:72165846784]
  
              Or for multi pointers:
              > [PointerPlan | me:55894304374
                   -> alice:72165846784
                   -> bob:72165846784
              ]
          """
          type_name = type(self).__name__
          out = f"[" f"{type_name} | " f"{str(self.owner.id)}:{self.id}"
          if len(self._locations) == 1:
              out += f" -> {str(self.location.id)}:{self.id_at_location}"
          else:
              for location, id_at_location in zip(self.location, self.id_at_location):
                  out += f"\n\t -> {str(location.id)}:{id_at_location}"
              out += "\n"
          out += "]"
  
          if self.tags is not None and len(self.tags):
              out += "\n\tTags: "
              for tag in self.tags:
                  out += str(tag) + " "
  
          if self.description is not None:
              out += "\n\tDescription: " + str(self.description).split("\n")[0] + "..."
  
          return out
  
      def __del__(self):
          """This method garbage collects the object this pointer is pointing to.
          By default, PySyft assumes that every object only has one pointer to it.
          Thus, if the pointer gets garbage collected, we want to automatically
          garbage collect the object being pointed to.
          """
          if self.garbage_collect_data:
              for id_at_location, location in zip(self._ids_at_location, self._locations):
                  self.owner.send_msg(ForceObjectDeleteMessage(id_at_location), location)
  