123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- import os
- import typing
- from collections.abc import Mapping, Sequence
- from functools import cached_property
- from pathlib import Path
- from typing import Any, ClassVar, List, Literal, NoReturn # noqa: UP035
- import click
- import pytest
- from packaging import version
- from reflex import constants
- from reflex.config import environment
- from reflex.event import EventHandler
- from reflex.state import BaseState
- from reflex.utils import build, prerequisites, types
- from reflex.utils import exec as utils_exec
- from reflex.utils.exceptions import ReflexError, SystemPackageMissingError
- from reflex.vars.base import Var
- class ExampleTestState(BaseState):
- """Test state class."""
- def test_event_handler(self):
- """Test event handler."""
- pass
- def test_func():
- pass
- @pytest.mark.parametrize(
- "cls,expected",
- [
- (str, False),
- (int, False),
- (float, False),
- (bool, False),
- (List, True), # noqa: UP006
- (list[int], True),
- ],
- )
- def test_is_generic_alias(cls: type, expected: bool):
- """Test checking if a class is a GenericAlias.
- Args:
- cls: The class to check.
- expected: Whether the class is a GenericAlias.
- """
- assert types.is_generic_alias(cls) == expected
- @pytest.mark.parametrize(
- ("subclass", "superclass", "expected"),
- [
- *[
- (base_type, base_type, True)
- for base_type in [int, float, str, bool, list, dict]
- ],
- *[
- (one_type, another_type, False)
- for one_type in [int, float, str, list, dict]
- for another_type in [int, float, str, list, dict]
- if one_type != another_type
- ],
- (bool, int, True),
- (int, bool, False),
- (list, list, True),
- (list, list[str], True), # this is wrong, but it's a limitation of the function
- (list, list, True),
- (list[int], list, True),
- (list[int], list, True),
- (list[int], list[str], False),
- (list[int], list[int], True),
- (list[int], list[float], False),
- (list[int], list[int | float], True),
- (list[int], list[float | str], False),
- (int | float, list[int | float], False),
- (int | float, int | float | str, True),
- (int | float, str | float, False),
- (dict[str, int], dict[str, int], True),
- (dict[str, bool], dict[str, int], True),
- (dict[str, int], dict[str, bool], False),
- (dict[str, Any], dict[str, str], False),
- (dict[str, str], dict[str, str], True),
- (dict[str, str], dict[str, Any], True),
- (dict[str, Any], dict[str, Any], True),
- (Mapping[str, int], dict[str, int], False),
- (Sequence[int], list[int], False),
- (Sequence[int] | list[int], list[int], False),
- (str, Literal["test", "value"], True),
- (str, Literal["test", "value", 2, 3], True),
- (int, Literal["test", "value"], False),
- (int, Literal["test", "value", 2, 3], True),
- (Literal["test", "value"], str, True),
- (Literal["test", "value", 2, 3], str, False),
- (Literal["test", "value"], int, False),
- (Literal["test", "value", 2, 3], int, False),
- *[
- (NoReturn, super_class, True)
- for super_class in [int, float, str, bool, list, dict, object, Any]
- ],
- *[
- (list[NoReturn], list[super_class], True)
- for super_class in [int, float, str, bool, list, dict, object, Any]
- ],
- ],
- )
- def test_typehint_issubclass(subclass, superclass, expected):
- assert types.typehint_issubclass(subclass, superclass) == expected
- @pytest.mark.parametrize(
- ("subclass", "superclass", "expected"),
- [
- *[
- (base_type, base_type, True)
- for base_type in [int, float, str, bool, list, dict]
- ],
- *[
- (one_type, another_type, False)
- for one_type in [int, float, str, list, dict]
- for another_type in [int, float, str, list, dict]
- if one_type != another_type
- ],
- (bool, int, True),
- (int, bool, False),
- (list, list, True),
- (list, list[str], True), # this is wrong, but it's a limitation of the function
- (list, list, True),
- (list[int], list, True),
- (list[int], list, True),
- (list[int], list[str], False),
- (list[int], list[int], True),
- (list[int], list[float], False),
- (list[int], list[int | float], True),
- (list[int], list[float | str], False),
- (int | float, list[int | float], False),
- (int | float, int | float | str, True),
- (int | float, str | float, False),
- (dict[str, int], dict[str, int], True),
- (dict[str, bool], dict[str, int], True),
- (dict[str, int], dict[str, bool], False),
- (dict[str, Any], dict[str, str], False),
- (dict[str, str], dict[str, str], True),
- (dict[str, str], dict[str, Any], True),
- (dict[str, Any], dict[str, Any], True),
- (Mapping[str, int], dict[str, int], True),
- (Sequence[int], list[int], True),
- (Sequence[int] | list[int], list[int], True),
- (str, Literal["test", "value"], True),
- (str, Literal["test", "value", 2, 3], True),
- (int, Literal["test", "value"], False),
- (int, Literal["test", "value", 2, 3], True),
- *[
- (NoReturn, super_class, True)
- for super_class in [int, float, str, bool, list, dict, object, Any]
- ],
- *[
- (list[NoReturn], list[super_class], True)
- for super_class in [int, float, str, bool, list, dict, object, Any]
- ],
- ],
- )
- def test_typehint_issubclass_mutable_as_immutable(subclass, superclass, expected):
- assert (
- types.typehint_issubclass(
- subclass, superclass, treat_mutable_superclasss_as_immutable=True
- )
- == expected
- )
- def test_validate_none_bun_path(mocker):
- """Test that an error is thrown when a bun path is not specified.
- Args:
- mocker: Pytest mocker object.
- """
- mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=None)
- # with pytest.raises(click.exceptions.Exit):
- prerequisites.validate_bun()
- def test_validate_invalid_bun_path(
- mocker,
- ):
- """Test that an error is thrown when a custom specified bun path is not valid
- or does not exist.
- Args:
- mocker: Pytest mocker object.
- """
- mock_path = mocker.Mock()
- mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path)
- mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
- mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None)
- with pytest.raises(click.exceptions.Exit):
- prerequisites.validate_bun()
- def test_validate_bun_path_incompatible_version(mocker):
- """Test that an error is thrown when the bun version does not meet minimum requirements.
- Args:
- mocker: Pytest mocker object.
- """
- mock_path = mocker.Mock()
- mock_path.samefile.return_value = False
- mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path)
- mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
- mocker.patch(
- "reflex.utils.prerequisites.get_bun_version",
- return_value=version.parse("0.6.5"),
- )
- # This will just warn the user, not raise an error
- prerequisites.validate_bun()
- def test_remove_existing_bun_installation(mocker):
- """Test that existing bun installation is removed.
- Args:
- mocker: Pytest mocker.
- """
- mocker.patch("reflex.utils.prerequisites.Path.exists", return_value=True)
- rm = mocker.patch("reflex.utils.prerequisites.path_ops.rm", mocker.Mock())
- prerequisites.remove_existing_bun_installation()
- rm.assert_called_once()
- def test_setup_frontend(tmp_path, mocker):
- """Test checking if assets content have been
- copied into the .web/public folder.
- Args:
- tmp_path: root path of test case data directory
- mocker: mocker object to allow mocking
- """
- web_public_folder = tmp_path / ".web" / "public"
- assets = tmp_path / "assets"
- assets.mkdir()
- (assets / "favicon.ico").touch()
- mocker.patch("reflex.utils.prerequisites.install_frontend_packages")
- mocker.patch("reflex.utils.build.set_env_json")
- build.setup_frontend(tmp_path, disable_telemetry=False)
- assert web_public_folder.exists()
- assert (web_public_folder / "favicon.ico").exists()
- @pytest.fixture
- def test_backend_variable_cls():
- class TestBackendVariable(BaseState):
- """Test backend variable."""
- _classvar: ClassVar[int] = 0
- _hidden: int = 0
- not_hidden: int = 0
- __dunderattr__: int = 0
- @classmethod
- def _class_method(cls):
- pass
- def _hidden_method(self):
- pass
- @property
- def _hidden_property(self):
- pass
- @cached_property
- def _cached_hidden_property(self):
- pass
- return TestBackendVariable
- @pytest.mark.parametrize(
- "input, output",
- [
- ("_classvar", False),
- ("_class_method", False),
- ("_hidden_method", False),
- ("_hidden", True),
- ("not_hidden", False),
- ("__dundermethod__", False),
- ("_hidden_property", False),
- ("_cached_hidden_property", False),
- ],
- )
- def test_is_backend_base_variable(
- test_backend_variable_cls: type[BaseState], input: str, output: bool
- ):
- assert types.is_backend_base_variable(input, test_backend_variable_cls) == output
- @pytest.mark.parametrize(
- "cls, cls_check, expected",
- [
- (int, int, True),
- (int, float, False),
- (int, int | float, True),
- (float, int | float, True),
- (str, int | float, False),
- (list[int], list[int], True),
- (list[int], list[float], True),
- (int | float, int | float, False),
- (int | Var[int], Var[int], False),
- (int, Any, True),
- (Any, Any, True),
- (int | float, Any, True),
- (str, Literal["test", "value"] | int, True),
- (int, Literal["test", "value"] | int, True),
- (str, Literal["test", "value"], True),
- (int, Literal["test", "value"], False),
- ],
- )
- def test_issubclass(cls: type, cls_check: type, expected: bool):
- assert types._issubclass(cls, cls_check) == expected
- @pytest.mark.parametrize("cls", [Literal["test", 1], Literal[1, "test"]])
- def test_unsupported_literals(cls: type):
- with pytest.raises(TypeError):
- types.get_base_class(cls)
- @pytest.mark.parametrize(
- "app_name,expected_config_name",
- [
- ("appname", "AppnameConfig"),
- ("app_name", "AppnameConfig"),
- ("app-name", "AppnameConfig"),
- ("appname2.io", "AppnameioConfig"),
- ],
- )
- def test_create_config(app_name: str, expected_config_name: str, mocker):
- """Test templates.RXCONFIG is formatted with correct app name and config class name.
- Args:
- app_name: App name.
- expected_config_name: Expected config name.
- mocker: Mocker object.
- """
- mocker.patch("pathlib.Path.write_text")
- tmpl_mock = mocker.patch("reflex.compiler.templates.RXCONFIG")
- prerequisites.create_config(app_name)
- tmpl_mock.render.assert_called_with(
- app_name=app_name, config_name=expected_config_name
- )
- @pytest.fixture
- def tmp_working_dir(tmp_path):
- """Create a temporary directory and chdir to it.
- After the test executes, chdir back to the original working directory.
- Args:
- tmp_path: pytest tmp_path fixture creates per-test temp dir
- Yields:
- subdirectory of tmp_path which is now the current working directory.
- """
- old_pwd = Path.cwd()
- working_dir = tmp_path / "working_dir"
- working_dir.mkdir()
- os.chdir(working_dir)
- yield working_dir
- os.chdir(old_pwd)
- def test_create_config_e2e(tmp_working_dir):
- """Create a new config file, exec it, and make assertions about the config.
- Args:
- tmp_working_dir: a new directory that is the current working directory
- for the duration of the test.
- """
- app_name = "e2e"
- prerequisites.create_config(app_name)
- eval_globals = {}
- exec((tmp_working_dir / constants.Config.FILE).read_text(), eval_globals)
- config = eval_globals["config"]
- assert config.app_name == app_name
- class DataFrame:
- """A Fake pandas DataFrame class."""
- pass
- @pytest.mark.parametrize(
- "class_type,expected",
- [
- (list, False),
- (int, False),
- (dict, False),
- (DataFrame, True),
- (typing.Any, False),
- (list, False),
- ],
- )
- def test_is_dataframe(class_type, expected):
- """Test that a type name is DataFrame.
- Args:
- class_type: the class type.
- expected: whether type name is DataFrame
- """
- assert types.is_dataframe(class_type) == expected
- @pytest.mark.parametrize("gitignore_exists", [True, False])
- def test_initialize_non_existent_gitignore(tmp_path, mocker, gitignore_exists):
- """Test that the generated .gitignore_file file on reflex init contains the correct file
- names with correct formatting.
- Args:
- tmp_path: The root test path.
- mocker: The mock object.
- gitignore_exists: Whether a gitignore file exists in the root dir.
- """
- expected = constants.GitIgnore.DEFAULTS.copy()
- mocker.patch("reflex.constants.GitIgnore.FILE", tmp_path / ".gitignore")
- gitignore_file = tmp_path / ".gitignore"
- if gitignore_exists:
- gitignore_file.touch()
- gitignore_file.write_text(
- """*.db
- __pycache__/
- """
- )
- prerequisites.initialize_gitignore(gitignore_file=gitignore_file)
- assert gitignore_file.exists()
- file_content = [
- line.strip() for line in gitignore_file.open().read().splitlines() if line
- ]
- assert set(file_content) - expected == set()
- def test_validate_app_name(tmp_path, mocker):
- """Test that an error is raised if the app name is reflex or if the name is not according to python package naming conventions.
- Args:
- tmp_path: Test working dir.
- mocker: Pytest mocker object.
- """
- reflex = tmp_path / "reflex"
- reflex.mkdir()
- mocker.patch("reflex.utils.prerequisites.os.getcwd", return_value=str(reflex))
- with pytest.raises(click.exceptions.Exit):
- prerequisites.validate_app_name()
- with pytest.raises(click.exceptions.Exit):
- prerequisites.validate_app_name(app_name="1_test")
- def test_bun_install_without_unzip(mocker):
- """Test that an error is thrown when installing bun with unzip not installed.
- Args:
- mocker: Pytest mocker object.
- """
- mocker.patch("reflex.utils.path_ops.which", return_value=None)
- mocker.patch("pathlib.Path.exists", return_value=False)
- mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
- with pytest.raises(SystemPackageMissingError):
- prerequisites.install_bun()
- @pytest.mark.parametrize("bun_version", [constants.Bun.VERSION, "1.0.0"])
- def test_bun_install_version(mocker, bun_version):
- """Test that bun is downloaded when the host version(installed by reflex)
- different from the current version set in reflex.
- Args:
- mocker: Pytest mocker object.
- bun_version: the host bun version
- """
- mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
- mocker.patch("pathlib.Path.exists", return_value=True)
- mocker.patch(
- "reflex.utils.prerequisites.get_bun_version",
- return_value=version.parse(bun_version),
- )
- mocker.patch("reflex.utils.path_ops.which")
- mock = mocker.patch("reflex.utils.prerequisites.download_and_run")
- prerequisites.install_bun()
- if bun_version == constants.Bun.VERSION:
- mock.assert_not_called()
- else:
- mock.assert_called_once()
- @pytest.mark.parametrize("is_windows", [True, False])
- def test_create_reflex_dir(mocker, is_windows):
- """Test that a reflex directory is created on initializing frontend
- dependencies.
- Args:
- mocker: Pytest mocker object.
- is_windows: Whether platform is windows.
- """
- mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", is_windows)
- mocker.patch("reflex.utils.prerequisites.processes.run_concurrently", mocker.Mock())
- mocker.patch("reflex.utils.prerequisites.initialize_web_directory", mocker.Mock())
- mocker.patch("reflex.utils.processes.run_concurrently")
- mocker.patch("reflex.utils.prerequisites.validate_bun")
- create_cmd = mocker.patch(
- "reflex.utils.prerequisites.path_ops.mkdir", mocker.Mock()
- )
- prerequisites.initialize_reflex_user_directory()
- assert create_cmd.called
- def test_output_system_info(mocker):
- """Make sure reflex does not crash dumping system info.
- Args:
- mocker: Pytest mocker object.
- This test makes no assertions about the output, other than it executes
- without crashing.
- """
- mocker.patch("reflex.utils.console._LOG_LEVEL", constants.LogLevel.DEBUG)
- utils_exec.output_system_info()
- @pytest.mark.parametrize(
- "callable", [ExampleTestState.test_event_handler, test_func, lambda x: x]
- )
- def test_style_prop_with_event_handler_value(callable):
- """Test that a type error is thrown when a style prop has a
- callable as value.
- Args:
- callable: The callable function or event handler.
- """
- import reflex as rx
- style = {
- "color": (
- EventHandler(fn=callable)
- if type(callable) is not EventHandler
- else callable
- )
- }
- with pytest.raises(ReflexError):
- rx.box(style=style)
- def test_is_prod_mode() -> None:
- """Test that the prod mode is correctly determined."""
- environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
- assert utils_exec.is_prod_mode()
- environment.REFLEX_ENV_MODE.set(None)
- assert not utils_exec.is_prod_mode()
|