Skip to content

Commit 8be411b

Browse files
authored
Implement var slicing (#289)
1 parent 5d3ecad commit 8be411b

File tree

6 files changed

+102
-30
lines changed

6 files changed

+102
-30
lines changed

pynecone/compiler/utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
import os
5-
from typing import Dict, List, Set, Tuple, Type
5+
from typing import Dict, List, Optional, Set, Tuple, Type
66

77
from pynecone import constants, utils
88
from pynecone.compiler import templates
@@ -306,13 +306,14 @@ def write_page(path: str, code: str):
306306
f.write(code)
307307

308308

309-
def empty_dir(path, keep_files=[]):
310-
"""Remove all files and folders in a directory except for the kept file- or foldernames.
309+
def empty_dir(path: str, keep_files: Optional[List[str]] = None):
310+
"""Remove all files and folders in a directory except for the keep_files.
311311
312312
Args:
313-
path (str): The path to the directory that will be emptied
314-
keep_files (list, optional): List of filenames or foldernames that will not be deleted. Defaults to [].
313+
path: The path to the directory that will be emptied
314+
keep_files: List of filenames or foldernames that will not be deleted.
315315
"""
316+
keep_files = keep_files or []
316317
directory_contents = os.listdir(path)
317318
for element in directory_contents:
318319
if element not in keep_files:

pynecone/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import os
44
import re
5-
65
from enum import Enum
76
from types import SimpleNamespace
87

pynecone/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import sys
1616
from collections import defaultdict
1717
from pathlib import Path
18-
from subprocess import PIPE, DEVNULL, STDOUT
18+
from subprocess import DEVNULL, PIPE, STDOUT
1919
from types import ModuleType
2020
from typing import _GenericAlias # type: ignore
2121
from typing import (

pynecone/var.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,33 +138,64 @@ def __getitem__(self, i: Any) -> Var:
138138
Raises:
139139
TypeError: If the var is not indexable.
140140
"""
141+
# Indexing is only supported for lists, dicts, and dataframes.
142+
if not (
143+
utils._issubclass(self.type_, Union[List, Dict])
144+
or utils.is_dataframe(self.type_)
145+
):
146+
raise TypeError(
147+
f"Var {self.name} of type {self.type_} does not support indexing."
148+
)
149+
141150
# The type of the indexed var.
142-
type_ = str
151+
type_ = Any
143152

144153
# Convert any vars to local vars.
145154
if isinstance(i, Var):
146155
i = BaseVar(name=i.name, type_=i.type_, state=i.state, is_local=True)
147156

157+
# Handle list indexing.
148158
if utils._issubclass(self.type_, List):
149-
assert isinstance(
150-
i, utils.get_args(Union[int, Var])
151-
), "Index must be an integer."
159+
# List indices must be ints, slices, or vars.
160+
if not isinstance(i, utils.get_args(Union[int, slice, Var])):
161+
raise TypeError("Index must be an integer.")
162+
163+
# Handle slices first.
164+
if isinstance(i, slice):
165+
# Get the start and stop indices.
166+
start = i.start or 0
167+
stop = i.stop or "undefined"
168+
169+
# Use the slice function.
170+
return BaseVar(
171+
name=f"{self.name}.slice({start}, {stop})",
172+
type_=self.type_,
173+
state=self.state,
174+
)
175+
176+
# Get the type of the indexed var.
152177
if utils.is_generic_alias(self.type_):
153178
type_ = utils.get_args(self.type_)[0]
154179
else:
155180
type_ = Any
156-
elif utils._issubclass(self.type_, Dict) or utils.is_dataframe(self.type_):
157-
if isinstance(i, str):
158-
i = utils.wrap(i, '"')
159-
if utils.is_generic_alias(self.type_):
160-
type_ = utils.get_args(self.type_)[1]
161-
else:
162-
type_ = Any
163-
else:
164-
raise TypeError(
165-
f"Var {self.name} of type {self.type_} does not support indexing."
181+
182+
# Use `at` to support negative indices.
183+
return BaseVar(
184+
name=f"{self.name}.at({i})",
185+
type_=type_,
186+
state=self.state,
166187
)
167188

189+
# Dictionary / dataframe indexing.
190+
# Get the type of the indexed var.
191+
if isinstance(i, str):
192+
i = utils.wrap(i, '"')
193+
if utils.is_generic_alias(self.type_):
194+
type_ = utils.get_args(self.type_)[1]
195+
else:
196+
type_ = Any
197+
198+
# Use normal indexing here.
168199
return BaseVar(
169200
name=f"{self.name}[{i}]",
170201
type_=type_,
@@ -621,6 +652,7 @@ class BaseVar(Var, Base):
621652
# Whether this is a local javascript variable.
622653
is_local: bool = False
623654

655+
# Whether this var is a raw string.
624656
is_string: bool = False
625657

626658
def __hash__(self) -> int:

tests/test_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,10 @@ def test_default_setters(test_state):
251251
def test_class_indexing_with_vars():
252252
"""Test that we can index into a state var with another var."""
253253
prop = TestState.array[TestState.num1]
254-
assert str(prop) == "{test_state.array[test_state.num1]}"
254+
assert str(prop) == "{test_state.array.at(test_state.num1)}"
255255

256256
prop = TestState.mapping["a"][TestState.num1]
257-
assert str(prop) == '{test_state.mapping["a"][test_state.num1]}'
257+
assert str(prop) == '{test_state.mapping["a"].at(test_state.num1)}'
258258

259259

260260
def test_class_attributes():

tests/test_var.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Dict, List
2+
13
import pytest
24

35
from pynecone.base import Base
@@ -135,18 +137,18 @@ def test_create(value, expected):
135137
assert prop.equals(expected) # type: ignore
136138

137139

140+
def v(value) -> Var:
141+
val = Var.create(value)
142+
assert val is not None
143+
return val
144+
145+
138146
def test_basic_operations(TestObj):
139147
"""Test the var operations.
140148
141149
Args:
142150
TestObj: The test object.
143151
"""
144-
145-
def v(value) -> Var:
146-
val = Var.create(value)
147-
assert val is not None
148-
return val
149-
150152
assert str(v(1) == v(2)) == "{(1 == 2)}"
151153
assert str(v(1) != v(2)) == "{(1 != 2)}"
152154
assert str(v(1) < v(2)) == "{(1 < 2)}"
@@ -162,8 +164,46 @@ def v(value) -> Var:
162164
assert str(v(1) ** v(2)) == "{Math.pow(1 , 2)}"
163165
assert str(v(1) & v(2)) == "{(1 && 2)}"
164166
assert str(v(1) | v(2)) == "{(1 || 2)}"
165-
assert str(v([1, 2, 3])[v(0)]) == "{[1, 2, 3][0]}"
167+
assert str(v([1, 2, 3])[v(0)]) == "{[1, 2, 3].at(0)}"
166168
assert str(v({"a": 1, "b": 2})["a"]) == '{{"a": 1, "b": 2}["a"]}'
167169
assert (
168170
str(BaseVar(name="foo", state="state", type_=TestObj).bar) == "{state.foo.bar}"
169171
)
172+
assert str(abs(v(1))) == "{Math.abs(1)}"
173+
assert str(v([1, 2, 3]).length()) == "{[1, 2, 3].length}"
174+
175+
176+
def test_var_indexing_lists():
177+
"""Test that we can index into list vars."""
178+
lst = BaseVar(name="lst", type_=List[int])
179+
180+
# Test basic indexing.
181+
assert str(lst[0]) == "{lst.at(0)}"
182+
assert str(lst[1]) == "{lst.at(1)}"
183+
184+
# Test negative indexing.
185+
assert str(lst[-1]) == "{lst.at(-1)}"
186+
187+
# Test non-integer indexing raises an error.
188+
with pytest.raises(TypeError):
189+
lst["a"]
190+
with pytest.raises(TypeError):
191+
lst[1.5]
192+
193+
194+
def test_var_list_slicing():
195+
"""Test that we can slice into list vars."""
196+
lst = BaseVar(name="lst", type_=List[int])
197+
198+
assert str(lst[0:1]) == "{lst.slice(0, 1)}"
199+
assert str(lst[:1]) == "{lst.slice(0, 1)}"
200+
assert str(lst[0:]) == "{lst.slice(0, undefined)}"
201+
202+
203+
def test_dict_indexing():
204+
"""Test that we can index into dict vars."""
205+
dct = BaseVar(name="dct", type_=Dict[str, int])
206+
207+
# Check correct indexing.
208+
assert str(dct["a"]) == '{dct["a"]}'
209+
assert str(dct["asdf"]) == '{dct["asdf"]}'

0 commit comments

Comments
 (0)