diff --git a/remotestate-py/CHANGES.md b/remotestate-py/CHANGES.md index f04126f..7f7ce54 100644 --- a/remotestate-py/CHANGES.md +++ b/remotestate-py/CHANGES.md @@ -21,6 +21,11 @@ - Added public `PathInput` and `PathSegmentInput` aliases plus `normalize_path()` and `normalize_path_segment()` helpers under `remotestate.path`. +- Removed the Python-only `Property` and `Index` parsed path segment classes. + Parsed paths now use primitive tuple segments, such as `("items", 0, + "label")`, matching the TypeScript path model. +- Aligned `PathInput` with TypeScript: pass a string path or a sequence of + path segments. Root array entries can be addressed as `"[0]"` or `(0,)`. ## Version 0.2.0 diff --git a/remotestate-py/README.md b/remotestate-py/README.md index ee9ad05..f15b37f 100644 --- a/remotestate-py/README.md +++ b/remotestate-py/README.md @@ -101,9 +101,9 @@ class User: def defaults(path: rs.path.Path): - if path == (rs.path.Property("user"),): + if path == ("user",): return User() - if path == (rs.path.Property("items"),): + if path == ("items",): return [] return {} @@ -187,14 +187,13 @@ print("UI Base URL: ", result.ui_base_url) ## Paths `remotestate.path` exposes the parsed path types used by `Store.default_factory` and other -advanced integrations: +advanced integrations. Parsed paths are tuples of string property names and integer array +indices, matching the TypeScript package's `string | number` path segments: - `Path` - `PathSegment` - `PathInput` - `PathSegmentInput` -- `Property` -- `Index` RemoteState paths use a simplified [JSONPath](https://www.rfc-editor.org/info/rfc9535/) subset without the `"$."` prefix: diff --git a/remotestate-py/src/remotestate/path.py b/remotestate-py/src/remotestate/path.py index 1856405..b751473 100644 --- a/remotestate-py/src/remotestate/path.py +++ b/remotestate-py/src/remotestate/path.py @@ -2,42 +2,19 @@ import json import re from collections.abc import Sequence -from dataclasses import dataclass -@dataclass(frozen=True) -class Property: - """A named property segment in a RemoteState path. - - Args: - key: Property name. - """ - - key: str - - -@dataclass(frozen=True) -class Index: - """A list index segment in a RemoteState path. - - Args: - i: Zero-based list index. - """ - - i: int - - -# One parsed path segment. -type PathSegment = Property | Index +# A single segment in a RemoteState path. +type PathSegment = str | int # A parsed RemoteState path. type Path = tuple[PathSegment, ...] # A raw value accepted as one path segment. -type PathSegmentInput = str | int | PathSegment +type PathSegmentInput = str | int # A raw value accepted anywhere a RemoteState path is needed. -type PathInput = str | int | Sequence[PathSegmentInput] | Path +type PathInput = str | Sequence[PathSegmentInput] _IDENTIFIER_RE = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*") _INVALID_PATH_MESSAGE = "RemoteState paths must be valid simplified JSONPath paths" @@ -54,6 +31,50 @@ class Index: } +def normalize_path(path: PathInput) -> Path: + """Normalize a path input value into a validated RemoteState path. + + Args: + path: RemoteState path string or a sequence of path segment inputs + such as ``("items", 0, "label")``. + + Returns: + Parsed path. + + Raises: + TypeError: If ``path`` is not a supported path input value. + ValueError: If ``path`` contains an invalid segment. + """ + if isinstance(path, str): + return parse_path(path) + if isinstance(path, Sequence): + return tuple(normalize_path_segment(segment) for segment in path) + raise TypeError("RemoteState path must be a string or sequence of path segments") + + +def normalize_path_segment(segment: PathSegmentInput) -> PathSegment: + """Normalize one path segment input value into a validated path segment. + + Args: + segment: A string property name or integer index. + + Returns: + Parsed path segment. + + Raises: + TypeError: If ``segment`` is not a supported path segment input value. + ValueError: If an integer index is negative. + """ + if isinstance(segment, bool): + raise ValueError("RemoteState path indices must be non-negative integers") + if isinstance(segment, int): + _validate_index(segment) + return segment + if isinstance(segment, str): + return segment + raise TypeError("RemoteState path segments must be strings or integers") + + @functools.cache def parse_path(path: str) -> Path: """Parse a RemoteState path string. @@ -96,7 +117,7 @@ def parse_path(path: str) -> Path: first = _read_identifier(path, 0) if first is not None: - segments: list[PathSegment] = [Property(first[0])] + segments: list[PathSegment] = [first[0]] pos = first[1] elif path[0] == "[": bracket = _read_bracket_segment(path, 0) @@ -114,7 +135,7 @@ def parse_path(path: str) -> Path: identifier = _read_identifier(path, pos) if identifier is None: raise _invalid_path(path, pos) - segments.append(Property(identifier[0])) + segments.append(identifier[0]) pos = identifier[1] case "[": bracket = _read_bracket_segment(path, pos) @@ -128,56 +149,27 @@ def parse_path(path: str) -> Path: return tuple(segments) -def normalize_path(path: PathInput) -> Path: - """Normalize a path input value into a parsed RemoteState path. - - Args: - path: RemoteState path string, root array index, or a sequence of path - segment inputs such as ``("items", 0, "label")``. - - Returns: - Parsed path. - - Raises: - TypeError: If ``path`` is not a supported path input value. - ValueError: If ``path`` contains an invalid segment. - """ - if isinstance(path, str): - return parse_path(path) - if isinstance(path, int): - return (normalize_path_segment(path),) - if isinstance(path, Sequence): - return tuple(normalize_path_segment(segment) for segment in path) - raise TypeError( - "RemoteState path must be a string, integer, or sequence of path segments" - ) - - -def normalize_path_segment(segment: PathSegmentInput) -> PathSegment: - """Normalize one path segment input value into a parsed path segment. +def format_path(path: Path) -> str: + """Convert parsed RemoteState path segments back to dotted/bracket syntax. Args: - segment: A string property name, integer index, ``Property``, or - ``Index``. + path: Parsed path. Returns: - Parsed path segment. - - Raises: - TypeError: If ``segment`` is not a supported path segment input value. - ValueError: If an integer index is negative. + The canonical string form used by the transport and cache keys. """ - if isinstance(segment, Property): - return segment - if isinstance(segment, Index): - return _normalize_index(segment.i) - if isinstance(segment, bool): - raise ValueError("RemoteState path indices must be non-negative integers") - if isinstance(segment, int): - return _normalize_index(segment) - if isinstance(segment, str): - return Property(segment) - raise TypeError("RemoteState path segments must be strings or integers") + _validate_path(path) + parts: list[str] = [] + for index, segment in enumerate(path): + if isinstance(segment, int): + parts.append(f"[{segment}]") + elif index == 0 and _IDENTIFIER_RE.fullmatch(segment): + parts.append(segment) + elif _IDENTIFIER_RE.fullmatch(segment): + parts.append(f".{segment}") + else: + parts.append(f"[{json.dumps(segment, ensure_ascii=False)}]") + return "".join(parts) def prefixes(path: Path) -> list[Path]: @@ -193,34 +185,6 @@ def prefixes(path: Path) -> list[Path]: return [path[:i] for i in range(1, len(path) + 1)] -def format_path(path: Path) -> str: - """Convert a parsed path back to a RemoteState path string. - - Args: - path: Parsed path. - - Returns: - String representation of ``path``. - """ - _validate_path(path) - if len(path) == 0: - return "" - - parts: list[str] = [] - for index, seg in enumerate(path): - match seg: - case Property(key): - if index == 0 and _IDENTIFIER_RE.fullmatch(key): - parts.append(key) - elif _IDENTIFIER_RE.fullmatch(key): - parts.append(f".{key}") - else: - parts.append(f"[{json.dumps(key, ensure_ascii=False)}]") - case Index(i): - parts.append(f"[{i}]") - return "".join(parts) - - def to_jsonpath(path: str) -> str: """Convert a RemoteState path to a simple JSONPath string. @@ -261,21 +225,17 @@ def from_jsonpath(path: str) -> str: def _validate_path(path: Path) -> None: for segment in path: - match segment: - case Property(key): - if not isinstance(key, str): - raise ValueError(_INVALID_PATH_MESSAGE) - case Index(i): - if not isinstance(i, int) or i < 0: - raise ValueError(_INVALID_PATH_MESSAGE) - case _: - raise ValueError(_INVALID_PATH_MESSAGE) + if isinstance(segment, bool): + raise ValueError(_INVALID_PATH_MESSAGE) + if isinstance(segment, int): + _validate_index(segment) + elif not isinstance(segment, str): + raise ValueError(_INVALID_PATH_MESSAGE) -def _normalize_index(index: int) -> Index: - if isinstance(index, bool) or index < 0: +def _validate_index(index: int) -> None: + if index < 0: raise ValueError("RemoteState path indices must be non-negative integers") - return Index(index) def _read_identifier(path: str, start: int) -> tuple[str, int] | None: @@ -303,7 +263,7 @@ def _read_bracket_segment(path: str, start: int) -> tuple[PathSegment, int] | No key, pos = parsed if pos >= len(path) or path[pos] != "]": return None - return Property(key), pos + 1 + return key, pos + 1 if not _is_digit(next_char): return None @@ -316,7 +276,7 @@ def _read_bracket_segment(path: str, start: int) -> tuple[PathSegment, int] | No return None if pos >= len(path) or path[pos] != "]": return None - return Index(int(digits)), pos + 1 + return int(digits), pos + 1 def _read_quoted_string_literal(path: str, start: int) -> tuple[str, int] | None: diff --git a/remotestate-py/src/remotestate/store.py b/remotestate-py/src/remotestate/store.py index 87a280d..227650e 100644 --- a/remotestate-py/src/remotestate/store.py +++ b/remotestate-py/src/remotestate/store.py @@ -7,11 +7,9 @@ from .context import _call_context from .path import ( - Index, Path, PathInput, PathSegment, - Property, format_path, normalize_path, ) @@ -42,7 +40,7 @@ def __init__( default_factory: Optional callable used by ``set()`` to create missing intermediate path values. It receives the missing prefix path as a ``Path`` tuple, such as one - containing ``Property("user")`` or ``Index(0)`` segments. + containing ``"user"`` or ``0`` segments. If omitted, missing parents raise the same ``KeyError``, ``IndexError``, or ``AttributeError`` as before. """ @@ -58,9 +56,9 @@ def state(self) -> T: def __getitem__(self, path: PathInput) -> Any: """Return the value at ``path``. - ``path`` may be a RemoteState path string, an integer root index, or a - tuple of path segments such as ``("items", 0, "label")``. The empty - tuple ``()`` addresses the root state value. + ``path`` may be a RemoteState path string or a tuple of path segments + such as ``("items", 0, "label")``. The empty tuple ``()`` addresses + the root state value. """ return self.get(path) @@ -177,46 +175,41 @@ def _flush(self, pending: PendingUpdates) -> None: def _set_or_append_segment( obj: Any, segment: PathSegment, value: Any, *, require_appendable: bool ) -> None: - if isinstance(segment, Index) and isinstance(obj, list) and segment.i == len(obj): + if isinstance(segment, int) and isinstance(obj, list) and segment == len(obj): obj.append(value) return if require_appendable: - if isinstance(segment, Index): - raise IndexError(segment.i) - else: - raise KeyError(segment.key) + if isinstance(segment, int): + raise IndexError(segment) + raise KeyError(segment) _set_segment(obj, segment, value) def _get_segment(obj: Any, segment: PathSegment, require: bool) -> Any: - match segment: - case Property(key): - if isinstance(obj, dict): - if require: - return obj[key] - return obj.get(key) - else: - if require: - return getattr(obj, key) - return getattr(obj, key, None) - case Index(i): - try: - return obj[i] - except (IndexError, KeyError, TypeError): - if require: - raise - return None + if isinstance(segment, str): + if isinstance(obj, dict): + if require: + return obj[segment] + return obj.get(segment) + if require: + return getattr(obj, segment) + return getattr(obj, segment, None) + try: + return obj[segment] + except (IndexError, KeyError, TypeError): + if require: + raise + return None def _set_segment(obj: Any, segment: PathSegment, value: Any) -> None: - match segment: - case Property(key): - if isinstance(obj, dict): - obj[key] = value - else: - setattr(obj, key, value) - case Index(i): - obj[i] = value + if isinstance(segment, str): + if isinstance(obj, dict): + obj[segment] = value + else: + setattr(obj, segment, value) + return + obj[segment] = value def _get_at(root: Any, path: Path, require: bool) -> Any: @@ -245,9 +238,9 @@ def _set_at( if default_factory is None: raise if ( - isinstance(segment, Index) + isinstance(segment, int) and isinstance(obj, list) - and segment.i > len(obj) + and segment > len(obj) ): raise default_value = default_factory(path[:i]) @@ -255,7 +248,7 @@ def _set_at( obj, segment, default_value, - require_appendable=isinstance(segment, Index), + require_appendable=isinstance(segment, int), ) obj = default_value diff --git a/remotestate-py/tests/test_path.py b/remotestate-py/tests/test_path.py index 0875677..5b9f845 100644 --- a/remotestate-py/tests/test_path.py +++ b/remotestate-py/tests/test_path.py @@ -2,8 +2,6 @@ import pytest from remotestate.path import ( - Index, - Property, from_jsonpath, format_path, normalize_path, @@ -13,241 +11,162 @@ to_jsonpath, ) -# --- parse_path --- +def test_parse_path_parses_dotted_and_indexed_paths(): + assert parse_path("items[1].label") == ("items", 1, "label") -def test_simple_property(): - assert parse_path("user") == (Property("user"),) +def test_parse_path_parses_bracketed_string_keys(): + assert parse_path('user["display name"]') == ("user", "display name") + assert parse_path("user['display name']") == ("user", "display name") + assert parse_path('user["0"]') == ("user", "0") + assert parse_path("user['0']") == ("user", "0") + assert parse_path('items[""].label') == ("items", "", "label") + assert parse_path('user["weird.key"].value') == ("user", "weird.key", "value") + assert parse_path('user[""]') == ("user", "") -def test_empty_path_is_root(): - assert parse_path("") == () - - -def test_nested_properties(): - assert parse_path("user.name") == (Property("user"), Property("name")) - - -def test_index(): - assert parse_path("items[3]") == (Property("items"), Index(3)) - - -def test_root_index(): - assert parse_path("[3].name") == (Index(3), Property("name")) - -def test_string_key(): - assert parse_path('user["display name"]') == ( - Property("user"), - Property("display name"), - ) - assert parse_path("user['display name']") == ( - Property("user"), - Property("display name"), - ) - assert parse_path('user["0"]') == (Property("user"), Property("0")) - assert parse_path("user['0']") == (Property("user"), Property("0")) - assert parse_path('items[""].label') == ( - Property("items"), - Property(""), - Property("label"), - ) - - -def test_string_key_escapes(): - assert parse_path('user["line\\nbreak"]') == ( - Property("user"), - Property("line\nbreak"), - ) - assert parse_path('user["tab\\tseparated"]') == ( - Property("user"), - Property("tab\tseparated"), - ) - assert parse_path('user["quote\\"slash\\\\"]') == ( - Property("user"), - Property('quote"slash\\'), - ) - assert parse_path("user['double\\\"quote']") == ( - Property("user"), - Property('double"quote'), - ) +def test_parse_path_parses_bracketed_string_key_escapes(): + assert parse_path('user["line\\nbreak"]') == ("user", "line\nbreak") + assert parse_path('user["tab\\tseparated"]') == ("user", "tab\tseparated") + assert parse_path('user["quote\\"slash\\\\"]') == ("user", 'quote"slash\\') + assert parse_path("user['double\\\"quote']") == ("user", 'double"quote') assert parse_path('user["emoji \\uD83D\\uDE00"]') == ( - Property("user"), - Property("emoji " + chr(0x1F600)), + "user", + "emoji " + chr(0x1F600), ) -def test_root_string_key(): - assert parse_path('["root"]') == (Property("root"),) - assert parse_path('["display name"].value') == ( - Property("display name"), - Property("value"), - ) - - -def test_nested_after_index(): - assert parse_path("items[3].name") == ( - Property("items"), - Index(3), - Property("name"), - ) +def test_parse_path_parses_a_single_root_segment(): + assert parse_path("count") == ("count",) -def test_deep(): - assert parse_path("a.b[0].c[1].d") == ( - Property("a"), - Property("b"), - Index(0), - Property("c"), - Index(1), - Property("d"), - ) +def test_parse_path_parses_the_empty_root_path(): + assert parse_path("") == () -def test_underscore_in_key(): - assert parse_path("my_field.sub_field") == ( - Property("my_field"), - Property("sub_field"), - ) +def test_parse_path_parses_root_bracket_segments(): + assert parse_path("[0].label") == (0, "label") + assert parse_path('["display name"].value') == ("display name", "value") -def test_invalid_starts_with_dot(): +def test_parse_path_throws_on_invalid_trailing_input(): with pytest.raises(ValueError): - parse_path(".user") - - -def test_allows_root_index(): - assert parse_path("[0].name") == (Index(0), Property("name")) - - -def test_allows_root_string_key(): - assert parse_path('["root"]') == (Property("root"),) - - -def test_allows_empty_root(): - assert parse_path("") == () - - -def test_invalid_trailing_dot(): + parse_path("items..label") with pytest.raises(ValueError): parse_path("user.") - - -def test_invalid_double_dot(): with pytest.raises(ValueError): - parse_path("user..name") + parse_path("items[*]") -def test_invalid_non_integer_index(): +def test_parse_path_throws_on_invalid_path_starts(): with pytest.raises(ValueError): - parse_path("items[foo]") + parse_path("1items") + with pytest.raises(ValueError): + parse_path(".items") -def test_invalid_leading_zero_index(): +def test_parse_path_throws_on_non_canonical_integer_syntax(): with pytest.raises(ValueError): parse_path("items[01]") - - -def test_invalid_jsonpath_wildcard(): with pytest.raises(ValueError): - parse_path("items[*]") - - -# --- normalize_path --- - - -def test_normalize_path_accepts_strings(): - assert normalize_path("items[0].label") == ( - Property("items"), - Index(0), - Property("label"), - ) + parse_path("items[foo]") -def test_normalize_path_accepts_segment_sequences(): - assert normalize_path(("items", 0, "label")) == ( - Property("items"), - Index(0), - Property("label"), - ) +def test_format_path_formats_dotted_and_indexed_paths(): + assert format_path(("items", 1, "label")) == "items[1].label" -def test_normalize_path_accepts_bare_root_index(): - assert normalize_path(0) == (Index(0),) +def test_format_path_formats_bracketed_string_keys_canonically(): + assert format_path(("user", "display name")) == 'user["display name"]' + assert format_path(("user", "0")) == 'user["0"]' + assert format_path(("items", "", "label")) == 'items[""].label' + assert format_path(("user", "weird.key", "value")) == 'user["weird.key"].value' + assert format_path(("items", "")) == 'items[""]' -def test_normalize_path_segment_accepts_raw_segments(): - assert normalize_path_segment("items") == Property("items") - assert normalize_path_segment(0) == Index(0) +def test_format_path_formats_a_single_root_segment(): + assert format_path(("count",)) == "count" -def test_normalize_path_rejects_invalid_segments(): - with pytest.raises(ValueError): - normalize_path(("items", -1)) +def test_format_path_formats_the_empty_root_path(): + assert format_path(()) == "" -# --- prefixes --- +def test_format_path_formats_root_bracket_segments(): + assert format_path((0, "label")) == "[0].label" + assert format_path(("display name", "value")) == '["display name"].value' -def test_prefixes_simple(): - path = parse_path("user.name") - result = [format_path(p) for p in prefixes(path)] - assert result == ["user", "user.name"] +def test_normalize_path_normalizes_dotted_strings_into_parsed_paths(): + assert normalize_path("items[1].label") == ("items", 1, "label") -def test_prefixes_with_index(): - path = parse_path("items[3].name") - result = [format_path(p) for p in prefixes(path)] - assert result == ["items", "items[3]", "items[3].name"] +def test_normalize_path_accepts_an_already_parsed_path_input_value(): + path = ("items", 1, "label") + assert normalize_path(path) == path -def test_prefixes_single(): - path = parse_path("user") - result = prefixes(path) - assert len(result) == 1 +def test_normalize_path_accepts_parsed_relative_paths(): + parsed = parse_path("items[1].label") -def test_prefixes_root(): - assert prefixes(parse_path("")) == [] + assert normalize_path(parsed) == ("items", 1, "label") -# --- format_path --- +def test_normalize_path_accepts_string_keys_in_array_form(): + assert normalize_path(("items", "display name")) == ("items", "display name") + assert normalize_path(("items", "")) == ("items", "") -def test_roundtrip_simple(): - assert format_path(parse_path("user.name")) == "user.name" +def test_normalize_path_accepts_empty_root_paths(): + assert normalize_path(()) == () + assert normalize_path("") == () -def test_roundtrip_root(): - assert format_path(parse_path("")) == "" +def test_normalize_path_accepts_root_index_and_string_key_paths(): + assert normalize_path((1, "label")) == (1, "label") + assert normalize_path(("", "label")) == ("", "label") -def test_roundtrip_index(): - assert format_path(parse_path("items[3].name")) == "items[3].name" - assert format_path(parse_path("[3].name")) == "[3].name" +def test_normalize_path_segment_accepts_raw_segment_values(): + assert normalize_path_segment("items") == "items" + assert normalize_path_segment(1) == 1 -def test_roundtrip_string_key(): - assert format_path(parse_path('user["display name"]')) == 'user["display name"]' - assert format_path(parse_path("user['display name']")) == 'user["display name"]' - assert format_path(parse_path('user["0"]')) == 'user["0"]' - assert format_path(parse_path("user['0']")) == 'user["0"]' - assert format_path(parse_path('items[""].label')) == 'items[""].label' - assert format_path(parse_path('["display name"].value')) == ( - '["display name"].value' - ) +def test_normalize_path_rejects_invalid_array_form_path_segments(): + with pytest.raises(TypeError): + normalize_path(("items", 1.5)) + with pytest.raises(ValueError): + normalize_path(("items", -1, "label")) + with pytest.raises(ValueError): + normalize_path(("items", True, "label")) -def test_roundtrip_deep(): - s = "a.b[0].c[1].d" - assert format_path(parse_path(s)) == s +def test_normalize_path_rejects_invalid_string_syntax(): + with pytest.raises(ValueError): + normalize_path("items..label") + with pytest.raises(ValueError): + normalize_path("items[01]") -def test_roundtrip_empty_string_key(): - assert format_path(parse_path('user[""]')) == 'user[""]' +def test_normalize_path_rejects_bare_root_index_input(): + with pytest.raises(TypeError): + normalize_path(0) # type: ignore[arg-type] -# --- jsonpath --- +def test_prefixes_returns_non_root_prefixes(): + assert [format_path(p) for p in prefixes(parse_path("user.name"))] == [ + "user", + "user.name", + ] + assert [format_path(p) for p in prefixes(parse_path("items[3].name"))] == [ + "items", + "items[3]", + "items[3].name", + ] + assert prefixes(parse_path("user")) == [("user",)] + assert prefixes(parse_path("")) == [] def test_to_jsonpath(): @@ -268,7 +187,8 @@ def test_from_jsonpath_invalid(): def test_path_namespace_is_exported_from_package_root(): - assert rs.path.Property("user") == Property("user") - assert rs.path.Index(3) == Index(3) + assert rs.path.parse_path("user") == ("user",) + assert not hasattr(rs.path, "Property") + assert not hasattr(rs.path, "Index") assert not hasattr(rs, "Property") assert not hasattr(rs, "Index") diff --git a/remotestate-py/tests/test_store.py b/remotestate-py/tests/test_store.py index c3cc226..9ff0fbb 100644 --- a/remotestate-py/tests/test_store.py +++ b/remotestate-py/tests/test_store.py @@ -3,8 +3,6 @@ import pytest from pydantic import BaseModel -from remotestate.path import Index, Property - # noinspection PyProtectedMember from remotestate.store import Store, _batch_pending_updates @@ -192,8 +190,8 @@ def factory(path): store.set("user.address.city", "Hamburg") assert calls == [ - (Property("user"),), - (Property("user"), Property("address")), + ("user",), + ("user", "address"), ] @@ -208,13 +206,13 @@ def factory(path): store.set("[0].label", "foo") - assert calls == [(Index(0),)] + assert calls == [(0,)] assert store.state == [{"label": "foo"}] def test_set_default_factory_can_create_pydantic_objects(): def factory(path): - if path == (Property("user"),): + if path == ("user",): return User( name="", age=0, @@ -232,7 +230,7 @@ def factory(path): def test_set_default_factory_can_create_list_items(): def factory(path): - if path == (Property("items"),): + if path == ("items",): return [] return {} @@ -263,7 +261,7 @@ def test_set_sparse_list_index_raises_with_default_factory(): def factory(path): calls.append(path) - if path == (Property("items"),): + if path == ("items",): return [] return {} @@ -272,7 +270,7 @@ def factory(path): with pytest.raises(IndexError): store.set("items[1].label", "foo") - assert calls == [(Property("items"),)] + assert calls == [("items",)] def test_get_never_calls_default_factory(): diff --git a/remotestate-ts/src/lib/path.ts b/remotestate-ts/src/lib/path.ts index 0843ccc..489803f 100644 --- a/remotestate-ts/src/lib/path.ts +++ b/remotestate-ts/src/lib/path.ts @@ -16,12 +16,12 @@ export type Path = readonly PathSegment[]; /** * A raw value accepted as one path segment. */ -export type PathSegmentInput = string | number | PathSegment; +export type PathSegmentInput = PathSegment; /** * A raw value accepted anywhere a RemoteState path is needed. */ -export type PathInput = string | readonly PathSegmentInput[] | Path; +export type PathInput = string | readonly PathSegmentInput[]; const IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/; const INVALID_PATH_MESSAGE =