-
Notifications
You must be signed in to change notification settings - Fork 8
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
Introduce API for granular (Sensor)Reader operation #33
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really appreciate this PR and see potential on the use case it implements.
I'm concerned about the additional complexity it introduce, so I ask you to be patient with me as I try to figure out a good balance between complexity and conciseness.
src/pms/core/reader.py
Outdated
@dataclass | ||
class _Reading: | ||
"""Represents a single sensor reading | ||
|
||
This class is only used by internal functions as a way of passing reading data between them. | ||
""" | ||
|
||
buffer: bytes | ||
obs_data: ObsData | ||
|
||
@property | ||
def raw_data(self) -> RawData: | ||
return RawData(self.time, self.buffer) | ||
|
||
@property | ||
def time(self) -> int: | ||
return self.obs_data.time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this internal class really needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use classes because they give meaning to the data being passed around. For example, what is a (buffer, obs_data)
? It's not obvious without a class to clarify that it represents the data from a Reading
.
That said, I don't feel strongly about it as this is an internal method. We could have this instead:
def _read_one(...):
...
return (buffer, obs_data)
...
def __call__(...):
...
(buffer, obs_data) = self._read_one(...)
...
yield RawData(obs_data.time, buffer) if raw else obs_data
What do you think?
(the construction of RawData
would also have to be duplicated in read_one
)
tests/core/test_reader.py
Outdated
def test_sensor_reader_read_one_warm_up( | ||
mock_sensor, | ||
sensor_reader_factory, | ||
mock_sensor_warm_up, | ||
): | ||
sensor_reader = sensor_reader_factory() | ||
|
||
with sensor_reader as r: | ||
with pytest.raises(pms.SensorNotReady): | ||
sensor_reader.read_one() | ||
|
||
|
||
def test_sensor_reader_read_one_temp_failure( | ||
mock_sensor, | ||
sensor_reader_factory, | ||
mock_sensor_temp_failure, | ||
): | ||
sensor_reader = sensor_reader_factory() | ||
|
||
with sensor_reader as r: | ||
with pytest.raises(pms.SensorWarning): | ||
sensor_reader.read_one() | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the purpose of read_one
?
If it is to read one observation, these exceptions should be addressed internally and ask the sensor for an new message (silently).
If it is meant to request one observation (and let the caller handle the retry),
then this test are appropriate.
I think that read_one
should deliver one observation, independently of how many samples are requested from the sensor and discarded for different reasons.
If you want the caller to handle the retries
, I could add a max_tries
option to read_one
and __call__
. This way when the retry
exceeds max_tries
the exception will bubble up to the caller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is meant to request one observation (and let the caller handle the retry)
Yes, that's right: that's what I need it to do.
I had some questions, but then thought it would be better to share an example. I've appended some extra commits, which are what I think you're aiming for with max_(re)tries
and read_one
.
The penultimate commit isn't great. I'd argue that's why read_one
and ultimately Stream
s are better, because it avoids confusion over when samples
, interval
and (now) max_retries
are relevant. Given where we are, it would be clearer to drop that commit and for me to just do next(reader())
manually.
Anyway, how does it look?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is what I had in mind. Thanks for the implementation.
I'll merge this PR as soon as I you confirm that you're done with it.
Thanks again,
Á.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be ready for another look now...
This will give us more confidence when we refactor these exception handlers in the next commits.
This clarifies that both exceptions relate to a temporary condition where it makes sense to wait before trying again.
In response to [^1]. Previously it was hard to operate the reader classes from calling code that separates resource management ("open" / "close") and the actual polling of data ("read_one"). This exposes new APIs to make it easier to use reader classes in such a granular way. Implementation notes: - Reinstating a type hint for __enter__ of "-> Self" will become possible in Python 3.11. - "start" / "stop" were discussed in relation to the Sensor being operated by a SensorReader, but these concepts don't make much sense for the MessageReader class, so I've used "open" / "close" instead. [^1]: avaldebe#32 (comment)
This resets the code to allow experimentation with a different API implementation for [^1]. [^1]: avaldebe#33 (comment)
This resets the code to allow experimentation with a different API implementation for [^1]. [^1]: avaldebe#33 (comment)
This resets the code to allow experimentation with a different API implementation for [^1]. [^1]: avaldebe#33 (comment)
3b1acea
to
cbf4d24
Compare
In response to [^1]. [^1]: avaldebe#33 (comment)
This is viable since [^1]. [^1]: avaldebe/PyPMS#33
This is viable since [^1]. [^1]: avaldebe/PyPMS#33
This is viable since [^1]. There are some minor differences to note: - SensorReader._cmd uses Serial.flush(), but this shouldn't be an issue in newer versions of Python [^2]. - SensorReader._cmd reads all available input into the buffer and only calls Serial.reset after a "_read", or if the buffer turns out to be invalid. This is less robust than just calling Serial.reset before each command, but it seems to work. - SensorReader.open does an additional check to verify the sensor. [^1]: avaldebe/PyPMS#33 [^2]: python/cpython#97001
This is viable since [^1]. There are some minor differences to note: - SensorReader._cmd uses Serial.flush(), but this shouldn't be an issue in newer versions of Python [^2]. - SensorReader._cmd reads all available input into the buffer and only calls Serial.reset after a "_read", or if the buffer turns out to be invalid. This is less robust than just calling Serial.reset before each command, but it seems to work. - SensorReader.open does an additional check to verify the sensor. [^1]: avaldebe/PyPMS#33 [^2]: python/cpython#97001
Original PR: #32
The current reader classes aren't optimal for some usecases.
For example, it's hard to "plug in" a SensorReader in this code:
In particular:
The current classes make it hard to separate management of
resources ("start" / "stop") from getting data ("poll").
The current classes mix CLI-oriented error handling (example)
with the lower-level concern of just getting the data.
This PR reorganises some of the reader code to address (2) and
exposes more of these internals as a set of new "open", "close"
and "read_one" methods for more granular control to address (1).