From 85a5bf36019dff0077dfa2a29ab4eba4c25ee8e6 Mon Sep 17 00:00:00 2001 From: tarsil Date: Tue, 10 Oct 2023 21:16:54 +0100 Subject: [PATCH] Add initial documents --- .github/ISSUE_TEMPLATE/1-issue.md | 2 +- README.md | 34 +++- docs_src/.gitkeep => docs/config.md | 0 docs/decorator.md | 0 docs/index.md | 240 +++++++++++++++++++++++- docs/model.md | 67 +++++++ docs/overrides/white.png | Bin 0 -> 23758 bytes docs/statics/images/favicon.ico | Bin 0 -> 15406 bytes docs/statics/images/white.png | Bin 0 -> 23758 bytes docs_src/model/disable.py | 44 +++++ docs_src/model/example.py | 42 +++++ docs_src/model/ignored_types.py | 28 +++ docs_src/model/integrations.py | 24 +++ mkdocs.yml | 15 +- polyforce/decorator.py | 13 +- pyproject.toml | 1 + tests/models/test_ignored_types.py | 36 ++++ tests/models/test_integration.py | 45 +++++ tests/models/test_model_construction.py | 8 + tests/test_function.py | 7 +- 20 files changed, 584 insertions(+), 22 deletions(-) rename docs_src/.gitkeep => docs/config.md (100%) create mode 100644 docs/decorator.md create mode 100644 docs/model.md create mode 100644 docs/overrides/white.png create mode 100644 docs/statics/images/favicon.ico create mode 100644 docs/statics/images/white.png create mode 100644 docs_src/model/disable.py create mode 100644 docs_src/model/example.py create mode 100644 docs_src/model/ignored_types.py create mode 100644 docs_src/model/integrations.py create mode 100644 tests/models/test_ignored_types.py create mode 100644 tests/models/test_integration.py diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md index 2abb9cf..f58dde5 100644 --- a/.github/ISSUE_TEMPLATE/1-issue.md +++ b/.github/ISSUE_TEMPLATE/1-issue.md @@ -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**: diff --git a/README.md b/README.md index 1f75252..5828336 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ -# polyforce +# Polyforce -Enforce annotations in your python code +

+ Polyforce +

+ +

+ 🔥 Enforce static typing in your codebase 🔥 +

+ +

+ + Test Suite + + + + Package version + + + + Supported Python versions + +

+ +--- + +**Documentation**: [https://polyforce.tarsild.io][polyforce] 📚 + +**Source Code**: [https://github.com/tarsil/polyforce](https://github.com/tarsil/polyforce) + +--- + +[polyforce]: https://polyforce.tarsild.io diff --git a/docs_src/.gitkeep b/docs/config.md similarity index 100% rename from docs_src/.gitkeep rename to docs/config.md diff --git a/docs/decorator.md b/docs/decorator.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/index.md b/docs/index.md index 1f75252..e33909e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,239 @@ -# polyforce +# Polyforce -Enforce annotations in your python code +

+ Polyforce +

+ +

+ 🔥 Enforce static typing in your codebase at runtime 🔥 +

+ +

+ + Test Suite + + + + Package version + + + + Supported Python versions + +

+ +--- + +**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/ diff --git a/docs/model.md b/docs/model.md new file mode 100644 index 0000000..381d301 --- /dev/null +++ b/docs/model.md @@ -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**. diff --git a/docs/overrides/white.png b/docs/overrides/white.png new file mode 100644 index 0000000000000000000000000000000000000000..41441f8ee37bfff380e1d6eb8d87cb063714e656 GIT binary patch literal 23758 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 literal 0 HcmV?d00001 diff --git a/docs/statics/images/favicon.ico b/docs/statics/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..41eb0c49a2da5cdd420c47da67207563e06412aa GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/docs/statics/images/white.png b/docs/statics/images/white.png new file mode 100644 index 0000000000000000000000000000000000000000..41441f8ee37bfff380e1d6eb8d87cb063714e656 GIT binary patch literal 23758 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 literal 0 HcmV?d00001 diff --git a/docs_src/model/disable.py b/docs_src/model/disable.py new file mode 100644 index 0000000..1af9d6a --- /dev/null +++ b/docs_src/model/disable.py @@ -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) + """ + ... diff --git a/docs_src/model/example.py b/docs_src/model/example.py new file mode 100644 index 0000000..de03efd --- /dev/null +++ b/docs_src/model/example.py @@ -0,0 +1,42 @@ +from typing import List, Union + +from typing_extensions import Self + +from polyforce import PolyModel + + +class Movie(PolyModel): + 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) + """ + ... diff --git a/docs_src/model/ignored_types.py b/docs_src/model/ignored_types.py new file mode 100644 index 0000000..58869ee --- /dev/null +++ b/docs_src/model/ignored_types.py @@ -0,0 +1,28 @@ +from typing import List, Union + +from polyforce import Config, PolyModel + + +class Actor: + ... + + +class Movie(PolyModel): + config: Config(ignored_types=(Actor,)) + + def __init__( + self, + name: str, + year: int, + tags: Union[List[str], None] = None, + ) -> None: + self.name = name + self.year = year + self.tags = tags + self.actors: List[Actor] = [] + + def add_actor(self, actor: Actor) -> None: + """ + Returns a movie + """ + self.actors.append(actor) diff --git a/docs_src/model/integrations.py b/docs_src/model/integrations.py new file mode 100644 index 0000000..76c9200 --- /dev/null +++ b/docs_src/model/integrations.py @@ -0,0 +1,24 @@ +from typing import List, Union + +from pydantic import BaseModel, ConfigDict + +from polyforce import polycheck + + +class Actor: + ... + + +class Movie(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + name: str + year: int + actors: Union[List[Actor], None] = None + + @polycheck + def add_actor(self, actor: Actor) -> None: + self.actors.append(actor) + + @polycheck + def set_actor(self, actor: Actor) -> None: + ... diff --git a/mkdocs.yml b/mkdocs.yml index a893ed3..501a1c2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Polyforce -site_description: Enforce annotations in your python code. +site_description: 🔥 Enforce static typing in your codebase at runtime 🔥 site_url: https://polyforce.tarsild.io theme: @@ -8,21 +8,21 @@ theme: language: en palette: - scheme: "default" - primary: "purple" - accent: "amber" + primary: "pink" + accent: "red" media: "(prefers-color-scheme: light)" toggle: icon: "material/lightbulb" name: "Switch to dark mode" - scheme: "slate" media: "(prefers-color-scheme: dark)" - primary: "purple" - accent: "amber" + primary: "pink" + accent: "red" toggle: icon: "material/lightbulb-outline" name: "Switch to light mode" favicon: statics/images/favicon.ico - logo: statics/images/logo-white.svg + logo: statics/images/white.png features: - search.suggest - search.highlight @@ -38,6 +38,9 @@ plugins: nav: - Introduction: "index.md" + - Model: "model.md" + - Decorator: "decorator.md" + - Config: "config.md" - Contributing: "contributing.md" - Sponsorship: "sponsorship.md" - Release Notes: "release-notes.md" diff --git a/polyforce/decorator.py b/polyforce/decorator.py index 1e14092..bdeb061 100644 --- a/polyforce/decorator.py +++ b/polyforce/decorator.py @@ -6,6 +6,7 @@ from typing_extensions import get_args, get_origin from polyforce.constants import CLASS_SPECIAL_WORDS +from polyforce.exceptions import MissingAnnotation, ReturnSignatureMissing def polycheck(wrapped: Any) -> Any: @@ -28,15 +29,11 @@ def check_signature(func: Any) -> Any: signature: inspect.Signature = inspect.Signature.from_callable(func) if signature.return_annotation == inspect.Signature.empty: - raise TypeError( - "A return value of a function should be type annotated. " - "If your function doesn't return a value or returns None, annotate it as returning 'NoReturn' or 'None' respectively." - ) + raise ReturnSignatureMissing(func=func.__name__) + for name, parameter in signature.parameters.items(): if name not in CLASS_SPECIAL_WORDS and parameter.annotation == inspect.Signature.empty: - raise TypeError( - f"'{name}' is not typed. If you are not sure, annotate with 'typing.Any'." - ) + raise MissingAnnotation(name=name) def check_types(*args: Any, **kwargs: Any) -> Any: params = dict(zip(args_spec.args, args)) @@ -52,7 +49,7 @@ def check_types(*args: Any, **kwargs: Any) -> Any: if not isinstance(value, actual_type): raise TypeError( f"Expected type '{type_hint}' for attribute '{name}'" - f" but received type '{type(value)}') instead." + f" but received type '{type(value)}' instead." ) def get_actual_type(type_hint: Any, value: Any) -> Any: diff --git a/pyproject.toml b/pyproject.toml index c0d90c4..56d34c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ test = [ "black>=23.3.0,<24.0.0", "isort>=5.12.0,<6.0.0", "mypy>=1.1.0,<2.0.0", + "pydantic>=2.4.0", "pytest>=7.2.2,<8.0.0", "pytest-cov>=4.0.0,<5.0.0", "requests>=2.28.2", diff --git a/tests/models/test_ignored_types.py b/tests/models/test_ignored_types.py new file mode 100644 index 0000000..d398e27 --- /dev/null +++ b/tests/models/test_ignored_types.py @@ -0,0 +1,36 @@ +from typing import Any, List, Union + +from polyforce import Config, PolyModel + + +class Dummy: + ... + + +class Actor: + ... + + +class Movie(PolyModel): + config = Config(ignored_types=(Actor,)) + + def __init__( + self, name: str, year: int, tags: Union[List[str], None] = None, **kwargs: Any + ) -> None: + self.name = name + self.year = year + self.tags = tags + self.actors: List[Actor] = [] + + def add_actor(self, actor: Actor) -> None: + """ + Returns a movie + """ + self.actors.append(actor) + + +def test_add_value(): + movie = Movie(name="Avengers", year="2023") + movie.add_actor(actor=Dummy()) + + assert len(movie.actors) == 1 diff --git a/tests/models/test_integration.py b/tests/models/test_integration.py new file mode 100644 index 0000000..a03c773 --- /dev/null +++ b/tests/models/test_integration.py @@ -0,0 +1,45 @@ +from typing import List, Union + +import pytest +from pydantic import BaseModel + +from polyforce import polycheck +from polyforce.exceptions import ReturnSignatureMissing + + +class Dummy: + ... + + +class Actor: + ... + + +class Movie(BaseModel): + model_config = {"arbitrary_types_allowed": True} + + name: str + year: int + actors: Union[List[Actor], None] = None + + @polycheck + def add_actor(self, actor: Actor) -> None: + self.actors.append(actor) + + @polycheck + def set_actor(self, actor: Actor): + ... + + +def test_add_value(): + movie = Movie(name="Avengers", year="2023") + + with pytest.raises(TypeError): + movie.add_actor(actor=Dummy()) + + +def test_missing_return(): + movie = Movie(name="Avengers", year="2023") + + with pytest.raises(ReturnSignatureMissing): + movie.set_actor(actor=Dummy()) diff --git a/tests/models/test_model_construction.py b/tests/models/test_model_construction.py index c58e2cc..6e9b861 100644 --- a/tests/models/test_model_construction.py +++ b/tests/models/test_model_construction.py @@ -210,3 +210,11 @@ def test_dict_and_not_str_raise_error_name_function_static(): def test_str_and_not_int_raise_error_function_static(): with pytest.raises(TypeError): Poly.my_function_static(int_value="a") + + +def test_double_underscore(): + with pytest.raises(ReturnSignatureMissing): + + class Double(PolyModel): + def __test(self, name: str): + ... diff --git a/tests/test_function.py b/tests/test_function.py index 5c901b0..77797b0 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -3,6 +3,7 @@ import pytest from polyforce import polycheck +from polyforce.exceptions import MissingAnnotation, ReturnSignatureMissing @polycheck @@ -26,7 +27,7 @@ def test_polycheck_all(): def test_missing_return_annotation(): - with pytest.raises(TypeError) as raised: + with pytest.raises(ReturnSignatureMissing) as raised: @polycheck def test_func(name=None): @@ -36,12 +37,12 @@ def test_func(name=None): assert ( str(raised.value) - == "A return value of a function should be type annotated. If your function doesn't return a value or returns None, annotate it as returning 'NoReturn' or 'None' respectively." + == "Missing return in 'test_func'. A return value of a function should be type annotated. If your function doesn't return a value or returns None, annotate it as returning 'NoReturn' or 'None' respectively." ) def test_missing_typing_annotation(): - with pytest.raises(TypeError) as raised: + with pytest.raises(MissingAnnotation) as raised: @polycheck def test_func(name=None) -> None: