Collection of Utilities
Project description
WorkToy v0.40.0
pip install worktoy
Table of Contents
WorkToyClass
Parent class providing general utility functions on the class itself.
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def __init__(self, *args, **kwargs) -> None:
WorkToyClass.__init__(self, *args, **kwargs)
By inheriting from the WorkToyClass
, instances now have access to a
collection of utility functions:
WorkToyClass.maybe
from typing import Any
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *args) -> Any:
"""Instance method using ``maybe`` use a default argument value."""
return self.maybe(*args)
myInstance = MyClass()
myInstance.instanceMethod(None, [], ) # >>> []
WorkToyClass.maybeType
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *args, **kwargs) -> int:
"""Instance method using ``maybeType`` to extract an integer from the
positional arguments."""
return self.maybeType(int, *args)
myInstance = MyClass()
myInstance.instanceMethod('one', 2, '3', 4, 5) # >>> 2
WorkToyClass.maybeTypes
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *args, **kwargs) -> int:
"""Instance method using 'maybeTypes' to extract every integer from the
positional arguments."""
out = self.maybeTypes(int, *args)
if isinstance(out, int):
return out
myInstance = MyClass()
myInstance.instanceMethod('one', 2, '3', 4, 5) # >>> [2, 4, 5]
WorkToyClass.searchKey
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *keys, **kwargs) -> int:
"""Instance method using ``searchKey`` to search for keyword argument
value."""
return self.searchKey(*keys, **kwargs)
myInstance = MyClass()
myInstance.instanceMethod('count', 'Count', 'amount', count=7) # >>> 7
WorkToyClass.searchKeys
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *keys, **kwargs) -> int:
"""Instance method using ``searchKeys`` to search for every keyword
argument."""
return self.searchKeys(*keys, **kwargs)
myInstance = MyClass()
myInstance.instanceMethod('a', 'd', 'e', a=1, b=2, c=3, d=4) # >>> [1, 4]
WorkToyClass.maybeKey
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *args, **kwargs) -> int:
"""Instance method using ``maybeKey`` to search for a keyword argument
value with a type restriction argument."""
return self.maybeKey(*args, **kwargs)
myInstance = MyClass()
myInstance.instanceMethod('a', 'b', int, a='1', b=2, c=3, d=4) # >>> 2
WorkToyClass.maybeKeys
from worktoy.worktoyclass import WorkToyClass
class MyClass(WorkToyClass):
"""Example class"""
def instanceMethod(self, *args, **kwargs) -> int:
"""Instance method using ``maybeKeys`` to search for every keyword
argument restricted to a certain type."""
return self.maybeKeys(*args, **kwargs)
myInstance = MyClass()
myInstance.instanceMethod('a', 'b', int, a=1, b=2, c=3, d=4) # >>> [1, 2]
WorkToyClass - Guards
The following methods are various type guards.
WorkToyClass.noneGuard
Raises UnavailableNameException
if the given object is not None.
WorkToyClass.someGuard
Raises MissingArgumentException
if given object is None
WorkToyClass.overRideGuard
Raises UnavailableNameException
if given object is not None
WorkToyClass.functionGuard
Raises TypeSupportError
if given object is not a function
WorkToyClass.intGuard
Raises TypeSupportError
if given object is None or not an integer
WorkToyClass.floatGuard
Raises TypeSupportError
if given object is None or not a float
WorkToyClass.strGuard
Raises TypeSupportError
if given object is None or not a string
Descriptors
The Descriptors package implements descriptors. These serve as
alternatives to the property
descriptor. WorkToy provides two ways of
using descriptors: Attribute
and Field
.
AbstractAttribute
This is the simpler implementation beginning with AbstractAttribute
.
WorkToy provides the following subclasses of this abstract baseclass:
IntAttribute
FloatAttribute
StrAttribute
The above are distinguished only by their type. When instantiating these a default value may be provided, but is optional.
from worktoy.worktoyclass import WorkToyClass
from worktoy.descriptors import Field, FloatAttribute
from worktoy.descriptors import IntAttribute, StrAttribute
class MyClass(WorkToyClass):
"""Example class"""
n = IntAttribute(77)
r = FloatAttribute(0.5)
name = StrAttribute()
def __init__(self, *args, **kwargs) -> None:
WorkToyClass.__init__(self, *args, **kwargs)
myInstance = MyClass()
myInstance.n # >>> 77
myInstance.n = 7
myInstance.n # >>> 7
myInstance.r # 0.5
myInstance.name # >>> ````
myInstance.name = 'Name'
Field
This class are defined in the class body in the same way as the attributes mentioned above. Unlike attributes, instances of Field defined on a class body must also decorate their own accessor methods.
from worktoy.worktoyclass import WorkToyClass
from worktoy.descriptors import Field, FloatAttribute
class MyClass(WorkToyClass):
"""Example class"""
area = Field()
width = FloatAttribute(6)
height = FloatAttribute(8)
def __init__(self, *args, **kwargs) -> None:
WorkToyClass.__init__(self, *args, **kwargs)
@area.getter
def getArea(self) -> float:
"""Getter-Function for the area"""
return self.width * self.height
@area.setter
def setArea(self, newArea: float) -> None:
"""Setter-Function for the area."""
oldArea = self.getArea()
if not oldArea:
raise ZeroDivisionError
scale = (newArea / oldArea) ** 0.5
self.width *= scale
self.height *= scale
myInstance = MyClass()
myInstance.width # >>> 3
myInstance.height # >>> 4
myInstance.area # >>> 12
myInstance.area = 48
myInstance.width # >>> 3
myInstance.height # >>> 4
Notice the flexibility available by defining a setter function for the
area
field.
MetaClass
Metaclasses are certainly the most powerful tool available in Python
development. The WorkToy package provides a basic skeleton for
implementing custom metaclasses in the form of AbstractMetaClass
and
AbstractNameSpace
. Before explaining the merits of these, a
examination of how metaclasses work seem appropriate.
Introduction to metaclasses
You are already familiar with the default baseclass: type
. In a
somewhat unfortunate choice of nomenclature, we face an ambiguity here:
do we mean type
as in: isinstance(int, type)
or do we mean:
type(int)
? The first treats type
as a type
, but the second
treats type
as a function. To illustrate how unfortunate this
nomenclature is, consider this expression:
type(type) is type
or isinstance(type, type) >>> True
A metaclass
is a custom type
. Consider TestClass
defined below:
from worktoy.worktoyclass import WorkToyClass
class TestClass(WorkToyClass):
"""Created with traditional class body."""
def __init__(self, *args, **kwargs) -> None:
WorkToyClass.__init__(self, *args, **kwargs)
def instanceMethod(self, *args, **kwargs) -> int:
"""Instance method"""
return self.maybeType(int, *args)
The above is entirely equivalent to:
from typing import Any
from worktoy.worktoyclass import WorkToyClass
def initFunc(instance: Any, *args, **kwargs) -> None:
"""Init function"""
WorkToyClass.__init__(instance, *args, **kwargs)
def someFunc(self, *args, **kwargs) -> int:
"""Instance Method"""
return self._maybeType(int, *args)
name = 'TestClass'
bases = (WorkToyClass,)
nameSpace = dict(__init__=initFunc, instanceMethod=someFunc)
TestClass = type(name, bases, nameSpace)
type
The type
object used above specifies the creation of new classes. By
creating a custom metaclass
, we are able to define our own class
creation. Below we define a metaclass
that behaves entirely like
type
allowing us to recognize the class creation we are familiar with
and see how we can change this behaviour.
(Please note, the naming convention: mcls
: metaclass, cls
: new
class, self
new instance).
from typing import Any
class BaseMeta(type): # metaclasses inherit from type
"""Base metaclass behaving line ``type``"""
@classmethod
def __prepare__(mcls, name: str, bases: tuple[type], **kwargs) -> dict:
"""The prepare method creates the empty mapping object providing the
namespace for the newly created class. The base implementation
returns an empty instance of ``dict``."""
return {}
def __new__(mcls, name: str, bases: tuple[type], nameSpace: dict,
**kwargs) -> type:
"""The ``__new__`` method createds the new class,"""
cls = type.__new__(mcls, name, bases, nameSpace, **kwargs)
return cls
def __init__(cls, name: str, bases: tuple[type], nameSpace: dict,
**kwargs) -> None:
"""Once the new class is created it is initialised by this method. """
type.__init__(cls, name, bases, nameSpace, **kwargs)
def __call__(cls: type, *args, **kwargs) -> Any:
"""This method specifies how the newly creatd class creates instances
of itself. The default behaviour is as shown below: The instance is
created with the __new__ method on the newly created class, and then
it is initialized with the __init__ on the newly created class."""
self = cls.__new__(cls, )
cls.__init__(self, *args, **kwargs)
return self
By introducing custom metaclasses, we are free to customize the above steps to achieve any imaginable functionality. People say that Python does not support function overloading. What they mean is that function overloading in Python must be implemented at the metaclass level. (That is dumb, function overloading should not require custom metaclasses, but the point stands).
NameSpace
Customizing the __prepare__
method gives the greatest
opportunity to customize the class creation. Let us examine the
requirements for the namespace object returned by the __prepare__
method. When attempting to use a custom class for this purppose, one is
likely to encounter errors like:
"""TypeError: type.__new__() argument 3 must be dict, not NameSpace"""
"""TypeError: META.__prepare__() must return a mapping, not NameSpace"""
"""TypeError: ``NameSpace`` object does not support item assignment"""
It is possible to create a custom class that does not trigger any such
TypeError
, which is able to create classes without any problem. Until
one day, you introduce a staticmethod
and then receive:
"""
@staticmethod
^^^^^^^^^^^^
TypeError: ``NoneType`` object is not callable"""
What even is that error message? The above happens if the __getitem__
method on the namespace object does not raise a KeyError when receiving a
missing key. The expected behaviour from the namespace object receiving a
missing key is to raise a KeyError with the missing key as the message.
For example:
from typing import Any
def __getitem__(self, key: str, ) -> Any:
try:
dict.__getitem__(self, key)
except KeyError as e:
print(key)
raise e
By including the print statement, we can see that the problems occur where the class body has a valid expression without an equal sign. For example when decorating a function. Consider the following example:
from typing import Any, Callable
class NameSpace(dict, ):
"""NameSpace custom class"""
def __getitem__(self, key: str, ) -> Any:
"""Prints missing keys that are encountered."""
try:
return dict.__getitem__(self, key)
except KeyError as e:
print(key)
raise e
class META(type):
"""Metaclass implementing the __prepare__ method which returns an
instance of the NameSpace class."""
@classmethod
def __prepare__(mcls, name, bases, **kwargs) -> Any:
nameSpace = NameSpace()
return nameSpace
def func(f: Callable) -> None:
"""Decorator"""
return f
class TestClass(metaclass=META):
"""TestClass """
@staticmethod
@func
def a(self) -> None:
pass
if __name__ == '__main__':
TestClass()
When running the above script, we see the following printed to the console:
'__name__'
'staticmethod'
'func'
Fortunately, WorkToy provides the AbstractNameSpace
class which
implements all required mapping funcionality. Besides implementing
dict
methods, it logs every line in the class body.
In the following section, we shall see a practical use case for the metaclass system.
SYM
The SYM package provides symbolic classes similar to Enum
. It
achieves this by the use of the aforementioned AbstractNameSpace
in a
metaclass implementation.
The new class should inherit from a symbolic baseclass such as the SYM
class which may be used directly or as a baseclass for a custom
implementation. In the class body of the new class, create the instances.
The SYM
class provides a way of creating instances. The default
implementation uses the auto
method. For example,
class WeekDay(SYM):
Symbolic class representation of weekdays
MONDAY=SYM.auto() TUESDAY=SYM.auto() ...
Note that the instances are named in upper case as is convention. The worktoyclass implementation is case-insensitive, but presents instances in upper case. This means that:
WeekDay.Wednesday is WeekDay.WEDNESDAY
The SYM baseclass implements hashing allowing dictionaries to use
instances as keys. The SYM baseclass also implements __eq__
and
__ne__
.
In the SyMeta metaclass, the SymSpace class is used to provide the
namespace object returned by the __prepare__
method. The SymSpace class
relies on the SYM-subclass to decide if an entry in the class body denotes
an instance creation. The SymSpace dictionary splits the data to
instanceSpace and to namespace. The namespace data is then used in the
super call as part of the normal class creation process, and the
instanceSpace data is used in the __init__
method on the metaclass to
facilitate instance creation. Finally, the metaclass implements the
__call__
method to restrict instance creation to those instances defined
in the instanceSpace.
A custom implementation of the SYM class must provide the following to be recognized by the rest of the SYM module:
-
Identification. The custom implementation should explicitly define itself as a symbolic baseclass by setting:
__symbolic_baseclass__ = True
If no baseclass has this variable set, the SymSpace defaults to using the default SYM class, even if an intended symbolic baseclass is present. This allows symbolic classes to inherit from other classes. -
Validation. The custom implementation must provide a method called
validateInstance
mapping[str, object] -> bool
which is used by the SymSpace instance to determine when an entry in the class body is to mean instance of the symbolic class. -
Differentiation. The custom implementation must set a unique value on each instance. By default, this value is an integer assigned in the order received in the class body beginning at
0
. Custom implementations are free to use any data type and reimplement the__eq__
method as appropriate. -
Instantiation. The custom implementation is responsible for creating the instance given a key, value pair. The default implementation creates a worktoyclass instance of the new class and an instance of itself that wraps it. Please note that the default implementation requires the new class to support instance creation with:
instance = NewClass()
. Otherwise, necessary arguments must be provided in the class body, for example,instance = SYM.auto(420, 69)
The above will result in theinstance
wrapping:NewClass(420, 69)
-
Parsing (OPTIONAL). If provided, the
parseInstance
method should take*args, **kwargs
and return the instance matching or raise a NameError. The default implementation allows for the new class to be callable. By default, the instance valueint
or the instancename
may be used to retrieve the desired instance by calling the class. For example:WeekDay.Tuesday == WeekDay(tuesday)
# case-insensitive If the parsing method is not defined, or the new class itself implements__call__
the above will not be available. -
Decoration (OPTIONAL). The default implementation provides the following enhancements of the new class:
- Iteration:
for item in
Weekday
:Weekday.MONDAY
# the wrapped instanceWeekday.TUESDAY
... The above is achieved with__iter__
__next__
and__len__
implemented as class methods. The__len__
is taken to mean the number of instances on the class.
- Dictionary-like behaviour:
- Implementation of
keys
,values
anditems
. - Implementation of
__getitem__
:Weekday[0]
->Weekday.MONDAY
#int
refers to valueWeekday[
monday]
->Weekday.MONDAY
#str
refers to name
- Implementation of binary arithmetic operations including cases of
reflected operands.
Weekday[2] + 3
=Weekday.SATURDAY
#SYM.__add__(self, other)
3 + Weekday[2] = 5
#SYM.__radd__(self, other)
Weekday[2] += 1 -> Weekday[3]
#SYM.__iadd__(self, other)
The arithmetic implementation provides integer like behaviour. So when an instance appears as theother
operand, it behaves like an integer of the same value as itself. Instances use modular arithmetic such that:Weekday.SATURDAY + 2 -> Weekday.MONDAY
In general withn = len(SYM)
:SYM.INSTANCE[a] + SYM.INSTANCE[b] = SYM.INSTANCE[(a + b) % n]
The default implementation provides parsing such that:Weekday(4) -> Weekday.FRIDAY
Weekday(friday) -> Weekday.FRIDAY
# Case insensitive by default.
- Implementation of
- Iteration:
for item in
Wait A Minute!
In this module, WorkToy provides the custom exceptions used throughout the entire package.
MetaXcept
Just like the SYM module, the custom exceptions
implement a custom metaclass inheriting from AbstractMetaClass
. This
metaclass MetaXcept
uses a custom namespace class inheriting from the
AbstractNameSpace
in its __prepare__
method.
Below is a reference list of the custom exceptions currently implemented:
MetaTypeSupportError
Description
Indicates that an instance is not a member of class derived from the correct metaclass.
Signature
from typing import Any
expMetaClass: type # The expected metaclass
actualValue: Any # The actual value received
argName: str # Argument name
MissingArgumentException
Description
Invoked when a function is called before it is ready.
Signature
from typing import Any
missingArgument: str # Argument name
RecursiveCreateGetError
Description
Raised when a getter function calls a creator function a second time.
Signature
from worktoy.core import Function
from typing import Any
creator: Function # The expected type
variableType: type # The type of the variable
variableName: str # Argument name
TypeSupportError
Description
This exception should be raised when the argument type is not supported.
Signature
from typing import Any
expectedType: type # The expected type
actualValue: Any # The actual value received
argName: str # Argument name
UnavailableNameException
Description
Exception raised when a name is already occupied. Meaning that the named
argument were expected to be None
.
Signature
from typing import Any
argName: str # The unavailable name
existingValue: Any # The present value at the name
newValue: str # The new value attempted to set
UnexpectedEventException
Description
Raised when receiving a QEvent
of the wrong QEvent.Type
. (Please note
that this exception is intended for use with the companion WorkSide
module.)
Signature
from typing import Any
expectedQEventType: str # The expected QEvent.Type
actualEvent: Any # The actual instance of QEvent received
argumentName: str # The argument name
UnsupportedSubclassException
Description
This exception should be raised when encountering a variable of correct type, but of incorrect subclass.
Signature
from typing import Any
argumentName: str # The argument name
expectedParent: type # The actual instance of QEvent received
actualValue: str # The actual value of the variable
Core
This module provides common types and constants.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.