-
Notifications
You must be signed in to change notification settings - Fork 8
Refactor Reader classes to use new Stream API #32
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
Conversation
Reading DRYs-up knowledge of how to construct "RawData". Since Reader classes don't know how to construct "ObsData", it makes sense that the knowledge of how to construct "RawData" is also abstracted away. The new type will also be heavily used in the following commits, as we introduce a new interface to read from sensors. In future, the new type could replace "RawData" and "ObsData" as a standard interface.
This is part of adapting the reader module to support its use as a library. Previously it was only possible to read *all* the data in one go, which meant that error handling was hard to customise. SensorReader.read_one uses StopIteration to indicate there are no more readings. The choice of exception is to be consistent with the MessageReader, but a custom one could potentially be used.
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.
This makes it easier for a library to implement its own polling scheme. Streams are lower level than Readers, hence the granular Stream.open and Stream.close API instead of a context manager. Implementation notes: - Reinstating a type hint for __enter__ of "-> Self" will become possible in Python 3.11. - SensorReader.sensor property is necessary as it's used outside the class by the CLI [^1]. [^1]: https://github.com/benthorner/PyPMS/blob/f8d6d59a514aca757c0f8dd7d0f5b4b9b84fa2d5/src/pms/cli.py#L60
This takes advantage of the new Stream classes to move towards a single Reader class: SensorReader and MessageReader are now largely the same except for the stream they use. Using a common definition of Reader has the advantage of keeping their behaviour consistent. Implementation notes: - The sample counting code looks different between the two readers but actually behaves the same way except that MessageReader would only count once - at the class level. - The "interval" default of "None" means MessageReader will continue to behave as it did before, although in future it could of course make use of this feature if needed. - The two new "Stream" exceptions represent a layer of abstraction that makes it possible for Reader.__call__ to make generic decisions about Streams, rather than just sensors.
avaldebe
left a comment
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 do not see where is this PR going.
I'm quite happy with the context manager interface as it is. I would not mind extending it, but I would like to know why the extension is needed.
What does the stream interface achieve that was not possible before?
Maybe this is a stepping stone towards a new use case, but I can not tel from this PR alone.
| class StreamNotReady(Exception): | ||
| pass | ||
|
|
||
|
|
||
| class TemporaryFailure(Exception): | ||
| pass |
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.
maybe these and the "old" exceptions should be collected on a separate module
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.
Sure, can do. I'll come back to this when we decide what should happen with the PR overall.
|
Hey @avaldebe. Sure, I can give you the background here. The usecase I have is to be able to plug this library into a system I'm using, as a "data source". Data sources look a bit like the following: At the moment there's no good way to plug the library in. "start" and "stop" are called separately so don't match up with the context manager API. And "poll" expects a single sample, not an array. Here are two approaches I've tried to integrate the library in the system: My first thought was: I will need to write my own version of SensorReader. Not a big deal, but also not ideal. However, as I looked at the reader code I saw some an opportunity, in the form of Stream*. * "Stream" is an arbitrary name, by the way. I also thought of e.g. "Source". My aim in this PR is to try and pull out / separate the code to get data (my usecase, the "Stream") from the code that governs how much data to get and how often (CLI usecase, the "Reader"). At the same time, I also wanted to preserve the existing Reader API, hence adding all the tests to avoid any breaking changes. Does that help? |
|
I understand the usage now. Thanks for the clarification. What do you think about expanding the class Reader(AbstractContextManager):
@abstractmethod
def __call__(self, *, raw: Optional[bool] = None) -> Iterator[Union[RawData, ObsData]]:
...
def start(self) -> None:
self.__enter__()
def poll(self, *, raw: Optional[bool] = None) -> Union[RawData, ObsData]:
return next(self(raw))
def stop(self) -> None:
self.__exit__(
exception_type=None,
exception_value=None,
traceback=None,
) |
|
That works for me. A few comments / questions:
Sure, if that's a problem then something like the above approach is the way to go. I'm curious though: what are the issues you anticipate with more classes? I get that loads of classes can be hard to follow, but we're only talking about 2 / 3 new classes here. I get that adding all this stuff is overkill for the features I'm interested in. My thinking with the PR was: rather than just make the Reader classes more complicated for my own benefit, first try to make them simpler. Anyway, hopefully that's helpful context. Let me know what you think. If you prefer to keep everything inside the Reader classes, then I can make another PR to do that instead - depending on those comments / questions ☝️. |
Yes to all. For consistency with 2, please call
The sensors implementation uses inheritance to avoid repetition between vendor variants, and when something breaks down it takes me some time to find out where the behaviours are defined. Therefore, I would like to keep things simple and inheritance shallow. Keep in mind that, for my uses, this project is quite close to feature completion. Therefore, I only engage with it when something breaks down, and I do not want to spend time looking for the right class and...
You can keep this PR (and edit the PR's name and description), or start a new one. It is up to you. |
In response to [^1].
Previously it was hard to operate the reader classes from calling
code that separates resource management ("start" / "stop") and the
actual polling of data ("read_one"). This exposes new APIs to make
it easier for to reader classes in such a granular way.
[^1]: avaldebe#32 (comment)
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 for to reader classes in such a granular way.
Note: I've used "open" and "close" instead of the "start" / "stop"
discussed, as those terms are a better match of the behaviour of
the reader e.g. saying "open a message reader to read" makes sense,
whereas "start a message reader ???" doesn't make sense.
[^1]: avaldebe#32 (comment)
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 for to reader classes in such a granular way.
Note: I've used "open" and "close" instead of the "start" / "stop"
discussed, as those terms are a better match of the behaviour of
the reader e.g. saying "open a message reader to read" makes sense,
whereas "start a message reader ???" doesn't make sense.
[^1]: avaldebe#32 (comment)
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 for to reader classes in such a granular way.
Note: I've used "open" and "close" instead of the "start" / "stop"
discussed, as those terms are a better match of the behaviour of
the reader e.g. saying "open a message reader to read" makes sense,
whereas "start a message reader ???" doesn't make sense.
[^1]: avaldebe#32 (comment)
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.
Note: I've used "open" and "close" instead of the "start" / "stop"
discussed, as those terms are a better match of the behaviour of
the reader e.g. saying "open a message reader to read" makes sense,
whereas "start a message reader ???" doesn't make sense.
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)
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)
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)
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)
I had a similar experience when I tried to understand how the decoding works. @avaldebe I've made a new PR that follows the approach we discussed: #33. However, implementing I went back to some of the ideas in this PR to make progress, but also tried to stay true to the principles and signatures we agreed on here. Please take a look and let me know what you think. |
|
Thanks for this and the new PR. I'll close this PR, and continue the discussion #33. Thanks 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)
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]: #32 (comment)
This introduces a couple of new abstractions - Readings and
Streams - to expose more
reader.pycode for use by libraries.The new abstractions separate how to get readings from the
CLI concerns of how many to get and how often.
Example of Stream / Reading API:
The last three commits are more optional: introducing Streams
isn't necessary for library usage but I think it's clearer / simpler
- the final commit showcases this by DRYing-up the sampling
behaviour across both of the Reader classes.