Runtime Typing¶
Typing is at the very core of Symbolic Programming as a way of constraining symbolic attributes, it also maximizes the productivity when developing new components with PyGlove. pg.typing
provides a runtime typing system for such purposes.
!pip install pyglove
import pyglove as pg
1. Get started with pg.typing
¶
@pg.members([
('x', pg.typing.Int(min_value=1, max_value=10).noneable()),
('y', pg.typing.Union([
pg.typing.Int(), pg.typing.Enum('a', ['a', 'b', 'c'])
], default=1))
])
class Foo(pg.Object):
pass
import contextlib
@contextlib.contextmanager
def expect_error(error_type):
has_error = False
try:
yield
except error_type as e:
print(f'ERROR: {e!r}')
has_error = True
finally:
assert has_error
# Use the defaults: x=0, y=1
f = Foo()
print(f)
Foo(
x = None,
y = 1
)
# Raises as `x` is out of range ([1, 10]).
with expect_error(ValueError):
_ = Foo(x=11)
ERROR: ValueError('Value 11 is out of range (min=1, max=10). (path=x)')
# Raises as `y` is not an integer nor among ['a', 'b', 'c'].
with expect_error(ValueError):
_ = Foo(y='d')
ERROR: ValueError("Value 'd' is not in candidate list ['a', 'b', 'c']. (path=y)")
2. Applicability of pg.typing
¶
pg.typing
is available for specifying the constraints for all symbolic types.
2.1 pg.List
and pg.Dict
¶
l = pg.List(value_spec=pg.typing.List(pg.typing.Int()))
# Okay
l.append(1)
# Not okay. 1.0 is not an integer.
with expect_error(TypeError):
l.append(1.0)
ERROR: TypeError("Expect <class 'int'> but encountered <class 'float'>: 1.0. (path=[1])")
d = pg.Dict(x='foo', value_spec=pg.typing.Dict([
('x', pg.typing.Any()),
('y', pg.typing.Int().noneable())
]))
print(d)
# Okay. 1 is an integer.
d.y = 1
# Not okay, 'z' is not allowed as a key in the Dict.
with expect_error(KeyError):
d.z = True
{
x = 'foo',
y = None
}
ERROR: KeyError("Key 'z' is not allowed for <class 'pyglove.core.symbolic.Dict'>. (path=)")
# Define a dict of str to integers.
d = pg.Dict(value_spec=pg.typing.Dict([
(pg.typing.StrKey(), pg.typing.Int()),
]))
# Okay. string keys with integer values.
d.x = 1
d.y = 2
# Not okay. The value is not an integer.
with expect_error(TypeError):
d.z = 'foo'
ERROR: TypeError("Expect <class 'int'> but encountered <class 'str'>: foo. (path=z)")
# Constraint on key names
d = pg.Dict(value_spec=pg.typing.Dict([
(pg.typing.StrKey('.*_file'), pg.typing.Str()),
]))
# Okay. String keys ended with '_file'.
d.data1_file = 'abc'
d.data2_file = 'cde'
# Not okay. Key does not end with '_file'.
with expect_error(KeyError):
d.data1 = 'efg'
ERROR: KeyError("Key 'data1' is not allowed for <class 'pyglove.core.symbolic.Dict'>. (path=)")
2.2 pg.Object
subclasses¶
# pg.typing.Dict makes it convenient to define hierarchical
# symbolic attributes.
@pg.members([
('x', pg.typing.Int()),
('y', pg.typing.Dict([
('z', pg.typing.Float(default=1.0))
]))
])
class Foo(pg.Object):
pass
f = Foo(x=1, y=pg.Dict(z=2.0))
print(f)
Foo(
x = 1,
y = {
z = 2.0
}
)
2.3. Functors via pg.functor
.¶
# Type definition in functor can inherit default values
# from the function signature.
@pg.functor([
('x', pg.typing.Float()),
('y', pg.typing.Float())
])
def foo(x=1.0, y=2.0):
pass
f = foo()
print(f)
foo(
x = 1.0,
y = 2.0
)
2.4 Class wrappers via pg.symbolize
¶
class _Foo:
def __init__(self, x, y):
pass
# Type constraint can be passed as the second argument
# of `pg.symbolize`.
Foo = pg.symbolize(_Foo, [
('x', pg.typing.Int()),
('y', pg.typing.Int()),
])
# Not okay. 1.0 is not an integer.
with expect_error(TypeError):
_ = Foo(1.0, 2)
ERROR: TypeError("Expect <class 'int'> but encountered <class 'float'>: 1.0. (path=x)")
3. Value specifications¶
PyGlove supports value specifications for almost all common types in Python.
A value specification is an object of a pg.typing.ValueSpec
subclass. All ValueSpec
subclasses have common traits:
default
argument: Set the default value of current field..noneable()
: Marks the field can be None, and use None as the default value if thedefault
is not specified.
3.1 Any type¶
print(pg.typing.Any())
Any()
3.2 Numbers and string¶
# Boolean
print(pg.typing.Bool(default=True))
# Integer: with optional min/max constraint.
print(pg.typing.Int(min_value=0, max_value=10, default=1))
# Float: with optional min/max constraint.
print(pg.typing.Float(min_value=0.0, max_value=1.0).noneable())
# Str: with optinal constraint with regular expression.
print(pg.typing.Str(regex='.*file'))
Bool(default=True)
Int(default=1, min=0, max=10)
Float(default=None, min=0.0, max=1.0, noneable=True)
Str(regex='.*file')
3.3 Lists¶
# Lists of non-negative integers.
print(pg.typing.List(pg.typing.Int(min_value=0)))
# Lists of fixed size (2).
print(pg.typing.List(pg.typing.Int(), size=2))
# Lists of min/max size.
print(pg.typing.List(pg.typing.Any(), min_size=1, max_size=10))
List(Int(min=0))
List(Int(), min_size=2, max_size=2)
List(Any(), min_size=1, max_size=10)
3.4 Dicts¶
# Free-form dict.
print(pg.typing.Dict())
# Dict with fixed schema.
print(pg.typing.Dict([
('x', pg.typing.Int(), 'Optional docstr for x'),
('y', pg.typing.Float().noneable())
]))
# Dict with any string keys.
print(pg.typing.Dict([
(pg.typing.StrKey(), pg.typing.Int())
]))
# Dict with string keys with a regex pattern.
print(pg.typing.Dict([
(pg.typing.StrKey('.*file'), pg.typing.Int())
]))
Dict()
Dict({
# Optional docstr for x
x = Int(),
y = Float(default=None, noneable=True)
})
Dict({
StrKey() = Int()
})
Dict({
StrKey(regex='.*file') = Int()
})
3.5. Object of a class¶
class Foo:
pass
# Object of Foo.
print(pg.typing.Object(Foo))
Object(Foo)
3.6. Type¶
# Subclasses of Foo.
print(pg.typing.Type(Foo))
Type(<class '__main__.Foo'>)
3.7. Callable¶
# Any callable.
print(pg.typing.Callable())
# Callable with 2 positional integer arguments.
print(pg.typing.Callable([
pg.typing.Int(), pg.typing.Int()
]))
# Callable with 1 positional argument, 1 keyword argument
# and requires the return value to be a boolean.
print(pg.typing.Callable(
[pg.typing.Int()],
kw=[('x', pg.typing.Float())],
returns=pg.typing.Bool()))
Callable()
Callable(args=[Int(), Int()])
Callable(args=[Int()], kw=[('x', Float())], returns=Bool())
3.8 Union¶
class Foo:
pass
class Bar:
pass
# An union of int, float, Foo or Bar.
print(pg.typing.Union([
pg.typing.Int(),
pg.typing.Float(),
pg.typing.Object(Foo),
pg.typing.Object(Bar)
]))
# An union of a nested union and a callable.
print(pg.typing.Union([
pg.typing.Union([pg.typing.Int(), pg.typing.Float()]) ,
pg.typing.Callable(returns=pg.typing.Int())
]))
Union([
Int(),
Float(),
Object(Foo),
Object(Bar)
])
Union([
Union([Int(), Float()]),
Callable(returns=Int())
])
4. Automatic type conversions¶
In programming language like C++, types can define automatic conversion rules, e.g:
class MyType {
public:
operator int() { return this->value }
};
a = 1 + MyType(1)
There is no such concept of implicit type conversion in Python. However, the need of implicit conversion is necessary. For example, for a pg.typing.Int()
, it should also accept a numpy.integer
.
This can be done with pg.typing.register_converter
.
import numpy as np
# Not okay. np.int32 is not int.
with expect_error(TypeError):
pg.typing.Int().apply(np.int32(0))
# Register automatic conversion
pg.typing.register_converter(np.int32, int, int)
# Okay. Conversion is effective.
pg.typing.Int().apply(np.int32(0))
ERROR: TypeError("Expect <class 'int'> but encountered <class 'numpy.int32'>: 0. (path=)")
0