Drop-down widgets for extension functions via UnresolvedSymbol (#7115)

Fixes #6955 by:
- using `visualisationModule` to specify the module where the visualization is to be used
- referring to method in `Meta.get_annotation` with `.method_name` - e.g. unresolved symbol notation
- evaluating arguments to `Meta.get_annotation` in the context of the user module (which can access the extension functions)
This commit is contained in:
Jaroslav Tulach 2023-06-27 17:19:42 +02:00 committed by GitHub
parent 22259e696d
commit 477dd82670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 213 additions and 109 deletions

View File

@ -434,6 +434,7 @@ fn test_execution_context() {
}; };
let positional_arguments_expressions = vec![1, 2, 3].iter().map(|x| x.to_string()).collect(); let positional_arguments_expressions = vec![1, 2, 3].iter().map(|x| x.to_string()).collect();
let visualization_config = VisualizationConfiguration { let visualization_config = VisualizationConfiguration {
visualization_module: "Foo.Bar.Baz".to_string(),
execution_context_id: context_id, execution_context_id: context_id,
expression, expression,
positional_arguments_expressions, positional_arguments_expressions,
@ -453,6 +454,7 @@ fn test_execution_context() {
"definedOnType" : "[Foo.Bar.Baz]", "definedOnType" : "[Foo.Bar.Baz]",
"name" : "foo" "name" : "foo"
}, },
"visualizationModule" : "Foo.Bar.Baz",
"positionalArgumentsExpressions" : ["1", "2", "3"] "positionalArgumentsExpressions" : ["1", "2", "3"]
} }
}), }),
@ -479,6 +481,7 @@ fn test_execution_context() {
}; };
let positional_arguments_expressions = vec!["foo"].iter().map(|x| x.to_string()).collect(); let positional_arguments_expressions = vec!["foo"].iter().map(|x| x.to_string()).collect();
let visualization_config = VisualizationConfiguration { let visualization_config = VisualizationConfiguration {
visualization_module: "Foo.Bar.Baz".to_string(),
execution_context_id: context_id, execution_context_id: context_id,
expression, expression,
positional_arguments_expressions, positional_arguments_expressions,
@ -495,6 +498,7 @@ fn test_execution_context() {
"definedOnType" : "[Foo.Bar.Baz]", "definedOnType" : "[Foo.Bar.Baz]",
"name" : "foo" "name" : "foo"
}, },
"visualizationModule" : "Foo.Bar.Baz",
"positionalArgumentsExpressions" : ["foo"] "positionalArgumentsExpressions" : ["foo"]
} }
}), }),

View File

@ -713,6 +713,8 @@ pub type ExpressionId = Uuid;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct VisualizationConfiguration { pub struct VisualizationConfiguration {
/// Module to evaluate visualization in context of.
pub visualization_module: String,
/// An execution context of the visualization. /// An execution context of the visualization.
pub execution_context_id: ContextId, pub execution_context_id: ContextId,
/// An enso function that will transform the data into expected format. /// An enso function that will transform the data into expected format.

View File

@ -403,10 +403,19 @@ impl Handle {
} }
/// Get a fully qualified name of the module in the [`graph`]. The name is obtained from the
/// module's path and the `project` name.
pub fn module_qualified_name_with_project(
&self,
project: &dyn model::project::API,
) -> QualifiedName {
self.graph().module.path().qualified_module_name(project.qualified_name())
}
/// Get a full qualified name of the module in the [`graph`]. The name is obtained from the /// Get a full qualified name of the module in the [`graph`]. The name is obtained from the
/// module's path and the `project` name. /// module's path and the `project` name.
pub fn module_qualified_name(&self, project: &dyn model::project::API) -> QualifiedName { pub fn module_qualified_name(&self) -> QualifiedName {
self.graph().module.path().qualified_module_name(project.qualified_name()) self.graph().module.path().qualified_module_name(self.project.qualified_name())
} }
/// Returns information about all the connections between graph's nodes. /// Returns information about all the connections between graph's nodes.

View File

@ -392,7 +392,7 @@ impl QueryData {
/// Generate visualization metadata for this query. /// Generate visualization metadata for this query.
fn visualization_metadata(&self) -> Metadata { fn visualization_metadata(&self) -> Metadata {
let arguments: Vec<Code> = vec![ let arguments: Vec<Code> = vec![
Self::escape_visualization_argument(&self.method_name).into(), Self::as_unresolved_symbol(&self.method_name).into(),
Self::arg_sequence(&self.arguments).into(), Self::arg_sequence(&self.arguments).into(),
]; ];
@ -410,6 +410,12 @@ impl QueryData {
Ast::raw_text_literal(arg).repr() Ast::raw_text_literal(arg).repr()
} }
/// Creates unresolved symbol via ".name" syntax. Unresolved symbol contains name and also
/// module scope to resolve it properly.
fn as_unresolved_symbol(arg: &str) -> String {
format!(".{arg}")
}
/// Escape a list of strings to be used as a visualization argument. Transforms the strings into /// Escape a list of strings to be used as a visualization argument. Transforms the strings into
/// an enso expression with a list of string literals. /// an enso expression with a list of string literals.
fn arg_sequence(args: &[ImString]) -> String { fn arg_sequence(args: &[ImString]) -> String {

View File

@ -574,7 +574,7 @@ impl Searcher {
"Standard.Visualization.AI", "Standard.Visualization.AI",
"build_ai_prompt", "build_ai_prompt",
)?; )?;
let vis = Visualization::new(this.id, vis_ptr, vec![]); let vis = Visualization::new(vis_ptr.module.to_owned(), this.id, vis_ptr, vec![]);
let mut result = graph.attach_visualization(vis.clone()).await?; let mut result = graph.attach_visualization(vis.clone()).await?;
let next = result.next().await.ok_or(NoAIVisualizationDataReceived)?; let next = result.next().await.ok_or(NoAIVisualizationDataReceived)?;
let prompt = std::str::from_utf8(&next)?; let prompt = std::str::from_utf8(&next)?;
@ -1091,7 +1091,7 @@ impl Searcher {
} }
fn module_qualified_name(&self) -> QualifiedName { fn module_qualified_name(&self) -> QualifiedName {
self.graph.module_qualified_name(&*self.project) self.graph.module_qualified_name_with_project(&*self.project)
} }
fn filter(&self) -> Filter { fn filter(&self) -> Filter {

View File

@ -10,6 +10,7 @@ use crate::model::execution_context::VisualizationId;
use crate::model::execution_context::VisualizationUpdateData; use crate::model::execution_context::VisualizationUpdateData;
use crate::sync::Synchronized; use crate::sync::Synchronized;
use double_representation::name::QualifiedName;
use futures::channel::mpsc::UnboundedReceiver; use futures::channel::mpsc::UnboundedReceiver;
use futures::future::ready; use futures::future::ready;
use ide_view::graph_editor::component::visualization::Metadata; use ide_view::graph_editor::component::visualization::Metadata;
@ -158,6 +159,7 @@ impl Default for Status {
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Desired { pub struct Desired {
pub module: QualifiedName,
pub visualization_id: VisualizationId, pub visualization_id: VisualizationId,
pub expression_id: ast::Id, pub expression_id: ast::Id,
pub metadata: Metadata, pub metadata: Metadata,
@ -285,8 +287,12 @@ impl Manager {
// Early return: requested to remove visualization that was already removed. // Early return: requested to remove visualization that was already removed.
return; return;
}; };
let prj = self.executed_graph.module_qualified_name();
let graph = self.executed_graph.graph();
let module = prj.new_child(graph.module.name());
let current_id = current.as_ref().and_then(|current| current.latest_id()); let current_id = current.as_ref().and_then(|current| current.latest_id());
let new_desired = new_desired.map(|new_desired| Desired { let new_desired = new_desired.map(|new_desired| Desired {
module,
expression_id: target, expression_id: target,
visualization_id: current_id.unwrap_or_else(VisualizationId::new_v4), visualization_id: current_id.unwrap_or_else(VisualizationId::new_v4),
metadata: new_desired, metadata: new_desired,
@ -332,6 +338,7 @@ impl Manager {
Ok(Visualization { Ok(Visualization {
id: desired.visualization_id, id: desired.visualization_id,
expression_id: desired.expression_id, expression_id: desired.expression_id,
module: desired.module,
method_pointer, method_pointer,
arguments, arguments,
}) })
@ -556,13 +563,14 @@ mod tests {
let qualified_module = inner.project.qualified_module_name(inner.module.path()); let qualified_module = inner.project.qualified_module_name(inner.module.path());
let method_pointer = QualifiedMethodPointer { let method_pointer = QualifiedMethodPointer {
module: qualified_module.clone(), module: qualified_module.clone(),
defined_on_type: qualified_module, defined_on_type: qualified_module.clone(),
name: Identifier::from_text("faux").unwrap(), name: Identifier::from_text("faux").unwrap(),
}; };
let arguments = vec!["foo".to_owned()]; let arguments = vec!["foo".to_owned()];
let faux_vis = Visualization { let faux_vis = Visualization {
id: default(), id: default(),
expression_id: default(), expression_id: default(),
module: qualified_module,
method_pointer, method_pointer,
arguments, arguments,
}; };
@ -666,6 +674,7 @@ mod tests {
// We don't attach it separately, as Manager identifies visualizations by their // We don't attach it separately, as Manager identifies visualizations by their
// expression ID rather than visualization ID. // expression ID rather than visualization ID.
let desired_vis_3 = Desired { let desired_vis_3 = Desired {
module: QualifiedName::from_text("local.Widgets.Main").unwrap(),
visualization_id: VisualizationId::from_u128(900), visualization_id: VisualizationId::from_u128(900),
expression_id: node_id, expression_id: node_id,
metadata: desired_vis_1, metadata: desired_vis_1,

View File

@ -323,6 +323,8 @@ pub struct Visualization {
pub id: VisualizationId, pub id: VisualizationId,
/// Expression that is to be visualized. /// Expression that is to be visualized.
pub expression_id: ExpressionId, pub expression_id: ExpressionId,
/// Module to evaluate visualization in context of.
pub module: QualifiedName,
/// A pointer to the enso method that will transform the data into expected format. /// A pointer to the enso method that will transform the data into expected format.
pub method_pointer: QualifiedMethodPointer, pub method_pointer: QualifiedMethodPointer,
/// Enso expressions for positional arguments /// Enso expressions for positional arguments
@ -333,12 +335,13 @@ impl Visualization {
/// Creates a new visualization description. The visualization will get a randomly assigned /// Creates a new visualization description. The visualization will get a randomly assigned
/// identifier. /// identifier.
pub fn new( pub fn new(
module: QualifiedName,
expression_id: ExpressionId, expression_id: ExpressionId,
method_pointer: QualifiedMethodPointer, method_pointer: QualifiedMethodPointer,
arguments: Vec<String>, arguments: Vec<String>,
) -> Visualization { ) -> Visualization {
let id = VisualizationId::new_v4(); let id = VisualizationId::new_v4();
Visualization { id, expression_id, method_pointer, arguments } Visualization { id, expression_id, module, method_pointer, arguments }
} }
/// Creates a `VisualizationConfiguration` that is used in communication with language server. /// Creates a `VisualizationConfiguration` that is used in communication with language server.
@ -346,6 +349,7 @@ impl Visualization {
let expression = self.method_pointer.clone().into(); let expression = self.method_pointer.clone().into();
let positional_arguments_expressions = self.arguments.clone(); let positional_arguments_expressions = self.arguments.clone();
VisualizationConfiguration { VisualizationConfiguration {
visualization_module: self.module.to_string_with_main_segment(),
execution_context_id, execution_context_id,
expression, expression,
positional_arguments_expressions, positional_arguments_expressions,

View File

@ -532,6 +532,7 @@ pub mod test {
let arguments = vec![]; let arguments = vec![];
let vis = Visualization { let vis = Visualization {
id: model::execution_context::VisualizationId::new_v4(), id: model::execution_context::VisualizationId::new_v4(),
module: method_pointer.module.clone(),
expression_id: model::execution_context::ExpressionId::new_v4(), expression_id: model::execution_context::ExpressionId::new_v4(),
method_pointer, method_pointer,
arguments, arguments,
@ -581,6 +582,7 @@ pub mod test {
let arguments = vec!["foo".to_owned()]; let arguments = vec!["foo".to_owned()];
let vis = Visualization { let vis = Visualization {
id: model::execution_context::VisualizationId::new_v4(), id: model::execution_context::VisualizationId::new_v4(),
module: method_pointer.module.clone(),
expression_id: model::execution_context::ExpressionId::new_v4(), expression_id: model::execution_context::ExpressionId::new_v4(),
method_pointer, method_pointer,
arguments, arguments,
@ -618,6 +620,7 @@ pub mod test {
let arguments = vec!["bar".to_owned()]; let arguments = vec!["bar".to_owned()];
let vis = Visualization { let vis = Visualization {
id: model::execution_context::VisualizationId::new_v4(), id: model::execution_context::VisualizationId::new_v4(),
module: method_pointer.module.clone(),
expression_id: model::execution_context::ExpressionId::new_v4(), expression_id: model::execution_context::ExpressionId::new_v4(),
method_pointer, method_pointer,
arguments: arguments.clone(), arguments: arguments.clone(),
@ -634,6 +637,7 @@ pub mod test {
let expected_config = language_server::types::VisualizationConfiguration { let expected_config = language_server::types::VisualizationConfiguration {
execution_context_id: data.context_id, execution_context_id: data.context_id,
visualization_module: MockData::new().module_qualified_name().to_string(),
expression: new_expression.clone().into(), expression: new_expression.clone().into(),
positional_arguments_expressions: arguments.clone(), positional_arguments_expressions: arguments.clone(),
}; };

View File

@ -140,6 +140,7 @@ async fn ls_text_protocol_test() {
execution_context_id, execution_context_id,
expression, expression,
positional_arguments_expressions, positional_arguments_expressions,
visualization_module: visualization_module.to_string(),
}; };
let response = let response =
client.attach_visualization(&visualization_id, &expression_id, &visualization_config); client.attach_visualization(&visualization_id, &expression_id, &visualization_config);
@ -157,6 +158,7 @@ async fn ls_text_protocol_test() {
execution_context_id, execution_context_id,
expression, expression,
positional_arguments_expressions, positional_arguments_expressions,
visualization_module: visualization_module.to_string(),
}; };
let response = client.modify_visualization(&visualization_id, &visualization_config).await; let response = client.modify_visualization(&visualization_id, &visualization_config).await;
response.expect("Couldn't modify visualization."); response.expect("Couldn't modify visualization.");
@ -375,7 +377,8 @@ async fn binary_visualization_updates_test_hlp() {
module_qualified_name, module_qualified_name,
Identifier::from_text("quux").unwrap(), Identifier::from_text("quux").unwrap(),
); );
let visualization = Visualization::new(the_node.id(), method_pointer, vec![]); let visualization =
Visualization::new(method_pointer.module.clone(), the_node.id(), method_pointer, vec![]);
let stream = graph_executed.attach_visualization(visualization.clone()).await.unwrap(); let stream = graph_executed.attach_visualization(visualization.clone()).await.unwrap();
info!("Attached the visualization {}", visualization.id); info!("Attached the visualization {}", visualization.id);
let mut stream = stream.boxed_local(); let mut stream = stream.boxed_local();

View File

@ -419,10 +419,10 @@ type_of value = @Builtin_Method "Meta.type_of"
Arguments: Arguments:
- target: The value or type to get the attribute from. - target: The value or type to get the attribute from.
- method_name: The name of the method or constructor to get the attribute for. - method: The symbol representing method or constructor to get the attribute for.
- parameter_name: The name of the parameter to get the attribute for. - parameter_name: The name of the parameter to get the attribute for.
get_annotation : Any -> Text -> Text -> Any | Nothing get_annotation : Any -> Any -> Text -> Any | Nothing
get_annotation target method_name parameter_name = @Builtin_Method "Meta.get_annotation" get_annotation target method parameter_name = @Builtin_Method "Meta.get_annotation"
## PRIVATE ## PRIVATE
Represents a polyglot language. Represents a polyglot language.

View File

@ -4,7 +4,7 @@ from Standard.Base import all
Basic preprocessor for widgets metadata visualization. Basic preprocessor for widgets metadata visualization.
Returns full annotation data for all requested arguments. Returns full annotation data for all requested arguments.
get_widget_json : Any -> Text -> Vector Text -> Text get_widget_json : Any -> Any -> Vector Text -> Text
get_widget_json value call_name argument_names = get_widget_json value call_name argument_names =
read_annotation argument = read_annotation argument =
annotation = Warning.clear <| Meta.get_annotation value call_name argument annotation = Warning.clear <| Meta.get_annotation value call_name argument

View File

@ -453,10 +453,9 @@ interface VisualizationConfiguration {
executionContextId: UUID; executionContextId: UUID;
/** /**
* A qualified name of the module containing the expression which creates * A qualified name of the module to be used to evaluate the arguments for the visualization expression.
* visualization.
*/ */
visualizationModule?: String; visualizationModule: String;
/** An expression that creates a visualization. */ /** An expression that creates a visualization. */
expression: String | MethodPointer; expression: String | MethodPointer;

View File

@ -12,16 +12,16 @@ import java.util.UUID
* *
* @param executionContextId an execution context of the visualization * @param executionContextId an execution context of the visualization
* @param expression an expression that creates a visualization * @param expression an expression that creates a visualization
* @param visualizationModule the name of a module to execute expression at
* @param executionContextId an execution context of the visualization
* @param expression an expression that creates a visualization
*/ */
case class VisualizationConfiguration( case class VisualizationConfiguration(
executionContextId: UUID, executionContextId: UUID,
expression: VisualizationExpression expression: VisualizationExpression,
visualizationModule: String
) extends ToLogString { ) extends ToLogString {
/** A qualified module name containing the expression. */
def visualizationModule: String =
expression.module
/** @inheritdoc */ /** @inheritdoc */
override def toLogString(shouldMask: Boolean): String = override def toLogString(shouldMask: Boolean): String =
s"VisualizationConfiguration(" + s"VisualizationConfiguration(" +
@ -32,7 +32,8 @@ case class VisualizationConfiguration(
def toApi: Api.VisualizationConfiguration = def toApi: Api.VisualizationConfiguration =
Api.VisualizationConfiguration( Api.VisualizationConfiguration(
executionContextId = executionContextId, executionContextId = executionContextId,
expression = expression.toApi expression = expression.toApi,
visualizationModule = visualizationModule
) )
} }
@ -52,7 +53,8 @@ object VisualizationConfiguration {
): VisualizationConfiguration = ): VisualizationConfiguration =
new VisualizationConfiguration( new VisualizationConfiguration(
contextId, contextId,
VisualizationExpression.Text(module, expression) VisualizationExpression.Text(module, expression),
module
) )
/** Create a visualization configuration. /** Create a visualization configuration.
@ -65,6 +67,7 @@ object VisualizationConfiguration {
*/ */
def apply( def apply(
contextId: UUID, contextId: UUID,
module: String,
expression: MethodPointer, expression: MethodPointer,
positionalArgumentsExpressions: Vector[String] positionalArgumentsExpressions: Vector[String]
): VisualizationConfiguration = ): VisualizationConfiguration =
@ -73,7 +76,8 @@ object VisualizationConfiguration {
VisualizationExpression.ModuleMethod( VisualizationExpression.ModuleMethod(
expression, expression,
positionalArgumentsExpressions positionalArgumentsExpressions
) ),
module
) )
private object CodecField { private object CodecField {
@ -99,11 +103,15 @@ object VisualizationConfiguration {
expression <- cursor expression <- cursor
.downField(CodecField.Expression) .downField(CodecField.Expression)
.as[MethodPointer] .as[MethodPointer]
visualizationModule <- cursor
.downField(CodecField.VisualizationModule)
.as[String]
arguments <- cursor arguments <- cursor
.downField(CodecField.Arguments) .downField(CodecField.Arguments)
.as[Option[Vector[String]]] .as[Option[Vector[String]]]
} yield VisualizationConfiguration( } yield VisualizationConfiguration(
contextId, contextId,
visualizationModule,
expression, expression,
arguments.getOrElse(Vector()) arguments.getOrElse(Vector())
) )

View File

@ -204,6 +204,7 @@ object ExecutionContextJsonMessages {
"expressionId": $expressionId, "expressionId": $expressionId,
"visualizationConfig": { "visualizationConfig": {
"executionContextId": ${configuration.executionContextId}, "executionContextId": ${configuration.executionContextId},
"visualizationModule": ${configuration.visualizationModule},
"expression": { "expression": {
"module": ${methodPointer.module}, "module": ${methodPointer.module},
"definedOnType": ${methodPointer.definedOnType}, "definedOnType": ${methodPointer.definedOnType},
@ -223,6 +224,7 @@ object ExecutionContextJsonMessages {
"expressionId": $expressionId, "expressionId": $expressionId,
"visualizationConfig": { "visualizationConfig": {
"executionContextId": ${configuration.executionContextId}, "executionContextId": ${configuration.executionContextId},
"visualizationModule": ${methodPointer.module},
"expression": { "expression": {
"module": ${methodPointer.module}, "module": ${methodPointer.module},
"definedOnType": ${methodPointer.definedOnType}, "definedOnType": ${methodPointer.definedOnType},

View File

@ -66,6 +66,7 @@ class VisualizationOperationsTest extends BaseServerTest {
val visualizationConfig = val visualizationConfig =
VisualizationConfiguration( VisualizationConfiguration(
contextId, contextId,
visualizationModule,
MethodPointer( MethodPointer(
visualizationModule, visualizationModule,
visualizationModule, visualizationModule,
@ -120,6 +121,7 @@ class VisualizationOperationsTest extends BaseServerTest {
val visualizationConfig = val visualizationConfig =
VisualizationConfiguration( VisualizationConfiguration(
contextId, contextId,
visualizationModule,
MethodPointer( MethodPointer(
visualizationModule, visualizationModule,
visualizationModule, visualizationModule,

View File

@ -615,21 +615,20 @@ object Runtime {
* *
* @param executionContextId an execution context of the visualization * @param executionContextId an execution context of the visualization
* @param expression the expression that creates a visualization * @param expression the expression that creates a visualization
* @param visualizationModule module to evaluate arguments for visualization at
*/ */
case class VisualizationConfiguration( case class VisualizationConfiguration(
executionContextId: ContextId, executionContextId: ContextId,
expression: VisualizationExpression expression: VisualizationExpression,
visualizationModule: String
) extends ToLogString { ) extends ToLogString {
/** A qualified module name containing the expression. */
def visualizationModule: String =
expression.module
/** @inheritdoc */ /** @inheritdoc */
override def toLogString(shouldMask: Boolean): String = override def toLogString(shouldMask: Boolean): String =
s"VisualizationConfiguration(" + s"VisualizationConfiguration(" +
s"executionContextId=$executionContextId," + s"executionContextId=$executionContextId," +
s"expression=${expression.toLogString(shouldMask)})" s"expression=${expression.toLogString(shouldMask)})" +
s"visualizationModule=${visualizationModule})"
} }
/** An operation applied to the suggestion argument. */ /** An operation applied to the suggestion argument. */

View File

@ -59,6 +59,7 @@ class UpsertVisualizationJob(
try { try {
val maybeCallable = val maybeCallable =
UpsertVisualizationJob.evaluateVisualizationExpression( UpsertVisualizationJob.evaluateVisualizationExpression(
config.visualizationModule,
config.expression config.expression
) )
@ -180,7 +181,10 @@ object UpsertVisualizationJob {
val expressionId = visualization.expressionId val expressionId = visualization.expressionId
val visualizationId = visualization.id val visualizationId = visualization.id
val maybeCallable = val maybeCallable =
evaluateVisualizationExpression(visualization.config.expression) evaluateVisualizationExpression(
visualizationConfig.visualizationModule,
visualizationConfig.expression
)
maybeCallable.foreach { result => maybeCallable.foreach { result =>
updateVisualization( updateVisualization(
@ -215,8 +219,9 @@ object UpsertVisualizationJob {
/** Evaluate the visualization expression in a given module. /** Evaluate the visualization expression in a given module.
* *
* @param module the module where to evaluate the expression * @param module the module where to evaluate arguments for the expression
* @param expression the visualization expression * @param expression the visualization expression
* @param expressionModule the module where to evaluate the expression
* @param retryCount the number of attempted retries * @param retryCount the number of attempted retries
* @param ctx the runtime context * @param ctx the runtime context
* @return either the evaluation result or an evaluation failure * @return either the evaluation result or an evaluation failure
@ -224,6 +229,7 @@ object UpsertVisualizationJob {
private def evaluateModuleExpression( private def evaluateModuleExpression(
module: Module, module: Module,
expression: Api.VisualizationExpression, expression: Api.VisualizationExpression,
expressionModule: Module,
retryCount: Int = 0 retryCount: Int = 0
)(implicit )(implicit
ctx: RuntimeContext ctx: RuntimeContext
@ -233,7 +239,7 @@ object UpsertVisualizationJob {
val (callback, arguments) = expression match { val (callback, arguments) = expression match {
case Api.VisualizationExpression.Text(_, expression) => case Api.VisualizationExpression.Text(_, expression) =>
val callback = ctx.executionService.evaluateExpression( val callback = ctx.executionService.evaluateExpression(
module, expressionModule,
expression expression
) )
val arguments = Vector() val arguments = Vector()
@ -243,7 +249,7 @@ object UpsertVisualizationJob {
argumentExpressions argumentExpressions
) => ) =>
val callback = ctx.executionService.prepareFunctionCall( val callback = ctx.executionService.prepareFunctionCall(
module, expressionModule,
QualifiedName.fromString(definedOnType).item, QualifiedName.fromString(definedOnType).item,
name name
) )
@ -261,7 +267,12 @@ object UpsertVisualizationJob {
Level.FINE, Level.FINE,
s"Evaluation of visualization was interrupted. Retrying [${retryCount + 1}]." s"Evaluation of visualization was interrupted. Retrying [${retryCount + 1}]."
) )
evaluateModuleExpression(module, expression, retryCount + 1) evaluateModuleExpression(
module,
expression,
expressionModule,
retryCount + 1
)
case error: ThreadInterruptedException => case error: ThreadInterruptedException =>
val message = val message =
@ -297,18 +308,25 @@ object UpsertVisualizationJob {
/** Evaluate the visualization expression. /** Evaluate the visualization expression.
* *
* @param module module to evaluate the expression arguments at
* @param expression the visualization expression to evaluate * @param expression the visualization expression to evaluate
* @param ctx the runtime context * @param ctx the runtime context
* @return either the evaluation result or an evaluation error * @return either the evaluation result or an evaluation error
*/ */
private def evaluateVisualizationExpression( private def evaluateVisualizationExpression(
module: String,
expression: Api.VisualizationExpression expression: Api.VisualizationExpression
)(implicit )(implicit
ctx: RuntimeContext ctx: RuntimeContext
): Either[EvaluationFailure, EvaluationResult] = { ): Either[EvaluationFailure, EvaluationResult] = {
for { for {
module <- findModule(expression.module) module <- findModule(module)
expression <- evaluateModuleExpression(module, expression) expressionModule <- findModule(expression.module)
expression <- evaluateModuleExpression(
module,
expression,
expressionModule
)
} yield expression } yield expression
} }

View File

@ -358,7 +358,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> encode x" "x -> encode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -476,7 +477,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> encode x" "x -> encode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -611,7 +613,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> encode x" "x -> encode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -739,7 +742,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"encode" "encode"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -867,7 +871,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"encode" "encode"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -919,7 +924,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"encode" "encode"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1085,7 +1091,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> encode x" "x -> encode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1123,7 +1130,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> incAndEncode x" "x -> incAndEncode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1192,7 +1200,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> encode x" "x -> encode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1356,7 +1365,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"encode" "encode"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1468,7 +1478,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> encode x" "x -> encode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1506,7 +1517,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"x -> incAndEncode x" "x -> incAndEncode x"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -1600,7 +1612,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Test.Undefined", "Test.Undefined",
"x -> x" "x -> x"
) ),
"Test.Undefined"
) )
) )
) )
@ -1666,7 +1679,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Standard.Visualization.Main", "Standard.Visualization.Main",
"x -> x.default_visualization.to_text" "x -> x.default_visualization.to_text"
) ),
"Standard.Visualization.Main"
) )
) )
) )
@ -1762,7 +1776,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Main", "Enso_Test.Test.Main",
"Main.does_not_exist" "Main.does_not_exist"
) ),
"Enso_Test.Test.Main"
) )
) )
) )
@ -1843,7 +1858,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
moduleName, moduleName,
"x -> x.visualise_me" "x -> x.visualise_me"
) ),
moduleName
) )
) )
) )
@ -1955,7 +1971,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Visualization", "Enso_Test.Test.Visualization",
"inc_and_encode" "inc_and_encode"
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -2071,7 +2088,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
moduleName, moduleName,
"x -> x.catch_primitive _.to_text" "x -> x.catch_primitive _.to_text"
) ),
moduleName
) )
) )
) )
@ -2170,7 +2188,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
moduleName, moduleName,
"x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text" "x -> Panic.catch_primitive x caught_panic-> caught_panic.payload.to_text"
) ),
moduleName
) )
) )
) )
@ -2302,7 +2321,8 @@ class RuntimeVisualizationsTest
visualizationFunction visualizationFunction
), ),
Vector() Vector()
) ),
visualizationModule
) )
) )
) )
@ -2403,7 +2423,8 @@ class RuntimeVisualizationsTest
visualizationFunction visualizationFunction
), ),
Vector() Vector()
) ),
visualizationModule
) )
) )
) )
@ -2500,7 +2521,8 @@ class RuntimeVisualizationsTest
"incAndEncode" "incAndEncode"
), ),
Vector() Vector()
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -2628,7 +2650,8 @@ class RuntimeVisualizationsTest
"incAndEncode" "incAndEncode"
), ),
Vector("2", "3") Vector("2", "3")
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -2698,7 +2721,8 @@ class RuntimeVisualizationsTest
"incAndEncode" "incAndEncode"
), ),
Vector("2", "4") Vector("2", "4")
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -2801,7 +2825,8 @@ class RuntimeVisualizationsTest
"incAndEncode" "incAndEncode"
), ),
Vector() Vector()
) ),
"Enso_Test.Test.Visualization"
) )
) )
) )
@ -2963,7 +2988,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Main", "Enso_Test.Test.Main",
"x -> x.to_text" "x -> x.to_text"
) ),
"Enso_Test.Test.Main"
) )
) )
) )
@ -3063,7 +3089,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Main", "Enso_Test.Test.Main",
"x -> x.to_text" "x -> x.to_text"
) ),
"Enso_Test.Test.Main"
) )
) )
) )
@ -3191,7 +3218,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
"Enso_Test.Test.Main", "Enso_Test.Test.Main",
"x -> x.to_text" "x -> x.to_text"
) ),
"Enso_Test.Test.Main"
) )
) )
) )
@ -3308,7 +3336,8 @@ class RuntimeVisualizationsTest
Api.VisualizationExpression.Text( Api.VisualizationExpression.Text(
moduleName, moduleName,
"x -> x.to_text" "x -> x.to_text"
) ),
moduleName
) )
) )
) )

View File

@ -1,24 +1,23 @@
package org.enso.interpreter.node.expression.builtin.meta; package org.enso.interpreter.node.expression.builtin.meta;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import org.enso.interpreter.dsl.BuiltinMethod; import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.BaseNode; import org.enso.interpreter.node.BaseNode;
import org.enso.interpreter.node.callable.thunk.ThunkExecutorNode; import org.enso.interpreter.node.callable.thunk.ThunkExecutorNode;
import org.enso.interpreter.node.expression.builtin.text.util.ExpectStringNode; import org.enso.interpreter.node.expression.builtin.text.util.ExpectStringNode;
import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.Annotation; import org.enso.interpreter.runtime.callable.Annotation;
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor; import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.Type; import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.DataflowError; import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.scope.ModuleScope; import org.enso.interpreter.runtime.error.PanicException;
import org.enso.interpreter.runtime.state.State; import org.enso.interpreter.runtime.state.State;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.library.CachedLibrary;
@BuiltinMethod( @BuiltinMethod(
type = "Meta", type = "Meta",
name = "get_annotation", name = "get_annotation",
@ -39,16 +38,22 @@ public abstract class GetAnnotationNode extends BaseNode {
@Cached ThunkExecutorNode thunkExecutorNode, @Cached ThunkExecutorNode thunkExecutorNode,
@Cached ExpectStringNode expectStringNode, @Cached ExpectStringNode expectStringNode,
@Cached TypeOfNode typeOfNode) { @Cached TypeOfNode typeOfNode) {
String methodName = expectStringNode.execute(method);
Object targetTypeResult = typeOfNode.execute(target); Object targetTypeResult = typeOfNode.execute(target);
if (targetTypeResult instanceof DataflowError error) { if (targetTypeResult instanceof DataflowError error) {
return error; return error;
} }
if (targetTypeResult instanceof Type targetType) { if (targetTypeResult instanceof Type targetType) {
ModuleScope scope = targetType.getDefinitionScope(); Function methodFunction;
Function methodFunction = scope.lookupMethodDefinition(targetType, methodName); if (method instanceof UnresolvedSymbol symbol) {
methodFunction = symbol.resolveFor(targetType);
} else {
CompilerDirectives.transferToInterpreter();
var ctx = EnsoContext.get(this);
var err = ctx.getBuiltins().error();
var payload = err.makeUnsupportedArgumentsError(new Object[] { method }, "Use .name to specify name of function");
throw new PanicException(payload, this);
}
if (methodFunction != null) { if (methodFunction != null) {
String parameterName = expectStringNode.execute(parameter); String parameterName = expectStringNode.execute(parameter);
Annotation annotation = methodFunction.getSchema().getAnnotation(parameterName); Annotation annotation = methodFunction.getSchema().getAnnotation(parameterName);
@ -59,6 +64,7 @@ public abstract class GetAnnotationNode extends BaseNode {
} }
} }
if (target instanceof Type type) { if (target instanceof Type type) {
String methodName = ((UnresolvedSymbol) symbol).getName();
AtomConstructor constructor = getAtomConstructor(type, methodName); AtomConstructor constructor = getAtomConstructor(type, methodName);
if (constructor != null) { if (constructor != null) {
Function constructorFunction = constructor.getConstructorFunction(); Function constructorFunction = constructor.getConstructorFunction();

View File

@ -282,32 +282,32 @@ spec =
(Test_Type.Value a)==(Test_Type.Value c) . should_be_false (Test_Type.Value a)==(Test_Type.Value c) . should_be_false
Test.specify "get annotations" <| Test.specify "get annotations" <|
Meta.get_annotation Meta_Spec "test_method" "a" . should_equal 7 Meta.get_annotation Meta_Spec .test_method "a" . should_equal 7
Meta.get_annotation Meta_Spec "test_method" "b" . should_equal (Test_Type.Value 49) Meta.get_annotation Meta_Spec .test_method "b" . should_equal (Test_Type.Value 49)
Meta.get_annotation Meta_Spec "test_method" "c" . should_fail_with Text Meta.get_annotation Meta_Spec .test_method "c" . should_fail_with Text
Meta.get_annotation Meta_Spec "test_method" "c" . catch . should_equal "Error Value" Meta.get_annotation Meta_Spec .test_method "c" . catch . should_equal "Error Value"
Meta.get_annotation Meta_Spec "test_method" "x" . should_equal Nothing Meta.get_annotation Meta_Spec .test_method "x" . should_equal Nothing
value = My_Type.Value 99 "bar" True value = My_Type.Value 99 "bar" True
Meta.get_annotation value "first_method" "param" . should_equal 11 Meta.get_annotation value .first_method "param" . should_equal 11
Meta.get_annotation value "second_method" "param" . should_equal Nothing Meta.get_annotation value .second_method "param" . should_equal Nothing
Meta.get_annotation value "third_method" "param" . should_equal Nothing Meta.get_annotation value .third_method "param" . should_equal Nothing
Meta.get_annotation value "other_method" "a" 7 . should_equal 12 Meta.get_annotation value .other_method "a" 7 . should_equal 12
Meta.get_annotation value "other_method" "b" value . should_equal 99 Meta.get_annotation value .other_method "b" value . should_equal 99
Meta.get_annotation value "other_method" "c" . should_equal Nothing Meta.get_annotation value .other_method "c" . should_equal Nothing
Meta.get_annotation value "my_method" "self" . should_equal "self" Meta.get_annotation value .my_method "self" . should_equal "self"
Test.specify "no constructor annotations on value" <| Test.specify "no constructor annotations on value" <|
value = My_Type.Value 99 "bar" True value = My_Type.Value 99 "bar" True
Meta.get_annotation value "Value" "foo" . should_equal Nothing Meta.get_annotation value .Value "foo" . should_equal Nothing
Meta.get_annotation value "Value" "bar" . should_equal Nothing Meta.get_annotation value .Value "bar" . should_equal Nothing
Meta.get_annotation value "Value" "baz" . should_equal Nothing Meta.get_annotation value .Value "baz" . should_equal Nothing
Test.specify "get annotations on constructor" <| Test.specify "get annotations on constructor" <|
Meta.get_annotation My_Type "Value" "foo" 7 8 . should_equal 15 Meta.get_annotation My_Type .Value "foo" 7 8 . should_equal 15
Meta.get_annotation My_Type "Value" "bar" . should_equal Nothing Meta.get_annotation My_Type .Value "bar" . should_equal Nothing
Meta.get_annotation My_Type "Value" "baz" . should_equal (My_Type.Value 1 2 3) Meta.get_annotation My_Type .Value "baz" . should_equal (My_Type.Value 1 2 3)
Test.group "Check Nothing and NaN" <| Test.group "Check Nothing and NaN" <|
Test.specify "Nothing.is_a Nothing" <| Test.specify "Nothing.is_a Nothing" <|

View File

@ -21,7 +21,7 @@ spec =
Test.group "Widgets for In-Database Connection with table types" <| Test.group "Widgets for In-Database Connection with table types" <|
Test.specify "works for `tables`" <| Test.specify "works for `tables`" <|
result = Widgets.get_widget_json connection "tables" ["types"] result = Widgets.get_widget_json connection .tables ["types"]
result.should_contain "'TABLE'" result.should_contain "'TABLE'"
result.should_contain "'VIEW'" result.should_contain "'VIEW'"
@ -29,8 +29,8 @@ spec =
Test.specify "works for `query` and `read`" <| Test.specify "works for `query` and `read`" <|
choices = ['a_table', 'another', 'mock_table'] . map n-> Choice.Option n n.pretty choices = ['a_table', 'another', 'mock_table'] . map n-> Choice.Option n n.pretty
expect = [["query", Widget.Single_Choice choices Nothing Display.Always]] . to_json expect = [["query", Widget.Single_Choice choices Nothing Display.Always]] . to_json
Widgets.get_widget_json connection "query" ["query"] . should_equal expect Widgets.get_widget_json connection .query ["query"] . should_equal expect
Widgets.get_widget_json connection "read" ["query"] . should_equal expect Widgets.get_widget_json connection .read ["query"] . should_equal expect
Test.group "Widgets for In-Database Table with column name sets" <| Test.group "Widgets for In-Database Table with column name sets" <|
mock_table = connection.query "mock_table" mock_table = connection.query "mock_table"
@ -38,17 +38,17 @@ spec =
Test.specify "works for `get` and `at`" <| Test.specify "works for `get` and `at`" <|
choices = mock_table.column_names . map n-> Choice.Option n n.pretty choices = mock_table.column_names . map n-> Choice.Option n n.pretty
expect = [["selector", Widget.Single_Choice choices Nothing Display.Always]] . to_json expect = [["selector", Widget.Single_Choice choices Nothing Display.Always]] . to_json
Widgets.get_widget_json mock_table "get" ["selector"] . should_equal expect Widgets.get_widget_json mock_table .get ["selector"] . should_equal expect
Widgets.get_widget_json mock_table "at" ["selector"] . should_equal expect Widgets.get_widget_json mock_table .at ["selector"] . should_equal expect
Test.specify "works for `filter`" <| Test.specify "works for `filter`" <|
choices = mock_table.column_names . map n-> Choice.Option n n.pretty choices = mock_table.column_names . map n-> Choice.Option n n.pretty
expect = [["column", Widget.Single_Choice choices Nothing Display.Always]] . to_json expect = [["column", Widget.Single_Choice choices Nothing Display.Always]] . to_json
Widgets.get_widget_json mock_table "filter" ["column"] . should_equal expect Widgets.get_widget_json mock_table .filter ["column"] . should_equal expect
Test.group "Widgets for Database" <| Test.group "Widgets for Database" <|
Test.specify "works for `connect`" <| Test.specify "works for `connect`" <|
result = Widgets.get_widget_json Database "connect" ["details"] result = Widgets.get_widget_json Database .connect ["details"]
result.should_contain "SQLite" result.should_contain "SQLite"
result.should_contain "Postgres" result.should_contain "Postgres"
result.should_contain "Redshift" result.should_contain "Redshift"

View File

@ -18,12 +18,12 @@ spec =
Test.specify "works for `get` and `at`" <| Test.specify "works for `get` and `at`" <|
choices = mock_table.column_names . map n-> Choice.Option n n.pretty choices = mock_table.column_names . map n-> Choice.Option n n.pretty
expect = [["selector", Widget.Single_Choice choices Nothing Display.Always]] . to_json expect = [["selector", Widget.Single_Choice choices Nothing Display.Always]] . to_json
Widgets.get_widget_json mock_table "get" ["selector"] . should_equal expect Widgets.get_widget_json mock_table .get ["selector"] . should_equal expect
Widgets.get_widget_json mock_table "at" ["selector"] . should_equal expect Widgets.get_widget_json mock_table .at ["selector"] . should_equal expect
Test.specify "works for `filter`" <| Test.specify "works for `filter`" <|
choices = mock_table.column_names . map n-> Choice.Option n n.pretty choices = mock_table.column_names . map n-> Choice.Option n n.pretty
expect = [["column", Widget.Single_Choice choices Nothing Display.Always]] . to_json expect = [["column", Widget.Single_Choice choices Nothing Display.Always]] . to_json
Widgets.get_widget_json mock_table "filter" ["column"] . should_equal expect Widgets.get_widget_json mock_table .filter ["column"] . should_equal expect
main = Test_Suite.run_main spec main = Test_Suite.run_main spec

View File

@ -16,9 +16,9 @@ spec =
mock_text = "abc def" mock_text = "abc def"
default_widget = Text_Sub_Range.default_widget default_widget = Text_Sub_Range.default_widget
expect = [["range", default_widget]] . to_json expect = [["range", default_widget]] . to_json
json = Widgets.get_widget_json mock_text "take" ["range"] json = Widgets.get_widget_json mock_text .take ["range"]
json . should_equal expect json . should_equal expect
Widgets.get_widget_json mock_text "drop" ["range"] . should_equal expect Widgets.get_widget_json mock_text .drop ["range"] . should_equal expect
obj = json.parse_json obj = json.parse_json
widget = obj.first.second widget = obj.first.second
options = widget . at "values" options = widget . at "values"