Skip to content

Commit 3c80909

Browse files
authored
Version 0.27.0 (#2375)
1 parent 9f20e85 commit 3c80909

File tree

34 files changed

+972
-854
lines changed

34 files changed

+972
-854
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
strategy:
2121
fail-fast: false
2222
matrix:
23-
python-version: ['3.10', '3.11', '3.12', '3.13']
23+
python-version: ['3.11', '3.12', '3.13', '3.14']
2424

2525
steps:
2626
- uses: actions/checkout@v6

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ incremental in minor, bugfixes only are patches.
66
See [0Ver](https://0ver.org/).
77

88

9+
## 0.27.0
10+
11+
### Features
12+
13+
14+
- Drop `python3.10` support
15+
- Add `python3.14` support
16+
- Add `mypy>=1.19,<1.21` support
17+
18+
919
## 0.26.0
1020

1121
### Features

README.md

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ user: Optional[User]
9090
discount_program: Optional['DiscountProgram'] = None
9191

9292
if user is not None:
93-
balance = user.get_balance()
94-
if balance is not None:
95-
credit = balance.credit_amount()
96-
if credit is not None and credit > 0:
97-
discount_program = choose_discount(credit)
93+
balance = user.get_balance()
94+
if balance is not None:
95+
credit = balance.credit_amount()
96+
if credit is not None and credit > 0:
97+
discount_program = choose_discount(credit)
9898
```
9999

100100
Or you can use
@@ -106,9 +106,10 @@ representing existing state and empty (instead of `None`) state respectively.
106106
from typing import Optional
107107
from returns.maybe import Maybe, maybe
108108

109+
109110
@maybe # decorator to convert existing Optional[int] to Maybe[int]
110-
def bad_function() -> Optional[int]:
111-
...
111+
def bad_function() -> Optional[int]: ...
112+
112113

113114
maybe_number: Maybe[float] = bad_function().bind_optional(
114115
lambda number: number / 2,
@@ -129,14 +130,20 @@ And here's how your initial refactored code will look:
129130
user: Optional[User]
130131

131132
# Type hint here is optional, it only helps the reader here:
132-
discount_program: Maybe['DiscountProgram'] = Maybe.from_optional(
133-
user,
134-
).bind_optional( # This won't be called if `user is None`
135-
lambda real_user: real_user.get_balance(),
136-
).bind_optional( # This won't be called if `real_user.get_balance()` is None
137-
lambda balance: balance.credit_amount(),
138-
).bind_optional( # And so on!
139-
lambda credit: choose_discount(credit) if credit > 0 else None,
133+
discount_program: Maybe['DiscountProgram'] = (
134+
Maybe
135+
.from_optional(
136+
user,
137+
)
138+
.bind_optional( # This won't be called if `user is None`
139+
lambda real_user: real_user.get_balance(),
140+
)
141+
.bind_optional( # This won't be called if `real_user.get_balance()` is None
142+
lambda balance: balance.credit_amount(),
143+
)
144+
.bind_optional( # And so on!
145+
lambda credit: choose_discount(credit) if credit > 0 else None,
146+
)
140147
)
141148
```
142149

@@ -157,17 +164,21 @@ Imagine that you have a `django` based game, where you award users with points f
157164
from django.http import HttpRequest, HttpResponse
158165
from words_app.logic import calculate_points
159166

167+
160168
def view(request: HttpRequest) -> HttpResponse:
161169
user_word: str = request.POST['word'] # just an example
162170
points = calculate_points(user_word)
163171
... # later you show the result to user somehow
164172

173+
165174
# Somewhere in your `words_app/logic.py`:
166175

176+
167177
def calculate_points(word: str) -> int:
168178
guessed_letters_count = len([letter for letter in word if letter != '.'])
169179
return _award_points_for_letters(guessed_letters_count)
170180

181+
171182
def _award_points_for_letters(guessed: int) -> int:
172183
return 0 if guessed < 5 else guessed # minimum 6 points possible!
173184
```
@@ -202,23 +213,28 @@ from django.conf import settings
202213
from django.http import HttpRequest, HttpResponse
203214
from words_app.logic import calculate_points
204215

216+
205217
def view(request: HttpRequest) -> HttpResponse:
206218
user_word: str = request.POST['word'] # just an example
207219
points = calculate_points(user_word)(settings) # passing the dependencies
208220
... # later you show the result to user somehow
209221

222+
210223
# Somewhere in your `words_app/logic.py`:
211224

212225
from typing import Protocol
213226
from returns.context import RequiresContext
214227

228+
215229
class _Deps(Protocol): # we rely on abstractions, not direct values or types
216230
WORD_THRESHOLD: int
217231

232+
218233
def calculate_points(word: str) -> RequiresContext[int, _Deps]:
219234
guessed_letters_count = len([letter for letter in word if letter != '.'])
220235
return _award_points_for_letters(guessed_letters_count)
221236

237+
222238
def _award_points_for_letters(guessed: int) -> RequiresContext[int, _Deps]:
223239
return RequiresContext(
224240
lambda deps: 0 if guessed < deps.WORD_THRESHOLD else guessed,
@@ -245,6 +261,7 @@ Consider this code that you can find in **any** `python` project.
245261
```python
246262
import requests
247263

264+
248265
def fetch_user_profile(user_id: int) -> 'UserProfile':
249266
"""Fetches UserProfile dict from foreign API."""
250267
response = requests.get('/api/users/{0}'.format(user_id))
@@ -267,6 +284,7 @@ but with the all hidden problems explained.
267284
```python
268285
import requests
269286

287+
270288
def fetch_user_profile(user_id: int) -> 'UserProfile':
271289
"""Fetches UserProfile dict from foreign API."""
272290
response = requests.get('/api/users/{0}'.format(user_id))
@@ -304,6 +322,7 @@ from returns.result import Result, safe
304322
from returns.pipeline import flow
305323
from returns.pointfree import bind
306324

325+
307326
def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
308327
"""Fetches `UserProfile` TypedDict from foreign API."""
309328
return flow(
@@ -312,13 +331,15 @@ def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
312331
bind(_parse_json),
313332
)
314333

334+
315335
@safe
316336
def _make_request(user_id: int) -> requests.Response:
317337
# TODO: we are not yet done with this example, read more about `IO`:
318338
response = requests.get('/api/users/{0}'.format(user_id))
319339
response.raise_for_status()
320340
return response
321341

342+
322343
@safe
323344
def _parse_json(response: requests.Response) -> 'UserProfile':
324345
return response.json()
@@ -377,11 +398,14 @@ import datetime as dt
377398

378399
from returns.io import IO
379400

401+
380402
def get_random_number() -> IO[int]: # or use `@impure` decorator
381403
return IO(random.randint(1, 10)) # isn't pure, because random
382404

405+
383406
now: Callable[[], IO[dt.datetime]] = impure(dt.datetime.now)
384407

408+
385409
@impure
386410
def return_and_show_next_number(previous: int) -> int:
387411
next_number = previous + 1
@@ -427,6 +451,7 @@ from returns.result import safe
427451
from returns.pipeline import flow
428452
from returns.pointfree import bind_result
429453

454+
430455
def fetch_user_profile(user_id: int) -> IOResult['UserProfile', Exception]:
431456
"""Fetches `UserProfile` TypedDict from foreign API."""
432457
return flow(
@@ -438,12 +463,14 @@ def fetch_user_profile(user_id: int) -> IOResult['UserProfile', Exception]:
438463
bind_result(_parse_json),
439464
)
440465

466+
441467
@impure_safe
442468
def _make_request(user_id: int) -> requests.Response:
443469
response = requests.get('/api/users/{0}'.format(user_id))
444470
response.raise_for_status()
445471
return response
446472

473+
447474
@safe
448475
def _parse_json(response: requests.Response) -> 'UserProfile':
449476
return response.json()
@@ -482,6 +509,7 @@ the `first` one returns a number and the `second` one increments it:
482509
async def first() -> int:
483510
return 1
484511

512+
485513
def second(): # How can we call `first()` from here?
486514
return first() + 1 # Boom! Don't do this. We illustrate a problem here.
487515
```
@@ -498,6 +526,7 @@ However, with `Future` we can "pretend" to call async code from sync code:
498526
```python
499527
from returns.future import Future
500528

529+
501530
def second() -> Future[int]:
502531
return Future(first()).map(lambda num: num + 1)
503532
```
@@ -543,10 +572,12 @@ import anyio
543572
from returns.future import future_safe
544573
from returns.io import IOFailure
545574

575+
546576
@future_safe
547577
async def raising():
548578
raise ValueError('Not so fast!')
549579

580+
550581
ioresult = anyio.run(raising.awaitable) # all `Future`s return IO containers
551582
assert ioresult == IOFailure(ValueError('Not so fast!')) # True
552583
```
@@ -560,14 +591,14 @@ to get sync `IOResult` instance to work with it in a sync manner.
560591
Previously, you had to do quite a lot of `await`ing while writing `async` code:
561592

562593
```python
563-
async def fetch_user(user_id: int) -> 'User':
564-
...
594+
async def fetch_user(user_id: int) -> 'User': ...
565595

566-
async def get_user_permissions(user: 'User') -> 'Permissions':
567-
...
568596

569-
async def ensure_allowed(permissions: 'Permissions') -> bool:
570-
...
597+
async def get_user_permissions(user: 'User') -> 'Permissions': ...
598+
599+
600+
async def ensure_allowed(permissions: 'Permissions') -> bool: ...
601+
571602

572603
async def main(user_id: int) -> bool:
573604
# Also, don't forget to handle all possible errors with `try / except`!
@@ -587,23 +618,25 @@ import anyio
587618
from returns.future import FutureResultE, future_safe
588619
from returns.io import IOSuccess, IOFailure
589620

621+
590622
@future_safe
591-
async def fetch_user(user_id: int) -> 'User':
592-
...
623+
async def fetch_user(user_id: int) -> 'User': ...
624+
593625

594626
@future_safe
595-
async def get_user_permissions(user: 'User') -> 'Permissions':
596-
...
627+
async def get_user_permissions(user: 'User') -> 'Permissions': ...
628+
597629

598630
@future_safe
599-
async def ensure_allowed(permissions: 'Permissions') -> bool:
600-
...
631+
async def ensure_allowed(permissions: 'Permissions') -> bool: ...
632+
601633

602634
def main(user_id: int) -> FutureResultE[bool]:
603635
# We can now turn `main` into a sync function, it does not `await` at all.
604636
# We also don't care about exceptions anymore, they are already handled.
605637
return fetch_user(user_id).bind(get_user_permissions).bind(ensure_allowed)
606638

639+
607640
correct_user_id: int # has required permissions
608641
banned_user_id: int # does not have required permissions
609642
wrong_user_id: int # does not exist
@@ -624,6 +657,7 @@ Or even something really fancy:
624657
from returns.pointfree import bind
625658
from returns.pipeline import flow
626659

660+
627661
def main(user_id: int) -> FutureResultE[bool]:
628662
return flow(
629663
fetch_user(user_id),

0 commit comments

Comments
 (0)