1 minute read

Many of my colleagues are working on writing better code, so I’m sharing a little tip that I find very useful.

Your main() function should be independent of the argparser.

You should be able to drive your application from both the cli/terminal (using the CliAdapter) and for example your tests (using the TestAdapter).

I have written about this before here in Your argparse is not the interface of your application.

Let me illustrate:

"""
Example script to show how to decouple your application from the CLI.

Your CLI shouldn't be the interface to your application, it should just be one
way of driving the application.

Notice how main() is decoupled from the CLI. It knows nothing about how to parse
arguments.
"""
import argparse
from abc import abstractmethod, ABC
from pydantic import BaseModel

class InputModel(BaseModel):
    value: int

class InputInterface(ABC):
    @abstractmethod
    def parse(self, args) -> InputModel:
        raise NotImplementedError

class CliAdapter(InputInterface):
    def parse(self, args) -> InputModel:
        parser = argparse.ArgumentParser(description='Process an integer.')
        parser.add_argument('integer', type=int, help='An integer value')
        args = parser.parse_args(args)
        return InputModel(value=args.integer)

class TestAdapter(InputInterface):
    def parse(self, args) -> InputModel:
        return InputModel(value=10)

def main(input: InputModel):
    print(input.value)

if __name__ == "__main__":
    # In your main entrypoint
    cli = CliAdapter()
    input = cli.parse(["10"])
    main(input)

    # In a pytest fixture and test code
    test = TestAdapter()
    input = test.parse(None)
    main(input)

Your main() should not care whether it gets the arguments from the cli or from any other adapter. If it gets a correct InputModel it should just run.

Subscribe

Comments