test_utils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import os
  2. import typing
  3. from collections.abc import Mapping, Sequence
  4. from functools import cached_property
  5. from pathlib import Path
  6. from typing import Any, ClassVar, List, Literal, NoReturn # noqa: UP035
  7. import click
  8. import pytest
  9. from packaging import version
  10. from pytest_mock import MockerFixture
  11. from reflex import constants
  12. from reflex.config import environment
  13. from reflex.event import EventHandler
  14. from reflex.state import BaseState
  15. from reflex.utils import build, prerequisites, types
  16. from reflex.utils import exec as utils_exec
  17. from reflex.utils.exceptions import ReflexError, SystemPackageMissingError
  18. from reflex.vars.base import Var
  19. class ExampleTestState(BaseState):
  20. """Test state class."""
  21. def test_event_handler(self):
  22. """Test event handler."""
  23. pass
  24. def test_func():
  25. pass
  26. @pytest.mark.parametrize(
  27. "cls,expected",
  28. [
  29. (str, False),
  30. (int, False),
  31. (float, False),
  32. (bool, False),
  33. (List, True), # noqa: UP006
  34. (list[int], True),
  35. ],
  36. )
  37. def test_is_generic_alias(cls: type, expected: bool):
  38. """Test checking if a class is a GenericAlias.
  39. Args:
  40. cls: The class to check.
  41. expected: Whether the class is a GenericAlias.
  42. """
  43. assert types.is_generic_alias(cls) == expected
  44. @pytest.mark.parametrize(
  45. ("subclass", "superclass", "expected"),
  46. [
  47. *[
  48. (base_type, base_type, True)
  49. for base_type in [int, float, str, bool, list, dict]
  50. ],
  51. *[
  52. (one_type, another_type, False)
  53. for one_type in [int, float, str, list, dict]
  54. for another_type in [int, float, str, list, dict]
  55. if one_type != another_type
  56. ],
  57. (bool, int, True),
  58. (int, bool, False),
  59. (list, list, True),
  60. (list, list[str], True), # this is wrong, but it's a limitation of the function
  61. (list, list, True),
  62. (list[int], list, True),
  63. (list[int], list, True),
  64. (list[int], list[str], False),
  65. (list[int], list[int], True),
  66. (list[int], list[float], False),
  67. (list[int], list[int | float], True),
  68. (list[int], list[float | str], False),
  69. (int | float, list[int | float], False),
  70. (int | float, int | float | str, True),
  71. (int | float, str | float, False),
  72. (dict[str, int], dict[str, int], True),
  73. (dict[str, bool], dict[str, int], True),
  74. (dict[str, int], dict[str, bool], False),
  75. (dict[str, Any], dict[str, str], False),
  76. (dict[str, str], dict[str, str], True),
  77. (dict[str, str], dict[str, Any], True),
  78. (dict[str, Any], dict[str, Any], True),
  79. (Mapping[str, int], dict[str, int], False),
  80. (Sequence[int], list[int], False),
  81. (Sequence[int] | list[int], list[int], False),
  82. (str, Literal["test", "value"], True),
  83. (str, Literal["test", "value", 2, 3], True),
  84. (int, Literal["test", "value"], False),
  85. (int, Literal["test", "value", 2, 3], True),
  86. (Literal["test", "value"], str, True),
  87. (Literal["test", "value", 2, 3], str, False),
  88. (Literal["test", "value"], int, False),
  89. (Literal["test", "value", 2, 3], int, False),
  90. *[
  91. (NoReturn, super_class, True)
  92. for super_class in [int, float, str, bool, list, dict, object, Any]
  93. ],
  94. *[
  95. (list[NoReturn], list[super_class], True)
  96. for super_class in [int, float, str, bool, list, dict, object, Any]
  97. ],
  98. ],
  99. )
  100. def test_typehint_issubclass(subclass, superclass, expected):
  101. assert types.typehint_issubclass(subclass, superclass) == expected
  102. @pytest.mark.parametrize(
  103. ("subclass", "superclass", "expected"),
  104. [
  105. *[
  106. (base_type, base_type, True)
  107. for base_type in [int, float, str, bool, list, dict]
  108. ],
  109. *[
  110. (one_type, another_type, False)
  111. for one_type in [int, float, str, list, dict]
  112. for another_type in [int, float, str, list, dict]
  113. if one_type != another_type
  114. ],
  115. (bool, int, True),
  116. (int, bool, False),
  117. (list, list, True),
  118. (list, list[str], True), # this is wrong, but it's a limitation of the function
  119. (list, list, True),
  120. (list[int], list, True),
  121. (list[int], list, True),
  122. (list[int], list[str], False),
  123. (list[int], list[int], True),
  124. (list[int], list[float], False),
  125. (list[int], list[int | float], True),
  126. (list[int], list[float | str], False),
  127. (int | float, list[int | float], False),
  128. (int | float, int | float | str, True),
  129. (int | float, str | float, False),
  130. (dict[str, int], dict[str, int], True),
  131. (dict[str, bool], dict[str, int], True),
  132. (dict[str, int], dict[str, bool], False),
  133. (dict[str, Any], dict[str, str], False),
  134. (dict[str, str], dict[str, str], True),
  135. (dict[str, str], dict[str, Any], True),
  136. (dict[str, Any], dict[str, Any], True),
  137. (Mapping[str, int], dict[str, int], True),
  138. (Sequence[int], list[int], True),
  139. (Sequence[int] | list[int], list[int], True),
  140. (str, Literal["test", "value"], True),
  141. (str, Literal["test", "value", 2, 3], True),
  142. (int, Literal["test", "value"], False),
  143. (int, Literal["test", "value", 2, 3], True),
  144. *[
  145. (NoReturn, super_class, True)
  146. for super_class in [int, float, str, bool, list, dict, object, Any]
  147. ],
  148. *[
  149. (list[NoReturn], list[super_class], True)
  150. for super_class in [int, float, str, bool, list, dict, object, Any]
  151. ],
  152. ],
  153. )
  154. def test_typehint_issubclass_mutable_as_immutable(subclass, superclass, expected):
  155. assert (
  156. types.typehint_issubclass(
  157. subclass, superclass, treat_mutable_superclasss_as_immutable=True
  158. )
  159. == expected
  160. )
  161. def test_validate_none_bun_path(mocker: MockerFixture):
  162. """Test that an error is thrown when a bun path is not specified.
  163. Args:
  164. mocker: Pytest mocker object.
  165. """
  166. mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=None)
  167. # with pytest.raises(click.exceptions.Exit):
  168. prerequisites.validate_bun()
  169. def test_validate_invalid_bun_path(mocker: MockerFixture):
  170. """Test that an error is thrown when a custom specified bun path is not valid
  171. or does not exist.
  172. Args:
  173. mocker: Pytest mocker object.
  174. """
  175. mock_path = mocker.Mock()
  176. mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path)
  177. mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
  178. mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None)
  179. with pytest.raises(click.exceptions.Exit):
  180. prerequisites.validate_bun()
  181. def test_validate_bun_path_incompatible_version(mocker: MockerFixture):
  182. """Test that an error is thrown when the bun version does not meet minimum requirements.
  183. Args:
  184. mocker: Pytest mocker object.
  185. """
  186. mock_path = mocker.Mock()
  187. mock_path.samefile.return_value = False
  188. mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path)
  189. mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
  190. mocker.patch(
  191. "reflex.utils.prerequisites.get_bun_version",
  192. return_value=version.parse("0.6.5"),
  193. )
  194. # This will just warn the user, not raise an error
  195. prerequisites.validate_bun()
  196. def test_remove_existing_bun_installation(mocker: MockerFixture):
  197. """Test that existing bun installation is removed.
  198. Args:
  199. mocker: Pytest mocker.
  200. """
  201. mocker.patch("reflex.utils.prerequisites.Path.exists", return_value=True)
  202. rm = mocker.patch("reflex.utils.prerequisites.path_ops.rm", mocker.Mock())
  203. prerequisites.remove_existing_bun_installation()
  204. rm.assert_called_once()
  205. def test_setup_frontend(tmp_path, mocker: MockerFixture):
  206. """Test checking if assets content have been
  207. copied into the .web/public folder.
  208. Args:
  209. tmp_path: root path of test case data directory
  210. mocker: mocker object to allow mocking
  211. """
  212. web_public_folder = tmp_path / ".web" / "public"
  213. assets = tmp_path / "assets"
  214. assets.mkdir()
  215. (assets / "favicon.ico").touch()
  216. mocker.patch("reflex.utils.prerequisites.install_frontend_packages")
  217. mocker.patch("reflex.utils.build.set_env_json")
  218. build.setup_frontend(tmp_path, disable_telemetry=False)
  219. assert web_public_folder.exists()
  220. assert (web_public_folder / "favicon.ico").exists()
  221. @pytest.fixture
  222. def test_backend_variable_cls():
  223. class TestBackendVariable(BaseState):
  224. """Test backend variable."""
  225. _classvar: ClassVar[int] = 0
  226. _hidden: int = 0
  227. not_hidden: int = 0
  228. __dunderattr__: int = 0
  229. @classmethod
  230. def _class_method(cls):
  231. pass
  232. def _hidden_method(self):
  233. pass
  234. @property
  235. def _hidden_property(self):
  236. pass
  237. @cached_property
  238. def _cached_hidden_property(self):
  239. pass
  240. return TestBackendVariable
  241. @pytest.mark.parametrize(
  242. "input, output",
  243. [
  244. ("_classvar", False),
  245. ("_class_method", False),
  246. ("_hidden_method", False),
  247. ("_hidden", True),
  248. ("not_hidden", False),
  249. ("__dundermethod__", False),
  250. ("_hidden_property", False),
  251. ("_cached_hidden_property", False),
  252. ],
  253. )
  254. def test_is_backend_base_variable(
  255. test_backend_variable_cls: type[BaseState], input: str, output: bool
  256. ):
  257. assert types.is_backend_base_variable(input, test_backend_variable_cls) == output
  258. @pytest.mark.parametrize(
  259. "cls, cls_check, expected",
  260. [
  261. (int, int, True),
  262. (int, float, False),
  263. (int, int | float, True),
  264. (float, int | float, True),
  265. (str, int | float, False),
  266. (list[int], list[int], True),
  267. (list[int], list[float], True),
  268. (int | float, int | float, False),
  269. (int | Var[int], Var[int], False),
  270. (int, Any, True),
  271. (Any, Any, True),
  272. (int | float, Any, True),
  273. (str, Literal["test", "value"] | int, True),
  274. (int, Literal["test", "value"] | int, True),
  275. (str, Literal["test", "value"], True),
  276. (int, Literal["test", "value"], False),
  277. ],
  278. )
  279. def test_issubclass(cls: type, cls_check: type, expected: bool):
  280. assert types._issubclass(cls, cls_check) == expected
  281. @pytest.mark.parametrize("cls", [Literal["test", 1], Literal[1, "test"]])
  282. def test_unsupported_literals(cls: type):
  283. with pytest.raises(TypeError):
  284. types.get_base_class(cls)
  285. @pytest.mark.parametrize(
  286. "app_name,expected_config_name",
  287. [
  288. ("appname", "AppnameConfig"),
  289. ("app_name", "AppnameConfig"),
  290. ("app-name", "AppnameConfig"),
  291. ("appname2.io", "AppnameioConfig"),
  292. ],
  293. )
  294. def test_create_config(app_name: str, expected_config_name: str, mocker: MockerFixture):
  295. """Test templates.RXCONFIG is formatted with correct app name and config class name.
  296. Args:
  297. app_name: App name.
  298. expected_config_name: Expected config name.
  299. mocker: Mocker object.
  300. """
  301. mocker.patch("pathlib.Path.write_text")
  302. tmpl_mock = mocker.patch("reflex.compiler.templates.RXCONFIG")
  303. prerequisites.create_config(app_name)
  304. tmpl_mock.render.assert_called_with(
  305. app_name=app_name, config_name=expected_config_name
  306. )
  307. @pytest.fixture
  308. def tmp_working_dir(tmp_path):
  309. """Create a temporary directory and chdir to it.
  310. After the test executes, chdir back to the original working directory.
  311. Args:
  312. tmp_path: pytest tmp_path fixture creates per-test temp dir
  313. Yields:
  314. subdirectory of tmp_path which is now the current working directory.
  315. """
  316. old_pwd = Path.cwd()
  317. working_dir = tmp_path / "working_dir"
  318. working_dir.mkdir()
  319. os.chdir(working_dir)
  320. yield working_dir
  321. os.chdir(old_pwd)
  322. def test_create_config_e2e(tmp_working_dir):
  323. """Create a new config file, exec it, and make assertions about the config.
  324. Args:
  325. tmp_working_dir: a new directory that is the current working directory
  326. for the duration of the test.
  327. """
  328. app_name = "e2e"
  329. prerequisites.create_config(app_name)
  330. eval_globals = {}
  331. exec((tmp_working_dir / constants.Config.FILE).read_text(), eval_globals)
  332. config = eval_globals["config"]
  333. assert config.app_name == app_name
  334. class DataFrame:
  335. """A Fake pandas DataFrame class."""
  336. pass
  337. @pytest.mark.parametrize(
  338. "class_type,expected",
  339. [
  340. (list, False),
  341. (int, False),
  342. (dict, False),
  343. (DataFrame, True),
  344. (typing.Any, False),
  345. (list, False),
  346. ],
  347. )
  348. def test_is_dataframe(class_type, expected):
  349. """Test that a type name is DataFrame.
  350. Args:
  351. class_type: the class type.
  352. expected: whether type name is DataFrame
  353. """
  354. assert types.is_dataframe(class_type) == expected
  355. @pytest.mark.parametrize("gitignore_exists", [True, False])
  356. def test_initialize_non_existent_gitignore(
  357. tmp_path, mocker: MockerFixture, gitignore_exists
  358. ):
  359. """Test that the generated .gitignore_file file on reflex init contains the correct file
  360. names with correct formatting.
  361. Args:
  362. tmp_path: The root test path.
  363. mocker: The mock object.
  364. gitignore_exists: Whether a gitignore file exists in the root dir.
  365. """
  366. expected = constants.GitIgnore.DEFAULTS.copy()
  367. mocker.patch("reflex.constants.GitIgnore.FILE", tmp_path / ".gitignore")
  368. gitignore_file = tmp_path / ".gitignore"
  369. if gitignore_exists:
  370. gitignore_file.touch()
  371. gitignore_file.write_text(
  372. """*.db
  373. __pycache__/
  374. """
  375. )
  376. prerequisites.initialize_gitignore(gitignore_file=gitignore_file)
  377. assert gitignore_file.exists()
  378. file_content = [
  379. line.strip() for line in gitignore_file.open().read().splitlines() if line
  380. ]
  381. assert set(file_content) - expected == set()
  382. def test_validate_app_name(tmp_path, mocker: MockerFixture):
  383. """Test that an error is raised if the app name is reflex or if the name is not according to python package naming conventions.
  384. Args:
  385. tmp_path: Test working dir.
  386. mocker: Pytest mocker object.
  387. """
  388. reflex = tmp_path / "reflex"
  389. reflex.mkdir()
  390. mocker.patch("reflex.utils.prerequisites.os.getcwd", return_value=str(reflex))
  391. with pytest.raises(click.exceptions.Exit):
  392. prerequisites.validate_app_name()
  393. with pytest.raises(click.exceptions.Exit):
  394. prerequisites.validate_app_name(app_name="1_test")
  395. def test_bun_install_without_unzip(mocker: MockerFixture):
  396. """Test that an error is thrown when installing bun with unzip not installed.
  397. Args:
  398. mocker: Pytest mocker object.
  399. """
  400. mocker.patch("reflex.utils.path_ops.which", return_value=None)
  401. mocker.patch("pathlib.Path.exists", return_value=False)
  402. mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
  403. with pytest.raises(SystemPackageMissingError):
  404. prerequisites.install_bun()
  405. @pytest.mark.parametrize("bun_version", [constants.Bun.VERSION, "1.0.0"])
  406. def test_bun_install_version(mocker: MockerFixture, bun_version):
  407. """Test that bun is downloaded when the host version(installed by reflex)
  408. different from the current version set in reflex.
  409. Args:
  410. mocker: Pytest mocker object.
  411. bun_version: the host bun version
  412. """
  413. mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
  414. mocker.patch("pathlib.Path.exists", return_value=True)
  415. mocker.patch(
  416. "reflex.utils.prerequisites.get_bun_version",
  417. return_value=version.parse(bun_version),
  418. )
  419. mocker.patch("reflex.utils.path_ops.which")
  420. mock = mocker.patch("reflex.utils.prerequisites.download_and_run")
  421. prerequisites.install_bun()
  422. if bun_version == constants.Bun.VERSION:
  423. mock.assert_not_called()
  424. else:
  425. mock.assert_called_once()
  426. @pytest.mark.parametrize("is_windows", [True, False])
  427. def test_create_reflex_dir(mocker: MockerFixture, is_windows):
  428. """Test that a reflex directory is created on initializing frontend
  429. dependencies.
  430. Args:
  431. mocker: Pytest mocker object.
  432. is_windows: Whether platform is windows.
  433. """
  434. mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", is_windows)
  435. mocker.patch("reflex.utils.prerequisites.processes.run_concurrently", mocker.Mock())
  436. mocker.patch("reflex.utils.prerequisites.initialize_web_directory", mocker.Mock())
  437. mocker.patch("reflex.utils.processes.run_concurrently")
  438. mocker.patch("reflex.utils.prerequisites.validate_bun")
  439. create_cmd = mocker.patch(
  440. "reflex.utils.prerequisites.path_ops.mkdir", mocker.Mock()
  441. )
  442. prerequisites.initialize_reflex_user_directory()
  443. assert create_cmd.called
  444. def test_output_system_info(mocker: MockerFixture):
  445. """Make sure reflex does not crash dumping system info.
  446. Args:
  447. mocker: Pytest mocker object.
  448. This test makes no assertions about the output, other than it executes
  449. without crashing.
  450. """
  451. mocker.patch("reflex.utils.console._LOG_LEVEL", constants.LogLevel.DEBUG)
  452. utils_exec.output_system_info()
  453. @pytest.mark.parametrize(
  454. "callable", [ExampleTestState.test_event_handler, test_func, lambda x: x]
  455. )
  456. def test_style_prop_with_event_handler_value(callable):
  457. """Test that a type error is thrown when a style prop has a
  458. callable as value.
  459. Args:
  460. callable: The callable function or event handler.
  461. """
  462. import reflex as rx
  463. style = {
  464. "color": (
  465. EventHandler(fn=callable)
  466. if type(callable) is not EventHandler
  467. else callable
  468. )
  469. }
  470. with pytest.raises(ReflexError):
  471. rx.box(style=style)
  472. def test_is_prod_mode() -> None:
  473. """Test that the prod mode is correctly determined."""
  474. environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
  475. assert utils_exec.is_prod_mode()
  476. environment.REFLEX_ENV_MODE.set(None)
  477. assert not utils_exec.is_prod_mode()