diff --git a/docs/i18n/en.json b/docs/i18n/en.json index e66ebd07b9..ce5ea47fa7 100644 --- a/docs/i18n/en.json +++ b/docs/i18n/en.json @@ -35,11 +35,11 @@ "node_modules/react/README": { "title": "node_modules/react/README" }, - "obj/perspective-python": { - "title": "perspective-python API" - }, - "obj/perspective-viewer": { - "title": "perspective-viewer API" + "obj/perspective-python": { + "title": "perspective-python API" + }, + "obj/perspective-viewer": { + "title": "perspective-viewer API" }, "obj/perspective": { "title": "perspective API" diff --git a/docs/md/js.md b/docs/md/js.md index 2040d6a996..5811ee0a87 100644 --- a/docs/md/js.md +++ b/docs/md/js.md @@ -100,7 +100,7 @@ different visualization on load or transform the dataset, use the viewer's attri ``` For more details about the full attribute API, see the -[``](/js.html#setting--reading-viewer-configuration-via-attributes)section of this user guide. +[``](#setting--reading-viewer-configuration-via-attributes)section of this user guide. ## Module Structure @@ -740,14 +740,10 @@ const fs = require("fs"); // module's directory. const host = new WebSocketServer({assets: [__dirname], port: 8080}); -// Read an arrow file from the file system and load it as a named table. +// Read an arrow file from the file system and host it as a named table. const arr = fs.readFileSync(__dirname + "/superstore.arrow"); const tbl = table(arr); host.host_table("table_one", tbl); - -// Or host a view -const view = tbl.view({filter: [["State", "==", "Texas"]]}); -host.host_view("view_one", view); ``` In the browser: @@ -760,11 +756,16 @@ const websocket = perspective.websocket(window.location.origin.replace("http", " // Bind the viewer to the preloaded data source. `table` and `view` objects // live on the server. -elem.load(websocket.open_table("table_one")); +const server_table = websocket.open_table("table_one"); +elem.load(server_table); -// Or load data from a view. The browser now also has a copy of this view in -// its own `table`, as well as its updates. Transfer uses Arrows. -elem.load(websocket.open_view("view_one")); +// Or load data from a table using a view. The browser now also has a copy of +// this view in its own `table`, as well as its updates transferred to the +// browser using Apache Arrow. +const worker = perspective.worker(); +const server_view = server_table.view(); +const client_table = worker.table(server_view); +elem.load(client_table); ``` `` instances bound in this way are otherwise no different diff --git a/docs/md/python.md b/docs/md/python.md index 07d9739989..d126ce156d 100644 --- a/docs/md/python.md +++ b/docs/md/python.md @@ -302,31 +302,44 @@ of `PerspectiveManager` with a string name and the instance to be hosted: ```python manager = PerspectiveManager() table = Table(data) -view = table.view() manager.host_table("data_source", table) -manager.host_view("view_1", view) ``` The `name` provided is important, as it enables Perspective in Javascript to look -up a `Table`/`View` and get a handle to it over the network. This enables +up a `Table` and get a handle to it over the network. This enables several powerful server/client implementations of Perspective, as explained in the next section. -### Using a hosted `Table` in Javascript +### Distributed Mode Using Tornado and [`PerspectiveTornadoHandler`](/docs/md/python.html#perspectivetornadohandler), -as well as `Perspective`'s Javascript library, we can create a client/server -architecture that hosts and transforms _massive_ datasets with minimal -client resource usage. - -Perspective's design allows a `table()` created in Javascript to _proxy_ its -operations to a `Table` created in Python, which executes the operations in -the Python kernel, and returns the results of the operation to the browser. -All of this is _enabled_ through `PerspectiveManager`, which handles messaging, -processing method calls, serializing outputs for the network, etc. - -In Python, use `PerspectiveManager` and `PerspectiveTornadoHandler` to create -a websocket server that exposes a `Table`: +as well as `Perspective`'s Javascript library, we can set up "distributed" +Perspective instances that allows multiple browser `perspective-viewer` +clients to read from a common `perspective-python` server. In exchange for sending the +entire dataset to the client on initialization, server load is reduced and +client performance is not network-dependent. + +[Example](https://github.com/finos/perspective/tree/master/examples/tornado-python) + +This architecture works by maintaining two `Tables`—one on the server, and one +on the client that mirrors the server's `Table` automatically using `on_update`. +All updates to the table on the server are automatically applied to each client, +which makes this architecture a natural fit for streaming dashboards and other +distributed use-cases. + +Because the `Table` is mirrored, the user gets all the performance benefits of +Perspective in WebAssembly, and can examine server-hosted datasets with zero +network lag on their interactions. + +In conjunction with [Async Mode](#async-mode), distributed Perspective offers +consistently high performance over large numbers of clients and large datasets. +As server dataset sizes increase, the initial load time of a client will +increase, but once the data is loaded there is no network lag visible to the +user. + +Using the [tornado-python](https://github.com/finos/perspective/tree/master/examples/tornado-python) +example, one can easily create a distributed Perspective server using +`server.py` and `index.html`: _*server.py*_ @@ -335,7 +348,7 @@ from perspective import Table, PerspectiveManager, PerspectiveTornadoHandler # Create an instance of PerspectiveManager, and host a Table MANAGER = PerspectiveManager() -TABLE = Table(large_dataset) +TABLE = Table(data) # The Table is exposed at `localhost:8888/websocket` with the name `data_source` MANAGER.host_table("data_source", TABLE) @@ -352,13 +365,9 @@ loop = tornado.ioloop.IOLoop.current() loop.start() ``` -`PerspectiveTornadoHandler`, as outlined in the [docs](/docs/md/python.html#perspectivetornadohandler), -takes a `PerspectiveManager` instance exposes it over a websocket at the URL -specified. This allows a `table()` in Javascript to access the `Table` in -Python and read data from it. - -Most importantly, the client code in Javascript does not require Webpack or any -bundler, and can be implemented in a single HTML file: +Instead of calling `load(server_table)`, create a `View` using `server_table` +and pass that into `viewer.load()`. This will automatically register an +`on_update` callback that synchronizes state between the server and the client. _*index.html*_ @@ -371,58 +380,61 @@ _*index.html*_ // to accept connections at the specified URL. const websocket = perspective.websocket("ws://localhost:8888/websocket"); - /* `table` is a proxy for the `Table` we created on the server. + // Get a handle to the Table on the server + const server_table = websocket.open_table("data_source_one"); - All operations that are possible through the Javascript API are possible - on the Python API as well, thus calling `view()`, `schema()`, `update()` - etc. on `const table` will pass those operations to the Python `Table`, - execute the commands, and return the result back to Javascript.*/ - const table = websocket.open_table("data_source_one"); + // Create a new view + const server_view = table.view(); - // Load this in the ``. - document.getElementById("viewer").load(table); + // Create a Table on the client using `perspective.worker()` + const worker = perspective.worker(); + const client_table = worker.table(view); + + // Load the client table in the ``. + document.getElementById("viewer").load(client_table); }); ``` -### Using a hosted `View` in Javascript +For a more complex example that offers distributed editing of the server +dataset, see [client_server_editing.html](https://github.com/finos/perspective/blob/master/examples/tornado-python/client_server_editing.html). -An alternative client/server architecture using `PerspectiveTornadoHandler` and -`PerspectiveManager` involves hosting a `View`, and creating a new `table()` in -Javascript on top of the Python `View`. +### Server Mode -When the `table()` is created in Javascript, it serializes the Python `View`'s -data into Arrow, transfers it into the Javascript `table()`, and sets up an -`on_update` callback to `update()` the Table whenever the Python `View`'s -`Table` updates. +An alternative architecture uses a single `Table` on the Python server, which +allows hosting of _massive_ datasets with minimal client resource usage. This +comes at the expense of client-side performance, as all operations must be +proxied over the network to the server. -Implementing the server in Python is extremely similar to the implementation -described in the last section. +The server setup is identical to [Distributed Mode](#distributed-mode) above, +but instead of creating a view, the client calls `load(server_table)`: +In Python, use `PerspectiveManager` and `PerspectiveTornadoHandler` to create +a websocket server that exposes a `Table`: -Replace `host_table` with `host_view`: +_*index.html*_ -```python -# we have an instance of `PerspectiveManager` -TABLE = Table(data) -VIEW = TABLE.view() -MANAGER.host_view("view_one", VIEW) +```html + -# Continue with Tornado setup -``` + +``` ## `PerspectiveWidget` diff --git a/examples/git-history/chained.html b/examples/git-history/chained.html index 90e1dd6855..d8ea44622e 100644 --- a/examples/git-history/chained.html +++ b/examples/git-history/chained.html @@ -41,7 +41,8 @@ window.addEventListener('WebComponentsReady', async function() { var elem = document.getElementById('view1'); var client = perspective.websocket(); - let view = client.open_view('data_source_one'); + let table = client.open_table('data_source'); + let view = table.view(); let arrow = await view.to_arrow(); elem.load(perspective.worker().table(arrow)); }); diff --git a/examples/remote-express-typescript/assets/index.html b/examples/remote-express-typescript/assets/index.html index 77bd3d6c4c..0c89a78cf8 100644 --- a/examples/remote-express-typescript/assets/index.html +++ b/examples/remote-express-typescript/assets/index.html @@ -47,10 +47,9 @@ const worker = perspective.worker(); worker.initialize_profile_thread(); - // Get a proxy for a view named "data_source_one", registered on - // the server with a reciprocal call to `host_view()`. - // No data is transferred, `view` is a virtual handle for data on - // the server. + // Open a `Table` that is hosted on the server. All instructions + // will be proxied to the server `Table` - no calculations are + // done on the client. const table = websocket.open_table('remote_table'); // Create a `table` from this, owned by the local WebWorker. diff --git a/examples/remote-express/index.html b/examples/remote-express/index.html index 77bd3d6c4c..0c89a78cf8 100644 --- a/examples/remote-express/index.html +++ b/examples/remote-express/index.html @@ -47,10 +47,9 @@ const worker = perspective.worker(); worker.initialize_profile_thread(); - // Get a proxy for a view named "data_source_one", registered on - // the server with a reciprocal call to `host_view()`. - // No data is transferred, `view` is a virtual handle for data on - // the server. + // Open a `Table` that is hosted on the server. All instructions + // will be proxied to the server `Table` - no calculations are + // done on the client. const table = websocket.open_table('remote_table'); // Create a `table` from this, owned by the local WebWorker. diff --git a/examples/remote-workspace/server.js b/examples/remote-workspace/server.js index 110301de08..6aad68cab0 100644 --- a/examples/remote-workspace/server.js +++ b/examples/remote-workspace/server.js @@ -2,4 +2,4 @@ const perspective = require("@finos/perspective"); const {securities} = require("../datasources"); const host = new perspective.WebSocketServer(); -securities().then(table => host.host_view("securities", table.view())); +securities().then(table => host.host_table("securities_table", table)); diff --git a/examples/remote-workspace/src/index.js b/examples/remote-workspace/src/index.js index 6486c0411a..753785def1 100644 --- a/examples/remote-workspace/src/index.js +++ b/examples/remote-workspace/src/index.js @@ -17,8 +17,8 @@ import "./index.less"; window.addEventListener("load", () => { const websocket = perspective.websocket("ws://localhost:8080"); const worker = perspective.shared_worker(); - const view = websocket.open_view("securities"); - const table = worker.table(view, {limit: 10000}); + const server_table = websocket.open_table("securities_table"); + const table = worker.table(server_table.view(), {limit: 10000}); const workspace = document.createElement("perspective-workspace"); document.body.appendChild(workspace); diff --git a/examples/tornado-python/client_server_editing.html b/examples/tornado-python/client_server_editing.html index 0f9481c544..04f8a6861b 100644 --- a/examples/tornado-python/client_server_editing.html +++ b/examples/tornado-python/client_server_editing.html @@ -55,17 +55,20 @@ const websocket = perspective.websocket("ws://localhost:8080/websocket"); const worker = perspective.shared_worker(); - // Get handles to both the `Table` and `View`, as we manually set - // up the client/server editing. + // Get handles to the `Table` on the server, and create a + // `view()` on the server. const server_table = websocket.open_table("data_source_one"); - const server_view = websocket.open_view("view_one"); + const server_view = server_table.view(); // Serialize the current state of the view to an arrow, and create // a Table on the client that has the same index as the Table - // on the server. Client/server editing does not work on an - // unindexed Table. + // on the server using `get_index()`. + // + // Client/server editing does not work on an unindexed Table. const arrow = await server_view.to_arrow(); - const client_table = worker.table(arrow, {index: "Row ID"}); + const client_table = worker.table(arrow, { + index: await server_table.get_index() + }); const client_view = client_table.view();// client -> server await viewer.load(client_table); diff --git a/examples/tornado-python/index.html b/examples/tornado-python/index.html index 06696d4a29..b53290eaa6 100644 --- a/examples/tornado-python/index.html +++ b/examples/tornado-python/index.html @@ -52,16 +52,23 @@ * state is automatically synchronized between the two. * * To create a Perspective client running in "distributed mode", - * host a `View` in the Python server and create a `Table` in the - * client using a proxy to the hosted `View`. + * host a `Table` in the Python server, create a `Table` on the + * client through a `View` on the server. This synchronizes the + * server's `Table` with the client's `Table`. */ const worker = perspective.worker(); /** - * `view` is a proxy to the hosted `View` in the Python server. All - * public API methods are available through this proxied view. + * `server_table` is a proxy to the hosted `Table` in the Python + * server. All public API methods are available through this + * proxied table. */ - const view = websocket.open_view('view_one'); + const server_table = websocket.open_table("data_source_one"); + + /** + * Create a `View` on the server. + */ + const server_view = server_table.view(); /** * When a `View` is passed into `table()` to create a new `Table`, @@ -74,11 +81,16 @@ * `Table` and its data (and associated viewer) are still fully * functional, but will not be kept up-to-date with server state. * + * Use `table.get_index()` in order to create a `Table` on the + * client that has the exact same settings as the server. + * * For a more complex example of manually controlling state * synchronization between server and client, see * `client_server_editing.html`. */ - const table = worker.table(view); + const table = worker.table(server_view, { + index: await server_table.get_index() + }); // Load the local table in the ``. document.getElementById('viewer').load(table); diff --git a/examples/tornado-python/server.py b/examples/tornado-python/server.py index e8203e0125..538b28e87b 100644 --- a/examples/tornado-python/server.py +++ b/examples/tornado-python/server.py @@ -31,7 +31,6 @@ def perspective_thread(manager): with open(file_path, mode="rb") as file: table = Table(file.read(), index="Row ID") manager.host_table("data_source_one", table) - manager.host_view("view_one", table.view()) psp_loop.start() diff --git a/examples/workspace-editing-python/src/index.js b/examples/workspace-editing-python/src/index.js index 0d1e235190..7cd287d11b 100644 --- a/examples/workspace-editing-python/src/index.js +++ b/examples/workspace-editing-python/src/index.js @@ -29,12 +29,11 @@ const websocket = perspective.websocket(URL); const worker = perspective.shared_worker(); /** - * `open_table` and `open_view` allow you to call API methods on remotely - * hosted Perspective tables and views, just as you would on a locally created - * table/view. + * `open_table` allows you to call API methods on remotely hosted Perspective + * tables just as you would on a locally created table. */ const server_table = websocket.open_table("data_source_one"); -const server_view = websocket.open_view("view_one"); +const server_view = server_table.view(); // All viewers are based on the same table, which then feed edits back to a // table on the server with a schema. diff --git a/examples/workspace-editing-python/src/server.py b/examples/workspace-editing-python/src/server.py index e141c605eb..9f0ad5ec1e 100644 --- a/examples/workspace-editing-python/src/server.py +++ b/examples/workspace-editing-python/src/server.py @@ -40,7 +40,6 @@ def perspective_thread(manager): with open(file_path, mode="rb") as file: table = Table(file.read(), index="Row ID") manager.host_table("data_source_one", table) - manager.host_view("view_one", table.view()) psp_loop.start() diff --git a/packages/perspective-jupyterlab/src/ts/view.ts b/packages/perspective-jupyterlab/src/ts/view.ts index ecd7f8f756..d0f6d57760 100644 --- a/packages/perspective-jupyterlab/src/ts/view.ts +++ b/packages/perspective-jupyterlab/src/ts/view.ts @@ -242,21 +242,28 @@ export class PerspectiveView extends DOMWidgetView { const table_options = msg.data["options"] || {}; if (this.pWidget.client) { - // In client mode, retrieve the serialized data and the options - // passed by the user, and create a new table on the client end. + /** + * In client mode, retrieve the serialized data and the options + * passed by the user, and create a new table on the client end. + */ const data = msg.data["data"]; this.pWidget.load(data, table_options); } else { if (this.pWidget.server && msg.data["table_name"]) { - // Get a remote table handle, and load the remote table in - // the client. + /** + * Get a remote table handle, and load the remote table in + * the client for server mode Perspective. + */ const table = this.perspective_client.open_table(msg.data["table_name"]); this.pWidget.load(table, table_options); - } else if (msg.data["table_name"] && msg.data["view_name"]) { - // Get a remote view handle from the Jupyter kernel, and - // create a client-side table using the view handle. + } else if (msg.data["table_name"]) { + /** + * Get a remote table handle from the Jupyter kernel, and + * create a view on that handle to run Perspective in + * distributed mode. + */ const kernel_table: Table = this.perspective_client.open_table(msg.data["table_name"]); - const kernel_view: View = this.perspective_client.open_view(msg.data["view_name"]); + const kernel_view: View = kernel_table.view(); // If the widget is editable, set up client/server editing if (this.pWidget.editable) { @@ -272,13 +279,15 @@ export class PerspectiveView extends DOMWidgetView { let client_edit_port: number, server_edit_port: number; // Create ports on the client and kernel. - Promise.all([this.pWidget.load(client_table), this.pWidget.getEditPort(), kernel_table.make_port()]).then(outs => { - client_edit_port = outs[1]; - server_edit_port = outs[2]; + Promise.all([this.pWidget.load(client_table), this.pWidget.getEditPort(), kernel_table.make_port()]).then(ports => { + client_edit_port = ports[1]; + server_edit_port = ports[2]; }); - // When the client updates, if the update comes through - // the edit port then forward it to the server. + /** + * When the client updates, if the update comes through + * the edit port then forward it to the server. + */ client_view.on_update( updated => { if (updated.port_id === client_edit_port) { @@ -290,9 +299,11 @@ export class PerspectiveView extends DOMWidgetView { {mode: "row"} ); - // If the server updates, and the edit is not coming - // from the server edit port, then synchronize state - // with the client. + /** + * If the server updates, and the edit is not coming + * from the server edit port, then synchronize state + * with the client. + */ kernel_view.on_update( updated => { if (updated.port_id !== server_edit_port) { @@ -303,8 +314,10 @@ export class PerspectiveView extends DOMWidgetView { ); }); } else { - // Just load the view into the widget, everything else - // is handled. + /** + * Just load the view into the widget, as the load + * semantics are handled by Perspective. + */ this.pWidget.load(kernel_view, table_options); } } else { diff --git a/packages/perspective-jupyterlab/test/js/unit/view.spec.js b/packages/perspective-jupyterlab/test/js/unit/view.spec.js index b6998da554..4e0306edaf 100644 --- a/packages/perspective-jupyterlab/test/js/unit/view.spec.js +++ b/packages/perspective-jupyterlab/test/js/unit/view.spec.js @@ -96,24 +96,27 @@ describe("PerspectiveView", function() { }); }); - it("Should handle a well-formed table/view message from the kernel", async function() { - const table_name = uuid(); - const view_name = uuid(); + it("Should handle a well-formed table message from the kernel", async function() { view = await manager.create_view(model)(); + const mock_client = PerspectiveJupyterClient.mock.instances[0]; + mock_client.open_table.mockReturnValue({ + view: jest.fn() + }); + + // Mock the output of open_table() so `view()` is valid + + const table_name = uuid(); view._handle_message({ id: -2, type: "table", data: { - table_name: table_name, - view_name: view_name + table_name: table_name } }); - const mock_client = PerspectiveJupyterClient.mock.instances[0]; - - // `open_view` should be called correctly - const open_view_arg = mock_client.open_view.mock.calls[0][0]; - expect(open_view_arg).toEqual(view_name); + // `open_table` should be called correctly + const open_table_arg = mock_client.open_table.mock.calls[0][0]; + expect(open_table_arg).toEqual(table_name); const send_arg = mock_client.send.mock.calls[0][0]; expect(send_arg).toEqual({ @@ -122,27 +125,28 @@ describe("PerspectiveView", function() { }); }); - it("Should handle a well-formed table/view message with index from the kernel", async function() { - const table_name = uuid(); - const view_name = uuid(); + it("Should handle a well-formed table message with index from the kernel", async function() { view = await manager.create_view(model)(); + const mock_client = PerspectiveJupyterClient.mock.instances[0]; + mock_client.open_table.mockReturnValue({ + view: jest.fn() + }); + + const table_name = uuid(); view._handle_message({ id: -2, type: "table", data: { table_name: table_name, - view_name: view_name, options: { index: "a" } } }); - const mock_client = PerspectiveJupyterClient.mock.instances[0]; - - // `open_view` should be called correctly - const open_view_arg = mock_client.open_view.mock.calls[0][0]; - expect(open_view_arg).toEqual(view_name); + // `open_table` should be called correctly + const open_table_arg = mock_client.open_table.mock.calls[0][0]; + expect(open_table_arg).toEqual(table_name); const send_arg = mock_client.send.mock.calls[0][0]; expect(send_arg).toEqual({ @@ -151,27 +155,28 @@ describe("PerspectiveView", function() { }); }); - it("Should handle a well-formed view message with limit from the kernel", async function() { - const table_name = uuid(); - const view_name = uuid(); + it("Should handle a well-formed table message with limit from the kernel", async function() { view = await manager.create_view(model)(); + const mock_client = PerspectiveJupyterClient.mock.instances[0]; + mock_client.open_table.mockReturnValue({ + view: jest.fn() + }); + + const table_name = uuid(); view._handle_message({ id: -2, type: "table", data: { table_name: table_name, - view_name: view_name, options: { limit: 1000 } } }); - const mock_client = PerspectiveJupyterClient.mock.instances[0]; - - // `open_view` should be called correctly - const open_view_arg = mock_client.open_view.mock.calls[0][0]; - expect(open_view_arg).toEqual(view_name); + // `open_table` should be called correctly + const open_table_arg = mock_client.open_table.mock.calls[0][0]; + expect(open_table_arg).toEqual(table_name); const send_arg = mock_client.send.mock.calls[0][0]; expect(send_arg).toEqual({ diff --git a/packages/perspective/README.md b/packages/perspective/README.md index c2b4774724..471f450d94 100644 --- a/packages/perspective/README.md +++ b/packages/perspective/README.md @@ -31,6 +31,8 @@ For more information, see the * [.remove_delete(callback)](#module_perspective..view+remove_delete) * [~table](#module_perspective..table) * [new table()](#new_module_perspective..table_new) + * [.get_index()](#module_perspective..table+get_index) + * [.get_limit()](#module_perspective..table+get_limit) * [.clear()](#module_perspective..table+clear) * [.replace()](#module_perspective..table+replace) * [.delete()](#module_perspective..table+delete) @@ -282,6 +284,7 @@ serialize. - .end_col number - The ending column index from which to serialize. + * * * @@ -415,8 +418,6 @@ receives an object with two keys: `port_id`, indicating which port the update was triggered on, and `delta`, whose value is dependent on the `mode` parameter: - "none" (default): `delta` is `undefined`. - - "cell": `delta` is the new data for each updated cell, serialized - to JSON format. - "row": `delta` is an Arrow of the updated rows. **Example** @@ -479,6 +480,8 @@ view.remove_delete(callback); * [~table](#module_perspective..table) * [new table()](#new_module_perspective..table_new) + * [.get_index()](#module_perspective..table+get_index) + * [.get_limit()](#module_perspective..table+get_limit) * [.clear()](#module_perspective..table+clear) * [.replace()](#module_perspective..table+replace) * [.delete()](#module_perspective..table+delete) @@ -510,6 +513,26 @@ on the perspective module object, or an a [module:perspective~worker](module:perspective~worker) instance. +* * * + + + +#### table.get\_index() +Returns the user-specified index column for this +[table](#module_perspective..table) or null if an index is not set. + +**Kind**: instance method of [table](#module_perspective..table) + +* * * + + + +#### table.get\_limit() +Returns the user-specified limit column for this +[table](#module_perspective..table) or null if an limit is not set. + +**Kind**: instance method of [table](#module_perspective..table) + * * * @@ -553,9 +576,8 @@ invoked. **Kind**: instance method of [table](#module_perspective..table) **Params** -- callback function - A callback function invoked on delete. The - parameter to this callback shares a structure with the return type of - [module:perspective~table#to_json](module:perspective~table#to_json). +- callback function - A callback function with no parameters + that will be invoked on `delete()`. * * * diff --git a/packages/perspective/index.d.ts b/packages/perspective/index.d.ts index 170a169a51..8cca867871 100644 --- a/packages/perspective/index.d.ts +++ b/packages/perspective/index.d.ts @@ -146,7 +146,6 @@ declare module "@finos/perspective" { export class WebSocketClient { open_table(name: string): Table; - open_view(name: string): View; terminate(): void; initialize_profile_thread(): void; send(msg: any): void; @@ -162,7 +161,6 @@ declare module "@finos/perspective" { export class WebSocketManager { add_connection(ws: ws): void; host_table(name: string, table: Table): void; - host_view(name: string, view: View): void; eject_table(name: string): void; eject_view(name: string): void; } diff --git a/packages/perspective/src/js/api/client.js b/packages/perspective/src/js/api/client.js index 3af78fc132..32cbe4cf93 100644 --- a/packages/perspective/src/js/api/client.js +++ b/packages/perspective/src/js/api/client.js @@ -8,7 +8,6 @@ */ import {table, proxy_table} from "./table_api.js"; -import {proxy_view} from "./view_api.js"; import {bindall} from "../utils.js"; /** @@ -87,10 +86,6 @@ export class Client { return new proxy_table(this, name); } - open_view(name) { - return new proxy_view(this, name); - } - /** * Handle a command from Perspective. If the Client is not initialized, * initialize it and dispatch the `perspective-ready` event. diff --git a/packages/perspective/src/js/websocket/manager.js b/packages/perspective/src/js/websocket/manager.js index a0794c475c..46e6d05f9a 100644 --- a/packages/perspective/src/js/websocket/manager.js +++ b/packages/perspective/src/js/websocket/manager.js @@ -185,18 +185,6 @@ export class WebSocketManager extends Server { this._host(this._tables, name, table); } - /** - * Expose a Perspective `view` through the WebSocket, allowing - * it to be accessed by a unique name from a client. Hosted objects - * are automatically `eject`ed when their `delete()` method is called. - * - * @param {String} name - * @param {perspective.view} view `view` to host. - */ - host_view(name, view) { - this._host(this._views, name, view); - } - /** * Cease hosting a `table` on this server. Hosted objects * are automatically `eject`ed when their `delete()` method is called. diff --git a/python/perspective/bench/tornado/async_server.py b/python/perspective/bench/tornado/async_server.py index d51f993298..45899b4331 100644 --- a/python/perspective/bench/tornado/async_server.py +++ b/python/perspective/bench/tornado/async_server.py @@ -114,7 +114,6 @@ def server(queue, is_async): manager = perspective.PerspectiveManager() table = get_table() manager.host_table("data_source_one", table) - manager.host_view("view_one", table.view()) if is_async: thread = threading.Thread(target=perspective_thread, args=(manager,)) diff --git a/python/perspective/bench/tornado/distributed_mode.py b/python/perspective/bench/tornado/distributed_mode.py index 731acfca79..6b05eb1e07 100644 --- a/python/perspective/bench/tornado/distributed_mode.py +++ b/python/perspective/bench/tornado/distributed_mode.py @@ -26,16 +26,6 @@ async def make_view_arrow(client): return [end] -async def open_view_arrow(client): - """Test how long it takes to open a remote view and retrieve an arrow""" - view = client.open_view("view_one") - start = time.time() - arrow = await view.to_arrow(end_row=ARROW_LENGTH) - end = time.time() - start - assert len(arrow) > 0 - return [end] - - if __name__ == "__main__": """To allow your test script to run within the benchmark harness, import and create a `PerspectiveTornadoBenchmark`, and call its @@ -56,6 +46,3 @@ async def open_view_arrow(client): logging.info("Create view, request arrow length %d", ARROW_LENGTH) runner = PerspectiveTornadoBenchmark(make_view_arrow) runner.run() - logging.info("Open view, request arrow length %d", ARROW_LENGTH) - runner2 = PerspectiveTornadoBenchmark(open_view_arrow) - runner2.run() diff --git a/python/perspective/perspective/client/client.py b/python/perspective/perspective/client/client.py index aaa5d292b6..e871ab5e36 100644 --- a/python/perspective/perspective/client/client.py +++ b/python/perspective/perspective/client/client.py @@ -11,7 +11,6 @@ from ..core.exception import PerspectiveError from .table_api import PerspectiveTableProxy from .table_api import table as make_table -from .view_api import PerspectiveViewProxy class PerspectiveClient(object): @@ -45,10 +44,6 @@ def open_table(self, name): """Return a proxy Table to a `Table` hosted in a server somewhere.""" return PerspectiveTableProxy(self, name) - def open_view(self, name): - """Return a proxy View to a `View` hosted in a server somewhere.""" - return PerspectiveViewProxy(self, name) - def _handle(self, msg): """Given a response from the Perspective server, resolve the Future with the response or an exception.""" diff --git a/python/perspective/perspective/manager/manager.py b/python/perspective/perspective/manager/manager.py index 08cba727e2..017200036d 100644 --- a/python/perspective/perspective/manager/manager.py +++ b/python/perspective/perspective/manager/manager.py @@ -11,7 +11,6 @@ from functools import partial from ..core.exception import PerspectiveError from ..table import Table -from ..table.view import View from .session import PerspectiveSession from .manager_internal import _PerspectiveManagerInternal @@ -27,14 +26,14 @@ class PerspectiveManager(_PerspectiveManagerInternal): The core functionality resides in `process()`, which receives JSON-serialized messages from a client (implemented by `perspective-viewer` in the browser), executes the commands in the message, and returns the - results of those commands back to the `post_callback`. Table/View instances - should be passed to the manager using `host_table` or `host_view` - this - allows server code to call Table/View APIs natively instead of proxying - them through the Manager. Because Perspective is designed to be used in a - shared context, i.e. multiple clients all accessing the same `Table`, - PerspectiveManager comes with the context of `sessions` - an - encapsulation of the actions and resources used by a single connection - to Perspective, which can be cleaned up after the connection is closed. + results of those commands back to the `post_callback`. Table instances + should be passed to the manager using `host_table` - this allows server + code to call Table APIs natively instead of proxying them through the + Manager. Because Perspective is designed to be used in a shared context, + i.e. multiple clients all accessing the same `Table`, PerspectiveManager + comes with the context of `sessions` - an encapsulation of the actions + and resources used by a single connection to Perspective, which can be + cleaned up after the connection is closed. - When a client connects, for example through a websocket, a new session should be spawned using `new_session()`. @@ -53,7 +52,7 @@ def __init__(self, lock=False): def lock(self): """Block messages that can mutate the state of :obj:`~perspective.Table` - and :obj:`~perspective.View` objects under management. + objects under management. All ``PerspectiveManager`` objects exposed over the internet should be locked to prevent content from being mutated by clients. @@ -62,33 +61,25 @@ def lock(self): def unlock(self): """Unblock messages that can mutate the state of - :obj:`~perspective.Table` and :obj:`~perspective.View` objects under - management.""" + :obj:`~perspective.Table` objects under management.""" self._lock = False def host(self, item, name=None): - """Given a :obj:`~perspective.Table` or :obj:`~perspective.View`, - place it under management and allow operations on it to be passed - through the Manager instance. + """Given a :obj:`~perspective.Table`, place it under management and + allow operations on it to be passed through the Manager instance. Args: - table_or_view (:obj:`~perspective.Table`/:obj:`~perspective.View`) : - a Table or View to be managed. + item (:obj:`~perspective.Table`) : a Table to be managed. Keyword Args: name (:obj:`str`) : an optional name to allow retrieval through - ``get_table`` or ``get_view``. A name will be generated if not - provided. + ``get_table``. A name will be generated if not provided. """ name = name or gen_name() if isinstance(item, Table): self.host_table(name, item) - elif isinstance(item, View): - self.host_view(name, item) else: - raise PerspectiveError( - "Only `Table()` and `View()` instances can be hosted." - ) + raise PerspectiveError("Only `Table()` instances can be hosted.") def host_table(self, name, table): """Given a reference to a `Table`, manage it and allow operations on it @@ -108,20 +99,10 @@ def host_table(self, name, table): self._tables[name] = table return name - def host_view(self, name, view): - """Given a :obj:`~perspective.View`, add it to the manager's views - container. - """ - self._views[name] = view - def get_table(self, name): """Return a table under management by name.""" return self._tables.get(name, None) - def get_view(self, name): - """Return a view under management by name.""" - return self._views.get(name, None) - def new_session(self): return PerspectiveSession(self) diff --git a/python/perspective/perspective/manager/manager_internal.py b/python/perspective/perspective/manager/manager_internal.py index a840f81236..7a5c2456ca 100644 --- a/python/perspective/perspective/manager/manager_internal.py +++ b/python/perspective/perspective/manager/manager_internal.py @@ -50,6 +50,10 @@ def __init__(self, lock=False): # special message flow. self._pending_binary = None + def _get_view(self, name): + """Return a view under management by name.""" + return self._views.get(name, None) + def _process(self, msg, post_callback, client_id=None): """Given a message from the client, process it through the Perspective engine. diff --git a/python/perspective/perspective/manager/session.py b/python/perspective/perspective/manager/session.py index bce47c0083..b0e9c9679b 100644 --- a/python/perspective/perspective/manager/session.py +++ b/python/perspective/perspective/manager/session.py @@ -46,7 +46,7 @@ def _clear_callbacks(self): # remove all callbacks from the view's cache for cb in self.manager._callback_cache: if cb["client_id"] == self.client_id: - view = self.manager.get_view(cb["name"]) + view = self.manager._get_view(cb["name"]) if view: view.remove_update(cb["callback"]) # remove all callbacks from the manager's cache diff --git a/python/perspective/perspective/table/view.py b/python/perspective/perspective/table/view.py index c4bdb5e3b0..7f632afe45 100644 --- a/python/perspective/perspective/table/view.py +++ b/python/perspective/perspective/table/view.py @@ -350,9 +350,9 @@ def delete(self): """Delete the :class:`~perspective.View` and clean up all associated callbacks. - This method must be called to clean up resources used by the - :class:`~perspective.View`, as it will last for the lifetime of the - underlying :class:`~perspective.Table` otherwise. + This method must be called to clean up callbacks used by the + :class:`~perspective.View`, as well as allow for deletion of the + underlying :class:`~perspective.Table`. Examples: >>> table = perspective.Table(data) diff --git a/python/perspective/perspective/tests/manager/test_manager.py b/python/perspective/perspective/tests/manager/test_manager.py index 0dd83778b8..89ed903e4e 100644 --- a/python/perspective/perspective/tests/manager/test_manager.py +++ b/python/perspective/perspective/tests/manager/test_manager.py @@ -23,7 +23,6 @@ class TestPerspectiveManager(object): def post(self, msg): '''boilerplate callback to simulate a client's `post()` method.''' msg = json.loads(msg) - print("self.post:", msg) assert msg["id"] is not None def validate_post(self, msg, expected=None): @@ -42,22 +41,6 @@ def test_manager_host_table(self): "b": str } - def test_manager_host_view(self): - manager = PerspectiveManager() - table = Table(data) - view = table.view() - manager.host_view("view1", view) - assert manager.get_view("view1").to_dict() == data - - def test_manager_host_table_or_view(self): - manager = PerspectiveManager() - table = Table(data) - view = table.view() - manager.host(table, name="table1") - manager.host(view, name="view1") - assert manager.get_table("table1").size() == 3 - assert manager.get_view("view1").to_dict() == data - def test_manager_host_invalid(self): manager = PerspectiveManager() with raises(PerspectiveError): @@ -137,28 +120,13 @@ def test_manager_create_indexed_table_and_remove(self): "b": ["c"] } - def test_manager_create_view(self): - message = {"id": 1, "name": "view1", "cmd": "view_method", "method": "schema", "args": []} - manager = PerspectiveManager() - table = Table(data) - view = table.view() - manager.host_table("table1", table) - manager.host_view("view1", view) - manager._process(message, self.post) - assert manager.get_view("view1").schema() == { - "a": int, - "b": str - } - def test_locked_manager_create_view(self): - message = {"id": 1, "name": "view1", "cmd": "view_method", "method": "schema", "args": []} + message = {"id": 1, "table_name": "table1", "view_name": "view1", "cmd": "view"} manager = PerspectiveManager(lock=True) table = Table(data) - view = table.view() manager.host_table("table1", table) - manager.host_view("view1", view) manager._process(message, self.post) - assert manager.get_view("view1").schema() == { + assert manager._get_view("view1").schema() == { "a": int, "b": str } @@ -329,26 +297,24 @@ def test_manager_table_schema(self): message = {"id": 1, "name": "table1", "cmd": "table_method", "method": "schema", "args": [False]} manager = PerspectiveManager() table = Table(data) - view = table.view() manager.host_table("table1", table) - manager.host_view("view1", view) manager._process(message, post_callback) def test_manager_view_schema(self): post_callback = partial(self.validate_post, expected={ - "id": 1, + "id": 2, "data": { "a": "integer", "b": "integer" } }) - message = {"id": 1, "name": "view1", "cmd": "view_method", "method": "schema", "args": [False]} + make_view_message = {"id": 1, "table_name": "table1", "view_name": "view1", "cmd": "view", "config": {"row_pivots": ["a"]}} + message = {"id": 2, "name": "view1", "cmd": "view_method", "method": "schema", "args": [False]} manager = PerspectiveManager() table = Table(data) - view = table.view(row_pivots=["a"]) manager.host_table("table1", table) - manager.host_view("view1", view) + manager._process(make_view_message, self.post) manager._process(message, post_callback) def test_manager_table_computed_schema(self): @@ -378,7 +344,6 @@ def test_manager_table_computed_schema(self): table = Table(data) view = table.view() manager.host_table("table1", table) - manager.host_view("view1", view) manager._process(message, post_callback) def test_manager_table_get_computation_input_types(self): @@ -396,31 +361,29 @@ def test_manager_table_get_computation_input_types(self): } manager = PerspectiveManager() table = Table(data) - view = table.view() manager.host_table("table1", table) - manager.host_view("view1", view) manager._process(message, post_callback) def test_manager_view_computed_schema(self): post_callback = partial(self.validate_post, expected={ - "id": 1, + "id": 2, "data": { "abc": "float" } }) - message = {"id": 1, "name": "view1", "cmd": "view_method", "method": "computed_schema", "args": [False]} - manager = PerspectiveManager() - table = Table(data) - view = table.view(computed_columns=[ + make_view_message = {"id": 1, "table_name": "table1", "view_name": "view1", "cmd": "view", "config": {"computed_columns": [ { "column": "abc", "computed_function_name": "+", "inputs": ["a", "a"] } - ]) + ]}} + message = {"id": 2, "name": "view1", "cmd": "view_method", "method": "computed_schema", "args": [False]} + manager = PerspectiveManager() + table = Table(data) manager.host_table("table1", table) - manager.host_view("view1", view) + manager._process(make_view_message, self.post) manager._process(message, post_callback) # serialization @@ -955,7 +918,7 @@ def delete_callback(): make_view = {"id": 2, "table_name": "table1", "view_name": "view1", "cmd": "view"} manager._process(make_view, self.post) - view = manager.get_view("view1") + view = manager._get_view("view1") view.on_delete(delete_callback) delete_view = {"id": 3, "name": "view1", "cmd": "view_method", "method": "delete"} @@ -977,7 +940,7 @@ def delete_callback(): make_view = {"id": 2, "table_name": "table1", "view_name": "view1", "cmd": "view"} manager._process(make_view, self.post) - view = manager.get_view("view1") + view = manager._get_view("view1") view.on_delete(delete_callback) view.remove_delete(delete_callback) diff --git a/python/perspective/perspective/tests/manager/test_session.py b/python/perspective/perspective/tests/manager/test_session.py index 3a161718af..3c25097f99 100644 --- a/python/perspective/perspective/tests/manager/test_session.py +++ b/python/perspective/perspective/tests/manager/test_session.py @@ -14,11 +14,9 @@ class TestPerspectiveSession(object): - def post(self, msg): '''boilerplate callback to simulate a client's `post()` method.''' msg = json.loads(msg) - print("self.post: ", msg) assert msg["id"] is not None def validate_post(self, msg, expected=None): @@ -49,7 +47,7 @@ def handle_to_dict(msg): # make sure the client ID is attached to the new view assert len(manager._views.keys()) == 1 - assert manager.get_view("view1")._client_id == client_id + assert manager._get_view("view1")._client_id == client_id to_dict_message = {"id": 2, "name": "view1", "cmd": "view_method", "method": "to_dict"} @@ -87,7 +85,7 @@ def handle_to_dict(msg): assert key in manager_views for i, session in enumerate(sessions): - view = manager.get_view("view" + str(i)) + view = manager._get_view("view" + str(i)) assert view._client_id == session.client_id # arbitrarily do an update @@ -118,7 +116,7 @@ def test_session_close_session_with_callbacks(self, sentinel): # make sure the client ID is attached to the new view assert len(manager._views.keys()) == 1 - assert manager.get_view("view1")._client_id == client_id + assert manager._get_view("view1")._client_id == client_id def callback(updated): assert updated["port_id"] == 0 @@ -184,7 +182,7 @@ def test_session_close_multiple_sessions_with_callbacks(self, sentinel): assert key in manager_views for i, session in enumerate(sessions): - view = manager.get_view("view" + str(i)) + view = manager._get_view("view" + str(i)) assert view._client_id == session.client_id def callback(updated): diff --git a/python/perspective/perspective/tests/viewer/test_viewer.py b/python/perspective/perspective/tests/viewer/test_viewer.py index 962eb855cf..c3ce9c7d76 100644 --- a/python/perspective/perspective/tests/viewer/test_viewer.py +++ b/python/perspective/perspective/tests/viewer/test_viewer.py @@ -72,14 +72,6 @@ def test_viewer_load_data_with_options(self): assert viewer.columns == ["a"] assert viewer.table.size() == 1 - def test_viewer_load_view(self): - table = Table({"a": [1, 2, 3]}) - viewer = PerspectiveViewer() - view = table.view() - viewer.load(view) - assert viewer._view == view - assert viewer.table == table - def test_viewer_load_clears_state(self): table = Table({"a": [1, 2, 3]}) viewer = PerspectiveViewer(dark=True, row_pivots=["a"]) @@ -211,17 +203,6 @@ def test_viewer_delete(self): assert viewer.table_name is None assert viewer.table is None - def test_viewer_delete_view(self): - table = Table({"a": [1, 2, 3]}) - viewer = PerspectiveViewer(plugin="x_bar", filters=[["a", "==", 2]]) - viewer.load(table.view()) - assert viewer.filters == [["a", "==", 2]] - viewer.delete() - assert viewer._perspective_view_name is None - assert viewer._view is None - assert viewer.table_name is None - assert viewer.table is None - def test_viewer_delete_without_table(self): table = Table({"a": [1, 2, 3]}) viewer = PerspectiveViewer(plugin="x_bar", filters=[["a", "==", 2]]) @@ -230,8 +211,6 @@ def test_viewer_delete_without_table(self): viewer.delete(delete_table=False) assert viewer.table_name is not None assert viewer.table is not None - assert viewer._perspective_view_name is not None - assert viewer._view is not None assert viewer.filters == [] def test_save_restore(self): diff --git a/python/perspective/perspective/tests/widget/test_widget.py b/python/perspective/perspective/tests/widget/test_widget.py index a9fc042c09..f676175b83 100644 --- a/python/perspective/perspective/tests/widget/test_widget.py +++ b/python/perspective/perspective/tests/widget/test_widget.py @@ -5,7 +5,7 @@ # This file is part of the Perspective library, distributed under the terms of # the Apache License 2.0. The full license can be found in the LICENSE file. # - +import six import numpy as np from datetime import date, datetime from functools import partial @@ -15,12 +15,11 @@ def mock_post(self, msg, msg_id=None, assert_msg=None): - '''Mock the widget's `post()` method so we can introspect the contents.''' + """Mock the widget's `post()` method so we can introspect the contents.""" assert msg == assert_msg class TestWidget: - def test_widget(self): data = {"a": np.arange(0, 50)} widget = PerspectiveWidget(data, plugin="x_bar") @@ -31,7 +30,6 @@ def test_widget(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": {} } } @@ -46,7 +44,6 @@ def test_widget_indexed(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": { "index": "a" } @@ -104,7 +101,6 @@ def test_widget_eventual_data(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": {} } } @@ -132,7 +128,6 @@ def test_widget_eventual_data_indexed(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": { "index": "a" } @@ -150,7 +145,6 @@ def test_widget_eventual_table_indexed(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": { "index": "a" } @@ -167,7 +161,6 @@ def test_widget_load_table(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": {} } } @@ -182,40 +175,17 @@ def test_widget_load_table_indexed(self): "type": "table", "data": { "table_name": widget.table_name, - "view_name": widget._perspective_view_name, "options": { "index": "a" } } } - def test_widget_load_view(self): - table = Table({"a": np.arange(0, 50)}) - view = table.view() - widget = PerspectiveWidget(view, plugin="x_bar") - assert widget.plugin == "x_bar" - load_msg = widget._make_load_message() - assert load_msg.to_dict() == { - "id": -2, - "type": "table", - "data": { - "table_name": widget.table_name, - "view_name": widget._perspective_view_name, - "options": {} - } - } - def test_widget_load_table_ignore_limit(self): table = Table({"a": np.arange(0, 50)}) widget = PerspectiveWidget(table, limit=1) assert widget.table.size() == 50 - def test_widget_load_view_ignore_limit(self): - table = Table({"a": np.arange(0, 50)}) - view = table.view() - widget = PerspectiveWidget(view, plugin="x_bar", limit=1) - assert widget.table.size() == 50 - def test_widget_pass_index(self): data = {"a": [1, 2, 3, 1]} widget = PerspectiveWidget(data, index="a") @@ -244,19 +214,6 @@ def test_widget_load_table_server(self): } } - def test_widget_load_view_server(self): - table = Table({"a": np.arange(0, 50)}) - view = table.view() - widget = PerspectiveWidget(view, server=True) - load_msg = widget._make_load_message() - assert load_msg.to_dict() == { - "id": -2, - "type": "table", - "data": { - "table_name": widget.table_name - } - } - def test_widget_no_data_with_server(self): # should fail widget = PerspectiveWidget(None, server=True) @@ -319,18 +276,24 @@ def test_widget_delete(self): }) widget.post = MethodType(mocked_post, widget) widget.delete() - assert widget._view is None assert widget.table is None - def test_widget_delete_view(self): + def test_widget_delete_with_view(self): data = {"a": np.arange(0, 50)} - table = Table(data) - view = table.view() - widget = PerspectiveWidget(view) + widget = PerspectiveWidget(data) + + # create a view on the manager + table_name, table = list(widget.manager._tables.items())[0] + make_view_message = {"id": 1, "table_name": table_name, "view_name": "view1", "cmd": "view", "config": {"row_pivots": ["a"]}} + widget.manager._process(make_view_message, lambda x: True) + + assert len(widget.manager._views) == 1 + mocked_post = partial(mock_post, assert_msg={ "cmd": "delete" }) + widget.post = MethodType(mocked_post, widget) widget.delete() - assert widget._view is None + assert widget.table is None diff --git a/python/perspective/perspective/viewer/viewer.py b/python/perspective/perspective/viewer/viewer.py index 1ff6571740..3a080c8d56 100644 --- a/python/perspective/perspective/viewer/viewer.py +++ b/python/perspective/perspective/viewer/viewer.py @@ -24,6 +24,7 @@ if is_libpsp(): from ..libpsp import Table, View, PerspectiveManager + from ..core.exception import PerspectiveError class PerspectiveViewer(PerspectiveTraitlets, object): @@ -121,11 +122,6 @@ def __init__( # attached PerspectiveManager self.table_name = None - # If `is_libpsp()` and the viewer has a Table, an internal view will be - # created to allow remote clients to host their own table that listens - # to updates from this table. - self._perspective_view_name = None - # Viewer configuration self.plugin = validate_plugin(plugin) self.columns = validate_columns(columns) or [] @@ -144,13 +140,6 @@ def table(self): """Returns the ``perspective.Table`` under management by the viewer.""" return self.manager.get_table(self.table_name) - @property - def _view(self): - """Returns the internal ``perspective.View`` under management by the - viewer, which has no bearing to the view that is displayed in the - widget.""" - return self.manager.get_view(self._perspective_view_name) - def load(self, data, **options): """Given a ``perspective.Table``, a ``perspective.View``, or data that can be handled by ``perspective.Table``, pass it to the @@ -198,7 +187,7 @@ def load(self, data, **options): if isinstance(data, Table): table = data elif isinstance(data, View): - table = data._table + raise PerspectiveError("Only `Table` or data can be loaded.") else: table = Table(data, **options) @@ -215,16 +204,6 @@ def load(self, data, **options): self.table_name = name - # If a `perspective.View` is loaded, then use it as the view that - # new clients will be built from. Otherwise, create a new view. - VIEW = data if isinstance(data, View) else table.view() - - # Create a view from the table, and host it with the manager so it can - # be accessed remotely. This view should not be deleted or changed, - # and remains private to the viewer. - self._perspective_view_name = str(random()) - self.manager.host_view(self._perspective_view_name, VIEW) - def update(self, data): """Update the table under management by the viewer with new data. This function follows the semantics of `Table.update()`, and will be @@ -291,10 +270,14 @@ def delete(self, delete_table=True): deleted. Defaults to True. """ if delete_table: - if self._view: - self._view.delete() - self._perspective_view_name = None + # Delete all created views on the widget's manager instance + for view in six.itervalues(self.manager._views): + view.delete() + + # Reset view cache + self.manager._views = {} + # Delete table self.table.delete() self.manager._tables.pop(self.table_name) self.table_name = None diff --git a/python/perspective/perspective/widget/widget.py b/python/perspective/perspective/widget/widget.py index a07446627b..794b17c1d3 100644 --- a/python/perspective/perspective/widget/widget.py +++ b/python/perspective/perspective/widget/widget.py @@ -473,7 +473,7 @@ def _make_load_message(self): # kernel and the front-end proxies pivots, sorts, data requests # etc. to the kernel and does not run a Table in the front-end. msg_data = {"table_name": self.table_name} - elif self._perspective_view_name is not None: + elif self.table_name is not None: # If a view is hosted by the widget's manager (by default), # run Perspective in client-server mode: a Table will be created # in the front-end that mirrors the Table hosted in the kernel, @@ -481,7 +481,6 @@ def _make_load_message(self): # and the server. msg_data = { "table_name": self.table_name, - "view_name": self._perspective_view_name, "options": {}, }