def resolve_form(form=None, base_dir=None): # type: (Union[None, dict, list, str], str) -> Optional[dict] """Resolve a form attribute Returns: Dictionary that fully describes a form """ if form is None or isinstance(form, dict): return form elif isinstance(form, list): return {"type": "fieldset", "items": form} elif isinstance(form, six.string_types): try: if form.startswith("http"): return _load_from_url(form) elif form.startswith("/") or form.startswith("."): return json.loads(_load_from_path(form, base_dir=base_dir)) else: raise PluginParamError( "Form was not a definition, file path, or URL") except Exception as ex: six.raise_from(PluginParamError("Error resolving form: %s" % ex), ex) else: raise PluginParamError( "Schema was not a definition, file path, or URL")
def _resolve_display_modifiers(wrapped, command_name, schema=None, form=None, template=None): def _load_from_url(url): return json.loads(requests.get(url).text) def _load_from_path(path): current_dir = os.path.dirname(inspect.getfile(wrapped)) file_path = os.path.abspath(os.path.join(current_dir, path)) with open(file_path, 'r') as definition_file: return definition_file.read() resolved = {} for key, value in { 'schema': schema, 'form': form, 'template': template }.items(): if isinstance(value, six.string_types): try: if value.startswith('http'): resolved[key] = _load_from_url(value) elif value.startswith('/') or value.startswith('.'): loaded_value = _load_from_path(value) resolved[ key] = loaded_value if key == 'template' else json.loads( loaded_value) elif key == 'template': resolved[key] = value else: raise PluginParamError( "%s specified for command '%s' was not a " "definition, file path, or URL" % (key, command_name)) except Exception as ex: raise PluginParamError( "Error reading %s definition from '%s' for command " "'%s': %s" % (key, value, command_name, ex)) elif value is None or (key in ['schema', 'form'] and isinstance(value, dict)): resolved[key] = value elif key == 'form' and isinstance(value, list): resolved[key] = {'type': 'fieldset', 'items': value} else: raise PluginParamError( "%s specified for command '%s' was not a definition, " "file path, or URL" % (key, command_name)) return resolved
def _signature_validate(cmd, method): # type: (Command, MethodType) -> None """Ensure that a Command conforms to the method signature This will do some validation and will raise a PluginParamError if there are any issues. It's expected that this will only be called for Parameters where this makes sense (aka top-level Parameters). It doesn't make sense to call this for model Parameters, so you shouldn't do that. Args: cmd: Command to validate method: Target method object Returns: None Raises: PluginParamError: There was a validation problem """ for param in cmd.parameters: sig_param = None has_kwargs = False for p in signature(method).parameters.values(): if p.name == param.key: sig_param = p if p.kind == InspectParameter.VAR_KEYWORD: has_kwargs = True # Couldn't find the parameter. That's OK if this parameter is meant to be part # of the **kwargs AND the function has a **kwargs parameter. if sig_param is None: if not param.is_kwarg: raise PluginParamError( "Parameter was not not marked as part of kwargs and wasn't found " "in the method signature (should is_kwarg be True?)") elif not has_kwargs: raise PluginParamError( "Parameter was declared as a kwarg (is_kwarg=True) but the method " "signature does not declare a **kwargs parameter") # Cool, found the parameter. Just verify that it's not pure positional and that # it's not marked as part of kwargs. else: if param.is_kwarg: raise PluginParamError( "Parameter was marked as part of kwargs but was found in the " "method signature (should is_kwarg be False?)") # I don't think this is even possible in Python < 3.8 if sig_param.kind == InspectParameter.POSITIONAL_ONLY: raise PluginParamError( "Sorry, positional-only type parameters are not supported")
def parameters(*args): """Specify multiple Parameter definitions at once This can be useful for commands which have a large number of complicated parameters but aren't good candidates for a Model. .. code-block:: python @parameter(**params[cmd1][param1]) @parameter(**params[cmd1][param2]) @parameter(**params[cmd1][param3]) def cmd1(self, **kwargs): pass Can become: .. code-block:: python @parameters(params[cmd1]) def cmd1(self, **kwargs): pass Args: *args (iterable): Positional arguments The first (and only) positional argument must be a list containing dictionaries that describe parameters. Returns: func: The decorated function """ if len(args) == 1: return functools.partial(parameters, args[0]) elif len(args) != 2: raise PluginParamError("@parameters takes a single argument") if not isinstance(args[1], types.FunctionType): raise PluginParamError("@parameters must be applied to a function") try: for param in args[0]: parameter(args[1], **param) except TypeError: raise PluginParamError( "@parameters arg must be an iterable of dictionaries") @wrapt.decorator(enabled=_wrap_functions) def wrapper(_double_wrapped, _, _args, _kwargs): return _double_wrapped(*_args, **_kwargs) return wrapper(args[1])
def _initialize_parameters(parameter_list): # type: (Iterable[Parameter, object, dict]) -> List[Parameter] """Initialize Parameters from a list of parameter definitions This exists for backwards compatibility with the old way of specifying Models. Previously, models were defined by creating a class with a ``parameters`` class attribute. This required constructing each parameter manually, without using the ``@parameter`` decorator. This function takes a list where members can be any of the following: - A Parameter object - A class object with a ``parameters`` attribute - A dict containing kwargs for constructing a Parameter The Parameters in the returned list will be initialized. See the function ``_initialize_parameter`` for information on what that entails. Args: parameter_list: List of parameter precursors Returns: List of initialized parameters """ initialized_params = [] for param in parameter_list: # This is already a Parameter. Only really need to interpret the choices # definition and recurse down into nested Parameters if isinstance(param, Parameter): initialized_params.append(_initialize_parameter(param=param)) # This is a model class object. Needed for backwards compatibility # See https://github.com/beer-garden/beer-garden/issues/354 elif hasattr(param, "parameters"): _deprecate( "Constructing a nested Parameters list using model class objects " "is deprecated. Please pass the model's parameter list directly." ) initialized_params += _initialize_parameters(param.parameters) # This is a dict of Parameter kwargs elif isinstance(param, dict): initialized_params.append(_initialize_parameter(**param)) # No clue! else: raise PluginParamError("Unable to generate parameter from '%s'" % param) return initialized_params
def _load_from_path(path, base_dir=None): # type: (str, str) -> str """Load a definition from a path This is a little odd because of the differences between command-level resources and system-level ones and the need to be backwards-compatible. The problem is determining the "correct" base to use when a relative path is given. For command resources we're able to use ``inspect`` on the method object to determine the current module file. So we've always just used the directory containing that file as the base for relative paths. However, we don't have that ability when it comes to system resources. For example, supposed the system template is specified in an environment variable: BG_SYSTEM_TEMPLATE=./resources/template.html The only thing that really makes sense in this case is starting in the plugin's current working directory. In hindsight, we probably should have done that for command resources as well. Finally, having ONLY system resources start at cwd and ONLY command resources be dependent on the file in which they're declared would be extremely confusing. So in an attempt to remain compatible this will attempt to use a provided base_dir as the starting point for relative path resolution. If there's no file there then it will re-resolve the path, this time using the cwd as he starting point. If no base_dir is provided then it'll just use the cwd. Going forward this will hopefully allow us to present using the cwd as a best practice for everything without forcing anyone to rewrite their plugins. """ if base_dir and os.path.exists( os.path.abspath(os.path.join(base_dir, path))): file_path = os.path.abspath(os.path.join(base_dir, path)) else: file_path = os.path.abspath(os.path.join(os.getcwd(), path)) try: with open(file_path, "r") as definition_file: return definition_file.read() except IOError as ex: six.raise_from( PluginParamError( "%s. Please remember that relative paths will be resolved starting " "from the plugin's current working directory." % ex), ex, )
def resolve_template(template=None, base_dir=None): # type: (str, str) -> Optional[str] """Resolve a template attribute Returns: Dictionary that fully describes a template """ if template is None: return None elif isinstance(template, six.string_types): try: if template.startswith("http"): return _load_from_url(template) elif template.startswith("/") or template.startswith("."): return _load_from_path(template, base_dir=base_dir) else: return template except Exception as ex: six.raise_from( PluginParamError("Error resolving template: %s" % ex), ex) else: raise PluginParamError( "Template specified was not a definition, file path, or URL")
def resolve_schema(schema=None, base_dir=None): # type: (Union[dict, str], str) -> Optional[dict] """Resolve a schema attribute Returns: Dictionary that fully describes a schema """ if schema is None or isinstance(schema, dict): return schema elif isinstance(schema, six.string_types): try: if schema.startswith("http"): return _load_from_url(schema) elif schema.startswith("/") or schema.startswith("."): return json.loads(_load_from_path(schema, base_dir=base_dir)) else: raise PluginParamError( "Schema was not a definition, file path, or URL") except Exception as ex: six.raise_from(PluginParamError("Error resolving schema: %s" % ex), ex) else: raise PluginParamError( "Schema specified was not a definition, file path, or URL")
def _parse_method(method): # type: (MethodType) -> Optional[Command] """Parse a method object as a Beer-garden command target If the method looks like a valid command target (based on the presence of certain attributes) then this method will initialize things: - The command will be initialized. - Every parameter will be initialized. Initializing a parameter is recursive - each nested parameter will also be initialized. - Top-level parameters are validated to ensure they match the method signature. Args: method: Method to parse Returns: Beergarden Command targeting the given method """ if (inspect.ismethod(method) or inspect.isfunction(method)) and (hasattr( method, "_command") or hasattr(method, "parameters")): # Create a command object if there isn't one already method_command = _initialize_command(method) try: # Need to initialize existing parameters before attempting to add parameters # pulled from the method signature. method_command.parameters = _initialize_parameters( method_command.parameters + getattr(method, "parameters", [])) # Add and update parameters based on the method signature _signature_parameters(method_command, method) # Verify that all parameters conform to the method signature _signature_validate(method_command, method) except PluginParamError as ex: six.raise_from( PluginParamError( "Error initializing parameters for command '%s': %s" % (method_command.name, ex)), ex, ) return method_command
def _initialize_command(method): # type: (MethodType) -> Command """Initialize a Command This takes care of ensuring a Command object is in the correct form. Things like: - Assigning the name from the method name - Pulling the description from the method docstring, if necessary - Resolving display modifiers (schema, form, template) Args: method: The method with the Command to initialize Returns: The initialized Command """ cmd = getattr(method, "_command", Command()) cmd.name = _method_name(method) cmd.description = cmd.description or _method_docstring(method) try: base_dir = os.path.dirname(inspect.getfile(method)) cmd.schema = resolve_schema(cmd.schema, base_dir=base_dir) cmd.form = resolve_form(cmd.form, base_dir=base_dir) cmd.template = resolve_template(cmd.template, base_dir=base_dir) except PluginParamError as ex: six.raise_from( PluginParamError("Error initializing command '%s': %s" % (cmd.name, ex)), ex, ) return cmd
def process_choices(choices): # type: (Union[dict, str, Iterable, Callable]) -> Optional[Choices] """Process a choices definition into a usable Choices object Args: choices: Raw choices definition, usually from a decorator Returns: Choices: Dictionary that fully describes a choices specification """ if choices is None or isinstance(choices, Choices): return choices # If choices is a Callable, call it if callable(choices): choices = choices() if isinstance(choices, dict): if not choices.get("value"): raise PluginParamError( "No 'value' provided for choices. You must at least " "provide valid values.") # Again, if value is a Callable, call it value = choices.get("value") if callable(value): value = value() display = choices.get("display", _determine_display(value)) choice_type = choices.get("type") strict = choices.get("strict", True) if choice_type is None: choice_type = _determine_type(value) elif choice_type not in Choices.TYPES: raise PluginParamError( "Invalid choices type '%s' - Valid type options are %s" % (choice_type, Choices.TYPES)) else: if ((choice_type == "command" and not isinstance(value, (six.string_types, dict))) or (choice_type == "url" and not isinstance(value, six.string_types)) or (choice_type == "static" and not isinstance(value, (list, dict)))): allowed_types = { "command": "('string', 'dictionary')", "url": "('string')", "static": "('list', 'dictionary)", } raise PluginParamError( "Invalid choices value type '%s' - Valid value types for " "choice type '%s' are %s" % (type(value), choice_type, allowed_types[choice_type])) if display not in Choices.DISPLAYS: raise PluginParamError( "Invalid choices display '%s' - Valid display options are %s" % (display, Choices.DISPLAYS)) elif isinstance(choices, str): value = choices display = _determine_display(value) choice_type = _determine_type(value) strict = True else: try: # Assume some sort of iterable value = list(choices) except TypeError: raise PluginParamError( "Invalid 'choices': must be a string, dictionary, or iterable." ) display = _determine_display(value) choice_type = _determine_type(value) strict = True # Now parse out type-specific aspects unparsed_value = "" try: if choice_type == "command": if isinstance(value, six.string_types): unparsed_value = value else: unparsed_value = value["command"] details = parse(unparsed_value, parse_as="func") elif choice_type == "url": unparsed_value = value details = parse(unparsed_value, parse_as="url") else: if isinstance(value, dict): unparsed_value = choices.get("key_reference") if unparsed_value is None: raise PluginParamError( "Specifying a static choices dictionary requires a " '"key_reference" field with a reference to another ' 'parameter ("key_reference": "${param_key}")') details = { "key_reference": parse(unparsed_value, parse_as="reference") } else: details = {} except ParseError: raise PluginParamError( "Invalid choices definition - Unable to parse '%s'" % unparsed_value) return Choices(type=choice_type, display=display, value=value, strict=strict, details=details)
def _format_choices(choices): def determine_display(display_value): if isinstance(display_value, six.string_types): return 'typeahead' return 'select' if len(display_value) <= 50 else 'typeahead' def determine_type(type_value): if isinstance(type_value, (list, dict)): return 'static' elif type_value.startswith('http'): return 'url' else: return 'command' if not choices: return None if not isinstance(choices, (list, six.string_types, dict)): raise PluginParamError( "Invalid 'choices' provided. Must be a list, dictionary or string." ) elif isinstance(choices, dict): if not choices.get('value'): raise PluginParamError( "No 'value' provided for choices. You must at least " "provide valid values.") value = choices.get('value') display = choices.get('display', determine_display(value)) choice_type = choices.get('type') strict = choices.get('strict', True) if choice_type is None: choice_type = determine_type(value) elif choice_type not in Choices.TYPES: raise PluginParamError( "Invalid choices type '%s' - Valid type options are %s" % (choice_type, Choices.TYPES)) else: if (choice_type == 'command' and not isinstance(value, (six.string_types, dict))) \ or (choice_type == 'url' and not isinstance(value, six.string_types)) \ or (choice_type == 'static' and not isinstance(value, (list, dict))): allowed_types = { 'command': "('string', 'dictionary')", 'url': "('string')", 'static': "('list', 'dictionary)" } raise PluginParamError( "Invalid choices value type '%s' - Valid value types for " "choice type '%s' are %s" % (type(value), choice_type, allowed_types[choice_type])) if display not in Choices.DISPLAYS: raise PluginParamError( "Invalid choices display '%s' - Valid display options are %s" % (display, Choices.DISPLAYS)) else: value = choices display = determine_display(value) choice_type = determine_type(value) strict = True # Now parse out type-specific aspects unparsed_value = '' try: if choice_type == 'command': if isinstance(value, six.string_types): unparsed_value = value else: unparsed_value = value['command'] details = parse(unparsed_value, parse_as='func') elif choice_type == 'url': unparsed_value = value details = parse(unparsed_value, parse_as='url') else: if isinstance(value, dict): unparsed_value = choices.get('key_reference') if unparsed_value is None: raise PluginParamError( 'Specifying a static choices dictionary requires a ' '"key_reference" field with a reference to another ' 'parameter ("key_reference": "${param_key}")') details = { 'key_reference': parse(unparsed_value, parse_as='reference') } else: details = {} except ParseError: raise PluginParamError( "Invalid choices definition - Unable to parse '%s'" % unparsed_value) return Choices(type=choice_type, display=display, value=value, strict=strict, details=details)
def parameter(_wrapped=None, key=None, type=None, multi=None, display_name=None, optional=None, default=None, description=None, choices=None, nullable=None, maximum=None, minimum=None, regex=None, is_kwarg=None, model=None, form_input_type=None): """Decorator that enables Parameter specifications for a beer-garden Command This decorator is intended to be used when more specification is desired for a Parameter. For example:: @parameter(key="message", description="Message to echo", optional=True, type="String", default="Hello, World!") def echo(self, message): return message :param _wrapped: The function to decorate. This is handled as a positional argument and shouldn't be explicitly set. :param key: String specifying the parameter identifier. Must match an argument name of the decorated function. :param type: String indicating the type to use for this parameter. :param multi: Boolean indicating if this parameter is a multi. See documentation for discussion of what this means. :param display_name: String that will be displayed as a label in the user interface. :param optional: Boolean indicating if this parameter must be specified. :param default: The value this parameter will be assigned if not overridden when creating a request. :param description: An additional string that will be displayed in the user interface. :param choices: List or dictionary specifying allowed values. See documentation for more information. :param nullable: Boolean indicating if this parameter is allowed to be null. :param maximum: Integer indicating the maximum value of the parameter. :param minimum: Integer indicating the minimum value of the parameter. :param regex: String describing a regular expression constraint on the parameter. :param is_kwarg: Boolean indicating if this parameter is meant to be part of the decorated function's kwargs. :param model: Class to be used as a model for this parameter. Must be a Python type object, not an instance. :param form_input_type: Only used for string fields. Changes the form input field (e.g. textarea) :return: The decorated function. """ if _wrapped is None: return functools.partial( parameter, key=key, type=type, multi=multi, display_name=display_name, optional=optional, default=default, description=description, choices=choices, nullable=nullable, maximum=maximum, minimum=minimum, regex=regex, is_kwarg=is_kwarg, model=model, form_input_type=form_input_type, ) # Create a command object if one isn't already associated cmd = getattr(_wrapped, '_command', None) if not cmd: cmd = _generate_command_from_function(_wrapped) _wrapped._command = cmd # Every parameter needs a key, so stop that right here if key is None: raise PluginParamError( "Found a parameter definition without a key for " "command '%s'" % cmd.name) # If the command doesn't already have a parameter with this key then the # method doesn't have an explicit keyword argument with <key> as the name. # That's only OK if this parameter is meant to be part of the **kwargs. param = cmd.get_parameter_by_key(key) if param is None: if is_kwarg: param = Parameter(key=key, optional=False) cmd.parameters.append(param) else: raise PluginParamError( "Parameter '%s' was not an explicit keyword argument for " "command '%s' and was not marked as part of kwargs (should " "is_kwarg be True?)" % (key, cmd.name)) # Update parameter definition with the plugin_param arguments param.type = _format_type(param.type if type is None else type) param.multi = param.multi if multi is None else multi param.display_name = param.display_name if display_name is None else display_name param.optional = param.optional if optional is None else optional param.default = param.default if default is None else default param.description = param.description if description is None else description param.choices = param.choices if choices is None else choices param.nullable = param.nullable if nullable is None else nullable param.maximum = param.maximum if maximum is None else maximum param.minimum = param.minimum if minimum is None else minimum param.regex = param.regex if regex is None else regex param.form_input_type = param.form_input_type if form_input_type is None else form_input_type param.choices = _format_choices(param.choices) # Model is another special case - it requires its own handling if model is not None: param.type = 'Dictionary' param.parameters = _generate_nested_params(model) # If the model is not nullable and does not have a default we will try # to generate a one using the defaults defined on the model parameters if not param.nullable and not param.default: param.default = {} for nested_param in param.parameters: if nested_param.default: param.default[nested_param.key] = nested_param.default @wrapt.decorator(enabled=_wrap_functions) def wrapper(_double_wrapped, _, _args, _kwargs): return _double_wrapped(*_args, **_kwargs) return wrapper(_wrapped)
def _format_choices(choices): def determine_display(display_value): if isinstance(display_value, six.string_types): return "typeahead" return "select" if len(display_value) <= 50 else "typeahead" def determine_type(type_value): if isinstance(type_value, (list, dict)): return "static" elif type_value.startswith("http"): return "url" else: return "command" if not choices: return None if not isinstance(choices, (list, six.string_types, dict)): raise PluginParamError( "Invalid 'choices' provided. Must be a list, dictionary or string." ) elif isinstance(choices, dict): if not choices.get("value"): raise PluginParamError( "No 'value' provided for choices. You must at least " "provide valid values.") value = choices.get("value") display = choices.get("display", determine_display(value)) choice_type = choices.get("type") strict = choices.get("strict", True) if choice_type is None: choice_type = determine_type(value) elif choice_type not in Choices.TYPES: raise PluginParamError( "Invalid choices type '%s' - Valid type options are %s" % (choice_type, Choices.TYPES)) else: if ((choice_type == "command" and not isinstance(value, (six.string_types, dict))) or (choice_type == "url" and not isinstance(value, six.string_types)) or (choice_type == "static" and not isinstance(value, (list, dict)))): allowed_types = { "command": "('string', 'dictionary')", "url": "('string')", "static": "('list', 'dictionary)", } raise PluginParamError( "Invalid choices value type '%s' - Valid value types for " "choice type '%s' are %s" % (type(value), choice_type, allowed_types[choice_type])) if display not in Choices.DISPLAYS: raise PluginParamError( "Invalid choices display '%s' - Valid display options are %s" % (display, Choices.DISPLAYS)) else: value = choices display = determine_display(value) choice_type = determine_type(value) strict = True # Now parse out type-specific aspects unparsed_value = "" try: if choice_type == "command": if isinstance(value, six.string_types): unparsed_value = value else: unparsed_value = value["command"] details = parse(unparsed_value, parse_as="func") elif choice_type == "url": unparsed_value = value details = parse(unparsed_value, parse_as="url") else: if isinstance(value, dict): unparsed_value = choices.get("key_reference") if unparsed_value is None: raise PluginParamError( "Specifying a static choices dictionary requires a " '"key_reference" field with a reference to another ' 'parameter ("key_reference": "${param_key}")') details = { "key_reference": parse(unparsed_value, parse_as="reference") } else: details = {} except ParseError: raise PluginParamError( "Invalid choices definition - Unable to parse '%s'" % unparsed_value) return Choices(type=choice_type, display=display, value=value, strict=strict, details=details)
def _initialize_parameter( param=None, key=None, type=None, multi=None, display_name=None, optional=None, default=None, description=None, choices=None, parameters=None, nullable=None, maximum=None, minimum=None, regex=None, form_input_type=None, type_info=None, is_kwarg=None, model=None, ): # type: (...) -> Parameter """Initialize a Parameter This exists to move logic out of the @parameter decorator. Previously there was a fair amount of logic in the decorator, which meant that it wasn't feasible to create a Parameter without using it. This made things like nested models difficult to do correctly. There are also some checks and translation that need to happen for every Parameter, most notably the "choices" attribute. This method also ensures that these checks and translations occur for child Parameters. Args: param: An already-created Parameter. If this is given all the other Parameter-creation kwargs will be ignored Keyword Args: Will be used to construct a new Parameter """ param = param or Parameter( key=key, type=type, multi=multi, display_name=display_name, optional=optional, default=default, description=description, choices=choices, parameters=parameters, nullable=nullable, maximum=maximum, minimum=minimum, regex=regex, form_input_type=form_input_type, type_info=type_info, is_kwarg=is_kwarg, model=model, ) # Every parameter needs a key, so stop that right here if param.key is None: raise PluginParamError("Attempted to create a parameter without a key") # Type and type info # Type info is where type specific information goes. For now, this is specific # to file types. See #289 for more details. param.type = _format_type(param.type) param.type_info = param.type_info or {} if param.type in Resolvable.TYPES: param.type_info["storage"] = "gridfs" # Also nullify default parameters for safety param.default = None # Process the raw choices into a Choices object param.choices = process_choices(param.choices) # Now deal with nested parameters if param.parameters or param.model: if param.model: # Can't specify a model and parameters - which should win? if param.parameters: raise PluginParamError( "Error initializing parameter '%s': A parameter with both a model " "and nested parameters is not allowed" % param.key) param.parameters = param.model.parameters param.model = None param.type = "Dictionary" param.parameters = _initialize_parameters(param.parameters) return param
def parameters(*args, **kwargs): """ .. deprecated:: 3.0 Will be removed in version 4.0. Please use ``@command`` instead. Decorator for specifying multiple Parameter definitions at once This can be useful for commands which have a large number of complicated parameters but aren't good candidates for a Model. .. code-block:: python @parameter(**params[cmd1][param1]) @parameter(**params[cmd1][param2]) @parameter(**params[cmd1][param3]) def cmd1(self, **kwargs): pass Can become: .. code-block:: python @parameters(params[cmd1]) def cmd1(self, **kwargs): pass Args: *args (iterable): Positional arguments The first (and only) positional argument must be a list containing dictionaries that describe parameters. **kwargs: Used for bookkeeping. Don't set any of these yourself! Returns: func: The decorated function """ # This is the first invocation if not kwargs.get("_partial"): # Need the callable check to prevent applying the decorator with no parenthesis if len(args) == 1 and not callable(args[0]): return functools.partial(parameters, args[0], _partial=True) raise PluginParamError("@parameters takes a single argument") # This is the second invocation else: if len(args) != 2: raise PluginParamError( "Incorrect number of arguments for parameters partial call. Did you " "set _partial=True? If so, please don't do that. If not, please let " "the Beergarden team know how you got here!") _deprecate( "Looks like you're using the '@parameters' decorator. This is now deprecated - " "for passing bulk parameter definitions it's recommended to use the @command " "decorator parameters kwarg, like this: @command(parameters=[...])") params = args[0] _wrapped = args[1] if not callable(_wrapped): raise PluginParamError("@parameters must be applied to a callable") try: for param in params: parameter(_wrapped, **param) except TypeError: raise PluginParamError( "@parameters arg must be an iterable of dictionaries") return _wrapped