pip install pyuilder
Getting started is as easily as inheriting from pyuilder.Buildable
:
import pyuilder
class MyBuildable(pyuilder.Buildable):
def __init__(self, field1: int, field2: str) -> None:
self.field1 = field1
self.field2 = field2
MyBuildable.builder().field1(1).field2("hello world").build()
The last example was great and easy to set up but you'll quickly notice you get no intellisense or typing annotations in your IDE. If that's alright with you: great! However, if you want a little more out of pyuilder it's pretty trivial to provide as much information as you want to pyuilder's Buildable
to get auto-complete, type-hinting, and other useful information in your IDE:
from __future__ import annotations
from pyuilder import Buildable, Builder, Setter
class MyBuildable(Buildable):
class builder(Builder["MyBuildable"]):
field1: Setter[MyBuildable.builder, int]
field2: Setter[MyBuildable.builder, str]
def __init__(self, field1: int, field2: str) -> None:
self.field1 = field1
self.field2 = field2
MyBuildable.builder()\
.field1(1)\
.field2("hello world")\
.build()
The setup here ensures correct auto-complete, type hints, etc.:
- The
TypeVar
onBuilder
(Builder["MyBuildable"]
) is necessary to correctly annotate the return type ofbuild()
:build() -> MyBuildable
. - The name of the builder class here (
builder
), is what you will actually will call to invoke the builder:MyBuildable.builder()
. Since theBuildable
class is annotated to have a method namedbuilder
I recommend sticking with the same name. - The
Setter[...]
annotations on the fields take twoTypeVars
which provide type hinting:- The first
TypeVar
indicates the return type after calling a field setter:.field1() -> MyBuildable.builder
. This ensures that type hints aren't lost on chained field setter calls:field1(1).field2("hello world")
. - The second
TypeVar
indicates the value type the field setter accepts:.field1(value: int).field2(value: str)
.
- The first
If you try out the example above in an IDE you may notice that trying to use the builder with any field name other than the ones you annotated in your custom builder class will end with red squigglies under your code. This is because the Builder
class is set up to appear (see note below) to only allow the fields you explicitly declared to static type checkers.
However, you may want to maintain intellisense for annotated fields but want to be able to use any field without warnings from your static type check. pyuilder offers a variant of Bulder
called RelaxedBuilder
that does just that. The setup is exactly the same as above. Just replace Builder
with RelaxedBuilder
.
NOTE: Under the hood Builder
and RelaxBuilder
really do exactly the same thing. There is no behavioral difference between the two classes during runtime. They only differ in how they are processed during static type analysis.
The other thing you might want to do is have better control how fields are set when you do something like .builder().field(...)
. If field
was a list
for example you might want to be able to call field(...)
on the builder multiple times to continuously append values to it (similar to Lombok's @Singular
in Java). This is certainly possible with pyuilder:
from __future__ import annotations
from pyuilder import Buildable, Builder, Setter
class MyBuildable(Buildable):
class builder(Builder["MyBuildable"]):
field: Setter[MyBuildable.builder, str | list[str]]
def __init__(self, field: list[str]) -> None:
self.field = field
# Examples
# Setting the field as a list explicitly
MyBuildable.builder().field(["foo", "bar"]).build()
# Specifying how it should be added via str
MyBuildable.builder()\
.field([])\
.field("foo", "append")\
.field("bar", "append")\
.build()
# Specifying how it should be added via callable
MyBuildable.builder()\
.field(["foo"])\
.field("bar", list.append)\
.build()
If the second parameter to a field setter is a str
as it is in the second example it is assumed to be a name of a method on the existing value for that field. In the example above, by the second call of field()
, the existing value for field is a list
. The setter will find the "append" method on the existing list
value and call it with the provided value as the first argument.
If the second parameter to a field setter is a Callable
as it is in the third example the setter will call that method. When the setter calls it it will give the existing value as the first argument to the callable and the provided value as the second argument to the callable. In the example above, the provided list.append
callable is called with two arguments as described above: list.append(existing, value)
where existing = ["foo"]
and value = "bar"
.
See CHANGELOG.
MIT.