I'm trying to understand __new__ and I'm fairly new to OOP python. I have this code...
import csv
from csv import DictReader
import logging
import typing as T
class MaxLength(str):
def __new__(cls, field: str):
# how do i pass this dynamically in the PCW obj
maximum = 4
if len(field) > maximum:
raise ValueError('invalid length of fields at: ' + field)
return super().__new__(cls, field)
class PCW(T.NamedTuple):
length_checker: MaxLength
#classmethod
def from_row(cls, row: dict):
return cls(**{key: type_(row[key]) for key, type_ in cls._field_types.items()})
def validate_csv(reader: DictReader) -> bool:
for row in reader:
try:
PCW.from_row(row)
except Exception as e:
logging.error('type: {} msg: {}'.format(type(e), e))
return False
return True
input_file = validate_csv(csv.DictReader(open("test.csv")))
This works but I want to be able to pass maximum in as an argument so as this will change. ie:
class PCW(T.NamedTuple):
length_checker: MaxLength(maximum=4)
...
input_file = validate_csv(csv.DictReader(open("test.csv")))
I think I have gone down the rabbit hole. Is this possible or what am I overlooking/misunderstanding?
One way to do it would be to use the object.__init_subclass__() classmethod that was added in Python 3.6. Making use of it requires subclassing your MaxLength subclass.
Here's what I mean:
import csv
from csv import DictReader
import logging
import typing as T
class MaxLength(str):
maximum = 8
#classmethod
def __init_subclass__(cls, **kwargs):
maximum = kwargs.pop('maximum', cls.maximum)
super().__init_subclass__(**kwargs)
cls.maximum = maximum
def __new__(cls, field: str):
if len(field) > cls.maximum:
raise ValueError('invalid length of fields at: ' + field)
return super().__new__(cls, field)
class PCW(T.NamedTuple):
# class PCWMaxLength(MaxLength):
class PCWMaxLength(MaxLength, maximum=4): # Override default maximum.
pass
length_checker: PCWMaxLength
#classmethod
def from_row(cls, row: dict):
return cls(**{key: type_(row[key]) for key, type_ in cls._field_types.items()})
# Display value assigned to nested class' constant.
print(f'PCWMaxLength.maximum: {PCWMaxLength.maximum}') # -> PCWMaxLength.maximum: 4
def validate_csv(reader: DictReader) -> bool:
for row in reader:
try:
PCW.from_row(row)
except Exception as e:
logging.error('type: {} msg: {}'.format(type(e), e))
return False
return True
Otherwise, I think you will need to do some actual metaclass programming…
You could make it a default argument:
class MaxLength(str):
def __new__(cls, field: str, maximum: int=4):
if len(field) > maximum:
raise ValueError('invalid length of fields at: ' + field)
return super().__new__(cls, field)
Related
I've been using this style of inheritance to validate values set on instances of objects, but I'm wondering if there is a more fluent way to do this.
I'm following a spec where items of a certain classification (Foo) contain elements of a certain composition (Fe).
class Typed:
def __set__(self, obj, value):
assert isinstance(value, self._type), 'Incorrect type'
class Integer(Typed):
_type = int
class Float(Typed):
_type = float
class Positive(Typed):
def __set__(self, obj, value):
super().__set__(obj, value)
assert value >= 0, 'Positive Values Only Accepted'
class PositiveInteger(Integer, Positive):
pass
class PositiveFloat(Float, Positive):
pass
class Sized(Typed):
def __set__(self, obj, value):
super().__set__(obj, value)
assert value <= 2**self.size-1, f'{value} is too High'
class Fe(Sized, PositiveInteger):
name = 'Integer, 8 bit unsigned'
size = 8
class Foo(Fe):
name = 'Classificaion1'
def __set__(self, obj, id):
super().__set__(obj, id)
obj._id = id
def __get__(self, obj, objType=None):
return obj._id
def __del__(self):
pass
If you really need this level of abstraction, this is possibly the best way you can do it. My suggestion bellow can maybe save one line per class.
If you can afford to have attributes like "size" and "type" to be defined
on the final class, a richer base class and a declarative structure containing the checks as "lambda functions" can be used like this.
Note the usage of __init_subclass__ to check if all the parametes
needed for the guard expressions are defined:
from typing import Sequence
GUARDS = {
"typed": ((lambda self, value: "Incorrect type" if not instance(value, self._type) else None), ("_typed",)),
"positive": ((lambda self, value: "Only positive values" if value < 0 else None), ()),
"sized": ((lambda self, value: None if value <= 2 ** self.size - 1 else f"{value} must be smaller than 2**{self.size}"), ("size",)),
}
class DescriptorBase:
guards: Sequence[str]
def __init_subclass__(cls):
_sentinel = object()
for guard_name in cls.guards:
guard = GUARDS[guard_name]
required_attrs = guard[1]
missing = []
for attr in required_attrs:
if getattr(cls, attr, _sentinel) is _sentinel:
missing.append(attr)
if missing:
raise TypeError("Guarded descriptor {cls.__name__} did not declare required attrs: {missing}")
def __set_name__(self, owner, name):
self._name = f"_{name}""
def __set__(self, instance, value):
errors = []
for guard_name in self.guards:
if (error:= GUARDS[guard_name](self, value)) is not None:
errors.append(error)
if errors:
raise ValueError("\n".join(errors))
setattr (instance, self._name, value)
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.name)
def __del__(self, instance):
delattr(instance, self._name)
class Foo(DescriptorBase):
guards = ("typed", "positive", "sized")
size = 8
type_ = int
# No other code required here: __get__, __set__, __del__ handled in superclass
class UseAttr:
# Actual smart-attr usage:
my_foo = Foo()
Actually, if you want the class hierarchy, with less lines (no need to declare a __set__ method in each class), this approach can be used as well:
just change __init_superclass__ to collect "guards" in all superclasses,
and consolidate a single guards list on the class being defined, and then
define your composable guard-classes just as:
class Positive(BaseDescriptor):
guards = ("positive",)
class Sized(BaseDescriptor):
guards = ("sized",)
size = None
class Foo(Positive, Sized):
size = 8
class Fe(Foo):
name = "Fe name"
Actually, the change needed for this to work can be as simple as:
def __init_subclass__(cls):
_sentinel = object()
all_guards = []
for supercls in cls.__mro__:
all_guards.extend(getattr(supercls, "guards", ()))
# filter unique:
seem = {}
new_guards = []
for guard in all_guards:
if guard not in seem:
new_guards.append(guard)
seem.add(guard)
cls.guards = new_guards
for guard_name in cls.guards:
Also note that you could also collect the contents of the "GUARDS" registry from each defined class, instead of having to declare everything as lambdas before hand. I think you can get the idea from here on.
I'm trying to get the following behavior with pydantic.BaseModel:
class MyClass:
def __init__(self, value: T) -> None:
self._value = value
# Maybe:
#property
def value(self) -> T:
return self._value
# Maybe:
#value.setter
def value(self, value: T) -> None:
# ...
self._value = value
If T is also a pydantic model, then recursive initialization using dictionaries should work:
# Initialize `x._value` with `T(foo="bar", spam="ham")`:
x = MyClass(value={"foo": "bar", "spam": "ham"})
Note that _value is initialized using the kwargs value. Validation must also be available for private fields.
The pydantic docs (PrivateAttr, etc.) seem to imply that pydantic will never expose private attributes. I'm sure there is some hack for this. But is there an idiomatic way to achieve the behavior in pydantic? Or should I just use a custom class?
Not sure it this solution is advisable, based on: https://github.com/samuelcolvin/pydantic/issues/1577
https://github.com/samuelcolvin/pydantic/issues/655
import inspect
from typing import Dict
from pydantic import BaseModel, PrivateAttr
from pydantic.main import no_type_check
class PatchedModel(BaseModel):
#no_type_check
def __setattr__(self, name, value):
"""
To be able to use properties with setters
"""
try:
super().__setattr__(name, value)
except ValueError as e:
setters = inspect.getmembers(
self.__class__,
predicate=lambda x: isinstance(x, property) and x.fset is not None
)
for setter_name, func in setters:
if setter_name == name:
object.__setattr__(self, name, value)
break
else:
raise e
class T(BaseModel):
value1: str
value2: int
class MyClassPydantic(PatchedModel):
_value: T = PrivateAttr()
def __init__(self, value: Dict, **kwargs):
super().__init__(**kwargs)
object.__setattr__(self, "_value", T(**value))
#property
def value(self) -> T:
return self._value
#value.setter
def value(self, value: T) -> None:
self._value: T = value
# To avoid the PatchedModel(BaseModel) use instead
# def set_value(self, value: T) -> None:
# self._value: T = value
if __name__ == "__main__":
my_pydantic_class = MyClassPydantic({"value1": "test1", "value2": 1})
print(my_pydantic_class.value)
my_pydantic_class.value = T(value1="test2", value2=2)
# my_pydantic_class.set_value(T(value1="test2", value2=2))
print(my_pydantic_class.value)
I ended up with something like this, it acts like a private field, but i can change it by public methods:
import inspect
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, Field
class Entity(BaseModel):
"""Base entity class."""
def __setattr__(self, name, value):
if "self" not in inspect.currentframe().f_back.f_locals:
raise Exception("set attr is protected")
super().__setattr__(name, value)
class PostId(UUID):
"""Post unique id."""
class Post(Entity):
"""Post."""
post_id: PostId = Field(description='unique post id')
title: Optional[str] = Field(None, description='title')
def change_title(self, new_title: str) -> None:
"""Changes title."""
self.title = new_title
I just looking at inspect.currentframe().f_back.f_locals and looking for self key.
Ispired by accessify
Tested with this little test:
from uuid import uuid4
import pytest
import post_pydantic
def test_pydantic():
"""Test pydantic varriant."""
post_id = uuid4()
post = post_pydantic.Post(post_id=post_id)
with pytest.raises(Exception) as e:
post.post_id = uuid4()
assert post.post_id == post_id
assert e.value.args[0] == "set attr is protected"
new_title = "New title"
post.change_title(new_title)
assert post.title == new_title
I created a class 'Stage' and want instantiate it only if argument passed to init(arg)
#example code
class Stage:
def __init__(self, arg):
if type(arg) == str:
#create object
else:
#do not create object
#main:
# entry = input()
obj = Stage(entry)
if obj:
print("created") # if entry is string
else:
print("not created") # if entry is float
Raise an exception:
def __init__(self, arg):
if not isinstance(arg, str):
raise TypeError("Stage.__init__ called with a non-str value: %r" % (arg,))
# continue initializing the object
However, consider whether it the value really needs to be a str, or just something that can be turned into a str:
def __init__(self, arg):
arg = str(arg)
# ...
If you want to avoid creating the instance altogether, you need to override __new__, not __init__ (with some of the previous advice folded in):
class Stage:
def __new__(cls, arg):
try:
arg = str(arg)
except ValueError:
raise TypeError("Could not convert arg to str: %r" % (arg, ))
return super().__new__(cls, arg)
Check for the type of argument before instantiating your object. Also consider using isinstance to check for a type, instead of type
class Stage:
def __init__(self, arg):
pass
if isinstance(str, entry):
obj = Stage(entry)
else:
raise TypeError('A str-type is required as an argument to the constructor')
You cannot initialize an object with that condition, but you can throw an error
class Stage:
def __init__(self, arg):
if not isinstance(arg, str):
raise TypeError("non-str value: %r was passed, str type argument required " % (arg,))
You can also use a classmethod to create an instance only if the passed value is a string:
class Stage:
def __init__(self, val):
self.val = val
#classmethod
def stage(cls, arg):
return None if not isinstance(arg, str) else cls(arg)
s = Stage.stage("name")
Now, s will either be instance of Stage if arg is a string or None if arg is any other type.
I have a simple custom field implemented to utilize Python 3 Enum instances. Assigning enum instances to my model attribute, and saving to the database works correctly. However, fetching model instances using a QuerySet results in the enum attribute being a string, instead of the respective Enum instance.
How do I get the below EnumField to return valid Enum instances, rather than strings?
fields.py:
from enum import Enum
from django.core.exceptions import ValidationError
from django.db import models
class EnumField(models.CharField):
description = 'Enum with strictly typed choices'
def __init__(self, enum_class, *args, **kwargs):
self._enum_class = enum_class
choices = []
for enum in self._enum_class:
title_case = enum.name.replace('_', ' ').title()
entry = (enum, title_case)
choices.append(entry)
kwargs['choices'] = choices
kwargs['blank'] = False # blank doesn't make sense for enum's
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
args.insert(0, self._enum_class)
del kwargs['choices']
return name, path, args, kwargs
def from_db_values(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value):
if value is None or isinstance(value, self._enum_class):
return value
else:
return self._parse_enum(value)
def _parse_enum(self, value):
try:
enum = self._enum_class[value]
except KeyError:
raise ValidationError("Invalid type '{}' for {}".format(
value, self._enum_class))
else:
return enum
def get_prep_value(self, value):
if value is None:
return None
elif isinstance(value, Enum):
return value.name
else:
msg = "'{}' must have type {}".format(
value, self._enum_class.__name__)
if self.null:
msg += ', or `None`'
raise TypeError(msg)
def get_choices(self, **kwargs):
kwargs['include_blank'] = False # Blank is not a valid option
choices = super().get_choices(**kwargs)
return choices
After a lot of digging, I was able to answer my own question:
SubfieldBase has been deprecated, and will be removed in Django 1.10; which is why I left it out of the implementation above. However, it seems that what it does is still important. Adding the following method to replaces the functionality that SubfieldBase would have added.
def contribute_to_class(self, cls, name, **kwargs):
super(EnumField, self).contribute_to_class(cls, name, **kwargs)
setattr(cls, self.name, Creator(self))
The Creator descriptor is what calls to_python on attributes. If this didn't happen, querys on models would result in the EnumField fields in the model instances being simply strings, instead of Enum instances like I wanted.
I have a class with several properties, each of which has to be a number. After repeating the same code over and over again I think there is a more pythonic way to declare multiple class instance properties as numbers.
Right now I set each property value to None and raise a type error if the value is set to a non number type. I'd prefer to set the property type to a number when the property is initialized.
Thanks!
Example:
import numbers
class classWithNumbers(object):
def __init__(self):
self._numProp1 = None
self._numProp2 = None
#property
def numProp1(self):
return self._numProp1
#numProp1.setter
def numProp1(self,value):
if not isinstance(value, numbers.Number): #repeated test for number
raise TypeError("Must be number")
self._numProp1 = value
#property
def numProp2(self):
return self._numProp2
#numProp2.setter
def numProp(self,value):
if not isinstance(value, numbers.Number):
raise TypeError("Must be number")
self._numProp2 = value
Also, I actually have this wrapped into a method that is repeated at each property setter:
def isNumber(value):
if not isinstance(value, numbers.Number):
raise TypeError("Must be number")
If every property of this class should be a number you can implement custom __setattr__ method:
class ClassWithNumbers(object):
def __init__(self):
self.num_prop1 = 0
self.num_prop2 = 0
def __setattr__(self, name, value):
if not isinstance(value, numbers.Number):
raise TypeError("Must be number")
super(ClassWithNumbers, self).__setattr__(name, value)
From documentation: __setattr__ (is) called when an attribute assignment is attempted. This is called instead of the normal mechanism (i.e. store the value in the instance dictionary). name is the attribute name, value is the value to be assigned to it.
More general approach would be to not allow type of once assigned attribute to change:
class ClassWithUnchangeableTypes(object):
def __init__(self):
self.num_prop1 = 0
self.num_prop2 = 0
def __setattr__(self, name, value):
if hasattr(self, name): # this means that we assigned value in the past
previous_value_type = type(getattr(self, name))
if not isinstance(value, previous_value_type):
raise TypeError("Must be {}".format(previous_value_type))
super(ClassWithUnchangeableTypes, self).__setattr__(name, value)
Speaking of pythonic, from pep8:
Class names should normally use the CapWords convention.
Use the function naming rules: lowercase with words separated by underscores as necessary to improve readability.
A fairly modern (python 3.5+) and pythonic way is using type hints
#property
def numProp1(self):
return self._numProp1
#numProp1.setter
def numProp1(self,value: int):
self._numProp1 = value
A more compatible way is to try to convert to int, which will then throw an exception for you if that fails. It might also have unwanted behaviour like accepting floats:
#property
def numProp1(self):
return self._numProp1
#numProp1.setter
def numProp1(self,value):
self._numProp1 = int(value)
But there's already nothing wrong with your implementation in general.
If you do not want to explicitly declare getters and setters, you could check their type when used, not when assigned.
The most Pythonic way is probably to call the int constructor and let it throw an exception:
class ClassWithNumbers(object):
def __init__(self, v1, v2):
self.numprop1 = int(v1)
self.numprop2 = int(v2)
if the numprops are part of your interface then creating #property accessors would be appropriate. You can also implement your own descriptor:
class Number(object):
def __init__(self, val=0):
self.__set__(self, val)
def __get__(self, obj, cls=None):
return self.val
def __set__(self, obj, val):
try:
self.val = int(val)
except ValueError as e:
raise TypeError(str(e))
class ClassWithNumbers(object):
numprop1 = Number(42)
numprop2 = Number(-1)
usage:
c = ClassWithNumbers()
print c.numprop1
c.numprop1 += 1
print c.numprop1
c.numprop1 = 'hello'