Metadata-Version: 2.1
Name: pedantic
Version: 1.0.4
Summary: Some useful Python decorators for cleaner software development.
Home-page: https://github.com/LostInDarkMath/pedantic-python-decorators
Author: Willi Sontopski
Maintainer: Willi Sontopski
License: Apache-2.0 License
Project-URL: Bug Tracker, https://github.com/LostInDarkMath/pedantic-python-decorators/issues
Project-URL: Documentation, https://github.com/LostInDarkMath/pedantic-python-decorators
Project-URL: Source Code, https://github.com/LostInDarkMath/pedantic-python-decorators
Keywords: decorators tools helpers type-checking pedantic type annotations
Platform: UNKNOWN
Description-Content-Type: text/markdown
Requires-Dist: docstring-parser

# pedantic-python-decorators [![Build Status](https://travis-ci.com/LostInDarkMath/pedantic-python-decorators.svg?branch=master)](https://travis-ci.com/LostInDarkMath/pedantic-python-decorators)  [![Coverage Status](https://coveralls.io/repos/github/LostInDarkMath/pedantic-python-decorators/badge.svg?branch=master)](https://coveralls.io/github/LostInDarkMath/pedantic-python-decorators?branch=master) [![PyPI version](https://badge.fury.io/py/pedantic.svg)](https://badge.fury.io/py/pedantic)
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:
  ```python
  @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](https://docs.python.org/3/library/typing.html). If some type hints are missing, an `AssertionError` is thrown. Examples:
  ```python
  @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:
    ```python
    @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](https://google.github.io/styleguide/pyguide.html) 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:
```python
@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:
```python
@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
```python
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](https://docs.python.org/3/library/abc.html) from the standard library, you can write things like that:

```python
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.
```python
@deprecated
def oudated_calculation():
    # perform some stuff
```

### @unimplemented
For documentation purposes. Throw `NotImplementedException` if the function is called.
```python
@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.
```python
@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.
```python
@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.
```python
@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.
```python
@timer
def some_calculation():
    # perform possible long taking calculation here
```

### @count_calls
Prints how often the method is called during program execution.
```python
@count_calls
def some_calculation():
    print('hello world')
```

### @trace
Prints the passed arguments and the return value on each function call.
```python
@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:
```python
@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:
```python
@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:
```python
# 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:
```python
@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:
```python
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](https://pypi.org/)
Run `pip install pedantic`.

## Option 2: Installing with pip and git
1. Install [Git](https://git-scm.com/downloads) 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](https://github.com/LostInDarkMath/PythonHelpers/releases/latest) 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:
- [Docstring-Parser](https://github.com/rr-/docstring_parser) (Version 0.7.2, requires Python 3.6 or later)

Created with Python 3.8.5. [It works with Python 3.6 or newer.](https://travis-ci.com/github/LostInDarkMath/PythonHelpers)

## 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]
```


