test_app.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. import io
  2. import os.path
  3. from typing import List, Tuple, Type
  4. import pytest
  5. from fastapi import UploadFile
  6. from pynecone.app import App, DefaultState, upload
  7. from pynecone.components import Box
  8. from pynecone.event import Event
  9. from pynecone.middleware import HydrateMiddleware
  10. from pynecone.state import State, StateUpdate
  11. from pynecone.style import Style
  12. @pytest.fixture
  13. def app() -> App:
  14. """A base app.
  15. Returns:
  16. The app.
  17. """
  18. return App()
  19. @pytest.fixture
  20. def index_page():
  21. """An index page.
  22. Returns:
  23. The index page.
  24. """
  25. def index():
  26. return Box.create("Index")
  27. return index
  28. @pytest.fixture
  29. def about_page():
  30. """An about page.
  31. Returns:
  32. The about page.
  33. """
  34. def about():
  35. return Box.create("About")
  36. return about
  37. @pytest.fixture()
  38. def test_state() -> Type[State]:
  39. """A default state.
  40. Returns:
  41. A default state.
  42. """
  43. class TestState(State):
  44. var: int
  45. return TestState
  46. def test_default_app(app: App):
  47. """Test creating an app with no args.
  48. Args:
  49. app: The app to test.
  50. """
  51. assert app.state() == DefaultState()
  52. assert app.middleware == [HydrateMiddleware()]
  53. assert app.style == Style()
  54. def test_add_page_default_route(app: App, index_page, about_page):
  55. """Test adding a page to an app.
  56. Args:
  57. app: The app to test.
  58. index_page: The index page.
  59. about_page: The about page.
  60. """
  61. assert app.pages == {}
  62. app.add_page(index_page)
  63. assert set(app.pages.keys()) == {"index"}
  64. app.add_page(about_page)
  65. assert set(app.pages.keys()) == {"index", "about"}
  66. def test_add_page_set_route(app: App, index_page, windows_platform: bool):
  67. """Test adding a page to an app.
  68. Args:
  69. app: The app to test.
  70. index_page: The index page.
  71. windows_platform: Whether the system is windows.
  72. """
  73. route = "test" if windows_platform else "/test"
  74. assert app.pages == {}
  75. app.add_page(index_page, route=route)
  76. assert set(app.pages.keys()) == {"test"}
  77. def test_add_page_set_route_dynamic(app: App, index_page, windows_platform: bool):
  78. """Test adding a page with dynamic route variable to an app.
  79. Args:
  80. app: The app to test.
  81. index_page: The index page.
  82. windows_platform: Whether the system is windows.
  83. """
  84. route = "/test/[dynamic]"
  85. if windows_platform:
  86. route.lstrip("/").replace("/", "\\")
  87. assert app.pages == {}
  88. app.add_page(index_page, route=route)
  89. assert set(app.pages.keys()) == {"test/[dynamic]"}
  90. assert "dynamic" in app.state.computed_vars
  91. assert app.state.computed_vars["dynamic"].deps(objclass=DefaultState) == {
  92. "router_data"
  93. }
  94. assert "router_data" in app.state().computed_var_dependencies
  95. def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
  96. """Test adding a page to an app.
  97. Args:
  98. app: The app to test.
  99. index_page: The index page.
  100. windows_platform: Whether the system is windows.
  101. """
  102. route = "test\\nested" if windows_platform else "/test/nested"
  103. assert app.pages == {}
  104. app.add_page(index_page, route=route)
  105. assert set(app.pages.keys()) == {route.strip(os.path.sep)}
  106. def test_initialize_with_state(test_state):
  107. """Test setting the state of an app.
  108. Args:
  109. test_state: The default state.
  110. """
  111. app = App(state=test_state)
  112. assert app.state == test_state
  113. # Get a state for a given token.
  114. token = "token"
  115. state = app.state_manager.get_state(token)
  116. assert isinstance(state, test_state)
  117. assert state.var == 0 # type: ignore
  118. def test_set_and_get_state(test_state):
  119. """Test setting and getting the state of an app with different tokens.
  120. Args:
  121. test_state: The default state.
  122. """
  123. app = App(state=test_state)
  124. # Create two tokens.
  125. token1 = "token1"
  126. token2 = "token2"
  127. # Get the default state for each token.
  128. state1 = app.state_manager.get_state(token1)
  129. state2 = app.state_manager.get_state(token2)
  130. assert state1.var == 0 # type: ignore
  131. assert state2.var == 0 # type: ignore
  132. # Set the vars to different values.
  133. state1.var = 1
  134. state2.var = 2
  135. app.state_manager.set_state(token1, state1)
  136. app.state_manager.set_state(token2, state2)
  137. # Get the states again and check the values.
  138. state1 = app.state_manager.get_state(token1)
  139. state2 = app.state_manager.get_state(token2)
  140. assert state1.var == 1 # type: ignore
  141. assert state2.var == 2 # type: ignore
  142. @pytest.mark.asyncio
  143. async def test_dynamic_var_event(test_state):
  144. """Test that the default handler of a dynamic generated var
  145. works as expected.
  146. Args:
  147. test_state: State Fixture.
  148. """
  149. test_state = test_state()
  150. test_state.add_var("int_val", int, 0)
  151. result = await test_state._process(
  152. Event(
  153. token="fake-token",
  154. name="test_state.set_int_val",
  155. router_data={"pathname": "/", "query": {}},
  156. payload={"value": 50},
  157. )
  158. )
  159. assert result.delta == {"test_state": {"int_val": 50}}
  160. @pytest.mark.asyncio
  161. @pytest.mark.parametrize(
  162. "event_tuples",
  163. [
  164. pytest.param(
  165. [
  166. (
  167. "test_state.make_friend",
  168. {"test_state": {"plain_friends": ["Tommy", "another-fd"]}},
  169. ),
  170. (
  171. "test_state.change_first_friend",
  172. {"test_state": {"plain_friends": ["Jenny", "another-fd"]}},
  173. ),
  174. ],
  175. id="append then __setitem__",
  176. ),
  177. pytest.param(
  178. [
  179. (
  180. "test_state.unfriend_first_friend",
  181. {"test_state": {"plain_friends": []}},
  182. ),
  183. (
  184. "test_state.make_friend",
  185. {"test_state": {"plain_friends": ["another-fd"]}},
  186. ),
  187. ],
  188. id="delitem then append",
  189. ),
  190. pytest.param(
  191. [
  192. (
  193. "test_state.make_friends_with_colleagues",
  194. {"test_state": {"plain_friends": ["Tommy", "Peter", "Jimmy"]}},
  195. ),
  196. (
  197. "test_state.remove_tommy",
  198. {"test_state": {"plain_friends": ["Peter", "Jimmy"]}},
  199. ),
  200. (
  201. "test_state.remove_last_friend",
  202. {"test_state": {"plain_friends": ["Peter"]}},
  203. ),
  204. (
  205. "test_state.unfriend_all_friends",
  206. {"test_state": {"plain_friends": []}},
  207. ),
  208. ],
  209. id="extend, remove, pop, clear",
  210. ),
  211. pytest.param(
  212. [
  213. (
  214. "test_state.add_jimmy_to_second_group",
  215. {
  216. "test_state": {
  217. "friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
  218. }
  219. },
  220. ),
  221. (
  222. "test_state.remove_first_person_from_first_group",
  223. {
  224. "test_state": {
  225. "friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
  226. }
  227. },
  228. ),
  229. (
  230. "test_state.remove_first_group",
  231. {"test_state": {"friends_in_nested_list": [["Jenny", "Jimmy"]]}},
  232. ),
  233. ],
  234. id="nested list",
  235. ),
  236. pytest.param(
  237. [
  238. (
  239. "test_state.add_jimmy_to_tommy_friends",
  240. {"test_state": {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}}},
  241. ),
  242. (
  243. "test_state.remove_jenny_from_tommy",
  244. {"test_state": {"friends_in_dict": {"Tommy": ["Jimmy"]}}},
  245. ),
  246. (
  247. "test_state.tommy_has_no_fds",
  248. {"test_state": {"friends_in_dict": {"Tommy": []}}},
  249. ),
  250. ],
  251. id="list in dict",
  252. ),
  253. ],
  254. )
  255. async def test_list_mutation_detection__plain_list(
  256. event_tuples: List[Tuple[str, List[str]]], list_mutation_state: State
  257. ):
  258. """Test list mutation detection
  259. when reassignment is not explicitly included in the logic.
  260. Args:
  261. event_tuples: From parametrization.
  262. list_mutation_state: A state with list mutation features.
  263. """
  264. for event_name, expected_delta in event_tuples:
  265. result = await list_mutation_state._process(
  266. Event(
  267. token="fake-token",
  268. name=event_name,
  269. router_data={"pathname": "/", "query": {}},
  270. payload={},
  271. )
  272. )
  273. assert result.delta == expected_delta
  274. @pytest.mark.asyncio
  275. @pytest.mark.parametrize(
  276. "event_tuples",
  277. [
  278. pytest.param(
  279. [
  280. (
  281. "test_state.add_age",
  282. {"test_state": {"details": {"name": "Tommy", "age": 20}}},
  283. ),
  284. (
  285. "test_state.change_name",
  286. {"test_state": {"details": {"name": "Jenny", "age": 20}}},
  287. ),
  288. (
  289. "test_state.remove_last_detail",
  290. {"test_state": {"details": {"name": "Jenny"}}},
  291. ),
  292. ],
  293. id="update then __setitem__",
  294. ),
  295. pytest.param(
  296. [
  297. (
  298. "test_state.clear_details",
  299. {"test_state": {"details": {}}},
  300. ),
  301. (
  302. "test_state.add_age",
  303. {"test_state": {"details": {"age": 20}}},
  304. ),
  305. ],
  306. id="delitem then update",
  307. ),
  308. pytest.param(
  309. [
  310. (
  311. "test_state.add_age",
  312. {"test_state": {"details": {"name": "Tommy", "age": 20}}},
  313. ),
  314. (
  315. "test_state.remove_name",
  316. {"test_state": {"details": {"age": 20}}},
  317. ),
  318. (
  319. "test_state.pop_out_age",
  320. {"test_state": {"details": {}}},
  321. ),
  322. ],
  323. id="add, remove, pop",
  324. ),
  325. pytest.param(
  326. [
  327. (
  328. "test_state.remove_home_address",
  329. {"test_state": {"address": [{}, {"work": "work address"}]}},
  330. ),
  331. (
  332. "test_state.add_street_to_home_address",
  333. {
  334. "test_state": {
  335. "address": [
  336. {"street": "street address"},
  337. {"work": "work address"},
  338. ]
  339. }
  340. },
  341. ),
  342. ],
  343. id="dict in list",
  344. ),
  345. pytest.param(
  346. [
  347. (
  348. "test_state.change_friend_name",
  349. {
  350. "test_state": {
  351. "friend_in_nested_dict": {
  352. "name": "Nikhil",
  353. "friend": {"name": "Tommy"},
  354. }
  355. }
  356. },
  357. ),
  358. (
  359. "test_state.add_friend_age",
  360. {
  361. "test_state": {
  362. "friend_in_nested_dict": {
  363. "name": "Nikhil",
  364. "friend": {"name": "Tommy", "age": 30},
  365. }
  366. }
  367. },
  368. ),
  369. (
  370. "test_state.remove_friend",
  371. {"test_state": {"friend_in_nested_dict": {"name": "Nikhil"}}},
  372. ),
  373. ],
  374. id="nested dict",
  375. ),
  376. ],
  377. )
  378. async def test_dict_mutation_detection__plain_list(
  379. event_tuples: List[Tuple[str, List[str]]], dict_mutation_state: State
  380. ):
  381. """Test dict mutation detection
  382. when reassignment is not explicitly included in the logic.
  383. Args:
  384. event_tuples: From parametrization.
  385. dict_mutation_state: A state with dict mutation features.
  386. """
  387. for event_name, expected_delta in event_tuples:
  388. result = await dict_mutation_state._process(
  389. Event(
  390. token="fake-token",
  391. name=event_name,
  392. router_data={"pathname": "/", "query": {}},
  393. payload={},
  394. )
  395. )
  396. assert result.delta == expected_delta
  397. @pytest.mark.asyncio
  398. @pytest.mark.parametrize(
  399. "fixture, expected",
  400. [
  401. (
  402. "upload_state",
  403. {"file_upload_state": {"img_list": ["image1.jpg", "image2.jpg"]}},
  404. ),
  405. (
  406. "upload_sub_state",
  407. {
  408. "file_state.file_upload_state": {
  409. "img_list": ["image1.jpg", "image2.jpg"]
  410. }
  411. },
  412. ),
  413. (
  414. "upload_grand_sub_state",
  415. {
  416. "base_file_state.file_sub_state.file_upload_state": {
  417. "img_list": ["image1.jpg", "image2.jpg"]
  418. }
  419. },
  420. ),
  421. ],
  422. )
  423. async def test_upload_file(fixture, request, expected):
  424. """Test that file upload works correctly.
  425. Args:
  426. fixture: The state.
  427. request: Fixture request.
  428. expected: Expected delta
  429. """
  430. data = b"This is binary data"
  431. # Create a binary IO object and write data to it
  432. bio = io.BytesIO()
  433. bio.write(data)
  434. app = App(state=request.getfixturevalue(fixture))
  435. file1 = UploadFile(
  436. filename="token:file_upload_state.multi_handle_upload:True:image1.jpg",
  437. file=bio,
  438. content_type="image/jpeg",
  439. )
  440. file2 = UploadFile(
  441. filename="token:file_upload_state.multi_handle_upload:True:image2.jpg",
  442. file=bio,
  443. content_type="image/jpeg",
  444. )
  445. fn = upload(app)
  446. result = await fn([file1, file2]) # type: ignore
  447. assert isinstance(result, StateUpdate)
  448. assert result.delta == expected
  449. @pytest.mark.asyncio
  450. @pytest.mark.parametrize(
  451. "fixture", ["upload_state", "upload_sub_state", "upload_grand_sub_state"]
  452. )
  453. async def test_upload_file_without_annotation(fixture, request):
  454. """Test that an error is thrown when there's no param annotated with pc.UploadFile or List[UploadFile].
  455. Args:
  456. fixture: The state.
  457. request: Fixture request.
  458. """
  459. data = b"This is binary data"
  460. # Create a binary IO object and write data to it
  461. bio = io.BytesIO()
  462. bio.write(data)
  463. app = App(state=request.getfixturevalue(fixture))
  464. file1 = UploadFile(
  465. filename="token:file_upload_state.handle_upload2:True:image1.jpg",
  466. file=bio,
  467. content_type="image/jpeg",
  468. )
  469. file2 = UploadFile(
  470. filename="token:file_upload_state.handle_upload2:True:image2.jpg",
  471. file=bio,
  472. content_type="image/jpeg",
  473. )
  474. fn = upload(app)
  475. with pytest.raises(ValueError) as err:
  476. await fn([file1, file2])
  477. assert (
  478. err.value.args[0]
  479. == "`file_upload_state.handle_upload2` handler should have a parameter annotated as List[pc.UploadFile]"
  480. )