def _project_observable(self, input_key: str,
                            input_observable: Any,
                            get_hist_args: Dict[str, Any] = None,
                            projection_name_args: Dict[str, Any] = None,
                            **kwargs) -> Hist:
        """ Perform a projection for a single observable.

        Note:
            All cuts on the original histograms will be reset when this function is completed.

        Args:
            input_key: Key to describe the input observable.
            input_observable: Observable to project from.
            get_hist_args: Arguments to pass to ``get_hist(...)``. Made available so the args can be cached
                to avoid a ``deepcopy`` when looping. Default: None. In this case, they will be retrieved
                automatically.
            projection_name_args: Arguments to pass to ``projection_name(...)``. Made available so the args
                can be cached to avoid a ``deepcopy`` when looping. Default: None. In this case, they will be
                retrieved automatically.
            kwargs: Additional named args to be passed to projection_name(...) and output_key_name(...).
        Returns:
            The projected histogram.
        """
        # Validation of other optional arguments.
        if get_hist_args is None:
            get_hist_args = copy.deepcopy(kwargs)
        if projection_name_args is None:
            projection_name_args = copy.deepcopy(kwargs)

        # Retrieve histogram
        # We update ``input_observable`` in ``get_hist_args`` every loop, so we don't have to worry
        # about passing the wrong observable.
        get_hist_args.update({"observable": input_observable})
        hist = self.get_hist(**get_hist_args)

        # Define projection name
        projection_name_args.update(self.projection_information)
        # In principle, we could have overwritten one of the kwargs, so we ensure with one of the other
        # updates, so we update it again to be certain.
        projection_name_args.update(kwargs)
        # Put the values included by default last to ensure nothing overwrites these values
        projection_name_args.update({  # type: ignore
            "input_key": input_key,
            "input_observable": input_observable,
            "input_hist": hist
        })
        projection_name = self.projection_name(**projection_name_args)

        # First apply the cuts
        # Restricting the range with SetRange(User) works properly for both THn and TH1.
        logger.debug(f"hist: {hist}")
        for axis in self.additional_axis_cuts:
            logger.debug(f"Apply additional axis hist range: {axis.name}")
            axis.apply_range_set(hist)

        # We need to ensure that it isn't empty so at least one project occurs
        if self.projection_dependent_cut_axes == []:
            self.projection_dependent_cut_axes.append([])

        # Validate the projection dependent cut axes
        # It is invalid to have PDCA on the same axes as the projection axes.
        duplicated_axes = [
            PDCA
            for PA in self.projection_axes
            for PDCA_group in self.projection_dependent_cut_axes
            for PDCA in PDCA_group
            if PDCA.axis_type == PA.axis_type
        ]
        if duplicated_axes:
            raise ValueError(
                f"Axis {duplicated_axes} is in the projection axes and the projection dependent cut axes."
                " This configuration is not allowed, as the range in the PDCA will be overwritten by the projection axes!"
                " Please revise your configuration."
            )

        # Perform the projections
        hists = []
        for i, axes in enumerate(self.projection_dependent_cut_axes):
            # Projection dependent range set
            for axis in axes:
                logger.debug(f"Apply projection dependent hist range: {axis.name}")
                axis.apply_range_set(hist)

            # Do the projection
            projected_hist = self.call_projection_function(hist)
            projected_hist.SetName(f"{projection_name}_{i}")

            hists.append(projected_hist)

            # Cleanup projection dependent cuts (although they should be set again on the next
            # iteration of the loop)
            self.cleanup_cuts(hist, cut_axes = axes)

        # Cleanup the rest of the cuts
        self.cleanup_cuts(hist, cut_axes = self.additional_axis_cuts)

        # Combine all of the projections together
        output_hist = hists[0]
        for temp_hist in hists[1:]:
            output_hist.Add(temp_hist)

        # Final settings
        output_hist.SetName(projection_name)
        # Ensure that the hist doesn't get deleted by ROOT
        # A reference to the histogram within python may not be enough
        output_hist.SetDirectory(0)

        return output_hist, projection_name, projection_name_args
Perform a projection for a single observable.

        Note:
            All cuts on the original histograms will be reset when this function is completed.

        Args:
            input_key: Key to describe the input observable.
            input_observable: Observable to project from.
            get_hist_args: Arguments to pass to ``get_hist(...)``. Made available so the args can be cached
                to avoid a ``deepcopy`` when looping. Default: None. In this case, they will be retrieved
                automatically.
            projection_name_args: Arguments to pass to ``projection_name(...)``. Made available so the args
                can be cached to avoid a ``deepcopy`` when looping. Default: None. In this case, they will be
                retrieved automatically.
            kwargs: Additional named args to be passed to projection_name(...) and output_key_name(...).
        Returns:
            The projected histogram.