-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
584 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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**. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
""" | ||
... |
Oops, something went wrong.