Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various updates #409

Merged
merged 5 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
Brewtils Changelog
==================

3.19.0
------
TBD
- Checks connection status when Plugin is initialized
- Added SystemClient(system_namespaces=[]) feature that round robins requests across multiple system_namespaces
- Expanded Auto Generation to support Doc String parameter extraction
- Plugins will break if Type Hinting and Parameter Type assignment do not matches
- Expanded Auto Generated parameter Typing from Type Hinting or Doc String to be
- str -> String
- int -> Integer
- float -> Float
- bool -> Boolean
- object -> Dictionary
- dict -> Dictionary
- DateTime -> DateTime
- bytes -> Bytes

3.18.0
------
10/13/2023
Expand Down
2 changes: 2 additions & 0 deletions brewtils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from brewtils.rest import normalize_url_prefix
from brewtils.rest.easy_client import get_easy_client, EasyClient
from brewtils.rest.system_client import SystemClient
from brewtils.auto_decorator import AutoDecorator

__all__ = [
"__version__",
Expand All @@ -23,6 +24,7 @@
"load_config",
"configure_logging",
"normalize_url_prefix",
"AutoDecorator",
]

# Aliased for compatibility
Expand Down
2 changes: 1 addition & 1 deletion brewtils/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-

__version__ = "3.18.0"
__version__ = "3.19.0"
66 changes: 3 additions & 63 deletions brewtils/auto_decorator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inspect

from brewtils.models import Parameter
from brewtils.models import Parameter, Command


class AutoDecorator:
Expand All @@ -25,69 +25,9 @@ def addFunctions(self, client):
for func in dir(client):
if callable(getattr(client, func)):
if not func.startswith("_"):
# https://docs.python.org/3/library/inspect.html#inspect.signature
_wrapped = getattr(client, func)
signature = inspect.signature(_wrapped)

for func_parameter in signature.parameters:
func_parameter_value = signature.parameters[func_parameter]

key = func_parameter_value.name
if key == "self":
continue

func_parameter_value.default
typeValue = "String"
default = None
optional = False
is_kwarg = False

if str(func_parameter_value.annotation) in [
"<class 'inspect._empty'>",
"<class 'str'>",
]:
pass
elif str(func_parameter_value.annotation) in ["<class 'int'>"]:
typeValue = "Integer"
elif str(func_parameter_value.annotation) in [
"<class 'float'>"
]:
typeValue = "Float"
elif str(func_parameter_value.annotation) in ["<class 'bool'>"]:
typeValue = "Boolean"
elif str(func_parameter_value.annotation) in [
"<class 'object'>",
"<class 'dict'>",
]:
typeValue = "Dictionary"

if (
str(func_parameter_value.default)
!= "<class 'inspect._empty'>"
):
default = func_parameter_value.default

new_parameter = Parameter(
key=key,
type=typeValue,
multi=False,
display_name=key,
optional=optional,
default=default,
description=None,
choices=None,
parameters=None,
nullable=None,
maximum=None,
minimum=None,
regex=None,
form_input_type=None,
type_info=None,
is_kwarg=is_kwarg,
model=None,
)

_wrapped.parameters = getattr(_wrapped, "parameters", [])
_wrapped.parameters.append(new_parameter)
# decorators.py will handle all of the markings
_wrapped._command = Command()

return client
134 changes: 126 additions & 8 deletions brewtils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def command(
description=None, # type: Optional[str]
parameters=None, # type: Optional[List[Parameter]]
command_type="ACTION", # type: str
output_type="STRING", # type: str
output_type=None, # type: str
schema=None, # type: Optional[Union[dict, str]]
form=None, # type: Optional[Union[dict, list, str]]
template=None, # type: Optional[str]
Expand Down Expand Up @@ -120,6 +120,8 @@ def echo_json(self, message):
"""

if _wrapped is None:
if output_type is None:
output_type = "STRING"
if form is not None:
_deprecate(
"Use of form with @command is now deprecated and will eventually be removed"
Expand Down Expand Up @@ -149,6 +151,14 @@ def echo_json(self, message):
metadata=metadata,
)

if output_type is None:
if str(inspect.signature(_wrapped)._return_annotation) in [
"<class 'object'>",
"<class 'dict'>",
]:
output_type = "JSON"
else:
output_type = "STRING"
new_command = Command(
description=description,
parameters=parameters,
Expand Down Expand Up @@ -413,12 +423,20 @@ def _parse_method(method):
# 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", [])
method_command.parameters + getattr(method, "parameters", []),
method=method,
)

# Add and update parameters based on the method signature
_signature_parameters(method_command, method)

# Checks Docstring and Type Hints for undefined properties
for cmd_parameter in method_command.parameters:
if cmd_parameter.description is None:
cmd_parameter.description = _parameter_docstring(
method, cmd_parameter.key
)

# Verify that all parameters conform to the method signature
_signature_validate(method_command, method)

Expand Down Expand Up @@ -513,6 +531,84 @@ def _method_docstring(method):
return docstring.split("\n")[0] if docstring else None


def _parameter_docstring(method, parameter):
# type: (...) -> str
"""Parse out the description associated with parameter of the docstring from a method


Args:
method: Method to inspect
parameter: Parameter to extract

Returns:
Description of parameter, if found

"""
if hasattr(method, "func_doc"):
docstring = method.func_doc
else:
docstring = method.__doc__

if docstring:
delimiters = [":", "--"]
for line in docstring.expandtabs().split("\n"):
line = line.strip()
for delimiter in delimiters:
if delimiter in line:
if line.startswith(parameter + " ") or line.startswith(
parameter + delimiter
):
return line.split(delimiter)[1].strip()

return None


def _parameter_type_hint(method, cmd_parameter):
for _, arg in enumerate(signature(method).parameters.values()):
if arg.name == cmd_parameter:
if str(arg.annotation) in ["<class 'str'>"]:
return "String"
if str(arg.annotation) in ["<class 'int'>"]:
return "Integer"
if str(arg.annotation) in ["<class 'float'>"]:
return "Float"
if str(arg.annotation) in ["<class 'bool'>"]:
return "Boolean"
if str(arg.annotation) in ["<class 'object'>", "<class 'dict'>"]:
return "Dictionary"
if str(arg.annotation).lower() in ["<class 'datetime'>"]:
return "DateTime"
if str(arg.annotation) in ["<class 'bytes'>"]:
return "Bytes"

if hasattr(method, "func_doc"):
docstring = method.func_doc
else:
docstring = method.__doc__
if docstring:
for line in docstring.expandtabs().split("\n"):
line = line.strip()

if line.startswith(cmd_parameter + " ") and line.find(")") > line.find("("):
docType = line.split("(")[1].split(")")[0]

if docType in ["str"]:
return "String"
if docType in ["int"]:
return "Integer"
if docType in ["float"]:
return "Float"
if docType in ["bool"]:
return "Boolean"
if docType in ["obj", "object", "dict"]:
return "Dictionary"
if docType.lower() in ["datetime"]:
return "DateTime"
if docType in ["bytes"]:
return "Bytes"
return None


def _sig_info(arg):
# type: (InspectParameter) -> Tuple[Any, bool]
"""Get the default and optionality of a method argument
Expand Down Expand Up @@ -573,6 +669,7 @@ def _initialize_parameter(
type_info=None,
is_kwarg=None,
model=None,
method=None,
):
# type: (...) -> Parameter
"""Initialize a Parameter
Expand Down Expand Up @@ -619,6 +716,10 @@ def _initialize_parameter(
if param.key is None:
raise PluginParamError("Attempted to create a parameter without a key")

# Extract Parameter Type if not present
if param.type is None and method is not None:
param.type = _parameter_type_hint(method, param.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.
Expand Down Expand Up @@ -680,8 +781,8 @@ def _format_type(param_type):
return str(param_type).title()


def _initialize_parameters(parameter_list):
# type: (Iterable[Parameter, object, dict]) -> List[Parameter]
def _initialize_parameters(parameter_list, method=None):
# type: (Iterable[Parameter, object, dict], obj) -> List[Parameter]
"""Initialize Parameters from a list of parameter definitions

This exists for backwards compatibility with the old way of specifying Models.
Expand Down Expand Up @@ -709,7 +810,7 @@ def _initialize_parameters(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))
initialized_params.append(_initialize_parameter(param=param, method=method))

# This is a model class object. Needed for backwards compatibility
# See https://github.com/beer-garden/beer-garden/issues/354
Expand All @@ -718,11 +819,13 @@ def _initialize_parameters(parameter_list):
"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)
initialized_params += _initialize_parameters(
param.parameters, method=method
)

# This is a dict of Parameter kwargs
elif isinstance(param, dict):
initialized_params.append(_initialize_parameter(**param))
initialized_params.append(_initialize_parameter(method=method, **param))

# No clue!
else:
Expand Down Expand Up @@ -786,7 +889,10 @@ def _signature_parameters(cmd, method):
if arg.name not in cmd.parameter_keys():
cmd.parameters.append(
_initialize_parameter(
key=arg.name, default=sig_default, optional=sig_optional
key=arg.name,
default=sig_default,
optional=sig_optional,
method=method,
)
)

Expand All @@ -801,6 +907,9 @@ def _signature_parameters(cmd, method):
if param.optional is None:
param.optional = sig_optional

if param.description is None:
param.description = _parameter_docstring(method, param.key)

return cmd


Expand Down Expand Up @@ -835,6 +944,15 @@ def _signature_validate(cmd, method):
if p.kind == InspectParameter.VAR_KEYWORD:
has_kwargs = True

if _parameter_type_hint(
method, param.key
) and param.type != _parameter_type_hint(method, param.key):
raise PluginParamError(
"Parameter Type assigned in the @parameter(type=?) does not match "
"either the function Type Hint or the Doc String definition. "
"Please evaluate your type matching."
)

# 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:
Expand Down
3 changes: 3 additions & 0 deletions brewtils/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ def __init__(self, client=None, system=None, logger=None, **kwargs):
# Now that the config is loaded we can create the EasyClient
self._ez_client = EasyClient(logger=self._logger, **self._config)

if not self._ez_client.can_connect():
raise RestConnectionError("Cannot connect to the Beer-garden server")

# With the EasyClient we can determine if this is an old garden
self._legacy = self._legacy_garden()

Expand Down
Loading
Loading