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

global

global and nonlocal

DEL042

import

import

DEL043

io

everything below

DEL044

read

read a file

DEL045

write

write into a file

DEL046

stdout

sys.stdout and print

DEL047

stderr

sys.stderr

DEL048

network

network communications, socket

DEL049

stdin

sys.stdin

DEL050

syscall

system calls: subprocess, os

DEL055

random

functions from random module

DEL056

time

accessing system time

Runtime

Some of the markers are checked at runtime:

  • If any of io, network, or socket 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, or stdout 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 or stdout is specified, deal.has will do the same as for stdout 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.