3

I'm creating a class to represent a query, like this:

class Query:
    height: int
    weight: int
    age: int
    name: str
    is_alive: bool = True

As you can see, some variables start off initialized with defaults, others don't.

I want to implement chainable setters like so

    def of_height(self, height):
        self.height = height
        return self

    def with_name(self, name):
        self.name = name
        return self

    ...

The goal is to call this from several places in the project like so:

q = Query()
q.of_height(175).with_name("Alice")

Then I want to call a q.validate() that checks if any fields were not set, before calling an API with this query.

I can't figure out a way to dynamically check all possible variables, set or not, to check if any were left unset. Ideally, I don't want to implement a validate that has to be changed every time I add a possible query dimension in this class.

Antrikshy
  • 2,918
  • 4
  • 31
  • 65
  • 1
    The answers in the [proposed duplicate](https://stackoverflow.com/q/1398022/674039) are not helpful here because it won't include uninitialized ones. – wim Jun 08 '22 at 00:59
  • There is quite a lot of information [in this thread](https://stackoverflow.com/questions/50563546/validating-detailed-types-in-python-dataclasses). – metatoaster Jun 08 '22 at 01:04

3 Answers3

5

The variable annotations collected during class body execution are stored in an __annotations__ attribute which you can use.

>>> Query.__annotations__
{'height': int, 'weight': int, 'age': int, 'name': str, 'is_alive': bool}

This is documented in the datamodel under the "Custom classes" section.

Usually, you would not access this attribute directly but use inspect.get_annotations instead, which provides a few conveniences.

wim
  • 338,267
  • 99
  • 616
  • 750
  • Is there a way to make this inheritance friendly? If I have `class Query2(Query):`, can I get the annotations from both? – tdelaney Jun 08 '22 at 01:16
  • @tdelaney Hmm, I guess that `ChainMap(*[inspect.get_annotations(Q) for Q in Query2.__mro__])` might be sufficient? – wim Jun 08 '22 at 01:22
  • This is perfect! This project is not on 3.10 yet unfortunately, but `__annotations__` works well. Also worth mentioning for posterity that it does work on class instances, which is what I needed. As @tdelaney showed in their answer, `hasattr` or `getattr` is needed to actually make use of the annotation string names returned. – Antrikshy Jun 08 '22 at 17:32
3

Following on @wim's solution, it would be desirable to get annotations from self so that a validate method will work with subclasses. Following is an implementation using inspect.get_annotations - but note that its a 3.10 feature.

#!/usr/bin/env python3.10

import inspect
import itertools

class Query:
    height: int
    weight: int
    age: int
    name: str
    is_alive: bool = True

class Query2(Query):
    foo: int

    def get_annotated_attrs(self):
        return set(itertools.chain.from_iterable(inspect.get_annotations(Q).keys() for Q in self.__class__.__mro__))

    def validate(self):
        for name in self.get_annotated_attrs():
            if not hasattr(self, name):
                return False
        return True

q2 = Query2()
print(q2.get_annotated_attrs())
print(q2.validate())
tdelaney
  • 73,364
  • 6
  • 83
  • 116
0

I was thinking of something like this

import inspect

class Query:
    height: int
    weight: int
    age: int
    name: str
    is_alive: bool = True
    
    avilable_dimentions = ['height', 'weight', 'age', 'name', 'is_alive']

    def of_height(self, height):
        self.height = height
        return self

    def with_name(self, name):
        self.name = name
        return self
    
    def validate(self):
        not_defined = []
        for dim in self.avilable_dimentions:
            try:
                eval(f'self.{dim}')
            except:
                not_defined.append(dim)
        
        if not_defined:
            raise Exception(f'Missing dimentions {not_defined}') 
        return self

class Query2(Query):
    height2: int
    weight2: int

    avilable_dimentions = Query.avilable_dimentions + ['height2', 'weight2']
    

q = Query2()
q = q.of_height(175).with_name("Alice").validate()
Hanna
  • 1,071
  • 1
  • 2
  • 14