docs: def is_numeric(property_name, *, numtype="float", min=None, max=None,
                   present_optional=False, message=None):
      """Returns a Validation that checks a property as a number, with optional range constraints."""
      if numtype == "int":
        cast = util.try_parse_int
      elif numtype == "decimal":
        cast = util.try_parse_decimal
      elif numtype == "float":
        cast = util.try_parse_float
      else:
        raise ValueError("numtype argument must be one of: int, decimal, float")
  
      def check(val):
        """Checks that a value can be parsed as a number."""
        if val is None:
          return present_optional
        else:
          is_num, new_val = cast(val)
          if not is_num:
            return False
          else:
            if min is not None and new_val < min:
              return False
            if max is not None and new_val > max:
              return False
            return True
  
      if not message:
        msg = ["must be a"]
        if numtype == "int":
          msg.append("whole number")
        else:
          msg.append("number")
        if min is not None and max is not None:
          msg.append("between {0} and {1}".format(min, max))
        elif min is not None:
          msg.append("greater than or equal to {0}".format(min))
        elif max is not None:
          msg.append("less than or equal to {0}".format(max))
        message = " ".join(msg)
  
      return Validation(check, property_name, message)