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
, orsocket
is specified,deal.has
will allow network operations. Otherwise, it will patch socket blocking all network requests. If the function tries to use the network,OfflineContractError
is raised.If any of
io
,print
, orstdout
is specified,deal.has
will allow using stdout. Otherwise, it will patch sys.stdout. If the function tries to use it,SilentContractError
is raised.If any of
io
orstdout
is specified,deal.has
will do the same as forstdout
marker 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.