SkillAgentSearch skills...

Pystachio

type-checked dictionary templating library for python

Install / Use

/learn @wickman/Pystachio
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Pystachio

Build Status

tl;dr

Pystachio is a type-checked dictionary templating library.

why?

Its primary use is for the construction of miniature domain-specific configuration languages. Schemas defined by Pystachio can themselves be serialized and reconstructed into other Python interpreters. Pystachio objects are tailored via Mustache templates, as explained in the section on templating.

Similar projects

This project is unrelated to the defunct Javascript Python interpreter.

Notable related projects:

Requirements

Tested and works in CPython3 and PyPy3.

Overview

You can define a structured type through the 'Struct' type:

from pystachio import (
  Integer,
  String,
  Struct)

class Employee(Struct):
  first = String
  last  = String
  age   = Integer

By default all fields are optional:

>>> Employee().check()
TypeCheck(OK)

>>> Employee(first = 'brian')
Employee(first=brian)
>>> Employee(first = 'brian').check()
TypeCheck(OK)

But it is possible to make certain fields required:

from pystachio import Required

class Employee(Struct):
  first = Required(String)
  last  = Required(String)
  age   = Integer

We can still instantiate objects with empty fields:

>>> Employee()
Employee()

But they will fail type checks:

>>> Employee().check()
TypeCheck(FAILED): Employee[last] is required.

Struct objects are purely functional and hence immutable after constructed, however they are composable like functors:

>>> brian = Employee(first = 'brian')
>>> brian(last = 'wickman')
Employee(last=wickman, first=brian)
>>> brian
Employee(first=brian)
>>> brian = brian(last='wickman')
>>> brian.check()
TypeCheck(OK)

Object fields may also acquire defaults:

class Employee(Struct):
  first    = Required(String)
  last     = Required(String)
  age      = Integer
  location = Default(String, "San Francisco")

>>> Employee()
Employee(location=San Francisco)

Schemas wouldn't be terribly useful without the ability to be hierarchical:

class Location(Struct):
  city = String
  state = String
  country = String

class Employee(Struct):
  first    = Required(String)
  last     = Required(String)
  age      = Integer
  location = Default(Location, Location(city = "San Francisco"))

>>> Employee(first="brian", last="wickman")
Employee(last=wickman, location=Location(city=San Francisco), first=brian)

>>> Employee(first="brian", last="wickman").check()
TypeCheck(OK)

The type system

There are five basic types, two basic container types and then the Struct and Choice types.

Basic Types

There are five basic types: String, Integer, Float, Boolean and Enum. The first four behave as expected:

>>> Float(1.0).check()
TypeCheck(OK)
>>> String("1.0").check()
TypeCheck(OK)
>>> Integer(1).check()
TypeCheck(OK)
>>> Boolean(False).check()
TypeCheck(OK)

They also make a best effort to coerce into the appropriate type:

>>> Float("1.0")
Float(1.0)
>>> String(1.0)
String(1.0)
>>> Integer("1")
Integer(1)
>>> Boolean("true")
Boolean(True)

Though the same gotchas apply as standard coercion in Python:

>>> int("1.0")
ValueError: invalid literal for int() with base 10: '1.0'
>>> Integer("1.0")
pystachio.objects.CoercionError: Cannot coerce '1.0' to Integer

with the exception of Boolean which accepts "false" as falsy.

Enum is a factory that produces new enumeration types:

>>> Enum('Red', 'Green', 'Blue')
<class 'pystachio.typing.Enum_Red_Green_Blue'>
>>> Color = Enum('Red', 'Green', 'Blue')
>>> Color('Red')
Enum_Red_Green_Blue(Red)
>>> Color('Brown')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/wickman/clients/pystachio/pystachio/basic.py", line 208, in __init__
    self.__class__.__name__, ', '.join(self.VALUES)))
ValueError: Enum_Red_Green_Blue only accepts the following values: Red, Green, Blue

Enums can also be constructed using namedtuple syntax to generate more illustrative class names:

>>> Enum('Color', ('Red', 'Green', 'Blue'))
<class 'pystachio.typing.Color'>
>>> Color = Enum('Color', ('Red', 'Green', 'Blue'))
>>> Color('Red')
Color(Red)

Choices

Choice types represent alternatives - values that can have one of some set of values.

>>> C = Choice([Integer, String])
>>> c1 = C("abc")
>>> c2 = C(343)

Container types

There are two container types: the List type and the Map type. Lists are parameterized by the type they contain, and Maps are parameterized from a key type to a value type.

Lists

You construct a List by specifying its type (it actually behaves like a metaclass, since it produces a type):

>>> List(String)
<class 'pystachio.container.StringList'>
>>> List(String)([])
StringList()
>>> List(String)(["a", "b", "c"])
StringList(a, b, c)

They compose like expected:

>>> li = List(Integer)
>>> li
<class 'pystachio.container.IntegerList'>
>>> List(li)
<class 'pystachio.container.IntegerListList'>
>>> List(li)([li([1,"2",3]), li([' 2', '3 ', 4])])
IntegerListList(IntegerList(1, 2, 3), IntegerList(2, 3, 4))

Type checking is done recursively:

>> List(li)([li([1,"2",3]), li([' 2', '3 ', 4])]).check()
TypeCheck(OK)

Maps

You construct a Map by specifying the source and destination types:

>>> ages = Map(String, Integer)({
...   'brian': 30,
...   'ian': 15,
...   'robey': 5000
... })
>>> ages
StringIntegerMap(brian => 28, ian => 15, robey => 5000)
>>> ages.check()
TypeCheck(OK)

Much like all other types, these types are immutable. The only way to "mutate" would be to create a whole new Map. Technically speaking these types are hashable as well, so you can construct stranger composite types (added indentation for clarity.)

>>> fake_ages = Map(String, Integer)({
...   'brian': 28,
...   'ian': 15,
...   'robey': 5000
... })
>>> real_ages = Map(String, Integer)({
...   'brian': 30,
...   'ian': 21,
...   'robey': 35
... })
>>> believability = Map(Map(String, Integer), Float)({
...   fake_ages: 0.2,
...   real_ages: 0.9
... })
>>> believability
StringIntegerMapFloatMap(
  StringIntegerMap(brian => 28, ian => 15, robey => 5000) => 0.2,
  StringIntegerMap(brian => 30, ian => 21, robey => 35) => 0.9)

Object scopes

Objects have "environments": a set of bound scopes that follow the Object around. Objects are still immutable. The act of binding a variable to an Object just creates a new object with an additional variable scope. You can print the scopes by using the scopes function:

>>> String("hello").scopes()
()

You can bind variables to that object with the bind function:

>>> String("hello").bind(herp = "derp")
String(hello)

The environment variables of an object do not alter equality, for example:

>>> String("hello") == String("hello")
True
>>> String("hello").bind(foo = "bar") == String("hello")
True

The object appears to be the same but it carries that scope around with it:

>>> String("hello").bind(herp = "derp").scopes()
(Environment({Ref(herp): 'derp'}),)

Furthermore you can bind multiple times:

>>> String("hello").bind(herp = "derp").bind(herp = "extra derp").scopes()
(Environment({Ref(herp): 'extra derp'}), Environment({Ref(herp): 'derp'}))

You can use keyword arguments, but you can also pass dictionaries directly:

>>> String("hello").bind({"herp": "derp"}).scopes()
(Environment({Ref(herp): 'derp'}),)

Think of this as a "mount table" for mounting objects at particular points in a namespace. This namespace is hierarchical:

>>> String("hello").bind(herp = "derp", metaherp = {"a": 1, "b": {"c": 2}}).scopes()
(Environment({Ref(herp): 'derp', Ref(metaherp.b.c): '2', Ref(metaherp.a): '1'}),)

In fact, you can bind any Namable object, including List, Map, and Struct types directly:

>>> class Person(Struct)
...   first = String
...   last = String
...
>>> String("hello").bind(Person(first="brian")).scopes()
(Person(first=brian),)

The Environment object is simply a mechanism to bind arbitrary strings into a namespace compatible with Namable objects.

Because you can bind multiple times, scopes just form a name-resolution order:

>>> (String("hello").bind(Person(first="brian"), first="john")
                    .bind({'first': "jake"}, Person(first="jane"))).scopes()
(Person(first=jane),
 Environment({Ref(first): 'jake'}),
 Environment({Ref(first): 'john'}),
 Person(first=brian))

The later a variable is bound, the "higher priority" its name resolution becomes. Binding to an object is to achieve the effect of local overriding. But you can also do a lower-priority "global" bindings via in_scope:

>>> env = Environment(globalvar = "global variable", sharedvar = "global shared variable")
>>> obj = String("hello").bind(localvar = "local variable", sharedvar = "local shared variable")
>>> obj.scopes()
(Environment({Ref(localvar): 'local variable', Ref(sharedvar): 'local shared variable'}),)

Now we can bind env directly into obj as if they were local variables using bind:

>>> obj.bind(env).scopes()
(Environment({Ref(globalvar): 'global variable', Ref(shared
View on GitHub
GitHub Stars92
CategoryDevelopment
Updated6mo ago
Forks21

Languages

Python

Security Score

87/100

Audited on Sep 17, 2025

No findings