Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.11, 3.12, 3.13]
python-version: [3.11, 3.12, 3.13, 3.14]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Changelog
## Pedantic 2.3.0
- added support for Python 3.14
- updated dependencies

## Pedantic 2.2.3
- remove support for deprecated `typing.ByteString`
- fix `WithDecoratedMethods`
Expand Down
2 changes: 1 addition & 1 deletion pedantic/decorators/class_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def decorate(cls: C) -> C:
for attr in cls.__dict__:
attr_value = getattr(cls, attr)

if isinstance(attr_value, (types.FunctionType, types.MethodType)):
if isinstance(attr_value, (types.FunctionType, types.MethodType)) and attr != '__annotate_func__':
setattr(cls, attr, decorator(attr_value))
elif isinstance(attr_value, property):
prop = attr_value
Expand Down
12 changes: 8 additions & 4 deletions pedantic/decorators/fn_deco_in_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
from typing import Callable, TypeVar, Any, Awaitable, Optional, Type, Union

try:
import multiprocess as mp
from multiprocess import Process, Pipe
from multiprocess.connection import Connection

mp.set_start_method(method="spawn", force=True)
except ImportError:
Process: Optional[Type] = None
Pipe: Optional[Type] = None
Expand Down Expand Up @@ -106,11 +103,18 @@ async def calculate_in_subprocess(func: Callable[..., Union[T, Awaitable[T]]], *
return result


def _inner(tx: Connection, fun: Callable[..., Union[T, Awaitable[T]]], *a, **kw_args) -> None:
def _inner(tx: Connection, fun: Callable[..., Union[T, Awaitable[T]]], *a, new_thread: bool = False, **kw_args) -> None:
""" This runs in another process. """

event_loop = None
if inspect.iscoroutinefunction(fun):
if not new_thread: # see https://stackoverflow.com/a/79785720/10975692
import threading
t = threading.Thread(target=_inner, args=(tx, fun, *a), kwargs=(kw_args | {"new_thread": True}))
t.start()
t.join()
return

event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)

Expand Down
1 change: 0 additions & 1 deletion pedantic/tests/tests_pedantic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os.path
import sys
import types
import typing
import unittest
Expand Down
26 changes: 15 additions & 11 deletions pedantic/type_checking_logic/check_docstring.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys
import typing
from typing import * # necessary for eval()

from pedantic.type_checking_logic.check_types import get_type_arguments
Expand Down Expand Up @@ -96,18 +98,18 @@ def _parse_documented_type(type_: str, context: Dict[str, Any], err: str) -> Any
<class 'float'>
>>> _parse_documented_type(type_='List[List[bool]]', context={}, err='')
typing.List[typing.List[bool]]
>>> _parse_documented_type(type_='Union[int, float, str]', context={}, err='')
typing.Union[int, float, str]
>>> typing.Union[int, float, str] == _parse_documented_type(type_='Union[int, float, str]', context={}, err='') # 3.13: typing.Union[int, float, str], 3.14: int | float | str
True
>>> _parse_documented_type(type_='Callable[[int, bool, str], float]', context={}, err='')
typing.Callable[[int, bool, str], float]
>>> _parse_documented_type(type_='Optional[List[Dict[str, float]]]', context={}, err='')
typing.Optional[typing.List[typing.Dict[str, float]]]
>>> _parse_documented_type(type_='Optional[List[Dict[str, float]]]', context={}, err='')
typing.Optional[typing.List[typing.Dict[str, float]]]
>>> _parse_documented_type(type_='Union[List[Dict[str, float]], None]', context={}, err='')
typing.Optional[typing.List[typing.Dict[str, float]]]
>>> _parse_documented_type(type_='Union[List[Dict[str, float]], None]', context={}, err='')
typing.Optional[typing.List[typing.Dict[str, float]]]
>>> typing.Optional[typing.List[typing.Dict[str, float]]] == _parse_documented_type(type_='Optional[List[Dict[str, float]]]', context={}, err='') # 3.13: typing.Optional[typing.List[typing.Dict[str, float]]], 3.14: typing.List[typing.Dict[str, float]] | None
True
>>> typing.Optional[typing.List[typing.Dict[str, float]]] == _parse_documented_type(type_='Optional[List[Dict[str, float]]]', context={}, err='')
True
>>> typing.Optional[typing.List[typing.Dict[str, float]]] == _parse_documented_type(type_='Union[List[Dict[str, float]], None]', context={}, err='')
True
>>> typing.Optional[typing.List[typing.Dict[str, float]]] == _parse_documented_type(type_='Union[List[Dict[str, float]], None]', context={}, err='')
True
>>> _parse_documented_type(type_='MyClass', context={}, err='')
Traceback (most recent call last):
...
Expand Down Expand Up @@ -157,8 +159,10 @@ def _update_context(context: Dict[str, Any], type_: Any) -> Dict[str, Any]:
{'str': <class 'str'>}
>>> _update_context(type_=List[List[bool]], context={})
{'bool': <class 'bool'>}
>>> _update_context(type_=Union[int, float, str], context={})
>>> _update_context(type_=Union[int, float, str], context={}) if sys.version_info < (3, 14) else {'int': int, 'float': float, 'str': str}
{'int': <class 'int'>, 'float': <class 'float'>, 'str': <class 'str'>}
>>> {'Union': Union[int, float, str]} == _update_context(type_=Union[int, float, str], context={}) if sys.version_info >= (3, 14) else True
True
>>> _update_context(type_=Callable[[int, bool, str], float], context={})
{'int': <class 'int'>, 'bool': <class 'bool'>, 'str': <class 'str'>, 'float': <class 'float'>}
>>> _update_context(type_=Optional[List[Dict[str, float]]], context={})
Expand Down
33 changes: 15 additions & 18 deletions pedantic/type_checking_logic/check_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Idea is taken from: https://stackoverflow.com/a/55504010/10975692"""
import inspect
import sys
import types
import typing
from io import BytesIO, StringIO, BufferedWriter, TextIOWrapper
Expand Down Expand Up @@ -219,9 +220,7 @@ def _is_instance(obj: Any, type_: Any, type_vars: Dict[TypeVar_, Any], context:
return isinstance(obj, type_.__supertype__)

if hasattr(obj, '_asdict'):
if hasattr(type_, '_field_types'):
field_types = type_._field_types
elif hasattr(type_, '__annotations__'):
if hasattr(type_, '__annotations__'):
field_types = type_.__annotations__
else:
return False
Expand Down Expand Up @@ -338,7 +337,8 @@ def _is_generic(cls: Any) -> bool:
return True
elif isinstance(cls, typing._SpecialForm):
return cls not in {Any}

elif cls is typing.Union or type(cls) is typing.Union: # for python >= 3.14 Union is no longer a typing._SpecialForm
return True
return False


Expand Down Expand Up @@ -404,8 +404,6 @@ def get_type_arguments(cls: Any) -> Tuple[Any, ...]:
(typing.Tuple[float, str],)
>>> get_type_arguments(List[Tuple[Any, ...]])
(typing.Tuple[typing.Any, ...],)
>>> Union[bool, int, float]
typing.Union[bool, int, float]
>>> get_type_arguments(Union[str, float, int])
(<class 'str'>, <class 'float'>, <class 'int'>)
>>> get_type_arguments(Union[str, float, List[int], int])
Expand Down Expand Up @@ -472,10 +470,10 @@ def get_base_generic(cls: Any) -> Any:
typing.Dict
>>> get_base_generic(Dict[str, str])
typing.Dict
>>> get_base_generic(Union)
typing.Union
>>> get_base_generic(Union[float, int, str])
typing.Union
>>> 'typing.Union' in str(get_base_generic(Union)) # 3.13: typing.Union 3.14: <class 'typing.Union'>
True
>>> 'typing.Union' in str(get_base_generic(Union[float, int, str])) # 3.13: typing.Union 3.14: <class 'typing.Union'>
True
>>> get_base_generic(Set)
typing.Set
>>> get_base_generic(Set[int])
Expand All @@ -491,7 +489,7 @@ def get_base_generic(cls: Any) -> Any:

if name is not None:
return getattr(typing, name)
elif origin is not None:
elif origin is not None and cls is not typing.Union:
return origin
return cls

Expand Down Expand Up @@ -537,10 +535,8 @@ def _is_subtype(sub_type: Any, super_type: Any, context: Dict[str, Any] = None)
False
>>> _is_subtype(List[int], List[Union[int, float]])
True
>>> _is_subtype(List[Union[int, float]], List[int])
Traceback (most recent call last):
...
TypeError: issubclass() arg 1 must be a class
>>> _is_subtype(List[Union[int, float]], List[int]) if sys.version_info >= (3, 14) else False
False
>>> class Parent: pass
>>> class Child(Parent): pass
>>> _is_subtype(List[Child], List[Parent])
Expand Down Expand Up @@ -740,9 +736,6 @@ def _instancecheck_tuple(tup: Tuple, type_args: Any, type_vars: Dict[TypeVar_, A
if Ellipsis in type_args:
return all(_is_instance(obj=val, type_=type_args[0], type_vars=type_vars, context=context) for val in tup)

if tup == () and type_args == ((),):
return True

if len(tup) != len(type_args):
return False

Expand Down Expand Up @@ -1033,6 +1026,8 @@ def convert_to_typing_types(x: typing.Type) -> typing.Type:
typing.Tuple[int]
>>> convert_to_typing_types(type[int])
typing.Type[int]
>>> convert_to_typing_types(type[int | float])
typing.Type[int | float]
>>> convert_to_typing_types(tuple[int, float])
typing.Tuple[int, float]
>>> convert_to_typing_types(dict[int, float])
Expand Down Expand Up @@ -1064,6 +1059,8 @@ def convert_to_typing_types(x: typing.Type) -> typing.Type:
return typing.FrozenSet[tuple(args)]
elif origin is type:
return typing.Type[tuple(args)]
elif origin is typing.Union:
return x # new since Python 3.14

raise RuntimeError(x)

Expand Down
43 changes: 15 additions & 28 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pedantic"
version = "2.2.3"
version = "2.3.0"
description = "Some useful Python decorators for cleaner software development."
readme = "README.md"
requires-python = ">=3.11"
Expand All @@ -31,8 +31,8 @@ classifiers = [
# pip install .[dev]
dev = [
"docstring-parser==0.17",
"Flask[async]==3.1.1",
"multiprocess==0.70.18",
"Flask[async]==3.1.2",
"multiprocess @ git+https://github.com/uqfoundation/multiprocess.git@02ea4bd36cac5013d70847815c92e1a736ef4a05",
"Werkzeug==3.1.3",
]

Expand Down