Side-effects¶
deal.has¶
@deal.has is a way to specify markers for a function. Markers are tags about kinds of side-effects which the function has. For example:
@deal.has('stdout', 'database')
def say_hello(id: int) -> None:
user = get_user(id=id)
print(f'Hello, {user.name}')
You can use any markers you want, and Deal will check that if you call a function with some markers, they are specified for the calling function as well. In the example above, print function has marker stdout, so it must be specified in markers of say_hello as well.
Motivation¶
Every application has side-effects. It needs to store data, to communicate with users. However, every side-effect makes testing and debugging much harder: it should be mocked, intercepted, cleaned after every test. The best solution is to have functional core and imperative shell. So, the function above can be refactored to be pure:
import sys
# now this is pure
@deal.has()
def make_hello(user) -> str:
return f'Hello, {user.name}'
# and the main function takes care of all impure things
@deal.has('stdout', 'database')
def main(stream=sys.stdout):
...
user = get_user(id=id)
hello = make_hello(user=user)
print(hello, file=stream)
Built-in markers¶
Deal already know about some markers and will report if they are violated:
code |
marker |
allows |
|---|---|---|
DEL041 |
|
|
DEL042 |
|
|
DEL043 |
|
everything below |
DEL044 |
– |
read a file |
DEL045 |
– |
write into a file |
DEL046 |
– |
|
DEL047 |
– |
|
DEL048 |
– |
network communications, |
DEL049 |
– |
|
DEL050 |
– |
system calls: |
DEL055 |
|
functions from |
DEL056 |
|
accessing system time |
Runtime¶
Some of the markers are checked at runtime:
If any of
io,network, orsocketis specified,deal.haswill allow network operations. Otherwise, it will patch socket blocking all network requests. If the function tries to use the network,OfflineContractErroris raised.If any of
io,print, orstdoutis specified,deal.haswill allow using stdout. Otherwise, it will patch sys.stdout. If the function tries to use it,SilentContractErroris raised.If any of
ioorstdoutis specified,deal.haswill do the same as forstdoutmarker but for sys.stderr
@deal.has()
def f():
print('hello')
f()
# SilentContractError:
Other markers aren’t checked in runtime yet but only checked by the linter.
Markers are properties¶
Markers and exceptions are properties of a function and don’t depend on conditions. That means if a function only sometimes in some conditions does io operation, the function has io marker regardless of possibility of hitting this condition branch. For example:
import deal
def run_job(job_name: str, silent: bool):
if not silent:
print('job started')
...
@deal.has() # must have 'stdout' here.
def main():
job_name = 'hello'
run_job(job_name, silent=True)
return 0
If we run linter on the code above, it will fail with “DEL046 missed marker (stdout)” message. main function calls run_job with silent=True, so print will not be called when calling main. However, run_job function has an implicit stdout marker, and main calls this function so it must have this marker as well.