From 1651289485d194118eafd6fa987608dbaaa100c6 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 6 Feb 2025 10:09:05 -0800 Subject: [PATCH 01/38] use getattr when given str in getitem (#4761) * use getattr when given str in getitem * stronger checking and tests * switch ordering * use safe issubclass * calculate origin differently --- reflex/vars/object.py | 17 +++++++++++++---- tests/integration/test_var_operations.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/reflex/vars/object.py b/reflex/vars/object.py index cb29cabfb..89479bbc4 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -22,7 +22,12 @@ from typing_extensions import is_typeddict from reflex.utils import types from reflex.utils.exceptions import VarAttributeError -from reflex.utils.types import GenericType, get_attribute_access_type, get_origin +from reflex.utils.types import ( + GenericType, + get_attribute_access_type, + get_origin, + safe_issubclass, +) from .base import ( CachedVarOperation, @@ -187,10 +192,14 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping): Returns: The item from the object. """ + from .sequence import LiteralStringVar + if not isinstance(key, (StringVar, str, int, NumberVar)) or ( isinstance(key, NumberVar) and key._is_strict_float() ): raise_unsupported_operand_types("[]", (type(self), type(key))) + if isinstance(key, str) and isinstance(Var.create(key), LiteralStringVar): + return self.__getattr__(key) return ObjectItemOperation.create(self, key).guess_type() # NoReturn is used here to catch when key value is Any @@ -260,12 +269,12 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping): if types.is_optional(var_type): var_type = get_args(var_type)[0] - fixed_type = var_type if isclass(var_type) else get_origin(var_type) + fixed_type = get_origin(var_type) or var_type if ( - (isclass(fixed_type) and not issubclass(fixed_type, Mapping)) + is_typeddict(fixed_type) + or (isclass(fixed_type) and not safe_issubclass(fixed_type, Mapping)) or (fixed_type in types.UnionTypes) - or is_typeddict(fixed_type) ): attribute_type = get_attribute_access_type(var_type, name) if attribute_type is None: diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index a5a74c9ee..16885cd06 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -10,6 +10,8 @@ from reflex.testing import AppHarness def VarOperations(): """App with var operations.""" + from typing import TypedDict + import reflex as rx from reflex.vars.base import LiteralVar from reflex.vars.sequence import ArrayVar @@ -17,6 +19,10 @@ def VarOperations(): class Object(rx.Base): name: str = "hello" + class Person(TypedDict): + name: str + age: int + class VarOperationState(rx.State): int_var1: rx.Field[int] = rx.field(10) int_var2: rx.Field[int] = rx.field(5) @@ -34,6 +40,9 @@ def VarOperations(): dict1: rx.Field[dict[int, int]] = rx.field({1: 2}) dict2: rx.Field[dict[int, int]] = rx.field({3: 4}) html_str: rx.Field[str] = rx.field("
hello
") + people: rx.Field[list[Person]] = rx.field( + [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}] + ) app = rx.App(_state=rx.State) @@ -619,6 +628,15 @@ def VarOperations(): ), id="dict_in_foreach3", ), + rx.box( + rx.foreach( + VarOperationState.people, + lambda person: rx.text.span( + "Hello " + person["name"], person["age"] + 3 + ), + ), + id="typed_dict_in_foreach", + ), ) @@ -826,6 +844,7 @@ def test_var_operations(driver, var_operations: AppHarness): ("dict_in_foreach1", "a1b2"), ("dict_in_foreach2", "12"), ("dict_in_foreach3", "1234"), + ("typed_dict_in_foreach", "Hello Alice33Hello Bob28"), ] for tag, expected in tests: From 9d23271c14a136df85475c465aad94a46fe90052 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 6 Feb 2025 10:09:26 -0800 Subject: [PATCH 02/38] improve behavior on missing language with markdown code blocks (#4750) * improve behavior on missing language with markdown code blocks * special case on literal var * fix tests * missing f * remove extra throw --- reflex/components/datadisplay/code.py | 46 +++++++++++++++---- reflex/components/datadisplay/code.pyi | 2 +- reflex/components/markdown/markdown.py | 35 +++++++++++--- reflex/components/markdown/markdown.pyi | 5 +- .../components/markdown/test_markdown.py | 9 ++-- 5 files changed, 73 insertions(+), 24 deletions(-) diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 4f1eb493e..3e4794482 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +import typing from typing import ClassVar, Dict, Literal, Optional, Union from reflex.components.component import Component, ComponentNamespace @@ -503,7 +504,7 @@ class CodeBlock(Component, MarkdownComponentMap): return ["can_copy", "copy_button"] @classmethod - def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> str: + def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> Var: """Get the hook to register the language. Args: @@ -514,21 +515,46 @@ class CodeBlock(Component, MarkdownComponentMap): Returns: The hook to register the language. """ - return f""" - if ({language_var!s}) {{ - (async () => {{ - try {{ + language_in_there = Var.create(typing.get_args(LiteralCodeLanguage)).contains( + language_var + ) + async_load = f""" +(async () => {{ + try {{ const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`); SyntaxHighlighter.registerLanguage({language_var!s}, module.default); - }} catch (error) {{ - console.error(`Error importing language module for ${{{language_var!s}}}:`, error); - }} - }})(); + }} catch (error) {{ + console.error(`Language ${{{language_var!s}}} is not supported for code blocks inside of markdown: `, error); + }} +}})(); +""" + return Var( + f""" + if ({language_var!s}) {{ + if (!{language_in_there!s}) {{ + console.warn(`Language \\`${{{language_var!s}}}\\` is not supported for code blocks inside of markdown.`); + {language_var!s} = ''; + }} else {{ + {async_load!s} + }} }} """ + if not isinstance(language_var, LiteralVar) + else f""" +if ({language_var!s}) {{ + {async_load!s} +}}""", + _var_data=VarData( + imports={ + cls.__fields__["library"].default: [ + ImportVar(tag="PrismAsyncLight", alias="SyntaxHighlighter") + ] + }, + ), + ) @classmethod - def get_component_map_custom_code(cls) -> str: + def get_component_map_custom_code(cls) -> Var: """Get the custom code for the component. Returns: diff --git a/reflex/components/datadisplay/code.pyi b/reflex/components/datadisplay/code.pyi index fc35092fe..fda92a974 100644 --- a/reflex/components/datadisplay/code.pyi +++ b/reflex/components/datadisplay/code.pyi @@ -984,7 +984,7 @@ class CodeBlock(Component, MarkdownComponentMap): def add_style(self): ... @classmethod - def get_component_map_custom_code(cls) -> str: ... + def get_component_map_custom_code(cls) -> Var: ... def add_hooks(self) -> list[str | Var]: ... class CodeblockNamespace(ComponentNamespace): diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index 27bd5bd62..91d34ea9b 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -12,7 +12,7 @@ from reflex.components.component import BaseComponent, Component, CustomComponen from reflex.components.tags.tag import Tag from reflex.utils import types from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars.base import LiteralVar, Var +from reflex.vars.base import LiteralVar, Var, VarData from reflex.vars.function import ARRAY_ISARRAY, ArgsFunctionOperation, DestructuredArg from reflex.vars.number import ternary_operation @@ -83,13 +83,13 @@ class MarkdownComponentMap: _explicit_return: bool = dataclasses.field(default=False) @classmethod - def get_component_map_custom_code(cls) -> str: + def get_component_map_custom_code(cls) -> Var: """Get the custom code for the component map. Returns: The custom code for the component map. """ - return "" + return Var("") @classmethod def create_map_fn_var( @@ -97,6 +97,7 @@ class MarkdownComponentMap: fn_body: Var | None = None, fn_args: Sequence[str] | None = None, explicit_return: bool | None = None, + var_data: VarData | None = None, ) -> Var: """Create a function Var for the component map. @@ -104,6 +105,7 @@ class MarkdownComponentMap: fn_body: The formatted component as a string. fn_args: The function arguments. explicit_return: Whether to use explicit return syntax. + var_data: The var data for the function. Returns: The function Var for the component map. @@ -116,6 +118,7 @@ class MarkdownComponentMap: args_names=(DestructuredArg(fields=tuple(fn_args)),), return_expr=fn_body, explicit_return=explicit_return, + _var_data=var_data, ) @classmethod @@ -239,6 +242,15 @@ class Markdown(Component): component(_MOCK_ARG)._get_all_imports() for component in self.component_map.values() ], + *( + [inline_code_var_data.old_school_imports()] + if ( + inline_code_var_data + := self._get_inline_code_fn_var()._get_all_var_data() + ) + is not None + else [] + ), ] def _get_tag_map_fn_var(self, tag: str) -> Var: @@ -278,12 +290,20 @@ class Markdown(Component): self._get_map_fn_custom_code_from_children(self.get_component("code")) ) - codeblock_custom_code = "\n".join(custom_code_list) + var_data = VarData.merge( + *[ + code._get_all_var_data() + for code in custom_code_list + if isinstance(code, Var) + ] + ) + + codeblock_custom_code = "\n".join(map(str, custom_code_list)) # Format the code to handle inline and block code. formatted_code = f""" const match = (className || '').match(/language-(?.*)/); -const {_LANGUAGE!s} = match ? match[1] : ''; +let {_LANGUAGE!s} = match ? match[1] : ''; {codeblock_custom_code}; return inline ? ( {self.format_component("code")} @@ -302,6 +322,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; ), fn_body=Var(_js_expr=formatted_code), explicit_return=True, + var_data=var_data, ) def get_component(self, tag: str, **props) -> Component: @@ -381,7 +402,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; def _get_map_fn_custom_code_from_children( self, component: BaseComponent - ) -> list[str]: + ) -> list[str | Var]: """Recursively get markdown custom code from children components. Args: @@ -390,7 +411,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; Returns: A list of markdown custom code strings. """ - custom_code_list = [] + custom_code_list: list[str | Var] = [] if isinstance(component, MarkdownComponentMap): custom_code_list.append(component.get_component_map_custom_code()) diff --git a/reflex/components/markdown/markdown.pyi b/reflex/components/markdown/markdown.pyi index 606780a7a..61ddee094 100644 --- a/reflex/components/markdown/markdown.pyi +++ b/reflex/components/markdown/markdown.pyi @@ -11,7 +11,7 @@ from reflex.components.component import Component from reflex.event import EventType from reflex.style import Style from reflex.utils.imports import ImportDict -from reflex.vars.base import LiteralVar, Var +from reflex.vars.base import LiteralVar, Var, VarData _CHILDREN = Var(_js_expr="children", _var_type=str) _PROPS = Var(_js_expr="...props") @@ -32,13 +32,14 @@ def get_base_component_map() -> dict[str, Callable]: ... @dataclasses.dataclass() class MarkdownComponentMap: @classmethod - def get_component_map_custom_code(cls) -> str: ... + def get_component_map_custom_code(cls) -> Var: ... @classmethod def create_map_fn_var( cls, fn_body: Var | None = None, fn_args: Sequence[str] | None = None, explicit_return: bool | None = None, + var_data: VarData | None = None, ) -> Var: ... @classmethod def get_fn_args(cls) -> Sequence[str]: ... diff --git a/tests/units/components/markdown/test_markdown.py b/tests/units/components/markdown/test_markdown.py index 866f32ae1..c6d395eb1 100644 --- a/tests/units/components/markdown/test_markdown.py +++ b/tests/units/components/markdown/test_markdown.py @@ -148,7 +148,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ( "code", {}, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; if (_language) { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Error importing language module for ${_language}:`, error); } })(); } ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; if (_language) { if (!["abap", "abnf", "actionscript", "ada", "agda", "al", "antlr4", "apacheconf", "apex", "apl", "applescript", "aql", "arduino", "arff", "asciidoc", "asm6502", "asmatmel", "aspnet", "autohotkey", "autoit", "avisynth", "avro-idl", "bash", "basic", "batch", "bbcode", "bicep", "birb", "bison", "bnf", "brainfuck", "brightscript", "bro", "bsl", "c", "cfscript", "chaiscript", "cil", "clike", "clojure", "cmake", "cobol", "coffeescript", "concurnas", "coq", "core", "cpp", "crystal", "csharp", "cshtml", "csp", "css", "css-extras", "csv", "cypher", "d", "dart", "dataweave", "dax", "dhall", "diff", "django", "dns-zone-file", "docker", "dot", "ebnf", "editorconfig", "eiffel", "ejs", "elixir", "elm", "erb", "erlang", "etlua", "excel-formula", "factor", "false", "firestore-security-rules", "flow", "fortran", "fsharp", "ftl", "gap", "gcode", "gdscript", "gedcom", "gherkin", "git", "glsl", "gml", "gn", "go", "go-module", "graphql", "groovy", "haml", "handlebars", "haskell", "haxe", "hcl", "hlsl", "hoon", "hpkp", "hsts", "http", "ichigojam", "icon", "icu-message-format", "idris", "iecst", "ignore", "index", "inform7", "ini", "io", "j", "java", "javadoc", "javadoclike", "javascript", "javastacktrace", "jexl", "jolie", "jq", "js-extras", "js-templates", "jsdoc", "json", "json5", "jsonp", "jsstacktrace", "jsx", "julia", "keepalived", "keyman", "kotlin", "kumir", "kusto", "latex", "latte", "less", "lilypond", "liquid", "lisp", "livescript", "llvm", "log", "lolcode", "lua", "magma", "makefile", "markdown", "markup", "markup-templating", "matlab", "maxscript", "mel", "mermaid", "mizar", "mongodb", "monkey", "moonscript", "n1ql", "n4js", "nand2tetris-hdl", "naniscript", "nasm", "neon", "nevod", "nginx", "nim", "nix", "nsis", "objectivec", "ocaml", "opencl", "openqasm", "oz", "parigp", "parser", "pascal", "pascaligo", "pcaxis", "peoplecode", "perl", "php", "php-extras", "phpdoc", "plsql", "powerquery", "powershell", "processing", "prolog", "promql", "properties", "protobuf", "psl", "pug", "puppet", "pure", "purebasic", "purescript", "python", "q", "qml", "qore", "qsharp", "r", "racket", "reason", "regex", "rego", "renpy", "rest", "rip", "roboconf", "robotframework", "ruby", "rust", "sas", "sass", "scala", "scheme", "scss", "shell-session", "smali", "smalltalk", "smarty", "sml", "solidity", "solution-file", "soy", "sparql", "splunk-spl", "sqf", "sql", "squirrel", "stan", "stylus", "swift", "systemd", "t4-cs", "t4-templating", "t4-vb", "tap", "tcl", "textile", "toml", "tremor", "tsx", "tt2", "turtle", "twig", "typescript", "typoscript", "unrealscript", "uorazor", "uri", "v", "vala", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "warpscript", "wasm", "web-idl", "wiki", "wolfram", "wren", "xeora", "xml-doc", "xojo", "xquery", "yaml", "yang", "zig"].includes(_language)) { console.warn(`Language \`${_language}\` is not supported for code blocks inside of markdown.`); _language = ''; } else { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Language ${_language} is not supported for code blocks inside of markdown: `, error); } })(); } } ; return inline ? ( {children} ) : ( ); })""", ), ( "code", @@ -157,7 +157,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe value, **props ) }, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", ), ( "h1", @@ -171,7 +171,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ( "code", {"codeblock": syntax_highlighter_memoized_component(CodeBlock)}, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; if (_language) { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Error importing language module for ${_language}:`, error); } })(); } ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; if (_language) { if (!["abap", "abnf", "actionscript", "ada", "agda", "al", "antlr4", "apacheconf", "apex", "apl", "applescript", "aql", "arduino", "arff", "asciidoc", "asm6502", "asmatmel", "aspnet", "autohotkey", "autoit", "avisynth", "avro-idl", "bash", "basic", "batch", "bbcode", "bicep", "birb", "bison", "bnf", "brainfuck", "brightscript", "bro", "bsl", "c", "cfscript", "chaiscript", "cil", "clike", "clojure", "cmake", "cobol", "coffeescript", "concurnas", "coq", "core", "cpp", "crystal", "csharp", "cshtml", "csp", "css", "css-extras", "csv", "cypher", "d", "dart", "dataweave", "dax", "dhall", "diff", "django", "dns-zone-file", "docker", "dot", "ebnf", "editorconfig", "eiffel", "ejs", "elixir", "elm", "erb", "erlang", "etlua", "excel-formula", "factor", "false", "firestore-security-rules", "flow", "fortran", "fsharp", "ftl", "gap", "gcode", "gdscript", "gedcom", "gherkin", "git", "glsl", "gml", "gn", "go", "go-module", "graphql", "groovy", "haml", "handlebars", "haskell", "haxe", "hcl", "hlsl", "hoon", "hpkp", "hsts", "http", "ichigojam", "icon", "icu-message-format", "idris", "iecst", "ignore", "index", "inform7", "ini", "io", "j", "java", "javadoc", "javadoclike", "javascript", "javastacktrace", "jexl", "jolie", "jq", "js-extras", "js-templates", "jsdoc", "json", "json5", "jsonp", "jsstacktrace", "jsx", "julia", "keepalived", "keyman", "kotlin", "kumir", "kusto", "latex", "latte", "less", "lilypond", "liquid", "lisp", "livescript", "llvm", "log", "lolcode", "lua", "magma", "makefile", "markdown", "markup", "markup-templating", "matlab", "maxscript", "mel", "mermaid", "mizar", "mongodb", "monkey", "moonscript", "n1ql", "n4js", "nand2tetris-hdl", "naniscript", "nasm", "neon", "nevod", "nginx", "nim", "nix", "nsis", "objectivec", "ocaml", "opencl", "openqasm", "oz", "parigp", "parser", "pascal", "pascaligo", "pcaxis", "peoplecode", "perl", "php", "php-extras", "phpdoc", "plsql", "powerquery", "powershell", "processing", "prolog", "promql", "properties", "protobuf", "psl", "pug", "puppet", "pure", "purebasic", "purescript", "python", "q", "qml", "qore", "qsharp", "r", "racket", "reason", "regex", "rego", "renpy", "rest", "rip", "roboconf", "robotframework", "ruby", "rust", "sas", "sass", "scala", "scheme", "scss", "shell-session", "smali", "smalltalk", "smarty", "sml", "solidity", "solution-file", "soy", "sparql", "splunk-spl", "sqf", "sql", "squirrel", "stan", "stylus", "swift", "systemd", "t4-cs", "t4-templating", "t4-vb", "tap", "tcl", "textile", "toml", "tremor", "tsx", "tt2", "turtle", "twig", "typescript", "typoscript", "unrealscript", "uorazor", "uri", "v", "vala", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "warpscript", "wasm", "web-idl", "wiki", "wolfram", "wren", "xeora", "xml-doc", "xojo", "xquery", "yaml", "yang", "zig"].includes(_language)) { console.warn(`Language \`${_language}\` is not supported for code blocks inside of markdown.`); _language = ''; } else { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Language ${_language} is not supported for code blocks inside of markdown: `, error); } })(); } } ; return inline ? ( {children} ) : ( ); })""", ), ( "code", @@ -180,11 +180,12 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ShikiHighLevelCodeBlock ) }, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", ), ], ) def test_markdown_format_component(key, component_map, expected): markdown = Markdown.create("# header", component_map=component_map) result = markdown.format_component_map() + print(str(result[key])) assert str(result[key]) == expected From ab558ce17285a30ccf88d479277f2b2ebea2760c Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 6 Feb 2025 10:09:40 -0800 Subject: [PATCH 03/38] increase nested type checking for component var types (#4756) * increase nested type checking for component var types * handle mapping as dict in type hint * fix weird cases of using _isinstance instead of isinstance * test out nested=0 * move union below * don't use _instance for simple unions --- reflex/components/component.py | 15 +++------ reflex/components/core/match.py | 8 ++--- reflex/components/markdown/markdown.py | 5 ++- reflex/components/tags/tag.py | 4 +-- reflex/utils/types.py | 42 +++++++++++++++++--------- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 6d1264f4d..6e4c6c37f 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -179,6 +179,7 @@ ComponentStyle = Dict[ Union[str, Type[BaseComponent], Callable, ComponentNamespace], Any ] ComponentChild = Union[types.PrimitiveType, Var, BaseComponent] +ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent) def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: @@ -191,11 +192,7 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: Returns: Whether the object satisfies the type hint. """ - if isinstance(obj, LiteralVar): - return types._isinstance(obj._var_value, type_hint) - if isinstance(obj, Var): - return types._issubclass(obj._var_type, type_hint) - return types._isinstance(obj, type_hint) + return types._isinstance(obj, type_hint, nested=1) class Component(BaseComponent, ABC): @@ -712,8 +709,8 @@ class Component(BaseComponent, ABC): validate_children(child) # Make sure the child is a valid type. - if isinstance(child, dict) or not types._isinstance( - child, ComponentChild + if isinstance(child, dict) or not isinstance( + child, ComponentChildTypes ): raise ChildrenTypeError(component=cls.__name__, child=child) @@ -1771,9 +1768,7 @@ class CustomComponent(Component): return [ Var( _js_expr=name, - _var_type=( - prop._var_type if types._isinstance(prop, Var) else type(prop) - ), + _var_type=(prop._var_type if isinstance(prop, Var) else type(prop)), ).guess_type() for name, prop in self.props.items() ] diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index 5c31669a1..2d936544a 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -178,9 +178,9 @@ class Match(MemoizationLeaf): first_case_return = match_cases[0][-1] return_type = type(first_case_return) - if types._isinstance(first_case_return, BaseComponent): + if isinstance(first_case_return, BaseComponent): return_type = BaseComponent - elif types._isinstance(first_case_return, Var): + elif isinstance(first_case_return, Var): return_type = Var for index, case in enumerate(match_cases): @@ -228,8 +228,8 @@ class Match(MemoizationLeaf): # Validate the match cases (as well as the default case) to have Var return types. if any( - case for case in match_cases if not types._isinstance(case[-1], Var) - ) or not types._isinstance(default, Var): + case for case in match_cases if not isinstance(case[-1], Var) + ) or not isinstance(default, Var): raise ValueError("Return types of match cases should be Vars.") return Var( diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index 91d34ea9b..51d3dd3dd 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -6,11 +6,10 @@ import dataclasses import textwrap from functools import lru_cache from hashlib import md5 -from typing import Any, Callable, Dict, Sequence, Union +from typing import Any, Callable, Dict, Sequence from reflex.components.component import BaseComponent, Component, CustomComponent from reflex.components.tags.tag import Tag -from reflex.utils import types from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import LiteralVar, Var, VarData from reflex.vars.function import ARRAY_ISARRAY, ArgsFunctionOperation, DestructuredArg @@ -169,7 +168,7 @@ class Markdown(Component): Returns: The markdown component. """ - if len(children) != 1 or not types._isinstance(children[0], Union[str, Var]): + if len(children) != 1 or not isinstance(children[0], (str, Var)): raise ValueError( "Markdown component must have exactly one child containing the markdown source." ) diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index 983726e56..515d9e05f 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Mapping, Optional, Sequence from reflex.event import EventChain from reflex.utils import format, types @@ -103,7 +103,7 @@ class Tag: { format.to_camel_case(name, allow_hyphens=True): ( prop - if types._isinstance(prop, Union[EventChain, dict]) + if types._isinstance(prop, (EventChain, Mapping)) else LiteralVar.create(prop) ) # rx.color is always a string for name, prop in kwargs.items() diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 58fec8f3b..b432319e0 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -95,6 +95,7 @@ GenericType = Union[Type, _GenericAlias] # Valid state var types. JSONType = {str, int, float, bool} PrimitiveType = Union[int, float, bool, str, list, dict, set, tuple] +PrimitiveTypes = (int, float, bool, str, list, dict, set, tuple) StateVar = Union[PrimitiveType, Base, None] StateIterVar = Union[list, set, tuple] @@ -551,13 +552,13 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool: return required_keys.issubset(required_keys) -def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: +def _isinstance(obj: Any, cls: GenericType, nested: int = 0) -> bool: """Check if an object is an instance of a class. Args: obj: The object to check. cls: The class to check against. - nested: Whether the check is nested. + nested: How many levels deep to check. Returns: Whether the object is an instance of the class. @@ -565,15 +566,24 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: if cls is Any: return True + from reflex.vars import LiteralVar, Var + + if cls is Var: + return isinstance(obj, Var) + if isinstance(obj, LiteralVar): + return _isinstance(obj._var_value, cls, nested=nested) + if isinstance(obj, Var): + return _issubclass(obj._var_type, cls) + if cls is None or cls is type(None): return obj is None + if cls and is_union(cls): + return any(_isinstance(obj, arg, nested=nested) for arg in get_args(cls)) + if is_literal(cls): return obj in get_args(cls) - if is_union(cls): - return any(_isinstance(obj, arg) for arg in get_args(cls)) - origin = get_origin(cls) if origin is None: @@ -596,38 +606,40 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: # cls is a simple generic class return isinstance(obj, origin) - if nested and args: + if nested > 0 and args: if origin is list: return isinstance(obj, list) and all( - _isinstance(item, args[0]) for item in obj + _isinstance(item, args[0], nested=nested - 1) for item in obj ) if origin is tuple: if args[-1] is Ellipsis: return isinstance(obj, tuple) and all( - _isinstance(item, args[0]) for item in obj + _isinstance(item, args[0], nested=nested - 1) for item in obj ) return ( isinstance(obj, tuple) and len(obj) == len(args) and all( - _isinstance(item, arg) for item, arg in zip(obj, args, strict=True) + _isinstance(item, arg, nested=nested - 1) + for item, arg in zip(obj, args, strict=True) ) ) - if origin in (dict, Breakpoints): - return isinstance(obj, dict) and all( - _isinstance(key, args[0]) and _isinstance(value, args[1]) + if origin in (dict, Mapping, Breakpoints): + return isinstance(obj, Mapping) and all( + _isinstance(key, args[0], nested=nested - 1) + and _isinstance(value, args[1], nested=nested - 1) for key, value in obj.items() ) if origin is set: return isinstance(obj, set) and all( - _isinstance(item, args[0]) for item in obj + _isinstance(item, args[0], nested=nested - 1) for item in obj ) if args: from reflex.vars import Field if origin is Field: - return _isinstance(obj, args[0]) + return _isinstance(obj, args[0], nested=nested) return isinstance(obj, get_base_class(cls)) @@ -749,7 +761,7 @@ def check_prop_in_allowed_types(prop: Any, allowed_types: Iterable) -> bool: """ from reflex.vars import Var - type_ = prop._var_type if _isinstance(prop, Var) else type(prop) + type_ = prop._var_type if isinstance(prop, Var) else type(prop) return type_ in allowed_types From b3b79a652d3e1b7f85740f0e6210f536236f7041 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 6 Feb 2025 10:41:38 -0800 Subject: [PATCH 04/38] improve foreach behavior with strings (#4751) * improve foreach behavior with strings * add a defensive guard before giving up * add integration tests --- reflex/components/core/foreach.py | 14 ++++++++++++-- tests/integration/test_var_operations.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index e9222b200..13db48575 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -54,9 +54,10 @@ class Foreach(Component): TypeError: If the render function is a ComponentState. UntypedVarError: If the iterable is of type Any without a type annotation. """ - from reflex.vars.object import ObjectVar + from reflex.vars import ArrayVar, ObjectVar, StringVar + + iterable = LiteralVar.create(iterable).guess_type() - iterable = LiteralVar.create(iterable) if iterable._var_type == Any: raise ForeachVarError( f"Could not foreach over var `{iterable!s}` of type Any. " @@ -75,6 +76,15 @@ class Foreach(Component): if isinstance(iterable, ObjectVar): iterable = iterable.entries() + if isinstance(iterable, StringVar): + iterable = iterable.split() + + if not isinstance(iterable, ArrayVar): + raise ForeachVarError( + f"Could not foreach over var `{iterable!s}` of type {iterable._var_type}. " + "See https://reflex.dev/docs/library/dynamic-rendering/foreach/" + ) + component = cls( iterable=iterable, render_fn=render_fn, diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index 16885cd06..35763556a 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -628,6 +628,14 @@ def VarOperations(): ), id="dict_in_foreach3", ), + rx.box( + rx.foreach("abcdef", lambda x: rx.text.span(x + " ")), + id="str_in_foreach", + ), + rx.box( + rx.foreach(VarOperationState.str_var1, lambda x: rx.text.span(x + " ")), + id="str_var_in_foreach", + ), rx.box( rx.foreach( VarOperationState.people, @@ -844,6 +852,8 @@ def test_var_operations(driver, var_operations: AppHarness): ("dict_in_foreach1", "a1b2"), ("dict_in_foreach2", "12"), ("dict_in_foreach3", "1234"), + ("str_in_foreach", "a b c d e f"), + ("str_var_in_foreach", "f i r s t"), ("typed_dict_in_foreach", "Hello Alice33Hello Bob28"), ] From f3220470e8141a2017edb3a2183a78b30e0a903e Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 7 Feb 2025 14:20:29 -0800 Subject: [PATCH 05/38] fix dynamic icons for underscore and positional argument (#4767) * fix dynamic icons for underscore and positional argument * use no return --- reflex/components/core/sticky.py | 4 +-- reflex/components/lucide/icon.py | 14 ++++++-- reflex/vars/base.py | 34 ++++++++++++++----- reflex/vars/number.py | 2 +- reflex/vars/sequence.py | 31 +++++++++++++++-- .../components/datadisplay/test_shiki_code.py | 5 ++- tests/units/vars/test_object.py | 8 ++--- 7 files changed, 75 insertions(+), 23 deletions(-) diff --git a/reflex/components/core/sticky.py b/reflex/components/core/sticky.py index 162bab3cd..cbcec00a9 100644 --- a/reflex/components/core/sticky.py +++ b/reflex/components/core/sticky.py @@ -107,9 +107,7 @@ class StickyBadge(A): default=True, global_ref=False, ) - localhost_hostnames = Var.create( - ["localhost", "127.0.0.1", "[::1]"] - ).guess_type() + localhost_hostnames = Var.create(["localhost", "127.0.0.1", "[::1]"]) is_localhost_expr = localhost_hostnames.contains( Var("window.location.hostname", _var_type=str).guess_type(), ) diff --git a/reflex/components/lucide/icon.py b/reflex/components/lucide/icon.py index 6c7cbede7..269ef7f79 100644 --- a/reflex/components/lucide/icon.py +++ b/reflex/components/lucide/icon.py @@ -4,7 +4,7 @@ from reflex.components.component import Component from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars.base import LiteralVar, Var -from reflex.vars.sequence import LiteralStringVar +from reflex.vars.sequence import LiteralStringVar, StringVar class LucideIconComponent(Component): @@ -40,7 +40,12 @@ class Icon(LucideIconComponent): The created component. """ if children: - if len(children) == 1 and isinstance(children[0], str): + if len(children) == 1: + child = Var.create(children[0]).guess_type() + if not isinstance(child, StringVar): + raise AttributeError( + f"Icon name must be a string, got {children[0]._var_type if isinstance(children[0], Var) else children[0]}" + ) props["tag"] = children[0] else: raise AttributeError( @@ -56,7 +61,10 @@ class Icon(LucideIconComponent): else: raise TypeError(f"Icon name must be a string, got {type(tag)}") elif isinstance(tag, Var): - return DynamicIcon.create(name=tag, **props) + tag_stringified = tag.guess_type() + if not isinstance(tag_stringified, StringVar): + raise TypeError(f"Icon name must be a string, got {tag._var_type}") + return DynamicIcon.create(name=tag_stringified.replace("_", "-"), **props) if ( not isinstance(tag, str) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index c9dd81986..0d8af8f3c 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -75,9 +75,9 @@ from reflex.utils.types import ( if TYPE_CHECKING: from reflex.state import BaseState - from .number import BooleanVar, NumberVar - from .object import ObjectVar - from .sequence import ArrayVar, StringVar + from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar + from .object import LiteralObjectVar, ObjectVar + from .sequence import ArrayVar, LiteralArrayVar, LiteralStringVar, StringVar VAR_TYPE = TypeVar("VAR_TYPE", covariant=True) @@ -573,13 +573,21 @@ class Var(Generic[VAR_TYPE]): return value_with_replaced + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: NoReturn, + _var_data: VarData | None = None, + ) -> Var[Any]: ... + @overload @classmethod def create( # pyright: ignore[reportOverlappingOverload] cls, value: bool, _var_data: VarData | None = None, - ) -> BooleanVar: ... + ) -> LiteralBooleanVar: ... @overload @classmethod @@ -587,7 +595,7 @@ class Var(Generic[VAR_TYPE]): cls, value: int, _var_data: VarData | None = None, - ) -> NumberVar[int]: ... + ) -> LiteralNumberVar[int]: ... @overload @classmethod @@ -595,7 +603,15 @@ class Var(Generic[VAR_TYPE]): cls, value: float, _var_data: VarData | None = None, - ) -> NumberVar[float]: ... + ) -> LiteralNumberVar[float]: ... + + @overload + @classmethod + def create( # pyright: ignore [reportOverlappingOverload] + cls, + value: str, + _var_data: VarData | None = None, + ) -> LiteralStringVar: ... @overload @classmethod @@ -611,7 +627,7 @@ class Var(Generic[VAR_TYPE]): cls, value: None, _var_data: VarData | None = None, - ) -> NoneVar: ... + ) -> LiteralNoneVar: ... @overload @classmethod @@ -619,7 +635,7 @@ class Var(Generic[VAR_TYPE]): cls, value: MAPPING_TYPE, _var_data: VarData | None = None, - ) -> ObjectVar[MAPPING_TYPE]: ... + ) -> LiteralObjectVar[MAPPING_TYPE]: ... @overload @classmethod @@ -627,7 +643,7 @@ class Var(Generic[VAR_TYPE]): cls, value: SEQUENCE_TYPE, _var_data: VarData | None = None, - ) -> ArrayVar[SEQUENCE_TYPE]: ... + ) -> LiteralArrayVar[SEQUENCE_TYPE]: ... @overload @classmethod diff --git a/reflex/vars/number.py b/reflex/vars/number.py index 35a55490a..87f1760a6 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -974,7 +974,7 @@ def boolean_not_operation(value: BooleanVar): frozen=True, slots=True, ) -class LiteralNumberVar(LiteralVar, NumberVar): +class LiteralNumberVar(LiteralVar, NumberVar[NUMBER_T]): """Base class for immutable literal number vars.""" _var_value: float | int = dataclasses.field(default=0) diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index fb797b4ec..0e7b082f9 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -372,6 +372,33 @@ class StringVar(Var[STRING_TYPE], python_types=str): return string_ge_operation(self, other) + @overload + def replace( # pyright: ignore [reportOverlappingOverload] + self, search_value: StringVar | str, new_value: StringVar | str + ) -> StringVar: ... + + @overload + def replace( + self, search_value: Any, new_value: Any + ) -> CustomVarOperationReturn[StringVar]: ... + + def replace(self, search_value: Any, new_value: Any) -> StringVar: # pyright: ignore [reportInconsistentOverload] + """Replace a string with a value. + + Args: + search_value: The string to search. + new_value: The value to be replaced with. + + Returns: + The string replace operation. + """ + if not isinstance(search_value, (StringVar, str)): + raise_unsupported_operand_types("replace", (type(self), type(search_value))) + if not isinstance(new_value, (StringVar, str)): + raise_unsupported_operand_types("replace", (type(self), type(new_value))) + + return string_replace_operation(self, search_value, new_value) + @var_operation def string_lt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): @@ -570,7 +597,7 @@ def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""): @var_operation def string_replace_operation( - string: StringVar, search_value: StringVar | str, new_value: StringVar | str + string: StringVar[Any], search_value: StringVar | str, new_value: StringVar | str ): """Replace a string with a value. @@ -583,7 +610,7 @@ def string_replace_operation( The string replace operation. """ return var_operation_return( - js_expression=f"{string}.replace({search_value}, {new_value})", + js_expression=f"{string}.replaceAll({search_value}, {new_value})", var_type=str, ) diff --git a/tests/units/components/datadisplay/test_shiki_code.py b/tests/units/components/datadisplay/test_shiki_code.py index cc05c35b0..e1c7984f1 100644 --- a/tests/units/components/datadisplay/test_shiki_code.py +++ b/tests/units/components/datadisplay/test_shiki_code.py @@ -11,6 +11,7 @@ from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.layout.box import Box from reflex.style import Style from reflex.vars import Var +from reflex.vars.base import LiteralVar @pytest.mark.parametrize( @@ -99,7 +100,9 @@ def test_create_shiki_code_block( applied_styles = component.style for key, value in expected_styles.items(): - assert Var.create(applied_styles[key])._var_value == value + var = Var.create(applied_styles[key]) + assert isinstance(var, LiteralVar) + assert var._var_value == value @pytest.mark.parametrize( diff --git a/tests/units/vars/test_object.py b/tests/units/vars/test_object.py index 90e34be96..89ace55bb 100644 --- a/tests/units/vars/test_object.py +++ b/tests/units/vars/test_object.py @@ -74,11 +74,11 @@ class ObjectState(rx.State): @pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) -def test_var_create(type_: GenericType) -> None: +def test_var_create(type_: type[Base | Bare | SqlaModel | Dataclass]) -> None: my_object = type_() var = Var.create(my_object) assert var._var_type is type_ - + assert isinstance(var, ObjectVar) quantity = var.quantity assert quantity._var_type is int @@ -94,12 +94,12 @@ def test_literal_create(type_: GenericType) -> None: @pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) -def test_guess(type_: GenericType) -> None: +def test_guess(type_: type[Base | Bare | SqlaModel | Dataclass]) -> None: my_object = type_() var = Var.create(my_object) var = var.guess_type() assert var._var_type is type_ - + assert isinstance(var, ObjectVar) quantity = var.quantity assert quantity._var_type is int From c17cda3e951a54e47222a6eb75c6584a8b56491b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 7 Feb 2025 14:57:12 -0800 Subject: [PATCH 06/38] Ensure EventCallback exposes EventActionsMixin properties (#4772) --- reflex/event.py | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index f247047cf..c2eb8db3a 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -26,6 +26,7 @@ from typing import ( from typing_extensions import ( Protocol, + Self, TypeAliasType, TypedDict, TypeVar, @@ -110,7 +111,7 @@ class EventActionsMixin: event_actions: Dict[str, Union[bool, int]] = dataclasses.field(default_factory=dict) @property - def stop_propagation(self): + def stop_propagation(self) -> Self: """Stop the event from bubbling up the DOM tree. Returns: @@ -122,7 +123,7 @@ class EventActionsMixin: ) @property - def prevent_default(self): + def prevent_default(self) -> Self: """Prevent the default behavior of the event. Returns: @@ -133,7 +134,7 @@ class EventActionsMixin: event_actions={"preventDefault": True, **self.event_actions}, ) - def throttle(self, limit_ms: int): + def throttle(self, limit_ms: int) -> Self: """Throttle the event handler. Args: @@ -147,7 +148,7 @@ class EventActionsMixin: event_actions={"throttle": limit_ms, **self.event_actions}, ) - def debounce(self, delay_ms: int): + def debounce(self, delay_ms: int) -> Self: """Debounce the event handler. Args: @@ -162,7 +163,7 @@ class EventActionsMixin: ) @property - def temporal(self): + def temporal(self) -> Self: """Do not queue the event if the backend is down. Returns: @@ -1773,7 +1774,7 @@ V4 = TypeVar("V4") V5 = TypeVar("V5") -class EventCallback(Generic[Unpack[P]]): +class EventCallback(Generic[Unpack[P]], EventActionsMixin): """A descriptor that wraps a function to be used as an event.""" def __init__(self, func: Callable[[Any, Unpack[P]], Any]): @@ -1784,24 +1785,6 @@ class EventCallback(Generic[Unpack[P]]): """ self.func = func - @property - def prevent_default(self): - """Prevent default behavior. - - Returns: - The event callback with prevent default behavior. - """ - return self - - @property - def stop_propagation(self): - """Stop event propagation. - - Returns: - The event callback with stop propagation behavior. - """ - return self - @overload def __call__( self: EventCallback[Unpack[Q]], From 70920a64be4fb1b55827010f33a6117006c132e1 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 7 Feb 2025 14:59:22 -0800 Subject: [PATCH 07/38] Copy/update assets on compile (#4765) * Add path_ops.update_directory_tree: Copy missing and newer files from src to dest * add console.timing context Log debug messages with timing for different processes. * Update assets tree as app._compile step. If the assets change between hot reload, then update them before reloading (in case a CSS file was added or something). * Add timing for other app._compile events * Only copy assets if assets exist * Fix docstring for update_directory_tree --- reflex/app.py | 66 +++++++++++++++++++++++++++------------- reflex/utils/console.py | 19 ++++++++++++ reflex/utils/path_ops.py | 30 ++++++++++++++++++ 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index a3d0d8e10..18cce69d2 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -99,7 +99,15 @@ from reflex.state import ( _substate_key, code_uses_state_contexts, ) -from reflex.utils import codespaces, console, exceptions, format, prerequisites, types +from reflex.utils import ( + codespaces, + console, + exceptions, + format, + path_ops, + prerequisites, + types, +) from reflex.utils.exec import is_prod_mode, is_testing_env from reflex.utils.imports import ImportVar @@ -991,9 +999,10 @@ class App(MiddlewareMixin, LifespanMixin): should_compile = self._should_compile() if not should_compile: - for route in self._unevaluated_pages: - console.debug(f"Evaluating page: {route}") - self._compile_page(route, save_page=should_compile) + with console.timing("Evaluate Pages (Backend)"): + for route in self._unevaluated_pages: + console.debug(f"Evaluating page: {route}") + self._compile_page(route, save_page=should_compile) # Add the optional endpoints (_upload) self._add_optional_endpoints() @@ -1019,10 +1028,11 @@ class App(MiddlewareMixin, LifespanMixin): + adhoc_steps_without_executor, ) - for route in self._unevaluated_pages: - console.debug(f"Evaluating page: {route}") - self._compile_page(route, save_page=should_compile) - progress.advance(task) + with console.timing("Evaluate Pages (Frontend)"): + for route in self._unevaluated_pages: + console.debug(f"Evaluating page: {route}") + self._compile_page(route, save_page=should_compile) + progress.advance(task) # Add the optional endpoints (_upload) self._add_optional_endpoints() @@ -1057,13 +1067,13 @@ class App(MiddlewareMixin, LifespanMixin): custom_components |= component._get_all_custom_components() # Perform auto-memoization of stateful components. - ( - stateful_components_path, - stateful_components_code, - page_components, - ) = compiler.compile_stateful_components(self._pages.values()) - - progress.advance(task) + with console.timing("Auto-memoize StatefulComponents"): + ( + stateful_components_path, + stateful_components_code, + page_components, + ) = compiler.compile_stateful_components(self._pages.values()) + progress.advance(task) # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State. if code_uses_state_contexts(stateful_components_code) and self._state is None: @@ -1086,6 +1096,17 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) + # Copy the assets. + assets_src = Path.cwd() / constants.Dirs.APP_ASSETS + if assets_src.is_dir(): + with console.timing("Copy assets"): + path_ops.update_directory_tree( + src=assets_src, + dest=( + Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC + ), + ) + # Use a forking process pool, if possible. Much faster, especially for large sites. # Fallback to ThreadPoolExecutor as something that will always work. executor = None @@ -1138,9 +1159,10 @@ class App(MiddlewareMixin, LifespanMixin): _submit_work(compiler.remove_tailwind_from_postcss) # Wait for all compilation tasks to complete. - for future in concurrent.futures.as_completed(result_futures): - compile_results.append(future.result()) - progress.advance(task) + with console.timing("Compile to Javascript"): + for future in concurrent.futures.as_completed(result_futures): + compile_results.append(future.result()) + progress.advance(task) app_root = self._app_root(app_wrappers=app_wrappers) @@ -1175,7 +1197,8 @@ class App(MiddlewareMixin, LifespanMixin): progress.stop() # Install frontend packages. - self._get_frontend_packages(all_imports) + with console.timing("Install Frontend Packages"): + self._get_frontend_packages(all_imports) # Setup the next.config.js transpile_packages = [ @@ -1201,8 +1224,9 @@ class App(MiddlewareMixin, LifespanMixin): # Remove pages that are no longer in the app. p.unlink() - for output_path, code in compile_results: - compiler_utils.write_page(output_path, code) + with console.timing("Write to Disk"): + for output_path, code in compile_results: + compiler_utils.write_page(output_path, code) @contextlib.asynccontextmanager async def modify_state(self, token: str) -> AsyncIterator[BaseState]: diff --git a/reflex/utils/console.py b/reflex/utils/console.py index d5b7a0d6e..5c47eee6f 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -2,8 +2,10 @@ from __future__ import annotations +import contextlib import inspect import shutil +import time from pathlib import Path from types import FrameType @@ -317,3 +319,20 @@ def status(*args, **kwargs): A new status. """ return _console.status(*args, **kwargs) + + +@contextlib.contextmanager +def timing(msg: str): + """Create a context manager to time a block of code. + + Args: + msg: The message to display. + + Yields: + None. + """ + start = time.time() + try: + yield + finally: + debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]") diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index 07a541201..92557d801 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -260,3 +260,33 @@ def find_replace(directory: str | Path, find: str, replace: str): text = filepath.read_text(encoding="utf-8") text = re.sub(find, replace, text) filepath.write_text(text, encoding="utf-8") + + +def update_directory_tree(src: Path, dest: Path): + """Recursively copies a directory tree from src to dest. + Only copies files if the destination file is missing or modified earlier than the source file. + + Args: + src: Source directory + dest: Destination directory + + Raises: + ValueError: If the source is not a directory + """ + if not src.is_dir(): + raise ValueError(f"Source {src} is not a directory") + + # Ensure the destination directory exists + dest.mkdir(parents=True, exist_ok=True) + + for item in src.iterdir(): + dest_item = dest / item.name + + if item.is_dir(): + # Recursively copy subdirectories + update_directory_tree(item, dest_item) + elif item.is_file() and ( + not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime + ): + # Copy file if it doesn't exist in the destination or is older than the source + shutil.copy2(item, dest_item) From ee731a908d0da39aae651d60f034d6a4a77350bb Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 7 Feb 2025 17:19:28 -0800 Subject: [PATCH 08/38] provide plotly subpackages (#4776) --- reflex/compiler/utils.py | 34 +- reflex/components/plotly/__init__.py | 31 +- reflex/components/plotly/plotly.py | 235 ++++++++ reflex/components/plotly/plotly.pyi | 765 +++++++++++++++++++++++++++ reflex/utils/imports.py | 3 + 5 files changed, 1054 insertions(+), 14 deletions(-) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index c797a095f..91ee18b86 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -119,24 +119,34 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]: validate_imports(collapsed_import_dict) import_dicts = [] for lib, fields in collapsed_import_dict.items(): - default, rest = compile_import_statement(fields) - # prevent lib from being rendered on the page if all imports are non rendered kind if not any(f.render for f in fields): continue - if not lib: - if default: - raise ValueError("No default field allowed for empty library.") - if rest is None or len(rest) == 0: - raise ValueError("No fields to import.") - import_dicts.extend(get_import_dict(module) for module in sorted(rest)) - continue + lib_paths: dict[str, list[ImportVar]] = {} - # remove the version before rendering the package imports - lib = format.format_library_name(lib) + for field in fields: + lib_paths.setdefault(field.package_path, []).append(field) - import_dicts.append(get_import_dict(lib, default, rest)) + compiled = { + path: compile_import_statement(fields) for path, fields in lib_paths.items() + } + + for path, (default, rest) in compiled.items(): + if not lib: + if default: + raise ValueError("No default field allowed for empty library.") + if rest is None or len(rest) == 0: + raise ValueError("No fields to import.") + import_dicts.extend(get_import_dict(module) for module in sorted(rest)) + continue + + # remove the version before rendering the package imports + formatted_lib = format.format_library_name(lib) + ( + path if path != "/" else "" + ) + + import_dicts.append(get_import_dict(formatted_lib, default, rest)) return import_dicts diff --git a/reflex/components/plotly/__init__.py b/reflex/components/plotly/__init__.py index 5620d5fc4..8743b31b2 100644 --- a/reflex/components/plotly/__init__.py +++ b/reflex/components/plotly/__init__.py @@ -1,5 +1,32 @@ """Plotly components.""" -from .plotly import Plotly +from reflex.components.component import ComponentNamespace -plotly = Plotly.create +from .plotly import ( + Plotly, + PlotlyBasic, + PlotlyCartesian, + PlotlyFinance, + PlotlyGeo, + PlotlyGl2d, + PlotlyGl3d, + PlotlyMapbox, + PlotlyStrict, +) + + +class PlotlyNamespace(ComponentNamespace): + """Plotly namespace.""" + + __call__ = Plotly.create + basic = PlotlyBasic.create + cartesian = PlotlyCartesian.create + geo = PlotlyGeo.create + gl2d = PlotlyGl2d.create + gl3d = PlotlyGl3d.create + finance = PlotlyFinance.create + mapbox = PlotlyMapbox.create + strict = PlotlyStrict.create + + +plotly = PlotlyNamespace() diff --git a/reflex/components/plotly/plotly.py b/reflex/components/plotly/plotly.py index c85423d35..2ddaad8d7 100644 --- a/reflex/components/plotly/plotly.py +++ b/reflex/components/plotly/plotly.py @@ -10,6 +10,7 @@ from reflex.components.component import Component, NoSSRComponent from reflex.components.core.cond import color_mode_cond from reflex.event import EventHandler, no_args_event_spec from reflex.utils import console +from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import LiteralVar, Var try: @@ -278,3 +279,237 @@ const extractPoints = (points) => { # Spread the figure dict over props, nothing to merge. tag.special_props.append(Var(_js_expr=f"{{...{figure!s}}}")) return tag + + +CREATE_PLOTLY_COMPONENT: ImportDict = { + "react-plotly.js": [ + ImportVar( + tag="createPlotlyComponent", + is_default=True, + package_path="/factory", + ), + ] +} + + +def dynamic_plotly_import(name: str, package: str) -> str: + """Create a dynamic import for a plotly component. + + Args: + name: The name of the component. + package: The package path of the component. + + Returns: + The dynamic import for the plotly component. + """ + return f""" +const {name} = dynamic(() => import('{package}').then(mod => createPlotlyComponent(mod)), {{ssr: false}}) +""" + + +class PlotlyBasic(Plotly): + """Display a basic plotly graph.""" + + tag: str = "BasicPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-basic-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly basic component. + + Returns: + The imports for the plotly basic component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly basic component. + + Returns: + The dynamic imports for the plotly basic component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-basic-dist-min") + + +class PlotlyCartesian(Plotly): + """Display a plotly cartesian graph.""" + + tag: str = "CartesianPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-cartesian-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly cartesian component. + + Returns: + The imports for the plotly cartesian component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly cartesian component. + + Returns: + The dynamic imports for the plotly cartesian component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-cartesian-dist-min") + + +class PlotlyGeo(Plotly): + """Display a plotly geo graph.""" + + tag: str = "GeoPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-geo-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly geo component. + + Returns: + The imports for the plotly geo component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly geo component. + + Returns: + The dynamic imports for the plotly geo component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-geo-dist-min") + + +class PlotlyGl3d(Plotly): + """Display a plotly 3d graph.""" + + tag: str = "Gl3dPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-gl3d-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly 3d component. + + Returns: + The imports for the plotly 3d component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly 3d component. + + Returns: + The dynamic imports for the plotly 3d component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-gl3d-dist-min") + + +class PlotlyGl2d(Plotly): + """Display a plotly 2d graph.""" + + tag: str = "Gl2dPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-gl2d-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly 2d component. + + Returns: + The imports for the plotly 2d component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly 2d component. + + Returns: + The dynamic imports for the plotly 2d component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-gl2d-dist-min") + + +class PlotlyMapbox(Plotly): + """Display a plotly mapbox graph.""" + + tag: str = "MapboxPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-mapbox-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly mapbox component. + + Returns: + The imports for the plotly mapbox component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly mapbox component. + + Returns: + The dynamic imports for the plotly mapbox component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-mapbox-dist-min") + + +class PlotlyFinance(Plotly): + """Display a plotly finance graph.""" + + tag: str = "FinancePlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-finance-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly finance component. + + Returns: + The imports for the plotly finance component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly finance component. + + Returns: + The dynamic imports for the plotly finance component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-finance-dist-min") + + +class PlotlyStrict(Plotly): + """Display a plotly strict graph.""" + + tag: str = "StrictPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-strict-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly strict component. + + Returns: + The imports for the plotly strict component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly strict component. + + Returns: + The dynamic imports for the plotly strict component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-strict-dist-min") diff --git a/reflex/components/plotly/plotly.pyi b/reflex/components/plotly/plotly.pyi index f60e5a6a4..c4d8bf64a 100644 --- a/reflex/components/plotly/plotly.pyi +++ b/reflex/components/plotly/plotly.pyi @@ -11,6 +11,7 @@ from reflex.components.component import NoSSRComponent from reflex.event import EventType from reflex.style import Style from reflex.utils import console +from reflex.utils.imports import ImportDict from reflex.vars.base import Var try: @@ -141,3 +142,767 @@ class Plotly(NoSSRComponent): The Plotly component. """ ... + +CREATE_PLOTLY_COMPONENT: ImportDict + +def dynamic_plotly_import(name: str, package: str) -> str: ... + +class PlotlyBasic(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyBasic": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyCartesian(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyCartesian": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyGeo(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyGeo": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyGl3d(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyGl3d": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyGl2d(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyGl2d": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyMapbox(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyMapbox": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyFinance(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyFinance": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyStrict(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyStrict": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... diff --git a/reflex/utils/imports.py b/reflex/utils/imports.py index 46e8e7362..66ae4b023 100644 --- a/reflex/utils/imports.py +++ b/reflex/utils/imports.py @@ -109,6 +109,9 @@ class ImportVar: # whether this import should be rendered or not render: Optional[bool] = True + # The path of the package to import from. + package_path: str = "/" + # whether this import package should be added to transpilePackages in next.config.js # https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages transpile: Optional[bool] = False From 3de04156e9da1ccbbbb80a7be170e9d7ffe319f4 Mon Sep 17 00:00:00 2001 From: Simon Young <40179067+Kastier1@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:20:35 -0800 Subject: [PATCH 09/38] allow gunicorn worker to be disabled (#4774) * allow gunicorn worker to be disabled * allow gunicorn worker to be disabled * rewrite the command --------- Co-authored-by: Khaleel Al-Adhami --- reflex/config.py | 2 +- reflex/utils/exec.py | 47 +++++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index f3d40dc37..dbc88619b 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -679,7 +679,7 @@ class Config(Base): # Number of gunicorn workers from user gunicorn_workers: Optional[int] = None - # Number of requests before a worker is restarted + # Number of requests before a worker is restarted; set to 0 to disable gunicorn_max_requests: int = 100 # Variance limit for max requests; gunicorn only diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 67df7ea91..de326dacc 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -368,34 +368,49 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel): app_module = get_app_module() - run_backend_prod = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split() - run_backend_prod_windows = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split() command = ( [ - *run_backend_prod_windows, - "--host", - host, - "--port", - str(port), + "uvicorn", + *( + [ + "--limit-max-requests", + str(config.gunicorn_max_requests), + ] + if config.gunicorn_max_requests > 0 + else [] + ), + *("--timeout-keep-alive", str(config.timeout)), + *("--host", host), + *("--port", str(port)), + *("--workers", str(_get_backend_workers())), app_module, ] if constants.IS_WINDOWS else [ - *run_backend_prod, - "--bind", - f"{host}:{port}", - "--threads", - str(_get_backend_workers()), + "gunicorn", + *("--worker-class", config.gunicorn_worker_class), + *( + [ + "--max-requests", + str(config.gunicorn_max_requests), + "--max-requests-jitter", + str(config.gunicorn_max_requests_jitter), + ] + if config.gunicorn_max_requests > 0 + else [] + ), + "--preload", + *("--timeout", str(config.timeout)), + *("--bind", f"{host}:{port}"), + *("--threads", str(_get_backend_workers())), f"{app_module}()", ] ) command += [ - "--log-level", - loglevel.value, - "--workers", - str(_get_backend_workers()), + *("--log-level", loglevel.value), ] + processes.new_process( command, run=True, From 8b2c7291d3ed605fc1cd82192555ac76ac8b76d3 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 7 Feb 2025 17:38:42 -0800 Subject: [PATCH 10/38] Add ComputedVar overloads for BASE_TYPE, SQLA_TYPE, and DATACLASS_TYPE (#4777) Allow typing to find __getattr__ for rx.var that returns an object-like model. --- reflex/vars/base.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 0d8af8f3c..a24db4010 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -2254,6 +2254,27 @@ class ComputedVar(Var[RETURN_TYPE]): owner: Type, ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + @overload + def __get__( + self: ComputedVar[BASE_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[BASE_TYPE]: ... + + @overload + def __get__( + self: ComputedVar[SQLA_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: ComputedVar[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... + @overload def __get__(self, instance: None, owner: Type) -> ComputedVar[RETURN_TYPE]: ... @@ -2500,6 +2521,27 @@ class AsyncComputedVar(ComputedVar[RETURN_TYPE]): owner: Type, ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + @overload + def __get__( + self: AsyncComputedVar[BASE_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[BASE_TYPE]: ... + + @overload + def __get__( + self: AsyncComputedVar[SQLA_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: AsyncComputedVar[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... + @overload def __get__(self, instance: None, owner: Type) -> AsyncComputedVar[RETURN_TYPE]: ... From 3a02d03cb1b9d78ff8ad221e95aa38fbe1e867f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Mon, 10 Feb 2025 20:44:44 +0100 Subject: [PATCH 11/38] fix bun path handling and add a test (#4785) * fix bun path handling and add a test * fix flags * fix tests * fix unit tests and mock object * fix units test again * revert some changes for now * remove unused test --- reflex/reflex.py | 21 ++++++++++++++------- reflex/utils/path_ops.py | 16 ++++++++++++++++ reflex/utils/prerequisites.py | 22 ++++++++++++++++++++-- tests/units/utils/test_utils.py | 19 ++++++++++++++++--- 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 70aa16a05..e4be0c89a 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -145,10 +145,7 @@ def _run( exec.output_system_info() # If no --frontend-only and no --backend-only, then turn on frontend and backend both - if not frontend and not backend: - frontend = True - backend = True - + frontend, backend = prerequisites.check_running_mode(frontend, backend) if not frontend and backend: _skip_compile() @@ -306,10 +303,18 @@ def export( True, "--no-zip", help="Disable zip for backend and frontend exports." ), frontend: bool = typer.Option( - True, "--backend-only", help="Export only backend.", show_default=False + False, + "--frontend-only", + help="Export only frontend.", + show_default=False, + envvar=environment.REFLEX_FRONTEND_ONLY.name, ), backend: bool = typer.Option( - True, "--frontend-only", help="Export only frontend.", show_default=False + False, + "--backend-only", + help="Export only backend.", + show_default=False, + envvar=environment.REFLEX_BACKEND_ONLY.name, ), zip_dest_dir: str = typer.Option( str(Path.cwd()), @@ -332,7 +337,9 @@ def export( from reflex.utils import export as export_utils from reflex.utils import prerequisites - if prerequisites.needs_reinit(frontend=True): + frontend, backend = prerequisites.check_running_mode(frontend, backend) + + if prerequisites.needs_reinit(frontend=frontend or not backend): _init(name=config.app_name, loglevel=loglevel) if frontend and not config.show_built_with_reflex: diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index 92557d801..dae938316 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -262,6 +262,22 @@ def find_replace(directory: str | Path, find: str, replace: str): filepath.write_text(text, encoding="utf-8") +def samefile(file1: Path, file2: Path) -> bool: + """Check if two files are the same. + + Args: + file1: The first file. + file2: The second file. + + Returns: + Whether the files are the same. If either file does not exist, returns False. + """ + if file1.exists() and file2.exists(): + return file1.samefile(file2) + + return False + + def update_directory_tree(src: Path, dest: Path): """Recursively copies a directory tree from src to dest. Only copies files if the destination file is missing or modified earlier than the source file. diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 6c6d34923..8047e1256 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1225,6 +1225,21 @@ def install_frontend_packages(packages: set[str], config: Config): ) +def check_running_mode(frontend: bool, backend: bool) -> tuple[bool, bool]: + """Check if the app is running in frontend or backend mode. + + Args: + frontend: Whether to run the frontend of the app. + backend: Whether to run the backend of the app. + + Returns: + The running modes. + """ + if not frontend and not backend: + return True, True + return frontend, backend + + def needs_reinit(frontend: bool = True) -> bool: """Check if an app needs to be reinitialized. @@ -1293,10 +1308,13 @@ def validate_bun(): """ bun_path = path_ops.get_bun_path() - if bun_path and not bun_path.samefile(constants.Bun.DEFAULT_PATH): + if bun_path is None: + return + + if not path_ops.samefile(bun_path, constants.Bun.DEFAULT_PATH): console.info(f"Using custom Bun path: {bun_path}") bun_version = get_bun_version() - if not bun_version: + if bun_version is None: console.error( "Failed to obtain bun version. Make sure the specified bun path in your config is correct." ) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 7cd53f14a..74dcf79b0 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -115,7 +115,20 @@ def test_typehint_issubclass(subclass, superclass, expected): assert types.typehint_issubclass(subclass, superclass) == expected -def test_validate_invalid_bun_path(mocker): +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(typer.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. @@ -123,13 +136,12 @@ def test_validate_invalid_bun_path(mocker): 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=None) with pytest.raises(typer.Exit): prerequisites.validate_bun() - mock_path.samefile.assert_called_once() def test_validate_bun_path_incompatible_version(mocker): @@ -141,6 +153,7 @@ def test_validate_bun_path_incompatible_version(mocker): 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"), From 85f07fcd89dcc17594ee6e5fad5424ba19019e8e Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 10 Feb 2025 12:22:37 -0800 Subject: [PATCH 12/38] Sticky tweaks: only show in prod mode (#4789) * Sticky tweaks: only show in prod mode Only display the sticky badge in prod mode. Display the mini-badge for mobile and tablet; full badge only displayed at desktop width. * Remove localhost checking --- reflex/app.py | 2 +- reflex/components/core/sticky.py | 32 ++++---------------------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 18cce69d2..7b7010521 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1040,7 +1040,7 @@ class App(MiddlewareMixin, LifespanMixin): self._validate_var_dependencies() self._setup_overlay_component() self._setup_error_boundary() - if config.show_built_with_reflex: + if is_prod_mode() and config.show_built_with_reflex: self._setup_sticky_badge() progress.advance(task) diff --git a/reflex/components/core/sticky.py b/reflex/components/core/sticky.py index cbcec00a9..b5dd4bcfd 100644 --- a/reflex/components/core/sticky.py +++ b/reflex/components/core/sticky.py @@ -2,14 +2,12 @@ from reflex.components.component import ComponentNamespace from reflex.components.core.colors import color -from reflex.components.core.cond import color_mode_cond, cond -from reflex.components.core.responsive import tablet_and_desktop +from reflex.components.core.cond import color_mode_cond +from reflex.components.core.responsive import desktop_only from reflex.components.el.elements.inline import A from reflex.components.el.elements.media import Path, Rect, Svg from reflex.components.radix.themes.typography.text import Text -from reflex.experimental.client_state import ClientStateVar from reflex.style import Style -from reflex.vars.base import Var, VarData class StickyLogo(Svg): @@ -87,7 +85,7 @@ class StickyBadge(A): """ return super().create( StickyLogo.create(), - tablet_and_desktop(StickyLabel.create()), + desktop_only(StickyLabel.create()), href="https://reflex.dev", target="_blank", width="auto", @@ -102,34 +100,12 @@ class StickyBadge(A): Returns: The style of the component. """ - is_localhost_cs = ClientStateVar.create( - "is_localhost", - default=True, - global_ref=False, - ) - localhost_hostnames = Var.create(["localhost", "127.0.0.1", "[::1]"]) - is_localhost_expr = localhost_hostnames.contains( - Var("window.location.hostname", _var_type=str).guess_type(), - ) - check_is_localhost = Var( - f"useEffect(({is_localhost_cs}) => {is_localhost_cs.set}({is_localhost_expr}), [])", - _var_data=VarData( - imports={"react": "useEffect"}, - ), - ) - is_localhost = is_localhost_cs.value._replace( - merge_var_data=VarData.merge( - check_is_localhost._get_all_var_data(), - VarData(hooks={str(check_is_localhost): None}), - ), - ) return Style( { "position": "fixed", "bottom": "1rem", "right": "1rem", - # Do not show the badge on localhost. - "display": cond(is_localhost, "none", "flex"), + "display": "flex", "flex-direction": "row", "gap": "0.375rem", "align-items": "center", From 90be6649811fcf9929bd897b35519cdbc8e931a8 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 11 Feb 2025 11:39:14 -0800 Subject: [PATCH 13/38] improve icon error message (#4796) --- reflex/components/lucide/icon.py | 14 +++++++++++--- reflex/utils/format.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/reflex/components/lucide/icon.py b/reflex/components/lucide/icon.py index 269ef7f79..ea6efe133 100644 --- a/reflex/components/lucide/icon.py +++ b/reflex/components/lucide/icon.py @@ -54,7 +54,7 @@ class Icon(LucideIconComponent): if "tag" not in props: raise AttributeError("Missing 'tag' keyword-argument for Icon") - tag: str | Var | LiteralVar = props.pop("tag") + tag: str | Var | LiteralVar = Var.create(props.pop("tag")) if isinstance(tag, LiteralVar): if isinstance(tag, LiteralStringVar): tag = tag._var_value @@ -70,9 +70,17 @@ class Icon(LucideIconComponent): not isinstance(tag, str) or format.to_snake_case(tag) not in LUCIDE_ICON_LIST ): + if isinstance(tag, str): + icons_sorted = sorted( + LUCIDE_ICON_LIST, + key=lambda s: format.length_of_largest_common_substring(tag, s), + reverse=True, + ) + else: + icons_sorted = LUCIDE_ICON_LIST raise ValueError( - f"Invalid icon tag: {tag}. Please use one of the following: {', '.join(LUCIDE_ICON_LIST[0:25])}, ..." - "\nSee full list at https://lucide.dev/icons." + f"Invalid icon tag: {tag}. Please use one of the following: {', '.join(icons_sorted[0:25])}, ..." + "\nSee full list at https://reflex.dev/docs/library/data-display/icon/#icons-list." ) if tag in LUCIDE_ICON_MAPPING_OVERRIDE: diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 225d52f3a..214c845f8 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -27,6 +27,36 @@ WRAP_MAP = { } +def length_of_largest_common_substring(str1: str, str2: str) -> int: + """Find the length of the largest common substring between two strings. + + Args: + str1: The first string. + str2: The second string. + + Returns: + The length of the largest common substring. + """ + if not str1 or not str2: + return 0 + + # Create a matrix of size (len(str1) + 1) x (len(str2) + 1) + dp = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)] + + # Variables to keep track of maximum length and ending position + max_length = 0 + + # Fill the dp matrix + for i in range(1, len(str1) + 1): + for j in range(1, len(str2) + 1): + if str1[i - 1] == str2[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + if dp[i][j] > max_length: + max_length = dp[i][j] + + return max_length + + def get_close_char(open: str, close: str | None = None) -> str: """Check if the given character is a valid brace. From a194c90d6f86bac0e049b83cc3a74812326b8d0a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 11 Feb 2025 11:39:28 -0800 Subject: [PATCH 14/38] improve hot reload handling (#4795) --- reflex/config.py | 22 +++++++++++++++- reflex/utils/exec.py | 53 +++++++++++++++++++++++++++++++------- tests/units/test_config.py | 9 +++++++ 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index dbc88619b..f5f000e82 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -23,6 +23,7 @@ from typing import ( Set, TypeVar, get_args, + get_origin, ) from typing_extensions import Annotated, get_type_hints @@ -304,6 +305,15 @@ def interpret_env_var_value( return interpret_path_env(value, field_name) elif field_type is ExistingPath: return interpret_existing_path_env(value, field_name) + elif get_origin(field_type) is list: + return [ + interpret_env_var_value( + v, + get_args(field_type)[0], + f"{field_name}[{i}]", + ) + for i, v in enumerate(value.split(":")) + ] elif inspect.isclass(field_type) and issubclass(field_type, enum.Enum): return interpret_enum_env(value, field_type, field_name) @@ -387,7 +397,11 @@ class EnvVar(Generic[T]): else: if isinstance(value, enum.Enum): value = value.value - os.environ[self.name] = str(value) + if isinstance(value, list): + str_value = ":".join(str(v) for v in value) + else: + str_value = str(value) + os.environ[self.name] = str_value class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] @@ -571,6 +585,12 @@ class EnvironmentVariables: # Whether to use the turbopack bundler. REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(True) + # Additional paths to include in the hot reload. Separated by a colon. + REFLEX_HOT_RELOAD_INCLUDE_PATHS: EnvVar[List[Path]] = env_var([]) + + # Paths to exclude from the hot reload. Takes precedence over include paths. Separated by a colon. + REFLEX_HOT_RELOAD_EXCLUDE_PATHS: EnvVar[List[Path]] = env_var([]) + environment = EnvironmentVariables() diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index de326dacc..7318b7ff6 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -10,6 +10,7 @@ import re import subprocess import sys from pathlib import Path +from typing import Sequence from urllib.parse import urljoin import psutil @@ -242,14 +243,14 @@ def run_backend( run_uvicorn_backend(host, port, loglevel) -def get_reload_dirs() -> list[Path]: - """Get the reload directories for the backend. +def get_reload_paths() -> Sequence[Path]: + """Get the reload paths for the backend. Returns: - The reload directories for the backend. + The reload paths for the backend. """ config = get_config() - reload_dirs = [Path(config.app_name)] + reload_paths = [Path(config.app_name).parent] if config.app_module is not None and config.app_module.__file__: module_path = Path(config.app_module.__file__).resolve().parent @@ -263,8 +264,43 @@ def get_reload_dirs() -> list[Path]: else: break - reload_dirs = [module_path] - return reload_dirs + reload_paths = [module_path] + + include_dirs = tuple( + map(Path.absolute, environment.REFLEX_HOT_RELOAD_INCLUDE_PATHS.get()) + ) + exclude_dirs = tuple( + map(Path.absolute, environment.REFLEX_HOT_RELOAD_EXCLUDE_PATHS.get()) + ) + + def is_excluded_by_default(path: Path) -> bool: + if path.is_dir(): + if path.name.startswith("."): + # exclude hidden directories + return True + if path.name.startswith("__"): + # ignore things like __pycache__ + return True + return path.name not in (".gitignore", "uploaded_files") + + reload_paths = ( + tuple( + path.absolute() + for dir in reload_paths + for path in dir.iterdir() + if not is_excluded_by_default(path) + ) + + include_dirs + ) + + if exclude_dirs: + reload_paths = tuple( + path + for path in reload_paths + if all(not path.samefile(exclude) for exclude in exclude_dirs) + ) + + return reload_paths def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel): @@ -283,7 +319,7 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel): port=port, log_level=loglevel.value, reload=True, - reload_dirs=list(map(str, get_reload_dirs())), + reload_dirs=list(map(str, get_reload_paths())), ) @@ -310,8 +346,7 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel): interface=Interfaces.ASGI, log_level=LogLevels(loglevel.value), reload=True, - reload_paths=get_reload_dirs(), - reload_ignore_dirs=[".web", ".states"], + reload_paths=get_reload_paths(), ).serve() except ImportError: console.error( diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 88d8b5f2f..18d8cd90c 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -252,6 +252,7 @@ def test_env_var(): BLUBB: EnvVar[str] = env_var("default") INTERNAL: EnvVar[str] = env_var("default", internal=True) BOOLEAN: EnvVar[bool] = env_var(False) + LIST: EnvVar[list[int]] = env_var([1, 2, 3]) assert TestEnv.BLUBB.get() == "default" assert TestEnv.BLUBB.name == "BLUBB" @@ -280,3 +281,11 @@ def test_env_var(): assert TestEnv.BOOLEAN.get() is False TestEnv.BOOLEAN.set(None) assert "BOOLEAN" not in os.environ + + assert TestEnv.LIST.get() == [1, 2, 3] + assert TestEnv.LIST.name == "LIST" + TestEnv.LIST.set([4, 5, 6]) + assert os.environ.get("LIST") == "4:5:6" + assert TestEnv.LIST.get() == [4, 5, 6] + TestEnv.LIST.set(None) + assert "LIST" not in os.environ From d545ee3f0baa036ecb34e8698b50615984d7c2c4 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 11 Feb 2025 11:39:38 -0800 Subject: [PATCH 15/38] move overlays to _app.js (#4794) * move overlays to _app.js * fix unit tests * fix dynamic imports app * fix unit cases once again * clear custom compoent cache between app harness tests --- reflex/app.py | 92 ++++++++++++++++++++++------------ reflex/compiler/compiler.py | 1 + reflex/components/component.py | 4 +- reflex/testing.py | 2 + reflex/utils/exec.py | 15 +++--- tests/units/test_app.py | 9 ++-- 6 files changed, 76 insertions(+), 47 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 7b7010521..281727b3f 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -164,11 +164,11 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: return window_alert("\n".join(error_message)) -def default_overlay_component() -> Component: - """Default overlay_component attribute for App. +def extra_overlay_function() -> Optional[Component]: + """Extra overlay function to add to the overlay component. Returns: - The default overlay_component, which is a connection_modal. + The extra overlay function. """ config = get_config() @@ -178,7 +178,8 @@ def default_overlay_component() -> Component: module, _, function_name = extra_config.rpartition(".") try: module = __import__(module) - config_overlay = getattr(module, function_name)() + config_overlay = Fragment.create(getattr(module, function_name)()) + config_overlay._get_all_imports() except Exception as e: from reflex.compiler.utils import save_error @@ -188,13 +189,27 @@ def default_overlay_component() -> Component: f"Error loading extra_overlay_function {extra_config}. Error saved to {log_path}" ) - return Fragment.create( - connection_pulser(), - connection_toaster(), - *([config_overlay] if config_overlay else []), - *([backend_disabled()] if config.is_reflex_cloud else []), - *codespaces.codespaces_auto_redirect(), - ) + return config_overlay + + +def default_overlay_component() -> Component: + """Default overlay_component attribute for App. + + Returns: + The default overlay_component, which is a connection_modal. + """ + config = get_config() + from reflex.components.component import memo + + def default_overlay_components(): + return Fragment.create( + connection_pulser(), + connection_toaster(), + *([backend_disabled()] if config.is_reflex_cloud else []), + *codespaces.codespaces_auto_redirect(), + ) + + return Fragment.create(memo(default_overlay_components)()) def default_error_boundary(*children: Component) -> Component: @@ -266,11 +281,26 @@ class App(MiddlewareMixin, LifespanMixin): # A component that is present on every page (defaults to the Connection Error banner). overlay_component: Optional[Union[Component, ComponentCallable]] = ( - dataclasses.field(default_factory=default_overlay_component) + dataclasses.field(default=None) ) # Error boundary component to wrap the app with. - error_boundary: Optional[ComponentCallable] = default_error_boundary + error_boundary: Optional[ComponentCallable] = dataclasses.field(default=None) + + # App wraps to be applied to the whole app. Expected to be a dictionary of (order, name) to a function that takes whether the state is enabled and optionally returns a component. + app_wraps: Dict[tuple[int, str], Callable[[bool], Optional[Component]]] = ( + dataclasses.field( + default_factory=lambda: { + (55, "ErrorBoundary"): ( + lambda stateful: default_error_boundary() if stateful else None + ), + (5, "Overlay"): ( + lambda stateful: default_overlay_component() if stateful else None + ), + (4, "ExtraOverlay"): lambda stateful: extra_overlay_function(), + } + ) + ) # Components to add to the head of every page. head_components: List[Component] = dataclasses.field(default_factory=list) @@ -880,25 +910,6 @@ class App(MiddlewareMixin, LifespanMixin): for k, component in self._pages.items(): self._pages[k] = self._add_overlay_to_component(component) - def _add_error_boundary_to_component(self, component: Component) -> Component: - if self.error_boundary is None: - return component - - component = self.error_boundary(*component.children) - - return component - - def _setup_error_boundary(self): - """If a State is not used and no error_boundary is specified, do not render the error boundary.""" - if self._state is None and self.error_boundary is default_error_boundary: - self.error_boundary = None - - for k, component in self._pages.items(): - # Skip the 404 page - if k == constants.Page404.SLUG: - continue - self._pages[k] = self._add_error_boundary_to_component(component) - def _setup_sticky_badge(self): """Add the sticky badge to the app.""" for k, component in self._pages.items(): @@ -1039,7 +1050,6 @@ class App(MiddlewareMixin, LifespanMixin): self._validate_var_dependencies() self._setup_overlay_component() - self._setup_error_boundary() if is_prod_mode() and config.show_built_with_reflex: self._setup_sticky_badge() @@ -1066,6 +1076,22 @@ class App(MiddlewareMixin, LifespanMixin): # Add the custom components from the page to the set. custom_components |= component._get_all_custom_components() + # Add the app wraps to the app. + for key, app_wrap in self.app_wraps.items(): + component = app_wrap(self._state is not None) + if component is not None: + app_wrappers[key] = component + custom_components |= component._get_all_custom_components() + + if self.error_boundary: + console.deprecate( + feature_name="App.error_boundary", + reason="Use app_wraps instead.", + deprecation_version="0.7.1", + removal_version="0.8.0", + ) + app_wrappers[(55, "ErrorBoundary")] = self.error_boundary() + # Perform auto-memoization of stateful components. with console.timing("Auto-memoize StatefulComponents"): ( diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index c2a76aad3..7cd87fb71 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -78,6 +78,7 @@ def _compile_app(app_root: Component) -> str: hooks=app_root._get_all_hooks(), window_libraries=window_libraries, render=app_root.render(), + dynamic_imports=app_root._get_all_dynamic_imports(), ) diff --git a/reflex/components/component.py b/reflex/components/component.py index 6e4c6c37f..9466933c5 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -23,6 +23,8 @@ from typing import ( Union, ) +from typing_extensions import Self + import reflex.state from reflex.base import Base from reflex.compiler.templates import STATEFUL_COMPONENT @@ -685,7 +687,7 @@ class Component(BaseComponent, ABC): } @classmethod - def create(cls, *children, **props) -> Component: + def create(cls, *children, **props) -> Self: """Create the component. Args: diff --git a/reflex/testing.py b/reflex/testing.py index 25f9e7aac..e463ddea7 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -43,6 +43,7 @@ import reflex.utils.exec import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes +from reflex.components.component import CustomComponent from reflex.config import environment from reflex.state import ( BaseState, @@ -254,6 +255,7 @@ class AppHarness: # disable telemetry reporting for tests os.environ["TELEMETRY_ENABLED"] = "false" + CustomComponent.create().get_component.cache_clear() self.app_path.mkdir(parents=True, exist_ok=True) if self.app_source is not None: app_globals = self._get_globals_from_signature(self.app_source) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 7318b7ff6..3e729cbf8 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -254,15 +254,12 @@ def get_reload_paths() -> Sequence[Path]: if config.app_module is not None and config.app_module.__file__: module_path = Path(config.app_module.__file__).resolve().parent - while module_path.parent.name: - if any( - sibling_file.name == "__init__.py" - for sibling_file in module_path.parent.iterdir() - ): - # go up a level to find dir without `__init__.py` - module_path = module_path.parent - else: - break + while module_path.parent.name and any( + sibling_file.name == "__init__.py" + for sibling_file in module_path.parent.iterdir() + ): + # go up a level to find dir without `__init__.py` + module_path = module_path.parent reload_paths = [module_path] diff --git a/tests/units/test_app.py b/tests/units/test_app.py index ae5a01c1a..5d25b09ac 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1299,6 +1299,7 @@ def test_app_wrap_compile_theme( app_js_lines = [ line.strip() for line in app_js_contents.splitlines() if line.strip() ] + lines = "".join(app_js_lines) assert ( "function AppWrap({children}) {" "return (" @@ -1313,7 +1314,7 @@ def test_app_wrap_compile_theme( + ("" if react_strict_mode else "") + ")" "}" - ) in "".join(app_js_lines) + ) in lines @pytest.mark.parametrize( @@ -1362,6 +1363,7 @@ def test_app_wrap_priority( app_js_lines = [ line.strip() for line in app_js_contents.splitlines() if line.strip() ] + lines = "".join(app_js_lines) assert ( "function AppWrap({children}) {" "return (" + ("" if react_strict_mode else "") + "" @@ -1374,9 +1376,8 @@ def test_app_wrap_priority( "" "" "" - "" + ("" if react_strict_mode else "") + ")" - "}" - ) in "".join(app_js_lines) + "" + ("" if react_strict_mode else "") + ) in lines def test_app_state_determination(): From 372bd22475a4f6d1a6c10d5a4091170b0b45ff18 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 11 Feb 2025 11:39:55 -0800 Subject: [PATCH 16/38] raise error when passing a str(var) (#4769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * raise error when passing a str(var) * make it faster * fix typo * fix tests * mocker consistency Co-authored-by: Thomas Brandého * ditto Co-authored-by: Thomas Brandého --------- Co-authored-by: Thomas Brandého --- reflex/components/base/bare.py | 40 ++++++++++++++++++++++++++++++++++ reflex/config.py | 2 +- reflex/utils/decorator.py | 25 +++++++++++++++++++++ tests/units/test_var.py | 25 +++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 reflex/utils/decorator.py diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index 0f0bef8b9..73b0680d3 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -7,9 +7,44 @@ from typing import Any, Iterator from reflex.components.component import Component, LiteralComponentVar from reflex.components.tags import Tag from reflex.components.tags.tagless import Tagless +from reflex.config import PerformanceMode, environment +from reflex.utils import console +from reflex.utils.decorator import once from reflex.utils.imports import ParsedImportDict from reflex.vars import BooleanVar, ObjectVar, Var from reflex.vars.base import VarData +from reflex.vars.sequence import LiteralStringVar + + +@once +def get_performance_mode(): + """Get the performance mode. + + Returns: + The performance mode. + """ + return environment.REFLEX_PERF_MODE.get() + + +def validate_str(value: str): + """Validate a string value. + + Args: + value: The value to validate. + + Raises: + ValueError: If the value is a Var and the performance mode is set to raise. + """ + perf_mode = get_performance_mode() + if perf_mode != PerformanceMode.OFF and value.startswith("reflex___state"): + if perf_mode == PerformanceMode.WARN: + console.warn( + f"Output includes {value!s} which will be displayed as a string. If you are calling `str` on a Var, consider using .to_string() instead." + ) + elif perf_mode == PerformanceMode.RAISE: + raise ValueError( + f"Output includes {value!s} which will be displayed as a string. If you are calling `str` on a Var, consider using .to_string() instead." + ) class Bare(Component): @@ -28,9 +63,14 @@ class Bare(Component): The component. """ if isinstance(contents, Var): + if isinstance(contents, LiteralStringVar): + validate_str(contents._var_value) return cls(contents=contents) else: + if isinstance(contents, str): + validate_str(contents) contents = str(contents) if contents is not None else "" + return cls(contents=contents) def _get_all_hooks_internal(self) -> dict[str, VarData | None]: diff --git a/reflex/config.py b/reflex/config.py index f5f000e82..8a062f4e6 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -577,7 +577,7 @@ class EnvironmentVariables: REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True) # In which performance mode to run the app. - REFLEX_PERF_MODE: EnvVar[Optional[PerformanceMode]] = env_var(PerformanceMode.WARN) + REFLEX_PERF_MODE: EnvVar[PerformanceMode] = env_var(PerformanceMode.WARN) # The maximum size of the reflex state in kilobytes. REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) diff --git a/reflex/utils/decorator.py b/reflex/utils/decorator.py new file mode 100644 index 000000000..5c9c0bf3a --- /dev/null +++ b/reflex/utils/decorator.py @@ -0,0 +1,25 @@ +"""Decorator utilities.""" + +from typing import Callable, TypeVar + +T = TypeVar("T") + + +def once(f: Callable[[], T]) -> Callable[[], T]: + """A decorator that calls the function once and caches the result. + + Args: + f: The function to call. + + Returns: + A function that calls the function once and caches the result. + """ + unset = object() + value: object | T = unset + + def wrapper() -> T: + nonlocal value + value = f() if value is unset else value + return value # pyright: ignore[reportReturnType] + + return wrapper diff --git a/tests/units/test_var.py b/tests/units/test_var.py index a72242814..8fcd288e6 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -8,6 +8,7 @@ from pandas import DataFrame import reflex as rx from reflex.base import Base +from reflex.config import PerformanceMode from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG from reflex.state import BaseState from reflex.utils.exceptions import ( @@ -1893,3 +1894,27 @@ def test_var_data_hooks(): def test_var_data_with_hooks_value(): var_data = VarData(hooks={"what": VarData(hooks={"whot": VarData(hooks="whott")})}) assert var_data == VarData(hooks=["what", "whot", "whott"]) + + +def test_str_var_in_components(mocker): + class StateWithVar(rx.State): + field: int = 1 + + mocker.patch( + "reflex.components.base.bare.get_performance_mode", + return_value=PerformanceMode.RAISE, + ) + + with pytest.raises(ValueError): + rx.vstack( + str(StateWithVar.field), + ) + + mocker.patch( + "reflex.components.base.bare.get_performance_mode", + return_value=PerformanceMode.OFF, + ) + + rx.vstack( + str(StateWithVar.field), + ) From 64b1630d02562e4fdb3e2cdda2f15b32ef3bfb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Tue, 11 Feb 2025 21:15:38 +0100 Subject: [PATCH 17/38] set global loglevel for subprocesses (#4791) --- reflex/utils/console.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/reflex/utils/console.py b/reflex/utils/console.py index 5c47eee6f..70bbb0e82 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -4,6 +4,7 @@ from __future__ import annotations import contextlib import inspect +import os import shutil import time from pathlib import Path @@ -60,6 +61,9 @@ def set_log_level(log_level: LogLevel): f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead." ) global _LOG_LEVEL + if log_level != _LOG_LEVEL: + # Set the loglevel persistenly for subprocesses. + os.environ["LOGLEVEL"] = log_level.value _LOG_LEVEL = log_level From 894a01a5a5cb2c75622e6fdcf71caedd029d5810 Mon Sep 17 00:00:00 2001 From: Declan Brady <36574477+drbrady8800@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:21:27 -0500 Subject: [PATCH 18/38] Add toast.loading from the sonner package (#4792) --- reflex/components/sonner/toast.py | 14 ++++++++++++++ reflex/components/sonner/toast.pyi | 3 +++ 2 files changed, 17 insertions(+) diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index dbac8e733..e215f356f 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -327,6 +327,19 @@ class Toaster(Component): """ return Toaster.send_toast(message, level="success", **kwargs) + @staticmethod + def toast_loading(message: str | Var = "", **kwargs: Any): + """Display a loading toast message. + + Args: + message: The message to display. + **kwargs: Additional toast props. + + Returns: + The toast event. + """ + return Toaster.send_toast(message, level="loading", **kwargs) + @staticmethod def toast_dismiss(id: Var | str | None = None): """Dismiss a toast. @@ -378,6 +391,7 @@ class ToastNamespace(ComponentNamespace): warning = staticmethod(Toaster.toast_warning) error = staticmethod(Toaster.toast_error) success = staticmethod(Toaster.toast_success) + loading = staticmethod(Toaster.toast_loading) dismiss = staticmethod(Toaster.toast_dismiss) __call__ = staticmethod(Toaster.send_toast) diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index cb12834d5..7ff0b9196 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -70,6 +70,8 @@ class Toaster(Component): @staticmethod def toast_success(message: str | Var = "", **kwargs: Any): ... @staticmethod + def toast_loading(message: str | Var = "", **kwargs: Any): ... + @staticmethod def toast_dismiss(id: Var | str | None = None): ... @overload @classmethod @@ -172,6 +174,7 @@ class ToastNamespace(ComponentNamespace): warning = staticmethod(Toaster.toast_warning) error = staticmethod(Toaster.toast_error) success = staticmethod(Toaster.toast_success) + loading = staticmethod(Toaster.toast_loading) dismiss = staticmethod(Toaster.toast_dismiss) @staticmethod From e5e6c4e1d77c42073e5e7fdfb10f0351946960c5 Mon Sep 17 00:00:00 2001 From: Simon Young <40179067+Kastier1@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:49:06 -0800 Subject: [PATCH 19/38] Create codeql.yml (#4799) * Create codeql.yml * add config * fix that guy who's mad --------- Co-authored-by: Khaleel Al-Adhami --- .github/codeql-config.yml | 2 + .github/workflows/codeql.yml | 101 ++++++++++++++++++++++++++++++++++ reflex/utils/prerequisites.py | 7 ++- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 .github/codeql-config.yml create mode 100644 .github/workflows/codeql.yml diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml new file mode 100644 index 000000000..f4091ea39 --- /dev/null +++ b/.github/codeql-config.yml @@ -0,0 +1,2 @@ +paths-ignore: + - "**/tests/**" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..18d826fe2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,101 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + schedule: + - cron: "36 7 * * 4" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + config-file: .github/codeql-config.yml + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 8047e1256..3cd65a7eb 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -24,6 +24,7 @@ from datetime import datetime from pathlib import Path from types import ModuleType from typing import Any, Callable, List, NamedTuple, Optional +from urllib.parse import urlparse import httpx import typer @@ -1679,9 +1680,11 @@ def validate_and_create_app_using_remote_template( template_url = templates[template].code_url else: + template_parsed_url = urlparse(template) # Check if the template is a github repo. - if template.startswith("https://github.com"): - template_url = f"{template.strip('/').replace('.git', '')}/archive/main.zip" + if template_parsed_url.hostname == "github.com": + path = template_parsed_url.path.strip("/").removesuffix(".git") + template_url = f"https://github.com/{path}/archive/main.zip" else: console.error(f"Template `{template}` not found or invalid.") raise typer.Exit(1) From 289d10d30e1b52cd2be36be9d629c1c67add3498 Mon Sep 17 00:00:00 2001 From: Simon Young <40179067+Kastier1@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:04:03 -0800 Subject: [PATCH 20/38] test actions in codeql (#4802) --- .github/workflows/codeql.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 18d826fe2..6b2a54dd5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,6 +47,8 @@ jobs: build-mode: none - language: python build-mode: none + - language: actions + build-mode: none # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both From 6cbdd0016985f0f7eadce725faf3bb2dddadbe49 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 11 Feb 2025 16:46:06 -0800 Subject: [PATCH 21/38] fix toast provider needed (#4801) * fix toast provider needed * fix tests --- reflex/app.py | 25 ++++++++++++++++++++++++- reflex/components/component.py | 3 --- reflex/components/sonner/toast.py | 6 +++++- reflex/components/sonner/toast.pyi | 8 +++++++- tests/units/test_app.py | 6 ++++++ 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 281727b3f..d290b8f49 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -355,6 +355,9 @@ class App(MiddlewareMixin, LifespanMixin): [Exception], Union[EventSpec, List[EventSpec], None] ] = default_backend_exception_handler + # Put the toast provider in the app wrap. + bundle_toaster: bool = True + @property def api(self) -> FastAPI | None: """Get the backend api. @@ -1010,6 +1013,10 @@ class App(MiddlewareMixin, LifespanMixin): should_compile = self._should_compile() if not should_compile: + if self.bundle_toaster: + from reflex.components.sonner.toast import Toaster + + Toaster.is_used = True with console.timing("Evaluate Pages (Backend)"): for route in self._unevaluated_pages: console.debug(f"Evaluating page: {route}") @@ -1039,6 +1046,20 @@ class App(MiddlewareMixin, LifespanMixin): + adhoc_steps_without_executor, ) + if self.bundle_toaster: + from reflex.components.component import memo + from reflex.components.sonner.toast import toast + + internal_toast_provider = toast.provider() + + @memo + def memoized_toast_provider(): + return internal_toast_provider + + toast_provider = Fragment.create(memoized_toast_provider()) + + app_wrappers[(1, "ToasterProvider")] = toast_provider + with console.timing("Evaluate Pages (Frontend)"): for route in self._unevaluated_pages: console.debug(f"Evaluating page: {route}") @@ -1081,7 +1102,9 @@ class App(MiddlewareMixin, LifespanMixin): component = app_wrap(self._state is not None) if component is not None: app_wrappers[key] = component - custom_components |= component._get_all_custom_components() + + for component in app_wrappers.values(): + custom_components |= component._get_all_custom_components() if self.error_boundary: console.deprecate( diff --git a/reflex/components/component.py b/reflex/components/component.py index 9466933c5..d27bddf78 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -1792,9 +1792,6 @@ class CustomComponent(Component): include_children=include_children, ignore_ids=ignore_ids ) yield from filter(lambda prop: isinstance(prop, Var), self.props.values()) - yield from self.get_component(self)._get_vars( - include_children=include_children, ignore_ids=ignore_ids - ) @lru_cache(maxsize=None) # noqa: B019 def get_component(self) -> Component: diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index e215f356f..d1f9464d8 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -8,6 +8,7 @@ from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps, PropsBase +from reflex.constants.base import Dirs from reflex.event import EventSpec, run_script from reflex.style import Style, resolved_color_mode from reflex.utils import format @@ -27,7 +28,10 @@ LiteralPosition = Literal[ "bottom-right", ] -toast_ref = Var(_js_expr="refs['__toast']") +toast_ref = Var( + _js_expr="refs['__toast']", + _var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}), +) class ToastAction(Base): diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 7ff0b9196..cb637bfff 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -9,9 +9,12 @@ from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps, PropsBase +from reflex.constants.base import Dirs from reflex.event import EventSpec, EventType from reflex.style import Style +from reflex.utils.imports import ImportVar from reflex.utils.serializers import serializer +from reflex.vars import VarData from reflex.vars.base import Var LiteralPosition = Literal[ @@ -22,7 +25,10 @@ LiteralPosition = Literal[ "bottom-center", "bottom-right", ] -toast_ref = Var(_js_expr="refs['__toast']") +toast_ref = Var( + _js_expr="refs['__toast']", + _var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}), +) class ToastAction(Base): label: str diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 5d25b09ac..88cb36509 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1307,8 +1307,11 @@ def test_app_wrap_compile_theme( + "" "" "" + "" + "" "{children}" "" + "" "" "" + ("" if react_strict_mode else "") @@ -1371,8 +1374,11 @@ def test_app_wrap_priority( "" "" "" + "" + "" "{children}" "" + "" "" "" "" From cb2e7df96a065a7ad15115b1137b4ec01039b7ea Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 11 Feb 2025 17:47:44 -0800 Subject: [PATCH 22/38] invert logic of default hot reload exclusion (#4807) * invert logic of default hot reload exclusion * console debug reload paths --- reflex/utils/exec.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 3e729cbf8..b16aaea1c 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -278,7 +278,7 @@ def get_reload_paths() -> Sequence[Path]: if path.name.startswith("__"): # ignore things like __pycache__ return True - return path.name not in (".gitignore", "uploaded_files") + return path.name in (".gitignore", "uploaded_files") reload_paths = ( tuple( @@ -297,6 +297,8 @@ def get_reload_paths() -> Sequence[Path]: if all(not path.samefile(exclude) for exclude in exclude_dirs) ) + console.debug(f"Reload paths: {list(map(str, reload_paths))}") + return reload_paths From 3f68a27a22b8e653038a43421cbf83b672d8af89 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 11 Feb 2025 18:05:33 -0800 Subject: [PATCH 23/38] [ENG-4647] Fix env_file handling (#4805) * [ENG-4647] Fix env_file handling * Import dotenv.load_dotenv early to avoid ImportError while loading rxconfig.py * Read ENV_FILE from the environment explicitly. fix #4803 * Config.Config: use_enum_values = False Save enum fields as the enum object rather than the value. --- reflex/config.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index 8a062f4e6..21614b9b1 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -26,8 +26,12 @@ from typing import ( get_origin, ) +from reflex_cli.constants.hosting import Hosting from typing_extensions import Annotated, get_type_hints +from reflex import constants +from reflex.base import Base +from reflex.utils import console from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError from reflex.utils.types import GenericType, is_union, value_inside_optional @@ -36,11 +40,11 @@ try: except ModuleNotFoundError: import pydantic -from reflex_cli.constants.hosting import Hosting -from reflex import constants -from reflex.base import Base -from reflex.utils import console +try: + from dotenv import load_dotenv # pyright: ignore [reportMissingImports] +except ImportError: + load_dotenv = None class DBConfig(Base): @@ -624,6 +628,7 @@ class Config(Base): """Pydantic config for the config.""" validate_assignment = True + use_enum_values = False # The name of the app (should match the name of the app directory). app_name: str @@ -754,6 +759,9 @@ class Config(Base): self._non_default_attributes.update(kwargs) self._replace_defaults(**kwargs) + # Set the log level for this process + console.set_log_level(self.loglevel) + if ( self.state_manager_mode == constants.StateManagerMode.REDIS and not self.redis_url @@ -793,16 +801,15 @@ class Config(Base): Returns: The updated config values. """ - if self.env_file: - try: - from dotenv import load_dotenv # pyright: ignore [reportMissingImports] - - # load env file if exists - load_dotenv(self.env_file, override=True) - except ImportError: + env_file = self.env_file or os.environ.get("ENV_FILE", None) + if env_file: + if load_dotenv is None: console.error( """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`.""" ) + else: + # load env file if exists + load_dotenv(env_file, override=True) updated_values = {} # Iterate over the fields. From 7da96a1175b02b6a62fe78ffe92604a5f4053bdd Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 11 Feb 2025 18:41:04 -0800 Subject: [PATCH 24/38] pyproject.toml: bump to 0.7.1 for further development (#4808) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b54f578fd..c192a1139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "reflex" -version = "0.7.0dev1" +version = "0.7.1dev1" description = "Web apps in pure Python." license = "Apache-2.0" authors = [ From a31301cb4f85e7030e641675df57c1f14fc30986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 12 Feb 2025 03:51:05 +0100 Subject: [PATCH 25/38] add stateful benchmarks (#4764) * add stateful benchmarks * make stateful stuff more complex * unpack tuple in itertag * fix comment --- reflex/components/tags/iter_tag.py | 4 + tests/benchmarks/conftest.py | 4 +- tests/benchmarks/fixtures.py | 147 ++++++++++++++++++++++++++++- tests/benchmarks/test_evaluate.py | 7 +- 4 files changed, 154 insertions(+), 8 deletions(-) diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index cb02ca000..221b65ca9 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -134,6 +134,10 @@ class IterTag(Tag): if isinstance(component, (Foreach, Cond)): component = Fragment.create(component) + # If the component is a tuple, unpack and wrap it in a fragment. + if isinstance(component, tuple): + component = Fragment.create(*component) + # Set the component key. if component.key is None: component.key = index diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py index bf777be2e..ff8f01f3a 100644 --- a/tests/benchmarks/conftest.py +++ b/tests/benchmarks/conftest.py @@ -1,3 +1,3 @@ -from .fixtures import evaluated_page +from .fixtures import evaluated_page, unevaluated_page -__all__ = ["evaluated_page"] +__all__ = ["evaluated_page", "unevaluated_page"] diff --git a/tests/benchmarks/fixtures.py b/tests/benchmarks/fixtures.py index d9c0d7688..16233100a 100644 --- a/tests/benchmarks/fixtures.py +++ b/tests/benchmarks/fixtures.py @@ -213,6 +213,75 @@ def side_bar(): ) +class NestedElement(rx.Base): + """A nested element.""" + + identifier: str + value: list[int] + + +class BenchmarkState(rx.State): + """State for the benchmark.""" + + counter: rx.Field[int] = rx.field(17) + + current_key: rx.Field[str] = rx.field("key_2") + + @rx.event + def increment(self): + """Increment the counter.""" + self.counter = self.counter + 1 + + @rx.event + def decrement(self): + """Decrement the counter.""" + self.counter = self.counter - 1 + + @rx.var + def elements(self) -> list[int]: + """List of elements. + + Returns: + List of elements. + """ + if self.counter < 0: + return list(range(0)) + return list(range(self.counter)) + + @rx.var + def nested_elements(self) -> list[NestedElement]: + """List of nested elements. + + Returns: + List of nested elements. + """ + return [ + NestedElement( + identifier=str(i), + value=list(range(i)), + ) + for i in range(self.counter) + ] + + @rx.var + def show_odd(self) -> bool: + """Check if the counter is odd. + + Returns: + True if the counter is odd, False otherwise. + """ + return self.counter % 2 == 1 + + @rx.var + def show_even(self) -> bool: + """Check if the counter is even. + + Returns: + True if the counter is even, False otherwise. + """ + return self.counter % 2 == 0 + + LOREM_IPSUM = "Lorem ipsum dolor sit amet, dolor ut dolore pariatur aliqua enim tempor sed. Labore excepteur sed exercitation. Ullamco aliquip lorem sunt enim in incididunt. Magna anim officia sint cillum labore. Ut eu non dolore minim nostrud magna eu, aute ex in incididunt irure eu. Fugiat et magna magna est excepteur eiusmod minim. Quis eiusmod et non pariatur dolor veniam incididunt, eiusmod irure enim sed dolor lorem pariatur do. Occaecat duis irure excepteur dolore. Proident ut laborum pariatur sit sit, nisi nostrud voluptate magna commodo laborum esse velit. Voluptate non minim deserunt adipiscing irure deserunt cupidatat. Laboris veniam commodo incididunt veniam lorem occaecat, fugiat ipsum dolor cupidatat. Ea officia sed eu excepteur culpa adipiscing, tempor consectetur ullamco eu. Anim ex proident nulla sunt culpa, voluptate veniam proident est adipiscing sint elit velit. Laboris adipiscing est culpa cillum magna. Sit veniam nulla nulla, aliqua eiusmod commodo lorem cupidatat commodo occaecat. Fugiat cillum dolor incididunt mollit eiusmod sint. Non lorem dolore labore excepteur minim laborum sed. Irure nisi do lorem nulla sunt commodo, deserunt quis mollit consectetur minim et esse est, proident nostrud officia enim sed reprehenderit. Magna cillum consequat aute reprehenderit duis sunt ullamco. Labore qui mollit voluptate. Duis dolor sint aute amet aliquip officia, est non mollit tempor enim quis fugiat, eu do culpa consectetur magna. Do ullamco aliqua voluptate culpa excepteur reprehenderit reprehenderit. Occaecat nulla sit est magna. Deserunt ea voluptate veniam cillum. Amet cupidatat duis est tempor fugiat ex eu, officia est sunt consectetur labore esse exercitation. Nisi cupidatat irure est nisi. Officia amet eu veniam reprehenderit. In amet incididunt tempor commodo ea labore. Mollit dolor aliquip excepteur, voluptate aute occaecat id officia proident. Ullamco est amet tempor. Proident aliquip proident mollit do aliquip ipsum, culpa quis aute id irure. Velit excepteur cillum cillum ut cupidatat. Occaecat qui elit esse nulla minim. Consequat velit id ad pariatur tempor. Eiusmod deserunt aliqua ex sed quis non. Dolor sint commodo ex in deserunt nostrud excepteur, pariatur ex aliqua anim adipiscing amet proident. Laboris eu laborum magna lorem ipsum fugiat velit." @@ -233,6 +302,82 @@ def _complicated_page(): ) -@pytest.fixture(params=[_simple_page, _complicated_page]) +def _counter(): + return ( + rx.text(BenchmarkState.counter), + rx.button("Increment", on_click=BenchmarkState.increment), + rx.button("Decrement", on_click=BenchmarkState.decrement), + rx.cond( + BenchmarkState.counter < 0, + rx.text("Counter is negative"), + rx.fragment( + rx.cond( + BenchmarkState.show_odd, + rx.text("Counter is odd"), + ), + rx.cond( + BenchmarkState.show_even, + rx.text("Counter is even"), + ), + ), + ), + ) + + +def _show_key(): + return rx.match( + BenchmarkState.current_key, + ( + "key_1", + rx.text("Key 1"), + ), + ( + "key_2", + rx.text("Key 2"), + ), + ( + "key_3", + rx.text("Key 3"), + ), + rx.text("Key not found"), + ) + + +def _simple_foreach(): + return rx.foreach( + BenchmarkState.elements, + lambda elem: rx.text(elem), + ) + + +def _render_nested_element(elem: NestedElement, idx): + return ( + rx.text(f"{idx} {elem.identifier}"), + rx.foreach(elem.value, lambda value: rx.text(value)), + ) + + +def _nested_foreach(): + return rx.foreach( + BenchmarkState.nested_elements, + _render_nested_element, + ) + + +def _stateful_page(): + return rx.hstack( + _counter(), + _show_key(), + _simple_foreach(), + _nested_foreach(), + ) + + +@pytest.fixture(params=[_simple_page, _complicated_page, _stateful_page]) +def unevaluated_page(request): + return request.param + + +@pytest.fixture(params=[_simple_page, _complicated_page, _stateful_page]) def evaluated_page(request): return request.param() diff --git a/tests/benchmarks/test_evaluate.py b/tests/benchmarks/test_evaluate.py index c8a6b392d..fbc75dea7 100644 --- a/tests/benchmarks/test_evaluate.py +++ b/tests/benchmarks/test_evaluate.py @@ -1,9 +1,6 @@ import pytest -from .fixtures import _complicated_page, _simple_page - @pytest.mark.benchmark -@pytest.mark.parametrize("page", [_simple_page, _complicated_page]) -def test_evaluate_page(page): - page() +def test_evaluate_page(unevaluated_page): + unevaluated_page() From 977e1dcb6720d5c0c65f92af14bff5efc53f8bdc Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 12 Feb 2025 09:05:36 -0800 Subject: [PATCH 26/38] update deps 2025-02-11 (#4804) --- poetry.lock | 355 ++++++++++++++++++++++++++-------------------------- 1 file changed, 180 insertions(+), 175 deletions(-) diff --git a/poetry.lock b/poetry.lock index f5007ee07..b96749316 100644 --- a/poetry.lock +++ b/poetry.lock @@ -403,75 +403,76 @@ markers = {main = "(platform_system == \"Windows\" or os_name == \"nt\") and (py [[package]] name = "coverage" -version = "7.6.10" +version = "7.6.12" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, ] [package.dependencies] @@ -482,40 +483,44 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "44.0.0" +version = "44.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" files = [ - {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, - {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, - {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, - {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, - {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, - {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, - {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, - {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, - {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, + {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, + {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, + {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, + {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, + {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, + {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, + {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, + {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, + {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, + {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, + {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, ] [package.dependencies] @@ -528,7 +533,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -852,15 +857,15 @@ test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] [[package]] name = "identify" -version = "2.6.6" +version = "2.6.7" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, - {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, + {file = "identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0"}, + {file = "identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684"}, ] [package.extras] @@ -1074,15 +1079,15 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "mako" -version = "1.3.8" +version = "1.3.9" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, - {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, + {file = "Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1"}, + {file = "mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac"}, ] [package.dependencies] @@ -1558,25 +1563,25 @@ type = ["mypy (>=1.11.2)"] [[package]] name = "playwright" -version = "1.49.1" +version = "1.50.0" description = "A high-level API to automate web browsers" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"}, - {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"}, - {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"}, - {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"}, - {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"}, + {file = "playwright-1.50.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f36d754a6c5bd9bf7f14e8f57a2aea6fd08f39ca4c8476481b9c83e299531148"}, + {file = "playwright-1.50.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40f274384591dfd27f2b014596250b2250c843ed1f7f4ef5d2960ecb91b4961e"}, + {file = "playwright-1.50.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9922ef9bcd316995f01e220acffd2d37a463b4ad10fd73e388add03841dfa230"}, + {file = "playwright-1.50.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:8fc628c492d12b13d1f347137b2ac6c04f98197ff0985ef0403a9a9ee0d39131"}, + {file = "playwright-1.50.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcff35f72db2689a79007aee78f1b0621a22e6e3d6c1f58aaa9ac805bf4497c"}, + {file = "playwright-1.50.0-py3-none-win32.whl", hash = "sha256:3b906f4d351260016a8c5cc1e003bb341651ae682f62213b50168ed581c7558a"}, + {file = "playwright-1.50.0-py3-none-win_amd64.whl", hash = "sha256:1859423da82de631704d5e3d88602d755462b0906824c1debe140979397d2e8d"}, ] [package.dependencies] -greenlet = "3.1.1" -pyee = "12.0.0" +greenlet = ">=3.1.1,<4.0.0" +pyee = ">=12,<13" [[package]] name = "plotly" @@ -1828,15 +1833,15 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyee" -version = "12.0.0" +version = "12.1.1" description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"}, - {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"}, + {file = "pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef"}, + {file = "pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3"}, ] [package.dependencies] @@ -2311,15 +2316,15 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)" [[package]] name = "reflex-hosting-cli" -version = "0.1.34" +version = "0.1.35" description = "Reflex Hosting CLI" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "reflex_hosting_cli-0.1.34-py3-none-any.whl", hash = "sha256:eabc4dc7bf68e022a9388614c1a35b5ab36b01021df063d0c3356eda0e245264"}, - {file = "reflex_hosting_cli-0.1.34.tar.gz", hash = "sha256:07be37fda6dcede0a5d4bc1fd1786d9a3df5ad4e49dc1b6ba335418563cfecec"}, + {file = "reflex_hosting_cli-0.1.35-py3-none-any.whl", hash = "sha256:619687be27e6691cb54f6cf038e98d4d622fcf25a85bc9986f8daf52b48e6744"}, + {file = "reflex_hosting_cli-0.1.35.tar.gz", hash = "sha256:9a5d02978b900045464a1a5581f3adc6260daaa09e8acf95fd05024cda926ae7"}, ] [package.dependencies] @@ -2571,70 +2576,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.37" +version = "2.0.38" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-win32.whl", hash = "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-win_amd64.whl", hash = "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-win32.whl", hash = "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-win_amd64.whl", hash = "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-win32.whl", hash = "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-win_amd64.whl", hash = "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b"}, - {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, - {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, + {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, + {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, ] [package.dependencies] @@ -2999,15 +3004,15 @@ standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.29.1" +version = "20.29.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, - {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, + {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, + {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, ] [package.dependencies] From dd5b817f0fddf98b50009b73b31f396b5a7d4831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 12 Feb 2025 19:06:01 +0100 Subject: [PATCH 27/38] fix port handling (#4773) * handle port better * setting port via envvar is possible again * change default deploy_url and api_url * fix for review * update docstring * type new envvar as optional --- reflex/config.py | 16 ++++++++++++---- reflex/reflex.py | 33 ++++++++++++++++++++++++--------- reflex/utils/processes.py | 25 ++++++++++++------------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index 21614b9b1..233087938 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -562,6 +562,12 @@ class EnvironmentVariables: # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY. REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False) + # The port to run the frontend on. + REFLEX_FRONTEND_PORT: EnvVar[int | None] = env_var(None) + + # The port to run the backend on. + REFLEX_BACKEND_PORT: EnvVar[int | None] = env_var(None) + # Reflex internal env to reload the config. RELOAD_CONFIG: EnvVar[bool] = env_var(False, internal=True) @@ -640,19 +646,21 @@ class Config(Base): loglevel: constants.LogLevel = constants.LogLevel.DEFAULT # The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken. - frontend_port: int = constants.DefaultPorts.FRONTEND_PORT + frontend_port: int | None = None # The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app frontend_path: str = "" # The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken. - backend_port: int = constants.DefaultPorts.BACKEND_PORT + backend_port: int | None = None # The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production. - api_url: str = f"http://localhost:{backend_port}" + api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}" # The url the frontend will be hosted on. - deploy_url: Optional[str] = f"http://localhost:{frontend_port}" + deploy_url: Optional[str] = ( + f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}" + ) # The url the backend will be hosted on. backend_host: str = "0.0.0.0" diff --git a/reflex/reflex.py b/reflex/reflex.py index e4be0c89a..410485551 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -127,8 +127,8 @@ def _run( env: constants.Env = constants.Env.DEV, frontend: bool = True, backend: bool = True, - frontend_port: int = config.frontend_port, - backend_port: int = config.backend_port, + frontend_port: int | None = None, + backend_port: int | None = None, backend_host: str = config.backend_host, loglevel: constants.LogLevel = config.loglevel, ): @@ -158,17 +158,28 @@ def _run( # Find the next available open port if applicable. if frontend: + auto_increment_frontend = not bool(frontend_port or config.frontend_port) frontend_port = processes.handle_port( "frontend", - frontend_port, - constants.DefaultPorts.FRONTEND_PORT, + ( + frontend_port + or config.frontend_port + or constants.DefaultPorts.FRONTEND_PORT + ), + auto_increment=auto_increment_frontend, ) if backend: + auto_increment_backend = not bool(backend_port or config.backend_port) + backend_port = processes.handle_port( "backend", - backend_port, - constants.DefaultPorts.BACKEND_PORT, + ( + backend_port + or config.backend_port + or constants.DefaultPorts.BACKEND_PORT + ), + auto_increment=auto_increment_backend, ) # Apply the new ports to the config. @@ -246,7 +257,7 @@ def _run( # Start the frontend and backend. with processes.run_concurrently_context(*commands): # In dev mode, run the backend on the main thread. - if backend and env == constants.Env.DEV: + if backend and backend_port and env == constants.Env.DEV: backend_cmd( backend_host, int(backend_port), loglevel.subprocess_level(), frontend ) @@ -275,10 +286,14 @@ def run( envvar=environment.REFLEX_BACKEND_ONLY.name, ), frontend_port: int = typer.Option( - config.frontend_port, help="Specify a different frontend port." + config.frontend_port, + help="Specify a different frontend port.", + envvar=environment.REFLEX_FRONTEND_PORT.name, ), backend_port: int = typer.Option( - config.backend_port, help="Specify a different backend port." + config.backend_port, + help="Specify a different backend port.", + envvar=environment.REFLEX_BACKEND_PORT.name, ), backend_host: str = typer.Option( config.backend_host, help="Specify the backend host." diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index c92fb7d1a..a0c13300d 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -116,17 +116,14 @@ def change_port(port: int, _type: str) -> int: return new_port -def handle_port(service_name: str, port: int, default_port: int) -> int: +def handle_port(service_name: str, port: int, auto_increment: bool) -> int: """Change port if the specified port is in use and is not explicitly specified as a CLI arg or config arg. - otherwise tell the user the port is in use and exit the app. - - We make an assumption that when port is the default port,then it hasn't been explicitly set since its not straightforward - to know whether a port was explicitly provided by the user unless its any other than the default. + Otherwise tell the user the port is in use and exit the app. Args: service_name: The frontend or backend. port: The provided port. - default_port: The default port number associated with the specified service. + auto_increment: Whether to automatically increment the port. Returns: The port to run the service on. @@ -134,13 +131,15 @@ def handle_port(service_name: str, port: int, default_port: int) -> int: Raises: Exit:when the port is in use. """ - if is_process_on_port(port): - if port == int(default_port): - return change_port(port, service_name) - else: - console.error(f"{service_name.capitalize()} port: {port} is already in use") - raise typer.Exit() - return port + if (process := get_process_on_port(port)) is None: + return port + if auto_increment: + return change_port(port, service_name) + else: + console.error( + f"{service_name.capitalize()} port: {port} is already in use by PID: {process.pid}." + ) + raise typer.Exit() def new_process( From d79366d8b29047bb1f45ff05c9f99ecfe71fbfca Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 12 Feb 2025 14:51:58 -0800 Subject: [PATCH 28/38] benchmark experimentation (#4811) * benchmark experimentation * do the same for test_evaluate_page * import templates beforehands * add auto reload * disable extensions --- reflex/compiler/templates.py | 3 +-- tests/benchmarks/fixtures.py | 4 ++-- tests/benchmarks/test_compilation.py | 27 +++++++++++++++++---------- tests/benchmarks/test_evaluate.py | 13 +++++++++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 117b655a9..e85e5fe6d 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -48,11 +48,10 @@ class ReflexJinjaEnvironment(Environment): def __init__(self) -> None: """Set default environment.""" - extensions = ["jinja2.ext.debug"] super().__init__( - extensions=extensions, trim_blocks=True, lstrip_blocks=True, + auto_reload=False, ) self.filters["json_dumps"] = json_dumps self.filters["react_setter"] = lambda state: f"set{state.capitalize()}" diff --git a/tests/benchmarks/fixtures.py b/tests/benchmarks/fixtures.py index 16233100a..334d48282 100644 --- a/tests/benchmarks/fixtures.py +++ b/tests/benchmarks/fixtures.py @@ -374,10 +374,10 @@ def _stateful_page(): @pytest.fixture(params=[_simple_page, _complicated_page, _stateful_page]) -def unevaluated_page(request): +def unevaluated_page(request: pytest.FixtureRequest): return request.param @pytest.fixture(params=[_simple_page, _complicated_page, _stateful_page]) -def evaluated_page(request): +def evaluated_page(request: pytest.FixtureRequest): return request.param() diff --git a/tests/benchmarks/test_compilation.py b/tests/benchmarks/test_compilation.py index 0a20ed521..dbea81023 100644 --- a/tests/benchmarks/test_compilation.py +++ b/tests/benchmarks/test_compilation.py @@ -1,18 +1,25 @@ -import pytest +from pytest_codspeed import BenchmarkFixture from reflex.compiler.compiler import _compile_page, _compile_stateful_components +from reflex.components.component import Component -@pytest.mark.benchmark -def test_compile_page(evaluated_page): - _compile_page(evaluated_page, None) +def import_templates(): + # Importing the templates module to avoid the import time in the benchmark + import reflex.compiler.templates # noqa: F401 -@pytest.mark.benchmark -def test_compile_stateful(evaluated_page): - _compile_stateful_components([evaluated_page]) +def test_compile_page(evaluated_page: Component, benchmark: BenchmarkFixture): + import_templates() + + benchmark(lambda: _compile_page(evaluated_page, None)) -@pytest.mark.benchmark -def test_get_all_imports(evaluated_page): - evaluated_page._get_all_imports() +def test_compile_stateful(evaluated_page: Component, benchmark: BenchmarkFixture): + import_templates() + + benchmark(lambda: _compile_stateful_components([evaluated_page])) + + +def test_get_all_imports(evaluated_page: Component, benchmark: BenchmarkFixture): + benchmark(lambda: evaluated_page._get_all_imports()) diff --git a/tests/benchmarks/test_evaluate.py b/tests/benchmarks/test_evaluate.py index fbc75dea7..7fd75fc6c 100644 --- a/tests/benchmarks/test_evaluate.py +++ b/tests/benchmarks/test_evaluate.py @@ -1,6 +1,11 @@ -import pytest +from typing import Callable + +from pytest_codspeed import BenchmarkFixture + +from reflex.components.component import Component -@pytest.mark.benchmark -def test_evaluate_page(unevaluated_page): - unevaluated_page() +def test_evaluate_page( + unevaluated_page: Callable[[], Component], benchmark: BenchmarkFixture +): + benchmark(unevaluated_page) From 2ba73f7ff930a1cc3363851faad2cb9d7119c4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Thu, 13 Feb 2025 19:19:33 +0100 Subject: [PATCH 29/38] bump ruff to 0.9.6 (#4817) --- .pre-commit-config.yaml | 2 +- poetry.lock | 40 ++++++++++++++++++++-------------------- pyproject.toml | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bad7b996..743f5f31a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ fail_fast: true repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.9.3 + rev: v0.9.6 hooks: - id: ruff-format args: [reflex, tests] diff --git a/poetry.lock b/poetry.lock index b96749316..032bd2d4a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2415,31 +2415,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.9.3" +version = "0.9.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624"}, - {file = "ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c"}, - {file = "ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b"}, - {file = "ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"}, - {file = "ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4"}, - {file = "ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b"}, - {file = "ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a"}, + {file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"}, + {file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"}, + {file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"}, + {file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"}, + {file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"}, + {file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"}, + {file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"}, ] [[package]] @@ -3188,4 +3188,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10, <4.0" -content-hash = "3b7e6e6e872c68f951f191d85a7d76fe1dd86caf32e2143a53a3152a3686fc7f" +content-hash = "7ae644e1c5b910f4fd0d8ab0b530818077a96e5d329b2be1269e967c6b0b3d25" diff --git a/pyproject.toml b/pyproject.toml index c192a1139..67611a867 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dill = ">=0.3.8" toml = ">=0.10.2,<1.0" pytest-asyncio = ">=0.24.0" pytest-cov = ">=4.0.0,<7.0" -ruff = "0.9.3" +ruff = "0.9.6" pandas = ">=2.1.1,<3.0" pillow = ">=10.0.0,<12.0" plotly = ">=5.13.0,<6.0" From 10c45b185c597d1c7828715d9360a5e92a8b7fd2 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 12:44:27 -0800 Subject: [PATCH 30/38] adjust setter to include type annotation (#4726) * adjust setter to include type annotation * apparently this discovered some bugs * remove some pyright ignores * add str to int/float conversion * dang it darglint --- benchmarks/test_benchmark_compile_pages.py | 32 ++++++++++++++++------ reflex/state.py | 20 ++++++++++++-- reflex/vars/base.py | 4 ++- tests/integration/test_background_task.py | 10 +++++-- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py index 149fc6130..6cf39f60c 100644 --- a/benchmarks/test_benchmark_compile_pages.py +++ b/benchmarks/test_benchmark_compile_pages.py @@ -46,10 +46,26 @@ def render_multiple_pages(app, num: int): class State(rx.State): """The app state.""" - position: str - college: str - age: Tuple[int, int] = (18, 50) - salary: Tuple[int, int] = (0, 25000000) + position: rx.Field[str] + college: rx.Field[str] + age: rx.Field[Tuple[int, int]] = rx.field((18, 50)) + salary: rx.Field[Tuple[int, int]] = rx.field((0, 25000000)) + + @rx.event + def set_position(self, value: str): + self.position = value + + @rx.event + def set_college(self, value: str): + self.college = value + + @rx.event + def set_age(self, value: list[int]): + self.age = (value[0], value[1]) + + @rx.event + def set_salary(self, value: list[int]): + self.salary = (value[0], value[1]) comp1 = rx.center( rx.theme_panel(), @@ -74,13 +90,13 @@ def render_multiple_pages(app, num: int): rx.select( ["C", "PF", "SF", "PG", "SG"], placeholder="Select a position. (All)", - on_change=State.set_position, # pyright: ignore [reportAttributeAccessIssue] + on_change=State.set_position, size="3", ), rx.select( college, placeholder="Select a college. (All)", - on_change=State.set_college, # pyright: ignore [reportAttributeAccessIssue] + on_change=State.set_college, size="3", ), ), @@ -95,7 +111,7 @@ def render_multiple_pages(app, num: int): default_value=[18, 50], min=18, max=50, - on_value_commit=State.set_age, # pyright: ignore [reportAttributeAccessIssue] + on_value_commit=State.set_age, ), align_items="left", width="100%", @@ -110,7 +126,7 @@ def render_multiple_pages(app, num: int): default_value=[0, 25000000], min=0, max=25000000, - on_value_commit=State.set_salary, # pyright: ignore [reportAttributeAccessIssue] + on_value_commit=State.set_salary, ), align_items="left", width="100%", diff --git a/reflex/state.py b/reflex/state.py index 92aaa4710..dceba7e3b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1742,6 +1742,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Yields: StateUpdate object + + Raises: + ValueError: If a string value is received for an int or float type and cannot be converted. """ from reflex.utils import telemetry @@ -1779,12 +1782,25 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): hinted_args, (Base, BaseModelV1, BaseModelV2) ): payload[arg] = hinted_args(**value) - if isinstance(value, list) and (hinted_args is set or hinted_args is Set): + elif isinstance(value, list) and (hinted_args is set or hinted_args is Set): payload[arg] = set(value) - if isinstance(value, list) and ( + elif isinstance(value, list) and ( hinted_args is tuple or hinted_args is Tuple ): payload[arg] = tuple(value) + elif isinstance(value, str) and ( + hinted_args is int or hinted_args is float + ): + try: + payload[arg] = hinted_args(value) + except ValueError: + raise ValueError( + f"Received a string value ({value}) for {arg} but expected a {hinted_args}" + ) from None + else: + console.warn( + f"Received a string value ({value}) for {arg} but expected a {hinted_args}. A simple conversion was successful." + ) # Wrap the function in a try/except block. try: diff --git a/reflex/vars/base.py b/reflex/vars/base.py index a24db4010..593c60f3e 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -951,7 +951,7 @@ class Var(Generic[VAR_TYPE]): """ actual_name = self._var_field_name - def setter(state: BaseState, value: Any): + def setter(state: Any, value: Any): """Get the setter for the var. Args: @@ -969,6 +969,8 @@ class Var(Generic[VAR_TYPE]): else: setattr(state, actual_name, value) + setter.__annotations__["value"] = self._var_type + setter.__qualname__ = self._get_setter_name() return setter diff --git a/tests/integration/test_background_task.py b/tests/integration/test_background_task.py index f312f8122..91a1b5ae1 100644 --- a/tests/integration/test_background_task.py +++ b/tests/integration/test_background_task.py @@ -20,7 +20,11 @@ def BackgroundTask(): class State(rx.State): counter: int = 0 _task_id: int = 0 - iterations: int = 10 + iterations: rx.Field[int] = rx.field(10) + + @rx.event + def set_iterations(self, value: str): + self.iterations = int(value) @rx.event(background=True) async def handle_event(self): @@ -125,8 +129,8 @@ def BackgroundTask(): rx.input( id="iterations", placeholder="Iterations", - value=State.iterations.to_string(), # pyright: ignore [reportAttributeAccessIssue] - on_change=State.set_iterations, # pyright: ignore [reportAttributeAccessIssue] + value=State.iterations.to_string(), + on_change=State.set_iterations, ), rx.button( "Delayed Increment", From c6fb4e238d93c4383f1774055fcdc096f5f0fec6 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 12:44:42 -0800 Subject: [PATCH 31/38] improve into component conversion (#4754) * improve into component conversion * correct the order of .State --- reflex/app.py | 4 ++- reflex/compiler/compiler.py | 55 +++++++++++++++++++++++++++++-------- reflex/state.py | 3 ++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index d290b8f49..67d4f5b91 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -591,7 +591,9 @@ class App(MiddlewareMixin, LifespanMixin): Returns: The generated component. """ - return component if isinstance(component, Component) else component() + from reflex.compiler.compiler import into_component + + return into_component(component) def add_page( self, diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 7cd87fb71..667a477e8 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, Iterable, Optional, Sequence, Tuple, Type, Union from reflex import constants from reflex.compiler import templates, utils @@ -545,7 +545,47 @@ def purge_web_pages_dir(): if TYPE_CHECKING: - from reflex.app import UnevaluatedPage + from reflex.app import ComponentCallable, UnevaluatedPage + + +def _into_component_once(component: Component | ComponentCallable) -> Component | None: + """Convert a component to a Component. + + Args: + component: The component to convert. + + Returns: + The converted component. + """ + if isinstance(component, Component): + return component + if isinstance(component, (Var, int, float, str)): + return Fragment.create(component) + if isinstance(component, Sequence): + return Fragment.create(*component) + return None + + +def into_component(component: Component | ComponentCallable) -> Component: + """Convert a component to a Component. + + Args: + component: The component to convert. + + Returns: + The converted component. + + Raises: + TypeError: If the component is not a Component. + """ + if (converted := _into_component_once(component)) is not None: + return converted + if ( + callable(component) + and (converted := _into_component_once(component())) is not None + ): + return converted + raise TypeError(f"Expected a Component, got {type(component)}") def compile_unevaluated_page( @@ -568,12 +608,7 @@ def compile_unevaluated_page( The compiled component and whether state should be enabled. """ # Generate the component if it is a callable. - component = page.component - component = component if isinstance(component, Component) else component() - - # unpack components that return tuples in an rx.fragment. - if isinstance(component, tuple): - component = Fragment.create(*component) + component = into_component(page.component) component._add_style_recursive(style or {}, theme) @@ -678,10 +713,8 @@ class ExecutorSafeFunctions: The route, compiled component, and compiled page. """ component, enable_state = compile_unevaluated_page( - route, cls.UNCOMPILED_PAGES[route] + route, cls.UNCOMPILED_PAGES[route], cls.STATE, style, theme ) - component = component if isinstance(component, Component) else component() - component._add_style_recursive(style, theme) return route, component, compile_page(route, component, cls.STATE) @classmethod diff --git a/reflex/state.py b/reflex/state.py index dceba7e3b..77c352cfa 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2475,6 +2475,8 @@ class ComponentState(State, mixin=True): Returns: A new instance of the Component with an independent copy of the State. """ + from reflex.compiler.compiler import into_component + cls._per_component_state_instance_count += 1 state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" component_state = type( @@ -2486,6 +2488,7 @@ class ComponentState(State, mixin=True): # Save a reference to the dynamic state for pickle/unpickle. setattr(reflex.istate.dynamic, state_cls_name, component_state) component = component_state.get_component(*children, **props) + component = into_component(component) component.State = component_state return component From 40294a7c9e68f2355bc71e2ccb9be1473c11b533 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 13:36:59 -0800 Subject: [PATCH 32/38] standarize filename from upload (#4734) * standarize filename from upload * all my friends hate fast api upload file * make deprecated filename private * lstrip the "/" Co-authored-by: Masen Furer --------- Co-authored-by: Masen Furer --- reflex/app.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 67d4f5b91..2c8e889fc 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -22,6 +22,7 @@ from typing import ( TYPE_CHECKING, Any, AsyncIterator, + BinaryIO, Callable, Coroutine, Dict, @@ -35,12 +36,15 @@ from typing import ( get_type_hints, ) -from fastapi import FastAPI, HTTPException, Request, UploadFile +from fastapi import FastAPI, HTTPException, Request +from fastapi import UploadFile as FastAPIUploadFile from fastapi.middleware import cors from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from socketio import ASGIApp, AsyncNamespace, AsyncServer +from starlette.datastructures import Headers +from starlette.datastructures import UploadFile as StarletteUploadFile from starlette_admin.contrib.sqla.admin import Admin from starlette_admin.contrib.sqla.view import ModelView @@ -231,6 +235,53 @@ class OverlayFragment(Fragment): pass +@dataclasses.dataclass(frozen=True) +class UploadFile(StarletteUploadFile): + """A file uploaded to the server. + + Args: + file: The standard Python file object (non-async). + filename: The original file name. + size: The size of the file in bytes. + headers: The headers of the request. + """ + + file: BinaryIO + + path: Optional[Path] = dataclasses.field(default=None) + + _deprecated_filename: Optional[str] = dataclasses.field(default=None) + + size: Optional[int] = dataclasses.field(default=None) + + headers: Headers = dataclasses.field(default_factory=Headers) + + @property + def name(self) -> Optional[str]: + """Get the name of the uploaded file. + + Returns: + The name of the uploaded file. + """ + if self.path: + return self.path.name + + @property + def filename(self) -> Optional[str]: + """Get the filename of the uploaded file. + + Returns: + The filename of the uploaded file. + """ + console.deprecate( + feature_name="UploadFile.filename", + reason="Use UploadFile.name instead.", + deprecation_version="0.7.1", + removal_version="0.8.0", + ) + return self._deprecated_filename + + @dataclasses.dataclass( frozen=True, ) @@ -1585,7 +1636,7 @@ def upload(app: App): The upload function. """ - async def upload_file(request: Request, files: List[UploadFile]): + async def upload_file(request: Request, files: List[FastAPIUploadFile]): """Upload a file. Args: @@ -1661,7 +1712,8 @@ def upload(app: App): file_copies.append( UploadFile( file=content_copy, - filename=file.filename, + path=Path(file.filename.lstrip("/")) if file.filename else None, + _deprecated_filename=file.filename, size=file.size, headers=file.headers, ) From 6fb491471bc845ec8a556cb82509cf8bb084cbd3 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 13:44:02 -0800 Subject: [PATCH 33/38] cache get_type_hints for environment (#4820) --- reflex/config.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index 233087938..33009b3bc 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -10,6 +10,7 @@ import os import sys import threading import urllib.parse +from functools import lru_cache from importlib.util import find_spec from pathlib import Path from types import ModuleType @@ -408,6 +409,19 @@ class EnvVar(Generic[T]): os.environ[self.name] = str_value +@lru_cache() +def get_type_hints_environment(cls: type) -> dict[str, Any]: + """Get the type hints for the environment variables. + + Args: + cls: The class. + + Returns: + The type hints. + """ + return get_type_hints(cls) + + class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] """Descriptor for environment variables.""" @@ -434,7 +448,9 @@ class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] """ self.name = name - def __get__(self, instance: Any, owner: Any): + def __get__( + self, instance: EnvironmentVariables, owner: type[EnvironmentVariables] + ): """Get the EnvVar instance. Args: @@ -444,7 +460,7 @@ class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] Returns: The EnvVar instance. """ - type_ = get_args(get_type_hints(owner)[self.name])[0] + type_ = get_args(get_type_hints_environment(owner)[self.name])[0] env_name = self.name if self.internal: env_name = f"__{env_name}" From aac61c69c2c2fdcb37f3cc67cab92623386c239f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 15:40:01 -0800 Subject: [PATCH 34/38] actually get rid of callable var fr fr (#4821) --- reflex/components/core/upload.py | 4 +- reflex/components/core/upload.pyi | 4 +- reflex/components/radix/themes/color_mode.py | 2 +- reflex/style.py | 3 +- reflex/vars/base.py | 55 ------------------- tests/integration/test_upload.py | 2 +- .../tests_playwright/test_appearance.py | 2 +- 7 files changed, 6 insertions(+), 66 deletions(-) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 897b89608..6c86d3c44 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -29,7 +29,7 @@ from reflex.event import ( from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, Var, get_unique_variable_name +from reflex.vars.base import Var, get_unique_variable_name from reflex.vars.sequence import LiteralStringVar DEFAULT_UPLOAD_ID: str = "default" @@ -45,7 +45,6 @@ upload_files_context_var_data: VarData = VarData( ) -@CallableVar def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: """Get the file upload drop trigger. @@ -75,7 +74,6 @@ def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: ) -@CallableVar def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var: """Get the list of selected files. diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index 6ed96a15e..d1ddceb4d 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -13,14 +13,12 @@ from reflex.event import CallableEventSpec, EventSpec, EventType from reflex.style import Style from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, Var +from reflex.vars.base import Var DEFAULT_UPLOAD_ID: str upload_files_context_var_data: VarData -@CallableVar def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: ... -@CallableVar def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var: ... @CallableEventSpec def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec: ... diff --git a/reflex/components/radix/themes/color_mode.py b/reflex/components/radix/themes/color_mode.py index d9b7c0b02..0718aaac9 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -144,7 +144,7 @@ class ColorModeIconButton(IconButton): if allow_system: - def color_mode_item(_color_mode: str): + def color_mode_item(_color_mode: Literal["light", "dark", "system"]): return dropdown_menu.item( _color_mode.title(), on_click=set_color_mode(_color_mode) ) diff --git a/reflex/style.py b/reflex/style.py index 192835ca3..1d818ed06 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -12,7 +12,7 @@ from reflex.utils.exceptions import ReflexError from reflex.utils.imports import ImportVar from reflex.utils.types import get_origin from reflex.vars import VarData -from reflex.vars.base import CallableVar, LiteralVar, Var +from reflex.vars.base import LiteralVar, Var from reflex.vars.function import FunctionVar from reflex.vars.object import ObjectVar @@ -48,7 +48,6 @@ def _color_mode_var(_js_expr: str, _var_type: Type = str) -> Var: ).guess_type() -@CallableVar def set_color_mode( new_color_mode: LiteralColorMode | Var[LiteralColorMode] | None = None, ) -> Var[EventChain]: diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 593c60f3e..a6786b18a 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -1903,61 +1903,6 @@ def _or_operation(a: Var, b: Var): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class CallableVar(Var): - """Decorate a Var-returning function to act as both a Var and a function. - - This is used as a compatibility shim for replacing Var objects in the - API with functions that return a family of Var. - """ - - fn: Callable[..., Var] = dataclasses.field( - default_factory=lambda: lambda: Var(_js_expr="undefined") - ) - original_var: Var = dataclasses.field( - default_factory=lambda: Var(_js_expr="undefined") - ) - - def __init__(self, fn: Callable[..., Var]): - """Initialize a CallableVar. - - Args: - fn: The function to decorate (must return Var) - """ - original_var = fn() - super(CallableVar, self).__init__( - _js_expr=original_var._js_expr, - _var_type=original_var._var_type, - _var_data=VarData.merge(original_var._get_all_var_data()), - ) - object.__setattr__(self, "fn", fn) - object.__setattr__(self, "original_var", original_var) - - def __call__(self, *args: Any, **kwargs: Any) -> Var: - """Call the decorated function. - - Args: - *args: The args to pass to the function. - **kwargs: The kwargs to pass to the function. - - Returns: - The Var returned from calling the function. - """ - return self.fn(*args, **kwargs) - - def __hash__(self) -> int: - """Calculate the hash of the object. - - Returns: - The hash of the object. - """ - return hash((type(self).__name__, self.original_var)) - - RETURN_TYPE = TypeVar("RETURN_TYPE") DICT_KEY = TypeVar("DICT_KEY") diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index e20b1cd6d..471382570 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -87,7 +87,7 @@ def UploadFile(): ), rx.box( rx.foreach( - rx.selected_files, + rx.selected_files(), lambda f: rx.text(f, as_="p"), ), id="selected_files", diff --git a/tests/integration/tests_playwright/test_appearance.py b/tests/integration/tests_playwright/test_appearance.py index d325b183f..0b1440ed1 100644 --- a/tests/integration/tests_playwright/test_appearance.py +++ b/tests/integration/tests_playwright/test_appearance.py @@ -61,7 +61,7 @@ def ColorToggleApp(): rx.icon(tag="moon", size=20), value="dark", ), - on_change=set_color_mode, + on_change=set_color_mode(), variant="classic", radius="large", value=color_mode, From b44bbc81a0a707ded71c9f0d0a9d4afb0c308bdd Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 15:54:10 -0800 Subject: [PATCH 35/38] import var perf improvements (#4813) * import var perf improvements * use tuples over iterator * the only thing that matters * maybe tuple map is faster than tuple list comprehension * do it in one list comprehension --- reflex/components/component.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index d27bddf78..005f7791d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -51,13 +51,7 @@ from reflex.event import ( ) from reflex.style import Style, format_as_emotion from reflex.utils import format, imports, types -from reflex.utils.imports import ( - ImmutableParsedImportDict, - ImportDict, - ImportVar, - ParsedImportDict, - parse_imports, -) +from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports from reflex.vars import VarData from reflex.vars.base import ( CachedVarOperation, @@ -1208,7 +1202,7 @@ class Component(BaseComponent, ABC): Returns: True if the dependency should be transpiled. """ - return ( + return bool(self.transpile_packages) and ( dep in self.transpile_packages or format.format_library_name(dep or "") in self.transpile_packages ) @@ -1291,9 +1285,10 @@ class Component(BaseComponent, ABC): event_imports = Imports.EVENTS if self.event_triggers else {} # Collect imports from Vars used directly by this component. - var_datas = [var._get_all_var_data() for var in self._get_vars()] - var_imports: List[ImmutableParsedImportDict] = [ - var_data.imports for var_data in var_datas if var_data is not None + var_imports = [ + var_data.imports + for var in self._get_vars() + if (var_data := var._get_all_var_data()) is not None ] added_import_dicts: list[ParsedImportDict] = [] From 8e579efe47e4e1a7ba6e61c00166a55ed36eff5f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 16:17:06 -0800 Subject: [PATCH 36/38] remove some benchmarks from CI (#4812) --- .github/workflows/benchmarks.yml | 50 -- .github/workflows/integration_tests.yml | 32 - .../test_benchmark_compile_components.py | 376 ----------- benchmarks/test_benchmark_compile_pages.py | 595 ------------------ 4 files changed, 1053 deletions(-) delete mode 100644 benchmarks/test_benchmark_compile_components.py delete mode 100644 benchmarks/test_benchmark_compile_pages.py diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6da40ef6f..b794106e1 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -70,56 +70,6 @@ jobs: env: GITHUB_SHA: ${{ github.sha }} - simple-apps-benchmarks: # This app tests the compile times of various compoonents and pages - if: github.event.pull_request.merged == true - env: - OUTPUT_FILE: benchmarks.json - timeout-minutes: 50 - strategy: - # Prioritize getting more information out of the workflow (even if something fails) - fail-fast: false - matrix: - # Show OS combos first in GUI - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10.16", "3.11.11", "3.12.8"] - exclude: - - os: windows-latest - python-version: "3.10.16" - - os: windows-latest - python-version: "3.11.11" - # keep only one python version for MacOS - - os: macos-latest - python-version: "3.10.16" - - os: macos-latest - python-version: "3.11.11" - include: - - os: windows-latest - python-version: "3.10.11" - - os: windows-latest - python-version: "3.11.9" - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup_build_env - with: - python-version: ${{ matrix.python-version }} - run-poetry-install: true - create-venv-at-path: .venv - - name: Run benchmark tests - env: - APP_HARNESS_HEADLESS: 1 - PYTHONUNBUFFERED: 1 - run: | - poetry run pytest -v benchmarks/ --benchmark-json=${{ env.OUTPUT_FILE }} -s - - name: Upload benchmark results - # Only run if the database creds are available in this context. - run: - poetry run python benchmarks/benchmark_compile_times.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --benchmark-json "${{ env.OUTPUT_FILE }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}" - reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file) if: github.event.pull_request.merged == true timeout-minutes: 30 diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index b02604fd6..dc5a14d88 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -94,26 +94,6 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-examples/counter dev - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" - --branch-name "${{ github.head_ref || github.ref_name }}" - --path ./reflex-examples/counter/.web - --app-name "counter" - - name: Install hyperfine - run: cargo install hyperfine - - name: Benchmark imports - working-directory: ./reflex-examples/counter - run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash - - name: Upload Benchmarks - run: - poetry run python benchmarks/benchmark_imports.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}" - --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}" - --app-name "counter" - name: Install requirements for nba proxy example working-directory: ./reflex-examples/nba-proxy run: | @@ -174,12 +154,6 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-web prod - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --app-name "reflex-web" --path ./reflex-web/.web rx-shout-from-template: strategy: @@ -243,9 +217,3 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-web prod - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --app-name "reflex-web" --path ./reflex-web/.web diff --git a/benchmarks/test_benchmark_compile_components.py b/benchmarks/test_benchmark_compile_components.py deleted file mode 100644 index 9bcfbf85b..000000000 --- a/benchmarks/test_benchmark_compile_components.py +++ /dev/null @@ -1,376 +0,0 @@ -"""Benchmark tests for apps with varying component numbers.""" - -from __future__ import annotations - -import functools -import time -from typing import Generator - -import pytest - -from benchmarks import WINDOWS_SKIP_REASON -from reflex import constants -from reflex.compiler import utils -from reflex.testing import AppHarness, chdir -from reflex.utils import build -from reflex.utils.prerequisites import get_web_dir - -web_pages = get_web_dir() / constants.Dirs.PAGES - - -def render_component(num: int): - """Generate a number of components based on num. - - Args: - num: number of components to produce. - - Returns: - The rendered number of components. - """ - import reflex as rx - - return [ - rx.fragment( - rx.box( - rx.accordion.root( - rx.accordion.item( - header="Full Ingredients", - content="Yes. It's built with accessibility in mind.", - font_size="3em", - ), - rx.accordion.item( - header="Applications", - content="Yes. It's unstyled by default, giving you freedom over the look and feel.", - ), - collapsible=True, - variant="ghost", - width="25rem", - ), - padding_top="20px", - ), - rx.box( - rx.drawer.root( - rx.drawer.trigger( - rx.button("Open Drawer with snap points"), as_child=True - ), - rx.drawer.overlay(), - rx.drawer.portal( - rx.drawer.content( - rx.flex( - rx.drawer.title("Drawer Content"), - rx.drawer.description("Drawer description"), - rx.drawer.close( - rx.button("Close Button"), - as_child=True, - ), - direction="column", - margin="5em", - align_items="center", - ), - top="auto", - height="100%", - flex_direction="column", - background_color="var(--green-3)", - ), - ), - snap_points=["148px", "355px", 1], - ), - ), - rx.box( - rx.callout( - "You will need admin privileges to install and access this application.", - icon="info", - size="3", - ), - ), - rx.box( - rx.table.root( - rx.table.header( - rx.table.row( - rx.table.column_header_cell("Full name"), - rx.table.column_header_cell("Email"), - rx.table.column_header_cell("Group"), - ), - ), - rx.table.body( - rx.table.row( - rx.table.row_header_cell("Danilo Sousa"), - rx.table.cell("danilo@example.com"), - rx.table.cell("Developer"), - ), - rx.table.row( - rx.table.row_header_cell("Zahra Ambessa"), - rx.table.cell("zahra@example.com"), - rx.table.cell("Admin"), - ), - rx.table.row( - rx.table.row_header_cell("Jasper Eriksson"), - rx.table.cell("jasper@example.com"), - rx.table.cell("Developer"), - ), - ), - ) - ), - ) - ] * num - - -def AppWithTenComponentsOnePage(): - """A reflex app with roughly 10 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(1))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithHundredComponentOnePage(): - """A reflex app with roughly 100 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(100))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithThousandComponentsOnePage(): - """A reflex app with roughly 1000 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(1000))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -@pytest.fixture(scope="session") -def app_with_10_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Start Blank Template app at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app10components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenComponentsOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_100_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Start Blank Template app at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app100components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithHundredComponentOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_1000_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 1000 components at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1000components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithThousandComponentsOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_cold(benchmark, app_with_10_components): - """Test the compile time on a cold start for an app with roughly 10 components. - - Args: - benchmark: The benchmark fixture. - app_with_10_components: The app harness. - """ - - def setup(): - with chdir(app_with_10_components.app_path): - utils.empty_dir(web_pages, ["_app.js"]) - app_with_10_components._initialize_app() - build.setup_frontend(app_with_10_components.app_path) - - def benchmark_fn(): - with chdir(app_with_10_components.app_path): - app_with_10_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=10) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_warm(benchmark, app_with_10_components): - """Test the compile time on a warm start for an app with roughly 10 components. - - Args: - benchmark: The benchmark fixture. - app_with_10_components: The app harness. - """ - with chdir(app_with_10_components.app_path): - app_with_10_components._initialize_app() - build.setup_frontend(app_with_10_components.app_path) - - def benchmark_fn(): - with chdir(app_with_10_components.app_path): - app_with_10_components.app_instance._compile() - - benchmark(benchmark_fn) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_cold(benchmark, app_with_100_components): - """Test the compile time on a cold start for an app with roughly 100 components. - - Args: - benchmark: The benchmark fixture. - app_with_100_components: The app harness. - """ - - def setup(): - with chdir(app_with_100_components.app_path): - utils.empty_dir(web_pages, ["_app.js"]) - app_with_100_components._initialize_app() - build.setup_frontend(app_with_100_components.app_path) - - def benchmark_fn(): - with chdir(app_with_100_components.app_path): - app_with_100_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_warm(benchmark, app_with_100_components): - """Test the compile time on a warm start for an app with roughly 100 components. - - Args: - benchmark: The benchmark fixture. - app_with_100_components: The app harness. - """ - with chdir(app_with_100_components.app_path): - app_with_100_components._initialize_app() - build.setup_frontend(app_with_100_components.app_path) - - def benchmark_fn(): - with chdir(app_with_100_components.app_path): - app_with_100_components.app_instance._compile() - - benchmark(benchmark_fn) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_cold(benchmark, app_with_1000_components): - """Test the compile time on a cold start for an app with roughly 1000 components. - - Args: - benchmark: The benchmark fixture. - app_with_1000_components: The app harness. - """ - - def setup(): - with chdir(app_with_1000_components.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_1000_components._initialize_app() - build.setup_frontend(app_with_1000_components.app_path) - - def benchmark_fn(): - with chdir(app_with_1000_components.app_path): - app_with_1000_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_warm(benchmark, app_with_1000_components): - """Test the compile time on a warm start for an app with roughly 1000 components. - - Args: - benchmark: The benchmark fixture. - app_with_1000_components: The app harness. - """ - with chdir(app_with_1000_components.app_path): - app_with_1000_components._initialize_app() - build.setup_frontend(app_with_1000_components.app_path) - - def benchmark_fn(): - with chdir(app_with_1000_components.app_path): - app_with_1000_components.app_instance._compile() - - benchmark(benchmark_fn) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py deleted file mode 100644 index 6cf39f60c..000000000 --- a/benchmarks/test_benchmark_compile_pages.py +++ /dev/null @@ -1,595 +0,0 @@ -"""Benchmark tests for apps with varying page numbers.""" - -from __future__ import annotations - -import functools -import time -from typing import Generator - -import pytest - -from benchmarks import WINDOWS_SKIP_REASON -from reflex import constants -from reflex.compiler import utils -from reflex.testing import AppHarness, chdir -from reflex.utils import build -from reflex.utils.prerequisites import get_web_dir - -web_pages = get_web_dir() / constants.Dirs.PAGES - - -def render_multiple_pages(app, num: int): - """Add multiple pages based on num. - - Args: - app: The App object. - num: number of pages to render. - - """ - from typing import Tuple - - from rxconfig import config # pyright: ignore [reportMissingImports] - - import reflex as rx - - docs_url = "https://reflex.dev/docs/getting-started/introduction/" - filename = f"{config.app_name}/{config.app_name}.py" - college = [ - "Stanford University", - "Arizona", - "Arizona state", - "Baylor", - "Boston College", - "Boston University", - ] - - class State(rx.State): - """The app state.""" - - position: rx.Field[str] - college: rx.Field[str] - age: rx.Field[Tuple[int, int]] = rx.field((18, 50)) - salary: rx.Field[Tuple[int, int]] = rx.field((0, 25000000)) - - @rx.event - def set_position(self, value: str): - self.position = value - - @rx.event - def set_college(self, value: str): - self.college = value - - @rx.event - def set_age(self, value: list[int]): - self.age = (value[0], value[1]) - - @rx.event - def set_salary(self, value: list[int]): - self.salary = (value[0], value[1]) - - comp1 = rx.center( - rx.theme_panel(), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text("Get started by editing ", rx.code(filename)), - rx.button( - "Check out our docs!", - on_click=lambda: rx.redirect(docs_url), - size="4", - ), - align="center", - spacing="7", - font_size="2em", - ), - height="100vh", - ) - - comp2 = rx.vstack( - rx.hstack( - rx.vstack( - rx.select( - ["C", "PF", "SF", "PG", "SG"], - placeholder="Select a position. (All)", - on_change=State.set_position, - size="3", - ), - rx.select( - college, - placeholder="Select a college. (All)", - on_change=State.set_college, - size="3", - ), - ), - rx.vstack( - rx.vstack( - rx.hstack( - rx.badge("Min Age: ", State.age[0]), - rx.divider(orientation="vertical"), - rx.badge("Max Age: ", State.age[1]), - ), - rx.slider( - default_value=[18, 50], - min=18, - max=50, - on_value_commit=State.set_age, - ), - align_items="left", - width="100%", - ), - rx.vstack( - rx.hstack( - rx.badge("Min Sal: ", State.salary[0] // 1000000, "M"), - rx.divider(orientation="vertical"), - rx.badge("Max Sal: ", State.salary[1] // 1000000, "M"), - ), - rx.slider( - default_value=[0, 25000000], - min=0, - max=25000000, - on_value_commit=State.set_salary, - ), - align_items="left", - width="100%", - ), - ), - spacing="4", - ), - width="100%", - ) - - for i in range(1, num + 1): - if i % 2 == 1: - app.add_page(comp1, route=f"page{i}") - else: - app.add_page(comp2, route=f"page{i}") - - -def AppWithOnePage(): - """A reflex app with one page.""" - from rxconfig import config # pyright: ignore [reportMissingImports] - - import reflex as rx - - docs_url = "https://reflex.dev/docs/getting-started/introduction/" - filename = f"{config.app_name}/{config.app_name}.py" - - class State(rx.State): - """The app state.""" - - pass - - def index() -> rx.Component: - return rx.center( - rx.input( - id="token", value=State.router.session.client_token, is_read_only=True - ), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text("Get started by editing ", rx.code(filename)), - rx.button( - "Check out our docs!", - on_click=lambda: rx.redirect(docs_url), - size="4", - ), - align="center", - spacing="7", - font_size="2em", - ), - height="100vh", - ) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithTenPages(): - """A reflex app with 10 pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 10) - - -def AppWithHundredPages(): - """A reflex app with 100 pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 100) - - -def AppWithThousandPages(): - """A reflex app with Thousand pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 1000) - - -def AppWithTenThousandPages(): - """A reflex app with ten thousand pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 10000) - - -@pytest.fixture(scope="session") -def app_with_one_page( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1") - - yield AppHarness.create(root=root, app_source=AppWithOnePage) - - -@pytest.fixture(scope="session") -def app_with_ten_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app10") - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_hundred_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 100 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app100") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithHundredPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_thousand_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 1000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1000") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithThousandPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_ten_thousand_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app10000") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenThousandPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1_compile_time_cold(benchmark, app_with_one_page): - """Test the compile time on a cold start for an app with 1 page. - - Args: - benchmark: The benchmark fixture. - app_with_one_page: The app harness. - """ - - def setup(): - with chdir(app_with_one_page.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_one_page._initialize_app() - build.setup_frontend(app_with_one_page.app_path) - - def benchmark_fn(): - with chdir(app_with_one_page.app_path): - app_with_one_page.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_one_page._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1_compile_time_warm(benchmark, app_with_one_page): - """Test the compile time on a warm start for an app with 1 page. - - Args: - benchmark: The benchmark fixture. - app_with_one_page: The app harness. - """ - with chdir(app_with_one_page.app_path): - app_with_one_page._initialize_app() - build.setup_frontend(app_with_one_page.app_path) - - def benchmark_fn(): - with chdir(app_with_one_page.app_path): - app_with_one_page.app_instance._compile() - - benchmark(benchmark_fn) - app_with_one_page._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_cold(benchmark, app_with_ten_pages): - """Test the compile time on a cold start for an app with 10 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_pages: The app harness. - """ - - def setup(): - with chdir(app_with_ten_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_ten_pages._initialize_app() - build.setup_frontend(app_with_ten_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_ten_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_warm(benchmark, app_with_ten_pages): - """Test the compile time on a warm start for an app with 10 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_pages: The app harness. - """ - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages._initialize_app() - build.setup_frontend(app_with_ten_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_ten_pages._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages): - """Test the compile time on a cold start for an app with 100 page. - - Args: - benchmark: The benchmark fixture. - app_with_hundred_pages: The app harness. - """ - - def setup(): - with chdir(app_with_hundred_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_hundred_pages._initialize_app() - build.setup_frontend(app_with_hundred_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_hundred_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_warm(benchmark, app_with_hundred_pages): - """Test the compile time on a warm start for an app with 100 page. - - Args: - benchmark: The benchmark fixture. - app_with_hundred_pages: The app harness. - """ - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages._initialize_app() - build.setup_frontend(app_with_hundred_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_hundred_pages._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages): - """Test the compile time on a cold start for an app with 1000 page. - - Args: - benchmark: The benchmark fixture. - app_with_thousand_pages: The app harness. - """ - - def setup(): - with chdir(app_with_thousand_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_thousand_pages._initialize_app() - build.setup_frontend(app_with_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_thousand_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_warm(benchmark, app_with_thousand_pages): - """Test the compile time on a warm start for an app with 1000 page. - - Args: - benchmark: The benchmark fixture. - app_with_thousand_pages: The app harness. - """ - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages._initialize_app() - build.setup_frontend(app_with_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_thousand_pages._reload_state_module() - - -@pytest.mark.skip -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages): - """Test the compile time on a cold start for an app with 10000 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_thousand_pages: The app harness. - """ - - def setup(): - with chdir(app_with_ten_thousand_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_ten_thousand_pages._initialize_app() - build.setup_frontend(app_with_ten_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_thousand_pages.app_path): - app_with_ten_thousand_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_ten_thousand_pages._reload_state_module() - - -@pytest.mark.skip -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10000_compile_time_warm(benchmark, app_with_ten_thousand_pages): - """Test the compile time on a warm start for an app with 10000 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_thousand_pages: The app harness. - """ - - def benchmark_fn(): - with chdir(app_with_ten_thousand_pages.app_path): - app_with_ten_thousand_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_ten_thousand_pages._reload_state_module() From 7c4257a222bcda6401f834b267948b2a953d1587 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 18:36:30 -0800 Subject: [PATCH 37/38] give option to only use main thread (#4809) * give option to only use main thread * change default to main thread * fix comment * default to None, as 0 would raise a ValueError Co-authored-by: Masen Furer * add warning about passing 0 * move executor to config --------- Co-authored-by: Masen Furer --- reflex/app.py | 62 ++++++++++++------------ reflex/compiler/compiler.py | 2 +- reflex/config.py | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 32 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 2c8e889fc..d0ee06ae9 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -11,12 +11,11 @@ import functools import inspect import io import json -import multiprocessing -import platform import sys import traceback from datetime import datetime from pathlib import Path +from timeit import default_timer as timer from types import SimpleNamespace from typing import ( TYPE_CHECKING, @@ -76,7 +75,7 @@ from reflex.components.core.client_side_routing import ( from reflex.components.core.sticky import sticky from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes -from reflex.config import environment, get_config +from reflex.config import ExecutorType, environment, get_config from reflex.event import ( _EVENT_FIELDS, Event, @@ -1114,10 +1113,23 @@ class App(MiddlewareMixin, LifespanMixin): app_wrappers[(1, "ToasterProvider")] = toast_provider with console.timing("Evaluate Pages (Frontend)"): + performance_metrics: list[tuple[str, float]] = [] for route in self._unevaluated_pages: console.debug(f"Evaluating page: {route}") + start = timer() self._compile_page(route, save_page=should_compile) + end = timer() + performance_metrics.append((route, end - start)) progress.advance(task) + console.debug( + "Slowest pages:\n" + + "\n".join( + f"{route}: {time * 1000:.1f}ms" + for route, time in sorted( + performance_metrics, key=lambda x: x[1], reverse=True + )[:10] + ) + ) # Add the optional endpoints (_upload) self._add_optional_endpoints() @@ -1130,7 +1142,7 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) # Store the compile results. - compile_results = [] + compile_results: list[tuple[str, str]] = [] progress.advance(task) @@ -1209,33 +1221,19 @@ class App(MiddlewareMixin, LifespanMixin): ), ) - # Use a forking process pool, if possible. Much faster, especially for large sites. - # Fallback to ThreadPoolExecutor as something that will always work. - executor = None - if ( - platform.system() in ("Linux", "Darwin") - and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get()) - is not None - ): - executor = concurrent.futures.ProcessPoolExecutor( - max_workers=number_of_processes or None, - mp_context=multiprocessing.get_context("fork"), - ) - else: - executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS.get() or None - ) + executor = ExecutorType.get_executor_from_environment() for route, component in zip(self._pages, page_components, strict=True): ExecutorSafeFunctions.COMPONENTS[route] = component ExecutorSafeFunctions.STATE = self._state - with executor: - result_futures = [] + with console.timing("Compile to Javascript"), executor as executor: + result_futures: list[concurrent.futures.Future[tuple[str, str]]] = [] - def _submit_work(fn: Callable, *args, **kwargs): + def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs): f = executor.submit(fn, *args, **kwargs) + f.add_done_callback(lambda _: progress.advance(task)) result_futures.append(f) # Compile the pre-compiled pages. @@ -1261,10 +1259,10 @@ class App(MiddlewareMixin, LifespanMixin): _submit_work(compiler.remove_tailwind_from_postcss) # Wait for all compilation tasks to complete. - with console.timing("Compile to Javascript"): - for future in concurrent.futures.as_completed(result_futures): - compile_results.append(future.result()) - progress.advance(task) + compile_results.extend( + future.result() + for future in concurrent.futures.as_completed(result_futures) + ) app_root = self._app_root(app_wrappers=app_wrappers) @@ -1289,10 +1287,12 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) # Compile custom components. - *custom_components_result, custom_components_imports = ( - compiler.compile_components(custom_components) - ) - compile_results.append(custom_components_result) + ( + custom_components_output, + custom_components_result, + custom_components_imports, + ) = compiler.compile_components(custom_components) + compile_results.append((custom_components_output, custom_components_result)) all_imports.update(custom_components_imports) progress.advance(task) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 667a477e8..81de50182 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -508,7 +508,7 @@ def compile_tailwind( The compiled Tailwind config. """ # Get the path for the output file. - output_path = get_web_dir() / constants.Tailwind.CONFIG + output_path = str((get_web_dir() / constants.Tailwind.CONFIG).absolute()) # Compile the config. code = _compile_tailwind(config) diff --git a/reflex/config.py b/reflex/config.py index 33009b3bc..d0829e627 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -2,11 +2,14 @@ from __future__ import annotations +import concurrent.futures import dataclasses import enum import importlib import inspect +import multiprocessing import os +import platform import sys import threading import urllib.parse @@ -17,6 +20,7 @@ from types import ModuleType from typing import ( TYPE_CHECKING, Any, + Callable, Dict, Generic, List, @@ -497,6 +501,95 @@ class PerformanceMode(enum.Enum): OFF = "off" +class ExecutorType(enum.Enum): + """Executor for compiling the frontend.""" + + THREAD = "thread" + PROCESS = "process" + MAIN_THREAD = "main_thread" + + @classmethod + def get_executor_from_environment(cls): + """Get the executor based on the environment variables. + + Returns: + The executor. + """ + executor_type = environment.REFLEX_COMPILE_EXECUTOR.get() + + reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get() + reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get() + # By default, use the main thread. Unless the user has specified a different executor. + # Using a process pool is much faster, but not supported on all platforms. It's gated behind a flag. + if executor_type is None: + if ( + platform.system() not in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + console.warn("Multiprocessing is only supported on Linux and MacOS.") + + if ( + platform.system() in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + if reflex_compile_processes == 0: + console.warn( + "Number of processes must be greater than 0. If you want to use the default number of processes, set REFLEX_COMPILE_EXECUTOR to 'process'. Defaulting to None." + ) + reflex_compile_processes = None + elif reflex_compile_processes < 0: + console.warn( + "Number of processes must be greater than 0. Defaulting to None." + ) + reflex_compile_processes = None + executor_type = ExecutorType.PROCESS + elif reflex_compile_threads is not None: + if reflex_compile_threads == 0: + console.warn( + "Number of threads must be greater than 0. If you want to use the default number of threads, set REFLEX_COMPILE_EXECUTOR to 'thread'. Defaulting to None." + ) + reflex_compile_threads = None + elif reflex_compile_threads < 0: + console.warn( + "Number of threads must be greater than 0. Defaulting to None." + ) + reflex_compile_threads = None + executor_type = ExecutorType.THREAD + else: + executor_type = ExecutorType.MAIN_THREAD + + match executor_type: + case ExecutorType.PROCESS: + executor = concurrent.futures.ProcessPoolExecutor( + max_workers=reflex_compile_processes, + mp_context=multiprocessing.get_context("fork"), + ) + case ExecutorType.THREAD: + executor = concurrent.futures.ThreadPoolExecutor( + max_workers=reflex_compile_threads + ) + case ExecutorType.MAIN_THREAD: + FUTURE_RESULT_TYPE = TypeVar("FUTURE_RESULT_TYPE") + + class MainThreadExecutor: + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def submit( + self, fn: Callable[..., FUTURE_RESULT_TYPE], *args, **kwargs + ) -> concurrent.futures.Future[FUTURE_RESULT_TYPE]: + future_job = concurrent.futures.Future() + future_job.set_result(fn(*args, **kwargs)) + return future_job + + executor = MainThreadExecutor() + + return executor + + class EnvironmentVariables: """Environment variables class to instantiate environment variables.""" @@ -538,6 +631,8 @@ class EnvironmentVariables: Path(constants.Dirs.UPLOADED_FILES) ) + REFLEX_COMPILE_EXECUTOR: EnvVar[Optional[ExecutorType]] = env_var(None) + # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor. REFLEX_COMPILE_PROCESSES: EnvVar[Optional[int]] = env_var(None) From 10bae9577c0f898f7ae9b2d540336058bda53837 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 13 Feb 2025 22:49:27 -0800 Subject: [PATCH 38/38] only write if file changed (#4822) * only write if file changed * preface it on it existing --- reflex/compiler/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 91ee18b86..c66dfe304 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -523,6 +523,8 @@ def write_page(path: str | Path, code: str): """ path = Path(path) path_ops.mkdir(path.parent) + if path.exists() and path.read_text(encoding="utf-8") == code: + return path.write_text(code, encoding="utf-8")