Revamp Imports (#926)

This commit is contained in:
Elijah Ahianyo 2023-05-05 05:11:01 +00:00 committed by GitHub
parent 4f182b3170
commit 18c715670a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 192 additions and 89 deletions

View File

@ -11,20 +11,25 @@ from pynecone.components.component import Component, CustomComponent
from pynecone.state import State
from pynecone.style import Style
from pynecone.utils import imports, path_ops
from pynecone.var import ImportVar
# Imports to be included in every Pynecone app.
DEFAULT_IMPORTS: imports.ImportDict = {
"react": {"useEffect", "useRef", "useState"},
"next/router": {"useRouter"},
f"/{constants.STATE_PATH}": {
"connect",
"updateState",
"uploadFiles",
"E",
"isTrue",
"react": {
ImportVar(tag="useEffect"),
ImportVar(tag="useRef"),
ImportVar(tag="useState"),
},
"": {"focus-visible/dist/focus-visible"},
"@chakra-ui/react": {constants.USE_COLOR_MODE},
"next/router": {ImportVar(tag="useRouter")},
f"/{constants.STATE_PATH}": {
ImportVar(tag="connect"),
ImportVar(tag="updateState"),
ImportVar(tag="uploadFiles"),
ImportVar(tag="E"),
ImportVar(tag="isTrue"),
},
"": {ImportVar(tag="focus-visible/dist/focus-visible")},
"@chakra-ui/react": {ImportVar(tag=constants.USE_COLOR_MODE)},
}
@ -91,8 +96,8 @@ def _compile_components(components: Set[CustomComponent]) -> str:
The compiled components.
"""
imports = {
"react": {"memo"},
f"/{constants.STATE_PATH}": {"E", "isTrue"},
"react": {ImportVar(tag="memo")},
f"/{constants.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")},
}
component_defs = []

View File

@ -25,12 +25,13 @@ from pynecone.event import get_hydrate_event
from pynecone.state import State
from pynecone.style import Style
from pynecone.utils import format, imports, path_ops
from pynecone.var import ImportVar
# To re-export this function.
merge_imports = imports.merge_imports
def compile_import_statement(lib: str, fields: Set[str]) -> str:
def compile_import_statement(lib: str, fields: Set[ImportVar]) -> str:
"""Compile an import statement.
Args:
@ -41,16 +42,12 @@ def compile_import_statement(lib: str, fields: Set[str]) -> str:
The compiled import statement.
"""
# Check for default imports.
defaults = {
field
for field in fields
if field.lower() == lib.lower().replace("-", "").replace("/", "")
}
defaults = {field for field in fields if field.is_default}
assert len(defaults) < 2
# Get the default import, and the specific imports.
default = next(iter(defaults), "")
rest = fields - defaults
default = next(iter({field.name for field in defaults}), "")
rest = {field.name for field in fields - defaults}
return templates.format_import(lib=lib, default=default, rest=rest)

View File

@ -13,3 +13,5 @@ class Head(NextHeadLib):
"""Head Component."""
tag = "NextHead"
is_default = True

View File

@ -22,7 +22,7 @@ from pynecone.event import (
)
from pynecone.style import Style
from pynecone.utils import format, imports, path_ops, types
from pynecone.var import BaseVar, Var
from pynecone.var import BaseVar, ImportVar, Var
class Component(Base, ABC):
@ -43,6 +43,12 @@ class Component(Base, ABC):
# The tag to use when rendering the component.
tag: Optional[str] = None
# The alias for the tag.
alias: Optional[str] = None
# Whether the import is default or named.
is_default: Optional[bool] = False
# A unique key for the component.
key: Any = None
@ -275,15 +281,6 @@ class Component(Base, ABC):
"""
return {}
@classmethod
def get_alias(cls) -> Optional[str]:
"""Get the alias for the component.
Returns:
The alias.
"""
return None
def __repr__(self) -> str:
"""Represent the component in React.
@ -307,9 +304,10 @@ class Component(Base, ABC):
The tag to render.
"""
# Create the base tag.
alias = self.get_alias()
name = alias if alias is not None else self.tag
tag = Tag(name=name, special_props=self.special_props)
tag = Tag(
name=self.tag if not self.alias else self.alias,
special_props=self.special_props,
)
# Add component props to the tag.
props = {attr: getattr(self, attr) for attr in self.get_props()}
@ -445,9 +443,7 @@ class Component(Base, ABC):
def _get_imports(self) -> imports.ImportDict:
if self.library is not None and self.tag is not None:
alias = self.get_alias()
tag = self.tag if alias is None else " as ".join([self.tag, alias])
return {self.library: {tag}}
return {self.library: {self.import_var}}
return {}
def get_imports(self) -> imports.ImportDict:
@ -521,6 +517,15 @@ class Component(Base, ABC):
custom_components |= child.get_custom_components(seen=seen)
return custom_components
@property
def import_var(self):
"""The tag to import.
Returns:
An import var.
"""
return ImportVar(tag=self.tag, is_default=self.is_default, alias=self.alias)
def is_full_control(self, kwargs: dict) -> bool:
"""Return if the component is fully controlled input.

View File

@ -6,7 +6,7 @@ from pynecone.components.component import Component
from pynecone.components.libs.chakra import ChakraComponent
from pynecone.style import Style
from pynecone.utils import imports
from pynecone.var import Var
from pynecone.var import ImportVar, Var
# Path to the prism styles.
PRISM_STYLES_PATH = "/styles/code/prism"
@ -19,6 +19,8 @@ class CodeBlock(Component):
tag = "Prism"
is_default = True
# The theme to use ("light" or "dark").
theme: Var[str]
@ -44,7 +46,7 @@ class CodeBlock(Component):
merged_imports = super()._get_imports()
if self.theme is not None:
merged_imports = imports.merge_imports(
merged_imports, {PRISM_STYLES_PATH: {self.theme.name}}
merged_imports, {PRISM_STYLES_PATH: {ImportVar(tag=self.theme.name)}}
)
return merged_imports

View File

@ -1,11 +1,11 @@
"""Table components."""
from typing import Any, List, Optional
from typing import Any, List
from pynecone.components.component import Component
from pynecone.components.tags import Tag
from pynecone.utils import format, imports, types
from pynecone.var import BaseVar, ComputedVar, Var
from pynecone.var import BaseVar, ComputedVar, ImportVar, Var
class Gridjs(Component):
@ -19,6 +19,8 @@ class DataTable(Gridjs):
tag = "Grid"
alias = "DataTableGrid"
# The data to display. Either a list of lists or a pandas dataframe.
data: Any
@ -38,15 +40,6 @@ class DataTable(Gridjs):
# Enable pagination.
pagination: Var[bool]
@classmethod
def get_alias(cls) -> Optional[str]:
"""Get the alias for the component.
Returns:
The alias.
"""
return "DataTableGrid"
@classmethod
def create(cls, *children, **props):
"""Create a datatable component.
@ -102,7 +95,8 @@ class DataTable(Gridjs):
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(), {"": {"gridjs/dist/theme/mermaid.css"}}
super()._get_imports(),
{"": {ImportVar(tag="gridjs/dist/theme/mermaid.css")}},
)
def _render(self) -> Tag:

View File

@ -5,7 +5,7 @@ from typing import Dict
from pynecone.components.component import EVENT_ARG
from pynecone.components.libs.chakra import ChakraComponent
from pynecone.utils import imports
from pynecone.var import Var
from pynecone.var import ImportVar, Var
class Input(ChakraComponent):
@ -13,7 +13,7 @@ class Input(ChakraComponent):
tag = "Input"
# State var to bind the the input.
# State var to bind the input.
value: Var[str]
# The default value of the input.
@ -52,7 +52,7 @@ class Input(ChakraComponent):
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
{"/utils/state": {"set_val"}},
{"/utils/state": {ImportVar(tag="set_val")}},
)
@classmethod

View File

@ -13,7 +13,7 @@ class NumberInput(ChakraComponent):
tag = "NumberInput"
# State var to bind the the input.
# State var to bind the input.
value: Var[int]
# If true, the input's value will change based on mouse wheel.

View File

@ -15,7 +15,7 @@ class Select(ChakraComponent):
tag = "Select"
# State var to bind the the select.
# State var to bind the select.
value: Var[str]
# The default value of the select.

View File

@ -13,7 +13,7 @@ class Slider(ChakraComponent):
tag = "Slider"
# State var to bind the the input.
# State var to bind the input.
value: Var[int]
# The color scheme.

View File

@ -12,7 +12,7 @@ class TextArea(ChakraComponent):
tag = "Textarea"
# State var to bind the the input.
# State var to bind the input.
value: Var[str]
# The default value of the textarea.

View File

@ -18,6 +18,8 @@ class Upload(Component):
tag = "ReactDropzone"
is_default = True
# The list of accepted file types. This should be a dictionary of MIME types as keys and array of file formats as
# values.
# supported MIME types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

View File

@ -11,6 +11,8 @@ class NextLink(Component):
tag = "NextLink"
is_default = True
# The page to link to.
href: Var[str]

View File

@ -5,7 +5,7 @@ from typing import List, Union
from pynecone.components.component import Component
from pynecone.utils import types
from pynecone.var import BaseVar, Var
from pynecone.var import BaseVar, ImportVar, Var
class Markdown(Component):
@ -15,6 +15,8 @@ class Markdown(Component):
tag = "ReactMarkdown"
is_default = True
@classmethod
def create(cls, *children, **props) -> Component:
"""Create a markdown component.
@ -39,20 +41,20 @@ class Markdown(Component):
def _get_imports(self):
imports = super()._get_imports()
imports["@chakra-ui/react"] = {
"Heading",
"Code",
"Text",
"Link",
"UnorderedList",
"OrderedList",
"ListItem",
ImportVar(tag="Heading"),
ImportVar(tag="Code"),
ImportVar(tag="Text"),
ImportVar(tag="Link"),
ImportVar(tag="UnorderedList"),
ImportVar(tag="OrderedList"),
ImportVar(tag="ListItem"),
}
imports["react-syntax-highlighter"] = {"Prism"}
imports["remark-math"] = {"remarkMath"}
imports["remark-gfm"] = {"remarkGfm"}
imports["rehype-katex"] = {"rehypeKatex"}
imports["rehype-raw"] = {"rehypeRaw"}
imports[""] = {"katex/dist/katex.min.css"}
imports["react-syntax-highlighter"] = {ImportVar(tag="Prism", is_default=True)}
imports["remark-math"] = {ImportVar(tag="remarkMath", is_default=True)}
imports["remark-gfm"] = {ImportVar(tag="remarkGfm", is_default=True)}
imports["rehype-katex"] = {ImportVar(tag="rehypeKatex", is_default=True)}
imports["rehype-raw"] = {ImportVar(tag="rehypeRaw", is_default=True)}
imports[""] = {ImportVar(tag="katex/dist/katex.min.css")}
return imports
def _render(self):

View File

@ -6,7 +6,7 @@ from pynecone.var import Var as PCVar
class A(Element): # noqa: E742
"""Display the a element."""
"""Display the 'a' element."""
tag = "a"

View File

@ -3,7 +3,9 @@
from collections import defaultdict
from typing import Dict, Set
ImportDict = Dict[str, Set[str]]
from pynecone.var import ImportVar
ImportDict = Dict[str, Set[ImportVar]]
def merge_imports(*imports) -> ImportDict:

View File

@ -34,7 +34,6 @@ from pynecone.utils import format, types
if TYPE_CHECKING:
from pynecone.state import State
# Set of unique variable names.
USED_VARIABLES = set()
@ -1071,3 +1070,33 @@ class PCDict(dict):
"""
super().__delitem__(*args, **kwargs)
self._reassign_field()
class ImportVar(Base):
"""An import var."""
# The name of the import tag.
tag: Optional[str]
# whether the import is default or named.
is_default: Optional[bool] = False
# The tag alias.
alias: Optional[str] = None
@property
def name(self) -> str:
"""The name of the import.
Returns:
The name(tag name with alias) of tag.
"""
return self.tag if not self.alias else " as ".join([self.tag, self.alias]) # type: ignore
def __hash__(self) -> int:
"""Define a hash function for the import var.
Returns:
The hash of the var.
"""
return hash((self.tag, self.is_default, self.alias))

View File

@ -4,17 +4,34 @@ import pytest
from pynecone.compiler import utils
from pynecone.utils import imports
from pynecone.var import ImportVar
@pytest.mark.parametrize(
"lib,fields,output",
[
("axios", {"axios"}, 'import axios from "axios"'),
("axios", {"foo", "bar"}, 'import {bar, foo} from "axios"'),
("axios", {"axios", "foo", "bar"}, 'import axios, {bar, foo} from "axios"'),
(
"axios",
{ImportVar(tag="axios", is_default=True)},
'import axios from "axios"',
),
(
"axios",
{ImportVar(tag="foo"), ImportVar(tag="bar")},
'import {bar, foo} from "axios"',
),
(
"axios",
{
ImportVar(tag="axios", is_default=True),
ImportVar(tag="foo"),
ImportVar(tag="bar"),
},
"import " "axios, " "{bar, " "foo} from " '"axios"',
),
],
)
def test_compile_import_statement(lib: str, fields: Set[str], output: str):
def test_compile_import_statement(lib: str, fields: Set[ImportVar], output: str):
"""Test the compile_import_statement function.
Args:
@ -29,15 +46,34 @@ def test_compile_import_statement(lib: str, fields: Set[str], output: str):
"import_dict,output",
[
({}, ""),
({"axios": {"axios"}}, 'import axios from "axios"'),
({"axios": {"foo", "bar"}}, 'import {bar, foo} from "axios"'),
(
{"axios": {"axios", "foo", "bar"}, "react": {"react"}},
{"axios": {ImportVar(tag="axios", is_default=True)}},
'import axios from "axios"',
),
(
{"axios": {ImportVar(tag="foo"), ImportVar(tag="bar")}},
'import {bar, foo} from "axios"',
),
(
{
"axios": {
ImportVar(tag="axios", is_default=True),
ImportVar(tag="foo"),
ImportVar(tag="bar"),
},
"react": {ImportVar(tag="react", is_default=True)},
},
'import axios, {bar, foo} from "axios"\nimport react from "react"',
),
({"": {"lib1.js", "lib2.js"}}, 'import "lib1.js"\nimport "lib2.js"'),
(
{"": {"lib1.js", "lib2.js"}, "axios": {"axios"}},
{"": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")}},
'import "lib1.js"\nimport "lib2.js"',
),
(
{
"": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")},
"axios": {ImportVar(tag="axios", is_default=True)},
},
'import "lib1.js"\nimport "lib2.js"\nimport axios from "axios"',
),
],

View File

@ -9,7 +9,7 @@ from pynecone.event import EVENT_ARG, EVENT_TRIGGERS, EventHandler
from pynecone.state import State
from pynecone.style import Style
from pynecone.utils import imports
from pynecone.var import Var
from pynecone.var import ImportVar, Var
@pytest.fixture
@ -42,7 +42,7 @@ def component1() -> Type[Component]:
number: Var[int]
def _get_imports(self) -> imports.ImportDict:
return {"react": {"Component"}}
return {"react": {ImportVar(tag="Component")}}
def _get_custom_code(self) -> str:
return "console.log('component1')"
@ -75,7 +75,7 @@ def component2() -> Type[Component]:
}
def _get_imports(self) -> imports.ImportDict:
return {"react-redux": {"connect"}}
return {"react-redux": {ImportVar(tag="connect")}}
def _get_custom_code(self) -> str:
return "console.log('component2')"
@ -206,8 +206,11 @@ def test_get_imports(component1, component2):
"""
c1 = component1.create()
c2 = component2.create(c1)
assert c1.get_imports() == {"react": {"Component"}}
assert c2.get_imports() == {"react-redux": {"connect"}, "react": {"Component"}}
assert c1.get_imports() == {"react": {ImportVar(tag="Component")}}
assert c2.get_imports() == {
"react-redux": {ImportVar(tag="connect")},
"react": {ImportVar(tag="Component")},
}
def test_get_custom_code(component1, component2):

View File

@ -4,7 +4,7 @@ import cloudpickle
import pytest
from pynecone.base import Base
from pynecone.var import BaseVar, PCDict, PCList, Var
from pynecone.var import BaseVar, ImportVar, PCDict, PCList, Var
test_vars = [
BaseVar(name="prop1", type_=int),
@ -14,6 +14,8 @@ test_vars = [
BaseVar(name="local2", type_=str, is_local=True),
]
test_import_vars = [ImportVar(tag="DataGrid"), ImportVar(tag="DataGrid", alias="Grid")]
@pytest.fixture
def TestObj():
@ -245,3 +247,23 @@ def test_pickleable_pc_dict():
pickled_dict = cloudpickle.dumps(pc_dict)
assert cloudpickle.loads(pickled_dict) == pc_dict
@pytest.mark.parametrize(
"import_var,expected",
zip(
test_import_vars,
[
"DataGrid",
"DataGrid as Grid",
],
),
)
def test_import_var(import_var, expected):
"""Test that the import var name is computed correctly.
Args:
import_var: The import var.
expected: expected name
"""
assert import_var.name == expected