python-autoclass¶
Write compact python classes
autoclass is now fully compliant with pyfields ! Check out how you can create very compact classes here
autoclass provides tools to automatically generate python classes code. The objective of this library is to reduce the amount of redundancy by automatically generating parts of the code from the information already available somewhere else (in the constructor signature or in the pyfields fields for example). It is made of several independent features that can be combined:
- with
@autoargsyou don't have to writeself.xxx = xxxin your constructor - with
@autopropsall or part your constructor arguments becomepropertiesand their setter is annotated with the same PEP484 type hints and value validation methods - with
@autohash, your object is hashable based on the tuple of all fields (so it can be used as a dictionary key or put in a set) - with
@autodict, your object behaves like a dictionary, is comparable with dictionaries, and gets a string representation - with
@autorepr, your object gets a string representation (use either this or@autodict, not both at the same time) - with
@autoclass, you get all of the above at once (but you can still disable some of them)
The intent is similar to attrs and PEP557 dataclasses: remove boilerplate code. However as opposed to these,
- this library can be applied on any class. It does not change anything in your coding habits: you can still create a
__init__constructor, and everything else is provided with decorators. - if information about fields is available from another library,
autoclasscan easily leverage it : for example you can now usepyfieldsto declare the fields,autoclasswill support it. - all decorators above can be used independently, for example if you just need to add a dictionary behaviour to an existing class you can use
@autodictonly. - all created code is simple and readable. You can easily step through the generated functions in debug mode to understand what's going on
- as opposed to
attrs, setters are generated for the fields so validation libraries such as valid8 can wrap them. Alternatively if you usepyfields, it directly provides this feature.
In other words, autoclass simply generates the same code that you would have written manually. For this reason, in many cases you can use other libraries on top of the resulting classes without hassle. A good example is that you can use any PEP484 type checking library of your choice.
Installing¶
> pip install autoclass You may wish to also install
pyfieldsto create compact classes.- a PEP484-based type checker: typeguard , pytypes or enforce.
- a value validator: valid8 was originally created in this project and is now independent.
Alternatively, you may use PyContracts to perform type and value validation at the same time using @contract, but this will not benefit from PEP484 and uses a dedicated syntax. This documentation also shows some examples.
> pip install pyfields > pip install pytypes > pip install valid8 > pip install PyContracts 1. Basic usage¶
The following code shows how you define a House with two attributes name and nb_floors:
from autoclass import autoclass @autoclass class House: def __init__(self, name, nb_floors = 1): pass That's it ! By default you get that the constructor is filled automatically, a "dictionary" behaviour is added to the class, a string representation of objects is available, and objects are comparable (equality) and hashable:
>>> obj = House('my_house', 3) >>> print(obj) # string representation House(name='my_house', nb_floors=3) >>> [att for att in obj.keys()] # dictionary behaviour ['name', 'nb_floors'] >>> assert {obj, obj} == {obj} # hashable: can be used in a set or as a dict key >>> assert obj == House('my_house', 3) # comparison (equality) >>> assert obj == {'name': 'my_house', 'nb_floors': 3} # comparison with dicts If you wish to add some behaviour (custom logic, logging...) when attributes are accessed or set, you can easily override the generated getters and setters. For example, below we will print a message everytime nb_floors is set:
from autoclass import autoclass, setter_override @autoclass class House: def __init__(self, name, nb_floors = 1): pass @setter_override def nb_floors(self, nb_floors = 1): print('Set nb_floors to {}'.format(nb_floors)) self._nb_floors = nb_floors We can test it:
>>> obj = House('my_house') Set nb_floors to 1 >>> obj.nb_floors = 3 Set nb_floors to 3 pyfields combo¶
If you already use pyfields to define mandatory/optional fields with type/value validation, simply decorate your class with @autoclass and you'll get all of the above (dict view, hashability, string representation, equality...) too.
Even better, pyfields now provide its own version of @autoclass, that has more relevant options for pyfields users. It is therefore highly recommended. See pyfields documentation.
You can do the same manually as shown below, but it is recommended to use the one in pyfields:
from pyfields import field from autoclass import autoclass from mini_lambda import x @autoclass class House: name: str = field(check_type=True, doc="the name of your house") nb_floors: int = field(default=1, check_type=True, doc="the nb floors", validators={ "should be positive": x >= 0, "should be a multiple of 100": x % 100 == 0 }) The above example works because behind the scenes, if autoclass detects that your class uses pyfields, it will automatically use the fields rather than the constructor signature to get the list of fields. You can check that all the features are there:
>>> obj = House('my_house', 200) >>> print(obj) # string representation House(name='my_house', nb_floors=200) >>> [att for att in obj.keys()] # dictionary behaviour ['name', 'nb_floors'] >>> assert {obj, obj} == {obj} # hashable: can be used in a set or as a dict key >>> assert obj == House('my_house', 200) # comparison (equality) >>> assert obj == {'name': 'my_house', 'nb_floors': 200} # comparison with dicts Also, @autoclass now provides the possibility to set autofields=True to apply pyfields.autofields automatically before applying autoclass.
Note: all of this works with python 2.7, and 3.5+. See pyfields documentation for details.
2. Type and Value validation¶
If you do not use pyfields, then you might be interested to add type and value validation to your fields through another means.
a- PEP484 Type validation¶
enforce¶
PEP484 is the standard for inserting python type hint in function signatures, starting from python 3.5 (a backport is available through the independent typing module). Many compliant type checkers are now available such as enforce or pytypes.
If you decorate your class constructor with PEP484 type hints, then autoclass detects it and will automatically decorate the generated property getters and setters. We use enforce runtime checker in this example:
from autoclass import autoclass from enforce import runtime_validation @runtime_validation @autoclass class House: # the constructor below is decorated with PEP484 type hints def __init__(self, name: str, nb_floors: int = 1): pass We can test it:
>>> obj = House('my_house') >>> obj.nb_floors = 'red' enforce.exceptions.RuntimeTypeError: The following runtime type errors were encountered: Argument 'nb_floors' was not of type <class 'int'>. Actual type was str. See enforce documentation for details.
pytypes¶
Below is the same example, but with pytypes instead of enforce:
from autoclass import autoclass from pytypes import typechecked @typechecked @autoclass class House: # the constructor below is decorated with PEP484 type hints def __init__(self, name: str, nb_floors: int = 1): pass typeguard¶
TODO
b- Simple Type+Value validation¶
valid8¶
valid8 was originally created in this project and is now independent. It provides mainly value validation, but also basic type validation. With valid8, in order to add validation to any function, you simply decorate that function with @validate_arg, possibly providing custom error types to raise:
from valid8 import validate_arg @validate_arg('foo', <validation functions>, error_type=MyErrorType) def my_func(foo): ... Now if you decorate your class constructor with @validate_arg, then autoclass detects it and will automatically decorate the generated property setters too.
from autoclass import autoclass from mini_lambda import s, x, Len from valid8 import validate_arg, InputValidationError from valid8.validation_lib import instance_of, is_multiple_of # 2 custom validation errors for valid8 class InvalidName(InputValidationError): help_msg = 'name should be a non-empty string' class InvalidSurface(InputValidationError): help_msg = 'Surface should be between 0 and 10000 and be a multiple of 100.' @autoclass class House: @validate_arg('name', instance_of(str), Len(s) > 0, error_type=InvalidName) @validate_arg('surface', (x >= 0) & (x < 10000), is_multiple_of(100), error_type=InvalidSurface) def __init__(self, name, surface=None): pass We can test it:
>>> obj = House('sweet home', 200) >>> obj.surface = None # Valid (surface is nonable by signature) >>> obj.name = 12 # Type validation InvalidName: name should be a non-empty string. >>> obj.surface = 10000 # Value validation InvalidSurface: Surface should be between 0 and 10000 and be a multiple of 100. See valid8 documentation for details. Note that other validation libraries relying on the same principles could probably be supported easily, please create an issue to suggest some !
PyContracts¶
PyContracts is also supported:
from autoclass import autoclass from contracts import contract @autoclass class House: @contract(name='str[>0]', surface='None|(int,>=0,<10000)') def __init__(self, name, surface): pass c- PEP484 Type+Value validation¶
Finally, in real-world applications you might wish to combine both PEP484 type checking and value validation. This works as expected, for example with enforce and valid8:
from autoclass import autoclass # Imports - for type validation from numbers import Integral from enforce import runtime_validation, config config(dict(mode='covariant')) # type validation will accept subclasses too # Imports - for value validation from mini_lambda import s, x, Len from valid8 import validate_arg, InputValidationError from valid8.validation_lib import is_multiple_of # 2 custom validation errors for valid8 class InvalidName(InputValidationError): help_msg = 'name should be a non-empty string' class InvalidSurface(InputValidationError): help_msg = 'Surface should be between 0 and 10000 and be a multiple of 100.' @runtime_validation @autoclass class House: @validate_arg('name', Len(s) > 0, error_type=InvalidName) @validate_arg('surface', (x >= 0) & (x < 10000), is_multiple_of(100), error_type=InvalidSurface) def __init__(self, name: str, surface: Integral=None): pass We can test that validation works:
>>> obj = House('sweet home', 200) >>> obj.surface = None # Valid (surface is nonable by signature) >>> obj.name = 12 # Type validation > PEP484 enforce.exceptions.RuntimeTypeError: The following runtime type errors were encountered: Argument 'name' was not of type <class 'str'>. Actual type was int. >>> obj.surface = 10000 # Value validation > valid8 InvalidSurface: Surface should be between 0 and 10000 and be a multiple of 100. Why autoclass ?¶
Python's primitive types (in particular dict and tuple) and it's dynamic typing system make it extremely powerful, to the point that it is often more convenient for developers to use primitive types or generic dynamic objects such as Munch, rather than small custom classes.
However there are certain cases where developers still want to define their own classes, for example to provide strongly-typed APIs to their clients. In such case, separation of concerns will typically lead developers to enforce attribute value validation directly in the class, rather than in the code using the object. Eventually developers end up with big classes like this one:
from valid8 import validate, Boolean from numbers import Real, Integral from typing import Optional, Union class House: def __init__(self, name: str, surface: Real, nb_floors: Optional[Integral] = 1, with_windows: Boolean = False): self.name = name self.surface = surface self.nb_floors = nb_floors self.with_windows = with_windows # --name @property def name(self): return self._name @name.setter def name(self, name: str): validate('name', name, instance_of=str) self._name = name # --surface @property def surface(self) -> Real: return self._surface @surface.setter def surface(self, surface: Real): validate('surface', surface, instance_of=Real, min_value=0, min_strict=True) self._surface = surface # --nb_floors @property def nb_floors(self) -> Optional[Integral]: return self._nb_floors @nb_floors.setter def nb_floors(self, nb_floors: Optional[Integral]): validate('nb_floors', nb_floors, instance_of=Integral, enforce_not_none=False) self._surface = nb_floors # !** # --with_windows @property def with_windows(self) -> Boolean: return self._with_windows @with_windows.setter def with_windows(self, with_windows: Boolean): validate('with_windows', with_windows, instance_of=Boolean) self._with_windows = with_windows Not to mention extra methods such as __str__, __eq__, from_dict, to_dict...
Now that's a lot of code - and only for 4 attributes ! Not mentioning the code for validate that was not included here for the sake of readability. And guess what - it is still highly prone to human mistakes. For example I made a mistake in the setter for nb_floors, did you spot it ? Also it makes the code less readable: did you spot that the setter for the surface property is different from the others?
Really, "there must be a better way" : yes there is, and that's what this library provides.
Main features¶
-
@autoargsis a decorator for the__init__method of a class. It automatically assigns all of the__init__method's parameters toself. For more fine-grain tuning, explicit inclusion and exclusion lists are supported, too. Note: the original @autoargs idea and code come from this answer from utnubu -
@autopropsis a decorator for a whole class. It automatically generates properties getters and setters for all attributes, with the correct PEP484 type hints. As for@autoargs, the default list of attributes is the list of parameters of the__init__method, and explicit inclusion and exclusion lists are supported.@autopropsautomatically adds@contract(PyContracts) or@validate_arg(fromvalid8) on the generated setters if a@contractor@validate_argexists for that attribute on the__init__method.@autoprops-generated getters and setters are fully PEP484 decorated so that type checkers like enforce automatically apply to generated methods when used to decorate the whole class. No explicit integration needed in autoclass!- You may override the getter or setter generated by
@autopropsusing@getter_overrideand@setter_override. Note that the@contractand@validatewill still be added on your custom setter if present on__init__, you don't have to repeat it yourself
-
@autodictis a decorator for a whole class. It makes a class behave like a (read-only) dict, with control on which attributes are visible in that dictionary. So this is a 'dict view' on top of an object, basically the opposite ofmunch(that is an 'object view' on top of a dict). It automatically implements__eq__,__str__and__repr__if they are not present already. -
@autohashis a decorator for a whole class. It makes the class hashable by implementing__hash__if not already present, where the hash is computed from the tuple of selected fields (all by default, customizable). -
@autorepris a decorator for a whole class. It adds a string representation by implementing__str__and__repr__if not already present. -
Equivalent manual wrapper methods are provided for all decorators in this library:
autoargs_decorate(init_func, include, exclude)autoprops_decorate(cls, include, exclude)autoprops_override_decorate(func, attribute, is_getter)autodict_decorate(cls, include, exclude, only_known_fields, only_public_fields)autohash_decorate(cls, include, exclude, only_known_fields, only_public_fields)autorepr_decorate(cls, include, exclude, only_known_fields, only_public_fields)
See Also¶
-
Initial idea of autoargs : this answer from utnubu
-
On properties in Python and why you should only use them if you really need to (for example, to perform validation by contract): Python is not java and the follow up article Getters/Setters/Fuxors
-
PEP484-based checkers:
-
attrs is a library with the same target, but the way to use it is quite different from 'standard' python. It is very powerful and elegant, though.
-
The new PEP out there, largely inspired by
attrs: PEP557. Check it out! There is also a discussion on python-ideas. -
decorator library, which provides everything one needs to create complex decorators easily (signature and annotations-preserving decorators, decorators with class factory) as well as provides some useful decorators (
@contextmanager,@blocking,@dispatch_on). We used it to preserve the signature of class constructors and overriden setter methods. Now we usemakefuninstead, which was inspired by it. -
When came the time to find a name for this library I was stuck for a while. In my quest for finding an explicit name that was not already used, I found many interesting libraries on PyPI. I did not test them all but found them 'good to know':
Do you like this library ? You might also like my other python libraries
Want to contribute ?¶
Details on the github page: https://github.com/smarie/python-autoclass