Skip to content
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

[bug fix] add do_async to cover inlining multiple async calls in a do… #152

Merged
merged 1 commit into from
Dec 20, 2023

Conversation

JLessinger
Copy link
Contributor

Due to subtle behavior in the way Python creates generators, do() breaks if you inline multiple await calls. If you either await before do() or only inline one await, do() works as expected. This PR makes two changes:

  1. Add do_async() which can handle the broken case (multiple inlined awaits). Also can handle any synchronous generator including one inlined await.
  2. Raise a more helpful message in the broken do() case pointing to do_async().

Docs are updated.

@codecov-commenter
Copy link

codecov-commenter commented Nov 24, 2023

Codecov Report

Attention: 2 lines in your changes are missing coverage. Please review.

Comparison is base (730ef99) 97.41% compared to head (ebae79b) 97.48%.
Report is 1 commits behind head on master.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #152      +/-   ##
==========================================
+ Coverage   97.41%   97.48%   +0.07%     
==========================================
  Files           4        4              
  Lines         464      556      +92     
==========================================
+ Hits          452      542      +90     
- Misses         12       14       +2     
Files Coverage Δ
src/result/__init__.py 100.00% <ø> (ø)
src/result/result.py 95.00% <100.00%> (+0.32%) ⬆️
tests/test_result_do.py 98.17% <97.78%> (-1.83%) ⬇️

@francium
Copy link
Member

francium commented Dec 5, 2023

I forgot you opened this before I released 0.15.0. Anyway, I'll release it in 0.15.1.

@francium
Copy link
Member

francium commented Dec 5, 2023

There's also this syntax, which I wasn't familar with since I haven't worked with async-python at all really.

async for x in foo()

Could/should we try to support this?

@JLessinger
Copy link
Contributor Author

There's also this syntax, which I wasn't familar with since I haven't worked with async-python at all really.

async for x in foo()

Could/should we try to support this?

As far as I understand, async for is for a different use case: async iterators ( __aiter__). In this case, we don't intend to have async iterators because our __iter__ doesn't do anything I/O-bound. It just looks up the value or raises immediately. In fact, we don't even implement __aiter__.

I think the cause of this bug is that Python seems to turn our synchronous iterators into async iterators if multiple await calls are inlined into the do expression. I don't fully understand why or how this works. But if you await your async calls before the do() then the iterator remains synchronous, and __iter__ is called as expected.

@JLessinger JLessinger force-pushed the do-async-fix branch 2 times, most recently from 265f996 to 4400902 Compare December 8, 2023 06:05
Copy link
Member

@francium francium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good otherwise. Just left a few questions we should go over prior to merging this.

README.rst Outdated Show resolved Hide resolved
src/result/result.py Outdated Show resolved Hide resolved
Ok(1)
for x in await foo()
for y in await bar()
)
Copy link
Member

@francium francium Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about code like this, I don't think it renders properly.

It's fine for now

There's an issue open to setup docs and we can look into it when that's being done. Perhaps we'd need to follow Sphinx's formatting rules for docstrings to render properly.

return next(gen)
except DoException as e:
out: Err[E] = e.err # type: ignore
return out
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we just replace do with this implementation? This seems to cover both cases, I don't see an obvious need to have both. Just having the one that works in all cases seems like the better choice.

Aside from this extra isinstance check at runtime,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah interesting idea. You really need the two "front-ends" because one is async and has to be awaited. It would be weird to have to await every do call even if you're not using any actual asyncio.

Question is how much implementation they can share, and I think the answer is not much. If you look at them side by side, they're pretty different. I could abstract out the try... except DoException but I think that's really overdoing abstraction. The duplicated code amounts to maybe 4 lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I didn't consider that Python necessitates awaits for async stuff and it would look weird for non-async.

@JLessinger JLessinger force-pushed the do-async-fix branch 3 times, most recently from 3883fd4 to 9a19c5e Compare December 18, 2023 15:51
@JLessinger
Copy link
Contributor Author

@francium , updated, PTAL

Copy link
Member

@francium francium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Could you have a look at my two comments prior to merging please.

I don't time currently to sit down and play with this more, I'd like to do that prior to releasing just to familiarize myself with some of Python's async stuff more. But we don't need to hold up this PR, I'll do some testing after it's merged.

Ok(len(x) + int(y) + z)
for x in await aget_resx(True)
for y in await aget_resy(True)
for z in get_resz(True)
Copy link
Member

@francium francium Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random question, does it at all depend on the order in which we have async and non-async lines? For example,

for x in foo(...)
for y in await bar(...)

vs

for y in await bar(...)
for x in foo(...)

I don't think it would, but perhaps good to verify in case it's some edge case there as well.

for x in await aget_resx(is_suc1)
for y in await aget_resy(is_suc2)
for z in get_resz(is_suc3)
for w in await process_xyz(x, y, z)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you, either in this PR, or in a separate PR, document this behavior as well, the fact you can use previous values in subsequent lines,

for a in A(...)
for b in B(a)
for c in C(b)

If someone has used do-notation in another language they may be familiar, but it may be unintuitive for new comers.

@JLessinger JLessinger force-pushed the do-async-fix branch 2 times, most recently from f59a1a1 to 5c76e89 Compare December 20, 2023 00:09
@JLessinger
Copy link
Contributor Author

@francium done, but I don't have write access.

@francium
Copy link
Member

francium commented Dec 20, 2023

Sorry, I thought PR creators would be allowed to hit merge after PR was approved. Merging. Thank for the changes.

Also, for future, best to avoid force pushes after PR has started getting reviews, otherwise you can't as easily view changes since last review :) (technically you can but, just easier overall if we avoid force pushes once reviews start)

@francium francium merged commit 8f09f1e into rustedpy:master Dec 20, 2023
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants