First, for the record: if the two methods must be called before the object is ready to be used, that means that calling them is part of the initialization of the object, and so they should be called by __init__
itself:
class Foo:
def __init__(self, ...):
...
self.not_raising_1()
self.not_raising_2()
def not_raising_1(self, ...):
...
def not_raising_2(self, ...):
...
But, moving on to the question as asked...
The problem is not well defined.
Unless you call the methods inside __init__
itself, it is trivially true that neither method has been called the instant __init__
exits.
Further, once __init__
exits, the class Foo
has no visibility into what happens outside its own definition. You need some sort of explicit state that maintains what happens after Foo.__init__
exits. Foo.not_raising_1
could examine that state to determine if anything else happened before it was called.
But that raises another problem: who will update that state? Every single bit of code would have to cooperate with Foo
. Is this illegal?
x = Foo()
y = 3
x.not_raising_1()
Then how are you going to make Python update your state when it executes y = 3
? The hooks just aren't there.
And finally, who is going to raise the exception if x.not_raising_1
is never called?
Refine the problem.
Rather than ask if the functions are never called, you can ensure they are called inside a with
statement using an appropriately defined context manager. This context manager can ensure that not_raising_1
and not_raising_2
are called before the with
statement completes, as well as ensure that they are only used inside a with
statement. You can't enforce that the object is used as a context manager, but you can ensure that it is only used in a with
statement.
class Foo:
def __init__(self, ...):
...
self._in_with_statement = False
self._r1_called = False
self._r2_called = False
def not_raising_1(self, ...):
self._r1_called = True
if not self._in_with_statement
raise RuntimeException("Foo instance must be used with context manager")
def not_raising_2(self, ...):
self._r2_called = True
if not self._in_with_statement
raise RuntimeException("Foo instance must be used with context manager")
def something_else(self):
if not self._r1_called or not self._r2_called:
raise RuntimeException("Failed to call not_raising_1 and/or not_raising_2")
...
def __enter__(self):
self._in_with_statement = True
def __exit__(self):
self._in_with_statement = False
if not self._r1_called or not self._r2_called:
raise RuntimeException("Failed to call not_raising_1 and/or not_raising_2")
self._r1_called = False
self._r2_called = False
Here, __init__
sets the condition that neither method has yet been called, nor are we yet executing in a with
statement. The instance itself acts as the external state that monitors how the instance is used.
The two required methods require themselves to be executed inside a with
statement (by checking if __enter__
has been called).
Every other method can check if the required methods have been called.
The __enter__
method simply marks the object as now being in a with
statement, allowing the required methods to be called.
The __exit_
method ensures that the required methods were eventually called, and resets the state of the object as being outside a context manger.
I think this is as strong a guarantee as you can enforce, short of a class that uses the inspect
module to examine the script's source code looking for violations.