viral_mutant
viral_mutant

Reputation: 1003

How to always call a class method, forcefully, on return in python

I have a ReportEntry class

class ReportEntry(object):
  def __init__(self):
      # Many attributes defined here

  ... # Lot many setattr/getattr here
  def validate(self):
      # Lot of validation code in here
      return self

Multiple other classes maintain has-a relation with ReportEntry class

class A(object):
  def test1(self):
    t1 = ReportEntry()
    # Assign the attribute values to t1
    return t1.validate()

  def test2(self):
    t2 = ReportEntry()
    # Assign the attribute values to t2
    return t2.validate()

And there are multiple such classes as A.

I need to enforce each ReportEntry class instance to call validate() on return or maybe just before return.

Basically, no instance of ReportEntry should escape validation since the final report generation will fail if something is missing.

How may I achieve that ?

Upvotes: 4

Views: 2316

Answers (3)

viral_mutant
viral_mutant

Reputation: 1003

One approach that I could think of is to define __enter__ and __exit__ methods where validate is called upon __exit__ in ReportEntry

class ReportEntry(object):
  def __enter__(self):
    return self
  def __init__(self):
  # Many attributes defined here
  ... # Lot many setattr/getattr here
  def validate(self):
    # Lot of validation code in here
    return self
  def __exit__(self, a,b,c):
    self.validate()
    return True

# And then use it as
with ReportEntry() as report:
    ...

But again, this will be enforced only when used with ReportEntry() as report:

Upvotes: 1

user2390182
user2390182

Reputation: 73460

You can write a class decorator:

import inspect

def validate_entries(cls):
    def validator(fnc):  # this is a function decorator ...
        def wrapper(*args, **kwargs):
            rval = fnc(*args, **kwargs)
            if isinstance(rval, ReportEntry):
                # print('validating')
                return rval.validate()
            return rval
        return wrapper
    for name, f in inspect.getmembers(cls, predicate=inspect.isfunction):
        setattr(cls, name, validator(f))  # .. that we apply to all functions
    return cls

Now you can define all A-like classes:

@validate_entries
class A(object):
    # ...

This will validate any ReportEntry that is returned by any of A's methods.

Upvotes: 3

maininformer
maininformer

Reputation: 1077

There are two ways I can think about to go about this. I cannot say more without knowing more implementation details:

  1. Decorate your methods: Where every return instance is run through the decorator function. You may want to put this as a stand-alone function or part of a class depending on your specific use case.

    def validate(func):
        return func().validate()
    
    class A(object):
        @validate
        def test1(self):
            t1 = ReportEntry()
            # Assign the attribute values to t1
            return t1
        @validate
        def test2(self):
            t2 = ReportEntry()
            # Assign the attribute values to t2
            return t2
    
  2. Updating the __setattr__ and decorate your class:

    def always_validate(cls):
        # save the old set attribute method
        old_setattr = getattr(cls, '__setattr__', None)
        def __setattr__(self, name, value):
            # set the attribute
            validate(name, value)
            old_setattr(self, name, value)
    
        cls.__setattr__ = __setattr__
    
        return cls
    

and then you could decorate your ReportEntry:

@alway_validate
class ReportEntry(object):
    ...

Upvotes: 2

Related Questions