Skip to main content

Some useful Python decorators for cleaner software development.

Project description

pedantic-python-decorators Build Status Coverage Status PyPI version

Some useful decorators which I use in almost every python project. These decorators will make you write cleaner and well-documented Python code.

The powerful decorators

@pedantic

The @pedantic decorator does the following things:

  • The decorated function can only be called by using keyword arguments. Positional arguments are not accepted. Normally, the following snippets are valid Python code, but @pedantic there aren't valid any longer:
    @pedantic
    def do(s: str, a: float: b: int) -> List[str]:
      return [s, str(a + b)]
    
    do('hi', 3.14, 4)  # error! Correct would be: do(s=hi', a=3.14, b=4)
    
  • The decorated function must have Type annotations. If some type hints are missing, an AssertionError is thrown. Examples:
    @pedantic
    def foo():    # will raise an error. Correct would be: def foo() -> None:
      print'bar'
    
    @pedantic
    def foo2(s): # will raise an error. Correct would be: def foo(s: str) -> None:
      print(s)
    
  • The decorator parses the type annotations and each time the function is called, is checks, the argument matches the type annotations, before the function is executed and that the return value matches the corresponding return type annotation. As a consquence, the arguments are also checked for None, because None is only a valid argument, if it is annoted via Optional[str] or equivalently Union[str, None]. So the following examples will cause @pedantic to raise an error:
    @pedantic
    def do(s: str, a: float: b: int) -> List[str]:
        return [s, str(a + b)]
    
    do(s=None, a=3.14, b=4)     # will raise an error. None is not a string.
    de(s='None', a=3.14, b=4.0) # will raise an error: 4.0 is not an int.
    
    @pedantic
    def do2(s: str, a: float: b: int) -> List[str]:
        return [s, a + b]  # will raise an error: Expected List[str], but a + b is a float
    
  • If the decorated function has a docstring, that lists the arguments, the docstring is parsed and compared with the type hints. Because the type hints are checked at runtime, the docstring is also checked at runtime. It is checked, that the type annotations matches exactly the arguments, types and return values in the docstring. Currently, only docstrings in the Google Format are supported. @pedantic raises an AssertionError if one of the following happend:
    • Not all arguments the function are documented in the docstring.
    • There are arguments documented in the doc string, that are not taken by the function.
    • The return value is not documented.
    • A return value is documented, but the function does not return anything.
    • The type annotations in function don't match the documented types in the docstring.

@pedantic_require_docstring

It's like @pedantic, but it additionally forces developers to create docstrings. It raises an AssertionError, if there is no docstring.

@validate_args

With the @validate_args decorator you can do contract checking outside of functions. Just pass a validator in, for example:

@validate_args(lambda x: (x > 42, f'Each arg should be > 42, but it was {x}.'))
def some_calculation(a, b, c):
    return a + b + c

some_calculation(30, 40, 50)  # this leads to an error
some_calculation(43, 45, 50)  # this is okay

The error message is optional. So you could also write:

@validate_args(lambda x: x > 42)
def some_calculation(a, b, c):

There are some shortcuts included for often used validations:

  • @require_not_none ensures that each argument is not None
  • @require_not_empty_strings ensures that each passed argument is a non_empty string, so passind " " will raise an AssertionError.

The small decorators:

@overrides

from pedantic.method_decorators import overrides


class Parent:
    def instance_method(self):
        pass


class Child(Parent):
    @overrides(Parent)
    def instance_method(self):
        print('hi')

Together with the Abstract Base Class from the standard library, you can write things like that:

from abc import ABC, abstractmethod
from pedantic.method_decorators import overrides


class Parent(ABC):

    @abstractmethod
    def instance_method(self):
        pass


class Child(Parent):

    @overrides(Parent)
    def instance_method(self):
        print('hi')

@deprecated

This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used.

@deprecated
def oudated_calculation():
    # perform some stuff

@unimplemented

For documentation purposes. Throw NotImplementedException if the function is called.

@unimplemented
def new_operation():
    pass

@needs_refactoring

Of course, you refactor immediately if you see something ugly. However, if you don't have the time for a big refactoring use this decorator at least. A warning is raised everytime the decorated function is called.

@needs_refactoring
def almost_messy_operation():
    pass

@dirty

A decorator for preventing from execution and therefore from causing damage. If the function gets called, a TooDirtyException is raised.

@dirty
def messy_operation():
    # messy code goes here

@require_kwargs

Checks that each passed argument is a keyword argument and raises an AssertionError if any positional arguments are passed.

@require_kwargs
    def some_calculation(a, b, c):
        return a + b + c
some_calculation(5, 4, 3)       # this will lead to an AssertionError
some_calculation(a=5, b=4, c=3) # this is okay

@timer

Prints out how long the execution of the function takes in seconds.

@timer
def some_calculation():
    # perform possible long taking calculation here

@count_calls

Prints how often the method is called during program execution.

@count_calls
def some_calculation():
    print('hello world')

@trace

Prints the passed arguments and the return value on each function call.

@trace
def some_calculation(a, b):
    return a + b

Decorators for classes

With the @for_all_methods you can use any decorator for classes instead of methods. It is shorthand for putting the same decorator on every method of the class. Example:

@forall_methods(pedantic)
class MyClass():
    # lots of methods

There are a few aliases defined:

  • pedantic_class is an alias for forall_methods(pedantic)
  • pedantic_class_require_docstring is an alias for forall_methods(pedantic_require_docstring)
  • timer_class is an alias for forall_methods(timer)
  • trace_class is an alias for forall_methods(trace)

That means by only changing one line of code you can make your class pedantic:

@pedantic_class
class MyClass:
    def __init__(self, a: int) -> None:
        self.a = a

    def calc(self, b: int) -> int:
        return self.a - b

    def print(self, s: str) -> None:
        print(f'{self.a} and {s}')

m = MyClass(a=5)
m.calc(b=42)
m.print(s='Hi')

Using multiple decorators

Sometimes you may want to use multiple decorators on the same method or class. But the normal way of doing this can cause probleme sometimes:

# this can cause trouble
@pedantic
@validate_args(lambda x: x > 0)
def some_calculation(self, x: int) -> int:
    return x

Instead, you can use the combine decorator, which takes a list of decorators as an argument. The order of the list doesn't matter:

@combine([pedantic, validate_args(lambda x: x > 0)])
def some_calculation(self, x: int) -> int:
    return x

This also works for class decorators as well. However, a very common scenario is to combine @pedantic_class with @overrides which both works well together even without using @combine. So you can write things like this:

from abc import ABC, abstractmethod
from pedantic import pedantic_class, overrides

@pedantic_class
class Abstract(ABC):
    @abstractmethod
    def func(self, b: str) -> str:
        pass

    @abstractmethod
    def bunk(self) -> int:
        pass

@pedantic_class
class Foo(Abstract):
    def __init__(self, a: int) -> None:
        self.a = a

    @overrides(Abstract)
    def func(self, b: str) -> str:
        return b

    @overrides(Abstract)
    def operation(self) -> int:
        return self.a

f = Foo(a=42)
f.func(b='Hi')
f.operation()

How to start

Installation

Option 1: installing with pip from Pypi

Run pip install pedantic.

Option 2: Installing with pip and git

  1. Install Git if you don't have it already.
  2. Run pip install git+https://github.com/LostInDarkMath/pedantic-python-decorators.git@master

Option 3: Offline installation using wheel

  1. Download the latest release here by clicking on pedantic-python-decorators-x.y.z-py-none-any.whl.
  2. Execute pip install pedantic-python-decorators-x.y.z-py3-none-any.whl where x.y.z needs to be the correct version.

Usage

In your Python file write from pedantic import pedantic, pedantic_class or whatever decorator you want to use. Use it like mentioned above. Happy coding!

Dependencies

Outside the Python standard library, the following dependencies are used:

Created with Python 3.8.5. It works with Python 3.6 or newer.

Risks and Side Effects

The usage of decorators may affect the performance of your application. For this reason, it would highly recommend you to disable the decorators during deployment. Best practice would be to integrate this in a automated depoly chain:

[CI runs tests] => [Remove decorators] => [deploy cleaned code]

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pedantic-1.0.4.tar.gz (24.4 kB view hashes)

Uploaded Source

Built Distribution

pedantic-1.0.4-py3-none-any.whl (28.9 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page