Skip to main content

Uniform JSON-shaped data structure manipulations — Python port of @voxgig/struct

Project description

Struct for Python

Full-parity Python port of the canonical TypeScript implementation.

For motivation, language-neutral concepts, and the cross-language parity matrix, see the top-level README and REPORT.md.

Install

cd python
pip install -e .

Package: voxgig_struct (single module voxgig_struct/voxgig_struct.py).

Or, without installing, add the source directory to sys.path:

import sys
sys.path.insert(0, '/path/to/struct/python')
from voxgig_struct import getpath, transform, validate

Quick start

from voxgig_struct import (
    getpath, setpath, merge, walk,
    inject, transform, validate, select,
)

getpath({'db': {'host': 'localhost'}}, 'db.host')
# 'localhost'

transform(
    {'user': {'first': 'Ada', 'last': 'Lovelace'}, 'age': 36},
    {'name': '`user.first`', 'surname': '`user.last`', 'years': '`age`'},
)
# {'name': 'Ada', 'surname': 'Lovelace', 'years': 36}

validate(
    {'name': 'Ada', 'age': 36},
    {'name': '`$STRING`', 'age': '`$INTEGER`'},
)
# {'name': 'Ada', 'age': 36}    (raises on mismatch)

Imports

from voxgig_struct import (
    # core canonical functions (the full canonical API is 48 names,
    # including the builders, re_* helpers, and injection helpers below)
    clone, delprop, escre, escurl, filter, flatten,
    getdef, getelem, getpath, getprop, haskey,
    inject, isempty, isfunc, iskey, islist, ismap, isnode,
    items, join, jsonify, keysof, merge,
    pad, pathify, select, setpath, setprop,
    size, slice, strkey, stringify, transform,
    typename, typify, validate, walk,

    # builders
    jm, jt,

    # extras (Python-specific convenience)
    replace, joinurl,

    # injection helpers
    Injection, StructUtility,
    checkPlacement, injectorArgs, injectChild,

    # sentinels and type constants
    SKIP, DELETE,
    T_any, T_noval, T_boolean, T_decimal, T_integer, T_number,
    T_string, T_function, T_symbol, T_null,
    T_list, T_map, T_instance, T_scalar, T_node,
    M_KEYPRE, M_KEYPOST, M_VAL,
)

Function reference

Source: voxgig_struct/voxgig_struct.py.

Predicates

def isnode(val)            # bool — map or list
def ismap(val)             # bool — dict
def islist(val)            # bool — list
def iskey(key)             # bool — non-empty str or int
def isempty(val)           # bool — None/''/{}/[]
def isfunc(val)            # bool — callable
isnode({'a': 1})          # True
ismap({'a': 1})           # True
ismap([])                 # False
islist([1, 2])            # True
islist({'a': 1})          # False
iskey('name')             # True
iskey(0)                  # True
iskey('')                 # False
isempty([])               # True
isempty(None)             # True
isempty('')               # True
isempty({})               # True
isempty(0)                # False
isfunc(lambda: 1)         # True

Type inspection

def typify(value) -> int        # bit-field
def typename(t: int) -> str     # human name

typify returns a bit-field combining a "kind" flag (T_scalar or T_node) with a specific type flag. typename looks up a human-friendly name.

typify(1)                 # T_scalar | T_number | T_integer  (201326720)
typify(42)                # T_scalar | T_number | T_integer
typify('hi')              # T_scalar | T_string
typify(None)              # T_scalar | T_null
typify({})                # T_node | T_map
typename(8192)            # 'map'  (8192 == T_map)
typename(typify('hi'))    # 'string'

Size, slice, pad

def size(val) -> int
def slice(val, start=UNDEF, end=UNDEF, mutate=False) -> Any
def pad(s, padding=UNDEF, padchar=UNDEF) -> str
size([1,2,3])             # 3
size({'a':1,'b':2})       # 2
size('abc')               # 3

slice keeps the first N; a negative start drops the last |start| items, and end is exclusive:

slice([1,2,3,4,5], 1, 4)  # [2, 3, 4]
slice('abcdef', -3)       # 'abc'  (drops the last 3)
pad('a', 3)               # 'a  '
pad('hi', 5)              # 'hi   '
pad('hi', -5, '*')        # '***hi'

Property access

def getprop(val, key, alt=UNDEF) -> Any
def setprop(parent, key, val) -> parent
def delprop(parent, key) -> parent
def getelem(val, key, alt=UNDEF) -> Any
def getdef(val, alt) -> Any
def haskey(val, key) -> bool
def keysof(val) -> list[str]
def items(val, apply=None) -> list
def strkey(key) -> str
getprop({'x': 1}, 'x')                # 1
getprop({'a': 1}, 'b', 'def')         # 'def'
getprop([10, 20, 30], 1)              # 20
setprop({'a': 1}, 'b', 2)             # {'a': 1, 'b': 2}
delprop({'a': 1, 'b': 2}, 'a')        # {'b': 2}
getelem([10, 20, 30], -1)             # 30
getdef(None, 'fallback')              # 'fallback'
haskey({'a': 1}, 'a')                 # True
items({'a': 1, 'b': 2})               # [['a', 1], ['b', 2]]
strkey(2.2)                           # '2'
strkey(1)                             # '1'
strkey('foo')                         # 'foo'
keysof({'b': 4, 'a': 5})              # ['a', 'b']  (sorted)

Path operations

def getpath(store, path, injdef=UNDEF) -> Any
def setpath(store, path, val, injdef=UNDEF) -> store
def pathify(val, startin=UNDEF, endin=UNDEF) -> str
getpath({'a': {'b': {'c': 42}}}, 'a.b.c')      # 42
getpath({'a': [10, 20]}, 'a.1')                # 20

store = {}
setpath(store, 'db.host', 'localhost')
# store == {'db': {'host': 'localhost'}}
setpath({'a': 1, 'b': 2}, 'b', 22)             # {'a': 1, 'b': 22}
pathify(['a', 'b', 'c'])                       # 'a.b.c'

Tree operations

def walk(val, apply=None, key=UNDEF, parent=UNDEF, path=UNDEF,
         *, before=None, after=None, maxdepth=None, pool=UNDEF) -> Any
def merge(objs, maxdepth=None) -> Any
def clone(val) -> Any
def flatten(lst, depth=None) -> list
def filter(val, check) -> list
def visit(key, val, parent, path):
    return 'DEFAULT' if val is None else val

walk(tree, after=visit)

Last input wins; maps deep-merge; lists merge by index:

merge([
    {'a': 1, 'b': 2, 'k': [10, 20], 'x': {'y': 5, 'z': 6}},
    {'b': 3, 'd': 4, 'e': 8, 'k': [11], 'x': {'y': 7}},
])
# {'a': 1, 'b': 3, 'd': 4, 'e': 8, 'k': [11, 20], 'x': {'y': 7, 'z': 6}}
clone({'a': {'b': [1, 2]}})     # {'a': {'b': [1, 2]}}  (a deep copy)
flatten([1, [2, [3]]])          # [1, 2, [3]]  (one level by default)
flatten([1, [2, [3, [4]]]])     # [1, 2, [3, [4]]]

filter passes each (key, value) pair to the check and returns the matching values (not the pairs):

filter([1, 2, 3, 4, 5], lambda kv: kv[1] > 3)
# [4, 5]

String / URL / JSON

def escre(s) -> str
def escurl(s) -> str
def join(arr, sep=UNDEF, url=UNDEF) -> str
def joinurl(parts) -> str         # convenience: join(parts, '/', True)
def jsonify(val, flags=None) -> str
def stringify(val, maxlen=UNDEF, pretty=None) -> str
def replace(s, from_pat, to_str) -> str
escre('a.b+c')                       # 'a\\.b\\+c'
escurl('hello world?')               # 'hello%20world%3F'
join(['a', 'b', 'c'], '/')           # 'a/b/c'
joinurl(['http:', '/foo/', '/bar'])  # 'http:/foo/bar'

jsonify pretty-prints by default (indent 2); pass {'indent': 0} for the compact form:

jsonify({'a': 1})
# {
#   "a": 1
# }
jsonify({'a': 1, 'b': 2}, {'indent': 0})  # '{"a":1,"b":2}'

stringify is the compact, quote-light form — keys are sorted and object braces are kept; the second argument caps the length (the ... counts):

stringify({'a': 1, 'b': [2, 3]})     # '{a:1,b:[2,3]}'
stringify('verylongstring', 5)       # 've...'

Inject / transform / validate / select

def inject(val, store, injdef=UNDEF) -> Any
def transform(data, spec, injdef=UNDEF) -> Any
def validate(data, spec, injdef=UNDEF) -> Any
def select(children, query) -> list
# Backtick refs in strings are replaced by store values.
inject({'x': '`a`', 'y': 2}, {'a': 1})    # {'x': 1, 'y': 2}
inject(
    {'greeting': 'hello `name`'},
    {'name': 'Ada'}
)
# {'greeting': 'hello Ada'}

# Build a result by example.
transform(
    {'hold': {'x': 1}, 'top': 99},
    {'a': '`hold.x`', 'b': '`top`'}
)
# {'a': 1, 'b': 99}
# Validate against a shape (raises on mismatch).
validate(
    {'name': 'Ada', 'age': 36},
    {'name': '`$STRING`', 'age': '`$INTEGER`'}
)
# {'name': 'Ada', 'age': 36}
# Find children matching a query.
select(
    {'a': {'name': 'Alice', 'age': 30}, 'b': {'name': 'Bob', 'age': 25}},
    {'age': 30}
)
# [{'name': 'Alice', 'age': 30, '$KEY': 'a'}]

Transform commands drive structural ops. A command like $EACH appears in value position — as the first element of a list ['$EACH', path, subspec] — mapping the sub-spec over every entry at path:

transform(
    {'v': 1, 'a': [{'q': 13}, {'q': 23}]},
    {'x': {'y': ['`$EACH`', 'a', {'q': '`$COPY`', 'r': '`.q`', 'p': '`...v`'}]}}
)
# {'x': {'y': [{'q': 13, 'r': 13, 'p': 1}, {'q': 23, 'r': 23, 'p': 1}]}}

Putting a command in key position (or, for $APPLY, directly under a map) is an error — commands must be list values:

transform({}, {'x': '`$APPLY`'})
# raises: $APPLY: invalid placement in parent map, expected: list.

Builders

jm('a', 1, 'b', 2)        # {'a': 1, 'b': 2}
jt(1, 2, 3)               # [1, 2, 3]

Injection helpers

def checkPlacement(modes, ijname, parentTypes, inj) -> bool
def injectorArgs(argTypes, args) -> Any
def injectChild(child, store, inj) -> Injection

Constants

Sentinels

SKIP        # emit nothing for this key
DELETE      # remove this key from the parent

Type bit-flags

T_any T_noval T_boolean T_decimal T_integer T_number T_string
T_function T_symbol T_null T_list T_map T_instance T_scalar T_node

Walk / inject phase flags

M_KEYPRE   M_KEYPOST   M_VAL

Transform commands

$DELETE  $COPY    $KEY     $META    $ANNO
$MERGE   $EACH    $PACK    $REF     $FORMAT  $APPLY

Validate checkers

$MAP   $LIST   $STRING   $NUMBER   $INTEGER   $DECIMAL  $BOOLEAN
$NULL  $NIL    $FUNCTION $INSTANCE $ANY       $CHILD    $ONE     $EXACT

Notes

None, null, and UNDEF

Python has only None. Internally the port uses an UNDEF sentinel (= None for ergonomics) to mean "absent". JSON null and "absent" both map to None at the user-facing API.

typify(None) returns T_scalar | T_null. Where the test corpus needs to disambiguate, the runner uses string sentinels __NULL__ and __UNDEF__.

Walk uses keyword arguments

Where canonical TypeScript has positional optional parameters, the Python port uses keyword arguments. For example:

walk(tree, before=None, after=visit, maxdepth=10)

Lowercase function names

Function names match canonical TypeScript exactly: getpath, setpath, getprop, etc. PEP 8 would suggest get_path, but parity with other ports beats style here.

Test status

94 tests pass against the shared corpus (3 skipped).

Regex

Uniform six-function regex API (see /design/REGEX_API.md). The Python port wraps the stdlib re module.

API

Function Maps to
re_compile(pattern, flags=0) re.compile(pattern, flags)
re_test(pattern, input) bool(re.search(pattern, input))
re_find(pattern, input) first match as [whole, group1, ...] or None
re_find_all(pattern, input) all matches, one row per match
re_replace(pattern, input, repl) re.sub(pattern, repl, input)
re_escape(s) re.escape(s)

Dialect

Patterns must stay inside the RE2 subset documented in /design/REGEX.md. Python's re supports backreferences and lookaround; using them will not be portable to the Go / Rust / C / Lua / Zig ports.

Sharp edges

  • Catastrophic backtracking. Python's re (the default C engine) is backtracking. ^(a+)+$ against 22 a's plus ! runs ~190 ms here; RE2-style ports finish the same case in <0.1 ms. Use flat patterns.
  • Zero-width replace. re_replace("a*", "abc", "X") returns "XXbXcX" — the ECMA convention shared by all PCRE/ECMA/.NET/Java/Onigmo engines plus the in-tree Thompson ports. Go (RE2) returns "XbXcX" instead; see /design/REGEX_PATHOLOGICAL.md.

See /design/REGEX_PATHOLOGICAL.md for the cross-port pathological-input panel.

Build and test

cd python
make test

Tests live in tests/ and read fixtures from ../build/test/.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

voxgig_struct-0.1.0.tar.gz (44.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

voxgig_struct-0.1.0-py3-none-any.whl (30.7 kB view details)

Uploaded Python 3

File details

Details for the file voxgig_struct-0.1.0.tar.gz.

File metadata

  • Download URL: voxgig_struct-0.1.0.tar.gz
  • Upload date:
  • Size: 44.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for voxgig_struct-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4131f74a0bd4df4c72cfec09f0ab12ad61669844601b3da05f37538a7e228bb4
MD5 710d74046e470053208e9db5ecb7aacb
BLAKE2b-256 5037d6303f8862a3e4a871c94e766baddbdc32df4ec07288ddd19a5eb80b4ad2

See more details on using hashes here.

File details

Details for the file voxgig_struct-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: voxgig_struct-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 30.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for voxgig_struct-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9de6b1a3d6970fe7f668f8c24803b47e3a04aaa3be7d69b608d1d0130b043d14
MD5 d3e2c460a19406852dd108c3b05fdf37
BLAKE2b-256 2284740b67d020b6a81826a0de6e3e669e731b3b1162b87d7d89ade940aa8ef5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page