Package: objetto
Objettois an opinionated object-oriented framework for building modular applications
and APIs.OverviewObjettoallows for the creation of anApplicationthat consists of high-level
mutable structures referred to asObjects.Objectsare associated with anApplicationwhen initialized.Objectsare part of a parent-childrenHierarchytree.Objectshave their schema defined byAttributes.Objectsencase an immutable subset version of themselves referred to
asData.Objectswill send anActionto themselves, their parent, and
grandparents everytime aChangehappens.Objectscan performReactionsin response toActionsreceived from themselves, their children, and grandchildren.Objectscan be observed by externalObserverssuch
as GUI widgets.Objectsfeature built-in human-readableSerializationandDeserializationcapabilities.Objectscan be automatically tracked by aHistory, which allows for easy and selective undo/redo
functionality.How to installYou can installObjettoby usingpip:pip install objettoApplicationAnApplicationoversees allObjectsthat are meant to work together. It
provides different contexts for managing and keeping track of theirChanges.Objectsthat are part of differentApplicationssee each
other as regular values and can never be part of the sameHierarchy.AnApplicationcan haveRoot Objects, which areObjectsthat
are always available at the top of the hierarchy, and cannot be parented under otherObjects.Example: Instantiate a newApplication.>>>fromobjettoimportApplication>>>app=Application()# instantiate a new applicationRead ContextWhile in aRead Context, allObjectsin theApplicationare guaranteed
not to be modified.Example: Enter aRead Context.>>>fromobjettoimportApplication>>>app=Application()>>>withapp.read_context():...pass# read access only, no changes allowed...Write ContextWhile in aWrite Context,Actionsare only sent internally until the
outermostWrite Contextexits without errors, after which externalObserverswill then receive them.If an unhandled exception gets raised, all changes are reverted to the moment the
context was entered, and externalObserverswill not receive anyActions. This behavior is similar totransactionsin a database.NoteYou cannot enter aWrite Contextwhile in aRead Context.Example: Enter aWrite Context.>>>fromobjettoimportApplication>>>app=Application()>>>withapp.write_context():...pass# send actions to external observers only at the end, revert if errors...RootsRootObjectscan be declared when creating a subclass of anApplicationby using a root descriptor and specifying theObjecttype and initialization
arguments.Example: DefineRoot Objectswhen subclassingApplication.>>>fromobjettoimportApplication,Object,attribute,root>>>classDocument(Object):...title=attribute(str)...>>>classCustomApplication(Application):# inherit from Application...document=root(Document,title="untitled")# specify object type and args...>>>app=CustomApplication()>>>type(app.document).__name__'Document'ObjectObjectsare the building blocks of anApplication. AnObjectis
mutable, has state, and can be a parent/child of anotherObject.To define our ownObject, we have to inherit fromobjetto.Objectand useAttributesto define its schema. You need to instantiate it by passing anApplication, which can later be accessed through the.appproperty:Example: Make our ownObjectsubclass and instantiate it.>>>fromobjettoimportApplication,Object,attribute>>>classHobby(Object):# inherit from Object...description=attribute(str)# example attribute called 'description'...>>>app=Application()# we need an application>>>hobby=Hobby(app,description="biking")# instantiate our object>>>hobby.appisappTrueAttributeAttributesdescribe the schema of anObject. When defining one, we can
specify relationship parameters between theObjectthat owns it and the value being
stored, such as aValue Type,Hierarchysettings,Historypropagation,SerializationandDeserializationoptions, etc.Example: Define customObjectswith multipleAttributes.>>>fromobjettoimportApplication,Object,attribute>>>classHobby(Object):...description=attribute(str)# specify value type, only takes strings...>>>classPerson(Object):...name=attribute(str,default="Phil")# specify a default value...hobby=attribute(Hobby)# specify value type, only takes 'Hobby' objects...busy=attribute(bool,serialized=False,default=False)# not serialized...>>>app=Application()>>>hobby=Hobby(app,description="biking")>>>person=Person(app,hobby=hobby)>>>person.name'Phil'>>>person.name="Gaimon">>>person.name'Gaimon'Value TypeWhen defining anAttribute, we can specify itsValue Type. This is leveraged by
the runtime type checking and by static ones such asmypy.Defining types is also helpful to informObjettoabout the schema of ourObjects, which is needed for properSerializationandDeserialization.Import strings are also valid (using the syntaxmodule.submodule|Class.NestedClass),
and they will be imported lazily during runtime. It’s also possible to use multipleTypesby specifying them in a tuple.NoteStatic type checkers such asmypymight not understand
types correctly when multiple/lazy types are declared. In that case, you can help
the type checker by adding a type hint/comment using theAttributebase like so:Example: Helping static type checkers with a type hint for the attribute.>>>fromtypingimportUnion>>>fromobjetto.objectsimportAttribute# use 'Attribute' base for type hint>>>fromobjettoimportObject,attribute>>>classExample(Object):...foo=attribute(...(str,int,"__main__|Example")...)# type: Attribute[Union[str, int, Example]]...The types are interpreted ‘exactly’ by default. This means they are checked and compared
by identity, so instances of subclasses are not accepted. However that behavior can be
changed by specifyingsubtypes=Truewhen defining anAttribute.IfNoneis also accepted as a value, we can specifyNoneas a valid type.Example: Define theValue TypesofAttributes.>>>fromobjettoimportObject,attribute>>>classPerson(Object):...name=attribute(str)# single exact value type...friend=attribute(("__main__|Person",None))# import path, accepts None...hobby=attribute("module.hobby|Hobby")# import path with module path...points=attribute((int,float))# multiple basic types..._status=attribute(serialized=False)# no value type, not serialized..._pet=attribute(..."pets|AbstractPet",subtypes=True...)# accepts instances of 'AbstractPet' subclassesValue FactoryAnAttributecan conform and/or verify new values by using aValue Factory, which
is simply a function or callable that takes the newly input value, does something to it,
and then return the actual value that gets stored in theObject.NoteThere’s a very important thing to note when it comes to defining your ownValueFactory, which is that any value returned by the factory should always
produce itself in case it’s fed again through the same factory. Also, theValueFactoryneeds to be deterministic.You can use simple functions or callable types asValue Factories,
butObjettooffers some very useful pre-defined ones that can be easily configured
with parameters.Here are some of those built-inValue Factories, which can be
imported fromobjetto.factories:IntegerFloatingPointRegexMatchRegexSubStringCuratedBooleanExample: UseValue Factoriesto conform/verify attribute values.>>>fromobjettoimportObject,attribute>>>fromobjetto.factoriesimportRegexMatch,Integer,Curated,String,Boolean>>>classPerson(Object):...name=attribute(str,factory=RegexMatch(r"^[a-z ,.'-]+$"))# regex match...age=attribute(int,factory=Integer(minimum=1))# minimum integer...pet=attribute(str,factory=Curated(("cat","dog")))# curated values...job=attribute(str,factory=String())# force string...happy=attribute(bool,factory=Boolean(),default=True)# force booleanAuxiliary AttributeThese are specialAttributesthat will hold multiple values instead of
just one.The most basicAuxiliary Attributesare:list_attributedict_attributeset_attributeExample: UseAuxiliary Attributesto hold values.>>>fromobjettoimportApplication,Object,attribute,list_attribute>>>classHobby(Object):...description=attribute(str)...>>>classPerson(Object):...hobbies=list_attribute(Hobby)# holds multiple 'hobbies'...>>>app=Application()>>>hobby_a=Hobby(app,description="biking")>>>hobby_b=Hobby(app,description="gaming")>>>person=Person(app,hobbies=(hobby_a,hobby_b))# initialize with iterable>>>person.hobbies[0]ishobby_aTrueDelegated AttributeAttributescan have delegate methods that will get, set and/or delete
the values of otherAttributesin the sameObject.When defining delegates, you have to specify whichAttributesthey will
read from asdependencies.NoteThe results of delegate methods are cached, and because of that they should never
rely on mutable external objects. Think of delegates as ‘pure functions’ in the
context of theObjectthey belong to.If anAttributevalue needs to change according to external factors,Reactionsor regular methods are encouraged to be used instead of
delegates.Example: Define aDelegated Attributewith agetterand asetter.>>>fromobjettoimportApplication,Object,attribute>>>classPerson(Object):...first_name=attribute(str)...last_name=attribute(str)...name=attribute(...str,delegated=True,dependencies=(first_name,last_name)...)# delegated attribute with read dependencies......@name.getter# define a getter delegate...defname(self):...returnself.first_name+" "+self.last_name......@name.setter# define a setter delegate...defname(self,value):...self.first_name,self.last_name=value.split()...>>>app=Application()>>>person=Person(app,first_name="Katherine",last_name="Johnson")>>>person.name'Katherine Johnson'>>>person.name="Grace Hopper">>>person.name'Grace Hopper'>>>person.first_name'Grace'>>>person.last_name'Hopper'Attribute HelperThere are patterns that come up very often when definingAttributes.
Instead of re-writing those patterns everytime, it’s possible to use helper functions
known asAttribute Helpersto get the same effect.Here are some examples ofAttribute Helpers:constant_attributeprotected_attribute_pairprotected_list_attribute_pairprotected_dict_attribute_pairprotected_set_attribute_pairExample: Define a simpleAttribute Helper.>>>fromobjettoimportApplication,Object,protected_attribute_pair>>>classPerson(Object):..._name,name=protected_attribute_pair(str,default="King")# helper......defset_name(self,name):...self._name=name.upper()# set the changeable private attribute...>>>app=Application()>>>person=Person(app)>>>person.name'King'>>>person.name="bb king"# can't set non-changeable public attributeTraceback(mostrecentcalllast):AttributeError:attribute'name'isread-only>>>person.set_name("bb king")# we have to use the method instead>>>person.name'BB KING'HierarchyAnObjectcan have one parent and/or multiple children.The parent-children hierarchy is central to the wayObjettoworks, as it provides an
elegant way to structure ourApplication. It’s essential for features like:Preventing cyclic references:Objectscan only have one parentImmutableData‘mirroring’: TheDatastructure will replace childObjectswith theirDataaccording to the hierarchyHuman-readableSerialization: The.serialize()and.deserialize()methods
utilize the hierarchy to format their input/outputActionsending and subsequentReactionresponse:Actionswill
propagate from where theChangehappened all the way up the hierarchy to the
topmost grandparent, triggeringReactionsalong the wayAutomaticHistorypropagation: Children can automatically be
assigned to the sameHistoryof the parent if desired.NoteThe hierarchical relationship can be turned off selectively at the expense of those
features by specifyingchild=Falsewhen we define anAttribute.Also note that the hierarchical relationship will only work betweenObjectswithin the sameApplication.Example: Access._parentand._childrenproperties.>>>fromobjettoimportApplication,Object,attribute>>>classHobby(Object):...description=attribute(str)...>>>classPerson(Object):...name=attribute(str)...hobby=attribute(Hobby)# child=True is the default behavior...>>>app=Application()>>>hobby=Hobby(app,description="animation")>>>person=Person(app,name="Hayao Miyazaki",hobby=hobby)>>>hobby._parentisperson# 'person' is the parent of 'hobby'True>>>hobbyinperson._children# 'hobby' is a child of 'person'TrueUndo/Redo HistoryObjetto has built-in support for a undo/redoHistory. It takes
care of managing its validity for internal changes by flushing itself automatically when
necessary, and it is extremely easy to implement.A history can be associated with anObjectby adding ahistory_descriptorto the
class definition. Accessing that attribute from anObject’s instance will give us the
history itself.A history will be propagated to children/grandchildren of theObjectwhich defines
it, however it’s possible to prevent that behavior by specifyinghistory=Falsewhen we
define anAttribute.Undo/redo can be triggered by running the history’s methods.undo()and.redo().Histories areObjectstoo, so they do sendActionsthat can be
observed byObservers.Example: Associate aHistorywith anObject.>>>fromobjettoimportApplication,Object,history_descriptor,attribute>>>classPerson(Object):...history=history_descriptor()# specify a history...name=attribute(str)...>>>app=Application()>>>person=Person(app,name="Dave")>>>person.name'Dave'>>>person.name="Dave Grohl">>>person.name'Dave Grohl'>>>person.history.undo()# undo the name change>>>person.name'Dave'Batch ContextAnObjectcan enter aBatch Context, which will group multipleChangeshappening to itself and/or to otherObjectsinto one single entry in the
associatedHistory.A specialActioncarrying the the name and the metadata of the batch context will be
sent when entering (PREPhase) and when exiting the context (POSTPhase).Example: Enter aBatch Context.>>>fromobjettoimportApplication,Object,history_descriptor,attribute>>>classHobby(Object):...description=attribute(str)...>>>classPerson(Object):...history=history_descriptor()# specify a history...name=attribute(str)...hobby=attribute(Hobby)# history will propagate by default......defset_info(self,name,hobby_description):...withself._batch_context("Set Person Info"):# enter batch...self.name=name# single change...self.hobby.description=hobby_description# single change...>>>app=Application()>>>hobby=Hobby(app,description="sailing")>>>person=Person(app,name="Albert",hobby=hobby)>>>person.name,person.hobby.description('Albert','sailing')>>>person.set_info("Einstein","physics")# batch change>>>person.name,person.hobby.description('Einstein','physics')>>>person.history.undo()# single undo will revert both changes>>>person.name,person.hobby.description('Albert','sailing')DataDataare analog structures toObjects, but they are immutable.Everytime anObjectchanges, their internalDataand all of its parent’s and
grandparents’Dataget replaced with a new one that reflects those changes.By default, everyObjectclass/subclass with automatically generate it’sDataclass based on its attributes and schema. You can access the data type of anObjectthrough its.Dataclass property.TheDatainstance for anObjectcan be accessed through its.dataproperty.Example: Access internalDataof anObject.>>>fromobjettoimportApplication,Object,attribute>>>classHobby(Object):...description=attribute(str)...>>>classPerson(Object):...hobby=attribute(Hobby)...>>>Person.Data.__fullname__# access to automatically generated 'Data' class'Person.Data'>>>app=Application()>>>hobby=Hobby(app,description="biking")>>>person=Person(app,hobby=hobby)>>>hobby_data=person.data.hobby# access 'hobby' data through 'person' data>>>hobby_dataishobby.dataTrue>>>hobby_data.description'biking'If you want to bind methods from theObjectto theDataas well, you can use thedata_methoddecorator.Example: Using thedata_methoddecorator.>>>fromobjettoimportApplication,Object,attribute,data_method>>>classHobby(Object):...description=attribute(str)......@data_method...defget_description(self):...return"Description:{}".format(self.description)...>>>app=Application()>>>hobby=Hobby(app,description="biking")>>>hobby.get_description()'Description: biking'>>>hobby.data.get_description()# 'hobby' data also has the method'Description: biking'And finally, if you want more control, you can define a customDataclass for anObject, but this only recommended for advanced behavior. Keep in mind that the class
must match the schema of theObject’sAttributes.Example: Defining a customDataclass for anObject.>>>fromobjettoimportApplication,Object,Data,attribute,data_attribute>>>classHobby(Object):...description=attribute(str)......classData(Data):...description=data_attribute(str,factory=lambdav,**_:v.upper())...>>>app=Application()>>>hobby=Hobby(app,description="biking")>>>hobby.description'biking'>>>hobby.data.description# data attribute has a custom factory'BIKING'It’s also possible to useDataon its own, without an encasingObject. Remember
thatDatainstances are immutable, so the only way to produce changes is by calling
methods that return a new version of the data when subclassing from aninteractiveDataclass.Example: Using an interactiveDataon its own.>>>fromobjettoimportInteractiveData,data_attribute>>>classHobbyData(InteractiveData):# inherit from InteractiveData...description=data_attribute(str)# use data attributes...>>>classPersonData(InteractiveData):...hobby=data_attribute((HobbyData,None))# specify data types...>>>hobby_data=HobbyData(description="biking")>>>new_hobby_data=hobby_data.set("description","programming")# make new>>>person_data=PersonData(hobby=hobby_data)>>>person_data.hobby=None# data is immutableTraceback(mostrecentcalllast):AttributeError:'PersonData'objectattribute'hobby'isread-onlyActionEvery time anObjectchanges, it will automatically send anActionup theHierarchyto its parent and grandparents.TheActioncarries information such as:The description of theChange(change)A reference to theObjectreceiving theAction(receiver)A reference to theObjectwhere the change originated from (sender)A list of relative indexes/keys from thereceiverto thesender(locations)PhaseA constant value that tells whether the change in the state is about to happen (PRE)
or if the change already happened (POST).ChangeAChangedescribes what exactly changed in the state of anObject.Here are some of theChangesprovided byObjects:BatchUpdateDictUpdateListInsertListDeleteListUpdateListMoveSetUpdateSetRemoveReactionObjectscan defineReactionsthat will get triggered onceActionsare received.Reactionsare special methods ofObjectsthat respond toActionsreceived from themselves, their children, and grandchildren.NoteWhile anObjectcan react to its own changes, its triggeredReactioncannot
perform any further changes to the sameObject, only to its children and
grandchildren.If anAttributevalue needs to change when anotherAttributein the sameObjectchanges,Delegated Attributesshould be used
instead ofReactions.Example: DefineReactionmethods.>>>fromobjettoimportApplication,Object,attribute,reaction,POST>>>classMyObject(Object):...value=attribute(int,default=0)......@reaction...def__on_received(self,action,phase):...ifnotself._initializingandphaseisPOST:...print(("LAST -",action.change.name,phase))......@reaction(priority=1)...def__on_received_first(self,action,phase):...ifnotself._initializingandphaseisPOST:...print(("FIRST -",action.change.name,phase))...>>>app=Application()>>>my_obj=MyObject(app)>>>my_obj.value=42('FIRST -','Update Attributes',<Phase.POST:'POST'>)('LAST -','Update Attributes',<Phase.POST:'POST'>)Action ObserverAfter all internalReactionswithin anWrite Contextrun without any
errors, theActionsare then finally sent to externalAction Observersso they have a chance to synchronize.Graphical user interface widgets are a good example ofAction Observers.Example: Register an externalAction Observer.>>>fromobjettoimportApplication,Object,ActionObserver,attribute>>>classPerson(Object):...name=attribute(str,default="Nina")...>>>classPersonObserver(ActionObserver):......def__observe__(self,action,phase):...print((action.change.name,phase.value))...>>>app=Application()>>>person=Person(app)>>>observer=PersonObserver()>>>token=observer.start_observing(person)>>>person.name="Simone"('Update Attributes','PRE')('Update Attributes','POST')Auxiliary Attribute ReactionIt is possible to specifyReactionsmethods/callables when definingAuxiliary Attributes.Objettooffers configurable reactions
that can be used for that purpose.Here are some of them:UniqueAttributesLimitChildrenLimitExample: Ensure unique names.>>>fromobjettoimportApplication,Object,attribute,list_attribute>>>fromobjetto.reactionsimportUniqueAttributes>>>classPerson(Object):...name=attribute(str)...>>>classBand(Object):...musicians=list_attribute(Person,reactions=UniqueAttributes("name"))...>>>app=Application()>>>person_a=Person(app,name="Paul")>>>person_b=Person(app,name="John")>>>band=Band(app,musicians=(person_a,person_b))>>>person_c=Person(app,name="Paul")>>>band.musicians.append(person_c)Traceback(mostrecentcalllast):ValueError:anotherobjectalreadyhas'name'setto'Paul'SerializationObjectssupport human-readable serialization out of the box.Example: Serialize anObject.>>>fromobjettoimportApplication,Object,attribute,list_attribute>>>classPerson(Object):...name=attribute(str)...>>>classBand(Object):...musicians=list_attribute(Person)...>>>app=Application()>>>person_a=Person(app,name="Oscar")>>>person_b=Person(app,name="Ray")>>>band=Band(app,musicians=(person_a,person_b))>>>band.serialize(){'musicians':[{'name':'Oscar'},{'name':'Ray'}]}DeserializationObjectssupport human-readable deserialization out of the box.Example: Deserialize anObject.>>>fromobjettoimportApplication,Object,attribute,list_attribute>>>classPerson(Object):...name=attribute(str)...>>>classBand(Object):...musicians=list_attribute(Person)...>>>app=Application()>>>Band.deserialize({"musicians":[{"name":"Oscar"},{"name":"Ray"}]},app=app)Band(musicians=[<Personat...>,<Personat...>])Custom Serializer/DeserializerYou can specify custom serializer/deserializer functions for attributes.Example: Serialize anEnumusing lambdas.>>>fromenumimportEnum>>>fromobjettoimportApplication,Object,attribute>>>classHobby(Enum):...GUITAR=1...BIKING=2...>>>classPerson(Object):...hobby=attribute(...Hobby,...serializer=lambdavalue,**_:value.name.lower(),...deserializer=lambdavalue,**_:Hobby[value.upper()],...)...>>>app=Application()>>>person=Person(app,hobby=Hobby.GUITAR)>>>person.serialize(){'hobby':'guitar'}>>>Person.deserialize({"hobby":"biking"},app=app)Person(hobby=<Hobby.BIKING:2>)Example: Serialize anEnumusing provided serializer/deserializer.>>>fromenumimportEnum>>>fromobjettoimportApplication,Object,attribute>>>fromobjetto.serializersimportEnumSerializer>>>fromobjetto.deserializersimportEnumDeserializer>>>classHobby(Enum):...GUITAR=1...BIKING=2...>>>classJob(Enum):...PROGRAMMER=1...TEACHER=2...>>>classPerson(Object):...hobby=attribute(...Hobby,...serializer=EnumSerializer(),...deserializer=EnumDeserializer(Hobby),...)...job=attribute(...Job,...serializer=EnumSerializer(by_name=True),...deserializer=EnumDeserializer(Job,by_name=True),...)...>>>app=Application()>>>person=Person(app,hobby=Hobby.GUITAR,job=Job.PROGRAMMER)>>>serialized=person.serialize()>>>serialized["hobby"]1>>>serialized["job"]'PROGRAMMER'>>>Person.deserialize({"hobby":2,"job":"TEACHER"},app=app)Person(hobby=<Hobby.BIKING:2>,job=<Job.TEACHER:2>)… And More!Take a look at the API documentation to learn more aboutObjetto.