From ece41b7a4b14f397cb5b50ecec8ad6d8dc822d22 Mon Sep 17 00:00:00 2001 From: Tim Conkling Date: Tue, 10 Nov 2020 09:39:29 -0800 Subject: [PATCH] Send initialization data in the NewReport message (#2317) Removes `ForwardMsg.initialize`. All initialization data is now carried in every `ForwardMsg.new_report` message. This lets us remove the state from the server that stores whether a given client has already received the initialize message. This flag had a race condition - it was possible for the flag to be set to true, but the client not to actually have received the message, if a script used the `st.experimental_rerun` function towards the top of a script. - (Python) `report_session. _maybe_enqueue_initialize_message` (and its associated flag) is gone. - (frontend) `App.handleNewReport` now conditionally calls through to a new `App.handleOneTimeInitialization` function for the first new report message. There's logic that detects and errors if the initialize data unexpectedly changes from message to message. - (frontend) Several new `App.test.tsx` tests that make sure initialization/newReport handling is correct. Fixes #2226 --- frontend/src/App.test.tsx | 117 +++++++++++++++++---- frontend/src/App.tsx | 107 ++++++++----------- frontend/src/lib/SessionEventDispatcher.ts | 2 +- frontend/src/lib/SessionInfo.test.ts | 40 +++++++ frontend/src/lib/SessionInfo.ts | 41 ++++++-- lib/streamlit/report_session.py | 74 +++++-------- lib/tests/streamlit/report_queue_test.py | 36 +++---- lib/tests/streamlit/report_test.py | 12 +-- proto/streamlit/proto/ForwardMsg.proto | 2 - proto/streamlit/proto/Initialize.proto | 68 ------------ proto/streamlit/proto/NewReport.proto | 72 ++++++++++++- 11 files changed, 333 insertions(+), 238 deletions(-) delete mode 100644 proto/streamlit/proto/Initialize.proto diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index b2c55495dd53..c00f1a25d012 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -17,7 +17,7 @@ import React from "react" import { shallow, mount, ReactWrapper } from "enzyme" -import { ForwardMsg } from "autogen/proto" +import { ForwardMsg, NewReport } from "autogen/proto" import { IMenuItem } from "hocs/withS4ACommunication/types" import { MetricsManager } from "./lib/MetricsManager" import { getMetricsManagerForTest } from "./lib/MetricsManagerTestUtils" @@ -28,17 +28,10 @@ import MainMenu from "./components/core/MainMenu" const getProps = (extend?: Partial): Props => ({ screenCast: { + currentState: "OFF", toggleRecordAudio: jest.fn(), startRecording: jest.fn(), stopRecording: jest.fn(), - fileName: "", - recording: false, - recordAudio: false, - countdown: -1, - startAnimation: false, - showRecordedDialog: false, - showScreencastDialog: false, - showUnsupportedDialog: false, }, s4aCommunication: { connect: jest.fn(), @@ -84,7 +77,8 @@ describe("App", () => { }) afterEach(() => { - SessionInfo.singleton = undefined + const UnsafeSessionInfo = SessionInfo as any + UnsafeSessionInfo.singleton = undefined }) it("renders without crashing", () => { @@ -93,7 +87,7 @@ describe("App", () => { expect(wrapper.html()).not.toBeNull() }) - it("should reload when streamlit server version changes", () => { + it("reloads when streamlit server version changes", () => { const props = getProps() const wrapper = shallow() @@ -101,14 +95,16 @@ describe("App", () => { const fwMessage = new ForwardMsg() - fwMessage.initialize = { - environmentInfo: { - streamlitVersion: "svv", + fwMessage.newReport = { + initialize: { + environmentInfo: { + streamlitVersion: "svv", + }, + sessionId: "sessionId", + userInfo: {}, + config: {}, + sessionState: {}, }, - sessionId: "sessionId", - userInfo: {}, - config: {}, - sessionState: {}, } // @ts-ignore @@ -117,7 +113,7 @@ describe("App", () => { expect(window.location.reload).toHaveBeenCalled() }) - it("should start screencast recording when the MainMenu is clicked", () => { + it("starts screencast recording when the MainMenu is clicked", () => { const props = getProps() const wrapper = shallow() @@ -135,7 +131,7 @@ describe("App", () => { ) }) - it("should stop screencast when esc is pressed", () => { + it("stops screencast when esc is pressed", () => { const props = getProps() const wrapper = shallow() @@ -145,7 +141,7 @@ describe("App", () => { expect(props.screenCast.stopRecording).toBeCalled() }) - it("should show s4aMenuItems", () => { + it("shows s4aMenuItems", () => { const props = getProps({ s4aCommunication: { connect: jest.fn(), @@ -167,3 +163,82 @@ describe("App", () => { ]) }) }) + +describe("App.handleNewReport", () => { + const NEW_REPORT = new NewReport({ + initialize: { + userInfo: { + installationId: "installationId", + installationIdV1: "installationIdV1", + installationIdV2: "installationIdV2", + email: "email", + }, + config: { + sharingEnabled: false, + gatherUsageStats: false, + maxCachedMessageAge: 0, + mapboxToken: "mapboxToken", + allowRunOnSave: false, + }, + environmentInfo: { + streamlitVersion: "streamlitVersion", + pythonVersion: "pythonVersion", + }, + sessionState: { + runOnSave: false, + reportIsRunning: false, + }, + sessionId: "sessionId", + commandLine: "commandLine", + }, + }) + + afterEach(() => { + const UnsafeSessionInfo = SessionInfo as any + UnsafeSessionInfo.singleton = undefined + }) + + it("performs one-time initialization", () => { + const wrapper = shallow() + const app = wrapper.instance() + + const oneTimeInitialization = jest.spyOn( + app, + // @ts-ignore + "handleOneTimeInitialization" + ) + + expect(SessionInfo.isSet()).toBe(false) + + // @ts-ignore + app.handleNewReport(NEW_REPORT) + + expect(oneTimeInitialization).toHaveBeenCalledTimes(1) + expect(SessionInfo.isSet()).toBe(true) + }) + + it("performs one-time initialization only once", () => { + const wrapper = shallow() + const app = wrapper.instance() + + const oneTimeInitialization = jest.spyOn( + app, + // @ts-ignore + "handleOneTimeInitialization" + ) + + expect(SessionInfo.isSet()).toBe(false) + + // @ts-ignore + app.handleNewReport(NEW_REPORT) + // @ts-ignore + app.handleNewReport(NEW_REPORT) + // @ts-ignore + app.handleNewReport(NEW_REPORT) + + // Multiple NEW_REPORT messages should not result in one-time + // initialization being performed more than once. + expect(oneTimeInitialization).toHaveBeenCalledTimes(1) + expect(SessionInfo.isSet()).toBe(true) + }) +}) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d3a12e604ae8..9146c33e2bef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -54,6 +54,7 @@ import { SessionEvent, WidgetStates, SessionState, + Config, } from "autogen/proto" import { RERUN_PROMPT_MODAL_DIALOG } from "lib/baseconsts" @@ -287,14 +288,12 @@ export class App extends PureComponent { try { dispatchProto(msgProto, "type", { - initialize: (initializeMsg: Initialize) => - this.handleInitialize(initializeMsg), + newReport: (newReportMsg: NewReport) => + this.handleNewReport(newReportMsg), sessionStateChanged: (msg: SessionState) => this.handleSessionStateChanged(msg), sessionEvent: (evtMsg: SessionEvent) => this.handleSessionEvent(evtMsg), - newReport: (newReportMsg: NewReport) => - this.handleNewReport(newReportMsg), delta: (deltaMsg: Delta) => this.handleDeltaMsg( deltaMsg, @@ -378,64 +377,6 @@ export class App extends PureComponent { }) } - /** - * Handler for ForwardMsg.initialize messages - * @param initializeMsg an Initialize protobuf - */ - handleInitialize = (initializeMsg: Initialize): void => { - const { - sessionId, - environmentInfo, - userInfo, - config, - sessionState, - } = initializeMsg - - if ( - sessionId == null || - !environmentInfo || - !userInfo || - !config || - !sessionState - ) { - throw new Error("InitializeMsg is missing a required field") - } - - if (App.hasStreamlitVersionChanged(initializeMsg)) { - window.location.reload() - return - } - - SessionInfo.current = new SessionInfo({ - sessionId, - streamlitVersion: environmentInfo.streamlitVersion, - pythonVersion: environmentInfo.pythonVersion, - installationId: userInfo.installationId, - installationIdV1: userInfo.installationIdV1, - installationIdV2: userInfo.installationIdV2, - authorEmail: userInfo.email, - maxCachedMessageAge: config.maxCachedMessageAge, - commandLine: initializeMsg.commandLine, - userMapboxToken: config.mapboxToken, - }) - - MetricsManager.current.initialize({ - gatherUsageStats: Boolean(config.gatherUsageStats), - }) - - MetricsManager.current.enqueue("createReport", { - pythonVersion: SessionInfo.current.pythonVersion, - }) - - this.setState({ - sharingEnabled: Boolean(config.sharingEnabled), - allowRunOnSave: Boolean(config.allowRunOnSave), - }) - - this.props.s4aCommunication.connect() - this.handleSessionStateChanged(sessionState) - } - /** * Handler for ForwardMsg.sessionStateChanged messages * @param stateChangeProto a SessionState protobuf @@ -530,9 +471,24 @@ export class App extends PureComponent { * @param newReportProto a NewReport protobuf */ handleNewReport = (newReportProto: NewReport): void => { + const initialize = newReportProto.initialize as Initialize + + if (App.hasStreamlitVersionChanged(initialize)) { + window.location.reload() + return + } + + // First, handle initialization logic. Each NewReport message has + // initialization data. If this is the _first_ time we're receiving + // the NewReport message, we perform some one-time initialization. + if (!SessionInfo.isSet()) { + // We're not initialized. Perform one-time initialization. + this.handleOneTimeInitialization(initialize) + } + const { reportHash } = this.state const { - id: reportId, + reportId, name: reportName, scriptPath, deployParams, @@ -561,6 +517,31 @@ export class App extends PureComponent { } } + /** + * Performs one-time initialization. This is called from `handleNewReport`. + */ + handleOneTimeInitialization = (initialize: Initialize): void => { + SessionInfo.current = SessionInfo.fromInitializeMessage(initialize) + + const config = initialize.config as Config + + MetricsManager.current.initialize({ + gatherUsageStats: config.gatherUsageStats, + }) + + MetricsManager.current.enqueue("createReport", { + pythonVersion: SessionInfo.current.pythonVersion, + }) + + this.setState({ + sharingEnabled: config.sharingEnabled, + allowRunOnSave: config.allowRunOnSave, + }) + + this.props.s4aCommunication.connect() + this.handleSessionStateChanged(initialize.sessionState) + } + /** * Handler for ForwardMsg.reportFinished messages * @param status the ReportFinishedStatus that the report finished with diff --git a/frontend/src/lib/SessionEventDispatcher.ts b/frontend/src/lib/SessionEventDispatcher.ts index 5edf098ee5f5..516abde1e945 100644 --- a/frontend/src/lib/SessionEventDispatcher.ts +++ b/frontend/src/lib/SessionEventDispatcher.ts @@ -16,7 +16,7 @@ */ import { Signal } from "typed-signals" -import { SessionEvent } from "../autogen/proto" +import { SessionEvent } from "autogen/proto" /** Redispatches SessionEvent messages received from the server. */ export class SessionEventDispatcher { diff --git a/frontend/src/lib/SessionInfo.test.ts b/frontend/src/lib/SessionInfo.test.ts index 0aba31cd1293..4f304808032e 100644 --- a/frontend/src/lib/SessionInfo.test.ts +++ b/frontend/src/lib/SessionInfo.test.ts @@ -16,7 +16,47 @@ */ import { SessionInfo } from "lib/SessionInfo" +import { Initialize } from "autogen/proto" test("Throws an error when used before initialization", () => { expect(() => SessionInfo.current).toThrow() }) + +test("Can be initialized from a protobuf", () => { + const MESSAGE = new Initialize({ + userInfo: { + installationId: "installationId", + installationIdV1: "installationIdV1", + installationIdV2: "installationIdV2", + email: "email", + }, + config: { + sharingEnabled: false, + gatherUsageStats: false, + maxCachedMessageAge: 31, + mapboxToken: "mapboxToken", + allowRunOnSave: false, + }, + environmentInfo: { + streamlitVersion: "streamlitVersion", + pythonVersion: "pythonVersion", + }, + sessionState: { + runOnSave: false, + reportIsRunning: false, + }, + sessionId: "sessionId", + commandLine: "commandLine", + }) + + const si = SessionInfo.fromInitializeMessage(MESSAGE) + expect(si.sessionId).toEqual("sessionId") + expect(si.streamlitVersion).toEqual("streamlitVersion") + expect(si.pythonVersion).toEqual("pythonVersion") + expect(si.installationId).toEqual("installationId") + expect(si.installationIdV1).toEqual("installationIdV1") + expect(si.installationIdV2).toEqual("installationIdV2") + expect(si.authorEmail).toEqual("email") + expect(si.maxCachedMessageAge).toEqual(31) + expect(si.commandLine).toEqual("commandLine") +}) diff --git a/frontend/src/lib/SessionInfo.ts b/frontend/src/lib/SessionInfo.ts index 1e683421c88e..b9ff184e0cbb 100644 --- a/frontend/src/lib/SessionInfo.ts +++ b/frontend/src/lib/SessionInfo.ts @@ -14,18 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Config, EnvironmentInfo, Initialize, UserInfo } from "autogen/proto" export interface Args { sessionId: string - streamlitVersion?: string | null - pythonVersion?: string | null - installationId?: string | null - installationIdV1?: string | null - installationIdV2?: string | null - authorEmail?: string | null - maxCachedMessageAge?: number | null - commandLine?: string | null - userMapboxToken?: string | null + streamlitVersion: string + pythonVersion: string + installationId: string + installationIdV1: string + installationIdV2: string + authorEmail: string + maxCachedMessageAge: number + commandLine: string + userMapboxToken: string } export class SessionInfo { @@ -84,7 +85,27 @@ export class SessionInfo { return this.current.commandLine === "streamlit hello" } - constructor({ + /** Create a SessionInfo from the relevant bits of an initialize message. */ + public static fromInitializeMessage(initialize: Initialize): SessionInfo { + const environmentInfo = initialize.environmentInfo as EnvironmentInfo + const userInfo = initialize.userInfo as UserInfo + const config = initialize.config as Config + + return new SessionInfo({ + sessionId: initialize.sessionId, + streamlitVersion: environmentInfo.streamlitVersion, + pythonVersion: environmentInfo.pythonVersion, + installationId: userInfo.installationId, + installationIdV1: userInfo.installationIdV1, + installationIdV2: userInfo.installationIdV2, + authorEmail: userInfo.email, + maxCachedMessageAge: config.maxCachedMessageAge, + commandLine: initialize.commandLine, + userMapboxToken: config.mapboxToken, + }) + } + + public constructor({ sessionId, streamlitVersion, pythonVersion, diff --git a/lib/streamlit/report_session.py b/lib/streamlit/report_session.py index 5fb1037b5da7..75fdf9112260 100644 --- a/lib/streamlit/report_session.py +++ b/lib/streamlit/report_session.py @@ -96,7 +96,6 @@ def __init__(self, ioloop, script_path, command_line, uploaded_file_manager): self._local_sources_watcher = LocalSourcesWatcher( self._report, self._on_source_file_changed ) - self._sent_initialize_message = False self._storage = None self._maybe_reuse_previous_run = False self._run_on_save = config.get_option("server.runOnSave") @@ -264,7 +263,6 @@ def _on_scriptrunner_event(self, event, exception=None, client_state=None): self._ioloop.spawn_callback(self._save_running_report) self._clear_queue() - self._maybe_enqueue_initialize_message() self._enqueue_new_report_message() elif ( @@ -348,15 +346,39 @@ def _enqueue_file_change_message(self): msg.session_event.report_changed_on_disk = True self.enqueue(msg) - def _maybe_enqueue_initialize_message(self): - if self._sent_initialize_message: - return + def get_deploy_params(self): + try: + from streamlit.git_util import GitRepo - self._sent_initialize_message = True + self._repo = GitRepo(self._report.script_path) + return self._repo.get_repo_info() + except: + # Issues can arise based on the git structure + # (e.g. if branch is in DETACHED HEAD state, + # git is not installed, etc) + # In this case, catch any errors + return None + def _enqueue_new_report_message(self): + self._report.generate_new_id() msg = ForwardMsg() - imsg = msg.initialize + msg.new_report.report_id = self._report.report_id + msg.new_report.name = self._report.name + msg.new_report.script_path = self._report.script_path + # git deploy params + deploy_params = self.get_deploy_params() + if deploy_params is not None: + repo, branch, module = deploy_params + msg.new_report.deploy_params.repository = repo + msg.new_report.deploy_params.branch = branch + msg.new_report.deploy_params.module = module + + # Immutable session data. We send this every time a new report is + # started, to avoid having to track whether the client has already + # received it. It does not change from run to run; it's up to the + # to perform one-time initialization only once. + imsg = msg.new_report.initialize imsg.config.sharing_enabled = config.get_option("global.sharingMode") != "off" imsg.config.gather_usage_stats = config.get_option("browser.gatherUsageStats") @@ -369,16 +391,6 @@ def _maybe_enqueue_initialize_message(self): imsg.config.allow_run_on_save = config.get_option("server.allowRunOnSave") - LOGGER.debug( - "New browser connection: " - "gather_usage_stats=%s, " - "sharing_enabled=%s, " - "max_cached_message_age=%s", - imsg.config.gather_usage_stats, - imsg.config.sharing_enabled, - imsg.config.max_cached_message_age, - ) - imsg.environment_info.streamlit_version = __version__ imsg.environment_info.python_version = ".".join(map(str, sys.version_info)) @@ -400,34 +412,6 @@ def _maybe_enqueue_initialize_message(self): self.enqueue(msg) - def get_deploy_params(self): - try: - from streamlit.git_util import GitRepo - - self._repo = GitRepo(self._report.script_path) - return self._repo.get_repo_info() - except: - # Issues can arise based on the git structure - # (e.g. if branch is in DETACHED HEAD state, - # git is not installed, etc) - # In this case, catch any errors - return None - - def _enqueue_new_report_message(self): - self._report.generate_new_id() - msg = ForwardMsg() - msg.new_report.id = self._report.report_id - msg.new_report.name = self._report.name - msg.new_report.script_path = self._report.script_path - - deploy_params = self.get_deploy_params() - if deploy_params is not None: - repo, branch, module = deploy_params - msg.new_report.deploy_params.repository = repo - msg.new_report.deploy_params.branch = branch - msg.new_report.deploy_params.module = module - self.enqueue(msg) - def _enqueue_report_finished_message(self, status): """Enqueue a report_finished ForwardMsg. diff --git a/lib/tests/streamlit/report_queue_test.py b/lib/tests/streamlit/report_queue_test.py index 97ba8c4868e0..91eafdb07c63 100644 --- a/lib/tests/streamlit/report_queue_test.py +++ b/lib/tests/streamlit/report_queue_test.py @@ -27,9 +27,9 @@ # For the messages below, we don't really care about their contents so much as # their general type. -INIT_MSG = ForwardMsg() -INIT_MSG.initialize.config.sharing_enabled = True -INIT_MSG.initialize.config.allow_run_on_save = True +NEW_REPORT_MSG = ForwardMsg() +NEW_REPORT_MSG.new_report.initialize.config.sharing_enabled = True +NEW_REPORT_MSG.new_report.initialize.config.allow_run_on_save = True TEXT_DELTA_MSG1 = ForwardMsg() TEXT_DELTA_MSG1.delta.new_element.text.body = "text1" @@ -57,20 +57,20 @@ def test_simple_enqueue(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) self.assertFalse(rq.is_empty()) queue = rq.flush() self.assertTrue(rq.is_empty()) self.assertEqual(len(queue), 1) - self.assertTrue(queue[0].initialize.config.sharing_enabled) - self.assertTrue(queue[0].initialize.config.allow_run_on_save) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.allow_run_on_save) def test_enqueue_two(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path( RootContainer.MAIN, (), 0 @@ -79,7 +79,7 @@ def test_enqueue_two(self): queue = rq.flush() self.assertEqual(len(queue), 2) - self.assertTrue(queue[0].initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) self.assertEqual( make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path ) @@ -89,7 +89,7 @@ def test_enqueue_three(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path( RootContainer.MAIN, (), 0 @@ -103,7 +103,7 @@ def test_enqueue_three(self): queue = rq.flush() self.assertEqual(len(queue), 3) - self.assertTrue(queue[0].initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) self.assertEqual( make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path ) @@ -117,7 +117,7 @@ def test_replace_element(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path( RootContainer.MAIN, (), 0 @@ -131,7 +131,7 @@ def test_replace_element(self): queue = rq.flush() self.assertEqual(len(queue), 2) - self.assertTrue(queue[0].initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) self.assertEqual( make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path ) @@ -141,7 +141,7 @@ def test_simple_add_rows(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path( RootContainer.MAIN, (), 0 @@ -156,7 +156,7 @@ def test_simple_add_rows(self): queue = rq.flush() self.assertEqual(len(queue), 3) - self.assertTrue(queue[0].initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) self.assertEqual( make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path ) @@ -173,7 +173,7 @@ def test_add_rows_rerun(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) # Simulate rerun for i in range(2): @@ -194,7 +194,7 @@ def test_add_rows_rerun(self): queue = rq.flush() self.assertEqual(len(queue), 3) - self.assertTrue(queue[0].initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) self.assertEqual( make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path ) @@ -212,7 +212,7 @@ def test_multiple_containers(self): rq = ReportQueue() self.assertTrue(rq.is_empty()) - rq.enqueue(INIT_MSG) + rq.enqueue(NEW_REPORT_MSG) def enqueue_deltas(container: RootContainer, path: Tuple[int, ...]): # We deep-copy the protos because we mutate each one @@ -248,7 +248,7 @@ def assert_deltas(container: RootContainer, path: Tuple[int, ...], idx: int): queue = rq.flush() self.assertEqual(5, len(queue)) - self.assertTrue(queue[0].initialize.config.sharing_enabled) + self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled) assert_deltas(RootContainer.MAIN, (), 1) assert_deltas(RootContainer.SIDEBAR, (0, 0, 1), 3) diff --git a/lib/tests/streamlit/report_test.py b/lib/tests/streamlit/report_test.py index 0bd9197726a4..898722523f38 100644 --- a/lib/tests/streamlit/report_test.py +++ b/lib/tests/streamlit/report_test.py @@ -27,9 +27,9 @@ from streamlit.proto.StaticManifest_pb2 import StaticManifest from tests import testutil -INIT_MSG = ForwardMsg() -INIT_MSG.initialize.config.sharing_enabled = True -INIT_MSG.metadata.delta_path[:] = make_delta_path(RootContainer.MAIN, (), 0) +NEW_REPORT_MSG = ForwardMsg() +NEW_REPORT_MSG.new_report.initialize.config.sharing_enabled = True +NEW_REPORT_MSG.metadata.delta_path[:] = make_delta_path(RootContainer.MAIN, (), 0) TEXT_DELTA_MSG = ForwardMsg() TEXT_DELTA_MSG.delta.new_element.text.body = "text1" @@ -56,7 +56,7 @@ def _parse_msg(msg_string): class ReportTest(unittest.TestCase): def test_serialize_final_report(self): report = Report("/not/a/script.py", "") - _enqueue(report, INIT_MSG) + _enqueue(report, NEW_REPORT_MSG) _enqueue(report, TEXT_DELTA_MSG) _enqueue(report, EMPTY_DELTA_MSG) @@ -65,7 +65,7 @@ def test_serialize_final_report(self): # Validate our messages. messages = [_parse_msg(msg_string) for _, msg_string in files[:-1]] self.assertEqual(3, len(messages)) - self.assertEqual("initialize", messages[0].WhichOneof("type")) + self.assertEqual("new_report", messages[0].WhichOneof("type")) self.assertEqual("text1", messages[1].delta.new_element.text.body) self.assertEqual("empty", messages[2].delta.new_element.WhichOneof("type")) @@ -82,7 +82,7 @@ def test_serialize_final_report(self): def test_serialize_running_report(self): report = Report("/not/a/script.py", "") - _enqueue(report, INIT_MSG) + _enqueue(report, NEW_REPORT_MSG) _enqueue(report, EMPTY_DELTA_MSG) _enqueue(report, TEXT_DELTA_MSG) _enqueue(report, EMPTY_DELTA_MSG) diff --git a/proto/streamlit/proto/ForwardMsg.proto b/proto/streamlit/proto/ForwardMsg.proto index 6e607cac94f5..76e0381a431a 100644 --- a/proto/streamlit/proto/ForwardMsg.proto +++ b/proto/streamlit/proto/ForwardMsg.proto @@ -17,7 +17,6 @@ syntax = "proto3"; import "streamlit/proto/Delta.proto"; -import "streamlit/proto/Initialize.proto"; import "streamlit/proto/NewReport.proto"; import "streamlit/proto/PageConfig.proto"; import "streamlit/proto/PageInfo.proto"; @@ -45,7 +44,6 @@ message ForwardMsg { oneof type { // Report lifecycle messages. - Initialize initialize = 3; NewReport new_report = 4; Delta delta = 5; PageInfo page_info_changed = 12; diff --git a/proto/streamlit/proto/Initialize.proto b/proto/streamlit/proto/Initialize.proto deleted file mode 100644 index d136599f1e8a..000000000000 --- a/proto/streamlit/proto/Initialize.proto +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright 2018-2020 Streamlit Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -syntax = "proto3"; - -import "streamlit/proto/SessionState.proto"; - -// This is the first message that is sent when the connection is opened. -// It contains streamlit configuration data, and the session state -// that existed at the time of connection. -message Initialize { - UserInfo user_info = 1; - Config config = 2; - EnvironmentInfo environment_info = 3; - - // The session state at the time the connection was established - SessionState session_state = 4; - - // the actual command line as a string - string command_line = 5; - - // The ReportSession.id for this connection's ReportSession. - // This is used to associate uploaded files with the client that uploaded - // them. - string session_id = 6; -} - -message Config { - // True if saving is properly configured. - bool sharing_enabled = 1; - - // See config option "browser.gatherUsageStats". - bool gather_usage_stats = 2; - - // See config option "global.maxCachedMessageAge". - int32 max_cached_message_age = 3; - - // See config option "mapbox.token". - string mapbox_token = 4; - - // See config option "server.allowRunOnSave" - bool allow_run_on_save = 5; -} - -message UserInfo { - string installation_id = 1; - string installation_id_v1 = 3; - string installation_id_v2 = 4; - string email = 2; -} - -message EnvironmentInfo { - string streamlit_version = 1; - string python_version = 2; -} diff --git a/proto/streamlit/proto/NewReport.proto b/proto/streamlit/proto/NewReport.proto index 61695a4a9ddb..2258148fc03b 100644 --- a/proto/streamlit/proto/NewReport.proto +++ b/proto/streamlit/proto/NewReport.proto @@ -16,21 +16,85 @@ syntax = "proto3"; -// This is the first message that is sent when a new report is run +import "streamlit/proto/SessionState.proto"; + +// This is the first message that is sent when a new report is run. message NewReport { + // Initialization data. This data does *not* change from rerun to rerun, + // but we send it every time so that we don't have to track whether the + // client has already received it. The client is responsible for + // performing initialization logic only once. + Initialize initialize = 1; + // The report ID - string id = 1; + string report_id = 2; // The basename of the script that launched this report. Example: "foo.py" - string name = 2; + string name = 3; // The full path of the script that launched this report. Example: // "/foo/bar/foo.py" - string script_path = 3; + string script_path = 4; DeployParams deploy_params = 5; } +// Contains Streamlit configuration data, and the session state that existed +// at the time the user connected. +message Initialize { + UserInfo user_info = 1; + Config config = 2; + EnvironmentInfo environment_info = 3; + + // The session state at the time the connection was established + SessionState session_state = 4; + + // the actual command line as a string + string command_line = 5; + + // The ReportSession.id for this connection's ReportSession. + // This is used to associate uploaded files with the client that uploaded + // them. + string session_id = 6; +} + +// App configuration options, initialized mainly from the +// .streamlit/config.toml file. Does not change over the app's lifetime. +message Config { + // True if saving is properly configured. + bool sharing_enabled = 1; + + // See config option "browser.gatherUsageStats". + bool gather_usage_stats = 2; + + // See config option "global.maxCachedMessageAge". + int32 max_cached_message_age = 3; + + // See config option "mapbox.token". + string mapbox_token = 4; + + // See config option "server.allowRunOnSave" + bool allow_run_on_save = 5; +} + +// Data that identifies the Streamlit app creator. +// Does not change over the app's lifetime. +message UserInfo { + string installation_id = 1; + string installation_id_v1 = 3; + string installation_id_v2 = 4; + string email = 2; +} + +// Data that identifies the Streamlit app's environment. +// Does not change over the app lifetime. +message EnvironmentInfo { + string streamlit_version = 1; + string python_version = 2; +} + +// Data about the git repository this app is deployed from (if any). +// Does not change over the app's lifetime. message DeployParams { string repository = 1; string branch = 2;