Welcome to py-objects
ATTENTION!
This repo is for the Python package py-objects, which was renamed to simpleset on 2023-10-09 at v0.1.0. This repo only exists now for reference purposes, and will not change further.
Please check out simpleset here:
What / Why
I dislike Python enums. I find them weird and off-putting, with their restrictive structure and foundation of metaprogramming black magic. It's especially awful for beginners, who are ill-equipped to deal with the madness that awaits them on that journey — especially if the poor fools escalate to Django enums, or god-forbid, Graphene enums. (Avoid Graphene like the plague.)
So, why use them at all, when plain old objects are already familiar and can do nearly anything? That's what this is — a pair of simple classes (Object and ObjectManager), inspired by the Django Model pattern, which lets you declare a family of multi-attribute objects in a single line, and then do cool stuff with them.
Object Definition
A new object type is defined as a subclass of Object, using the class method define:
from objects import Object
Color = Object.define( "Color", "RED", "GREEN", "BLUE" )
assert issubclass( Color, Object )
Each object has exactly one mandatory attribute — canonical_name —, and is accessible as a class attribute via it's canonical name:
assert isinstance( Color.RED, Color )
Color.RED.canonical_name -> "RED"
However, that's just the simplest case.
Form 2: With Labels
If you pass kwargs instead of args, the value will be given the attribute name label:
Color = Object.define( "Color", RED="ff0000", GREEN="00ff00", BLUE="0000ff" )
Color.RED.label -> "ff0000"
Note: It doesn't have to be a string.
Form 3: Arbitrary Attributes
The last and most advanced form lets you define as many attributes as you like:
Color = Object.define(
"Color",
RED=dict( hex="ff0000", like=True ),
GREEN=dict( hex="00ff00", like=True ),
BLUE=dict( hex="0000ff", like=False ),
)
Object Manager
If you want to work with the whole set of objects, use the Object Manager:
Color.objects.all -> [ Color.RED, Color.GREEN, ... ]
You can also fetch an object by name:
red = Color.objects[ "RED" ]
Or check if a name is part of the set:
assert "RED" in Color.objects
Both methods will accept either a string or object:
red = Color.objects[ Color.RED ]
assert Color.RED in Color.objects
So it's safe to use as a "wrapping" method to turn args of unknown type into a proper object:
obj = Color.objects[ some_arg ]
Select Methods
To search for objects with specific attribute values, use select() or one of the select-based methods, which are the ones that accept kwargs.
fav_colors = Color.objects.select( like=True )
fav_color = Color.objects.first( like=True )
If you pass multiple kwargs, they will be "AND"-ed, meaning only objects that match both criteria will be returned.
With no kwargs, it behavees identically to all.
Subclassing
If you want to add your own features to Object, just subclass it:
class MyObject( Object ):
def my_new_method( self ):
...
Color = MyObject.define( "Color", "RED", "GREEN", "BLUE" )
Color.RED.my_new_method()
That works well for defining a project-wide base class, but what if you just want a one-off class to add a feature to a specific object type in your collection? Easy — just skip past define, which is just a helper method around createmany, and use createmany directly:
class Planet( MyObject ):
@property
def circumference( self ):
return self.diameter * 3.14
Planet.createmany(
EARTH=dict( diameter=12742 ),
MARS=dict( diameter=6794 ),
)
Planet.EARTH.circumference
Bonus: ErrorSet
A variation on the enum theme for Python exceptions. Declare a family of Exception classes in one line:
from objects import ErrorSet
APIError = ErrorSet( "APIError", "CERTIFICATE_ERROR", "CONNECTION_ERROR", "VERSION_ERROR" )
raise APIError("...")
raise APIError.VERSION_ERROR("...")
Catch the most specific errors:
try:
...
except APIError.VERSION_ERROR as e:
...
Or the whole family:
try:
...
except APIError as e:
...
Notice the difference — Objects are classes with instances attached, whereas ErrorSets are classes with subclasses attached.
Bonus: Help for Django Users
Stop using Django.
.
If you must use Django, I suggest adding the following to your personal Object base class:
def django_pair( self ):
django_value = self.canonical_name
django_label = self.label if hasattr( self, "label" ) else self.canonical_name
return ( django_value, django_label )
@classmethod
def choices( cls, filter=None ):
objects = cls.objects.filter( filter ) if filter else cls.objects.all
return [ obj.django_pair() for obj in objects ]
Then you can use Objects instead of Enums in model fields:
color = models.CharField(
choices = Color.choices(), # use all values
-or-
choices = Color.choices( filter=lambda o: o != "BLUE" ), # filter out some
max_length = Color.objects.max_length,
default = Color.RED,
)
Bonus: Help for Graphene Users
Stop using Graphene.
.
Seriously, it's really really bad.
.
If you must use Graphene, I suggest adding the following to you personal Object base class:
from functools import cache
from graphene import Enum
def graphene_pair( self ):
graphene_name = self.canonical_name
graphene_value = self.canonical_name
return ( graphene_name, graphene_value )
def graphene_description( self ):
if hasattr( self, "description" ):
return self.description
if hasattr( self, "label" ):
return self.label
return None
@classmethod
@cache
def graphene( cls, name=None, filter=None ):
name = name or cls.__name__
objects = cls.objects.filter( filter ) if filter else cls.objects.all
enum_items = [ obj.graphene_pair() for obj in objects ]
descriptions = { obj.graphene_pair()[ 0 ]: obj.graphene_description() for obj in objects }
def description( enum_obj ):
return descriptions[ enum_obj.name ] if enum_obj else cls.__doc__
return graphene.Enum( name, enum_items, description=description )
Then you can use Objects instead of Enums and still generate Graphene enums from them:
ColorEnum = Color.graphene()
class Foo( graphene.ObjectType ):
color = ColorEnum( required=True )
...