|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import operator |
5 | 6 | from importlib.util import find_spec |
6 | 7 | from pathlib import Path |
7 | 8 | from typing import TYPE_CHECKING |
@@ -842,6 +843,189 @@ def rename(new_name: str): |
842 | 843 | rename_app(new_name, get_config().loglevel) |
843 | 844 |
|
844 | 845 |
|
| 846 | +@cli.command(name="state-tree") |
| 847 | +@loglevel_option |
| 848 | +@click.option( |
| 849 | + "--json", |
| 850 | + "output_json", |
| 851 | + is_flag=True, |
| 852 | + help="Output as JSON.", |
| 853 | +) |
| 854 | +def state_tree(output_json: bool): |
| 855 | + """Print the state tree with state_id's and event handlers with event_id's.""" |
| 856 | + from reflex.event import EVENT_ID_MARKER |
| 857 | + from reflex.state import BaseState, State, _int_to_minified_name |
| 858 | + from reflex.utils import prerequisites |
| 859 | + |
| 860 | + # Load the user's app to register all state classes |
| 861 | + prerequisites.get_app() |
| 862 | + |
| 863 | + def build_state_tree(state_cls: type[BaseState]) -> dict: |
| 864 | + """Recursively build state tree data. |
| 865 | +
|
| 866 | + Args: |
| 867 | + state_cls: The state class to build the tree for. |
| 868 | +
|
| 869 | + Returns: |
| 870 | + A dictionary containing the state tree data. |
| 871 | + """ |
| 872 | + state_id = state_cls._state_id |
| 873 | + |
| 874 | + # Build event handlers list |
| 875 | + handlers = [] |
| 876 | + for name, handler in state_cls.event_handlers.items(): |
| 877 | + event_id = getattr(handler.fn, EVENT_ID_MARKER, None) |
| 878 | + handlers.append({ |
| 879 | + "name": name, |
| 880 | + "event_id": event_id, |
| 881 | + "minified_name": ( |
| 882 | + _int_to_minified_name(event_id) if event_id is not None else None |
| 883 | + ), |
| 884 | + }) |
| 885 | + handlers.sort(key=operator.itemgetter("name")) |
| 886 | + |
| 887 | + # Build substates recursively |
| 888 | + substates = [ |
| 889 | + build_state_tree(substate) |
| 890 | + for substate in sorted(state_cls.class_subclasses, key=lambda s: s.__name__) |
| 891 | + ] |
| 892 | + |
| 893 | + return { |
| 894 | + "name": state_cls.__name__, |
| 895 | + "full_name": state_cls.get_full_name(), |
| 896 | + "state_id": state_id, |
| 897 | + "minified_name": ( |
| 898 | + _int_to_minified_name(state_id) if state_id is not None else None |
| 899 | + ), |
| 900 | + "event_handlers": handlers, |
| 901 | + "substates": substates, |
| 902 | + } |
| 903 | + |
| 904 | + def print_state_tree(state_data: dict, prefix: str = "", is_last: bool = True): |
| 905 | + """Print a state and its children as a tree. |
| 906 | +
|
| 907 | + Args: |
| 908 | + state_data: The state data dictionary. |
| 909 | + prefix: The prefix for indentation. |
| 910 | + is_last: Whether this is the last item in the current level. |
| 911 | + """ |
| 912 | + state_id = state_data["state_id"] |
| 913 | + minified = state_data["minified_name"] |
| 914 | + |
| 915 | + if state_id is not None: |
| 916 | + f'{state_data["name"]} (state_id={state_id} -> "{minified}")' |
| 917 | + else: |
| 918 | + f"{state_data['name']} (state_id=None)" |
| 919 | + |
| 920 | + # Calculate new prefix for children |
| 921 | + child_prefix = prefix + (" " if is_last else "| ") |
| 922 | + |
| 923 | + # Print event handlers |
| 924 | + handlers = state_data["event_handlers"] |
| 925 | + substates = state_data["substates"] |
| 926 | + has_substates = len(substates) > 0 |
| 927 | + |
| 928 | + if handlers: |
| 929 | + handler_prefix = child_prefix + ("| " if has_substates else " ") |
| 930 | + for i, handler in enumerate(handlers): |
| 931 | + is_last_handler = i == len(handlers) - 1 |
| 932 | + event_id = handler["event_id"] |
| 933 | + if event_id is not None: |
| 934 | + _ = ( |
| 935 | + handler_prefix, |
| 936 | + is_last_handler, |
| 937 | + ) # silence unused variable warnings |
| 938 | + |
| 939 | + # Print substates recursively |
| 940 | + for i, substate in enumerate(substates): |
| 941 | + is_last_substate = i == len(substates) - 1 |
| 942 | + print_state_tree(substate, child_prefix, is_last_substate) |
| 943 | + |
| 944 | + tree_data = build_state_tree(State) |
| 945 | + |
| 946 | + if output_json: |
| 947 | + pass |
| 948 | + else: |
| 949 | + print_state_tree(tree_data) |
| 950 | + |
| 951 | + |
| 952 | +@cli.command(name="state-lookup") |
| 953 | +@loglevel_option |
| 954 | +@click.option( |
| 955 | + "--json", |
| 956 | + "output_json", |
| 957 | + is_flag=True, |
| 958 | + help="Output detailed info as JSON.", |
| 959 | +) |
| 960 | +@click.argument("minified_path") |
| 961 | +def state_lookup(output_json: bool, minified_path: str): |
| 962 | + """Lookup a state by its minified path (e.g., 'a.bU').""" |
| 963 | + from reflex.state import _minified_name_to_int, _state_id_registry |
| 964 | + from reflex.utils import prerequisites |
| 965 | + |
| 966 | + # Load the user's app to register all state classes |
| 967 | + prerequisites.get_app() |
| 968 | + |
| 969 | + # Parse the dotted path |
| 970 | + parts = minified_path.split(".") |
| 971 | + |
| 972 | + # Resolve each part |
| 973 | + result_parts = [] |
| 974 | + for part in parts: |
| 975 | + try: |
| 976 | + state_id = _minified_name_to_int(part) |
| 977 | + except ValueError as err: |
| 978 | + raise SystemExit(1) from err |
| 979 | + |
| 980 | + state_cls = _state_id_registry.get(state_id) |
| 981 | + if state_cls is None: |
| 982 | + raise SystemExit(1) |
| 983 | + |
| 984 | + result_parts.append({ |
| 985 | + "minified": part, |
| 986 | + "state_id": state_id, |
| 987 | + "module": state_cls.__module__, |
| 988 | + "class": state_cls.__name__, |
| 989 | + "full_name": state_cls.get_full_name(), |
| 990 | + }) |
| 991 | + |
| 992 | + if output_json: |
| 993 | + pass |
| 994 | + else: |
| 995 | + # Simple output: module.ClassName for each part |
| 996 | + for _info in result_parts: |
| 997 | + pass |
| 998 | + |
| 999 | + |
| 1000 | +@cli.command(name="state-next-id") |
| 1001 | +@loglevel_option |
| 1002 | +@click.option( |
| 1003 | + "--after-max", |
| 1004 | + is_flag=True, |
| 1005 | + help="Return max(state_id) + 1 instead of first gap.", |
| 1006 | +) |
| 1007 | +def state_next_id(after_max: bool): |
| 1008 | + """Print the next available state_id.""" |
| 1009 | + from reflex.state import _state_id_registry |
| 1010 | + from reflex.utils import prerequisites |
| 1011 | + |
| 1012 | + # Load the user's app to register all state classes |
| 1013 | + prerequisites.get_app() |
| 1014 | + |
| 1015 | + if not _state_id_registry: |
| 1016 | + return |
| 1017 | + |
| 1018 | + if after_max: |
| 1019 | + # Return max + 1 |
| 1020 | + next_id = max(_state_id_registry.keys()) + 1 |
| 1021 | + else: |
| 1022 | + # Find first gap starting from 0 |
| 1023 | + used_ids = set(_state_id_registry.keys()) |
| 1024 | + next_id = 0 |
| 1025 | + while next_id in used_ids: |
| 1026 | + next_id += 1 |
| 1027 | + |
| 1028 | + |
845 | 1029 | def _convert_reflex_loglevel_to_reflex_cli_loglevel( |
846 | 1030 | loglevel: constants.LogLevel, |
847 | 1031 | ) -> HostingLogLevel: |
|
0 commit comments