Reputation: 557
Below is an example of my my_create
method, and an example of that method in use.
@contextmanager
def my_create(**attributes):
obj = MyObject(**attributes)
yield obj
obj.save()
with my_create(a=10) as new_obj:
new_obj.b = 7
new_obj.a # => 10
new_obj.b # => 7
new_obj.is_saved() # => True
To users of Ruby/Rails, this may look familiar. It's similar to the ActiveRecord::create
method, with the code inside the with
block acting as, well, a block.
However:
with my_create(a=10) as new_obj:
pass
new_obj.a # => 10
new_obj.is_saved() # => True
In the above example, I've passed an empty "block" to my my_create
function. Things work as expected (my_obj
was initialized, and saved), but the formatting looks a little wonky, and the with
block seems unnecessary.
I would prefer to be able to call my_create
directly, without having to setup a pass
ing with
block. Unfortunately, that's not possible with my current implementation of my_create
.
my_obj = create(a=10)
my_obj # => <contextlib.GeneratorContextManager at 0x107c21050>
I'd have to call both __enter__
and __exit__
on the GeneratorContextManager
to get my desired result.
The question:
Is there a way to write my my_create
function so that it can be called with a "block" as an optional "parameter"? I don't want to pass an optional function to my_create
. I want my_create
to optionally yield execution to a block of code.
The solution doesn't have to involve with
or contextmanager
. For instance, the same results as above can be achieved with a generator
and a for
loop, although the syntax becomes even more unclear.
At this point I'm afraid that a readable-enough-to-be-sensibly-usable solution doesn't exist, but I'm still interested to see what everyone comes up with.
Some clarification:
Another example would be:
@contextmanager
def header_file(path):
touch(path)
f = open(path, 'w')
f.write('This is the header')
yield f
f.close()
with header_file('some/path') as f:
f.write('some more stuff')
another_f = header_file('some/other/path')
I always want to do the __enter__
and __exit__
parts of the context manager. I don't always want to supply a block. I don't want to have to set up a pass
ing with
block if I don't have to.
This is possible and easy in Ruby. It would be cool if it were possible in Python too, since we're already so close (we just have to set up a pass
ing with
block). I understand that the language mechanics make it a difficult (technically impossible?) but a close-enough solution is interesting to me.
Upvotes: 22
Views: 14228
Reputation: 2176
There is currently no solution without needing at least two different call signatures.
Here is a solution that uses a flag inside create()
:
class MyObject:
def __init__(self, a, b):
self.a = a
self.b = b
self.saved = True
self.in_context = False
def __enter__(self):
if not self.in_context:
raise TypeError(f"Context manager only supported with MyObject::create(..., in_context=True)")
self.saved = False
return self
def __exit__(self, exc_type, exc_value, traceback):
self.saved = True
self.in_context = False
@classmethod
def create(cls, a, b, in_context=False):
product = cls(a, b)
product.in_context = in_context
return product
def __eq__(self, other: object) -> bool:
return isinstance(other, MyObject) and self.a == other.a and self.b == other.b
>>> with MyObject.create(1, 2, in_context=True) as product:
... product.a=2
...
>>> assert product == MyObject.create(2, 2)
>>> assert product.saved
>>> # We don't want this to work outside of MyObject::create()
>>> with product:
... product.a=3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in __enter__
TypeError: Context manager only supported with MyObject::create(..., in_context=True)
>>> # Should fail since in_context is False
>>> with MyObject.create(1, 2) as product:
... product.a=2
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in __enter__
TypeError: Context manager only supported with MyObject::create(..., in_context=True)
However you could implement without in_context
at all as long as you don't mind that with MyObject()
and with MyObject.create()
behaves the same.
Upvotes: 0
Reputation: 16677
With a slight change, you can can really close to what you want, just not via implementation using contextlib.contextmanager
:
creator = build_creator_obj()
# "with" contextmanager interface
with creator as obj:
obj.attr = 'value'
# "call" interface
obj = creator(attr='value')
Where creator
is an object that implements __enter__
and __exit__
for the first usage and implements __call__
for the second usage.
You can also hide the construction of creator
inside a property
on some persistent object, e.g.:
class MyDatabase():
@property
def create(self):
return build_creator_obj()
db = MyDatabase()
# so that you can do either/both:
with db.create as obj:
obj.attr = 'value'
obj = db.create(attr='value')
Upvotes: 1
Reputation: 104712
I'd suggest using different functions to get a context manager that saves an object on __exit__
and to get an automatically saved object. There's no easy way to have one function do both things. (There are no "blocks" that you can pass around, other than functions, which you say you don't want.)
For instance, you could create a second function that just creates and immediately saves an object without running any extra code to run in between:
def create_and_save(**args):
obj = MyObject(**args)
obj.save()
return obj
So you could make it work with two functions. But a more Pythonic approach would probably be to get rid of the context manager function and make the MyObject
class serve as its own context manager. You can give it very simple __enter__
and __exit__
methods:
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, traceback):
if exception_type is None:
self.save()
Your first example would become:
with MyObject(a=10) as new_obj:
new_obj.b = 7
You could also turn the create_and_save
function I showed above into a classmethod
:
@classmethod
def create_and_save(cls, **args):
obj = cls(**args)
obj.save()
return obj
Your second example would then be:
new_obj = MyObject.create_and_save(a=10)
Both of those methods could be written in a base class and simply inherited by other classes, so don't think you'd need to rewrite them all the time.
Upvotes: 3
Reputation: 557
Ok, there seems to be some confusion so I've been forced to come up with an example solution. Here's the best I've been able to come up with so far.
class my_create(object):
def __new__(cls, **attributes):
with cls.block(**attributes) as obj:
pass
return obj
@classmethod
@contextmanager
def block(cls, **attributes):
obj = MyClass(**attributes)
yield obj
obj.save()
If we design my_create
like above, we can use it normally without a block:
new_obj = my_create(a=10)
new_obj.a # => 10
new_obj.is_saved() # => True
And we can call it slightly differently with a block.
with my_create.block(a=10) as new_obj:
new_obj.b = 7
new_obj.a # => 10
new_obj.b # => 7
new_obj.saved # => True
Calling my_create.block
is kind of similar to calling Celery tasks Task.s
, and users who don't want to call my_create
with a block just call it normally, so I'll allow it.
However, this implementation of my_create
looks wonky, so we can create a wrapper to make it more like the implementation of context_manager(my_create)
in the question.
import types
# The abstract base class for a block accepting "function"
class BlockAcceptor(object):
def __new__(cls, *args, **kwargs):
with cls.block(*args, **kwargs) as yielded_value:
pass
return yielded_value
@classmethod
@contextmanager
def block(cls, *args, **kwargs):
raise NotImplementedError
# The wrapper
def block_acceptor(f):
block_accepting_f = type(f.func_name, (BlockAcceptor,), {})
f.func_name = 'block'
block_accepting_f.block = types.MethodType(contextmanager(f), block_accepting_f)
return block_accepting_f
Then my_create
becomes:
@block_acceptor
def my_create(cls, **attributes):
obj = MyClass(**attributes)
yield obj
obj.save()
In use:
# creating with a block
with my_create.block(a=10) as new_obj:
new_obj.b = 7
new_obj.a # => 10
new_obj.b # => 7
new_obj.saved # => True
# creating without a block
new_obj = my_create(a=10)
new_obj.a # => 10
new_obj.saved # => True
Ideally the my_create
function wouldn't need to accept a cls
, and the block_acceptor
wrapper would handle that, but I haven't got time to make those changes just now.
pythonic? no. useful? possibly?
I'm still interested to see what others come up with.
Upvotes: 3
Reputation: 362617
Add a new method on MyObject
which creates and saves.
class MyObject:
@classmethod
def create(cls, **attributes):
obj = cls(**attributes)
obj.save()
return obj
This is an alternate initializer, a factory, and the design pattern has precedent in Python standard libraries and in many popular frameworks. Django models use this pattern where an alternate initializer Model.create(**args)
can offer additional features that the usual Model(**args)
would not (e.g. persisting to the database).
Is there a way to write my my_create function so that it can be called with a "block" as an optional "parameter"?
No.
Upvotes: 7