Ejemplo n.º 1
0
    def _get_unit_checks(
        self, bound_args: inspect.BoundArguments
    ) -> Dict[str, Dict[str, Any]]:
        """
        Review :attr:`checks` and function bound arguments to build a complete 'checks'
        dictionary.  If a check key is omitted from the argument checks, then a default
        value is assumed (see `check units`_)

        Parameters
        ----------
        bound_args: :class:`inspect.BoundArguments`
            arguments passed into the function being wrapped

            .. code-block:: python

                bound_args = inspect.signature(f).bind(*args, **kwargs)

        Returns
        -------
        Dict[str, Dict[str, Any]]
            A complete 'checks' dictionary for checking function input arguments
            and return.
        """
        # initialize validation dictionary
        out_checks = {}

        # Iterate through function bound arguments + return and build `out_checks`:
        #
        # artificially add "return" to parameters
        things_to_check = bound_args.signature.parameters.copy()
        things_to_check["checks_on_return"] = inspect.Parameter(
            "checks_on_return",
            inspect.Parameter.POSITIONAL_ONLY,
            annotation=bound_args.signature.return_annotation,
        )
        for param in things_to_check.values():
            # variable arguments are NOT checked
            # e.g. in foo(x, y, *args, d=None, **kwargs) variable arguments
            #      *args and **kwargs will NOT be checked
            #
            if param.kind in (
                inspect.Parameter.VAR_KEYWORD,
                inspect.Parameter.VAR_POSITIONAL,
            ):
                continue

            # grab the checks dictionary for the desired parameter
            try:
                param_checks = self.checks[param.name]
            except KeyError:
                param_checks = None

            # -- Determine target units `_units` --
            # target units can be defined in one of three ways (in
            # preferential order):
            #   1. direct keyword pass-through
            #      i.e. CheckUnits(x=u.cm)
            #           CheckUnits(x=[u.cm, u.s])
            #   2. keyword pass-through via dictionary definition
            #      i.e. CheckUnits(x={'units': u.cm})
            #           CheckUnits(x={'units': [u.cm, u.s]})
            #   3. function annotations
            #
            # * if option (3) is used simultaneously with option (1) or (2), then
            #   checks defined by (3) must be consistent with checks from (1) or (2)
            #   to avoid raising an error.
            # * if None is included in the units list, then None values are allowed
            #
            _none_shall_pass = False
            _units = None
            _units_are_from_anno = False
            if param_checks is not None:
                # checks for argument were defined with decorator
                try:
                    _units = param_checks["units"]
                except TypeError:
                    # if checks is NOT None and is NOT a dictionary, then assume
                    # only units were specified
                    #   e.g. CheckUnits(x=u.cm)
                    #
                    _units = param_checks
                except KeyError:
                    # if checks does NOT have 'units' but is still a dictionary,
                    # then other check conditions may have been specified and the
                    # user is relying on function annotations to define desired
                    # units
                    _units = None

            # If no units have been specified by decorator checks, then look for
            # function annotations.
            #
            # Reconcile units specified by decorator checks and function annotations
            _units_anno = None
            if param.annotation is not inspect.Parameter.empty:
                # unit annotations defined
                _units_anno = param.annotation

            if _units is None and _units_anno is None and param_checks is None:
                # no checks specified and no unit annotations defined
                continue
            elif _units is None and _units_anno is None:
                # checks specified, but NO unit checks
                msg = "No astropy.units specified for "
                if param.name == "checks_on_return":
                    msg += "return value "
                else:
                    msg += f"argument {param.name} "
                msg += f"of function {self.f.__name__}()."
                raise ValueError(msg)
            elif _units is None:
                _units = _units_anno
                _units_are_from_anno = True
                _units_anno = None

            # Ensure `_units` is an iterable
            if not isinstance(_units, collections.abc.Iterable):
                _units = [_units]
            if not isinstance(_units_anno, collections.abc.Iterable):
                _units_anno = [_units_anno]

            # Is None allowed?
            if None in _units or param.default is None:
                _none_shall_pass = True

            # Remove Nones
            if None in _units:
                _units = [t for t in _units if t is not None]
            if None in _units_anno:
                _units_anno = [t for t in _units_anno if t is not None]

            # ensure all _units are astropy.units.Unit or physical types &
            # define 'units' for unit checks &
            # define 'none_shall_pass' check
            _units = self._condition_target_units(
                _units, from_annotations=_units_are_from_anno
            )
            _units_anno = self._condition_target_units(
                _units_anno, from_annotations=True
            )
            if not all(_u in _units for _u in _units_anno):
                raise ValueError(
                    f"For argument '{param.name}', "
                    f"annotation units ({_units_anno}) are not included in the units "
                    f"specified by decorator arguments ({_units}).  Use either "
                    f"decorator arguments or function annotations to defined unit "
                    f"types, or make sure annotation specifications match decorator "
                    f"argument specifications."
                )
            if len(_units) == 0 and len(_units_anno) == 0 and param_checks is None:
                # annotations did not specify units
                continue
            elif len(_units) == 0 and len(_units_anno) == 0:
                # checks specified, but NO unit checks
                msg = "No astropy.units specified for "
                if param.name == "checks_on_return":
                    msg += "return value "
                else:
                    msg += f"argument {param.name} "
                msg += f"of function {self.f.__name__}()."
                raise ValueError(msg)

            out_checks[param.name] = {
                "units": _units,
                "none_shall_pass": _none_shall_pass,
            }

            # -- Determine target equivalencies --
            # Unit equivalences can be defined by:
            # 1. keyword pass-through via dictionary definition
            #    e.g. CheckUnits(x={'units': u.C,
            #                       'equivalencies': u.temperature})
            #
            # initialize equivalencies
            try:
                _equivs = param_checks["equivalencies"]
            except (KeyError, TypeError):
                _equivs = self.__check_defaults["equivalencies"]

            # ensure equivalences are properly formatted
            if _equivs is None or _equivs == [None]:
                _equivs = None
            elif isinstance(_equivs, Equivalency):
                pass
            elif isinstance(_equivs, (list, tuple)):

                # flatten list to non-list elements
                if isinstance(_equivs, tuple):
                    _equivs = [_equivs]
                else:
                    _equivs = self._flatten_equivalencies_list(_equivs)

                # ensure passed equivalencies list is structured properly
                #   [(), ...]
                #   or [Equivalency(), ...]
                #
                # * All equivalencies must be a list of 2, 3, or 4 element tuples
                #   structured like...
                #     (from_unit, to_unit, forward_func, backward_func)
                #
                if all(isinstance(el, Equivalency) for el in _equivs):
                    _equivs = reduce(add, _equivs)
                else:
                    _equivs = self._normalize_equivalencies(_equivs)

            out_checks[param.name]["equivalencies"] = _equivs

            # -- Determine if equivalent units pass --
            try:
                peu = param_checks.get(
                    "pass_equivalent_units",
                    self.__check_defaults["pass_equivalent_units"],
                )
            except (AttributeError, TypeError):
                peu = self.__check_defaults["pass_equivalent_units"]

            out_checks[param.name]["pass_equivalent_units"] = peu

        # Does `self.checks` indicate arguments not used by f?
        missing_params = [
            param for param in set(self.checks.keys()) - set(out_checks.keys())
        ]
        if len(missing_params) > 0:
            params_str = ", ".join(missing_params)
            warnings.warn(
                PlasmaPyWarning(
                    f"Expected to unit check parameters {params_str} but they "
                    f"are missing from the call to {self.f.__name__}"
                )
            )

        return out_checks
Ejemplo n.º 2
0
    def _get_value_checks(
        self, bound_args: inspect.BoundArguments
    ) -> Dict[str, Dict[str, bool]]:
        """
        Review :attr:`checks` and function bound arguments to build a complete 'checks'
        dictionary.  If a check key is omitted from the argument checks, then a default
        value is assumed (see `check values`_).

        Parameters
        ----------
        bound_args: :class:`inspect.BoundArguments`
            arguments passed into the function being wrapped

            .. code-block:: python

                bound_args = inspect.signature(f).bind(*args, **kwargs)

        Returns
        -------
        Dict[str, Dict[str, bool]]
            A complete 'checks' dictionary for checking function input arguments
            and return.
        """
        # initialize validation dictionary
        out_checks = {}

        # Iterate through function bound arguments + return and build `out_checks:
        #
        # artificially add "return" to parameters
        things_to_check = bound_args.signature.parameters.copy()
        things_to_check["checks_on_return"] = inspect.Parameter(
            "checks_on_return",
            inspect.Parameter.POSITIONAL_ONLY,
            annotation=bound_args.signature.return_annotation,
        )
        for param in things_to_check.values():
            # variable arguments are NOT checked
            # e.g. in foo(x, y, *args, d=None, **kwargs) variable arguments
            #      *args and **kwargs will NOT be checked
            #
            if param.kind in (
                inspect.Parameter.VAR_KEYWORD,
                inspect.Parameter.VAR_POSITIONAL,
            ):
                continue

            # grab the checks dictionary for the desired parameter
            try:
                param_in_checks = self.checks[param.name]
            except KeyError:
                # checks for parameter not specified
                continue

            # build `out_checks`
            # read checks and/or apply defaults values
            out_checks[param.name] = {}
            for v_name, v_default in self.__check_defaults.items():
                try:
                    out_checks[param.name][v_name] = param_in_checks.get(
                        v_name, v_default
                    )
                except AttributeError:
                    # for the case that checks are defined for an argument,
                    # but is NOT a dictionary
                    # (e.g. CheckValues(x=u.cm) ... this scenario could happen
                    # during subclassing)
                    out_checks[param.name][v_name] = v_default

        # Does `self.checks` indicate arguments not used by f?
        missing_params = [
            param for param in set(self.checks.keys()) - set(out_checks.keys())
        ]
        if len(missing_params) > 0:
            params_str = ", ".join(missing_params)
            warnings.warn(
                PlasmaPyWarning(
                    f"Expected to value check parameters {params_str} but they "
                    f"are missing from the call to {self.f.__name__}"
                )
            )

        return out_checks