GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-127647: Add typing.Reader and Writer protocols by srittau · Pull Request #127648 · python/cpython Conversation
|
|
A few design considerations:
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit sad that we can't use covariance and contravariance in the protocols since a subclass could use it with an invariant type.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Small improvements to the docstrings and signature
There was an error while loading. Please reload this page.
|
|
||
| Protocol for reading from a file or other input stream. | ||
|
|
||
| .. method:: read(size=...) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
| .. method:: read(size=...) | |
| .. method:: read(size=..., /) |
(Same for other methods)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I was unsure of how much of the signature to document. For example, would it also make sense to add argument and/or return types? I've added the slashes for now.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
That feels terribly hackish to me, even if there is precedence. And in my experience, hackish stuff often falls on one's feet one way or the other. But I can move it to |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
This has been the way the cpython/Lib/collections/__init__.py Lines 32 to 33 in 023b7d2
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Well, considering that |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'm open to something less hacky if you can find a generalized solution that doesn't make |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
I don't think the typing spec needs to be cognisant of the hacks we use to make things work at runtime in the stdlib! This knowledge is irrelevant for third-party users of protocols, to users of type checkers and to implementers of type checkers. It's definitely not encouraged for any external users to make use of the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
CI seems flaky, failures are unrelated. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Co-authored-by: Adam Turner <[email protected]>
Co-authored-by: Adam Turner <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This LGTM now but it might be good to have one or two simple tests covering the new code. It probably doesn't need to be much more than this (added to test_io.py)
cpython/Lib/test/test_typing.py
Lines 4260 to 4262 in a083633
| def test_supports_index(self): | |
| self.assertIsSubclass(int, typing.SupportsIndex) | |
| self.assertNotIsSubclass(str, typing.SupportsIndex) |
and maybe an addition to the test_typing tests similar to this one:
cpython/Lib/test/test_typing.py
Lines 4274 to 4303 in a083633
| def test_collections_protocols_allowed(self): | |
| @runtime_checkable | |
| class Custom(collections.abc.Iterable, Protocol): | |
| def close(self): ... | |
| class A: pass | |
| class B: | |
| def __iter__(self): | |
| return [] | |
| def close(self): | |
| return 0 | |
| self.assertIsSubclass(B, Custom) | |
| self.assertNotIsSubclass(A, Custom) | |
| @runtime_checkable | |
| class ReleasableBuffer(collections.abc.Buffer, Protocol): | |
| def __release_buffer__(self, mv: memoryview) -> None: ... | |
| class C: pass | |
| class D: | |
| def __buffer__(self, flags: int) -> memoryview: | |
| return memoryview(b'') | |
| def __release_buffer__(self, mv: memoryview) -> None: | |
| pass | |
| self.assertIsSubclass(D, ReleasableBuffer) | |
| self.assertIsInstance(D(), ReleasableBuffer) | |
| self.assertNotIsSubclass(C, ReleasableBuffer) | |
| self.assertNotIsInstance(C(), ReleasableBuffer) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This is great, thank you! Just a few docs nits remaining
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Co-authored-by: Alex Waygood <[email protected]>
There was an error while loading. Please reload this page.
|
I'll leave it a little bit in case any of the other reviewers here have any remaining concerns before merging |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| __slots__ = () | ||
|
|
||
| @abc.abstractmethod | ||
| def read(self, size=..., /): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Sorry if this has been discussed before, but I'm unsure on the runtime use of size=... (I didn't notice this earlier in my documentation review, sorry).
Almost every other read(size) method I can find has a default of either None or -1. I also can't find another method in the stdlib with a default of ... (outside of the recently-added protocols in wsgiref.types).
Would it be better to have size=-1, to indicate that the method takes an int? I'm not sure how much we want typeshed-like practices to leak into the standard library.
| def read(self, size=..., /): | |
| def read(self, size=-1, /): |
A
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Mandating defaults is not really something you can do in a protocol. I also wouldn't want to mandate that implementors have to use a default of -1, because – as you said – some implementations use None.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Right, but this isn't a protocol -- it's an ABC, which do have defaults -- see e.g. collections.abc.Generator. All of the read() methods in io are documented as having read(size=-1, /), and given these ABCs are going into io, I think we should be consistent with that interface, or have good reason to diverge from it (& document why).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
It's supposed to be a protocol, not an ABC. (Notwithstanding the fact that all protocols are ABCs.) It's just a protocol in the implementation for performance reasons. And using -1 as a default would give users the very wrong impression that they can use read(-1) when that may or may not actually be supported.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
using
-1as a default would give users the very wrong impression that they can useread(-1)when that may or may not actually be supported.
From the documentation of io.RawIOBase.read():
Read up to size bytes from the object and return them. As a convenience, if size is unspecified or -1, all bytes until EOF are returned.
I would expect the io.Reader.read() ABC/protocol to have this same guarantee, for a 'properly' implemented read() method (according to the io expecations). In the proposed documentation, we say:
Read data from the input stream and return it. If size is specified, it should be an integer, and at most size items (bytes/characters) will be read.
This forbids None (good!), but is silent on what happens should size be omitted. I still think we should use -1 instead of ..., but at the very least we should include in the documentation the contract for what happens when read() is called with no arguments.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I disagree. -1 is the default for some implementations, but the protocol should not make any default mandatory.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I'm still not sure I follow, though -- currently the ABC/protocol has the mandatory default of size=..., which is always invalid and not the correct type. -1 is valid for all interfaces specified by the io documentation, which is where this new type is being added.
I ran the following with mypy --strict and it passed, so I don't think type checkers care about default values (and as discussed, the type forbids using non-integer types):
import abc class Reader(metaclass=abc.ABCMeta): __slots__ = () @abc.abstractmethod def read(self, size: int = -1, /) -> bytes: pass class CustomReader(Reader): def read(self, size: int = -1, /) -> bytes: return b'' class CustomReaderZero(Reader): def read(self, size: int = 0, /) -> bytes: return b'' assert issubclass(CustomReader, Reader) assert issubclass(CustomReaderZero, Reader) assert isinstance(CustomReader(), Reader) assert isinstance(CustomReaderZero(), Reader) def reader_func(r: Reader) -> None: r.read() reader_func(CustomReader()) reader_func(CustomReaderZero())
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I agree with Sebastian here; we should use ... because the protocol need not mandate any particular default.
-1 is valid for all interfaces specified by the io documentation
The protocol should also match other file-like classes defined elsewhere in the stdlib or even in third-party libraries. When defining a protocol it's often useful to be permissive, so that all objects that are intended to match the protocol actually match it.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Could this be merged before the next alpha next week? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I only saw the update now, great that it's merged! I have a few comments. I hope they're not too bothersome - I just think these protocols are important so it's worth the effort. Can the docs include type annotations? I know that Sphinx supports it. Given that this is meant for static typing, it makes sense to me to include. I think it is important to document the expected semantics of these methods, otherwise you don't really know what you're going to get when you accept a
I don't like the parameter name The docs say "If size is specified, it should be an integer, and at most size items (bytes/characters) will be read" and "Write data to the output stream and return the number of items (bytes/characters) written". I would drop the "(bytes/characters)" since The docs say "The following protocols can be used for annotating function and method arguments for simple stream reading or writing operations". I have two nitpicks:
I think |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
A merged PR is probably not the best place to discuss this, but a few points:
Personally, I'd be fine with, and actually prefer if the docs used type annotations more, but I didn't want to divert from the existing style in the documentation.
While it would make sense to me to document some of those semantics (e.g. size must be
It might have been clearer to say "(e.g. bytes or characters)", but I think calling out those specific types here makes the documentation clearer, as in 99% of cases it will be either of those. Otherwise the concept of "item" is a bit nebulous.
I don't think it's necessary to make the protocol more complex. Whoever is using the protocol will have to choose an type that they can work with. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Also in general, protocols are not a replacement for API documentation. It doesn't make too much sense to include expectations around a protocol that can't be checked statically. To document these expectations is the responsibility of the author of a function that uses these protocols, and to follow them is the responsibility of the user. These expectations can vary significantly from function to function. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
(Please feel free to ignore this if you don't want to discuss this further -- I couldn't resist posting another comment...) If I understand correctly, you prefer a narrow approach to these protocols because 1) they can't be checked statically 2) semantics can vary between uses. My replies:
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@srittau I've been working on refactoring the I/O tests and I don't think
You should be able to run just the
With that I currently get: ./python -m test test_io.test_general -v -m 'ProtocolsTest' == CPython 3.15.0a0 (heads/exp/test_bufferedio_split_v0-dirty:f4f150e5bbd, Sep 1 2025, 17:15:19) [Clang 20.1.8 ] == Linux-6.16.4-arch1-1-x86_64-with-glibc2.42 little-endian == Python build: debug == cwd: <build_dir>/build/build/test_python_worker_790373æ == CPU count: 32 == encodings: locale=UTF-8 FS=utf-8 == resources: all test resources are disabled, use -u option to unskip tests Using random seed: 1859464968 0:00:00 load avg: 0.74 Run 1 test sequentially in a single process 0:00:00 load avg: 0.74 [1/1] test_io.test_general test_reader_subclass (test.test_io.test_general.ProtocolsTest.test_reader_subclass) ... ERROR test_writer_subclass (test.test_io.test_general.ProtocolsTest.test_writer_subclass) ... ERROR ====================================================================== ERROR: test_reader_subclass (test.test_io.test_general.ProtocolsTest.test_reader_subclass) ---------------------------------------------------------------------- Traceback (most recent call last): File "/<source_dir>/cpython/Lib/test/test_io/test_general.py", line 5032, in test_reader_subclass self.assertIsSubclass(MyReader, io.Reader[bytes]) ^^^^^^^^ NameError: name 'MyReader' is not defined. Did you mean: 'self.MyReader'? ====================================================================== ERROR: test_writer_subclass (test.test_io.test_general.ProtocolsTest.test_writer_subclass) ---------------------------------------------------------------------- Traceback (most recent call last): File "/<source_dir>/cpython/Lib/test/test_io/test_general.py", line 5036, in test_writer_subclass self.assertIsSubclass(MyWriter, io.Writer[bytes]) ^^^^^^^^ NameError: name 'MyWriter' is not defined. Did you mean: 'self.MyWriter'? ---------------------------------------------------------------------- Ran 2 tests in 0.003s FAILED (errors=2) test test_io.test_general failed 0:00:00 load avg: 0.74 [1/1/1] test_io.test_general failed (2 errors) == Tests result: FAILURE == 1 test failed: test_io.test_general Total duration: 55 ms Total tests: run=2 (filtered) Total test files: run=1/1 (filtered) failed=1 Result: FAILURE |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@cmaloney Thanks, #138369 should fix that! Do you think it would make sense to add a warning to the top of the file (similar to the warning about testing both the C and Python versions)? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@srittau PR looks good. I'm hoping to remove the custom #138366 gets |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.