Skip to content

Commit

Permalink
Add initial documents
Browse files Browse the repository at this point in the history
  • Loading branch information
tarsil committed Oct 10, 2023
1 parent 8079508 commit 85a5bf3
Show file tree
Hide file tree
Showing 20 changed files with 584 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/1-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ We can then decide if the discussion needs to be escalated into an "Issue" or no
This will make sure that everything is organised properly.
---

**polyforce version**:
**Polyforce version**:
**Python version**:
**OS**:
**Platform**:
Expand Down
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# polyforce
# Polyforce

Enforce annotations in your python code
<p align="center">
<a href="https://polyforce.tarsild.io"><img src="https://res.cloudinary.com/tarsild/image/upload/v1696959172/packages/polyforce/logo_pyynl9.png" alt='Polyforce'></a>
</p>

<p align="center">
<em>🔥 Enforce static typing in your codebase 🔥</em>
</p>

<p align="center">
<a href="https://github.com/tarsil/polyforce/workflows/Test%20Suite/badge.svg?event=push&branch=main" target="_blank">
<img src="https://github.com/tarsil/polyforce/workflows/Test%20Suite/badge.svg?event=push&branch=main" alt="Test Suite">
</a>

<a href="https://pypi.org/project/polyforce" target="_blank">
<img src="https://img.shields.io/pypi/v/polyforce?color=%2334D058&label=pypi%20package" alt="Package version">
</a>

<a href="https://pypi.org/project/polyforce" target="_blank">
<img src="https://img.shields.io/pypi/pyversions/polyforce.svg?color=%2334D058" alt="Supported Python versions">
</a>
</p>

---

**Documentation**: [https://polyforce.tarsild.io][polyforce] 📚

**Source Code**: [https://github.com/tarsil/polyforce](https://github.com/tarsil/polyforce)

---

[polyforce]: https://polyforce.tarsild.io
File renamed without changes.
Empty file added docs/decorator.md
Empty file.
240 changes: 238 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,239 @@
# polyforce
# Polyforce

Enforce annotations in your python code
<p align="center">
<a href="https://polyforce.tarsild.io"><img src="https://res.cloudinary.com/tarsild/image/upload/v1696959172/packages/polyforce/logo_pyynl9.png" alt='Polyforce'></a>
</p>

<p align="center">
<em>🔥 Enforce static typing in your codebase at runtime 🔥</em>
</p>

<p align="center">
<a href="https://github.com/tarsil/polyforce/workflows/Test%20Suite/badge.svg?event=push&branch=main" target="_blank">
<img src="https://github.com/tarsil/polyforce/workflows/Test%20Suite/badge.svg?event=push&branch=main" alt="Test Suite">
</a>

<a href="https://pypi.org/project/polyforce" target="_blank">
<img src="https://img.shields.io/pypi/v/polyforce?color=%2334D058&label=pypi%20package" alt="Package version">
</a>

<a href="https://pypi.org/project/polyforce" target="_blank">
<img src="https://img.shields.io/pypi/pyversions/polyforce.svg?color=%2334D058" alt="Supported Python versions">
</a>
</p>

---

**Documentation**: [https://polyforce.tarsild.io][polyforce] 📚

**Source Code**: [https://github.com/tarsil/polyforce](https://github.com/tarsil/polyforce)

---

## Motivation

During software development we face issues where we don't know what do pass as specific parameters
or even return of the functions itself.

Tools like [mypy][mypy] for example, allow you to run static checking in your code and therefore
allowing to type your codebase properly but **it does not enforce it when running**.

For those coming from hevily static typed languages like **Java**, **.net** and many others, Python
can be overwhelming and sometimes confusing because of its versatility.

**Polyforce** was created to make sure you:

* Don't forget to type your functions and variables.
* Validates the typing in **runtime**.
* Don't forget thr return annotations.

Wouldn't be cool to have something like this:

> What if my function that expects a parameter of type string, if something else is passed could
simply fail, as intended?

This is where **Polyforce enters**.

## The library

Polyforce was designed to enforce the static typing **everywhere** in your code base. From functions
to parameters.

It was also designed to make sure the typing is enforced at runtime.

In other words, if you declare a type `string` and decide to pass an `integer`, it will blow throw
and intended error.

The library offers two ways of implementing the solution.

* [Via model](./model.md)
* [Via decorator](./decorator.md)

## How to use it

Let us see some scenarios where the conventional python is applied and then where **Polyforce**
can make the whole difference for you.

### Conventional Python

Let us start with a simple python function.

#### Simple function

```python
def my_function(name: str):
return name
```

In the normal python world, this wouldn't make any difference, and let us be honest, if you don't care
about mypy or any related tools, this will work without any issues.

This will also allow this to run without any errors:

```python
my_function("Polyfactory") # returns "Polyfactory"
my_function(1) # returns 1
my_function(2.0) # returns 2.0
```

The example above is 100% valid for that specific function and all values passed will be returned
equaly valid and the reson for this is because Python **does not enforce the static typing** so
the `str` declared for the parameter `name` **is merely visual**.

#### With objects

```python
class MyClass:

def my_function(name: str):
return name
```

And then this will be also valid.

```python
my_class = MyClass()

my_class.my_function("Polyfactory") # returns "Polyfactory"
my_class.my_function(1) # returns 1
my_class.my_function(2.0) # returns 2.0
```

I believe you understand the gist of what is being referred here. So, what if there was a solution
where we actually enforce the typing at runtime? Throw some errors when something is missing from
the typing and also when the wrong type is being sent into a function?

Enters [Polyforce](#polyforce)

### Polyforce

Now, let us use the same examples used before but using **Polyforce** and see what happens?

#### Simple function

```python hl_lines="1"
from polyforce import polycheck


@polycheck
def my_function(name: str):
return name
```

The example above it will throw a `ReturnSignatureMissing` or a `MissingAnnotation`
because the **missing return annotation** of the function or a parameter annotation respectively.

```python
my_function("Polyforce") # Throws an exception
```

The correct way would be:

```python hl_lines="1 5"
from polyforce import polycheck


@polycheck
def my_function(name: str) -> str:
return name
```

So what if now you pass a value that is not of type string?

```python
my_function(1) # Throws an exception
```

This will also throw a `TypeError` exception because you are trying to pass a type `int` into a
declared type `str`.

#### With objects

The same level of validations are applied within class objects too.

```python hl_lines="1 4"
from polyforce import PolyModel


class MyClass(PolyModel):

def __init__(self, name, age: int):
...

def my_function(self, name: str):
return name
```

The example above it will throw a `ReturnSignatureMissing` and a `MissingAnnotation`
because the **missing return annotation for both __init__ and the function** as well as the missing
types for the parameters in both.

The correct way would be:

```python hl_lines="1 4"
from polyforce import PolyModel


class MyClass(PolyModel):

def __init__(self, name: str, age: int) -> None:
...

def my_function(self, name: str) -> str:
return name
```

## The Polyforce

As you can see, utilising the library is very simple and very easy, in fact, it was never so easy to
enforce statuc typing in python.

For classes, you simply need to import the `PolyModel`.

```python
from polyforce import PolyModel
```

And to use the decorator you simply can:

```python
from polyforce import polycheck
```

## PolyModel vs polycheck

When using `PolyModel`, there is no need to apply the `polycheck` decorator. The `PolyModel` is
smart enough to apply the same level of validations as the `polycheck`.

When using the `PolyModel` you can use normal python as you would normally do and that means
`classmethod`, `staticmethod` and normal functions.

This like this, the `polycheck` is used for all the functions that are not inside a class.

## Limitations

For now, **Polyforce** is not looking at **native magic methods** (usually start and end with double underscore).
In the future it is planned to understand those on a class level.

[polyforce]: https://polyforce.tarsild.io
[mypy]: https://mypy.readthedocs.io/en/stable/
67 changes: 67 additions & 0 deletions docs/model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# PolyModel

This is the object used for all the classes that want to enforce the static typing all over
the object itself.

This object is different from the [decorator](./decorator.md) as you don't need to specify
which functions should be enforced.

## How to use it

When using the `PolyModel` you must import it first.

```python
from polyforce import PolyModel
```

Once it is imported you can simply subclass it in your objects. Something like this:

```python hl_lines="5 8"
{!> ../docs_src/model/example.py !}
```

When adding the `PolyModel` object, will enable the static type checking to happen all over the
functions declared in the object.

### Ignore the checks

Well, there is not too much benefit of using `PolyModel` if you want to ignore the checks, correct?
Well, yes but you still can do it if you want.

There might be some scenarios where you want to override some checks and ignore the checks.

For this, **Polyforce** uses the [Config](./config.md) dictionary.

You simply need to pass `ignore=True` and the static type checking will be disabled for the class.

It will look like this:

```python hl_lines="9"
{!> ../docs_src/model/disable.py !}
```

### Ignore specific types

What if you want to simply ignore some types? Meaning, you might want to pass arbitraty values that
you don't want them to be static checked.

```python hl_lines="3 10 24"
{!> ../docs_src/model/ignored_types.py !}
```

This will make sure that the type `Actor` is actually ignore and assumed as type `Any` which also means
you can pass whatever value you desire since the type `Actor` is no longer checked.


### Integrations

**Polyforce** works also really well with integrations, for instance with [Pydantic](https://pydantic.dev).

The only thing you need to do is to import the [decorator](./decorator.md) and use it inside the
functions you want to enforce.

```python hl_lines="5 18 22"
{!> ../docs_src/model/integrations.py !}
```

This way you can use your favourite libraries with **Polyforce**.
Binary file added docs/overrides/white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/statics/images/favicon.ico
Binary file not shown.
Binary file added docs/statics/images/white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions docs_src/model/disable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import List, Union

from typing_extensions import Self

from polyforce import Config, PolyModel


class Movie(PolyModel):
config: Config(ignore=True)

def __init__(
self,
name: str,
year: int,
tags: Union[List[str], None] = None,
) -> None:
self.name = name
self.year = year
self.tags = tags

def get_movie(self, name: str) -> Self:
"""
Returns a movie
"""
...

def _set_name(self, name: str) -> None:
"""
Sets the name of the movie.
"""

@classmethod
def create_movie(cls, name: str, year: int) -> Self:
"""
Creates a movie object
"""
return cls(name=name, year=year)

@staticmethod
def evaluate_movie(name: str, tags: List[str]) -> bool:
"""
Evaluates a movie in good (true) or bad (false)
"""
...
Loading

0 comments on commit 85a5bf3

Please sign in to comment.