-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What should happen if we start to read without starting the reader? #18
Comments
As far as convenience goes, it would be nice to be able to have the reader turn things whenever someone starts to read. But obviously, there's something really ugly about that. It's best practice to use context managers, not to manually do a But this convenience is useful as far as easy to use, concern-separated, code. For example, when using app = SlabIter(
audio_read=AudioStream(...).read,
keyboard_read=KeyboardStream(...).read
# further callables that do stuff with the data
)
The problem here is that So can we get the best of both worlds? |
A few solution ideas. Callable Stream. Have a Contextualizer (decorator): We could (could we?) make I vote for explicit (the latter). The app = SlabIter(
audio_read=contextualize(AudioStream(...).read),
keyboard_read=contextualize(KeyboardStream(...).read),
# further callables that do stuff with the data
) which is not to bad if you ask me. |
Here's some code that implements my wishes above, with tests included. # ------------------------------------------------------------------------------------------------------------
# The general solution objects
from inspect import signature, Parameter
from abc import abstractstaticmethod
from functools import wraps
class TypeValidationError(TypeError):
"""Raised if an object is not valid"""
@abstractstaticmethod
def is_valid(obj) -> bool:
"""Returns True if and only if obj is considered valid"""
@classmethod
def validate(cls, obj, *args, **kwargs):
if not cls.is_valid(obj):
raise cls(*args, **kwargs)
def null_enter_func(obj):
return obj
def null_exit_func(obj, *exception_info):
return None
def _has_min_num_of_arguments(func, mininum_n_args):
return len(signature(func).parameters) >= mininum_n_args
def _callable_has_a_variadic_pos(func):
"""True if and only if the function has a variadic positional argument (i.e. *args)"""
return any(x.kind == Parameter.VAR_POSITIONAL for x in signature(func).parameters.values())
class EnterFunctionValidation(TypeValidationError):
"""Raised if an function isn't a valid (context manager) __enter__ function"""
@staticmethod
def is_valid(obj):
return callable(obj) and _has_min_num_of_arguments(obj, 1)
class ExitFunctionValidation(TypeValidationError):
"""Raised if an function isn't a valid (context manager) __exit__ function"""
@staticmethod
def is_valid(obj):
return callable(obj) and (_callable_has_a_variadic_pos or _has_min_num_of_arguments(obj, 4))
# TODO: If needed:
# A more general Contextualizer would proxy/delegate all methods (including special ones) to the wrapped
class ContextualizeCall:
def __init__(self, func, enter_func=null_enter_func, exit_func=null_exit_func):
# validation
if not callable(func):
raise TypeError(f"First argument should be a callable, was: {func}")
EnterFunctionValidation.validate(
enter_func,
f"Not a valid enter function (should be a callable with at least one arg): {enter_func}"
)
ExitFunctionValidation.validate(
exit_func,
f"Not a valid exit function (should be a callable with at least one arg or varadic args): {exit_func}",
)
# assignment
self.func = func
self.enter_func = enter_func
self.exit_func = exit_func
# wrapping self with attributes of func (signature, name, etc.)
wraps(func)(self)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __enter__(self):
# print(f'{type(self).__name__}.__enter__')
return self.enter_func(self.func)
def __exit__(self, *exception_info):
# print(f'{type(self).__name__}.__exit__')
return self.exit_func(self.func, *exception_info)
# ------------------------------------------------------------------------------------------------------------
# Using these general objects for the particular case of having bound methods forward their context
# management to the instances they're bound to
from functools import partial
def forward_to_instance_enter(obj):
return obj.__self__.__enter__()
def forward_to_instance_exit(obj, *exception_info):
return obj.__self__.__exit__(obj, *exception_info)
contextualize_with_instance = partial(
ContextualizeCall,
enter_func=forward_to_instance_enter,
exit_func=forward_to_instance_exit
)
contextualize_with_instance.__doc__ = (
"To be applied to a bound method. "
"Returns a callable that forwards context enters/exits to the bound instance"
)
# ------------------------------------------------------------------------------------------------------------
# Testing this
# Test objects
class StreamHasNotBeenStarted(RuntimeError):
"""Raised when an action requires the stream to be 'on'"""
class Streamer:
def __init__(self, iterable):
self.iterable = iterable
self.is_running = False
self._read = None
def enter(self):
# print(f'{type(self).__name__}.enter')
self._read = iter(self.iterable).__next__
self.is_running = True
def exit(self, *exc):
# print(f'{type(self).__name__}.exit')
self._read = None
self.is_running = False
__enter__, __exit__ = enter, exit
def read(self):
if not self.is_running:
raise StreamHasNotBeenStarted(
"The stream needs to be on/started (in a context) for that!"
)
return self._read()
# Testing the test objects
s = Streamer('stream')
# demo
with s:
assert s.read() == 's'
assert s.read() == 't'
# a reader test function
def test_reader(reader):
assert ''.join(reader() for _ in range(6)) == 'stream'
# Normal case: With a context
reader = s.read
with s:
test_reader(reader)
# Normal case: Manual entering/exiting
s.enter()
reader = s.read
test_reader(reader)
s.exit()
# But if we don't turn things on...
reader = s.read
try:
# oops, forgot the enter s context
test_reader(reader)
it_worked = True
except StreamHasNotBeenStarted as e:
it_worked = False
assert isinstance(e, StreamHasNotBeenStarted)
assert e.args[0] == 'The stream needs to be on/started (in a context) for that!'
assert not it_worked
# But we can't turn the read method on -- it's not a context (it's instance is!)
reader = s.read
try:
with reader: # can we actually do this (answer: no! We can enter s, not s.read)
test_reader(reader)
it_worked = True
except Exception as e:
it_worked = False
assert isinstance(e, AttributeError)
assert e.args[0] == '__enter__' # well yeah, reader doesn't have an __enter__!
assert not it_worked
# But with contextualize_with_instance...
reader = contextualize_with_instance(s.read)
try:
with reader: # now we can enter the reader!
test_reader(reader)
it_worked = True
except Exception as e:
it_worked = False
assert it_worked # Hurray! |
@thorwhalen I'm a little surprised you'd vote for explicit instead of always doing it in the background since you're an advocate for reduced overhead. |
@andeaseme: I'm an advocate for explicit over implicit by default, and as long as it's not in the way of usage too much. (The illusion of the opposite opinion might come from the fact that have low tolerance for difficult/unclear usage.) |
I now implemented and pushed my solution in stream2py. I'll leave the issue open until you and @SRHerzog vet the solution, and then we can close the issue. See the code here: https://github.com/i2mint/stream2py/blob/d5ba8fdd1718ca084831a6b97a9698c111caa10a/stream2py/util.py And the tests here: |
Again, this is where the "problem" arose first, but I depend on nothing in stream2py, nor does stream2py depend on it. On Monday, let's discuss also removing what ever we did to have those automatic context entering. |
And @andeaseme , about
I might be for doing it in the background here if it didn't have the downsides mentioned above. If you have an idea that in at the intersection of the best of both worlds, let us know! |
If you haven't read the solution yet, know that the core of the solution is simply: from functools import wraps
class ContextualizeCall:
def __init__(self, func, enter_func, exit_func):
self.func, self.enter_func, self.exit_func = func, enter_func, exit_func
# wrapping self with attributes of func (signature, name, etc.)
wraps(func)(self)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __enter__(self):
return self.enter_func(self.func)
def __exit__(self, *exception_info):
return self.exit_func(self.func, *exception_info) It was the original code -- I just expanded on that the code to make it more production-ready (validation, etc.) |
When revision is made we'll have:
Creating the reader requires the buffer to start; with reader: just increments a counter in the source to say that there is one active reader
What should happen if the user fails to enter the reader context before trying to read? should it raise an exception?
The text was updated successfully, but these errors were encountered: