diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85ce5ac..d137fdb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b2e02..da6f3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/pedantic/decorators/class_decorators.py b/pedantic/decorators/class_decorators.py index fac4165..dca6cd1 100644 --- a/pedantic/decorators/class_decorators.py +++ b/pedantic/decorators/class_decorators.py @@ -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 diff --git a/pedantic/decorators/fn_deco_in_subprocess.py b/pedantic/decorators/fn_deco_in_subprocess.py index dfc285e..dd4af36 100644 --- a/pedantic/decorators/fn_deco_in_subprocess.py +++ b/pedantic/decorators/fn_deco_in_subprocess.py @@ -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 @@ -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) diff --git a/pedantic/tests/tests_pedantic.py b/pedantic/tests/tests_pedantic.py index eaa0c82..fc0d692 100644 --- a/pedantic/tests/tests_pedantic.py +++ b/pedantic/tests/tests_pedantic.py @@ -1,5 +1,4 @@ import os.path -import sys import types import typing import unittest diff --git a/pedantic/type_checking_logic/check_docstring.py b/pedantic/type_checking_logic/check_docstring.py index 535bfa2..f443c4a 100644 --- a/pedantic/type_checking_logic/check_docstring.py +++ b/pedantic/type_checking_logic/check_docstring.py @@ -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 @@ -96,18 +98,18 @@ def _parse_documented_type(type_: str, context: Dict[str, Any], err: str) -> Any >>> _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): ... @@ -157,8 +159,10 @@ def _update_context(context: Dict[str, Any], type_: Any) -> Dict[str, Any]: {'str': } >>> _update_context(type_=List[List[bool]], context={}) {'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': , 'float': , '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': , 'bool': , 'str': , 'float': } >>> _update_context(type_=Optional[List[Dict[str, float]]], context={}) diff --git a/pedantic/type_checking_logic/check_types.py b/pedantic/type_checking_logic/check_types.py index 293329c..4e2e164 100644 --- a/pedantic/type_checking_logic/check_types.py +++ b/pedantic/type_checking_logic/check_types.py @@ -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 @@ -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 @@ -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 @@ -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]) (, , ) >>> get_type_arguments(Union[str, float, List[int], int]) @@ -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: + True + >>> 'typing.Union' in str(get_base_generic(Union[float, int, str])) # 3.13: typing.Union 3.14: + True >>> get_base_generic(Set) typing.Set >>> get_base_generic(Set[int]) @@ -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 @@ -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]) @@ -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 @@ -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]) @@ -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) diff --git a/poetry.lock b/poetry.lock index 5c4d859..46cd924 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "asgiref" @@ -95,15 +95,15 @@ test = ["pytest"] [[package]] name = "flask" -version = "3.1.1" +version = "3.1.2" description = "A simple framework for building complex web applications." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"dev\"" files = [ - {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, - {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, + {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, + {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, ] [package.dependencies] @@ -225,37 +225,24 @@ files = [ [[package]] name = "multiprocess" -version = "0.70.18" +version = "0.70.19.dev0" description = "better multiprocessing and multithreading in Python" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "extra == \"dev\"" -files = [ - {file = "multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df"}, - {file = "multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213"}, - {file = "multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2"}, - {file = "multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6"}, - {file = "multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3"}, - {file = "multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797"}, - {file = "multiprocess-0.70.18-pp38-pypy38_pp73-macosx_10_9_arm64.whl", hash = "sha256:2dbaae9bffa1fb2d58077c0044ffe87a8c8974e90fcf778cdf90e139c970d42a"}, - {file = "multiprocess-0.70.18-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bcac5a4e81f1554d98d1bba963eeb1bd24966432f04fcbd29b6e1a16251ad712"}, - {file = "multiprocess-0.70.18-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c0c7cd75d0987ab6166d64e654787c781dbacbcbcaaede4c1ffe664720b3e14b"}, - {file = "multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_arm64.whl", hash = "sha256:9fd8d662f7524a95a1be7cbea271f0b33089fe792baabec17d93103d368907da"}, - {file = "multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:3fbba48bfcd932747c33f0b152b26207c4e0840c35cab359afaff7a8672b1031"}, - {file = "multiprocess-0.70.18-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5f9be0342e597dde86152c10442c5fb6c07994b1c29de441b7a3a08b0e6be2a0"}, - {file = "multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea"}, - {file = "multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d"}, - {file = "multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2"}, - {file = "multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334"}, - {file = "multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b"}, - {file = "multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8"}, - {file = "multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d"}, -] +files = [] +develop = false [package.dependencies] dill = ">=0.4.0" +[package.source] +type = "git" +url = "https://github.com/uqfoundation/multiprocess.git" +reference = "02ea4bd36cac5013d70847815c92e1a736ef4a05" +resolved_reference = "02ea4bd36cac5013d70847815c92e1a736ef4a05" + [[package]] name = "werkzeug" version = "3.1.3" @@ -281,4 +268,4 @@ dev = ["Flask", "Werkzeug", "docstring-parser", "multiprocess"] [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "4ad134bec4bf19fedfc15116e80e3366e5f7d3e1dcef6d03b01aa957ba80694c" +content-hash = "202df79b10160250c9730d41410cc6bbe13bd548baaca06da790515dd4b312e1" diff --git a/pyproject.toml b/pyproject.toml index 92ecf82..5543a81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -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", ]