Skip to content

Commit

Permalink
Add sigparse.classparse (#2)
Browse files Browse the repository at this point in the history
* add classparse impl

* clean up imports

* limit forbiddenfruit python version

* fix typing

* add comment

* remove types.FunctionType

* update readme

* fix linting

* fix typing
  • Loading branch information
zunda-arrow authored Oct 9, 2022
1 parent 74a366c commit 8a7e46e
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 114 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ def func(param_a: list[str], param_b: str | int, param_c: tuple[int | None]):
sigparse.sigparse(func)
```

Sigparse also supports classes.

```python
import sigparse

class MyClass:
a: list[str]
b: str | int
c: tuple[int | None]

sigparse.classparse(MyClass)
```


### PEP604
By default PEP 604 (| for unions) is only enabled for `sigparse.sigparse`.
To enable globally:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ homepage = "https://github.com/Lunarmagpie/sigparse"

[tool.poetry.dependencies]
python = ">=3.7"
forbiddenfruit = "^0.1.4"
forbiddenfruit = { version="^0.1.4", python = "<3.10" }

[tool.poetry.dev-dependencies]
black = "^22.8.0"
Expand Down
115 changes: 4 additions & 111 deletions sigparse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# MIT License

# Copyright (c) 2022 Lunarmagpie
# L37-L51 Copyright (c) 2022 Endercheif

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand All @@ -24,115 +23,9 @@

from __future__ import annotations

import dataclasses
import typing
import sys
import inspect
import forbiddenfruit # type: ignore
from sigparse._sigparse import sigparse, Parameter
from sigparse._classparse import classparse
from sigparse._pep604 import global_PEP604


__all__: typing.Sequence[str] = ("sigparse", "Parameter", "global_PEP604")


def _apply_PEP604() -> None:
"""
Allow writing union types as X | Y
"""

if sys.version_info >= (3, 10):
return

def _union_or(left: typing.Any, right: typing.Any) -> typing.Any:
return typing.Union[left, right]

setattr(typing._GenericAlias, "__or__", _union_or) # type: ignore
setattr(typing._GenericAlias, "__ror__", _union_or) # type: ignore

forbiddenfruit.curse(type, "__or__", _union_or)


def _revert_PEP604() -> None:
if sys.version_info >= (3, 10):
return

forbiddenfruit.reverse(type, "__or__")


GLOBAL_PEP604 = False


def global_PEP604() -> None:
global GLOBAL_PEP604
GLOBAL_PEP604 = True
_apply_PEP604()


@dataclasses.dataclass
class Parameter:
"""
`default` and `annotation` are `inspect._empty` when there is no default or
annotation respectively.
"""

name: str
annotation: typing.Any
default: typing.Any
kind: inspect._ParameterKind

@property
def has_default(self) -> bool:
"""
Return `True` if this argument has a default value.
"""
return self.default is not inspect._empty

@property
def has_annotation(self) -> bool:
"""
Return `True` if this argument has an annotation.
"""
return self.annotation is not inspect._empty


def _convert_signiture(
param: inspect.Parameter, type_hints: dict[str, type[typing.Any]]
) -> Parameter:
annotation = type_hints.get(param.name)
return Parameter(
name=param.name,
annotation=annotation or param.annotation,
default=param.default,
kind=param.kind,
)


def sigparse(func: typing.Callable[..., typing.Any]) -> list[Parameter]:
if sys.version_info >= (3, 10):
return [
_convert_signiture(param, {})
for param in inspect.signature(func, eval_str=True).parameters.values()
]

localns: dict[str, typing.Any] = {
"list": typing.List,
"type": typing.Type,
"dict": typing.Dict,
"tuple": typing.Tuple,
}

if not GLOBAL_PEP604:
_apply_PEP604()

if sys.version_info >= (3, 9):
type_hints: dict[str, typing.Any] = typing.get_type_hints(
func, include_extras=True, localns=localns
)
else:
type_hints = typing.get_type_hints(func, localns=localns)

sig = inspect.signature(func)

if not GLOBAL_PEP604:
_revert_PEP604()

return [_convert_signiture(param, type_hints) for param in sig.parameters.values()]
__all__: typing.Sequence[str] = ("classparse", "sigparse", "Parameter", "global_PEP604")
68 changes: 68 additions & 0 deletions sigparse/_applicator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# MIT License

# Copyright (c) 2022 Lunarmagpie

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import annotations
import abc
import typing
import sys

from sigparse._pep604 import PEP604_CTX

LOCALNS: dict[str, typing.Any] = {
"list": typing.List,
"type": typing.Type,
"dict": typing.Dict,
"tuple": typing.Tuple,
}


IN = typing.TypeVar("IN")
OUT = typing.TypeVar("OUT")


class Applicator(abc.ABC, typing.Generic[IN, OUT]):
def __init__(self, obj: IN) -> None:
if sys.version_info >= (3, 10):
self.return_value = self.gt_or_eq_310(obj)

elif sys.version_info >= (3, 9):
with PEP604_CTX():
self.return_value = self.eq_309(obj)

else:
with PEP604_CTX():
self.return_value = self.lt_or_eq_308(obj, LOCALNS)

def __call__(self) -> OUT:
return self.return_value

@abc.abstractmethod
def gt_or_eq_310(self, obj: IN) -> OUT:
...

@abc.abstractmethod
def eq_309(self, obj: IN) -> OUT:
...

@abc.abstractmethod
def lt_or_eq_308(self, obj: IN, localns: dict[str, type]) -> OUT:
...
48 changes: 48 additions & 0 deletions sigparse/_classparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# MIT License

# Copyright (c) 2022 Lunarmagpie

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import annotations


import typing

from sigparse._applicator import Applicator


class Classparse(Applicator[type, "dict[str, type]"]):
@typing.no_type_check
def gt_or_eq_310(self, func: typing.Any) -> dict[str, type]:
return typing.get_type_hints(func, include_extras=True)

@typing.no_type_check
def eq_309(self, func: typing.Any) -> dict[str, type]:
return typing.get_type_hints(func, include_extras=True)

@typing.no_type_check
def lt_or_eq_308(
self, func: typing.Any, localns: dict[str, type]
) -> dict[str, type]:
return typing.get_type_hints(func, localns=localns)


def classparse(cls: type) -> dict[str, type]:
return Classparse(cls)()
73 changes: 73 additions & 0 deletions sigparse/_pep604.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# MIT License

# Copyright (c) 2022 Endercheif

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import typing
import sys

if sys.version_info < (3, 10):
import forbiddenfruit # type: ignore


__all__: typing.Sequence[str] = ("PEP604_CTX",)


def _apply_PEP604() -> None:
"""
Allow writing union types as X | Y
"""

if sys.version_info >= (3, 10):
return

def _union_or(left: typing.Any, right: typing.Any) -> typing.Any:
return typing.Union[left, right]

setattr(typing._GenericAlias, "__or__", _union_or) # type: ignore
setattr(typing._GenericAlias, "__ror__", _union_or) # type: ignore

forbiddenfruit.curse(type, "__or__", _union_or)


def _revert_PEP604() -> None:
if sys.version_info >= (3, 10):
return

forbiddenfruit.reverse(type, "__or__")


GLOBAL_PEP604 = False


def global_PEP604() -> None:
global GLOBAL_PEP604
GLOBAL_PEP604 = True
_apply_PEP604()


class PEP604_CTX:
def __enter__(self) -> None:
_apply_PEP604()

def __exit__(self, *_: typing.Any) -> None:
if not GLOBAL_PEP604:
_revert_PEP604()
Loading

0 comments on commit 8a7e46e

Please sign in to comment.