diff --git a/.github/workflows/github-action-test-python.yml b/.github/workflows/github-action-test-python.yml index db70dcc9..46b34cf1 100644 --- a/.github/workflows/github-action-test-python.yml +++ b/.github/workflows/github-action-test-python.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: # You need to change to branch protection rules if you change the versions here - python-version: [3.12.1, 3.13.0] + python-version: [3.12.11, 3.13.7] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -24,5 +24,4 @@ jobs: cd python && ./allTestsForPyVersion - name: Test pytrace-generator run: | - python3 python/src/runYourProgram.py --install-mode installOnly python3 pytrace-generator/test/runTests.py diff --git a/ChangeLog.md b/ChangeLog.md index c2ecf21c..d9be4f54 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,13 @@ # Write Your Python Program - CHANGELOG +* 2.0.0 (2025-09-24) + * Remove wrappers, only check types at function enter/exit points + * Restructure directory layout + * RUN button now longer copies files to the site-lib directory +* 1.3.2 (2025-05-11) + * Fix release (version 1.3.1. did no include the latest changes) +* 1.3.1 (2025-05-10) + * Fix bug with python 3.13 and wrappers * 1.3.0 (2024-12-01) * Fix bug with union of unions * Improve scrolling #148 diff --git a/README.md b/README.md index 8cc650ae..fde41b84 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -[![Python CI](https://github.com/skogsbaer/write-your-python-program/actions/workflows/github-action-test-python.yml/badge.svg)](https://github.com/skogsbaer/write-your-python-program/actions/workflows/github-action-test-python.yml) -[![Node.js CI](https://github.com/skogsbaer/write-your-python-program/actions/workflows/github-action-test-js.yml/badge.svg)](https://github.com/skogsbaer/write-your-python-program/actions/workflows/github-action-test-js.yml) - # Write Your Python Program A user-friendly python programming environment for beginners. @@ -40,6 +37,11 @@ Run `wypp --help` for usage information. Here is the [Changelog](ChangeLog.md). +* **Breaking change** in version 2.0.0 (2025-0-24): type annotations are now only + checked when entering/exiting a function. Before, certain things such as lists + or callable were put behind wrapper objects. For example, these wrappers ensured + that only ints could be appended to a list of type `list[int]`. However, these + wrappers came with several drawbacks, so they were removed in release 2.0.0 * **Breaking change** in version 0.12.0 (2021-09-28): type annotations are now checked dynamically when the code is executed. This behavior can be deactivated in the settings of the extension. @@ -50,7 +52,10 @@ You need an explicit import statement such as `from wypp import *`. Here is a screen shot: -![Screenshot](screenshot.jpg) +![Screenshot](screenshot.png) + +There is also a visualization mode, similar to [Python Tutor](https://pythontutor.com/): +![Screenshot](screenshot2.png) When hitting the RUN button, the vscode extension saves the current file, opens a terminal and executes the file with Python, staying in interactive mode after @@ -59,7 +64,7 @@ all definitions have been executed. The file being executed should contain the following import statement in the first line: ~~~python -from wypp import* +from wypp import * ~~~ Running the file with the RUN button makes the following features available: @@ -193,39 +198,6 @@ before the type being defined, for example to define recursive types or as the type of `self` inside of classes. In fact, there is no check at all to make sure that anotations refer to existing types. -For builtin `list[T]` the following operations are typechecked: -- `list[idx]` -- `list[idx] = value` -- `list += [...]` -- `list.append(value)` -- `list.insert(idx, value)` -- `list.extend(iterator)` -- `for i in list:` (Iterator) - -For builtin `set[T]` these operations are typechecked: -- `set.add(value)` -- `set.pop()` -- `set.remove(value)` Value must be of `T` -- `set.update(other, ...)` -- `value in set` Value must be of `T` -- `for i in set:` (Iterator) - -For builtin `dict[K,V]` the supported typechecked operations are: -- `dict.get(key)` -- `dict.items()` -- `dict.keys()` -- `dict.pop()` -- `dict.popitem()` -- `dict.setdefault(key, default)`
_Note:_ In contrast to the standard library `default` is required, to avoid inserting `None` as value into otherwise typed dicts. -- `dict.update(other)` -- `dict.update(key=value, ...)` -- `dict.values()` -- `key in dict` Key must be of `K` -- `del dict[key]` -- `for k in dict` (Iterator) -- `reversed(dict)` -- `dict[key]` -- `dict[key] = value` ## Module name and current working directory diff --git a/package.json b/package.json index 39112370..195b628a 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "displayName": "Write Your Python Program!", "description": "A user friendly python environment for beginners", "license": "See license in LICENSE", - "version": "1.3.0", + "version": "2.0.0", "publisher": "StefanWehr", "icon": "icon.png", "engines": { - "vscode": "^1.85.0" + "vscode": "^1.94.2" }, "keywords": [ "Python", diff --git a/python/.ignore b/python/.ignore deleted file mode 100644 index eed76414..00000000 --- a/python/.ignore +++ /dev/null @@ -1,2 +0,0 @@ -site-lib/wypp -site-lib/untypy diff --git a/python/.vscode/settings.json b/python/.vscode/settings.json new file mode 100644 index 00000000..1e488efe --- /dev/null +++ b/python/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.analysis.diagnosticMode": "workspace", + "python.autoComplete.extraPaths": [ + "/Users/swehr/.vscode/extensions/stefanwehr.write-your-python-program-1.3.2/python/src/" + ] +} diff --git a/python/TODO.org b/python/TODO.org new file mode 100644 index 00000000..6a6a06d1 --- /dev/null +++ b/python/TODO.org @@ -0,0 +1,2 @@ +* Code duplication +- wrappedclass.py, protocol.py and typedfunction.py diff --git a/python/TODO_nowrappers.md b/python/TODO_nowrappers.md new file mode 100644 index 00000000..d5352319 --- /dev/null +++ b/python/TODO_nowrappers.md @@ -0,0 +1,4 @@ +* Test windows +* location matcher for vscode +* show "@record\nclass C" for record attributes + diff --git a/python/allTests b/python/allTests index 3837a1e3..1128200f 100755 --- a/python/allTests +++ b/python/allTests @@ -15,5 +15,5 @@ function run() echo } -PYENV_VERSION=3.12.1 run -PYENV_VERSION=3.13.0 run +PYENV_VERSION=3.12 run +PYENV_VERSION=3.13 run diff --git a/python/allTestsForPyVersion b/python/allTestsForPyVersion index fac2ba23..52d27349 100755 --- a/python/allTestsForPyVersion +++ b/python/allTestsForPyVersion @@ -5,16 +5,8 @@ set -u cd $(dirname $0) -unit_test_path=src:tests:deps/untypy - -function prepare_integration_tests() -{ - echo "Preparing integration tests by install the WYPP library" - local d=$(mktemp -d) - trap "rm -rf $d" EXIT - WYPP_INSTALL_DIR=$d python3 src/runYourProgram.py --install-mode installOnly - integ_test_path=integration-tests:$d -} +unit_test_path=code/wypp:tests:code +integration_test_path=code/wypp:integration-tests:code function usage() { @@ -22,47 +14,52 @@ function usage() exit 1 } -if [ -z "${1:-}" ]; then - echo "Python version:" - python3 --version - echo "Running untypy tests" - pushd deps/untypy > /dev/null - ./runtests.sh || exit 1 - popd > /dev/null - echo "Done with untypy tests" - echo "Running all unit tests, PYTHONPATH=$unit_test_path" - PYTHONPATH=$unit_test_path python3 -m unittest tests/test*.py - echo "Done with unit tests" - echo - prepare_integration_tests - echo "Running all integration tests, PYTHONPATH=$integ_test_path" - PYTHONPATH=$integ_test_path python3 -m unittest integration-tests/test*.py - echo "Done with integration tests" -else - if [ "$1" == "--unit" ]; then - what="unit" - dir=tests - p=$unit_test_path - elif [ "$1" == "--integration" ]; then - what="integration" - dir=integration-tests - prepare_integration_tests - p=$integ_test_path +function run_unit_tests() +{ + echo "Running unit tests, PYTHONPATH=$unit_test_path" + if [ -z "${1:-}" ]; then + PYTHONPATH=$unit_test_path python3 -m unittest tests/test*.py + ecode=$? else - usage + PYTHONPATH=$unit_test_path python3 -m unittest "$@" + ecode=$? fi - shift - echo "Running $what tests $@ with PYTHONPATH=$p" + echo "Done with unit tests" +} + +function run_integration_tests() +{ + echo "Running all integration tests, PYTHONPATH=$integration_test_path" if [ -z "${1:-}" ]; then - PYTHONPATH=$p python3 -m unittest $dir/test*.py + PYTHONPATH=$integration_test_path python3 -m unittest integration-tests/test*.py ecode=$? else - PYTHONPATH=$p python3 -m unittest "$@" + PYTHONPATH=$integration_test_path python3 -m unittest "$@" ecode=$? fi - echo "$what tests finished with exit code $ecode" + echo "Done with integration tests" +} + + +echo "Python version:" +python3 --version + +if [ -z "${1:-}" ]; then + run_unit_tests + echo + run_integration_tests + echo + echo "Running file tests ..." + python3 ./fileTests.py +elif [ "$1" == "--unit" ]; then + shift + run_unit_tests "$@" exit $ecode +elif [ "$1" == "--integration" ]; then + shift + run_integration_tests "$@" + exit $ecode +else + usage fi -echo "Running file tests ..." -./fileTests diff --git a/python/code/typeguard/__init__.py b/python/code/typeguard/__init__.py new file mode 100644 index 00000000..6781cad0 --- /dev/null +++ b/python/code/typeguard/__init__.py @@ -0,0 +1,48 @@ +import os +from typing import Any + +from ._checkers import TypeCheckerCallable as TypeCheckerCallable +from ._checkers import TypeCheckLookupCallback as TypeCheckLookupCallback +from ._checkers import check_type_internal as check_type_internal +from ._checkers import checker_lookup_functions as checker_lookup_functions +from ._checkers import load_plugins as load_plugins +from ._config import CollectionCheckStrategy as CollectionCheckStrategy +from ._config import ForwardRefPolicy as ForwardRefPolicy +from ._config import TypeCheckConfiguration as TypeCheckConfiguration +from ._decorators import typechecked as typechecked +from ._decorators import typeguard_ignore as typeguard_ignore +from ._exceptions import InstrumentationWarning as InstrumentationWarning +from ._exceptions import TypeCheckError as TypeCheckError +from ._exceptions import TypeCheckWarning as TypeCheckWarning +from ._exceptions import TypeHintWarning as TypeHintWarning +from ._functions import TypeCheckFailCallback as TypeCheckFailCallback +from ._functions import check_type as check_type +from ._functions import warn_on_error as warn_on_error +from ._importhook import ImportHookManager as ImportHookManager +from ._importhook import TypeguardFinder as TypeguardFinder +from ._importhook import install_import_hook as install_import_hook +from ._memo import TypeCheckMemo as TypeCheckMemo +from ._suppression import suppress_type_checks as suppress_type_checks +from ._utils import Unset as Unset + +# Re-export imports so they look like they live directly in this package +for value in list(locals().values()): + if getattr(value, "__module__", "").startswith(f"{__name__}."): + value.__module__ = __name__ + + +config: TypeCheckConfiguration + + +def __getattr__(name: str) -> Any: + if name == "config": + from ._config import global_config + + return global_config + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +# Automatically load checker lookup functions unless explicitly disabled +if "TYPEGUARD_DISABLE_PLUGIN_AUTOLOAD" not in os.environ: + load_plugins() diff --git a/python/code/typeguard/_checkers.py b/python/code/typeguard/_checkers.py new file mode 100644 index 00000000..d3875f7a --- /dev/null +++ b/python/code/typeguard/_checkers.py @@ -0,0 +1,1094 @@ +from __future__ import annotations + +import collections.abc +import inspect +import sys +import types +import typing +import warnings +from collections.abc import Mapping, MutableMapping, Sequence +from enum import Enum +from inspect import Parameter, isclass, isfunction +from io import BufferedIOBase, IOBase, RawIOBase, TextIOBase +from itertools import zip_longest +from textwrap import indent +from typing import ( + IO, + AbstractSet, + Annotated, + Any, + BinaryIO, + Callable, + Dict, + ForwardRef, + List, + NewType, + Optional, + Set, + TextIO, + Tuple, + Type, + TypeVar, + Union, +) +from unittest.mock import Mock + +import typing_extensions + +# Must use this because typing.is_typeddict does not recognize +# TypedDict from typing_extensions, and as of version 4.12.0 +# typing_extensions.TypedDict is different from typing.TypedDict +# on all versions. +from typing_extensions import is_typeddict + +from ._config import ForwardRefPolicy +from ._exceptions import TypeCheckError, TypeHintWarning +from ._memo import TypeCheckMemo +from ._utils import evaluate_forwardref, get_stacklevel, get_type_name, qualified_name + +if sys.version_info >= (3, 11): + from typing import ( + NotRequired, + TypeAlias, + get_args, + get_origin, + ) + + SubclassableAny = Any +else: + from typing_extensions import Any as SubclassableAny + from typing_extensions import ( + NotRequired, + TypeAlias, + get_args, + get_origin, + ) + +if sys.version_info >= (3, 10): + from importlib.metadata import entry_points + from typing import ParamSpec +else: + from importlib_metadata import entry_points + from typing_extensions import ParamSpec + +TypeCheckerCallable: TypeAlias = Callable[ + [Any, Any, Tuple[Any, ...], TypeCheckMemo], Any +] +TypeCheckLookupCallback: TypeAlias = Callable[ + [Any, Tuple[Any, ...], Tuple[Any, ...]], Optional[TypeCheckerCallable] +] + +checker_lookup_functions: list[TypeCheckLookupCallback] = [] +generic_alias_types: tuple[type, ...] = ( + type(List), + type(List[Any]), + types.GenericAlias, +) + +# Sentinel +_missing = object() + +# Lifted from mypy.sharedparse +BINARY_MAGIC_METHODS = { + "__add__", + "__and__", + "__cmp__", + "__divmod__", + "__div__", + "__eq__", + "__floordiv__", + "__ge__", + "__gt__", + "__iadd__", + "__iand__", + "__idiv__", + "__ifloordiv__", + "__ilshift__", + "__imatmul__", + "__imod__", + "__imul__", + "__ior__", + "__ipow__", + "__irshift__", + "__isub__", + "__itruediv__", + "__ixor__", + "__le__", + "__lshift__", + "__lt__", + "__matmul__", + "__mod__", + "__mul__", + "__ne__", + "__or__", + "__pow__", + "__radd__", + "__rand__", + "__rdiv__", + "__rfloordiv__", + "__rlshift__", + "__rmatmul__", + "__rmod__", + "__rmul__", + "__ror__", + "__rpow__", + "__rrshift__", + "__rshift__", + "__rsub__", + "__rtruediv__", + "__rxor__", + "__sub__", + "__truediv__", + "__xor__", +} + + +def check_callable( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not callable(value): + raise TypeCheckError("is not callable") + + if args: + try: + signature = inspect.signature(value) + except (TypeError, ValueError): + return + + argument_types = args[0] + if isinstance(argument_types, list) and not any( + type(item) is ParamSpec for item in argument_types + ): + # The callable must not have keyword-only arguments without defaults + unfulfilled_kwonlyargs = [ + param.name + for param in signature.parameters.values() + if param.kind == Parameter.KEYWORD_ONLY + and param.default == Parameter.empty + ] + if unfulfilled_kwonlyargs: + raise TypeCheckError( + f"has mandatory keyword-only arguments in its declaration: " + f'{", ".join(unfulfilled_kwonlyargs)}' + ) + + num_positional_args = num_mandatory_pos_args = 0 + has_varargs = False + for param in signature.parameters.values(): + if param.kind in ( + Parameter.POSITIONAL_ONLY, + Parameter.POSITIONAL_OR_KEYWORD, + ): + num_positional_args += 1 + if param.default is Parameter.empty: + num_mandatory_pos_args += 1 + elif param.kind == Parameter.VAR_POSITIONAL: + has_varargs = True + + if num_mandatory_pos_args > len(argument_types): + raise TypeCheckError( + f"has too many mandatory positional arguments in its declaration; " + f"expected {len(argument_types)} but {num_mandatory_pos_args} " + f"mandatory positional argument(s) declared" + ) + elif not has_varargs and num_positional_args < len(argument_types): + raise TypeCheckError( + f"has too few arguments in its declaration; expected " + f"{len(argument_types)} but {num_positional_args} argument(s) " + f"declared" + ) + + +def check_mapping( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is Dict or origin_type is dict: + if not isinstance(value, dict): + raise TypeCheckError("is not a dict") + if origin_type is MutableMapping or origin_type is collections.abc.MutableMapping: + if not isinstance(value, collections.abc.MutableMapping): + raise TypeCheckError("is not a mutable mapping") + elif not isinstance(value, collections.abc.Mapping): + raise TypeCheckError("is not a mapping") + + if args: + key_type, value_type = args + if key_type is not Any or value_type is not Any: + samples = memo.config.collection_check_strategy.iterate_samples( + value.items() + ) + for k, v in samples: + try: + check_type_internal(k, key_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"key {k!r}") + raise + + try: + check_type_internal(v, value_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"value of key {k!r}") + raise + + +def check_typed_dict( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, dict): + raise TypeCheckError("is not a dict") + + declared_keys = frozenset(origin_type.__annotations__) + if hasattr(origin_type, "__required_keys__"): + required_keys = set(origin_type.__required_keys__) + else: # py3.8 and lower + required_keys = set(declared_keys) if origin_type.__total__ else set() + + existing_keys = set(value) + extra_keys = existing_keys - declared_keys + if extra_keys: + keys_formatted = ", ".join(f'"{key}"' for key in sorted(extra_keys, key=repr)) + raise TypeCheckError(f"has unexpected extra key(s): {keys_formatted}") + + # Detect NotRequired fields which are hidden by get_type_hints() + type_hints: dict[str, type] = {} + for key, annotation in origin_type.__annotations__.items(): + if isinstance(annotation, ForwardRef): + annotation = evaluate_forwardref(annotation, memo) + + if get_origin(annotation) is NotRequired: + required_keys.discard(key) + annotation = get_args(annotation)[0] + + type_hints[key] = annotation + + missing_keys = required_keys - existing_keys + if missing_keys: + keys_formatted = ", ".join(f'"{key}"' for key in sorted(missing_keys, key=repr)) + raise TypeCheckError(f"is missing required key(s): {keys_formatted}") + + for key, argtype in type_hints.items(): + argvalue = value.get(key, _missing) + if argvalue is not _missing: + try: + check_type_internal(argvalue, argtype, memo) + except TypeCheckError as exc: + exc.append_path_element(f"value of key {key!r}") + raise + + +def check_list( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, list): + raise TypeCheckError("is not a list") + + if args and args != (Any,): + samples = memo.config.collection_check_strategy.iterate_samples(value) + for i, v in enumerate(samples): + try: + check_type_internal(v, args[0], memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + + +def check_sequence( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, collections.abc.Sequence): + raise TypeCheckError("is not a sequence") + + if args and args != (Any,): + samples = memo.config.collection_check_strategy.iterate_samples(value) + for i, v in enumerate(samples): + try: + check_type_internal(v, args[0], memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + + +def check_set( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is frozenset: + if not isinstance(value, frozenset): + raise TypeCheckError("is not a frozenset") + elif not isinstance(value, AbstractSet): + raise TypeCheckError("is not a set") + + if args and args != (Any,): + samples = memo.config.collection_check_strategy.iterate_samples(value) + for v in samples: + try: + check_type_internal(v, args[0], memo) + except TypeCheckError as exc: + exc.append_path_element(f"[{v}]") + raise + + +def check_tuple( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + # Specialized check for NamedTuples + if field_types := getattr(origin_type, "__annotations__", None): + if not isinstance(value, origin_type): + raise TypeCheckError( + f"is not a named tuple of type {qualified_name(origin_type)}" + ) + + for name, field_type in field_types.items(): + try: + check_type_internal(getattr(value, name), field_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"attribute {name!r}") + raise + + return + elif not isinstance(value, tuple): + raise TypeCheckError("is not a tuple") + + if args: + use_ellipsis = args[-1] is Ellipsis + tuple_params = args[: -1 if use_ellipsis else None] + else: + # Unparametrized Tuple or plain tuple + return + + if use_ellipsis: + element_type = tuple_params[0] + samples = memo.config.collection_check_strategy.iterate_samples(value) + for i, element in enumerate(samples): + try: + check_type_internal(element, element_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + elif tuple_params == ((),): + if value != (): + raise TypeCheckError("is not an empty tuple") + else: + if len(value) != len(tuple_params): + raise TypeCheckError( + f"has wrong number of elements (expected {len(tuple_params)}, got " + f"{len(value)} instead)" + ) + + for i, (element, element_type) in enumerate(zip(value, tuple_params)): + try: + check_type_internal(element, element_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + + +def check_union( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + errors: dict[str, TypeCheckError] = {} + try: + for type_ in args: + try: + check_type_internal(value, type_, memo) + return + except TypeCheckError as exc: + errors[get_type_name(type_)] = exc + + formatted_errors = indent( + "\n".join(f"{key}: {error}" for key, error in errors.items()), " " + ) + finally: + del errors # avoid creating ref cycle + + raise TypeCheckError(f"did not match any element in the union:\n{formatted_errors}") + + +def check_uniontype( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not args: + return check_instance(value, types.UnionType, (), memo) + + errors: dict[str, TypeCheckError] = {} + try: + for type_ in args: + try: + check_type_internal(value, type_, memo) + return + except TypeCheckError as exc: + errors[get_type_name(type_)] = exc + + formatted_errors = indent( + "\n".join(f"{key}: {error}" for key, error in errors.items()), " " + ) + finally: + del errors # avoid creating ref cycle + + raise TypeCheckError(f"did not match any element in the union:\n{formatted_errors}") + + +def check_class( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isclass(value) and not isinstance(value, generic_alias_types): + raise TypeCheckError("is not a class") + + if not args: + return + + if isinstance(args[0], ForwardRef): + expected_class = evaluate_forwardref(args[0], memo) + else: + expected_class = args[0] + + if expected_class is Any: + return + elif expected_class is typing_extensions.Self: + check_self(value, get_origin(expected_class), get_args(expected_class), memo) + elif getattr(expected_class, "_is_protocol", False): + check_protocol(value, expected_class, (), memo) + elif isinstance(expected_class, TypeVar): + check_typevar(value, expected_class, (), memo, subclass_check=True) + elif get_origin(expected_class) is Union: + errors: dict[str, TypeCheckError] = {} + try: + for arg in get_args(expected_class): + if arg is Any: + return + + try: + check_class(value, type, (arg,), memo) + return + except TypeCheckError as exc: + errors[get_type_name(arg)] = exc + else: + formatted_errors = indent( + "\n".join(f"{key}: {error}" for key, error in errors.items()), " " + ) + raise TypeCheckError( + f"did not match any element in the union:\n{formatted_errors}" + ) + finally: + del errors # avoid creating ref cycle + elif not issubclass(value, expected_class): # type: ignore[arg-type] + raise TypeCheckError(f"is not a subclass of {qualified_name(expected_class)}") + + +def check_newtype( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + check_type_internal(value, origin_type.__supertype__, memo) + + +def check_instance( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, origin_type): + raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}") + + +def check_typevar( + value: Any, + origin_type: TypeVar, + args: tuple[Any, ...], + memo: TypeCheckMemo, + *, + subclass_check: bool = False, +) -> None: + if origin_type.__bound__ is not None: + annotation = ( + type[origin_type.__bound__] if subclass_check else origin_type.__bound__ + ) + check_type_internal(value, annotation, memo) + elif origin_type.__constraints__: + for constraint in origin_type.__constraints__: + annotation = Type[constraint] if subclass_check else constraint + try: + check_type_internal(value, annotation, memo) + except TypeCheckError: + pass + else: + break + else: + formatted_constraints = ", ".join( + get_type_name(constraint) for constraint in origin_type.__constraints__ + ) + raise TypeCheckError( + f"does not match any of the constraints " f"({formatted_constraints})" + ) + + +def _is_literal_type(typ: object) -> bool: + return typ is typing.Literal or typ is typing_extensions.Literal + + +def check_literal( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + def get_literal_args(literal_args: tuple[Any, ...]) -> tuple[Any, ...]: + retval: list[Any] = [] + for arg in literal_args: + if _is_literal_type(get_origin(arg)): + retval.extend(get_literal_args(arg.__args__)) + elif arg is None or isinstance(arg, (int, str, bytes, bool, Enum)): + retval.append(arg) + else: + raise TypeError( + f"Illegal literal value: {arg}" + ) # TypeError here is deliberate + + return tuple(retval) + + final_args = tuple(get_literal_args(args)) + try: + index = final_args.index(value) + except ValueError: + pass + else: + if type(final_args[index]) is type(value): + return + + formatted_args = ", ".join(repr(arg) for arg in final_args) + raise TypeCheckError(f"is not any of ({formatted_args})") from None + + +def check_literal_string( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + check_type_internal(value, str, memo) + + +def check_typeguard( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + check_type_internal(value, bool, memo) + + +def check_none( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if value is not None: + raise TypeCheckError("is not None") + + +def check_number( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is complex and not isinstance(value, (complex, float, int)): + raise TypeCheckError("is neither complex, float or int") + elif origin_type is float and not isinstance(value, (float, int)): + raise TypeCheckError("is neither float or int") + + +def check_io( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is TextIO or (origin_type is IO and args == (str,)): + if not isinstance(value, TextIOBase): + raise TypeCheckError("is not a text based I/O object") + elif origin_type is BinaryIO or (origin_type is IO and args == (bytes,)): + if not isinstance(value, (RawIOBase, BufferedIOBase)): + raise TypeCheckError("is not a binary I/O object") + elif not isinstance(value, IOBase): + raise TypeCheckError("is not an I/O object") + + +def check_signature_compatible(subject: type, protocol: type, attrname: str) -> None: + subject_attr = getattr(subject, attrname) + try: + subject_sig = inspect.signature(subject_attr) + except ValueError: + return # this can happen with builtins where the signature cannot be retrieved + + protocol_sig = inspect.signature(getattr(protocol, attrname)) + protocol_type: typing.Literal["instance", "class", "static"] = "instance" + subject_type: typing.Literal["instance", "class", "static"] = "instance" + + # Check if the protocol-side method is a class method or static method + if attrname in protocol.__dict__: + descriptor = protocol.__dict__[attrname] + if isinstance(descriptor, staticmethod): + protocol_type = "static" + elif isinstance(descriptor, classmethod): + protocol_type = "class" + + # Check if the subject-side method is a class method or static method + if attrname in subject.__dict__: + descriptor = subject.__dict__[attrname] + if isinstance(descriptor, staticmethod): + subject_type = "static" + elif isinstance(descriptor, classmethod): + subject_type = "class" + + if protocol_type == "instance" and subject_type != "instance": + raise TypeCheckError( + f"should be an instance method but it's a {subject_type} method" + ) + elif protocol_type != "instance" and subject_type == "instance": + raise TypeCheckError( + f"should be a {protocol_type} method but it's an instance method" + ) + + expected_varargs = any( + param + for param in protocol_sig.parameters.values() + if param.kind is Parameter.VAR_POSITIONAL + ) + has_varargs = any( + param + for param in subject_sig.parameters.values() + if param.kind is Parameter.VAR_POSITIONAL + ) + if expected_varargs and not has_varargs: + raise TypeCheckError("should accept variable positional arguments but doesn't") + + protocol_has_varkwargs = any( + param + for param in protocol_sig.parameters.values() + if param.kind is Parameter.VAR_KEYWORD + ) + subject_has_varkwargs = any( + param + for param in subject_sig.parameters.values() + if param.kind is Parameter.VAR_KEYWORD + ) + if protocol_has_varkwargs and not subject_has_varkwargs: + raise TypeCheckError("should accept variable keyword arguments but doesn't") + + # Check that the callable has at least the expect amount of positional-only + # arguments (and no extra positional-only arguments without default values) + if not has_varargs: + protocol_args = [ + param + for param in protocol_sig.parameters.values() + if param.kind + in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + ] + subject_args = [ + param + for param in subject_sig.parameters.values() + if param.kind + in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + ] + + # Remove the "self" parameter from the protocol arguments to match + if protocol_type == "instance": + protocol_args.pop(0) + + # Remove the "self" parameter from the subject arguments to match + if subject_type == "instance": + subject_args.pop(0) + + for protocol_arg, subject_arg in zip_longest(protocol_args, subject_args): + if protocol_arg is None: + if subject_arg.default is Parameter.empty: + raise TypeCheckError("has too many mandatory positional arguments") + + break + + if subject_arg is None: + raise TypeCheckError("has too few positional arguments") + + if ( + protocol_arg.kind is Parameter.POSITIONAL_OR_KEYWORD + and subject_arg.kind is Parameter.POSITIONAL_ONLY + ): + raise TypeCheckError( + f"has an argument ({subject_arg.name}) that should not be " + f"positional-only" + ) + + if ( + protocol_arg.kind is Parameter.POSITIONAL_OR_KEYWORD + and protocol_arg.name != subject_arg.name + ): + raise TypeCheckError( + f"has a positional argument ({subject_arg.name}) that should be " + f"named {protocol_arg.name!r} at this position" + ) + + protocol_kwonlyargs = { + param.name: param + for param in protocol_sig.parameters.values() + if param.kind is Parameter.KEYWORD_ONLY + } + subject_kwonlyargs = { + param.name: param + for param in subject_sig.parameters.values() + if param.kind is Parameter.KEYWORD_ONLY + } + if not subject_has_varkwargs: + # Check that the signature has at least the required keyword-only arguments, and + # no extra mandatory keyword-only arguments + if missing_kwonlyargs := [ + param.name + for param in protocol_kwonlyargs.values() + if param.name not in subject_kwonlyargs + ]: + raise TypeCheckError( + "is missing keyword-only arguments: " + ", ".join(missing_kwonlyargs) + ) + + if not protocol_has_varkwargs: + if extra_kwonlyargs := [ + param.name + for param in subject_kwonlyargs.values() + if param.default is Parameter.empty + and param.name not in protocol_kwonlyargs + ]: + raise TypeCheckError( + "has mandatory keyword-only arguments not present in the protocol: " + + ", ".join(extra_kwonlyargs) + ) + + +def check_protocol( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + origin_annotations = typing.get_type_hints(origin_type) + for attrname in sorted(typing_extensions.get_protocol_members(origin_type)): + if (annotation := origin_annotations.get(attrname)) is not None: + try: + subject_member = getattr(value, attrname) + except AttributeError: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because it has no attribute named {attrname!r}" + ) from None + + try: + check_type_internal(subject_member, annotation, memo) + except TypeCheckError as exc: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because its {attrname!r} attribute {exc}" + ) from None + elif callable(getattr(origin_type, attrname)): + try: + subject_member = getattr(value, attrname) + except AttributeError: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because it has no method named {attrname!r}" + ) from None + + if not callable(subject_member): + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because its {attrname!r} attribute is not a callable" + ) + + # TODO: implement assignability checks for parameter and return value + # annotations + subject = value if isclass(value) else value.__class__ + try: + check_signature_compatible(subject, origin_type, attrname) + except TypeCheckError as exc: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because its {attrname!r} method {exc}" + ) from None + + +def check_byteslike( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, (bytearray, bytes, memoryview)): + raise TypeCheckError("is not bytes-like") + + +def check_self( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if memo.self_type is None: + raise TypeCheckError("cannot be checked against Self outside of a method call") + + if isclass(value): + if not issubclass(value, memo.self_type): + raise TypeCheckError( + f"is not a subclass of the self type ({qualified_name(memo.self_type)})" + ) + elif not isinstance(value, memo.self_type): + raise TypeCheckError( + f"is not an instance of the self type ({qualified_name(memo.self_type)})" + ) + + +def check_paramspec( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + pass # No-op for now + +def resolve_annotation_str(s: str, globalns: dict, localns: dict): + Tmp = type("_Tmp", (), {"__annotations__": {"x": s}}) + return typing.get_type_hints(Tmp, globalns=globalns, localns=localns)["x"] + +def resolve_alias_chains(tp: Any): + # 1) Follow PEP 695 alias chains (type My = ... -> TypeAliasType) + seen = set() + t = type(tp) + while isinstance(tp, typing.TypeAliasType) and id(tp) not in seen: + seen.add(id(tp)) + tp = tp.__value__ # evaluated lazily in its defining module + return tp + +def check_type_internal( + value: Any, + annotation: Any, + memo: TypeCheckMemo, +) -> None: + """ + Check that the given object is compatible with the given type annotation. + + This function should only be used by type checker callables. Applications should use + :func:`~.check_type` instead. + + :param value: the value to check + :param annotation: the type annotation to check against + :param memo: a memo object containing configuration and information necessary for + looking up forward references + """ + annotation = resolve_alias_chains(annotation) + + if isinstance(annotation, ForwardRef): + try: + annotation = evaluate_forwardref(annotation, memo) + except NameError: + if memo.config.forward_ref_policy is ForwardRefPolicy.ERROR: + raise + elif memo.config.forward_ref_policy is ForwardRefPolicy.WARN: + warnings.warn( + f"Cannot resolve forward reference {annotation.__forward_arg__!r}", + TypeHintWarning, + stacklevel=get_stacklevel(), + ) + return + if isinstance(annotation, str): + annotation = resolve_annotation_str(annotation, memo.globals, memo.locals) + + if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock): + return + + # Skip type checks if value is an instance of a class that inherits from Any + if not isclass(value) and SubclassableAny in type(value).__bases__: + return + + extras: tuple[Any, ...] + origin_type = get_origin(annotation) + if origin_type is Annotated: + annotation, *extras_ = get_args(annotation) + extras = tuple(extras_) + origin_type = get_origin(annotation) + else: + extras = () + + if origin_type is not None: + args = get_args(annotation) + + # Compatibility hack to distinguish between unparametrized and empty tuple + # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137 + if origin_type in (tuple, Tuple) and annotation is not Tuple and not args: + args = ((),) + else: + origin_type = annotation + args = () + + for lookup_func in checker_lookup_functions: + checker = lookup_func(origin_type, args, extras) + if checker: + checker(value, origin_type, args, memo) + return + + if isclass(origin_type): + if not isinstance(value, origin_type): + raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}") + elif type(origin_type) is str: # noqa: E721 + warnings.warn( + f"Skipping type check against {origin_type!r}; this looks like a " + f"string-form forward reference imported from another module", + TypeHintWarning, + stacklevel=get_stacklevel(), + ) + + +# Equality checks are applied to these +origin_type_checkers = { + bytes: check_byteslike, + AbstractSet: check_set, + BinaryIO: check_io, + Callable: check_callable, + collections.abc.Callable: check_callable, + complex: check_number, + dict: check_mapping, + Dict: check_mapping, + float: check_number, + frozenset: check_set, + IO: check_io, + list: check_list, + List: check_list, + typing.Literal: check_literal, + Mapping: check_mapping, + MutableMapping: check_mapping, + None: check_none, + collections.abc.Mapping: check_mapping, + collections.abc.MutableMapping: check_mapping, + Sequence: check_sequence, + collections.abc.Sequence: check_sequence, + collections.abc.Set: check_set, + set: check_set, + Set: check_set, + TextIO: check_io, + tuple: check_tuple, + Tuple: check_tuple, + type: check_class, + Type: check_class, + Union: check_union, + # On some versions of Python, these may simply be re-exports from "typing", + # but exactly which Python versions is subject to change. + # It's best to err on the safe side and just always specify these. + typing_extensions.Literal: check_literal, + typing_extensions.LiteralString: check_literal_string, + typing_extensions.Self: check_self, + typing_extensions.TypeGuard: check_typeguard, +} +if sys.version_info >= (3, 10): + origin_type_checkers[types.UnionType] = check_uniontype + origin_type_checkers[typing.TypeGuard] = check_typeguard +if sys.version_info >= (3, 11): + origin_type_checkers.update( + {typing.LiteralString: check_literal_string, typing.Self: check_self} + ) + + +def builtin_checker_lookup( + origin_type: Any, args: tuple[Any, ...], extras: tuple[Any, ...] +) -> TypeCheckerCallable | None: + checker = origin_type_checkers.get(origin_type) + if checker is not None: + return checker + elif is_typeddict(origin_type): + return check_typed_dict + elif isclass(origin_type) and issubclass( + origin_type, + Tuple, # type: ignore[arg-type] + ): + # NamedTuple + return check_tuple + elif getattr(origin_type, "_is_protocol", False): + return check_protocol + elif isinstance(origin_type, ParamSpec): + return check_paramspec + elif isinstance(origin_type, TypeVar): + return check_typevar + elif origin_type.__class__ is NewType: + # typing.NewType on Python 3.10+ + return check_newtype + elif ( + isfunction(origin_type) + and getattr(origin_type, "__module__", None) == "typing" + and getattr(origin_type, "__qualname__", "").startswith("NewType.") + and hasattr(origin_type, "__supertype__") + ): + # typing.NewType on Python 3.9 + return check_newtype + + return None + + +checker_lookup_functions.append(builtin_checker_lookup) + + +def load_plugins() -> None: + """ + Load all type checker lookup functions from entry points. + + All entry points from the ``typeguard.checker_lookup`` group are loaded, and the + returned lookup functions are added to :data:`typeguard.checker_lookup_functions`. + + .. note:: This function is called implicitly on import, unless the + ``TYPEGUARD_DISABLE_PLUGIN_AUTOLOAD`` environment variable is present. + """ + + for ep in entry_points(group="typeguard.checker_lookup"): + try: + plugin = ep.load() + except Exception as exc: + warnings.warn( + f"Failed to load plugin {ep.name!r}: " f"{qualified_name(exc)}: {exc}", + stacklevel=2, + ) + continue + + if not callable(plugin): + warnings.warn( + f"Plugin {ep} returned a non-callable object: {plugin!r}", stacklevel=2 + ) + continue + + checker_lookup_functions.insert(0, plugin) diff --git a/python/code/typeguard/_config.py b/python/code/typeguard/_config.py new file mode 100644 index 00000000..c3097640 --- /dev/null +++ b/python/code/typeguard/_config.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import Enum, auto +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from ._functions import TypeCheckFailCallback + +T = TypeVar("T") + + +class ForwardRefPolicy(Enum): + """ + Defines how unresolved forward references are handled. + + Members: + + * ``ERROR``: propagate the :exc:`NameError` when the forward reference lookup fails + * ``WARN``: emit a :class:`~.TypeHintWarning` if the forward reference lookup fails + * ``IGNORE``: silently skip checks for unresolveable forward references + """ + + ERROR = auto() + WARN = auto() + IGNORE = auto() + + +class CollectionCheckStrategy(Enum): + """ + Specifies how thoroughly the contents of collections are type checked. + + This has an effect on the following built-in checkers: + + * ``AbstractSet`` + * ``Dict`` + * ``List`` + * ``Mapping`` + * ``Set`` + * ``Tuple[, ...]`` (arbitrarily sized tuples) + + Members: + + * ``FIRST_ITEM``: check only the first item + * ``ALL_ITEMS``: check all items + """ + + FIRST_ITEM = auto() + ALL_ITEMS = auto() + + def iterate_samples(self, collection: Iterable[T]) -> Iterable[T]: + if self is CollectionCheckStrategy.FIRST_ITEM: + try: + return [next(iter(collection))] + except StopIteration: + return () + else: + return collection + + +@dataclass +class TypeCheckConfiguration: + """ + You can change Typeguard's behavior with these settings. + + .. attribute:: typecheck_fail_callback + :type: Callable[[TypeCheckError, TypeCheckMemo], Any] + + Callable that is called when type checking fails. + + Default: ``None`` (the :exc:`~.TypeCheckError` is raised directly) + + .. attribute:: forward_ref_policy + :type: ForwardRefPolicy + + Specifies what to do when a forward reference fails to resolve. + + Default: ``WARN`` + + .. attribute:: collection_check_strategy + :type: CollectionCheckStrategy + + Specifies how thoroughly the contents of collections (list, dict, etc.) are + type checked. + + Default: ``FIRST_ITEM`` + + .. attribute:: debug_instrumentation + :type: bool + + If set to ``True``, the code of modules or functions instrumented by typeguard + is printed to ``sys.stderr`` after the instrumentation is done + + Default: ``False`` + """ + + forward_ref_policy: ForwardRefPolicy = ForwardRefPolicy.WARN + typecheck_fail_callback: TypeCheckFailCallback | None = None + collection_check_strategy: CollectionCheckStrategy = ( + CollectionCheckStrategy.FIRST_ITEM + ) + debug_instrumentation: bool = False + + +global_config = TypeCheckConfiguration() diff --git a/python/code/typeguard/_decorators.py b/python/code/typeguard/_decorators.py new file mode 100644 index 00000000..3ed85b0c --- /dev/null +++ b/python/code/typeguard/_decorators.py @@ -0,0 +1,239 @@ +from __future__ import annotations + +import ast +import inspect +import sys +from collections.abc import Sequence +from functools import partial +from inspect import isclass, isfunction +from types import CodeType, FrameType, FunctionType +from typing import TYPE_CHECKING, Any, Callable, ForwardRef, TypeVar, cast, overload +from warnings import warn + +from ._config import CollectionCheckStrategy, ForwardRefPolicy, global_config +from ._exceptions import InstrumentationWarning +from ._functions import TypeCheckFailCallback +from ._transformer import TypeguardTransformer +from ._utils import Unset, function_name, get_stacklevel, is_method_of, unset + +T_CallableOrType = TypeVar("T_CallableOrType", bound=Callable[..., Any]) + +if TYPE_CHECKING: + from typeshed.stdlib.types import _Cell + + def typeguard_ignore(arg: T_CallableOrType) -> T_CallableOrType: + """This decorator is a noop during static type-checking.""" + return arg + +else: + from typing import no_type_check as typeguard_ignore # noqa: F401 + + +def make_cell(value: object) -> _Cell: + return (lambda: value).__closure__[0] # type: ignore[index] + + +def find_target_function( + new_code: CodeType, target_path: Sequence[str], firstlineno: int +) -> CodeType | None: + for const in new_code.co_consts: + if isinstance(const, CodeType): + new_path = ( + target_path[1:] if const.co_name == target_path[0] else target_path + ) + if not new_path and const.co_firstlineno == firstlineno: + return const + + if target_code := find_target_function(const, new_path, firstlineno): + return target_code + + return None + + +def instrument(f: T_CallableOrType) -> FunctionType | str: + if not getattr(f, "__code__", None): + return "no code associated" + elif not getattr(f, "__module__", None): + return "__module__ attribute is not set" + elif f.__code__.co_filename == "": + return "cannot instrument functions defined in a REPL" + elif hasattr(f, "__wrapped__"): + return ( + "@typechecked only supports instrumenting functions wrapped with " + "@classmethod, @staticmethod or @property" + ) + + target_path = [item for item in f.__qualname__.split(".") if item != ""] + module_source = inspect.getsource(sys.modules[f.__module__]) + module_ast = ast.parse(module_source) + instrumentor = TypeguardTransformer(target_path, f.__code__.co_firstlineno) + instrumentor.visit(module_ast) + + if not instrumentor.target_node or instrumentor.target_lineno is None: + return "instrumentor did not find the target function" + + module_code = compile(module_ast, f.__code__.co_filename, "exec", dont_inherit=True) + new_code = find_target_function( + module_code, target_path, instrumentor.target_lineno + ) + if not new_code: + return "cannot find the target function in the AST" + + if global_config.debug_instrumentation and sys.version_info >= (3, 9): + # Find the matching AST node, then unparse it to source and print to stdout + print( + f"Source code of {f.__qualname__}() after instrumentation:" + "\n----------------------------------------------", + file=sys.stderr, + ) + print(ast.unparse(instrumentor.target_node), file=sys.stderr) + print( + "----------------------------------------------", + file=sys.stderr, + ) + + closure = f.__closure__ + if new_code.co_freevars != f.__code__.co_freevars: + # Create a new closure and find values for the new free variables + frame = cast(FrameType, inspect.currentframe()) + frame = cast(FrameType, frame.f_back) + frame_locals = cast(FrameType, frame.f_back).f_locals + cells: list[_Cell] = [] + for key in new_code.co_freevars: + if key in instrumentor.names_used_in_annotations: + # Find the value and make a new cell from it + value = frame_locals.get(key) or ForwardRef(key) + cells.append(make_cell(value)) + else: + # Reuse the cell from the existing closure + assert f.__closure__ + cells.append(f.__closure__[f.__code__.co_freevars.index(key)]) + + closure = tuple(cells) + + new_function = FunctionType(new_code, f.__globals__, f.__name__, closure=closure) + new_function.__module__ = f.__module__ + new_function.__name__ = f.__name__ + new_function.__qualname__ = f.__qualname__ + new_function.__doc__ = f.__doc__ + new_function.__defaults__ = f.__defaults__ + new_function.__kwdefaults__ = f.__kwdefaults__ + + if sys.version_info >= (3, 12): + new_function.__type_params__ = f.__type_params__ + + if sys.version_info >= (3, 14): + new_function.__annotate__ = f.__annotate__ + else: + new_function.__annotations__ = f.__annotations__ + + return new_function + + +@overload +def typechecked( + *, + forward_ref_policy: ForwardRefPolicy | Unset = unset, + typecheck_fail_callback: TypeCheckFailCallback | Unset = unset, + collection_check_strategy: CollectionCheckStrategy | Unset = unset, + debug_instrumentation: bool | Unset = unset, +) -> Callable[[T_CallableOrType], T_CallableOrType]: ... + + +@overload +def typechecked(target: T_CallableOrType) -> T_CallableOrType: ... + + +def typechecked( + target: T_CallableOrType | None = None, + *, + forward_ref_policy: ForwardRefPolicy | Unset = unset, + typecheck_fail_callback: TypeCheckFailCallback | Unset = unset, + collection_check_strategy: CollectionCheckStrategy | Unset = unset, + debug_instrumentation: bool | Unset = unset, +) -> Any: + """ + Instrument the target function to perform run-time type checking. + + This decorator recompiles the target function, injecting code to type check + arguments, return values, yield values (excluding ``yield from``) and assignments to + annotated local variables. + + This can also be used as a class decorator. This will instrument all type annotated + methods, including :func:`@classmethod `, + :func:`@staticmethod `, and :class:`@property ` decorated + methods in the class. + + .. note:: When Python is run in optimized mode (``-O`` or ``-OO``, this decorator + is a no-op). This is a feature meant for selectively introducing type checking + into a code base where the checks aren't meant to be run in production. + + :param target: the function or class to enable type checking for + :param forward_ref_policy: override for + :attr:`.TypeCheckConfiguration.forward_ref_policy` + :param typecheck_fail_callback: override for + :attr:`.TypeCheckConfiguration.typecheck_fail_callback` + :param collection_check_strategy: override for + :attr:`.TypeCheckConfiguration.collection_check_strategy` + :param debug_instrumentation: override for + :attr:`.TypeCheckConfiguration.debug_instrumentation` + + """ + if target is None: + return partial( + typechecked, + forward_ref_policy=forward_ref_policy, + typecheck_fail_callback=typecheck_fail_callback, + collection_check_strategy=collection_check_strategy, + debug_instrumentation=debug_instrumentation, + ) + + if not __debug__: + return target + + if isclass(target): + for key, attr in target.__dict__.items(): + if is_method_of(attr, target): + retval = instrument(attr) + if isfunction(retval): + setattr(target, key, retval) + elif isinstance(attr, (classmethod, staticmethod)): + if is_method_of(attr.__func__, target): + retval = instrument(attr.__func__) + if isfunction(retval): + wrapper = attr.__class__(retval) + setattr(target, key, wrapper) + elif isinstance(attr, property): + kwargs: dict[str, Any] = dict(doc=attr.__doc__) + for name in ("fset", "fget", "fdel"): + property_func = kwargs[name] = getattr(attr, name) + if is_method_of(property_func, target): + retval = instrument(property_func) + if isfunction(retval): + kwargs[name] = retval + + setattr(target, key, attr.__class__(**kwargs)) + + return target + + # Find either the first Python wrapper or the actual function + wrapper_class: ( + type[classmethod[Any, Any, Any]] | type[staticmethod[Any, Any]] | None + ) = None + if isinstance(target, (classmethod, staticmethod)): + wrapper_class = target.__class__ + target = target.__func__ # type: ignore[assignment] + + retval = instrument(target) + if isinstance(retval, str): + warn( + f"{retval} -- not typechecking {function_name(target)}", + InstrumentationWarning, + stacklevel=get_stacklevel(), + ) + return target + + if wrapper_class is None: + return retval + else: + return wrapper_class(retval) diff --git a/python/code/typeguard/_exceptions.py b/python/code/typeguard/_exceptions.py new file mode 100644 index 00000000..625437a6 --- /dev/null +++ b/python/code/typeguard/_exceptions.py @@ -0,0 +1,42 @@ +from collections import deque +from typing import Deque + + +class TypeHintWarning(UserWarning): + """ + A warning that is emitted when a type hint in string form could not be resolved to + an actual type. + """ + + +class TypeCheckWarning(UserWarning): + """Emitted by typeguard's type checkers when a type mismatch is detected.""" + + def __init__(self, message: str): + super().__init__(message) + + +class InstrumentationWarning(UserWarning): + """Emitted when there's a problem with instrumenting a function for type checks.""" + + def __init__(self, message: str): + super().__init__(message) + + +class TypeCheckError(Exception): + """ + Raised by typeguard's type checkers when a type mismatch is detected. + """ + + def __init__(self, message: str): + super().__init__(message) + self._path: Deque[str] = deque() + + def append_path_element(self, element: str) -> None: + self._path.append(element) + + def __str__(self) -> str: + if self._path: + return " of ".join(self._path) + " " + str(self.args[0]) + else: + return str(self.args[0]) diff --git a/python/code/typeguard/_functions.py b/python/code/typeguard/_functions.py new file mode 100644 index 00000000..528d41bb --- /dev/null +++ b/python/code/typeguard/_functions.py @@ -0,0 +1,309 @@ +from __future__ import annotations + +import sys +import warnings +from collections.abc import Sequence +from typing import Any, Callable, NoReturn, TypeVar, Union, overload + +from . import _suppression +from ._checkers import BINARY_MAGIC_METHODS, check_type_internal +from ._config import ( + CollectionCheckStrategy, + ForwardRefPolicy, + TypeCheckConfiguration, +) +from ._exceptions import TypeCheckError, TypeCheckWarning +from ._memo import TypeCheckMemo +from ._utils import get_stacklevel, qualified_name + +if sys.version_info >= (3, 11): + from typing import Literal, Never, TypeAlias +else: + from typing_extensions import Literal, Never, TypeAlias + +T = TypeVar("T") +TypeCheckFailCallback: TypeAlias = Callable[[TypeCheckError, TypeCheckMemo], Any] + +@overload +def check_type( + value: object, + expected_type: type[T], + *, + forward_ref_policy: ForwardRefPolicy = ..., + typecheck_fail_callback: TypeCheckFailCallback | None = ..., + collection_check_strategy: CollectionCheckStrategy = ..., + ns: tuple[dict, dict] | None = None, +) -> T: ... + + +@overload +def check_type( + value: object, + expected_type: Any, + *, + forward_ref_policy: ForwardRefPolicy = ..., + typecheck_fail_callback: TypeCheckFailCallback | None = ..., + collection_check_strategy: CollectionCheckStrategy = ..., + ns: tuple[dict, dict] | None = None, +) -> Any: ... + + +def check_type( + value: object, + expected_type: Any, + *, + forward_ref_policy: ForwardRefPolicy = TypeCheckConfiguration().forward_ref_policy, + typecheck_fail_callback: TypeCheckFailCallback | None = ( + TypeCheckConfiguration().typecheck_fail_callback + ), + collection_check_strategy: CollectionCheckStrategy = ( + TypeCheckConfiguration().collection_check_strategy + ), + ns: tuple[dict, dict] | None = None, +) -> Any: + """ + Ensure that ``value`` matches ``expected_type``. + + The types from the :mod:`typing` module do not support :func:`isinstance` or + :func:`issubclass` so a number of type specific checks are required. This function + knows which checker to call for which type. + + This function wraps :func:`~.check_type_internal` in the following ways: + + * Respects type checking suppression (:func:`~.suppress_type_checks`) + * Forms a :class:`~.TypeCheckMemo` from the current stack frame + * Calls the configured type check fail callback if the check fails + + Note that this function is independent of the globally shared configuration in + :data:`typeguard.config`. This means that usage within libraries is safe from being + affected configuration changes made by other libraries or by the integrating + application. Instead, configuration options have the same default values as their + corresponding fields in :class:`TypeCheckConfiguration`. + + :param value: value to be checked against ``expected_type`` + :param expected_type: a class or generic type instance, or a tuple of such things + :param forward_ref_policy: see :attr:`TypeCheckConfiguration.forward_ref_policy` + :param typecheck_fail_callback: + see :attr`TypeCheckConfiguration.typecheck_fail_callback` + :param collection_check_strategy: + see :attr:`TypeCheckConfiguration.collection_check_strategy` + :return: ``value``, unmodified + :raises TypeCheckError: if there is a type mismatch + + """ + if type(expected_type) is tuple: + expected_type = Union[expected_type] + + config = TypeCheckConfiguration( + forward_ref_policy=forward_ref_policy, + typecheck_fail_callback=typecheck_fail_callback, + collection_check_strategy=collection_check_strategy, + ) + + if _suppression.type_checks_suppressed or expected_type is Any: + return value + + if ns: + memo = TypeCheckMemo(ns[0], ns[1], config=config) + else: + frame = sys._getframe(1) + memo = TypeCheckMemo(frame.f_globals, frame.f_locals, config=config) + del frame + try: + check_type_internal(value, expected_type, memo) + except TypeCheckError as exc: + exc.append_path_element(qualified_name(value, add_class_prefix=True)) + if config.typecheck_fail_callback: + config.typecheck_fail_callback(exc, memo) + else: + raise + + return value + + +def check_argument_types( + func_name: str, + arguments: dict[str, tuple[Any, Any]], + memo: TypeCheckMemo, +) -> Literal[True]: + if _suppression.type_checks_suppressed: + return True + + for argname, (value, annotation) in arguments.items(): + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError( + f"{func_name}() was declared never to be called but it was" + ) + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(value, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(value, add_class_prefix=True) + exc.append_path_element(f'argument "{argname}" ({qualname})') + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return True + + +def check_return_type( + func_name: str, + retval: T, + annotation: Any, + memo: TypeCheckMemo, +) -> T: + if _suppression.type_checks_suppressed: + return retval + + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError(f"{func_name}() was declared never to return but it did") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(retval, annotation, memo) + except TypeCheckError as exc: + # Allow NotImplemented if this is a binary magic method (__eq__() et al) + if retval is NotImplemented and annotation is bool: + # This does (and cannot) not check if it's actually a method + func_name = func_name.rsplit(".", 1)[-1] + if func_name in BINARY_MAGIC_METHODS: + return retval + + qualname = qualified_name(retval, add_class_prefix=True) + exc.append_path_element(f"the return value ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return retval + + +def check_send_type( + func_name: str, + sendval: T, + annotation: Any, + memo: TypeCheckMemo, +) -> T: + if _suppression.type_checks_suppressed: + return sendval + + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError( + f"{func_name}() was declared never to be sent a value to but it was" + ) + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(sendval, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(sendval, add_class_prefix=True) + exc.append_path_element(f"the value sent to generator ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return sendval + + +def check_yield_type( + func_name: str, + yieldval: T, + annotation: Any, + memo: TypeCheckMemo, +) -> T: + if _suppression.type_checks_suppressed: + return yieldval + + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError(f"{func_name}() was declared never to yield but it did") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(yieldval, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(yieldval, add_class_prefix=True) + exc.append_path_element(f"the yielded value ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return yieldval + + +def check_variable_assignment( + value: Any, targets: Sequence[list[tuple[str, Any]]], memo: TypeCheckMemo +) -> Any: + if _suppression.type_checks_suppressed: + return value + + value_to_return = value + for target in targets: + star_variable_index = next( + (i for i, (varname, _) in enumerate(target) if varname.startswith("*")), + None, + ) + if star_variable_index is not None: + value_to_return = list(value) + remaining_vars = len(target) - 1 - star_variable_index + end_index = len(value_to_return) - remaining_vars + values_to_check = ( + value_to_return[:star_variable_index] + + [value_to_return[star_variable_index:end_index]] + + value_to_return[end_index:] + ) + elif len(target) > 1: + values_to_check = value_to_return = [] + iterator = iter(value) + for _ in target: + try: + values_to_check.append(next(iterator)) + except StopIteration: + raise ValueError( + f"not enough values to unpack (expected {len(target)}, got " + f"{len(values_to_check)})" + ) from None + + else: + values_to_check = [value] + + for val, (varname, annotation) in zip(values_to_check, target): + try: + check_type_internal(val, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(val, add_class_prefix=True) + exc.append_path_element(f"value assigned to {varname} ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return value_to_return + + +def warn_on_error(exc: TypeCheckError, memo: TypeCheckMemo) -> None: + """ + Emit a warning on a type mismatch. + + This is intended to be used as an error handler in + :attr:`TypeCheckConfiguration.typecheck_fail_callback`. + + """ + warnings.warn(TypeCheckWarning(str(exc)), stacklevel=get_stacklevel()) diff --git a/python/code/typeguard/_importhook.py b/python/code/typeguard/_importhook.py new file mode 100644 index 00000000..0d1c6274 --- /dev/null +++ b/python/code/typeguard/_importhook.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import ast +import sys +import types +from collections.abc import Callable, Iterable, Sequence +from importlib.abc import MetaPathFinder +from importlib.machinery import ModuleSpec, SourceFileLoader +from importlib.util import cache_from_source, decode_source +from inspect import isclass +from os import PathLike +from types import CodeType, ModuleType, TracebackType +from typing import TypeVar +from unittest.mock import patch + +from ._config import global_config +from ._transformer import TypeguardTransformer + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing_extensions import Buffer + +if sys.version_info >= (3, 11): + from typing import ParamSpec +else: + from typing_extensions import ParamSpec + +if sys.version_info >= (3, 10): + from importlib.metadata import PackageNotFoundError, version +else: + from importlib_metadata import PackageNotFoundError, version + +try: + OPTIMIZATION = "typeguard" + "".join(version("typeguard").split(".")[:3]) +except PackageNotFoundError: + OPTIMIZATION = "typeguard" + +P = ParamSpec("P") +T = TypeVar("T") + + +# The name of this function is magical +def _call_with_frames_removed( + f: Callable[P, T], *args: P.args, **kwargs: P.kwargs +) -> T: + return f(*args, **kwargs) + + +def optimized_cache_from_source(path: str, debug_override: bool | None = None) -> str: + return cache_from_source(path, debug_override, optimization=OPTIMIZATION) + + +class TypeguardLoader(SourceFileLoader): + @staticmethod + def source_to_code( + data: Buffer | str | ast.Module | ast.Expression | ast.Interactive, + path: Buffer | str | PathLike[str] = "", + ) -> CodeType: + if isinstance(data, (ast.Module, ast.Expression, ast.Interactive)): + tree = data + else: + if isinstance(data, str): + source = data + else: + source = decode_source(data) + + tree = _call_with_frames_removed( + ast.parse, + source, + path, + "exec", + ) + + tree = TypeguardTransformer().visit(tree) + ast.fix_missing_locations(tree) + + if global_config.debug_instrumentation and sys.version_info >= (3, 9): + print( + f"Source code of {path!r} after instrumentation:\n" + "----------------------------------------------", + file=sys.stderr, + ) + print(ast.unparse(tree), file=sys.stderr) + print("----------------------------------------------", file=sys.stderr) + + return _call_with_frames_removed( + compile, tree, path, "exec", 0, dont_inherit=True + ) + + def exec_module(self, module: ModuleType) -> None: + # Use a custom optimization marker – the import lock should make this monkey + # patch safe + with patch( + "importlib._bootstrap_external.cache_from_source", + optimized_cache_from_source, + ): + super().exec_module(module) + + +class TypeguardFinder(MetaPathFinder): + """ + Wraps another path finder and instruments the module with + :func:`@typechecked ` if :meth:`should_instrument` returns + ``True``. + + Should not be used directly, but rather via :func:`~.install_import_hook`. + + .. versionadded:: 2.6 + """ + + def __init__(self, packages: list[str] | None, original_pathfinder: MetaPathFinder): + self.packages = packages + self._original_pathfinder = original_pathfinder + + def find_spec( + self, + fullname: str, + path: Sequence[str] | None, + target: types.ModuleType | None = None, + ) -> ModuleSpec | None: + if self.should_instrument(fullname): + spec = self._original_pathfinder.find_spec(fullname, path, target) + if spec is not None and isinstance(spec.loader, SourceFileLoader): + spec.loader = TypeguardLoader(spec.loader.name, spec.loader.path) + return spec + + return None + + def should_instrument(self, module_name: str) -> bool: + """ + Determine whether the module with the given name should be instrumented. + + :param module_name: full name of the module that is about to be imported (e.g. + ``xyz.abc``) + + """ + if self.packages is None: + return True + + for package in self.packages: + if module_name == package or module_name.startswith(package + "."): + return True + + return False + + +class ImportHookManager: + """ + A handle that can be used to uninstall the Typeguard import hook. + """ + + def __init__(self, hook: MetaPathFinder): + self.hook = hook + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exc_type: type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType, + ) -> None: + self.uninstall() + + def uninstall(self) -> None: + """Uninstall the import hook.""" + try: + sys.meta_path.remove(self.hook) + except ValueError: + pass # already removed + + +def install_import_hook( + packages: Iterable[str] | None = None, + *, + cls: type[TypeguardFinder] = TypeguardFinder, +) -> ImportHookManager: + """ + Install an import hook that instruments functions for automatic type checking. + + This only affects modules loaded **after** this hook has been installed. + + :param packages: an iterable of package names to instrument, or ``None`` to + instrument all packages + :param cls: a custom meta path finder class + :return: a context manager that uninstalls the hook on exit (or when you call + ``.uninstall()``) + + .. versionadded:: 2.6 + + """ + if packages is None: + target_packages: list[str] | None = None + elif isinstance(packages, str): + target_packages = [packages] + else: + target_packages = list(packages) + + for finder in sys.meta_path: + if ( + isclass(finder) + and finder.__name__ == "PathFinder" + and hasattr(finder, "find_spec") + ): + break + else: + raise RuntimeError("Cannot find a PathFinder in sys.meta_path") + + hook = cls(target_packages, finder) + sys.meta_path.insert(0, hook) + return ImportHookManager(hook) diff --git a/python/code/typeguard/_memo.py b/python/code/typeguard/_memo.py new file mode 100644 index 00000000..4c97588c --- /dev/null +++ b/python/code/typeguard/_memo.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Any + +from ._config import TypeCheckConfiguration, global_config + + +class TypeCheckMemo: + """ + Contains information necessary for type checkers to do their work. + + .. attribute:: globals + :type: dict[str, Any] + + Dictionary of global variables to use for resolving forward references. + + .. attribute:: locals + :type: dict[str, Any] + + Dictionary of local variables to use for resolving forward references. + + .. attribute:: self_type + :type: type | None + + When running type checks within an instance method or class method, this is the + class object that the first argument (usually named ``self`` or ``cls``) refers + to. + + .. attribute:: config + :type: TypeCheckConfiguration + + Contains the configuration for a particular set of type checking operations. + """ + + __slots__ = "globals", "locals", "self_type", "config" + + def __init__( + self, + globals: dict[str, Any], + locals: dict[str, Any], + *, + self_type: type | None = None, + config: TypeCheckConfiguration = global_config, + ): + self.globals = globals + self.locals = locals + self.self_type = self_type + self.config = config diff --git a/python/code/typeguard/_pytest_plugin.py b/python/code/typeguard/_pytest_plugin.py new file mode 100644 index 00000000..7b2f494e --- /dev/null +++ b/python/code/typeguard/_pytest_plugin.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import sys +import warnings +from typing import TYPE_CHECKING, Any, Literal + +from typeguard._config import CollectionCheckStrategy, ForwardRefPolicy, global_config +from typeguard._exceptions import InstrumentationWarning +from typeguard._importhook import install_import_hook +from typeguard._utils import qualified_name, resolve_reference + +if TYPE_CHECKING: + from pytest import Config, Parser + + +def pytest_addoption(parser: Parser) -> None: + def add_ini_option( + opt_type: ( + Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None + ), + ) -> None: + parser.addini( + group.options[-1].names()[0][2:], + group.options[-1].attrs()["help"], + opt_type, + ) + + group = parser.getgroup("typeguard") + group.addoption( + "--typeguard-packages", + action="store", + help="comma separated name list of packages and modules to instrument for " + "type checking, or :all: to instrument all modules loaded after typeguard", + ) + add_ini_option("linelist") + + group.addoption( + "--typeguard-debug-instrumentation", + action="store_true", + help="print all instrumented code to stderr", + ) + add_ini_option("bool") + + group.addoption( + "--typeguard-typecheck-fail-callback", + action="store", + help=( + "a module:varname (e.g. typeguard:warn_on_error) reference to a function " + "that is called (with the exception, and memo object as arguments) to " + "handle a TypeCheckError" + ), + ) + add_ini_option("string") + + group.addoption( + "--typeguard-forward-ref-policy", + action="store", + choices=list(ForwardRefPolicy.__members__), + help=( + "determines how to deal with unresolveable forward references in type " + "annotations" + ), + ) + add_ini_option("string") + + group.addoption( + "--typeguard-collection-check-strategy", + action="store", + choices=list(CollectionCheckStrategy.__members__), + help="determines how thoroughly to check collections (list, dict, etc)", + ) + add_ini_option("string") + + +def pytest_configure(config: Config) -> None: + def getoption(name: str) -> Any: + return config.getoption(name.replace("-", "_")) or config.getini(name) + + packages: list[str] | None = [] + if packages_option := config.getoption("typeguard_packages"): + packages = [pkg.strip() for pkg in packages_option.split(",")] + elif packages_ini := config.getini("typeguard-packages"): + packages = packages_ini + + if packages: + if packages == [":all:"]: + packages = None + else: + already_imported_packages = sorted( + package for package in packages if package in sys.modules + ) + if already_imported_packages: + warnings.warn( + f"typeguard cannot check these packages because they are already " + f"imported: {', '.join(already_imported_packages)}", + InstrumentationWarning, + stacklevel=1, + ) + + install_import_hook(packages=packages) + + debug_option = getoption("typeguard-debug-instrumentation") + if debug_option: + global_config.debug_instrumentation = True + + fail_callback_option = getoption("typeguard-typecheck-fail-callback") + if fail_callback_option: + callback = resolve_reference(fail_callback_option) + if not callable(callback): + raise TypeError( + f"{fail_callback_option} ({qualified_name(callback.__class__)}) is not " + f"a callable" + ) + + global_config.typecheck_fail_callback = callback + + forward_ref_policy_option = getoption("typeguard-forward-ref-policy") + if forward_ref_policy_option: + forward_ref_policy = ForwardRefPolicy.__members__[forward_ref_policy_option] + global_config.forward_ref_policy = forward_ref_policy + + collection_check_strategy_option = getoption("typeguard-collection-check-strategy") + if collection_check_strategy_option: + collection_check_strategy = CollectionCheckStrategy.__members__[ + collection_check_strategy_option + ] + global_config.collection_check_strategy = collection_check_strategy diff --git a/python/code/typeguard/_suppression.py b/python/code/typeguard/_suppression.py new file mode 100644 index 00000000..bbbfbfbe --- /dev/null +++ b/python/code/typeguard/_suppression.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import sys +from collections.abc import Callable, Generator +from contextlib import contextmanager +from functools import update_wrapper +from threading import Lock +from typing import ContextManager, TypeVar, overload + +if sys.version_info >= (3, 10): + from typing import ParamSpec +else: + from typing_extensions import ParamSpec + +P = ParamSpec("P") +T = TypeVar("T") + +type_checks_suppressed = 0 +type_checks_suppress_lock = Lock() + + +@overload +def suppress_type_checks(func: Callable[P, T]) -> Callable[P, T]: ... + + +@overload +def suppress_type_checks() -> ContextManager[None]: ... + + +def suppress_type_checks( + func: Callable[P, T] | None = None, +) -> Callable[P, T] | ContextManager[None]: + """ + Temporarily suppress all type checking. + + This function has two operating modes, based on how it's used: + + #. as a context manager (``with suppress_type_checks(): ...``) + #. as a decorator (``@suppress_type_checks``) + + When used as a context manager, :func:`check_type` and any automatically + instrumented functions skip the actual type checking. These context managers can be + nested. + + When used as a decorator, all type checking is suppressed while the function is + running. + + Type checking will resume once no more context managers are active and no decorated + functions are running. + + Both operating modes are thread-safe. + + """ + + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + global type_checks_suppressed + + with type_checks_suppress_lock: + type_checks_suppressed += 1 + + assert func is not None + try: + return func(*args, **kwargs) + finally: + with type_checks_suppress_lock: + type_checks_suppressed -= 1 + + def cm() -> Generator[None, None, None]: + global type_checks_suppressed + + with type_checks_suppress_lock: + type_checks_suppressed += 1 + + try: + yield + finally: + with type_checks_suppress_lock: + type_checks_suppressed -= 1 + + if func is None: + # Context manager mode + return contextmanager(cm)() + else: + # Decorator mode + update_wrapper(wrapper, func) + return wrapper diff --git a/python/code/typeguard/_transformer.py b/python/code/typeguard/_transformer.py new file mode 100644 index 00000000..7b6dda85 --- /dev/null +++ b/python/code/typeguard/_transformer.py @@ -0,0 +1,1228 @@ +from __future__ import annotations + +import ast +import builtins +import sys +import typing +from ast import ( + AST, + Add, + AnnAssign, + Assign, + AsyncFunctionDef, + Attribute, + AugAssign, + BinOp, + BitAnd, + BitOr, + BitXor, + Call, + ClassDef, + Constant, + Dict, + Div, + Expr, + Expression, + FloorDiv, + FunctionDef, + If, + Import, + ImportFrom, + List, + Load, + LShift, + MatMult, + Mod, + Module, + Mult, + Name, + NamedExpr, + NodeTransformer, + NodeVisitor, + Pass, + Pow, + Return, + RShift, + Starred, + Store, + Sub, + Subscript, + Tuple, + Yield, + YieldFrom, + alias, + copy_location, + expr, + fix_missing_locations, + keyword, + walk, +) +from collections import defaultdict +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from copy import deepcopy +from dataclasses import dataclass, field +from typing import Any, ClassVar, cast, overload + +generator_names = ( + "typing.Generator", + "collections.abc.Generator", + "typing.Iterator", + "collections.abc.Iterator", + "typing.Iterable", + "collections.abc.Iterable", + "typing.AsyncIterator", + "collections.abc.AsyncIterator", + "typing.AsyncIterable", + "collections.abc.AsyncIterable", + "typing.AsyncGenerator", + "collections.abc.AsyncGenerator", +) +anytype_names = ( + "typing.Any", + "typing_extensions.Any", +) +literal_names = ( + "typing.Literal", + "typing_extensions.Literal", +) +annotated_names = ( + "typing.Annotated", + "typing_extensions.Annotated", +) +ignore_decorators = ( + "typing.no_type_check", + "typeguard.typeguard_ignore", +) +aug_assign_functions = { + Add: "iadd", + Sub: "isub", + Mult: "imul", + MatMult: "imatmul", + Div: "itruediv", + FloorDiv: "ifloordiv", + Mod: "imod", + Pow: "ipow", + LShift: "ilshift", + RShift: "irshift", + BitAnd: "iand", + BitXor: "ixor", + BitOr: "ior", +} + + +@dataclass +class TransformMemo: + node: Module | ClassDef | FunctionDef | AsyncFunctionDef | None + parent: TransformMemo | None + path: tuple[str, ...] + joined_path: Constant = field(init=False) + return_annotation: expr | None = None + yield_annotation: expr | None = None + send_annotation: expr | None = None + is_async: bool = False + local_names: set[str] = field(init=False, default_factory=set) + imported_names: dict[str, str] = field(init=False, default_factory=dict) + ignored_names: set[str] = field(init=False, default_factory=set) + load_names: defaultdict[str, dict[str, Name]] = field( + init=False, default_factory=lambda: defaultdict(dict) + ) + has_yield_expressions: bool = field(init=False, default=False) + has_return_expressions: bool = field(init=False, default=False) + memo_var_name: Name | None = field(init=False, default=None) + should_instrument: bool = field(init=False, default=True) + variable_annotations: dict[str, expr] = field(init=False, default_factory=dict) + configuration_overrides: dict[str, Any] = field(init=False, default_factory=dict) + code_inject_index: int = field(init=False, default=0) + + def __post_init__(self) -> None: + elements: list[str] = [] + memo = self + while isinstance(memo.node, (ClassDef, FunctionDef, AsyncFunctionDef)): + elements.insert(0, memo.node.name) + if not memo.parent: + break + + memo = memo.parent + if isinstance(memo.node, (FunctionDef, AsyncFunctionDef)): + elements.insert(0, "") + + self.joined_path = Constant(".".join(elements)) + + # Figure out where to insert instrumentation code + if self.node: + for index, child in enumerate(self.node.body): + if isinstance(child, ImportFrom) and child.module == "__future__": + # (module only) __future__ imports must come first + continue + elif ( + isinstance(child, Expr) + and isinstance(child.value, Constant) + and isinstance(child.value.value, str) + ): + continue # docstring + + self.code_inject_index = index + break + + def get_unused_name(self, name: str) -> str: + memo: TransformMemo | None = self + while memo is not None: + if name in memo.local_names: + memo = self + name += "_" + else: + memo = memo.parent + + self.local_names.add(name) + return name + + def is_ignored_name(self, expression: expr | Expr | None) -> bool: + top_expression = ( + expression.value if isinstance(expression, Expr) else expression + ) + + if isinstance(top_expression, Attribute) and isinstance( + top_expression.value, Name + ): + name = top_expression.value.id + elif isinstance(top_expression, Name): + name = top_expression.id + else: + return False + + memo: TransformMemo | None = self + while memo is not None: + if name in memo.ignored_names: + return True + + memo = memo.parent + + return False + + def get_memo_name(self) -> Name: + if not self.memo_var_name: + self.memo_var_name = Name(id="memo", ctx=Load()) + + return self.memo_var_name + + def get_import(self, module: str, name: str) -> Name: + if module in self.load_names and name in self.load_names[module]: + return self.load_names[module][name] + + qualified_name = f"{module}.{name}" + if name in self.imported_names and self.imported_names[name] == qualified_name: + return Name(id=name, ctx=Load()) + + alias = self.get_unused_name(name) + node = self.load_names[module][name] = Name(id=alias, ctx=Load()) + self.imported_names[name] = qualified_name + return node + + def insert_imports(self, node: Module | FunctionDef | AsyncFunctionDef) -> None: + """Insert imports needed by injected code.""" + if not self.load_names: + return + + # Insert imports after any "from __future__ ..." imports and any docstring + for modulename, names in self.load_names.items(): + aliases = [ + alias(orig_name, new_name.id if orig_name != new_name.id else None) + for orig_name, new_name in sorted(names.items()) + ] + node.body.insert(self.code_inject_index, ImportFrom(modulename, aliases, 0)) + + def name_matches(self, expression: expr | Expr | None, *names: str) -> bool: + if expression is None: + return False + + path: list[str] = [] + top_expression = ( + expression.value if isinstance(expression, Expr) else expression + ) + + if isinstance(top_expression, Subscript): + top_expression = top_expression.value + elif isinstance(top_expression, Call): + top_expression = top_expression.func + + while isinstance(top_expression, Attribute): + path.insert(0, top_expression.attr) + top_expression = top_expression.value + + if not isinstance(top_expression, Name): + return False + + if top_expression.id in self.imported_names: + translated = self.imported_names[top_expression.id] + elif hasattr(builtins, top_expression.id): + translated = "builtins." + top_expression.id + else: + translated = top_expression.id + + path.insert(0, translated) + joined_path = ".".join(path) + if joined_path in names: + return True + elif self.parent: + return self.parent.name_matches(expression, *names) + else: + return False + + def get_config_keywords(self) -> list[keyword]: + if self.parent and isinstance(self.parent.node, ClassDef): + overrides = self.parent.configuration_overrides.copy() + else: + overrides = {} + + overrides.update(self.configuration_overrides) + return [keyword(key, value) for key, value in overrides.items()] + + +class NameCollector(NodeVisitor): + def __init__(self) -> None: + self.names: set[str] = set() + + def visit_Import(self, node: Import) -> None: + for name in node.names: + self.names.add(name.asname or name.name) + + def visit_ImportFrom(self, node: ImportFrom) -> None: + for name in node.names: + self.names.add(name.asname or name.name) + + def visit_Assign(self, node: Assign) -> None: + for target in node.targets: + if isinstance(target, Name): + self.names.add(target.id) + + def visit_NamedExpr(self, node: NamedExpr) -> Any: + if isinstance(node.target, Name): + self.names.add(node.target.id) + + def visit_FunctionDef(self, node: FunctionDef) -> None: + pass + + def visit_ClassDef(self, node: ClassDef) -> None: + pass + + +class GeneratorDetector(NodeVisitor): + """Detects if a function node is a generator function.""" + + contains_yields: bool = False + in_root_function: bool = False + + def visit_Yield(self, node: Yield) -> Any: + self.contains_yields = True + + def visit_YieldFrom(self, node: YieldFrom) -> Any: + self.contains_yields = True + + def visit_ClassDef(self, node: ClassDef) -> Any: + pass + + def visit_FunctionDef(self, node: FunctionDef | AsyncFunctionDef) -> Any: + if not self.in_root_function: + self.in_root_function = True + self.generic_visit(node) + self.in_root_function = False + + def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> Any: + self.visit_FunctionDef(node) + + +class AnnotationTransformer(NodeTransformer): + type_substitutions: ClassVar[dict[str, tuple[str, str]]] = { + "builtins.dict": ("typing", "Dict"), + "builtins.list": ("typing", "List"), + "builtins.tuple": ("typing", "Tuple"), + "builtins.set": ("typing", "Set"), + "builtins.frozenset": ("typing", "FrozenSet"), + } + + def __init__(self, transformer: TypeguardTransformer): + self.transformer = transformer + self._memo = transformer._memo + self._level = 0 + + def visit(self, node: AST) -> Any: + # Don't process Literals + if isinstance(node, expr) and self._memo.name_matches(node, *literal_names): + return node + + self._level += 1 + new_node = super().visit(node) + self._level -= 1 + + if isinstance(new_node, Expression) and not hasattr(new_node, "body"): + return None + + # Return None if this new node matches a variation of typing.Any + if ( + self._level == 0 + and isinstance(new_node, expr) + and self._memo.name_matches(new_node, *anytype_names) + ): + return None + + return new_node + + def visit_BinOp(self, node: BinOp) -> Any: + self.generic_visit(node) + + if isinstance(node.op, BitOr): + # If either branch of the BinOp has been transformed to `None`, it means + # that a type in the union was ignored, so the entire annotation should be + # ignored + if not hasattr(node, "left") or not hasattr(node, "right"): + return None + + # Return Any if either side is Any + if self._memo.name_matches(node.left, *anytype_names): + return node.left + elif self._memo.name_matches(node.right, *anytype_names): + return node.right + + # Turn union types to typing.Union constructs on Python 3.9 + if sys.version_info < (3, 10): + union_name = self.transformer._get_import("typing", "Union") + return Subscript( + value=union_name, + slice=Tuple(elts=[node.left, node.right], ctx=Load()), + ctx=Load(), + ) + + return node + + def visit_Attribute(self, node: Attribute) -> Any: + if self._memo.is_ignored_name(node): + return None + + return node + + def visit_Subscript(self, node: Subscript) -> Any: + if self._memo.is_ignored_name(node.value): + return None + + # The subscript of typing(_extensions).Literal can be any arbitrary string, so + # don't try to evaluate it as code + if node.slice: + if isinstance(node.slice, Tuple): + if self._memo.name_matches(node.value, *annotated_names): + # Only treat the first argument to typing.Annotated as a potential + # forward reference + items = cast( + typing.List[expr], + [self.visit(node.slice.elts[0])] + node.slice.elts[1:], + ) + else: + items = cast( + typing.List[expr], + [self.visit(item) for item in node.slice.elts], + ) + + # If this is a Union and any of the items is Any, erase the entire + # annotation + if self._memo.name_matches(node.value, "typing.Union") and any( + item is None + or ( + isinstance(item, expr) + and self._memo.name_matches(item, *anytype_names) + ) + for item in items + ): + return None + + # If all items in the subscript were Any, erase the subscript entirely + if all(item is None for item in items): + return node.value + + for index, item in enumerate(items): + if item is None: + items[index] = self.transformer._get_import("typing", "Any") + + node.slice.elts = items + else: + self.generic_visit(node) + + # If the transformer erased the slice entirely, just return the node + # value without the subscript (unless it's Optional, in which case erase + # the node entirely + if self._memo.name_matches( + node.value, "typing.Optional" + ) and not hasattr(node, "slice"): + return None + if sys.version_info >= (3, 9) and not hasattr(node, "slice"): + return node.value + elif sys.version_info < (3, 9) and not hasattr(node.slice, "value"): + return node.value + + return node + + def visit_Name(self, node: Name) -> Any: + if self._memo.is_ignored_name(node): + return None + + return node + + def visit_Call(self, node: Call) -> Any: + # Don't recurse into calls + return node + + def visit_Constant(self, node: Constant) -> Any: + if isinstance(node.value, str): + expression = ast.parse(node.value, mode="eval") + new_node = self.visit(expression) + if new_node: + return copy_location(new_node.body, node) + else: + return None + + return node + + +class TypeguardTransformer(NodeTransformer): + def __init__( + self, target_path: Sequence[str] | None = None, target_lineno: int | None = None + ) -> None: + self._target_path = tuple(target_path) if target_path else None + self._memo = self._module_memo = TransformMemo(None, None, ()) + self.names_used_in_annotations: set[str] = set() + self.target_node: FunctionDef | AsyncFunctionDef | None = None + self.target_lineno = target_lineno + + def generic_visit(self, node: AST) -> AST: + has_non_empty_body_initially = bool(getattr(node, "body", None)) + initial_type = type(node) + + node = super().generic_visit(node) + + if ( + type(node) is initial_type + and has_non_empty_body_initially + and hasattr(node, "body") + and not node.body + ): + # If we have still the same node type after transformation + # but we've optimised it's body away, we add a `pass` statement. + node.body = [Pass()] + + return node + + @contextmanager + def _use_memo( + self, node: ClassDef | FunctionDef | AsyncFunctionDef + ) -> Generator[None, Any, None]: + new_memo = TransformMemo(node, self._memo, self._memo.path + (node.name,)) + old_memo = self._memo + self._memo = new_memo + + if isinstance(node, (FunctionDef, AsyncFunctionDef)): + new_memo.should_instrument = ( + self._target_path is None or new_memo.path == self._target_path + ) + if new_memo.should_instrument: + # Check if the function is a generator function + detector = GeneratorDetector() + detector.visit(node) + + # Extract yield, send and return types where possible from a subscripted + # annotation like Generator[int, str, bool] + return_annotation = deepcopy(node.returns) + if detector.contains_yields and new_memo.name_matches( + return_annotation, *generator_names + ): + if isinstance(return_annotation, Subscript): + if isinstance(return_annotation.slice, Tuple): + items = return_annotation.slice.elts + else: + items = [return_annotation.slice] + + if len(items) > 0: + new_memo.yield_annotation = self._convert_annotation( + items[0] + ) + + if len(items) > 1: + new_memo.send_annotation = self._convert_annotation( + items[1] + ) + + if len(items) > 2: + new_memo.return_annotation = self._convert_annotation( + items[2] + ) + else: + new_memo.return_annotation = self._convert_annotation( + return_annotation + ) + + if isinstance(node, AsyncFunctionDef): + new_memo.is_async = True + + yield + self._memo = old_memo + + def _get_import(self, module: str, name: str) -> Name: + memo = self._memo if self._target_path else self._module_memo + return memo.get_import(module, name) + + @overload + def _convert_annotation(self, annotation: None) -> None: ... + + @overload + def _convert_annotation(self, annotation: expr) -> expr: ... + + def _convert_annotation(self, annotation: expr | None) -> expr | None: + if annotation is None: + return None + + # Convert PEP 604 unions (x | y) and generic built-in collections where + # necessary, and undo forward references + new_annotation = cast(expr, AnnotationTransformer(self).visit(annotation)) + if isinstance(new_annotation, expr): + new_annotation = ast.copy_location(new_annotation, annotation) + + # Store names used in the annotation + names = {node.id for node in walk(new_annotation) if isinstance(node, Name)} + self.names_used_in_annotations.update(names) + + return new_annotation + + def visit_Name(self, node: Name) -> Name: + self._memo.local_names.add(node.id) + return node + + def visit_Module(self, node: Module) -> Module: + self._module_memo = self._memo = TransformMemo(node, None, ()) + self.generic_visit(node) + self._module_memo.insert_imports(node) + + fix_missing_locations(node) + return node + + def visit_Import(self, node: Import) -> Import: + for name in node.names: + self._memo.local_names.add(name.asname or name.name) + self._memo.imported_names[name.asname or name.name] = name.name + + return node + + def visit_ImportFrom(self, node: ImportFrom) -> ImportFrom: + for name in node.names: + if name.name != "*": + alias = name.asname or name.name + self._memo.local_names.add(alias) + self._memo.imported_names[alias] = f"{node.module}.{name.name}" + + return node + + def visit_ClassDef(self, node: ClassDef) -> ClassDef | None: + self._memo.local_names.add(node.name) + + # Eliminate top level classes not belonging to the target path + if ( + self._target_path is not None + and not self._memo.path + and node.name != self._target_path[0] + ): + return None + + with self._use_memo(node): + for decorator in node.decorator_list.copy(): + if self._memo.name_matches(decorator, "typeguard.typechecked"): + # Remove the decorator to prevent duplicate instrumentation + node.decorator_list.remove(decorator) + + # Store any configuration overrides + if isinstance(decorator, Call) and decorator.keywords: + self._memo.configuration_overrides.update( + {kw.arg: kw.value for kw in decorator.keywords if kw.arg} + ) + + self.generic_visit(node) + return node + + def visit_FunctionDef( + self, node: FunctionDef | AsyncFunctionDef + ) -> FunctionDef | AsyncFunctionDef | None: + """ + Injects type checks for function arguments, and for a return of None if the + function is annotated to return something else than Any or None, and the body + ends without an explicit "return". + + """ + self._memo.local_names.add(node.name) + + # Eliminate top level functions not belonging to the target path + if ( + self._target_path is not None + and not self._memo.path + and node.name != self._target_path[0] + ): + return None + + # Skip instrumentation if we're instrumenting the whole module and the function + # contains either @no_type_check or @typeguard_ignore + if self._target_path is None: + for decorator in node.decorator_list: + if self._memo.name_matches(decorator, *ignore_decorators): + return node + + with self._use_memo(node): + arg_annotations: dict[str, Any] = {} + if self._target_path is None or self._memo.path == self._target_path: + # Find line number we're supposed to match against + if node.decorator_list: + first_lineno = node.decorator_list[0].lineno + else: + first_lineno = node.lineno + + for decorator in node.decorator_list.copy(): + if self._memo.name_matches(decorator, "typing.overload"): + # Remove overloads entirely + return None + elif self._memo.name_matches(decorator, "typeguard.typechecked"): + # Remove the decorator to prevent duplicate instrumentation + node.decorator_list.remove(decorator) + + # Store any configuration overrides + if isinstance(decorator, Call) and decorator.keywords: + self._memo.configuration_overrides = { + kw.arg: kw.value for kw in decorator.keywords if kw.arg + } + + if self.target_lineno == first_lineno: + assert self.target_node is None + self.target_node = node + if node.decorator_list: + self.target_lineno = node.decorator_list[0].lineno + else: + self.target_lineno = node.lineno + + all_args = node.args.posonlyargs + node.args.args + node.args.kwonlyargs + + # Ensure that any type shadowed by the positional or keyword-only + # argument names are ignored in this function + for arg in all_args: + self._memo.ignored_names.add(arg.arg) + + # Ensure that any type shadowed by the variable positional argument name + # (e.g. "args" in *args) is ignored this function + if node.args.vararg: + self._memo.ignored_names.add(node.args.vararg.arg) + + # Ensure that any type shadowed by the variable keywrod argument name + # (e.g. "kwargs" in *kwargs) is ignored this function + if node.args.kwarg: + self._memo.ignored_names.add(node.args.kwarg.arg) + + for arg in all_args: + annotation = self._convert_annotation(deepcopy(arg.annotation)) + if annotation: + arg_annotations[arg.arg] = annotation + + if node.args.vararg: + annotation_ = self._convert_annotation(node.args.vararg.annotation) + if annotation_: + container = Name("tuple", ctx=Load()) + subscript_slice = Tuple( + [ + annotation_, + Constant(Ellipsis), + ], + ctx=Load(), + ) + arg_annotations[node.args.vararg.arg] = Subscript( + container, subscript_slice, ctx=Load() + ) + + if node.args.kwarg: + annotation_ = self._convert_annotation(node.args.kwarg.annotation) + if annotation_: + container = Name("dict", ctx=Load()) + subscript_slice = Tuple( + [ + Name("str", ctx=Load()), + annotation_, + ], + ctx=Load(), + ) + arg_annotations[node.args.kwarg.arg] = Subscript( + container, subscript_slice, ctx=Load() + ) + + if arg_annotations: + self._memo.variable_annotations.update(arg_annotations) + + self.generic_visit(node) + + if arg_annotations: + annotations_dict = Dict( + keys=[Constant(key) for key in arg_annotations.keys()], + values=[ + Tuple([Name(key, ctx=Load()), annotation], ctx=Load()) + for key, annotation in arg_annotations.items() + ], + ) + func_name = self._get_import( + "typeguard._functions", "check_argument_types" + ) + args = [ + self._memo.joined_path, + annotations_dict, + self._memo.get_memo_name(), + ] + node.body.insert( + self._memo.code_inject_index, Expr(Call(func_name, args, [])) + ) + + # Add a checked "return None" to the end if there's no explicit return + # Skip if the return annotation is None or Any + if ( + self._memo.return_annotation + and (not self._memo.is_async or not self._memo.has_yield_expressions) + and not isinstance(node.body[-1], Return) + and ( + not isinstance(self._memo.return_annotation, Constant) + or self._memo.return_annotation.value is not None + ) + ): + func_name = self._get_import( + "typeguard._functions", "check_return_type" + ) + return_node = Return( + Call( + func_name, + [ + self._memo.joined_path, + Constant(None), + self._memo.return_annotation, + self._memo.get_memo_name(), + ], + [], + ) + ) + + # Replace a placeholder "pass" at the end + if isinstance(node.body[-1], Pass): + copy_location(return_node, node.body[-1]) + del node.body[-1] + + node.body.append(return_node) + + # Insert code to create the call memo, if it was ever needed for this + # function + if self._memo.memo_var_name: + memo_kwargs: dict[str, Any] = {} + if self._memo.parent and isinstance(self._memo.parent.node, ClassDef): + for decorator in node.decorator_list: + if ( + isinstance(decorator, Name) + and decorator.id == "staticmethod" + ): + break + elif ( + isinstance(decorator, Name) + and decorator.id == "classmethod" + ): + arglist = node.args.posonlyargs or node.args.args + memo_kwargs["self_type"] = Name( + id=arglist[0].arg, ctx=Load() + ) + break + else: + if arglist := node.args.posonlyargs or node.args.args: + if node.name == "__new__": + memo_kwargs["self_type"] = Name( + id=arglist[0].arg, ctx=Load() + ) + else: + memo_kwargs["self_type"] = Attribute( + Name(id=arglist[0].arg, ctx=Load()), + "__class__", + ctx=Load(), + ) + + # Construct the function reference + # Nested functions get special treatment: the function name is added + # to free variables (and the closure of the resulting function) + names: list[str] = [node.name] + memo = self._memo.parent + while memo: + if isinstance(memo.node, (FunctionDef, AsyncFunctionDef)): + # This is a nested function. Use the function name as-is. + del names[:-1] + break + elif not isinstance(memo.node, ClassDef): + break + + names.insert(0, memo.node.name) + memo = memo.parent + + config_keywords = self._memo.get_config_keywords() + if config_keywords: + memo_kwargs["config"] = Call( + self._get_import("dataclasses", "replace"), + [self._get_import("typeguard._config", "global_config")], + config_keywords, + ) + + self._memo.memo_var_name.id = self._memo.get_unused_name("memo") + memo_store_name = Name(id=self._memo.memo_var_name.id, ctx=Store()) + globals_call = Call(Name(id="globals", ctx=Load()), [], []) + locals_call = Call(Name(id="locals", ctx=Load()), [], []) + memo_expr = Call( + self._get_import("typeguard", "TypeCheckMemo"), + [globals_call, locals_call], + [keyword(key, value) for key, value in memo_kwargs.items()], + ) + node.body.insert( + self._memo.code_inject_index, + Assign([memo_store_name], memo_expr), + ) + + self._memo.insert_imports(node) + + # Special case the __new__() method to create a local alias from the + # class name to the first argument (usually "cls") + if ( + isinstance(node, FunctionDef) + and node.args + and self._memo.parent is not None + and isinstance(self._memo.parent.node, ClassDef) + and node.name == "__new__" + ): + first_args_expr = Name(node.args.args[0].arg, ctx=Load()) + cls_name = Name(self._memo.parent.node.name, ctx=Store()) + node.body.insert( + self._memo.code_inject_index, + Assign([cls_name], first_args_expr), + ) + + # Rmove any placeholder "pass" at the end + if isinstance(node.body[-1], Pass): + del node.body[-1] + + return node + + def visit_AsyncFunctionDef( + self, node: AsyncFunctionDef + ) -> FunctionDef | AsyncFunctionDef | None: + return self.visit_FunctionDef(node) + + def visit_Return(self, node: Return) -> Return: + """This injects type checks into "return" statements.""" + self.generic_visit(node) + if ( + self._memo.return_annotation + and self._memo.should_instrument + and not self._memo.is_ignored_name(self._memo.return_annotation) + ): + func_name = self._get_import("typeguard._functions", "check_return_type") + old_node = node + retval = old_node.value or Constant(None) + node = Return( + Call( + func_name, + [ + self._memo.joined_path, + retval, + self._memo.return_annotation, + self._memo.get_memo_name(), + ], + [], + ) + ) + copy_location(node, old_node) + + return node + + def visit_Yield(self, node: Yield) -> Yield | Call: + """ + This injects type checks into "yield" expressions, checking both the yielded + value and the value sent back to the generator, when appropriate. + + """ + self._memo.has_yield_expressions = True + self.generic_visit(node) + + if ( + self._memo.yield_annotation + and self._memo.should_instrument + and not self._memo.is_ignored_name(self._memo.yield_annotation) + ): + func_name = self._get_import("typeguard._functions", "check_yield_type") + yieldval = node.value or Constant(None) + node.value = Call( + func_name, + [ + self._memo.joined_path, + yieldval, + self._memo.yield_annotation, + self._memo.get_memo_name(), + ], + [], + ) + + if ( + self._memo.send_annotation + and self._memo.should_instrument + and not self._memo.is_ignored_name(self._memo.send_annotation) + ): + func_name = self._get_import("typeguard._functions", "check_send_type") + old_node = node + call_node = Call( + func_name, + [ + self._memo.joined_path, + old_node, + self._memo.send_annotation, + self._memo.get_memo_name(), + ], + [], + ) + copy_location(call_node, old_node) + return call_node + + return node + + def visit_AnnAssign(self, node: AnnAssign) -> Any: + """ + This injects a type check into a local variable annotation-assignment within a + function body. + + """ + self.generic_visit(node) + + if ( + isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)) + and node.annotation + and isinstance(node.target, Name) + ): + self._memo.ignored_names.add(node.target.id) + annotation = self._convert_annotation(deepcopy(node.annotation)) + if annotation: + self._memo.variable_annotations[node.target.id] = annotation + if node.value: + func_name = self._get_import( + "typeguard._functions", "check_variable_assignment" + ) + targets_arg = List( + [ + List( + [ + Tuple( + [Constant(node.target.id), annotation], + ctx=Load(), + ) + ], + ctx=Load(), + ) + ], + ctx=Load(), + ) + node.value = Call( + func_name, + [ + node.value, + targets_arg, + self._memo.get_memo_name(), + ], + [], + ) + + return node + + def visit_Assign(self, node: Assign) -> Any: + """ + This injects a type check into a local variable assignment within a function + body. The variable must have been annotated earlier in the function body. + + """ + self.generic_visit(node) + + # Only instrument function-local assignments + if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)): + preliminary_targets: list[list[tuple[Constant, expr | None]]] = [] + check_required = False + for target in node.targets: + elts: Sequence[expr] + if isinstance(target, Name): + elts = [target] + elif isinstance(target, Tuple): + elts = target.elts + else: + continue + + annotations_: list[tuple[Constant, expr | None]] = [] + for exp in elts: + prefix = "" + if isinstance(exp, Starred): + exp = exp.value + prefix = "*" + + path: list[str] = [] + while isinstance(exp, Attribute): + path.insert(0, exp.attr) + exp = exp.value + + if isinstance(exp, Name): + if not path: + self._memo.ignored_names.add(exp.id) + + path.insert(0, exp.id) + name = prefix + ".".join(path) + if len(path) == 1 and ( + annotation := self._memo.variable_annotations.get(exp.id) + ): + annotations_.append((Constant(name), annotation)) + check_required = True + else: + annotations_.append((Constant(name), None)) + + preliminary_targets.append(annotations_) + + if check_required: + # Replace missing annotations with typing.Any + targets: list[list[tuple[Constant, expr]]] = [] + for items in preliminary_targets: + target_list: list[tuple[Constant, expr]] = [] + targets.append(target_list) + for key, expression in items: + if expression is None: + target_list.append((key, self._get_import("typing", "Any"))) + else: + target_list.append((key, expression)) + + func_name = self._get_import( + "typeguard._functions", "check_variable_assignment" + ) + targets_arg = List( + [ + List( + [Tuple([name, ann], ctx=Load()) for name, ann in target], + ctx=Load(), + ) + for target in targets + ], + ctx=Load(), + ) + node.value = Call( + func_name, + [node.value, targets_arg, self._memo.get_memo_name()], + [], + ) + + return node + + def visit_NamedExpr(self, node: NamedExpr) -> Any: + """This injects a type check into an assignment expression (a := foo()).""" + self.generic_visit(node) + + # Only instrument function-local assignments + if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)) and isinstance( + node.target, Name + ): + self._memo.ignored_names.add(node.target.id) + + # Bail out if no matching annotation is found + annotation = self._memo.variable_annotations.get(node.target.id) + if annotation is None: + return node + + func_name = self._get_import( + "typeguard._functions", "check_variable_assignment" + ) + node.value = Call( + func_name, + [ + node.value, + List( + [ + List( + [ + Tuple( + [Constant(node.target.id), annotation], + ctx=Load(), + ) + ], + ctx=Load(), + ) + ], + ctx=Load(), + ), + self._memo.get_memo_name(), + ], + [], + ) + + return node + + def visit_AugAssign(self, node: AugAssign) -> Any: + """ + This injects a type check into an augmented assignment expression (a += 1). + + """ + self.generic_visit(node) + + # Only instrument function-local assignments + if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)) and isinstance( + node.target, Name + ): + # Bail out if no matching annotation is found + annotation = self._memo.variable_annotations.get(node.target.id) + if annotation is None: + return node + + # Bail out if the operator is not found (newer Python version?) + try: + operator_func_name = aug_assign_functions[node.op.__class__] + except KeyError: + return node + + operator_func = self._get_import("operator", operator_func_name) + operator_call = Call( + operator_func, [Name(node.target.id, ctx=Load()), node.value], [] + ) + targets_arg = List( + [ + List( + [Tuple([Constant(node.target.id), annotation], ctx=Load())], + ctx=Load(), + ) + ], + ctx=Load(), + ) + check_call = Call( + self._get_import("typeguard._functions", "check_variable_assignment"), + [ + operator_call, + targets_arg, + self._memo.get_memo_name(), + ], + [], + ) + return Assign(targets=[node.target], value=check_call) + + return node + + def visit_If(self, node: If) -> Any: + """ + This blocks names from being collected from a module-level + "if typing.TYPE_CHECKING:" block, so that they won't be type checked. + + """ + self.generic_visit(node) + + if ( + self._memo is self._module_memo + and isinstance(node.test, Name) + and self._memo.name_matches(node.test, "typing.TYPE_CHECKING") + ): + collector = NameCollector() + collector.visit(node) + self._memo.ignored_names.update(collector.names) + + return node diff --git a/python/code/typeguard/_union_transformer.py b/python/code/typeguard/_union_transformer.py new file mode 100644 index 00000000..1c296d35 --- /dev/null +++ b/python/code/typeguard/_union_transformer.py @@ -0,0 +1,43 @@ +""" +Transforms lazily evaluated PEP 604 unions into typing.Unions, for compatibility with +Python versions older than 3.10. +""" + +from __future__ import annotations + +from ast import ( + BinOp, + BitOr, + Load, + Name, + NodeTransformer, + Subscript, + Tuple, + fix_missing_locations, + parse, +) +from types import CodeType +from typing import Any + + +class UnionTransformer(NodeTransformer): + def __init__(self, union_name: Name | None = None): + self.union_name = union_name or Name(id="Union", ctx=Load()) + + def visit_BinOp(self, node: BinOp) -> Any: + self.generic_visit(node) + if isinstance(node.op, BitOr): + return Subscript( + value=self.union_name, + slice=Tuple(elts=[node.left, node.right], ctx=Load()), + ctx=Load(), + ) + + return node + + +def compile_type_hint(hint: str) -> CodeType: + parsed = parse(hint, "", "eval") + UnionTransformer().visit(parsed) + fix_missing_locations(parsed) + return compile(parsed, "", "eval", flags=0) diff --git a/python/code/typeguard/_utils.py b/python/code/typeguard/_utils.py new file mode 100644 index 00000000..72b9b861 --- /dev/null +++ b/python/code/typeguard/_utils.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import inspect +import sys +from importlib import import_module +from inspect import currentframe +from types import FrameType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ForwardRef, + Union, + cast, + final, + get_args, + get_origin, +) + +if TYPE_CHECKING: + from ._memo import TypeCheckMemo + +if sys.version_info >= (3, 14): + + def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + return forwardref.evaluate( + globals=memo.globals, locals=memo.locals, type_params=() + ) +elif sys.version_info >= (3, 13): + + def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + return forwardref._evaluate( + memo.globals, memo.locals, type_params=(), recursive_guard=frozenset() + ) +else: + + def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + try: + return forwardref._evaluate( + memo.globals, memo.locals, recursive_guard=frozenset() + ) + except NameError: + if sys.version_info < (3, 10): + # Try again, with the type substitutions (list -> List etc.) in place + new_globals = memo.globals.copy() + new_globals.setdefault("Union", Union) + + return forwardref._evaluate( + new_globals, memo.locals or new_globals, recursive_guard=frozenset() + ) + + raise + + +def get_type_name(type_: Any, noPrefix: list[str]=[]) -> str: + name: str + for attrname in "__name__", "_name", "__forward_arg__": + candidate = getattr(type_, attrname, None) + if isinstance(candidate, str): + name = candidate + break + else: + origin = get_origin(type_) + candidate = getattr(origin, "_name", None) + if candidate is None: + candidate = type_.__class__.__name__.strip("_") + + if isinstance(candidate, str): + name = candidate + else: + return "(unknown)" + + args = get_args(type_) + if args: + if name == "Literal": + formatted_args = ", ".join(repr(arg) for arg in args) + else: + formatted_args = ", ".join(get_type_name(arg) for arg in args) + + name += f"[{formatted_args}]" + + # For ForwardRefs, use the module stored on the object if available + if hasattr(type_, "__forward_module__"): + module = type_.__forward_module__ + else: + module = getattr(type_, "__module__", None) + if module and module not in (None, "typing", "typing_extensions", "builtins") + tuple(noPrefix): + name = module + "." + name + + return name + + +def qualified_name(obj: Any, *, add_class_prefix: bool = False) -> str: + """ + Return the qualified name (e.g. package.module.Type) for the given object. + + Builtins and types from the :mod:`typing` package get special treatment by having + the module name stripped from the generated name. + + """ + if obj is None: + return "None" + elif inspect.isclass(obj): + prefix = "class " if add_class_prefix else "" + type_ = obj + else: + prefix = "" + type_ = type(obj) + + module = type_.__module__ + qualname = type_.__qualname__ + name = qualname if module in ("typing", "builtins") else f"{module}.{qualname}" + return prefix + name + + +def function_name(func: Callable[..., Any]) -> str: + """ + Return the qualified name of the given function. + + Builtins and types from the :mod:`typing` package get special treatment by having + the module name stripped from the generated name. + + """ + # For partial functions and objects with __call__ defined, __qualname__ does not + # exist + module = getattr(func, "__module__", "") + qualname = (module + ".") if module not in ("builtins", "") else "" + return qualname + getattr(func, "__qualname__", repr(func)) + + +def resolve_reference(reference: str) -> Any: + modulename, varname = reference.partition(":")[::2] + if not modulename or not varname: + raise ValueError(f"{reference!r} is not a module:varname reference") + + obj = import_module(modulename) + for attr in varname.split("."): + obj = getattr(obj, attr) + + return obj + + +def is_method_of(obj: object, cls: type) -> bool: + return ( + inspect.isfunction(obj) + and obj.__module__ == cls.__module__ + and obj.__qualname__.startswith(cls.__qualname__ + ".") + ) + + +def get_stacklevel() -> int: + level = 1 + frame = cast(FrameType, currentframe()).f_back + while frame and frame.f_globals.get("__name__", "").startswith("typeguard."): + level += 1 + frame = frame.f_back + + return level + + +@final +class Unset: + __slots__ = () + + def __repr__(self) -> str: + return "" + + +unset = Unset() diff --git a/python/deps/untypy/examples/mylogfile b/python/code/typeguard/py.typed similarity index 100% rename from python/deps/untypy/examples/mylogfile rename to python/code/typeguard/py.typed diff --git a/python/code/typing_extensions.py b/python/code/typing_extensions.py new file mode 100644 index 00000000..efa09d55 --- /dev/null +++ b/python/code/typing_extensions.py @@ -0,0 +1,4244 @@ +import abc +import builtins +import collections +import collections.abc +import contextlib +import enum +import functools +import inspect +import io +import keyword +import operator +import sys +import types as _types +import typing +import warnings + +if sys.version_info >= (3, 14): + import annotationlib + +__all__ = [ + # Super-special typing primitives. + 'Any', + 'ClassVar', + 'Concatenate', + 'Final', + 'LiteralString', + 'ParamSpec', + 'ParamSpecArgs', + 'ParamSpecKwargs', + 'Self', + 'Type', + 'TypeVar', + 'TypeVarTuple', + 'Unpack', + + # ABCs (from collections.abc). + 'Awaitable', + 'AsyncIterator', + 'AsyncIterable', + 'Coroutine', + 'AsyncGenerator', + 'AsyncContextManager', + 'Buffer', + 'ChainMap', + + # Concrete collection types. + 'ContextManager', + 'Counter', + 'Deque', + 'DefaultDict', + 'NamedTuple', + 'OrderedDict', + 'TypedDict', + + # Structural checks, a.k.a. protocols. + 'SupportsAbs', + 'SupportsBytes', + 'SupportsComplex', + 'SupportsFloat', + 'SupportsIndex', + 'SupportsInt', + 'SupportsRound', + 'Reader', + 'Writer', + + # One-off things. + 'Annotated', + 'assert_never', + 'assert_type', + 'clear_overloads', + 'dataclass_transform', + 'deprecated', + 'Doc', + 'evaluate_forward_ref', + 'get_overloads', + 'final', + 'Format', + 'get_annotations', + 'get_args', + 'get_origin', + 'get_original_bases', + 'get_protocol_members', + 'get_type_hints', + 'IntVar', + 'is_protocol', + 'is_typeddict', + 'Literal', + 'NewType', + 'overload', + 'override', + 'Protocol', + 'Sentinel', + 'reveal_type', + 'runtime', + 'runtime_checkable', + 'Text', + 'TypeAlias', + 'TypeAliasType', + 'TypeForm', + 'TypeGuard', + 'TypeIs', + 'TYPE_CHECKING', + 'Never', + 'NoReturn', + 'ReadOnly', + 'Required', + 'NotRequired', + 'NoDefault', + 'NoExtraItems', + + # Pure aliases, have always been in typing + 'AbstractSet', + 'AnyStr', + 'BinaryIO', + 'Callable', + 'Collection', + 'Container', + 'Dict', + 'ForwardRef', + 'FrozenSet', + 'Generator', + 'Generic', + 'Hashable', + 'IO', + 'ItemsView', + 'Iterable', + 'Iterator', + 'KeysView', + 'List', + 'Mapping', + 'MappingView', + 'Match', + 'MutableMapping', + 'MutableSequence', + 'MutableSet', + 'Optional', + 'Pattern', + 'Reversible', + 'Sequence', + 'Set', + 'Sized', + 'TextIO', + 'Tuple', + 'Union', + 'ValuesView', + 'cast', + 'no_type_check', + 'no_type_check_decorator', +] + +# for backward compatibility +PEP_560 = True +GenericMeta = type +_PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") + +# Added with bpo-45166 to 3.10.1+ and some 3.9 versions +_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ + +# The functions below are modified copies of typing internal helpers. +# They are needed by _ProtocolMeta and they provide support for PEP 646. + + +class _Sentinel: + def __repr__(self): + return "" + + +_marker = _Sentinel() + + +if sys.version_info >= (3, 10): + def _should_collect_from_parameters(t): + return isinstance( + t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType) + ) +else: + def _should_collect_from_parameters(t): + return isinstance(t, (typing._GenericAlias, _types.GenericAlias)) + + +NoReturn = typing.NoReturn + +# Some unconstrained type variables. These are used by the container types. +# (These are not for export.) +T = typing.TypeVar('T') # Any type. +KT = typing.TypeVar('KT') # Key type. +VT = typing.TypeVar('VT') # Value type. +T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers. +T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant. + + +if sys.version_info >= (3, 11): + from typing import Any +else: + + class _AnyMeta(type): + def __instancecheck__(self, obj): + if self is Any: + raise TypeError("typing_extensions.Any cannot be used with isinstance()") + return super().__instancecheck__(obj) + + def __repr__(self): + if self is Any: + return "typing_extensions.Any" + return super().__repr__() + + class Any(metaclass=_AnyMeta): + """Special type indicating an unconstrained type. + - Any is compatible with every type. + - Any assumed to have all methods. + - All values assumed to be instances of Any. + Note that all the above statements are true from the point of view of + static type checkers. At runtime, Any should not be used with instance + checks. + """ + def __new__(cls, *args, **kwargs): + if cls is Any: + raise TypeError("Any cannot be instantiated") + return super().__new__(cls, *args, **kwargs) + + +ClassVar = typing.ClassVar + +# Vendored from cpython typing._SpecialFrom +# Having a separate class means that instances will not be rejected by +# typing._type_check. +class _SpecialForm(typing._Final, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') + + def __init__(self, getitem): + self._getitem = getitem + self._name = getitem.__name__ + self.__doc__ = getitem.__doc__ + + def __getattr__(self, item): + if item in {'__name__', '__qualname__'}: + return self._name + + raise AttributeError(item) + + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass {self!r}") + + def __repr__(self): + return f'typing_extensions.{self._name}' + + def __reduce__(self): + return self._name + + def __call__(self, *args, **kwds): + raise TypeError(f"Cannot instantiate {self!r}") + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance()") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass()") + + @typing._tp_cache + def __getitem__(self, parameters): + return self._getitem(self, parameters) + + +# Note that inheriting from this class means that the object will be +# rejected by typing._type_check, so do not use it if the special form +# is arguably valid as a type by itself. +class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + +Final = typing.Final + +if sys.version_info >= (3, 11): + final = typing.final +else: + # @final exists in 3.8+, but we backport it for all versions + # before 3.11 to keep support for the __final__ attribute. + # See https://bugs.python.org/issue46342 + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. The decorator + sets the ``__final__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + """ + try: + f.__final__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return f + + +def IntVar(name): + return typing.TypeVar(name) + + +# A Literal bug was fixed in 3.11.0, 3.10.1 and 3.9.8 +if sys.version_info >= (3, 10, 1): + Literal = typing.Literal +else: + def _flatten_literal_params(parameters): + """An internal helper for Literal creation: flatten Literals among parameters""" + params = [] + for p in parameters: + if isinstance(p, _LiteralGenericAlias): + params.extend(p.__args__) + else: + params.append(p) + return tuple(params) + + def _value_and_type_iter(params): + for p in params: + yield p, type(p) + + class _LiteralGenericAlias(typing._GenericAlias, _root=True): + def __eq__(self, other): + if not isinstance(other, _LiteralGenericAlias): + return NotImplemented + these_args_deduped = set(_value_and_type_iter(self.__args__)) + other_args_deduped = set(_value_and_type_iter(other.__args__)) + return these_args_deduped == other_args_deduped + + def __hash__(self): + return hash(frozenset(_value_and_type_iter(self.__args__))) + + class _LiteralForm(_ExtensionsSpecialForm, _root=True): + def __init__(self, doc: str): + self._name = 'Literal' + self._doc = self.__doc__ = doc + + def __getitem__(self, parameters): + if not isinstance(parameters, tuple): + parameters = (parameters,) + + parameters = _flatten_literal_params(parameters) + + val_type_pairs = list(_value_and_type_iter(parameters)) + try: + deduped_pairs = set(val_type_pairs) + except TypeError: + # unhashable parameters + pass + else: + # similar logic to typing._deduplicate on Python 3.9+ + if len(deduped_pairs) < len(val_type_pairs): + new_parameters = [] + for pair in val_type_pairs: + if pair in deduped_pairs: + new_parameters.append(pair[0]) + deduped_pairs.remove(pair) + assert not deduped_pairs, deduped_pairs + parameters = tuple(new_parameters) + + return _LiteralGenericAlias(self, parameters) + + Literal = _LiteralForm(doc="""\ + A type that can be used to indicate to type checkers + that the corresponding value has a value literally equivalent + to the provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to + the value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime + checking verifying that the parameter is actually a value + instead of a type.""") + + +_overload_dummy = typing._overload_dummy + + +if hasattr(typing, "get_overloads"): # 3.11+ + overload = typing.overload + get_overloads = typing.get_overloads + clear_overloads = typing.clear_overloads +else: + # {module: {qualname: {firstlineno: func}}} + _overload_registry = collections.defaultdict( + functools.partial(collections.defaultdict, dict) + ) + + def overload(func): + """Decorator for overloaded functions/methods. + + In a stub file, place two or more stub definitions for the same + function in a row, each decorated with @overload. For example: + + @overload + def utf8(value: None) -> None: ... + @overload + def utf8(value: bytes) -> bytes: ... + @overload + def utf8(value: str) -> bytes: ... + + In a non-stub file (i.e. a regular .py file), do the same but + follow it with an implementation. The implementation should *not* + be decorated with @overload. For example: + + @overload + def utf8(value: None) -> None: ... + @overload + def utf8(value: bytes) -> bytes: ... + @overload + def utf8(value: str) -> bytes: ... + def utf8(value): + # implementation goes here + + The overloads for a function can be retrieved at runtime using the + get_overloads() function. + """ + # classmethod and staticmethod + f = getattr(func, "__func__", func) + try: + _overload_registry[f.__module__][f.__qualname__][ + f.__code__.co_firstlineno + ] = func + except AttributeError: + # Not a normal function; ignore. + pass + return _overload_dummy + + def get_overloads(func): + """Return all defined overloads for *func* as a sequence.""" + # classmethod and staticmethod + f = getattr(func, "__func__", func) + if f.__module__ not in _overload_registry: + return [] + mod_dict = _overload_registry[f.__module__] + if f.__qualname__ not in mod_dict: + return [] + return list(mod_dict[f.__qualname__].values()) + + def clear_overloads(): + """Clear all overloads in the registry.""" + _overload_registry.clear() + + +# This is not a real generic class. Don't use outside annotations. +Type = typing.Type + +# Various ABCs mimicking those in collections.abc. +# A few are simply re-exported for completeness. +Awaitable = typing.Awaitable +Coroutine = typing.Coroutine +AsyncIterable = typing.AsyncIterable +AsyncIterator = typing.AsyncIterator +Deque = typing.Deque +DefaultDict = typing.DefaultDict +OrderedDict = typing.OrderedDict +Counter = typing.Counter +ChainMap = typing.ChainMap +Text = typing.Text +TYPE_CHECKING = typing.TYPE_CHECKING + + +if sys.version_info >= (3, 13, 0, "beta"): + from typing import AsyncContextManager, AsyncGenerator, ContextManager, Generator +else: + def _is_dunder(attr): + return attr.startswith('__') and attr.endswith('__') + + + class _SpecialGenericAlias(typing._SpecialGenericAlias, _root=True): + def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()): + super().__init__(origin, nparams, inst=inst, name=name) + self._defaults = defaults + + def __setattr__(self, attr, val): + allowed_attrs = {'_name', '_inst', '_nparams', '_defaults'} + if _is_dunder(attr) or attr in allowed_attrs: + object.__setattr__(self, attr, val) + else: + setattr(self.__origin__, attr, val) + + @typing._tp_cache + def __getitem__(self, params): + if not isinstance(params, tuple): + params = (params,) + msg = "Parameters to generic types must be types." + params = tuple(typing._type_check(p, msg) for p in params) + if ( + self._defaults + and len(params) < self._nparams + and len(params) + len(self._defaults) >= self._nparams + ): + params = (*params, *self._defaults[len(params) - self._nparams:]) + actual_len = len(params) + + if actual_len != self._nparams: + if self._defaults: + expected = f"at least {self._nparams - len(self._defaults)}" + else: + expected = str(self._nparams) + if not self._nparams: + raise TypeError(f"{self} is not a generic class") + raise TypeError( + f"Too {'many' if actual_len > self._nparams else 'few'}" + f" arguments for {self};" + f" actual {actual_len}, expected {expected}" + ) + return self.copy_with(params) + + _NoneType = type(None) + Generator = _SpecialGenericAlias( + collections.abc.Generator, 3, defaults=(_NoneType, _NoneType) + ) + AsyncGenerator = _SpecialGenericAlias( + collections.abc.AsyncGenerator, 2, defaults=(_NoneType,) + ) + ContextManager = _SpecialGenericAlias( + contextlib.AbstractContextManager, + 2, + name="ContextManager", + defaults=(typing.Optional[bool],) + ) + AsyncContextManager = _SpecialGenericAlias( + contextlib.AbstractAsyncContextManager, + 2, + name="AsyncContextManager", + defaults=(typing.Optional[bool],) + ) + + +_PROTO_ALLOWLIST = { + 'collections.abc': [ + 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', 'Buffer', + ], + 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], + 'typing_extensions': ['Buffer'], +} + + +_EXCLUDED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | { + "__match_args__", "__protocol_attrs__", "__non_callable_proto_members__", + "__final__", +} + + +def _get_protocol_attrs(cls): + attrs = set() + for base in cls.__mro__[:-1]: # without object + if base.__name__ in {'Protocol', 'Generic'}: + continue + annotations = getattr(base, '__annotations__', {}) + for attr in (*base.__dict__, *annotations): + if (not attr.startswith('_abc_') and attr not in _EXCLUDED_ATTRS): + attrs.add(attr) + return attrs + + +def _caller(depth=1, default='__main__'): + try: + return sys._getframemodulename(depth + 1) or default + except AttributeError: # For platforms without _getframemodulename() + pass + try: + return sys._getframe(depth + 1).f_globals.get('__name__', default) + except (AttributeError, ValueError): # For platforms without _getframe() + pass + return None + + +# `__match_args__` attribute was removed from protocol members in 3.13, +# we want to backport this change to older Python versions. +if sys.version_info >= (3, 13): + Protocol = typing.Protocol +else: + def _allow_reckless_class_checks(depth=2): + """Allow instance and class checks for special stdlib modules. + The abc and functools modules indiscriminately call isinstance() and + issubclass() on the whole MRO of a user class, which may contain protocols. + """ + return _caller(depth) in {'abc', 'functools', None} + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + + def _type_check_issubclass_arg_1(arg): + """Raise TypeError if `arg` is not an instance of `type` + in `issubclass(arg, )`. + + In most cases, this is verified by type.__subclasscheck__. + Checking it again unnecessarily would slow down issubclass() checks, + so, we don't perform this check unless we absolutely have to. + + For various error paths, however, + we want to ensure that *this* error message is shown to the user + where relevant, rather than a typing.py-specific error message. + """ + if not isinstance(arg, type): + # Same error message as for issubclass(1, int). + raise TypeError('issubclass() arg 1 must be a class') + + # Inheriting from typing._ProtocolMeta isn't actually desirable, + # but is necessary to allow typing.Protocol and typing_extensions.Protocol + # to mix without getting TypeErrors about "metaclass conflict" + class _ProtocolMeta(type(typing.Protocol)): + # This metaclass is somewhat unfortunate, + # but is necessary for several reasons... + # + # NOTE: DO NOT call super() in any methods in this class + # That would call the methods on typing._ProtocolMeta on Python <=3.11 + # and those are slow + def __new__(mcls, name, bases, namespace, **kwargs): + if name == "Protocol" and len(bases) < 2: + pass + elif {Protocol, typing.Protocol} & set(bases): + for base in bases: + if not ( + base in {object, typing.Generic, Protocol, typing.Protocol} + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, []) + or is_protocol(base) + ): + raise TypeError( + f"Protocols can only inherit from other protocols, " + f"got {base!r}" + ) + return abc.ABCMeta.__new__(mcls, name, bases, namespace, **kwargs) + + def __init__(cls, *args, **kwargs): + abc.ABCMeta.__init__(cls, *args, **kwargs) + if getattr(cls, "_is_protocol", False): + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + + def __subclasscheck__(cls, other): + if cls is Protocol: + return type.__subclasscheck__(cls, other) + if ( + getattr(cls, '_is_protocol', False) + and not _allow_reckless_class_checks() + ): + if not getattr(cls, '_is_runtime_protocol', False): + _type_check_issubclass_arg_1(other) + raise TypeError( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) + if ( + # this attribute is set by @runtime_checkable: + cls.__non_callable_proto_members__ + and cls.__dict__.get("__subclasshook__") is _proto_hook + ): + _type_check_issubclass_arg_1(other) + non_method_attrs = sorted(cls.__non_callable_proto_members__) + raise TypeError( + "Protocols with non-method members don't support issubclass()." + f" Non-method members: {str(non_method_attrs)[1:-1]}." + ) + return abc.ABCMeta.__subclasscheck__(cls, other) + + def __instancecheck__(cls, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if cls is Protocol: + return type.__instancecheck__(cls, instance) + if not getattr(cls, "_is_protocol", False): + # i.e., it's a concrete subclass of a protocol + return abc.ABCMeta.__instancecheck__(cls, instance) + + if ( + not getattr(cls, '_is_runtime_protocol', False) and + not _allow_reckless_class_checks() + ): + raise TypeError("Instance and class checks can only be used with" + " @runtime_checkable protocols") + + if abc.ABCMeta.__instancecheck__(cls, instance): + return True + + for attr in cls.__protocol_attrs__: + try: + val = inspect.getattr_static(instance, attr) + except AttributeError: + break + # this attribute is set by @runtime_checkable: + if val is None and attr not in cls.__non_callable_proto_members__: + break + else: + return True + + return False + + def __eq__(cls, other): + # Hack so that typing.Generic.__class_getitem__ + # treats typing_extensions.Protocol + # as equivalent to typing.Protocol + if abc.ABCMeta.__eq__(cls, other) is True: + return True + return cls is Protocol and other is typing.Protocol + + # This has to be defined, or the abc-module cache + # complains about classes with this metaclass being unhashable, + # if we define only __eq__! + def __hash__(cls) -> int: + return type.__hash__(cls) + + @classmethod + def _proto_hook(cls, other): + if not cls.__dict__.get('_is_protocol', False): + return NotImplemented + + for attr in cls.__protocol_attrs__: + for base in other.__mro__: + # Check if the members appears in the class dictionary... + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + + # ...or in annotations, if it is a sub-protocol. + annotations = getattr(base, '__annotations__', {}) + if ( + isinstance(annotations, collections.abc.Mapping) + and attr in annotations + and is_protocol(other) + ): + break + else: + return NotImplemented + return True + + class Protocol(typing.Generic, metaclass=_ProtocolMeta): + __doc__ = typing.Protocol.__doc__ + __slots__ = () + _is_protocol = True + _is_runtime_protocol = False + + def __init_subclass__(cls, *args, **kwargs): + super().__init_subclass__(*args, **kwargs) + + # Determine if this is a protocol or a concrete subclass. + if not cls.__dict__.get('_is_protocol', False): + cls._is_protocol = any(b is Protocol for b in cls.__bases__) + + # Set (or override) the protocol subclass hook. + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: + cls.__init__ = _no_init + + +if sys.version_info >= (3, 13): + runtime_checkable = typing.runtime_checkable +else: + def runtime_checkable(cls): + """Mark a protocol class as a runtime protocol. + + Such protocol can be used with isinstance() and issubclass(). + Raise TypeError if applied to a non-protocol class. + This allows a simple-minded structural check very similar to + one trick ponies in collections.abc such as Iterable. + + For example:: + + @runtime_checkable + class Closable(Protocol): + def close(self): ... + + assert isinstance(open('/some/file'), Closable) + + Warning: this will check only the presence of the required methods, + not their type signatures! + """ + if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False): + raise TypeError(f'@runtime_checkable can be only applied to protocol classes,' + f' got {cls!r}') + cls._is_runtime_protocol = True + + # typing.Protocol classes on <=3.11 break if we execute this block, + # because typing.Protocol classes on <=3.11 don't have a + # `__protocol_attrs__` attribute, and this block relies on the + # `__protocol_attrs__` attribute. Meanwhile, typing.Protocol classes on 3.12.2+ + # break if we *don't* execute this block, because *they* assume that all + # protocol classes have a `__non_callable_proto_members__` attribute + # (which this block sets) + if isinstance(cls, _ProtocolMeta) or sys.version_info >= (3, 12, 2): + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + # See gh-113320 for why we compute this attribute here, + # rather than in `_ProtocolMeta.__init__` + cls.__non_callable_proto_members__ = set() + for attr in cls.__protocol_attrs__: + try: + is_callable = callable(getattr(cls, attr, None)) + except Exception as e: + raise TypeError( + f"Failed to determine whether protocol member {attr!r} " + "is a method member" + ) from e + else: + if not is_callable: + cls.__non_callable_proto_members__.add(attr) + + return cls + + +# The "runtime" alias exists for backwards compatibility. +runtime = runtime_checkable + + +# Our version of runtime-checkable protocols is faster on Python <=3.11 +if sys.version_info >= (3, 12): + SupportsInt = typing.SupportsInt + SupportsFloat = typing.SupportsFloat + SupportsComplex = typing.SupportsComplex + SupportsBytes = typing.SupportsBytes + SupportsIndex = typing.SupportsIndex + SupportsAbs = typing.SupportsAbs + SupportsRound = typing.SupportsRound +else: + @runtime_checkable + class SupportsInt(Protocol): + """An ABC with one abstract method __int__.""" + __slots__ = () + + @abc.abstractmethod + def __int__(self) -> int: + pass + + @runtime_checkable + class SupportsFloat(Protocol): + """An ABC with one abstract method __float__.""" + __slots__ = () + + @abc.abstractmethod + def __float__(self) -> float: + pass + + @runtime_checkable + class SupportsComplex(Protocol): + """An ABC with one abstract method __complex__.""" + __slots__ = () + + @abc.abstractmethod + def __complex__(self) -> complex: + pass + + @runtime_checkable + class SupportsBytes(Protocol): + """An ABC with one abstract method __bytes__.""" + __slots__ = () + + @abc.abstractmethod + def __bytes__(self) -> bytes: + pass + + @runtime_checkable + class SupportsIndex(Protocol): + __slots__ = () + + @abc.abstractmethod + def __index__(self) -> int: + pass + + @runtime_checkable + class SupportsAbs(Protocol[T_co]): + """ + An ABC with one abstract method __abs__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __abs__(self) -> T_co: + pass + + @runtime_checkable + class SupportsRound(Protocol[T_co]): + """ + An ABC with one abstract method __round__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __round__(self, ndigits: int = 0) -> T_co: + pass + + +if hasattr(io, "Reader") and hasattr(io, "Writer"): + Reader = io.Reader + Writer = io.Writer +else: + @runtime_checkable + class Reader(Protocol[T_co]): + """Protocol for simple I/O reader instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def read(self, size: int = ..., /) -> T_co: + """Read data from the input stream and return it. + + If *size* is specified, at most *size* items (bytes/characters) will be + read. + """ + + @runtime_checkable + class Writer(Protocol[T_contra]): + """Protocol for simple I/O writer instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def write(self, data: T_contra, /) -> int: + """Write *data* to the output stream and return the number of items written.""" # noqa: E501 + + +_NEEDS_SINGLETONMETA = ( + not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") +) + +if _NEEDS_SINGLETONMETA: + class SingletonMeta(type): + def __setattr__(cls, attr, value): + # TypeError is consistent with the behavior of NoneType + raise TypeError( + f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" + ) + + +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType(metaclass=SingletonMeta): + """The type of the NoDefault singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoDefault") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoDefault" + + def __reduce__(self): + return "NoDefault" + + NoDefault = NoDefaultType() + del NoDefaultType + +if hasattr(typing, "NoExtraItems"): + NoExtraItems = typing.NoExtraItems +else: + class NoExtraItemsType(metaclass=SingletonMeta): + """The type of the NoExtraItems singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoExtraItems") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoExtraItems" + + def __reduce__(self): + return "NoExtraItems" + + NoExtraItems = NoExtraItemsType() + del NoExtraItemsType + +if _NEEDS_SINGLETONMETA: + del SingletonMeta + + +# Update this to something like >=3.13.0b1 if and when +# PEP 728 is implemented in CPython +_PEP_728_IMPLEMENTED = False + +if _PEP_728_IMPLEMENTED: + # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" + # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 + # The standard library TypedDict below Python 3.11 does not store runtime + # information about optional and required keys when using Required or NotRequired. + # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11. + # Aaaand on 3.12 we add __orig_bases__ to TypedDict + # to enable better runtime introspection. + # On 3.13 we deprecate some odd ways of creating TypedDicts. + # Also on 3.13, PEP 705 adds the ReadOnly[] qualifier. + # PEP 728 (still pending) makes more changes. + TypedDict = typing.TypedDict + _TypedDictMeta = typing._TypedDictMeta + is_typeddict = typing.is_typeddict +else: + # 3.10.0 and later + _TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters + + def _get_typeddict_qualifiers(annotation_type): + while True: + annotation_origin = get_origin(annotation_type) + if annotation_origin is Annotated: + annotation_args = get_args(annotation_type) + if annotation_args: + annotation_type = annotation_args[0] + else: + break + elif annotation_origin is Required: + yield Required + annotation_type, = get_args(annotation_type) + elif annotation_origin is NotRequired: + yield NotRequired + annotation_type, = get_args(annotation_type) + elif annotation_origin is ReadOnly: + yield ReadOnly + annotation_type, = get_args(annotation_type) + else: + break + + class _TypedDictMeta(type): + + def __new__(cls, name, bases, ns, *, total=True, closed=None, + extra_items=NoExtraItems): + """Create new typed dict class object. + + This method is called when TypedDict is subclassed, + or when TypedDict is instantiated. This way + TypedDict supports all three syntax forms described in its docstring. + Subclasses and instances of TypedDict return actual dictionaries. + """ + for base in bases: + if type(base) is not _TypedDictMeta and base is not typing.Generic: + raise TypeError('cannot inherit from both a TypedDict type ' + 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") + + if any(issubclass(b, typing.Generic) for b in bases): + generic_base = (typing.Generic,) + else: + generic_base = () + + ns_annotations = ns.pop('__annotations__', None) + + # typing.py generally doesn't let you inherit from plain Generic, unless + # the name of the class happens to be "Protocol" + tp_dict = type.__new__(_TypedDictMeta, "Protocol", (*generic_base, dict), ns) + tp_dict.__name__ = name + if tp_dict.__qualname__ == "Protocol": + tp_dict.__qualname__ = name + + if not hasattr(tp_dict, '__orig_bases__'): + tp_dict.__orig_bases__ = bases + + annotations = {} + own_annotate = None + if ns_annotations is not None: + own_annotations = ns_annotations + elif sys.version_info >= (3, 14): + if hasattr(annotationlib, "get_annotate_from_class_namespace"): + own_annotate = annotationlib.get_annotate_from_class_namespace(ns) + else: + # 3.14.0a7 and earlier + own_annotate = ns.get("__annotate__") + if own_annotate is not None: + own_annotations = annotationlib.call_annotate_function( + own_annotate, Format.FORWARDREF, owner=tp_dict + ) + else: + own_annotations = {} + else: + own_annotations = {} + msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" + if _TAKES_MODULE: + own_checked_annotations = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own_annotations.items() + } + else: + own_checked_annotations = { + n: typing._type_check(tp, msg) + for n, tp in own_annotations.items() + } + required_keys = set() + optional_keys = set() + readonly_keys = set() + mutable_keys = set() + extra_items_type = extra_items + + for base in bases: + base_dict = base.__dict__ + + if sys.version_info <= (3, 14): + annotations.update(base_dict.get('__annotations__', {})) + required_keys.update(base_dict.get('__required_keys__', ())) + optional_keys.update(base_dict.get('__optional_keys__', ())) + readonly_keys.update(base_dict.get('__readonly_keys__', ())) + mutable_keys.update(base_dict.get('__mutable_keys__', ())) + + # This was specified in an earlier version of PEP 728. Support + # is retained for backwards compatibility, but only for Python + # 3.13 and lower. + if (closed and sys.version_info < (3, 14) + and "__extra_items__" in own_checked_annotations): + annotation_type = own_checked_annotations.pop("__extra_items__") + qualifiers = set(_get_typeddict_qualifiers(annotation_type)) + if Required in qualifiers: + raise TypeError( + "Special key __extra_items__ does not support " + "Required" + ) + if NotRequired in qualifiers: + raise TypeError( + "Special key __extra_items__ does not support " + "NotRequired" + ) + extra_items_type = annotation_type + + annotations.update(own_checked_annotations) + for annotation_key, annotation_type in own_checked_annotations.items(): + qualifiers = set(_get_typeddict_qualifiers(annotation_type)) + + if Required in qualifiers: + required_keys.add(annotation_key) + elif NotRequired in qualifiers: + optional_keys.add(annotation_key) + elif total: + required_keys.add(annotation_key) + else: + optional_keys.add(annotation_key) + if ReadOnly in qualifiers: + mutable_keys.discard(annotation_key) + readonly_keys.add(annotation_key) + else: + mutable_keys.add(annotation_key) + readonly_keys.discard(annotation_key) + + if sys.version_info >= (3, 14): + def __annotate__(format): + annos = {} + for base in bases: + if base is Generic: + continue + base_annotate = base.__annotate__ + if base_annotate is None: + continue + base_annos = annotationlib.call_annotate_function( + base_annotate, format, owner=base) + annos.update(base_annos) + if own_annotate is not None: + own = annotationlib.call_annotate_function( + own_annotate, format, owner=tp_dict) + if format != Format.STRING: + own = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own.items() + } + elif format == Format.STRING: + own = annotationlib.annotations_to_string(own_annotations) + elif format in (Format.FORWARDREF, Format.VALUE): + own = own_checked_annotations + else: + raise NotImplementedError(format) + annos.update(own) + return annos + + tp_dict.__annotate__ = __annotate__ + else: + tp_dict.__annotations__ = annotations + tp_dict.__required_keys__ = frozenset(required_keys) + tp_dict.__optional_keys__ = frozenset(optional_keys) + tp_dict.__readonly_keys__ = frozenset(readonly_keys) + tp_dict.__mutable_keys__ = frozenset(mutable_keys) + tp_dict.__total__ = total + tp_dict.__closed__ = closed + tp_dict.__extra_items__ = extra_items_type + return tp_dict + + __call__ = dict # static method + + def __subclasscheck__(cls, other): + # Typed dicts are only for static structural subtyping. + raise TypeError('TypedDict does not support instance and class checks') + + __instancecheck__ = __subclasscheck__ + + _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) + + def _create_typeddict( + typename, + fields, + /, + *, + typing_is_inline, + total, + closed, + extra_items, + **kwargs, + ): + if fields is _marker or fields is None: + if fields is _marker: + deprecated_thing = ( + "Failing to pass a value for the 'fields' parameter" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + + example = f"`{typename} = TypedDict({typename!r}, {{}})`" + deprecation_msg = ( + f"{deprecated_thing} is deprecated and will be disallowed in " + "Python 3.15. To create a TypedDict class with 0 fields " + "using the functional syntax, pass an empty dictionary, e.g. " + ) + example + "." + warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: + kwargs["closed"] = closed + closed = None + # Or "extra_items" + if extra_items is not NoExtraItems: + kwargs["extra_items"] = extra_items + extra_items = NoExtraItems + fields = kwargs + elif kwargs: + raise TypeError("TypedDict takes either a dict or keyword arguments," + " but not both") + if kwargs: + if sys.version_info >= (3, 13): + raise TypeError("TypedDict takes no keyword arguments") + warnings.warn( + "The kwargs-based syntax for TypedDict definitions is deprecated " + "in Python 3.11, will be removed in Python 3.13, and may not be " + "understood by third-party type checkers.", + DeprecationWarning, + stacklevel=2, + ) + + ns = {'__annotations__': dict(fields)} + module = _caller(depth=4 if typing_is_inline else 2) + if module is not None: + # Setting correct module is necessary to make typed dict classes + # pickleable. + ns['__module__'] = module + + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) + td.__orig_bases__ = (TypedDict,) + return td + + class _TypedDictSpecialForm(_SpecialForm, _root=True): + def __call__( + self, + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **kwargs + ): + return _create_typeddict( + typename, + fields, + typing_is_inline=False, + total=total, + closed=closed, + extra_items=extra_items, + **kwargs, + ) + + def __mro_entries__(self, bases): + return (_TypedDict,) + + @_TypedDictSpecialForm + def TypedDict(self, args): + """A simple typed namespace. At runtime it is equivalent to a plain dict. + + TypedDict creates a dictionary type such that a type checker will expect all + instances to have a certain set of keys, where each key is + associated with a value of a consistent type. This expectation + is not checked at runtime. + + Usage:: + + class Point2D(TypedDict): + x: int + y: int + label: str + + a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK + b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check + + assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') + + The type info can be accessed via the Point2D.__annotations__ dict, and + the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. + TypedDict supports an additional equivalent form:: + + Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) + + By default, all keys must be present in a TypedDict. It is possible + to override this by specifying totality:: + + class Point2D(TypedDict, total=False): + x: int + y: int + + This means that a Point2D TypedDict can have any of the keys omitted. A type + checker is only expected to support a literal False or True as the value of + the total argument. True is the default, and makes all items defined in the + class body be required. + + The Required and NotRequired special forms can also be used to mark + individual keys as being required or not required:: + + class Point2D(TypedDict): + x: int # the "x" key must always be present (Required is the default) + y: NotRequired[int] # the "y" key can be omitted + + See PEP 655 for more details on Required and NotRequired. + """ + # This runs when creating inline TypedDicts: + if not isinstance(args, dict): + raise TypeError( + "TypedDict[...] should be used with a single dict argument" + ) + + return _create_typeddict( + "", + args, + typing_is_inline=True, + total=True, + closed=True, + extra_items=NoExtraItems, + ) + + _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) + + def is_typeddict(tp): + """Check if an annotation is a TypedDict class + + For example:: + class Film(TypedDict): + title: str + year: int + + is_typeddict(Film) # => True + is_typeddict(Union[list, str]) # => False + """ + return isinstance(tp, _TYPEDDICT_TYPES) + + +if hasattr(typing, "assert_type"): + assert_type = typing.assert_type + +else: + def assert_type(val, typ, /): + """Assert (to the type checker) that the value is of the given type. + + When the type checker encounters a call to assert_type(), it + emits an error if the value is not of the specified type:: + + def greet(name: str) -> None: + assert_type(name, str) # ok + assert_type(name, int) # type checker error + + At runtime this returns the first argument unchanged and otherwise + does nothing. + """ + return val + + +if hasattr(typing, "ReadOnly"): # 3.13+ + get_type_hints = typing.get_type_hints +else: # <=3.13 + # replaces _strip_annotations() + def _strip_extras(t): + """Strips Annotated, Required and NotRequired from a given type.""" + if isinstance(t, typing._AnnotatedAlias): + return _strip_extras(t.__origin__) + if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly): + return _strip_extras(t.__args__[0]) + if isinstance(t, typing._GenericAlias): + stripped_args = tuple(_strip_extras(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + return t.copy_with(stripped_args) + if hasattr(_types, "GenericAlias") and isinstance(t, _types.GenericAlias): + stripped_args = tuple(_strip_extras(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + return _types.GenericAlias(t.__origin__, stripped_args) + if hasattr(_types, "UnionType") and isinstance(t, _types.UnionType): + stripped_args = tuple(_strip_extras(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + return functools.reduce(operator.or_, stripped_args) + + return t + + def get_type_hints(obj, globalns=None, localns=None, include_extras=False): + """Return type hints for an object. + + This is often the same as obj.__annotations__, but it handles + forward references encoded as string literals, adds Optional[t] if a + default value equal to None is set and recursively replaces all + 'Annotated[T, ...]', 'Required[T]' or 'NotRequired[T]' with 'T' + (unless 'include_extras=True'). + + The argument may be a module, class, method, or function. The annotations + are returned as a dictionary. For classes, annotations include also + inherited members. + + TypeError is raised if the argument is not of a type that can contain + annotations, and an empty dictionary is returned if no annotations are + present. + + BEWARE -- the behavior of globalns and localns is counterintuitive + (unless you are familiar with how eval() and exec() work). The + search order is locals first, then globals. + + - If no dict arguments are passed, an attempt is made to use the + globals from obj (or the respective module's globals for classes), + and these are also used as the locals. If the object does not appear + to have globals, an empty dictionary is used. + + - If one dict argument is passed, it is used for both globals and + locals. + + - If two dict arguments are passed, they specify globals and + locals, respectively. + """ + hint = typing.get_type_hints( + obj, globalns=globalns, localns=localns, include_extras=True + ) + if sys.version_info < (3, 11): + _clean_optional(obj, hint, globalns, localns) + if include_extras: + return hint + return {k: _strip_extras(t) for k, t in hint.items()} + + _NoneType = type(None) + + def _could_be_inserted_optional(t): + """detects Union[..., None] pattern""" + if not isinstance(t, typing._UnionGenericAlias): + return False + # Assume if last argument is not None they are user defined + if t.__args__[-1] is not _NoneType: + return False + return True + + # < 3.11 + def _clean_optional(obj, hints, globalns=None, localns=None): + # reverts injected Union[..., None] cases from typing.get_type_hints + # when a None default value is used. + # see https://github.com/python/typing_extensions/issues/310 + if not hints or isinstance(obj, type): + return + defaults = typing._get_defaults(obj) # avoid accessing __annotations___ + if not defaults: + return + original_hints = obj.__annotations__ + for name, value in hints.items(): + # Not a Union[..., None] or replacement conditions not fullfilled + if (not _could_be_inserted_optional(value) + or name not in defaults + or defaults[name] is not None + ): + continue + original_value = original_hints[name] + # value=NoneType should have caused a skip above but check for safety + if original_value is None: + original_value = _NoneType + # Forward reference + if isinstance(original_value, str): + if globalns is None: + if isinstance(obj, _types.ModuleType): + globalns = obj.__dict__ + else: + nsobj = obj + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, '__globals__', {}) + if localns is None: + localns = globalns + elif localns is None: + localns = globalns + + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) + original_evaluated = typing._eval_type(original_value, globalns, localns) + # Compare if values differ. Note that even if equal + # value might be cached by typing._tp_cache contrary to original_evaluated + if original_evaluated != value or ( + # 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias + hasattr(_types, "UnionType") + and isinstance(original_evaluated, _types.UnionType) + and not isinstance(value, _types.UnionType) + ): + hints[name] = original_evaluated + +# Python 3.9 has get_origin() and get_args() but those implementations don't support +# ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do. +if sys.version_info[:2] >= (3, 10): + get_origin = typing.get_origin + get_args = typing.get_args +# 3.9 +else: + def get_origin(tp): + """Get the unsubscripted version of a type. + + This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar + and Annotated. Return None for unsupported types. Examples:: + + get_origin(Literal[42]) is Literal + get_origin(int) is None + get_origin(ClassVar[int]) is ClassVar + get_origin(Generic) is Generic + get_origin(Generic[T]) is Generic + get_origin(Union[T, int]) is Union + get_origin(List[Tuple[T, T]][int]) == list + get_origin(P.args) is P + """ + if isinstance(tp, typing._AnnotatedAlias): + return Annotated + if isinstance(tp, (typing._BaseGenericAlias, _types.GenericAlias, + ParamSpecArgs, ParamSpecKwargs)): + return tp.__origin__ + if tp is typing.Generic: + return typing.Generic + return None + + def get_args(tp): + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if isinstance(tp, typing._AnnotatedAlias): + return (tp.__origin__, *tp.__metadata__) + if isinstance(tp, (typing._GenericAlias, _types.GenericAlias)): + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return () + + +# 3.10+ +if hasattr(typing, 'TypeAlias'): + TypeAlias = typing.TypeAlias +# 3.9 +else: + @_ExtensionsSpecialForm + def TypeAlias(self, parameters): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + raise TypeError(f"{self} is not subscriptable") + + +def _set_default(type_param, default): + type_param.has_default = lambda: default is not NoDefault + type_param.__default__ = default + + +def _set_module(typevarlike): + # for pickling: + def_mod = _caller(depth=2) + if def_mod != 'typing_extensions': + typevarlike.__module__ = def_mod + + +class _DefaultMixin: + """Mixin for TypeVarLike defaults.""" + + __slots__ = () + __init__ = _set_default + + +# Classes using this metaclass must provide a _backported_typevarlike ClassVar +class _TypeVarLikeMeta(type): + def __instancecheck__(cls, __instance: Any) -> bool: + return isinstance(__instance, cls._backported_typevarlike) + + +if _PEP_696_IMPLEMENTED: + from typing import TypeVar +else: + # Add default and infer_variance parameters from PEP 696 and 695 + class TypeVar(metaclass=_TypeVarLikeMeta): + """Type variable.""" + + _backported_typevarlike = typing.TypeVar + + def __new__(cls, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=NoDefault, infer_variance=False): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant, + infer_variance=infer_variance) + else: + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant) + if infer_variance and (covariant or contravariant): + raise ValueError("Variance cannot be specified with infer_variance.") + typevar.__infer_variance__ = infer_variance + + _set_default(typevar, default) + _set_module(typevar) + + def _tvar_prepare_subst(alias, args): + if ( + typevar.has_default() + and alias.__parameters__.index(typevar) == len(args) + ): + args += (typevar.__default__,) + return args + + typevar.__typing_prepare_subst__ = _tvar_prepare_subst + return typevar + + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") + + +# Python 3.10+ has PEP 612 +if hasattr(typing, 'ParamSpecArgs'): + ParamSpecArgs = typing.ParamSpecArgs + ParamSpecKwargs = typing.ParamSpecKwargs +# 3.9 +else: + class _Immutable: + """Mixin to indicate that object should not be copied.""" + __slots__ = () + + def __copy__(self): + return self + + def __deepcopy__(self, memo): + return self + + class ParamSpecArgs(_Immutable): + """The args for a ParamSpec object. + + Given a ParamSpec object P, P.args is an instance of ParamSpecArgs. + + ParamSpecArgs objects have a reference back to their ParamSpec: + + P.args.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.args" + + def __eq__(self, other): + if not isinstance(other, ParamSpecArgs): + return NotImplemented + return self.__origin__ == other.__origin__ + + class ParamSpecKwargs(_Immutable): + """The kwargs for a ParamSpec object. + + Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs. + + ParamSpecKwargs objects have a reference back to their ParamSpec: + + P.kwargs.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.kwargs" + + def __eq__(self, other): + if not isinstance(other, ParamSpecKwargs): + return NotImplemented + return self.__origin__ == other.__origin__ + + +if _PEP_696_IMPLEMENTED: + from typing import ParamSpec + +# 3.10+ +elif hasattr(typing, 'ParamSpec'): + + # Add default parameter - PEP 696 + class ParamSpec(metaclass=_TypeVarLikeMeta): + """Parameter specification.""" + + _backported_typevarlike = typing.ParamSpec + + def __new__(cls, name, *, bound=None, + covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented, can pass infer_variance to typing.TypeVar + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant, + infer_variance=infer_variance) + else: + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant) + paramspec.__infer_variance__ = infer_variance + + _set_default(paramspec, default) + _set_module(paramspec) + + def _paramspec_prepare_subst(alias, args): + params = alias.__parameters__ + i = params.index(paramspec) + if i == len(args) and paramspec.has_default(): + args = [*args, paramspec.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {alias}") + # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. + if len(params) == 1 and not typing._is_param_expr(args[0]): + assert i == 0 + args = (args,) + # Convert lists to tuples to help other libraries cache the results. + elif isinstance(args[i], list): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + return args + + paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst + return paramspec + + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") + +# 3.9 +else: + + # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + class ParamSpec(list, _DefaultMixin): + """Parameter specification variable. + + Usage:: + + P = ParamSpec('P') + + Parameter specification variables exist primarily for the benefit of static + type checkers. They are used to forward the parameter types of one + callable to another callable, a pattern commonly found in higher order + functions and decorators. They are only valid when used in ``Concatenate``, + or s the first argument to ``Callable``. In Python 3.10 and higher, + they are also supported in user-defined Generics at runtime. + See class Generic for more information on generic types. An + example for annotating a decorator:: + + T = TypeVar('T') + P = ParamSpec('P') + + def add_logging(f: Callable[P, T]) -> Callable[P, T]: + '''A type-safe decorator to add logging to a function.''' + def inner(*args: P.args, **kwargs: P.kwargs) -> T: + logging.info(f'{f.__name__} was called') + return f(*args, **kwargs) + return inner + + @add_logging + def add_two(x: float, y: float) -> float: + '''Add two numbers together.''' + return x + y + + Parameter specification variables defined with covariant=True or + contravariant=True can be used to declare covariant or contravariant + generic types. These keyword arguments are valid, but their actual semantics + are yet to be decided. See PEP 612 for details. + + Parameter specification variables can be introspected. e.g.: + + P.__name__ == 'T' + P.__bound__ == None + P.__covariant__ == False + P.__contravariant__ == False + + Note that only parameter specification variables defined in global scope can + be pickled. + """ + + # Trick Generic __parameters__. + __class__ = typing.TypeVar + + @property + def args(self): + return ParamSpecArgs(self) + + @property + def kwargs(self): + return ParamSpecKwargs(self) + + def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + infer_variance=False, default=NoDefault): + list.__init__(self, [self]) + self.__name__ = name + self.__covariant__ = bool(covariant) + self.__contravariant__ = bool(contravariant) + self.__infer_variance__ = bool(infer_variance) + if bound: + self.__bound__ = typing._type_check(bound, 'Bound must be a type.') + else: + self.__bound__ = None + _DefaultMixin.__init__(self, default) + + # for pickling: + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __repr__(self): + if self.__infer_variance__: + prefix = '' + elif self.__covariant__: + prefix = '+' + elif self.__contravariant__: + prefix = '-' + else: + prefix = '~' + return prefix + self.__name__ + + def __hash__(self): + return object.__hash__(self) + + def __eq__(self, other): + return self is other + + def __reduce__(self): + return self.__name__ + + # Hack to get typing._type_check to pass. + def __call__(self, *args, **kwargs): + pass + + +# 3.9 +if not hasattr(typing, 'Concatenate'): + # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + + # 3.9.0-1 + if not hasattr(typing, '_type_convert'): + def _type_convert(arg, module=None, *, allow_special_forms=False): + """For converting None to type(None), and strings to ForwardRef.""" + if arg is None: + return type(None) + if isinstance(arg, str): + if sys.version_info <= (3, 9, 6): + return ForwardRef(arg) + if sys.version_info <= (3, 9, 7): + return ForwardRef(arg, module=module) + return ForwardRef(arg, module=module, is_class=allow_special_forms) + return arg + else: + _type_convert = typing._type_convert + + class _ConcatenateGenericAlias(list): + + # Trick Generic into looking into this for __parameters__. + __class__ = typing._GenericAlias + + def __init__(self, origin, args): + super().__init__(args) + self.__origin__ = origin + self.__args__ = args + + def __repr__(self): + _type_repr = typing._type_repr + return (f'{_type_repr(self.__origin__)}' + f'[{", ".join(_type_repr(arg) for arg in self.__args__)}]') + + def __hash__(self): + return hash((self.__origin__, self.__args__)) + + # Hack to get typing._type_check to pass in Generic. + def __call__(self, *args, **kwargs): + pass + + @property + def __parameters__(self): + return tuple( + tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) + ) + + # 3.9 used by __getitem__ below + def copy_with(self, params): + if isinstance(params[-1], _ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + elif (not (params[-1] is ... or isinstance(params[-1], ParamSpec))): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return self.__class__(self.__origin__, params) + + # 3.9; accessed during GenericAlias.__getitem__ when substituting + def __getitem__(self, args): + if self.__origin__ in (Generic, Protocol): + # Can't subscript Generic[...] or Protocol[...]. + raise TypeError(f"Cannot subscript already-subscripted {self}") + if not self.__parameters__: + raise TypeError(f"{self} is not a generic class") + + if not isinstance(args, tuple): + args = (args,) + args = _unpack_args(*(_type_convert(p) for p in args)) + params = self.__parameters__ + for param in params: + prepare = getattr(param, "__typing_prepare_subst__", None) + if prepare is not None: + args = prepare(self, args) + # 3.9 & typing.ParamSpec + elif isinstance(param, ParamSpec): + i = params.index(param) + if ( + i == len(args) + and getattr(param, '__default__', NoDefault) is not NoDefault + ): + args = [*args, param.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {self}") + # Special case for Z[[int, str, bool]] == Z[int, str, bool] + if len(params) == 1 and not _is_param_expr(args[0]): + assert i == 0 + args = (args,) + elif ( + isinstance(args[i], list) + # 3.9 + # This class inherits from list do not convert + and not isinstance(args[i], _ConcatenateGenericAlias) + ): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + + alen = len(args) + plen = len(params) + if alen != plen: + raise TypeError( + f"Too {'many' if alen > plen else 'few'} arguments for {self};" + f" actual {alen}, expected {plen}" + ) + + subst = dict(zip(self.__parameters__, args)) + # determine new args + new_args = [] + for arg in self.__args__: + if isinstance(arg, type): + new_args.append(arg) + continue + if isinstance(arg, TypeVar): + arg = subst[arg] + if ( + (isinstance(arg, typing._GenericAlias) and _is_unpack(arg)) + or ( + hasattr(_types, "GenericAlias") + and isinstance(arg, _types.GenericAlias) + and getattr(arg, "__unpacked__", False) + ) + ): + raise TypeError(f"{arg} is not valid as type argument") + + elif isinstance(arg, + typing._GenericAlias + if not hasattr(_types, "GenericAlias") else + (typing._GenericAlias, _types.GenericAlias) + ): + subparams = arg.__parameters__ + if subparams: + subargs = tuple(subst[x] for x in subparams) + arg = arg[subargs] + new_args.append(arg) + return self.copy_with(tuple(new_args)) + +# 3.10+ +else: + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias + + # 3.10 + if sys.version_info < (3, 11): + + class _ConcatenateGenericAlias(typing._ConcatenateGenericAlias, _root=True): + # needed for checks in collections.abc.Callable to accept this class + __module__ = "typing" + + def copy_with(self, params): + if isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + if isinstance(params[-1], typing._ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return super(typing._ConcatenateGenericAlias, self).copy_with(params) + + def __getitem__(self, args): + value = super().__getitem__(args) + if isinstance(value, tuple) and any(_is_unpack(t) for t in value): + return tuple(_unpack_args(*(n for n in value))) + return value + + +# 3.9.2 +class _EllipsisDummy: ... + + +# <=3.10 +def _create_concatenate_alias(origin, parameters): + if parameters[-1] is ... and sys.version_info < (3, 9, 2): + # Hack: Arguments must be types, replace it with one. + parameters = (*parameters[:-1], _EllipsisDummy) + if sys.version_info >= (3, 10, 3): + concatenate = _ConcatenateGenericAlias(origin, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) + else: + concatenate = _ConcatenateGenericAlias(origin, parameters) + if parameters[-1] is not _EllipsisDummy: + return concatenate + # Remove dummy again + concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ... + for p in concatenate.__args__) + if sys.version_info < (3, 10): + # backport needs __args__ adjustment only + return concatenate + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _EllipsisDummy) + return concatenate + + +# <=3.10 +@typing._tp_cache +def _concatenate_getitem(self, parameters): + if parameters == (): + raise TypeError("Cannot take a Concatenate of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + return _create_concatenate_alias(self, parameters) + + +# 3.11+; Concatenate does not accept ellipsis in 3.10 +if sys.version_info >= (3, 11): + Concatenate = typing.Concatenate +# <=3.10 +else: + @_ExtensionsSpecialForm + def Concatenate(self, parameters): + """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a + higher order function which adds, removes or transforms parameters of a + callable. + + For example:: + + Callable[Concatenate[int, P], int] + + See PEP 612 for detailed information. + """ + return _concatenate_getitem(self, parameters) + + +# 3.10+ +if hasattr(typing, 'TypeGuard'): + TypeGuard = typing.TypeGuard +# 3.9 +else: + @_ExtensionsSpecialForm + def TypeGuard(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeGuard[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeGuard`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. + + For example:: + + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... + else: + # Else, type of ``val`` is narrowed to ``float``. + ... + + Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower + form of ``TypeA`` (it can even be a wider form) and this may lead to + type-unsafe results. The main reason is to allow for things like + narrowing ``List[object]`` to ``List[str]`` even though the latter is not + a subtype of the former, since ``List`` is invariant. The responsibility of + writing type-safe type guards is left to the user. + + ``TypeGuard`` also works with type variables. For more information, see + PEP 647 (User-Defined Type Guards). + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +# 3.13+ +if hasattr(typing, 'TypeIs'): + TypeIs = typing.TypeIs +# <=3.12 +else: + @_ExtensionsSpecialForm + def TypeIs(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type narrower function. ``TypeIs`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeIs[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeIs`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the intersection of the type inside ``TypeIs`` and the argument's + previously known type. + + For example:: + + def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: + return hasattr(val, '__await__') + + def f(val: Union[int, Awaitable[int]]) -> int: + if is_awaitable(val): + assert_type(val, Awaitable[int]) + else: + assert_type(val, int) + + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeIs). + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +# 3.14+? +if hasattr(typing, 'TypeForm'): + TypeForm = typing.TypeForm +# <=3.13 +else: + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. + def __call__(self, obj, /): + return obj + + @_TypeFormForm + def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. + + Usage: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + + + +if hasattr(typing, "LiteralString"): # 3.11+ + LiteralString = typing.LiteralString +else: + @_SpecialForm + def LiteralString(self, params): + """Represents an arbitrary literal string. + + Example:: + + from typing_extensions import LiteralString + + def query(sql: LiteralString) -> ...: + ... + + query("SELECT * FROM table") # ok + query(f"SELECT * FROM {input()}") # not ok + + See PEP 675 for details. + + """ + raise TypeError(f"{self} is not subscriptable") + + +if hasattr(typing, "Self"): # 3.11+ + Self = typing.Self +else: + @_SpecialForm + def Self(self, params): + """Used to spell the type of "self" in classes. + + Example:: + + from typing import Self + + class ReturnsSelf: + def parse(self, data: bytes) -> Self: + ... + return self + + """ + + raise TypeError(f"{self} is not subscriptable") + + +if hasattr(typing, "Never"): # 3.11+ + Never = typing.Never +else: + @_SpecialForm + def Never(self, params): + """The bottom type, a type that has no members. + + This can be used to define a function that should never be + called, or a function that never returns:: + + from typing_extensions import Never + + def never_call_me(arg: Never) -> None: + pass + + def int_or_str(arg: int | str) -> None: + never_call_me(arg) # type checker error + match arg: + case int(): + print("It's an int") + case str(): + print("It's a str") + case _: + never_call_me(arg) # ok, arg is of type Never + + """ + + raise TypeError(f"{self} is not subscriptable") + + +if hasattr(typing, 'Required'): # 3.11+ + Required = typing.Required + NotRequired = typing.NotRequired +else: # <=3.10 + @_ExtensionsSpecialForm + def Required(self, parameters): + """A special typing construct to mark a key of a total=False TypedDict + as required. For example: + + class Movie(TypedDict, total=False): + title: Required[str] + year: int + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + + There is no runtime checking that a required key is actually provided + when instantiating a related TypedDict. + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + @_ExtensionsSpecialForm + def NotRequired(self, parameters): + """A special typing construct to mark a key of a TypedDict as + potentially missing. For example: + + class Movie(TypedDict): + title: str + year: NotRequired[int] + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +if hasattr(typing, 'ReadOnly'): + ReadOnly = typing.ReadOnly +else: # <=3.12 + @_ExtensionsSpecialForm + def ReadOnly(self, parameters): + """A special typing construct to mark an item of a TypedDict as read-only. + + For example: + + class Movie(TypedDict): + title: ReadOnly[str] + year: int + + def mutate_movie(m: Movie) -> None: + m["year"] = 1992 # allowed + m["title"] = "The Matrix" # typechecker error + + There is no runtime checking for this property. + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + +_UNPACK_DOC = """\ +Type unpack operator. + +The type unpack operator takes the child types from some container type, +such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For +example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + +From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + +The operator can also be used along with a `TypedDict` to annotate +`**kwargs` in a function signature. For instance: + + class Movie(TypedDict): + name: str + year: int + + # This function expects two keyword arguments - *name* of type `str` and + # *year* of type `int`. + def foo(**kwargs: Unpack[Movie]): ... + +Note that there is only some runtime checking of this operator. Not +everything the runtime allows may be accepted by static type checkers. + +For more information, see PEP 646 and PEP 692. +""" + + +if sys.version_info >= (3, 12): # PEP 692 changed the repr of Unpack[] + Unpack = typing.Unpack + + def _is_unpack(obj): + return get_origin(obj) is Unpack + +else: # <=3.11 + class _UnpackSpecialForm(_ExtensionsSpecialForm, _root=True): + def __init__(self, getitem): + super().__init__(getitem) + self.__doc__ = _UNPACK_DOC + + class _UnpackAlias(typing._GenericAlias, _root=True): + if sys.version_info < (3, 11): + # needed for compatibility with Generic[Unpack[Ts]] + __class__ = typing.TypeVar + + @property + def __typing_unpacked_tuple_args__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + arg, = self.__args__ + if isinstance(arg, (typing._GenericAlias, _types.GenericAlias)): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + return arg.__args__ + return None + + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + return super().__getitem__(args) + + @_UnpackSpecialForm + def Unpack(self, parameters): + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return _UnpackAlias(self, (item,)) + + def _is_unpack(obj): + return isinstance(obj, _UnpackAlias) + + +def _unpack_args(*args): + newargs = [] + for arg in args: + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs is not None and (not (subargs and subargs[-1] is ...)): + newargs.extend(subargs) + else: + newargs.append(arg) + return newargs + + +if _PEP_696_IMPLEMENTED: + from typing import TypeVarTuple + +elif hasattr(typing, "TypeVarTuple"): # 3.11+ + + # Add default parameter - PEP 696 + class TypeVarTuple(metaclass=_TypeVarLikeMeta): + """Type variable tuple.""" + + _backported_typevarlike = typing.TypeVarTuple + + def __new__(cls, name, *, default=NoDefault): + tvt = typing.TypeVarTuple(name) + _set_default(tvt, default) + _set_module(tvt) + + def _typevartuple_prepare_subst(alias, args): + params = alias.__parameters__ + typevartuple_index = params.index(tvt) + for param in params[typevartuple_index + 1:]: + if isinstance(param, TypeVarTuple): + raise TypeError( + f"More than one TypeVarTuple parameter in {alias}" + ) + + alen = len(args) + plen = len(params) + left = typevartuple_index + right = plen - typevartuple_index - 1 + var_tuple_index = None + fillarg = None + for k, arg in enumerate(args): + if not isinstance(arg, type): + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs and len(subargs) == 2 and subargs[-1] is ...: + if var_tuple_index is not None: + raise TypeError( + "More than one unpacked " + "arbitrary-length tuple argument" + ) + var_tuple_index = k + fillarg = subargs[0] + if var_tuple_index is not None: + left = min(left, var_tuple_index) + right = min(right, alen - var_tuple_index - 1) + elif left + right > alen: + raise TypeError(f"Too few arguments for {alias};" + f" actual {alen}, expected at least {plen - 1}") + if left == alen - right and tvt.has_default(): + replacement = _unpack_args(tvt.__default__) + else: + replacement = args[left: alen - right] + + return ( + *args[:left], + *([fillarg] * (typevartuple_index - left)), + replacement, + *([fillarg] * (plen - right - left - typevartuple_index - 1)), + *args[alen - right:], + ) + + tvt.__typing_prepare_subst__ = _typevartuple_prepare_subst + return tvt + + def __init_subclass__(self, *args, **kwds): + raise TypeError("Cannot subclass special typing classes") + +else: # <=3.10 + class TypeVarTuple(_DefaultMixin): + """Type variable tuple. + + Usage:: + + Ts = TypeVarTuple('Ts') + + In the same way that a normal type variable is a stand-in for a single + type such as ``int``, a type variable *tuple* is a stand-in for a *tuple* + type such as ``Tuple[int, str]``. + + Type variable tuples can be used in ``Generic`` declarations. + Consider the following example:: + + class Array(Generic[*Ts]): ... + + The ``Ts`` type variable tuple here behaves like ``tuple[T1, T2]``, + where ``T1`` and ``T2`` are type variables. To use these type variables + as type parameters of ``Array``, we must *unpack* the type variable tuple using + the star operator: ``*Ts``. The signature of ``Array`` then behaves + as if we had simply written ``class Array(Generic[T1, T2]): ...``. + In contrast to ``Generic[T1, T2]``, however, ``Generic[*Shape]`` allows + us to parameterise the class with an *arbitrary* number of type parameters. + + Type variable tuples can be used anywhere a normal ``TypeVar`` can. + This includes class definitions, as shown above, as well as function + signatures and variable annotations:: + + class Array(Generic[*Ts]): + + def __init__(self, shape: Tuple[*Ts]): + self._shape: Tuple[*Ts] = shape + + def get_shape(self) -> Tuple[*Ts]: + return self._shape + + shape = (Height(480), Width(640)) + x: Array[Height, Width] = Array(shape) + y = abs(x) # Inferred type is Array[Height, Width] + z = x + x # ... is Array[Height, Width] + x.get_shape() # ... is tuple[Height, Width] + + """ + + # Trick Generic __parameters__. + __class__ = typing.TypeVar + + def __iter__(self): + yield self.__unpacked__ + + def __init__(self, name, *, default=NoDefault): + self.__name__ = name + _DefaultMixin.__init__(self, default) + + # for pickling: + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + self.__unpacked__ = Unpack[self] + + def __repr__(self): + return self.__name__ + + def __hash__(self): + return object.__hash__(self) + + def __eq__(self, other): + return self is other + + def __reduce__(self): + return self.__name__ + + def __init_subclass__(self, *args, **kwds): + if '_root' not in kwds: + raise TypeError("Cannot subclass special typing classes") + + +if hasattr(typing, "reveal_type"): # 3.11+ + reveal_type = typing.reveal_type +else: # <=3.10 + def reveal_type(obj: T, /) -> T: + """Reveal the inferred type of a variable. + + When a static type checker encounters a call to ``reveal_type()``, + it will emit the inferred type of the argument:: + + x: int = 1 + reveal_type(x) + + Running a static type checker (e.g., ``mypy``) on this example + will produce output similar to 'Revealed type is "builtins.int"'. + + At runtime, the function prints the runtime type of the + argument and returns it unchanged. + + """ + print(f"Runtime type is {type(obj).__name__!r}", file=sys.stderr) + return obj + + +if hasattr(typing, "_ASSERT_NEVER_REPR_MAX_LENGTH"): # 3.11+ + _ASSERT_NEVER_REPR_MAX_LENGTH = typing._ASSERT_NEVER_REPR_MAX_LENGTH +else: # <=3.10 + _ASSERT_NEVER_REPR_MAX_LENGTH = 100 + + +if hasattr(typing, "assert_never"): # 3.11+ + assert_never = typing.assert_never +else: # <=3.10 + def assert_never(arg: Never, /) -> Never: + """Assert to the type checker that a line of code is unreachable. + + Example:: + + def int_or_str(arg: int | str) -> None: + match arg: + case int(): + print("It's an int") + case str(): + print("It's a str") + case _: + assert_never(arg) + + If a type checker finds that a call to assert_never() is + reachable, it will emit an error. + + At runtime, this throws an exception when called. + + """ + value = repr(arg) + if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH: + value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...' + raise AssertionError(f"Expected code to be unreachable, but got: {value}") + + +if sys.version_info >= (3, 12): # 3.12+ + # dataclass_transform exists in 3.11 but lacks the frozen_default parameter + dataclass_transform = typing.dataclass_transform +else: # <=3.11 + def dataclass_transform( + *, + eq_default: bool = True, + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, + field_specifiers: typing.Tuple[ + typing.Union[typing.Type[typing.Any], typing.Callable[..., typing.Any]], + ... + ] = (), + **kwargs: typing.Any, + ) -> typing.Callable[[T], T]: + """Decorator that marks a function, class, or metaclass as providing + dataclass-like behavior. + + Example: + + from typing_extensions import dataclass_transform + + _T = TypeVar("_T") + + # Used on a decorator function + @dataclass_transform() + def create_model(cls: type[_T]) -> type[_T]: + ... + return cls + + @create_model + class CustomerModel: + id: int + name: str + + # Used on a base class + @dataclass_transform() + class ModelBase: ... + + class CustomerModel(ModelBase): + id: int + name: str + + # Used on a metaclass + @dataclass_transform() + class ModelMeta(type): ... + + class ModelBase(metaclass=ModelMeta): ... + + class CustomerModel(ModelBase): + id: int + name: str + + Each of the ``CustomerModel`` classes defined in this example will now + behave similarly to a dataclass created with the ``@dataclasses.dataclass`` + decorator. For example, the type checker will synthesize an ``__init__`` + method. + + The arguments to this decorator can be used to customize this behavior: + - ``eq_default`` indicates whether the ``eq`` parameter is assumed to be + True or False if it is omitted by the caller. + - ``order_default`` indicates whether the ``order`` parameter is + assumed to be True or False if it is omitted by the caller. + - ``kw_only_default`` indicates whether the ``kw_only`` parameter is + assumed to be True or False if it is omitted by the caller. + - ``frozen_default`` indicates whether the ``frozen`` parameter is + assumed to be True or False if it is omitted by the caller. + - ``field_specifiers`` specifies a static list of supported classes + or functions that describe fields, similar to ``dataclasses.field()``. + + At runtime, this decorator records its arguments in the + ``__dataclass_transform__`` attribute on the decorated object. + + See PEP 681 for details. + + """ + def decorator(cls_or_fn): + cls_or_fn.__dataclass_transform__ = { + "eq_default": eq_default, + "order_default": order_default, + "kw_only_default": kw_only_default, + "frozen_default": frozen_default, + "field_specifiers": field_specifiers, + "kwargs": kwargs, + } + return cls_or_fn + return decorator + + +if hasattr(typing, "override"): # 3.12+ + override = typing.override +else: # <=3.11 + _F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) + + def override(arg: _F, /) -> _F: + """Indicate that a method is intended to override a method in a base class. + + Usage: + + class Base: + def method(self) -> None: + pass + + class Child(Base): + @override + def method(self) -> None: + super().method() + + When this decorator is applied to a method, the type checker will + validate that it overrides a method with the same name on a base class. + This helps prevent bugs that may occur when a base class is changed + without an equivalent change to a child class. + + There is no runtime checking of these properties. The decorator + sets the ``__override__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + + See PEP 698 for details. + + """ + try: + arg.__override__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return arg + + +# Python 3.13.3+ contains a fix for the wrapped __new__ +if sys.version_info >= (3, 13, 3): + deprecated = warnings.deprecated +else: + _T = typing.TypeVar("_T") + + class deprecated: + """Indicate that a class, function or overload is deprecated. + + When this decorator is applied to an object, the type checker + will generate a diagnostic on usage of the deprecated object. + + Usage: + + @deprecated("Use B instead") + class A: + pass + + @deprecated("Use g instead") + def f(): + pass + + @overload + @deprecated("int support is deprecated") + def g(x: int) -> int: ... + @overload + def g(x: str) -> int: ... + + The warning specified by *category* will be emitted at runtime + on use of deprecated objects. For functions, that happens on calls; + for classes, on instantiation and on creation of subclasses. + If the *category* is ``None``, no warning is emitted at runtime. + The *stacklevel* determines where the + warning is emitted. If it is ``1`` (the default), the warning + is emitted at the direct caller of the deprecated object; if it + is higher, it is emitted further up the stack. + Static type checker behavior is not affected by the *category* + and *stacklevel* arguments. + + The deprecation message passed to the decorator is saved in the + ``__deprecated__`` attribute on the decorated object. + If applied to an overload, the decorator + must be after the ``@overload`` decorator for the attribute to + exist on the overload as returned by ``get_overloads()``. + + See PEP 702 for details. + + """ + def __init__( + self, + message: str, + /, + *, + category: typing.Optional[typing.Type[Warning]] = DeprecationWarning, + stacklevel: int = 1, + ) -> None: + if not isinstance(message, str): + raise TypeError( + "Expected an object of type str for 'message', not " + f"{type(message).__name__!r}" + ) + self.message = message + self.category = category + self.stacklevel = stacklevel + + def __call__(self, arg: _T, /) -> _T: + # Make sure the inner functions created below don't + # retain a reference to self. + msg = self.message + category = self.category + stacklevel = self.stacklevel + if category is None: + arg.__deprecated__ = msg + return arg + elif isinstance(arg, type): + import functools + from types import MethodType + + original_new = arg.__new__ + + @functools.wraps(original_new) + def __new__(cls, /, *args, **kwargs): + if cls is arg: + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + if original_new is not object.__new__: + return original_new(cls, *args, **kwargs) + # Mirrors a similar check in object.__new__. + elif cls.__init__ is object.__init__ and (args or kwargs): + raise TypeError(f"{cls.__name__}() takes no arguments") + else: + return original_new(cls) + + arg.__new__ = staticmethod(__new__) + + original_init_subclass = arg.__init_subclass__ + # We need slightly different behavior if __init_subclass__ + # is a bound method (likely if it was implemented in Python) + if isinstance(original_init_subclass, MethodType): + original_init_subclass = original_init_subclass.__func__ + + @functools.wraps(original_init_subclass) + def __init_subclass__(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return original_init_subclass(*args, **kwargs) + + arg.__init_subclass__ = classmethod(__init_subclass__) + # Or otherwise, which likely means it's a builtin such as + # object's implementation of __init_subclass__. + else: + @functools.wraps(original_init_subclass) + def __init_subclass__(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return original_init_subclass(*args, **kwargs) + + arg.__init_subclass__ = __init_subclass__ + + arg.__deprecated__ = __new__.__deprecated__ = msg + __init_subclass__.__deprecated__ = msg + return arg + elif callable(arg): + import asyncio.coroutines + import functools + import inspect + + @functools.wraps(arg) + def wrapper(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return arg(*args, **kwargs) + + if asyncio.coroutines.iscoroutinefunction(arg): + if sys.version_info >= (3, 12): + wrapper = inspect.markcoroutinefunction(wrapper) + else: + wrapper._is_coroutine = asyncio.coroutines._is_coroutine + + arg.__deprecated__ = wrapper.__deprecated__ = msg + return wrapper + else: + raise TypeError( + "@deprecated decorator with non-None category must be applied to " + f"a class or callable, not {arg!r}" + ) + +if sys.version_info < (3, 10): + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, (tuple, list, ParamSpec, _ConcatenateGenericAlias) + ) +else: + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, + ( + tuple, + list, + ParamSpec, + _ConcatenateGenericAlias, + typing._ConcatenateGenericAlias, + ), + ) + + +# We have to do some monkey patching to deal with the dual nature of +# Unpack/TypeVarTuple: +# - We want Unpack to be a kind of TypeVar so it gets accepted in +# Generic[Unpack[Ts]] +# - We want it to *not* be treated as a TypeVar for the purposes of +# counting generic parameters, so that when we subscript a generic, +# the runtime doesn't try to substitute the Unpack with the subscripted type. +if not hasattr(typing, "TypeVarTuple"): + def _check_generic(cls, parameters, elen=_marker): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + # If substituting a single ParamSpec with multiple arguments + # we do not check the count + if (inspect.isclass(cls) and issubclass(cls, typing.Generic) + and len(cls.__parameters__) == 1 + and isinstance(cls.__parameters__[0], ParamSpec) + and parameters + and not _is_param_expr(parameters[0]) + ): + # Generic modifies parameters variable, but here we cannot do this + return + + if not elen: + raise TypeError(f"{cls} is not a generic class") + if elen is _marker: + if not hasattr(cls, "__parameters__") or not cls.__parameters__: + raise TypeError(f"{cls} is not a generic class") + elen = len(cls.__parameters__) + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) + if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): + return + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): + return + + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}" + f" for {cls}; actual {alen}, expected {expect_val}") +else: + # Python 3.11+ + + def _check_generic(cls, parameters, elen): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): + return + + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" + f" for {cls}; actual {alen}, expected {expect_val}") + +if not _PEP_696_IMPLEMENTED: + typing._check_generic = _check_generic + + +def _has_generic_or_protocol_as_origin() -> bool: + try: + frame = sys._getframe(2) + # - Catch AttributeError: not all Python implementations have sys._getframe() + # - Catch ValueError: maybe we're called from an unexpected module + # and the call stack isn't deep enough + except (AttributeError, ValueError): + return False # err on the side of leniency + else: + # If we somehow get invoked from outside typing.py, + # also err on the side of leniency + if frame.f_globals.get("__name__") != "typing": + return False + origin = frame.f_locals.get("origin") + # Cannot use "in" because origin may be an object with a buggy __eq__ that + # throws an error. + return origin is typing.Generic or origin is Protocol or origin is typing.Protocol + + +_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} + + +def _is_unpacked_typevartuple(x) -> bool: + if get_origin(x) is not Unpack: + return False + args = get_args(x) + return ( + bool(args) + and len(args) == 1 + and type(args[0]) in _TYPEVARTUPLE_TYPES + ) + + +# Python 3.11+ _collect_type_vars was renamed to _collect_parameters +if hasattr(typing, '_collect_type_vars'): + def _collect_type_vars(types, typevar_types=None): + """Collect all type variable contained in types in order of + first appearance (lexicographic order). For example:: + + _collect_type_vars((T, List[S, T])) == (T, S) + """ + if typevar_types is None: + typevar_types = typing.TypeVar + tvars = [] + + # A required TypeVarLike cannot appear after a TypeVarLike with a default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() + default_encountered = False + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple + type_var_tuple_encountered = False + + for t in types: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True + elif ( + isinstance(t, typevar_types) and not isinstance(t, _UnpackAlias) + and t not in tvars + ): + if enforce_default_ordering: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if has_default: + if type_var_tuple_encountered: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + tvars.append(t) + if _should_collect_from_parameters(t): + tvars.extend([t for t in t.__parameters__ if t not in tvars]) + elif isinstance(t, tuple): + # Collect nested type_vars + # tuple wrapped by _prepare_paramspec_params(cls, params) + for x in t: + for collected in _collect_type_vars([x]): + if collected not in tvars: + tvars.append(collected) + return tuple(tvars) + + typing._collect_type_vars = _collect_type_vars +else: + def _collect_parameters(args): + """Collect all type variables and parameter specifications in args + in order of first appearance (lexicographic order). + + For example:: + + assert _collect_parameters((T, Callable[P, T])) == (T, P) + """ + parameters = [] + + # A required TypeVarLike cannot appear after a TypeVarLike with default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() + default_encountered = False + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple + type_var_tuple_encountered = False + + for t in args: + if isinstance(t, type): + # We don't want __parameters__ descriptor of a bare Python class. + pass + elif isinstance(t, tuple): + # `t` might be a tuple, when `ParamSpec` is substituted with + # `[T, int]`, or `[int, *Ts]`, etc. + for x in t: + for collected in _collect_parameters([x]): + if collected not in parameters: + parameters.append(collected) + elif hasattr(t, '__typing_subst__'): + if t not in parameters: + if enforce_default_ordering: + has_default = ( + getattr(t, '__default__', NoDefault) is not NoDefault + ) + + if type_var_tuple_encountered and has_default: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + + if has_default: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + parameters.append(t) + else: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True + for x in getattr(t, '__parameters__', ()): + if x not in parameters: + parameters.append(x) + + return tuple(parameters) + + if not _PEP_696_IMPLEMENTED: + typing._collect_parameters = _collect_parameters + +# Backport typing.NamedTuple as it exists in Python 3.13. +# In 3.11, the ability to define generic `NamedTuple`s was supported. +# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8. +# On 3.12, we added __orig_bases__ to call-based NamedTuples +# On 3.13, we deprecated kwargs-based NamedTuples +if sys.version_info >= (3, 13): + NamedTuple = typing.NamedTuple +else: + def _make_nmtuple(name, types, module, defaults=()): + fields = [n for n, t in types] + annotations = {n: typing._type_check(t, f"field {n} annotation must be a type") + for n, t in types} + nm_tpl = collections.namedtuple(name, fields, + defaults=defaults, module=module) + nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations + return nm_tpl + + _prohibited_namedtuple_fields = typing._prohibited + _special_namedtuple_fields = frozenset({'__module__', '__name__', '__annotations__'}) + + class _NamedTupleMeta(type): + def __new__(cls, typename, bases, ns): + assert _NamedTuple in bases + for base in bases: + if base is not _NamedTuple and base is not typing.Generic: + raise TypeError( + 'can only inherit from a NamedTuple type and Generic') + bases = tuple(tuple if base is _NamedTuple else base for base in bases) + if "__annotations__" in ns: + types = ns["__annotations__"] + elif "__annotate__" in ns: + # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated + types = ns["__annotate__"](1) + else: + types = {} + default_names = [] + for field_name in types: + if field_name in ns: + default_names.append(field_name) + elif default_names: + raise TypeError(f"Non-default namedtuple field {field_name} " + f"cannot follow default field" + f"{'s' if len(default_names) > 1 else ''} " + f"{', '.join(default_names)}") + nm_tpl = _make_nmtuple( + typename, types.items(), + defaults=[ns[n] for n in default_names], + module=ns['__module__'] + ) + nm_tpl.__bases__ = bases + if typing.Generic in bases: + if hasattr(typing, '_generic_class_getitem'): # 3.12+ + nm_tpl.__class_getitem__ = classmethod(typing._generic_class_getitem) + else: + class_getitem = typing.Generic.__class_getitem__.__func__ + nm_tpl.__class_getitem__ = classmethod(class_getitem) + # update from user namespace without overriding special namedtuple attributes + for key, val in ns.items(): + if key in _prohibited_namedtuple_fields: + raise AttributeError("Cannot overwrite NamedTuple attribute " + key) + elif key not in _special_namedtuple_fields: + if key not in nm_tpl._fields: + setattr(nm_tpl, key, ns[key]) + try: + set_name = type(val).__set_name__ + except AttributeError: + pass + else: + try: + set_name(val, nm_tpl, key) + except BaseException as e: + msg = ( + f"Error calling __set_name__ on {type(val).__name__!r} " + f"instance {key!r} in {typename!r}" + ) + # BaseException.add_note() existed on py311, + # but the __set_name__ machinery didn't start + # using add_note() until py312. + # Making sure exceptions are raised in the same way + # as in "normal" classes seems most important here. + if sys.version_info >= (3, 12): + e.add_note(msg) + raise + else: + raise RuntimeError(msg) from e + + if typing.Generic in bases: + nm_tpl.__init_subclass__() + return nm_tpl + + _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) + + def _namedtuple_mro_entries(bases): + assert NamedTuple in bases + return (_NamedTuple,) + + def NamedTuple(typename, fields=_marker, /, **kwargs): + """Typed version of namedtuple. + + Usage:: + + class Employee(NamedTuple): + name: str + id: int + + This is equivalent to:: + + Employee = collections.namedtuple('Employee', ['name', 'id']) + + The resulting class has an extra __annotations__ attribute, giving a + dict that maps field names to types. (The field names are also in + the _fields attribute, which is part of the namedtuple API.) + An alternative equivalent functional syntax is also accepted:: + + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + """ + if fields is _marker: + if kwargs: + deprecated_thing = "Creating NamedTuple classes using keyword arguments" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "Use the class-based or functional syntax instead." + ) + else: + deprecated_thing = "Failing to pass a value for the 'fields' parameter" + example = f"`{typename} = NamedTuple({typename!r}, [])`" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. " + ) + example + "." + elif fields is None: + if kwargs: + raise TypeError( + "Cannot pass `None` as the 'fields' parameter " + "and also specify fields using keyword arguments" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + example = f"`{typename} = NamedTuple({typename!r}, [])`" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. " + ) + example + "." + elif kwargs: + raise TypeError("Either list of fields or keywords" + " can be provided to NamedTuple, not both") + if fields is _marker or fields is None: + warnings.warn( + deprecation_msg.format(name=deprecated_thing, remove="3.15"), + DeprecationWarning, + stacklevel=2, + ) + fields = kwargs.items() + nt = _make_nmtuple(typename, fields, module=_caller()) + nt.__orig_bases__ = (NamedTuple,) + return nt + + NamedTuple.__mro_entries__ = _namedtuple_mro_entries + + +if hasattr(collections.abc, "Buffer"): + Buffer = collections.abc.Buffer +else: + class Buffer(abc.ABC): # noqa: B024 + """Base class for classes that implement the buffer protocol. + + The buffer protocol allows Python objects to expose a low-level + memory buffer interface. Before Python 3.12, it is not possible + to implement the buffer protocol in pure Python code, or even + to check whether a class implements the buffer protocol. In + Python 3.12 and higher, the ``__buffer__`` method allows access + to the buffer protocol from Python code, and the + ``collections.abc.Buffer`` ABC allows checking whether a class + implements the buffer protocol. + + To indicate support for the buffer protocol in earlier versions, + inherit from this ABC, either in a stub file or at runtime, + or use ABC registration. This ABC provides no methods, because + there is no Python-accessible methods shared by pre-3.12 buffer + classes. It is useful primarily for static checks. + + """ + + # As a courtesy, register the most common stdlib buffer classes. + Buffer.register(memoryview) + Buffer.register(bytearray) + Buffer.register(bytes) + + +# Backport of types.get_original_bases, available on 3.12+ in CPython +if hasattr(_types, "get_original_bases"): + get_original_bases = _types.get_original_bases +else: + def get_original_bases(cls, /): + """Return the class's "original" bases prior to modification by `__mro_entries__`. + + Examples:: + + from typing import TypeVar, Generic + from typing_extensions import NamedTuple, TypedDict + + T = TypeVar("T") + class Foo(Generic[T]): ... + class Bar(Foo[int], float): ... + class Baz(list[str]): ... + Eggs = NamedTuple("Eggs", [("a", int), ("b", str)]) + Spam = TypedDict("Spam", {"a": int, "b": str}) + + assert get_original_bases(Bar) == (Foo[int], float) + assert get_original_bases(Baz) == (list[str],) + assert get_original_bases(Eggs) == (NamedTuple,) + assert get_original_bases(Spam) == (TypedDict,) + assert get_original_bases(int) == (object,) + """ + try: + return cls.__dict__.get("__orig_bases__", cls.__bases__) + except AttributeError: + raise TypeError( + f'Expected an instance of type, not {type(cls).__name__!r}' + ) from None + + +# NewType is a class on Python 3.10+, making it pickleable +# The error message for subclassing instances of NewType was improved on 3.11+ +if sys.version_info >= (3, 11): + NewType = typing.NewType +else: + class NewType: + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy callable that simply returns its argument. Usage:: + UserId = NewType('UserId', int) + def name_by_id(user_id: UserId) -> str: + ... + UserId('user') # Fails type check + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + num = UserId(5) + 1 # type: int + """ + + def __call__(self, obj, /): + return obj + + def __init__(self, name, tp): + self.__qualname__ = name + if '.' in name: + name = name.rpartition('.')[-1] + self.__name__ = name + self.__supertype__ = tp + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __mro_entries__(self, bases): + # We defined __mro_entries__ to get a better error message + # if a user attempts to subclass a NewType instance. bpo-46170 + supercls_name = self.__name__ + + class Dummy: + def __init_subclass__(cls): + subcls_name = cls.__name__ + raise TypeError( + f"Cannot subclass an instance of NewType. " + f"Perhaps you were looking for: " + f"`{subcls_name} = NewType({subcls_name!r}, {supercls_name})`" + ) + + return (Dummy,) + + def __repr__(self): + return f'{self.__module__}.{self.__qualname__}' + + def __reduce__(self): + return self.__qualname__ + + if sys.version_info >= (3, 10): + # PEP 604 methods + # It doesn't make sense to have these methods on Python <3.10 + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + +if sys.version_info >= (3, 14): + TypeAliasType = typing.TypeAliasType +# <=3.13 +else: + if sys.version_info >= (3, 12): + # 3.12-3.13 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + typing.TypeAliasType, + TypeAliasType, + )) + else: + # <=3.11 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) + + if sys.version_info < (3, 10): + # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, + # so that we emulate the behaviour of `types.GenericAlias` + # on the latest versions of CPython + _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ + "__class__", + "__bases__", + "__origin__", + "__args__", + "__unpacked__", + "__parameters__", + "__typing_unpacked_tuple_args__", + "__mro_entries__", + "__reduce_ex__", + "__reduce__", + "__copy__", + "__deepcopy__", + }) + + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): + def __getattr__(self, attr): + if attr in _ATTRIBUTE_DELEGATION_EXCLUSIONS: + return object.__getattr__(self, attr) + return getattr(self.__origin__, attr) + + + class TypeAliasType: + """Create named, parameterized type aliases. + + This provides a backport of the new `type` statement in Python 3.12: + + type ListOrSet[T] = list[T] | set[T] + + is equivalent to: + + T = TypeVar("T") + ListOrSet = TypeAliasType("ListOrSet", list[T] | set[T], type_params=(T,)) + + The name ListOrSet can then be used as an alias for the type it refers to. + + The type_params argument should contain all the type parameters used + in the value of the type alias. If the alias is not generic, this + argument is omitted. + + Static type checkers should only support type aliases declared using + TypeAliasType that follow these rules: + + - The first argument (the name) must be a string literal. + - The TypeAliasType instance must be immediately assigned to a variable + of the same name. (For example, 'X = TypeAliasType("Y", int)' is invalid, + as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). + + """ + + def __init__(self, name: str, value, *, type_params=()): + if not isinstance(name, str): + raise TypeError("TypeAliasType name must be a string") + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") + self.__value__ = value + self.__type_params__ = type_params + + default_value_encountered = False + parameters = [] + for type_param in type_params: + if ( + not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # <=3.11 + # Unpack Backport passes isinstance(type_param, TypeVar) + or _is_unpack(type_param) + ): + raise TypeError(f"Expected a type param, got {type_param!r}") + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) + if default_value_encountered and not has_default: + raise TypeError(f"non-default type parameter '{type_param!r}'" + " follows default type parameter") + if has_default: + default_value_encountered = True + if isinstance(type_param, TypeVarTuple): + parameters.extend(type_param) + else: + parameters.append(type_param) + self.__parameters__ = tuple(parameters) + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + # Setting this attribute closes the TypeAliasType from further modification + self.__name__ = name + + def __setattr__(self, name: str, value: object, /) -> None: + if hasattr(self, "__name__"): + self._raise_attribute_error(name) + super().__setattr__(name, value) + + def __delattr__(self, name: str, /) -> Never: + self._raise_attribute_error(name) + + def _raise_attribute_error(self, name: str) -> Never: + # Match the Python 3.12 error messages exactly + if name == "__name__": + raise AttributeError("readonly attribute") + elif name in {"__value__", "__type_params__", "__parameters__", "__module__"}: + raise AttributeError( + f"attribute '{name}' of 'typing.TypeAliasType' objects " + "is not writable" + ) + else: + raise AttributeError( + f"'typing.TypeAliasType' object has no attribute '{name}'" + ) + + def __repr__(self) -> str: + return self.__name__ + + if sys.version_info < (3, 11): + def _check_single_param(self, param, recursion=0): + # Allow [], [int], [int, str], [int, ...], [int, T] + if param is ...: + return ... + if param is None: + return None + # Note in <= 3.9 _ConcatenateGenericAlias inherits from list + if isinstance(param, list) and recursion == 0: + return [self._check_single_param(arg, recursion+1) + for arg in param] + return typing._type_check( + param, f'Subscripting {self.__name__} requires a type.' + ) + + def _check_parameters(self, parameters): + if sys.version_info < (3, 11): + return tuple( + self._check_single_param(item) + for item in parameters + ) + return tuple(typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ) + + def __getitem__(self, parameters): + if not self.__type_params__: + raise TypeError("Only generic type aliases are subscriptable") + if not isinstance(parameters, tuple): + parameters = (parameters,) + # Using 3.9 here will create problems with Concatenate + if sys.version_info >= (3, 10): + return _types.GenericAlias(self, parameters) + type_vars = _collect_type_vars(parameters) + parameters = self._check_parameters(parameters) + alias = _TypeAliasGenericAlias(self, parameters) + # alias.__parameters__ is not complete if Concatenate is present + # as it is converted to a list from which no parameters are extracted. + if alias.__parameters__ != type_vars: + alias.__parameters__ = type_vars + return alias + + def __reduce__(self): + return self.__name__ + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "type 'typing_extensions.TypeAliasType' is not an acceptable base type" + ) + + # The presence of this method convinces typing._type_check + # that TypeAliasTypes are types. + def __call__(self): + raise TypeError("Type alias is not callable") + + if sys.version_info >= (3, 10): + def __or__(self, right): + # For forward compatibility with 3.12, reject Unions + # that are not accepted by the built-in Union. + if not _is_unionable(right): + return NotImplemented + return typing.Union[self, right] + + def __ror__(self, left): + if not _is_unionable(left): + return NotImplemented + return typing.Union[left, self] + + +if hasattr(typing, "is_protocol"): + is_protocol = typing.is_protocol + get_protocol_members = typing.get_protocol_members +else: + def is_protocol(tp: type, /) -> bool: + """Return True if the given type is a Protocol. + + Example:: + + >>> from typing_extensions import Protocol, is_protocol + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> is_protocol(P) + True + >>> is_protocol(int) + False + """ + return ( + isinstance(tp, type) + and getattr(tp, '_is_protocol', False) + and tp is not Protocol + and tp is not typing.Protocol + ) + + def get_protocol_members(tp: type, /) -> typing.FrozenSet[str]: + """Return the set of members defined in a Protocol. + + Example:: + + >>> from typing_extensions import Protocol, get_protocol_members + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> get_protocol_members(P) + frozenset({'a', 'b'}) + + Raise a TypeError for arguments that are not Protocols. + """ + if not is_protocol(tp): + raise TypeError(f'{tp!r} is not a Protocol') + if hasattr(tp, '__protocol_attrs__'): + return frozenset(tp.__protocol_attrs__) + return frozenset(_get_protocol_attrs(tp)) + + +if hasattr(typing, "Doc"): + Doc = typing.Doc +else: + class Doc: + """Define the documentation of a type annotation using ``Annotated``, to be + used in class attributes, function and method parameters, return values, + and variables. + + The value should be a positional-only string literal to allow static tools + like editors and documentation generators to use it. + + This complements docstrings. + + The string value passed is available in the attribute ``documentation``. + + Example:: + + >>> from typing_extensions import Annotated, Doc + >>> def hi(to: Annotated[str, Doc("Who to say hi to")]) -> None: ... + """ + def __init__(self, documentation: str, /) -> None: + self.documentation = documentation + + def __repr__(self) -> str: + return f"Doc({self.documentation!r})" + + def __hash__(self) -> int: + return hash(self.documentation) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Doc): + return NotImplemented + return self.documentation == other.documentation + + +_CapsuleType = getattr(_types, "CapsuleType", None) + +if _CapsuleType is None: + try: + import _socket + except ImportError: + pass + else: + _CAPI = getattr(_socket, "CAPI", None) + if _CAPI is not None: + _CapsuleType = type(_CAPI) + +if _CapsuleType is not None: + CapsuleType = _CapsuleType + __all__.append("CapsuleType") + + +if sys.version_info >= (3,14): + from annotationlib import Format, get_annotations +else: + class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + + def get_annotations(obj, *, globals=None, locals=None, eval_str=False, + format=Format.VALUE): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This is a backport of `inspect.get_annotations`, which has been + in the standard library since Python 3.10. See the standard library + documentation for more: + + https://docs.python.org/3/library/inspect.html#inspect.get_annotations + + This backport adds the *format* argument introduced by PEP 649. The + three formats supported are: + * VALUE: the annotations are returned as-is. This is the default and + it is compatible with the behavior on previous Python versions. + * FORWARDREF: return annotations as-is if possible, but replace any + undefined names with ForwardRef objects. The implementation proposed by + PEP 649 relies on language changes that cannot be backported; the + typing-extensions implementation simply returns the same result as VALUE. + * STRING: return annotations as strings, in a format close to the original + source. Again, this behavior cannot be replicated directly in a backport. + As an approximation, typing-extensions retrieves the annotations under + VALUE semantics and then stringifies them. + + The purpose of this backport is to allow users who would like to use + FORWARDREF or STRING semantics once PEP 649 is implemented, but who also + want to support earlier Python versions, to simply write: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + """ + format = Format(format) + if format is Format.VALUE_WITH_FAKE_GLOBALS: + raise ValueError( + "The VALUE_WITH_FAKE_GLOBALS format is for internal use only" + ) + + if eval_str and format is not Format.VALUE: + raise ValueError("eval_str=True is only supported with format=Format.VALUE") + + if isinstance(obj, type): + # class + obj_dict = getattr(obj, '__dict__', None) + if obj_dict and hasattr(obj_dict, 'get'): + ann = obj_dict.get('__annotations__', None) + if isinstance(ann, _types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, _types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = obj.__dict__ + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + elif hasattr(obj, '__annotations__'): + ann = obj.__annotations__ + obj_globals = obj_locals = unwrap = None + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + if format is Format.STRING: + return { + key: value if isinstance(value, str) else typing._type_repr(value) + for key, value in ann.items() + } + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals or {} + + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + locals = {param.__name__: param for param in type_params} | locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value + + +if hasattr(typing, "evaluate_forward_ref"): + evaluate_forward_ref = typing.evaluate_forward_ref +else: + # Implements annotationlib.ForwardRef.evaluate + def _eval_with_owner( + forward_ref, *, owner=None, globals=None, locals=None, type_params=None + ): + if forward_ref.__forward_evaluated__: + return forward_ref.__forward_value__ + if getattr(forward_ref, "__cell__", None) is not None: + try: + value = forward_ref.__cell__.cell_contents + except ValueError: + pass + else: + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + if owner is None: + owner = getattr(forward_ref, "__owner__", None) + + if ( + globals is None + and getattr(forward_ref, "__forward_module__", None) is not None + ): + globals = getattr( + sys.modules.get(forward_ref.__forward_module__, None), "__dict__", None + ) + if globals is None: + globals = getattr(forward_ref, "__globals__", None) + if globals is None: + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, _types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + + # If we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + + if type_params is None and owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(owner, "__type_params__", None) + + # type parameters require some special handling, + # as they exist in their own scope + # but `eval()` does not have a dedicated parameter for that scope. + # For classes, names in type parameter scopes should override + # names in the global scope (which here are called `localns`!), + # but should in turn be overridden by names in the class scope + # (which here are called `globalns`!) + if type_params is not None: + globals = dict(globals) + locals = dict(locals) + for param in type_params: + param_name = param.__name__ + if ( + _FORWARD_REF_HAS_CLASS and not forward_ref.__forward_is_class__ + ) or param_name not in globals: + globals[param_name] = param + locals.pop(param_name, None) + + arg = forward_ref.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + value = locals[arg] + elif arg in globals: + value = globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + else: + raise NameError(arg) + else: + code = forward_ref.__forward_code__ + value = eval(code, globals, locals) + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + + def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=None, + _recursive_guard=frozenset(), + ): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *STRING* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annotationlib.Format enum. + + """ + if format == Format.STRING: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: + return forward_ref + + # Evaluate the forward reference + try: + value = _eval_with_owner( + forward_ref, + owner=owner, + globals=globals, + locals=locals, + type_params=type_params, + ) + except NameError: + if format == Format.FORWARDREF: + return forward_ref + else: + raise + + if isinstance(value, str): + value = ForwardRef(value) + + # Recursively evaluate the type + if isinstance(value, ForwardRef): + if getattr(value, "__forward_module__", True) is not None: + globals = None + return evaluate_forward_ref( + value, + globals=globals, + locals=locals, + type_params=type_params, owner=owner, + _recursive_guard=_recursive_guard, format=format + ) + if sys.version_info < (3, 12, 5) and type_params: + # Make use of type_params + locals = dict(locals) if locals else {} + for tvar in type_params: + if tvar.__name__ not in locals: # lets not overwrite something present + locals[tvar.__name__] = tvar + if sys.version_info < (3, 12, 5): + return typing._eval_type( + value, + globals, + locals, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + else: + return typing._eval_type( + value, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + + +class Sentinel: + """Create a unique sentinel object. + + *name* should be the name of the variable to which the return value shall be assigned. + + *repr*, if supplied, will be used for the repr of the sentinel object. + If not provided, "" will be used. + """ + + def __init__( + self, + name: str, + repr: typing.Optional[str] = None, + ): + self._name = name + self._repr = repr if repr is not None else f'<{name}>' + + def __repr__(self): + return self._repr + + if sys.version_info < (3, 11): + # The presence of this method convinces typing._type_check + # that Sentinels are types. + def __call__(self, *args, **kwargs): + raise TypeError(f"{type(self).__name__!r} object is not callable") + + if sys.version_info >= (3, 10): + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __getstate__(self): + raise TypeError(f"Cannot pickle {type(self).__name__!r} object") + + +# Aliases for items that are in typing in all supported versions. +# We use hasattr() checks so this library will continue to import on +# future versions of Python that may remove these names. +_typing_names = [ + "AbstractSet", + "AnyStr", + "BinaryIO", + "Callable", + "Collection", + "Container", + "Dict", + "FrozenSet", + "Hashable", + "IO", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "List", + "Mapping", + "MappingView", + "Match", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Optional", + "Pattern", + "Reversible", + "Sequence", + "Set", + "Sized", + "TextIO", + "Tuple", + "Union", + "ValuesView", + "cast", + "no_type_check", + "no_type_check_decorator", + # This is private, but it was defined by typing_extensions for a long time + # and some users rely on it. + "_AnnotatedAlias", +] +globals().update( + {name: getattr(typing, name) for name in _typing_names if hasattr(typing, name)} +) +# These are defined unconditionally because they are used in +# typing-extensions itself. +Generic = typing.Generic +ForwardRef = typing.ForwardRef +Annotated = typing.Annotated diff --git a/python/src/__init__.py b/python/code/wypp/__init__.py similarity index 82% rename from python/src/__init__.py rename to python/code/wypp/__init__.py index 5117affb..a25b4adc 100644 --- a/python/src/__init__.py +++ b/python/code/wypp/__init__.py @@ -1,4 +1,9 @@ -from . import writeYourProgram as w +try: + from . import writeYourProgram as w +except (ImportError, ModuleNotFoundError): + import writeYourProgram as w + +import typing # Exported names that are available for star imports (in alphabetic order) Any = w.Any @@ -26,11 +31,14 @@ intPositive = w.intPositive math = w.math nat = w.nat -record = w.record + +@typing.dataclass_transform() +def record(cls=None, mutable=False, globals={}, locals={}): + return w.record(cls, mutable, globals, locals) + T = w.T todo = w.todo U = w.U -unchecked = w.unchecked V = w.V __all__ = [ @@ -63,7 +71,6 @@ 'T', 'todo', 'U', - 'unchecked', 'V' ] @@ -74,3 +81,4 @@ printTestResults = w.printTestResults resetTestCount = w.resetTestCount deepEq = w.deepEq +wrapTypecheck = w.wrapTypecheck diff --git a/python/code/wypp/ansi.py b/python/code/wypp/ansi.py new file mode 100644 index 00000000..a919d0c0 --- /dev/null +++ b/python/code/wypp/ansi.py @@ -0,0 +1,32 @@ +RESET = "\u001b[0;0m" +BOLD = "\u001b[1m" +REVERSE = "\u001b[2m" + +BLACK = "\u001b[0;30m" +BLUE = "\u001b[0;34m" +GREEN = "\u001b[0;32m" +CYAN = "\u001b[0;36m" +RED = "\u001b[0;31m" +PURPLE = "\u001b[0;35m" +BROWN = "\u001b[0;33m" +GRAY = "\u001b[0;37m" +DARK_GRAY = "\u001b[1;30m" +LIGHT_BLUE = "\u001b[1;34m" +LIGHT_GREEN = "\u001b[1;32m" +LIGHT_CYAN = "\u001b[1;36m" +LIGHT_RED = "\u001b[1;31m" +LIGHT_PURPLE = "\u001b[1;35m" +YELLOW = "\u001b[1;33m" +WHITE = "\u001b[1;37m" + +def color(s, color): + return color + s + RESET + +def green(s): + return color(s, GREEN) + +def red(s): + return color(s, RED + BOLD) + +def blue(s): + return color(s, BLUE + BOLD) diff --git a/python/code/wypp/cmdlineArgs.py b/python/code/wypp/cmdlineArgs.py new file mode 100644 index 00000000..7ad2700d --- /dev/null +++ b/python/code/wypp/cmdlineArgs.py @@ -0,0 +1,53 @@ +import argparse +import sys +import utils +from myLogging import * + +def parseCmdlineArgs(argList): + parser = argparse.ArgumentParser(description='Run Your Program!', + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('--check-runnable', dest='checkRunnable', action='store_const', + const=True, default=False, + help='Abort with exit code 1 if loading the file raises errors') + parser.add_argument('--check', dest='check', action='store_const', + const=True, default=False, + help='Abort with exit code 1 if there are test errors.') + parser.add_argument('--verbose', dest='verbose', action='store_const', + const=True, default=False, + help='Be verbose') + parser.add_argument('--debug', dest='debug', action='store_const', + const=True, default=False, + help='Enable debugging') + parser.add_argument('--quiet', dest='quiet', action='store_const', + const=True, default=False, help='Be extra quiet') + parser.add_argument('--lang', dest='lang', + type=str, help='Display error messages in this language (either en or de).') + parser.add_argument('--no-clear', dest='noClear', action='store_const', + const=True, default=False, help='Do not clear the terminal') + parser.add_argument('--test-file', dest='testFile', + type=str, help='Run additional tests contained in this file.') + parser.add_argument('--extra-dir', dest='extraDirs', action='append', type=str, + help='Also typechecks files contained in the given directory.\n' \ + 'By default, only files in the same directory as the main file are\n' \ + 'checked.') + parser.add_argument('--change-directory', dest='changeDir', action='store_const', + const=True, default=False, + help='Change to the directory of FILE before running') + parser.add_argument('--interactive', dest='interactive', action='store_const', + const=True, default=False, + help='Run REPL after the programm has finished') + parser.add_argument('--no-typechecking', dest='checkTypes', action='store_const', + const=False, default=True, + help='Do not check type annotations') + parser.add_argument('file', metavar='FILE', + help='The file to run', nargs='?') + if argList is None: + argList = sys.argv[1:] + try: + args, restArgs = parser.parse_known_args(argList) + except SystemExit as ex: + utils.die(ex.code) + if args.file and not args.file.endswith('.py'): + printStderr(f'ERROR: file {args.file} is not a python file') + utils.die() + return (args, restArgs) diff --git a/python/code/wypp/constants.py b/python/code/wypp/constants.py new file mode 100644 index 00000000..d7562644 --- /dev/null +++ b/python/code/wypp/constants.py @@ -0,0 +1,6 @@ +import os + +SOURCE_DIR = os.path.normpath(os.path.dirname(__file__)) +CODE_DIR = os.path.dirname(SOURCE_DIR) + +# print(f'SOURCE_DIR: {SOURCE_DIR}, CODE_DIR: {CODE_DIR}') diff --git a/python/src/debug.py b/python/code/wypp/debug.py similarity index 100% rename from python/src/debug.py rename to python/code/wypp/debug.py diff --git a/python/src/drawingLib.py b/python/code/wypp/drawingLib.py similarity index 100% rename from python/src/drawingLib.py rename to python/code/wypp/drawingLib.py diff --git a/python/code/wypp/errors.py b/python/code/wypp/errors.py new file mode 100644 index 00000000..90cdde07 --- /dev/null +++ b/python/code/wypp/errors.py @@ -0,0 +1,288 @@ +from __future__ import annotations +from typing import * +import abc +import inspect +import location +import i18n +from renderTy import renderTy + +class WyppError(abc.ABC): + def __init__(self, extraFrames: list[inspect.FrameInfo] = []): + self.extraFrames = extraFrames[:] + +def renderLoc(loc: location.Loc) -> str: + s = '\n'.join([l.highlight() for l in location.highlightedLines(loc)]) + return s.rstrip() + +def renderStr(s: str, quote: str): + x = repr(s) + return quote + x[1:-1] + quote + +def renderGiven(givenValue: Any, givenLoc: Optional[location.Loc]) -> str: + if type(givenValue) == str and givenLoc is not None: + code = renderLoc(givenLoc) + single = renderStr(givenValue, "'") + double = renderStr(givenValue, '"') + if double in code and single not in code: + return double + elif '"' in code and "'" not in code: + return double + else: + return single + else: + return repr(givenValue) + +def isParameterizedType(t: Any) -> bool: + """True for any parameterized typing construct (incl. Union, Annotated, etc.).""" + return get_origin(t) is not None and len(get_args(t)) > 0 + +def shouldReportTyMismatch(expected: Any, given: Any) -> bool: + if isParameterizedType(expected) and get_origin(expected) == given: + # don't report a mismatch because givenTy will not be properly parameterized. + # Example: resultTy is `list[int]` and givenValue is [1, "blub"]. Then + # givenTy will be just `list` + return False + else: + return True + +def rewriteInvalidType(s: str) -> Optional[str]: + prefixes = ['Union', 'Literal', 'Optional', 'list', 'dict', 'tuple', 'set'] + for p in prefixes: + if s.startswith(f'{p}(') and s.endswith(')'): + args = s[len(p)+1:-1] + return f'{p}[{args}]' + +class WyppTypeError(TypeError, WyppError): + + def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): + WyppError.__init__(self, extraFrames) + self.msg = msg + self.add_note(msg) + + def __str__(self): + return f'WyppTypeError: {self.msg}' + + @staticmethod + def invalidType(ty: Any, loc: Optional[location.Loc]) -> WyppTypeError: + lines = [] + tyStr = renderTy(ty) + lines.append(i18n.invalidTy(tyStr)) + lines.append('') + rew = rewriteInvalidType(tyStr) + if rew: + lines.append(i18n.didYouMean(rew)) + lines.append('') + if loc is not None: + lines.append(f'## {i18n.tr("File")} {loc.filename}') + lines.append(f'## {i18n.tr("Type declared in line")} {loc.startLine}:\n') + lines.append(renderLoc(loc)) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def unknownKeywordArgument(callableName: location.CallableName, callLoc: Optional[location.Loc], name: str) -> WyppTypeError: + lines = [] + lines.append(i18n.tr('unknown keyword argument')) + lines.append('') + lines.append(i18n.unknownKeywordArgument(callableName, name)) + if callLoc and (callLocR := renderLoc(callLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {callLoc.filename}') + lines.append(f'## {i18n.tr("Problematic call in line")} {callLoc.startLine}:\n') + lines.append(callLocR) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def invalidRecordAnnotation(loc: Optional[location.Loc]) -> WyppTypeError: + lines = [] + lines.append(i18n.tr('invalid record definition')) + lines.append('') + if loc and (locR := renderLoc(loc)): + lines.append(f'## {i18n.tr("File")} {loc.filename}') + lines.append(f'## {i18n.tr("Line")} {loc.startLine}:\n') + lines.append(locR) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def resultError(callableName: location.CallableName, resultTypeLoc: Optional[location.Loc], resultTy: Any, + returnLoc: Optional[location.Loc], givenValue: Any, + callLoc: Optional[location.Loc], + extraFrames: list[inspect.FrameInfo]) -> WyppTypeError: + lines = [] + if givenValue is None: + lines.append(i18n.tr('no result returned')) + else: + lines.append(renderGiven(givenValue, returnLoc)) + lines.append('') + if resultTy is None: + # no result type expected but given + lines.append(i18n.expectingNoReturn(callableName)) + lines.append(i18n.wrongReturnValue(renderTy(type(givenValue)))) + elif givenValue is None: + # result type expected but none given + lines.append(i18n.expectingReturnOfType(callableName, renderTy(resultTy))) + lines.append(i18n.noReturnValue()) + else: + # result type expected but different type given + lines.append(i18n.expectingReturnOfType(callableName, renderTy(resultTy))) + givenTy = type(givenValue) + if shouldReportTyMismatch(resultTy, givenTy): + lines.append(i18n.wrongReturnValue(renderTy(givenTy))) + printedFileName = None + if resultTypeLoc and (resultTypeLocR := renderLoc(resultTypeLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {resultTypeLoc.filename}') + printedFileName = resultTypeLoc.filename + lines.append(f'## {i18n.tr("Result type declared in line")} {resultTypeLoc.startLine}:\n') + lines.append(resultTypeLocR) + if givenValue is not None and returnLoc and (returnLocR := renderLoc(returnLoc)): + lines.append('') + if printedFileName != returnLoc.filename: + lines.append(f'## {i18n.tr("File")} {returnLoc.filename}') + printedFileName = returnLoc.filename + lines.append(f'## {i18n.tr("Problematic return in line")} {returnLoc.startLine}:\n') + lines.append(returnLocR) + if callLoc and (callLocR := renderLoc(callLoc)): + lines.append('') + if printedFileName != callLoc.filename: + lines.append(f'## {i18n.tr("File")} {callLoc.filename}') + if givenValue is None: + lines.append(f'## {i18n.unexpectedNoReturn(callLoc.startLine)}\n') + else: + lines.append(f'## {i18n.unexpectedReturn(callLoc.startLine)}\n') + lines.append(callLocR) + raise WyppTypeError('\n'.join(lines), extraFrames) + + @staticmethod + def argumentError(callableName: location.CallableName, paramName: str, paramIndex: Optional[int], + paramLoc: Optional[location.Loc], + paramTy: Any, givenValue: Any, givenLoc: Optional[location.Loc]) -> WyppTypeError: + lines = [] + givenStr = renderGiven(givenValue, givenLoc) + lines.append(givenStr) + lines.append('') + if paramIndex is not None: + lines.append(i18n.expectingArgumentOfTy(callableName, renderTy(paramTy), paramIndex + 1)) + else: + lines.append(i18n.expectingArgumentOfTy(callableName, renderTy(paramTy), paramName)) + if shouldReportTyMismatch(paramTy, type(givenValue)): + lines.append(i18n.realArgumentTy(renderTy(type(givenValue)))) + if givenLoc and (givenLocR := renderLoc(givenLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {givenLoc.filename}') + lines.append(f'## {i18n.tr("Problematic call in line")} {givenLoc.startLine}:\n') + lines.append(givenLocR) + if paramLoc and (paramLocR := renderLoc(paramLoc)): + lines.append('') + if not givenLoc or paramLoc.filename != givenLoc.filename: + lines.append(f'## {i18n.tr("File")} {paramLoc.filename}') + lines.append(f'## {i18n.tr("Type declared in line")} {paramLoc.startLine}:\n') + lines.append(paramLocR) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def defaultError(callableName: location.CallableName, paramName: str, paramLoc: Optional[location.Loc], + paramTy: Any, givenValue: Any) -> WyppTypeError: + lines = [] + givenStr = renderGiven(givenValue, paramLoc) + lines.append(givenStr) + lines.append('') + lines.append(i18n.expectingDefaultValueOfTy(callableName, renderTy(paramTy), paramName)) + if shouldReportTyMismatch(paramTy, type(givenValue)): + lines.append(i18n.realDefaultValueTy(renderTy(type(givenValue)))) + if paramLoc and (paramLocR := renderLoc(paramLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {paramLoc.filename}') + lines.append(f'## {i18n.tr("Parameter declared in line")} {paramLoc.startLine}:\n') + lines.append(paramLocR) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def argCountMismatch(callableName: location.CallableName, callLoc: Optional[location.Loc], + numParams: int, numMandatoryParams: int, numArgs: int) -> WyppTypeError: + lines = [] + lines.append(i18n.tr('argument count mismatch')) + lines.append('') + if numArgs < numMandatoryParams: + if numParams == numMandatoryParams: + lines.append(i18n.argCountExact(callableName, numParams)) + else: + lines.append(i18n.argCountMin(callableName, numMandatoryParams)) + else: + if numParams == numMandatoryParams: + lines.append(i18n.argCountExact(callableName, numParams)) + else: + lines.append(i18n.argCountMax(callableName, numParams)) + lines.append(i18n.tr('Given: ') + i18n.argCount(numArgs)) + if callLoc and (callLocR := renderLoc(callLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {callLoc.filename}') + lines.append(f'## {i18n.tr("Call in line")} {callLoc.startLine}:\n') + lines.append(callLocR) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def partialAnnotationError(callableName: location.CallableName, paramName: str, paramLoc: Optional[location.Loc]) -> WyppTypeError: + lines = [] + lines.append(i18n.expectingTypeAnnotation(callableName, paramName)) + if paramLoc and (paramLocR := renderLoc(paramLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {paramLoc.filename}') + lines.append(f'## {i18n.tr("Parameter declared in line")} {paramLoc.startLine}:\n') + lines.append(paramLocR) + raise WyppTypeError('\n'.join(lines)) + + @staticmethod + def noTypeAnnotationForRecordAttribute(attrName: str, recordName: str) -> WyppTypeError: + return WyppTypeError(i18n.noTypeAnnotationForAttribute(attrName, recordName)) + + @staticmethod + def recordAssignError(recordName: str, + attrName: str, + attrTy: Any, + attrLoc: Optional[location.Loc], + setterValue: Any, + setterLoc: Optional[location.Loc]) -> WyppTypeError: + lines = [] + givenStr = renderGiven(setterValue, setterLoc) + lines.append(givenStr) + lines.append('') + lines.append(i18n.recordAttrDeclTy(recordName, attrName, renderTy(attrTy))) + if shouldReportTyMismatch(attrTy, type(setterValue)): + lines.append(i18n.realSetAttrTy(renderTy(type(setterValue)))) + if setterLoc and (setterLocR := renderLoc(setterLoc)): + lines.append('') + lines.append(f'## {i18n.tr("File")} {setterLoc.filename}') + lines.append(f'## {i18n.tr("Problematic assignment in line")} {setterLoc.startLine}:\n') + lines.append(setterLocR) + if attrLoc and (attrLocR := renderLoc(attrLoc)): + lines.append('') + if not setterLoc or setterLoc.filename != attrLoc.filename: + lines.append(f'## {i18n.tr("File")} {attrLoc.filename}') + lines.append(f'## {i18n.tr("Type declared in line")} {attrLoc.startLine}:\n') + lines.append(attrLocR) + raise WyppTypeError('\n'.join(lines)) + +class WyppAttributeError(AttributeError, WyppError): + def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): + WyppError.__init__(self, extraFrames) + self.msg = msg + self.add_note(msg) + + @staticmethod + def unknownAttr(clsName: str, attrName: str) -> WyppAttributeError: + return WyppAttributeError(i18n.tr('Unknown attribute {attrName} for record {clsName}', + clsName=clsName, attrName=attrName)) + +class TodoError(Exception, WyppError): + def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): + WyppError.__init__(self, extraFrames) + self.msg = msg + self.add_note(msg) + + +class ImpossibleError(Exception, WyppError): + def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): + WyppError.__init__(self, extraFrames) + self.msg = msg + self.add_note(msg) + diff --git a/python/code/wypp/exceptionHandler.py b/python/code/wypp/exceptionHandler.py new file mode 100644 index 00000000..dc575c13 --- /dev/null +++ b/python/code/wypp/exceptionHandler.py @@ -0,0 +1,67 @@ +import sys +import traceback +import re +from dataclasses import dataclass + +# local imports +from constants import * +import stacktrace +import paths +from myLogging import * +import errors +import utils + +_tbPattern = re.compile(r'(\s*File\s+")([^"]+)(".*)') +def _rewriteFilenameInTracebackLine(s: str) -> str: + # Match the pattern: File "filename", line number, in function + match = re.match(_tbPattern, s) + if match: + prefix = match.group(1) + filename = match.group(2) + suffix = match.group(3) + canonicalized = paths.canonicalizePath(filename) + return prefix + canonicalized + suffix + else: + return s + +def _rewriteFilenameInTraceback(s: str) -> str: + lines = s.split('\n') + result = [] + for l in lines: + result.append(_rewriteFilenameInTracebackLine(l)) + return '\n'.join(result) + +def handleCurrentException(exit=True, removeFirstTb=False, file=sys.stderr): + (etype, val, tb) = sys.exc_info() + if isinstance(val, SystemExit): + utils.die(val.code) + isWyppError = isinstance(val, errors.WyppError) + if isWyppError: + extra = val.extraFrames + else: + extra = [] + frameList = (stacktrace.tbToFrameList(tb) if tb is not None else []) + if frameList and removeFirstTb: + frameList = frameList[1:] + isBug = not isWyppError and not isinstance(val, SyntaxError) and \ + len(frameList) > 0 and stacktrace.isWyppFrame(frameList[-1]) + stackSummary = stacktrace.limitTraceback(frameList, extra, not isBug and not isDebug()) + header = False + for x in stackSummary.format(): + if not header: + file.write('Traceback (most recent call last):\n') + header = True + file.write(_rewriteFilenameInTraceback(x)) + if isWyppError: + s = str(val) + if s and s[0] != '\n': + file.write('\n') + file.write(s) + file.write('\n') + else: + for x in traceback.format_exception_only(etype, val): + file.write(x) + if isBug: + file.write(f'BUG: the error above is most likely a bug in WYPP!') + if exit: + utils.die(1) diff --git a/python/code/wypp/i18n.py b/python/code/wypp/i18n.py new file mode 100644 index 00000000..a9482ef0 --- /dev/null +++ b/python/code/wypp/i18n.py @@ -0,0 +1,348 @@ +from dataclasses import dataclass +import location +from typing import * +from contextlib import contextmanager +import lang +import utils + +type Lang = Literal['en', 'de'] + +allLanguages: list[Lang] = ['en', 'de'] + +# If not set explicitly, the current locale is asked for the language +_lang: Optional[Lang] = None + +@contextmanager +def explicitLang(newLang: Lang): + """Context manager to temporarily set the language.""" + global _lang + oldLang = _lang + _lang = newLang + try: + yield + finally: + _lang = oldLang + +def getLang() -> Lang: + if _lang: + return _lang + else: + return lang.pickLanguage(allLanguages, 'en') + +def setLang(lang: Lang): + global _lang + _lang = lang + +def tr(key: str, **kws) -> str: + match getLang(): + case 'en': + tmpl = key + case 'de': + if key not in DE and utils.isUnderTest(): + raise ValueError(f'Untranslated string: {key}') + tmpl = DE.get(key, key) + case x: + if utils.underTest(): + raise ValueError(f'Unknown language: {x}') + return tmpl.format(**kws) + +DE = { + 'Expecting no return value when calling function `{fun}`.': + 'Kein Rückgabewert erwartet bei Aufruf der Funktion `{fun}`.', + 'Expecting no return value when calling method `{method}` of class `{cls}.': + 'Kein Rückgabewert erwartet bei Aufruf der Methode `{method}` aus Klasse `{cls}`.', + 'Expecting no return value when calling constructor of record `{cls}`.': + 'Kein Rückgabewert erwartet bei Aufruf des Konstruktors des Records `{cls}`.', + + 'Expecting return value of type `{ty}` when calling function `{fun}`.': + 'Rückgabewert vom Typ `{ty}` erwartet bei Aufruf der Funktion `{fun}`.', + 'Expecting return value of type `{ty}` when calling method `{method}` of class `{cls}`.': + 'Rückgabewert vom Typ `{ty}` erwartet bei Aufruf von Methode `{method}` aus Klasse `{cls}`.', + 'Expecting return value of type `{ty}` when calling constructor of record `{cls}`.': + 'Rückgabewert vom Typ `{ty}` erwartet bei Aufruf des Konstruktors des Records `{cls}`.', + + 'But the call returns a value of type `{ty}`.': + 'Aber der Aufruf gibt einen Wert vom Typ `{ty}` zurück.', + + 'But no value returned.': + 'Aber kein Rückgabewert vorhanden.', + 'no result returned': 'kein Rückgabewert vorhanden', + 'Call in line {line} causes the function to return no value:': + 'Aufruf in Zeile {line} führt dazu, dass die Funktion keinen Wert zurückgibt:', + + 'The call of function `{fun}` expects value of type `{ty}` as {arg}.': + 'Der Aufruf der Funktion `{fun}` erwartet Wert vom Typ `{ty}` als {arg}.', + 'The call of method `{method}` of class `{cls}` expects value of type `{ty}` as {arg}.': + 'Der Aufruf der Methode `{method}` aus Klasse `{cls}` erwartet Wert vom Typ `{ty}` als {arg}.', + 'The call of the constructor of record `{cls}` expects value of type `{ty}` as {arg}.': + 'Der Aufruf des Konstruktors des Records `{cls}` erwartet Wert vom Typ `{ty}` als {arg}.', + + 'But the value given has type `{ty}`.': 'Aber der übergebene Wert hat Typ `{ty}`.', + 'But the default value has type `{ty}`.': 'Aber der Default-Wert hat Typ `{ty}`.', + + 'File': 'Datei', + 'Line': 'Zeile', + 'Result type declared in line': 'Rückgabetyp deklariert in Zeile', + 'Type declared in line': 'Typ deklariert in Zeile', + 'Parameter declared in line': 'Parameter deklariert in Zeile', + 'Problematic return in line': 'Fehlerhaftes return in Zeile', + 'Problematic call in line': 'Fehlerhafter Aufruf in Zeile', + 'Call in line {line} causes the problematic return:': + 'Aufruf in Zeile {line} verursacht das fehlerhafte return:', + + 'Parameter `{param}` of function `{fun}` requires a type annotation.': + 'Parameter `{param}` der Funktion `{fun}` benötigt eine Typannotation.', + 'Parameter `{param}` of method `{method}` from class `{cls}` requires a type annotation.': + 'Parameter `{param}` der Methode `{method}` aus Klasse `{cls}` benötigt eine Typannotation.', + 'Parameter `{param}` of constructor of record `{cls}` requires a type annotation.': + 'Parameter `{param}` des Konstruktors des Records `{cls}` benötigt eine Typannotation.', + + 'Attribute `{name}` of record `{record}` required a type annotation.': + 'Attribut `{name}` des Records `{record}` benötigt eine Typannotation.', + + 'invalid type `{ty}`': + 'ungültiger Typ `{ty}`', + 'Cannot set attribute to value of type `{ty}`.': + 'Das Attribute kann nicht auf einen Wert vom Typ `{ty}` gesetzt werden.', + 'Problematic assignment in line': 'Fehlerhafte Zuweisung in Zeile', + 'Attribute `{attrName}` of record `{recordName}` declared with type `{ty}.`': + 'Attribut `{attrName}` des Records `{recordName}` deklariert als Typ `{ty}`.', + + 'argument count mismatch': 'Anzahl der Argument passt nicht', + 'Call in line': 'Aufruf in Zeile', + 'Function `{fun}` takes ': 'Funktion `{fun}` benötigt ', + 'Function `{fun}` takes at least ': 'Funktion `{fun}` benötigt mindestens ', + 'Function `{fun}` takes at most ': 'Funktion `{fun}` akzeptiert höchstens ', + 'Method `{method}` of class `{cls}` takes ': 'Methode `{method}` der Klasse `{cls}` benötigt ', + 'Method `{method}` of class `{cls}` takes at least ': 'Methode `{method}` der Klasse `{cls}` benötigt mindestens ', + 'Method `{method}` of class `{cls}` takes at most ': 'Methode `{method}` der Klasse `{cls}` akzeptiert höchstens ', + 'Constructor of record `{cls}` takes ': 'Konstruktor des Records `{cls}` benötigt ', + 'Constructor of record `{cls}` takes at least ': 'Konstruktor des Records `{cls}` benötigt mindestens ', + 'Constructor of record `{cls}` takes at most ': 'Konstruktor des Records `{cls}` akzeptiert höchstens ', + 'Given: ': 'Gegeben: ', + + 'Default value for parameter `{paramName}` of function `{fun}` must have type `{ty}`.': + 'Default-Wert des Parameters `{paramName}` der Funktion `{fun}` muss vom Typ `{ty}` sein.', + 'Default value for parameter `{paramName}` of method `{method}` in class `{cls}` must have type `{ty}`.': + 'Default-Wert des Parameters `{paramName}` der Methode `{method}` aus Klasse `{cls}` muss vom Typ `{ty}` sein.', + 'Default value for attribute `{paramName}` of record `{cls}` must have type `{ty}`.': + 'Default-Wert des Attributs `{paramName}` des Records `{cls}` muss vom Typ `{ty}` sein.', + + 'Unknown attribute {attrName} for record {clsName}': + 'Attribut {attrName} ist nicht bekannt für Record {clsName}', + 'Did you mean `{ty}`?': 'Wolltest du `{ty}` schreiben?', + + 'unknown keyword argument': 'unbekanntes Schlüsselwort-Argument', + 'Function `{fun}` does not accept keyword argument `{name}`.': + 'Funktion `{fun}` akzeptiert kein Schlüsselwort-Argument `{name}`.', + 'Method `{method}` from class `{cls}` does not accept keyword argument `{name}`.': + 'Methode `{method}` der Klasse `{cls}` akzeptiert kein Schlüsselwort-Argument `{name}`.', + 'Constructor of record `{cls}` does not accept keyword argument `{name}`.': + 'Konstruktor des Records `{cls}` akzeptiert kein Schlüsselwort-Argument `{name}`.', + + 'invalid record definition': 'ungültige Record-Definition' +} + +def expectingNoReturn(cn: location.CallableName) -> str: + match cn.kind: + case 'function': + return tr('Expecting no return value when calling function `{fun}`.', + fun=cn.name) + case location.ClassMember('method', cls): + return tr('Expecting no return value when calling method `{method}` of class `{cls}.', + method=cn.name, cls=cls) + case location.ClassMember('recordConstructor', cls): + return tr('Expecting no return value when calling constructor of record `{cls}`.', + cls=cls) + raise ValueError(f'Unexpected: {cn}') + +def wrongReturnValue(ty: str) -> str: + return tr('But the call returns a value of type `{ty}`.', ty=ty) + +def unexpectedReturn(line: int) -> str: + return tr('Call in line {line} causes the problematic return:', line=line) + +def unexpectedNoReturn(line: int) -> str: + return tr('Call in line {line} causes the function to return no value:', line=line) + +def expectingReturnOfType(cn: location.CallableName, ty: str) -> str: + match cn.kind: + case 'function': + return tr('Expecting return value of type `{ty}` when calling function `{fun}`.', + fun=cn.name, ty=ty) + case location.ClassMember('method', cls): + return tr('Expecting return value of type `{ty}` when calling method `{method}` of class `{cls}`.', + method=cn.name, cls=cls, ty=ty) + case location.ClassMember('recordConstructor', cls): + return tr('Expecting return value of type `{ty}` when calling constructor of record `{cls}`.', + cls=cls, ty=ty) + raise ValueError(f'Unexpected: {cn}') + +def noReturnValue() -> str: + return tr('But no value returned.') + +def transArg(pos: int): + match getLang(): + case 'en': + match pos: + case 1: return '1st argument' + case 2: return '2nd argument' + case 3: return '3rd argument' + case _: return f'{pos}th argument' + case 'de': + match pos: + case 1: return 'erstes Argument' + case 2: return 'zweites Argument' + case 3: return 'drittes Argument' + case 4: return 'viertes Argument' + case 5: return 'fünftes Argument' + case 6: return 'sechstes Argument' + case 7: return 'siebtes Argument' + case 8: return 'achtes Argument' + case 9: return 'neuntes Argument' + case _: return f'{pos}. Argument' + case l: + raise ValueError(f'Unexpected language: {l}') + + +def expectingArgumentOfTy(cn: location.CallableName, ty: str, pos: int | str) -> str: + if isinstance(pos, int): + arg = transArg(pos) + else: + match getLang(): + case 'en': + arg = f'argument `{pos}`' + case 'de': + arg = f'Argument `{pos}`' + match cn.kind: + case 'function': + return tr('The call of function `{fun}` expects value of type `{ty}` as {arg}.', + fun=cn.name, ty=ty, arg=arg) + case location.ClassMember('method', cls): + return tr('The call of method `{method}` of class `{cls}` expects value of type `{ty}` as {arg}.', + method=cn.name, cls=cls, ty=ty, arg=arg) + case location.ClassMember('recordConstructor', cls): + return tr('The call of the constructor of record `{cls}` expects value of type `{ty}` as {arg}.', + cls=cls, ty=ty, arg=arg) + raise ValueError(f'Unexpected: {cn}') + +def expectingDefaultValueOfTy(cn: location.CallableName, ty: str, paramName: str) -> str: + match cn.kind: + case 'function': + return tr('Default value for parameter `{paramName}` of function `{fun}` must have type `{ty}`.', + paramName=paramName, fun=cn.name, ty=ty) + case location.ClassMember('method', cls): + return tr('Default value for parameter `{paramName}` of method `{method}` in class `{cls}` must have type `{ty}`.', + paramName=paramName, method=cn.name, cls=cls, ty=ty) + case location.ClassMember('recordConstructor', cls): + return tr('Default value for attribute `{paramName}` of record `{cls}` must have type `{ty}`.', + paramName=paramName, cls=cls, ty=ty) + raise ValueError(f'Unexpected: {cn}') + +def argCount(n: int) -> str: + match getLang(): + case 'en': + if n == 0: + return 'no arguments' + elif n == 1: + return '1 argument' + else: + return f'{n} arguments' + case 'de': + if n == 0: + return 'keine Argumente' + elif n == 1: + return '1 Argument' + else: + return f'{n} Argumente' + +def argCountExact(cn: location.CallableName, expected: int) -> str: + match cn.kind: + case 'function': + header = tr('Function `{fun}` takes ', fun=cn.name) + case location.ClassMember('method', cls): + header = tr('Method `{method}` of class `{cls}` takes ', + method=cn.name, cls=cls) + case location.ClassMember('recordConstructor', cls): + header = tr('Constructor of record `{cls}` takes ', + cls=cls) + case _: + raise ValueError(f'Unexpected: {cn}') + return header + argCount(expected) + '.' + +def argCountMin(cn: location.CallableName, expected: int) -> str: + match cn.kind: + case 'function': + header = tr('Function `{fun}` takes at least ', fun=cn.name) + case location.ClassMember('method', cls): + header = tr('Method `{method}` of class `{cls}` takes at least ', + method=cn.name, cls=cls) + case location.ClassMember('recordConstructor', cls): + header = tr('Constructor of record `{cls}` takes at least ', + cls=cls) + case _: + raise ValueError(f'Unexpected: {cn}') + return header + argCount(expected) + '.' + +def argCountMax(cn: location.CallableName, expected: int) -> str: + match cn.kind: + case 'function': + header = tr('Function `{fun}` takes at most ', fun=cn.name) + case location.ClassMember('method', cls): + header = tr('Method `{method}` of class `{cls}` takes at most ', + method=cn.name, cls=cls) + case location.ClassMember('recordConstructor', cls): + header = tr('Constructor of record `{cls}` takes at most ', + cls=cls) + case _: + raise ValueError(f'Unexpected: {cn}') + return header + argCount(expected) + '.' + +def realArgumentTy(ty: str) -> str: + return tr('But the value given has type `{ty}`.', ty=ty) + +def realDefaultValueTy(ty: str) -> str: + return tr('But the default value has type `{ty}`.', ty=ty) + +def realSetAttrTy(ty: str) -> str: + return tr('Cannot set attribute to value of type `{ty}`.', ty=ty) + +def expectingTypeAnnotation(cn: location.CallableName, param: str) -> str: + match cn.kind: + case 'function': + return tr('Parameter `{param}` of function `{fun}` requires a type annotation.', + fun=cn.name, param=param) + case location.ClassMember('method', cls): + return tr('Parameter `{param}` of method `{method}` from class `{cls}` requires a type annotation.', + method=cn.name, cls=cls, param=param) + case location.ClassMember('recordConstructor', cls): + return tr('Parameter `{param}` of constructor of record `{cls}` requires a type annotation.', + cls=cls, param=param) + raise ValueError(f'Unexpected: {cn}') + +def noTypeAnnotationForAttribute(attrName: str, recordName: str) -> str: + return tr('Attribute `{name}` of record `{record}` required a type annotation.', + name=attrName, record=recordName) + +def invalidTy(ty: Any) -> str: + return tr('invalid type `{ty}`', ty=ty) + +def didYouMean(ty: str) -> str: + return tr('Did you mean `{ty}`?', ty=ty) + +def recordAttrDeclTy(recordName: str, attrName: str, ty: Any) -> str: + return tr('Attribute `{attrName}` of record `{recordName}` declared with type `{ty}.`', + recordName=recordName, attrName=attrName, ty=ty) + +def unknownKeywordArgument(cn: location.CallableName, name: str) -> str: + match cn.kind: + case 'function': + return tr('Function `{fun}` does not accept keyword argument `{name}`.', + fun=cn.name, name=name) + case location.ClassMember('method', cls): + return tr('Method `{method}` from class `{cls}` does not accept keyword argument `{name}`.', + method=cn.name, cls=cls, name=name) + case location.ClassMember('recordConstructor', cls): + return tr('Constructor of record `{cls}` does not accept keyword argument `{name}`.', + cls=cls, name=name) + raise ValueError(f'Unexpected: {cn}') diff --git a/python/code/wypp/instrument.py b/python/code/wypp/instrument.py new file mode 100644 index 00000000..68178e6d --- /dev/null +++ b/python/code/wypp/instrument.py @@ -0,0 +1,180 @@ +from typing import * +import os +import ast +import importlib +import importlib.abc +from importlib.machinery import ModuleSpec, SourceFileLoader +from importlib.util import decode_source +from collections.abc import Buffer +import types +from os import PathLike +import utils +from myLogging import * +from contextlib import contextmanager +import errors +import location + +def parseExp(s: str) -> ast.expr: + match ast.parse(s): + case ast.Module([ast.Expr(e)], type_ignores): + return e + case m: + raise ValueError(f'String {repr(s)} does not parse as an expression: {m}') + +class Configs: + funConfig: ast.expr = parseExp("{'kind': 'function', 'globals': globals(), 'locals': locals()}") + @staticmethod + def methodConfig(clsName: str) -> ast.expr: + return parseExp("{'kind': 'method', 'className': " + repr(clsName) + ", 'globals': globals(), 'locals': locals()}") + immutableRecordConfig: ast.expr = parseExp('record(mutable=False, globals=globals(), locals=locals())') + mutableRecordConfig: ast.expr = parseExp('record(mutable=True, globals=globals(), locals=locals())') + +def transferLocs(old: ast.stmt | ast.expr, new: ast.stmt | ast.expr) -> Any: + new.lineno = old.lineno + new.col_offset = old.col_offset + new.end_lineno = old.end_lineno + new.end_col_offset = old.end_col_offset + return new + +def transformDecorator(e: ast.expr, path: str) -> ast.expr: + loc = location.Loc(path, e.lineno, e.col_offset, e.end_lineno, e.col_offset) + match e: + case ast.Name('record'): + return transferLocs(e, Configs.immutableRecordConfig) + case ast.Call(ast.Name('record'), [], kwArgs): + match kwArgs: + case [ast.keyword('mutable', ast.Constant(True))]: + return transferLocs(e, Configs.mutableRecordConfig) + case [ast.keyword('mutable', ast.Constant(False))]: + return transferLocs(e, Configs.immutableRecordConfig) + case _: + raise errors.WyppTypeError.invalidRecordAnnotation(loc) + case ast.Call(ast.Name('record'), _, _): + raise ValueError(f'Invalid record config') + case _: + return e + +def transformStmt(stmt: ast.stmt, outerClassName: Optional[str], path: str) -> ast.stmt: + cfg = Configs.methodConfig(outerClassName) if outerClassName else Configs.funConfig + wrapExp = ast.Call(ast.Name(id='wrapTypecheck', ctx=ast.Load()), [cfg], []) + match stmt: + case ast.FunctionDef(name, args, body, decorators, returns, tyComment, tyParams): + newBody = [transformStmt(s, outerClassName=outerClassName, path=path) for s in body] + x = ast.FunctionDef(name, args, newBody, decorators + [wrapExp], returns, tyComment, tyParams) + return transferLocs(stmt, x) + case ast.AsyncFunctionDef(name, args, body, decorators, returns, tyComment, tyParams): + newBody = [transformStmt(s, outerClassName=outerClassName, path=path) for s in body] + x = ast.AsyncFunctionDef(name, args, newBody, decorators + [wrapExp], returns, tyComment, tyParams) + return transferLocs(stmt, x) + case ast.ClassDef(className, bases, keywords, body, decoratorList, type_params): + newBody = [transformStmt(s, outerClassName=className, path=path) for s in body] + newDecoratorList = [transformDecorator(e, path=path) for e in decoratorList] + x = ast.ClassDef(className, bases, keywords, newBody, newDecoratorList, type_params) + return transferLocs(stmt, x) + case _: + return stmt + +def isImport(t: ast.stmt) -> bool: + match t: + case ast.Import(): return True + case ast.ImportFrom(): return True + case _: return False + +importWrapTypecheck = ast.parse("from wypp import wrapTypecheck", mode="exec").body[0] + +def transformModule(m: ast.Module | ast.Expression | ast.Interactive, path: str) -> ast.Module | ast.Expression | ast.Interactive: + match m: + case ast.Module(body, type_ignores): + newStmts = [transformStmt(stmt, outerClassName=None, path=path) for stmt in body] + (imports, noImports) = utils.split(newStmts, isImport) + # avoid inserting before from __future__ + newStmts = imports + [importWrapTypecheck] + noImports + return ast.Module(newStmts, type_ignores) + case _: + return m + +class InstrumentingLoader(SourceFileLoader): + @staticmethod + def source_to_code( + data: Buffer | str | ast.Module | ast.Expression | ast.Interactive, + path: Buffer | str | PathLike[str] = "", + ) -> types.CodeType: + if isinstance(data, (ast.Module, ast.Expression, ast.Interactive)): + tree = data + else: + if isinstance(data, str): + source = data + else: + source = decode_source(data) + tree = utils._call_with_frames_removed(ast.parse, source, path, "exec") + if isinstance(path, PathLike): + pathStr = str(path) + elif isinstance(path, str): + pathStr = path + else: + pathStr = "" + tree = transformModule(tree, pathStr) + ast.fix_missing_locations(tree) + + debug( + f"Source code of {path!r} after instrumentation:\n" + + "----------------------------------------------\n" + + ast.unparse(tree) + "\n" + "----------------------------------------------") + + code = utils._call_with_frames_removed(compile, tree, path, "exec", 0, dont_inherit=True) + return code + +class InstrumentingFinder(importlib.abc.MetaPathFinder): + def __init__(self, finder, modDir: str, extraDirs: list[str]): + self._origFinder = finder + self.modDir = os.path.realpath(modDir) + '/' + self.extraDirs = [os.path.realpath(d) for d in extraDirs] + + def find_spec( + self, + fullname: str, + path: Sequence[str] | None, + target: types.ModuleType | None = None, + ) -> ModuleSpec | None: + spec = self._origFinder.find_spec(fullname, path, target) + if spec is None: + return None + origin = os.path.realpath(spec.origin) + dirs = [self.modDir] + self.extraDirs + isLocalModule = False + for d in dirs: + if origin.startswith(d): + isLocalModule = True + break + # print(f'Module {fullname} is locale: {isLocalModule} ({origin})') + if spec and spec.loader and isLocalModule: + spec.loader = InstrumentingLoader(spec.loader.name, spec.loader.path) + return spec + +@contextmanager +def setupFinder(modDir: str, extraDirs: list[str], typechecking: bool): + if not typechecking: + yield + else: + # Find the PathFinder + for finder in sys.meta_path: + if ( + isinstance(finder, type) + and finder.__name__ == "PathFinder" + and hasattr(finder, "find_spec") + ): + break + else: + raise RuntimeError("Cannot find a PathFinder in sys.meta_path") + + # Create and install our custom finder + instrumenting_finder = InstrumentingFinder(finder, modDir, extraDirs) + sys.meta_path.insert(0, instrumenting_finder) + + try: + yield + finally: + # Remove our custom finder when exiting the context + if instrumenting_finder in sys.meta_path: + sys.meta_path.remove(instrumenting_finder) diff --git a/python/code/wypp/interactive.py b/python/code/wypp/interactive.py new file mode 100644 index 00000000..6e1ef9d7 --- /dev/null +++ b/python/code/wypp/interactive.py @@ -0,0 +1,63 @@ + +import sys +import os +import code +from dataclasses import dataclass + +# local imports +from constants import * +from myLogging import * +from exceptionHandler import handleCurrentException + +def prepareInteractive(reset=True): + print() + if reset: + if os.name == 'nt': + # clear the terminal + os.system('cls') + else: + # On linux & mac use ANSI Sequence for this + print('\033[2J\033[H') + + +HISTORY_SIZE = 1000 + +def getHistoryFilePath(): + envVar = 'HOME' + if os.name == 'nt': + envVar = 'USERPROFILE' + d = os.getenv(envVar, None) + if d: + return os.path.join(d, ".wypp_history") + else: + return None + +class TypecheckedInteractiveConsole(code.InteractiveConsole): + def showtraceback(self) -> None: + handleCurrentException(exit=False, removeFirstTb=True, file=sys.stdout) + +def enterInteractive(userDefs: dict, checkTypes: bool, loadingFailed: bool): + for k, v in userDefs.items(): + globals()[k] = v + print() + if loadingFailed: + print('NOTE: running the code failed, some definitions might not be available!') + print() + if checkTypes: + consoleClass = TypecheckedInteractiveConsole + else: + consoleClass = code.InteractiveConsole + historyFile = getHistoryFilePath() + try: + import readline + readline.parse_and_bind('tab: complete') + if historyFile and os.path.exists(historyFile): + readline.read_history_file(historyFile) + except: + pass + try: + consoleClass(locals=userDefs).interact(banner="", exitmsg='') + finally: + if readline and historyFile: + readline.set_history_length(HISTORY_SIZE) + readline.write_history_file(historyFile) diff --git a/python/code/wypp/lang.py b/python/code/wypp/lang.py new file mode 100644 index 00000000..e19a7b6f --- /dev/null +++ b/python/code/wypp/lang.py @@ -0,0 +1,51 @@ +import os +import locale +from _collections_abc import MutableMapping + +def _langFromEnv(env: MutableMapping) -> str | None: + # 1) GNU LANGUAGE: colon-separated fallbacks (e.g., "de:en_US:en") + os.getenv + lng = env.get("LANGUAGE") + if lng: + for part in lng.split(":"): + part = part.strip() + if part: + return part + + # 2) POSIX locale vars in order of precedence + for var in ("LC_ALL", "LC_MESSAGES", "LANG"): + val = env.get(var) + if val and val not in ("C", "POSIX"): + return val + + # 3) locale module fallback (works cross-platform-ish) + try: + locale.setlocale(locale.LC_CTYPE, "") # load user default + except locale.Error: + pass + lang, _enc = locale.getlocale() # e.g. "de_DE" + return lang + +def _normLang(tag: str) -> str: + # "de-DE.UTF-8" -> "de_DE" + tag = tag.replace("-", "_") + if "." in tag: + tag = tag.split(".", 1)[0] + return tag + +def pickLanguage[T: str](supported: list[T], default: T) -> T: + """Return best match like 'de' or 'de_DE' from supported codes.""" + raw = _langFromEnv(os.environ) + if not raw: + return default + want = _normLang(raw) + # exact match first + for s in supported: + if _normLang(s).lower() == want.lower(): + return s + # fallback to language-only match (de_DE -> de) + wantBase = want.split("_")[0].lower() + for s in supported: + if _normLang(s).split('_')[0].lower() == wantBase: + return s + return default diff --git a/python/code/wypp/location.py b/python/code/wypp/location.py new file mode 100644 index 00000000..12de0a0e --- /dev/null +++ b/python/code/wypp/location.py @@ -0,0 +1,296 @@ +from __future__ import annotations +from typing import * +from dataclasses import dataclass +import inspect +import linecache +import dis +import ast +import ansi +import utils +import myLogging +import sys +import abc +import parsecache +from parsecache import FunMatcher +import paths + +@dataclass +class Loc: + filename: str + startLine: int + startCol: Optional[int] + endLine: Optional[int] + endCol: Optional[int] + + def __post_init__(self): + self.filename = paths.canonicalizePath(self.filename) + + def fullSpan(self) -> Optional[tuple[int, int, int, int]]: + if self.startCol and self.endLine and self.endCol: + return (self.startLine, self.startCol, self.endLine, self.endCol) + else: + return None + + def code(self) -> Optional[str]: + match self.fullSpan(): + case None: + return None + case (startLine, startCol, endLine, endCol): + result = [] + for lineNo in range(startLine, startLine+1): + line = linecache.getline(self.filename, lineNo).rstrip("\n") + c1 = startCol if lineNo == startLine else 0 + c2 = endCol if lineNo == endLine else len(line) + result.append(line[c1:c2]) + return '\n'.join(result) + + @staticmethod + def fromFrameInfo(fi: inspect.FrameInfo) -> Loc: + default = Loc(fi.filename, fi.lineno, None, None, None) + if fi.positions is None: + return default + p: dis.Positions = fi.positions + startLine = p.lineno + endLine = p.end_lineno + startCol = p.col_offset + endCol = p.end_col_offset + if startLine is None or endLine is None or startCol is None or endCol is None: + return default + else: + return Loc(fi.filename, startLine, startCol, endLine, endCol) + +HIGHLIGHTING_ENV_VAR = 'WYPP_HIGHLIGHTING' +type HighlightMode = Literal['color', 'text', 'off'] + +def getHighlightMode(mode: HighlightMode | Literal['fromEnv']) -> HighlightMode: + if mode == 'fromEnv': + fromEnv = utils.getEnv(HIGHLIGHTING_ENV_VAR, lambda x: x, None) + match fromEnv: + case 'color': return 'color' + case 'text': return 'text' + case 'off': return 'off' + case None: return 'color' + case _: + myLogging.warn(f'Invalid highlighting mode in environment variable {HIGHLIGHTING_ENV_VAR}: {fromEnv} (supported: color, text, off)') + return 'off' + else: + return mode + +def highlight(s: str, mode: HighlightMode) -> str: + match mode: + case 'color': return ansi.red(s) + case 'off': return s + case 'text': return f'<<{s}>>' + +@dataclass +class SourceLine: + line: str # without trailing \n + span: Optional[tuple[int, int]] # (inclusive, exclusive) + + def highlight(self, mode: HighlightMode | Literal['fromEnv'] = 'fromEnv'): + mode = getHighlightMode(mode) + if self.span: + l = self.line + return l[:self.span[0]] + highlight(l[self.span[0]:self.span[1]], mode) + l[self.span[1]:] + else: + return self.line + +def highlightedLines(loc: Loc) -> list[SourceLine]: + match loc.fullSpan(): + case None: + line = linecache.getline(loc.filename, loc.startLine).rstrip("\n") + return [SourceLine(line, None)] + case (startLine, startCol, endLine, endCol): + result = [] + for lineNo in range(startLine, startLine+1): + line = linecache.getline(loc.filename, lineNo).rstrip("\n") + leadingSpaces = len(line) - len(line.lstrip()) + c1 = startCol if lineNo == startLine else leadingSpaces + c2 = endCol if lineNo == endLine else len(line) + result.append(SourceLine(line, (c1, c2))) + return result + +@dataclass +class ClassMember: + kind: Literal['method', 'recordConstructor'] + className: str + +type CallableKind = Literal['function'] | ClassMember + +@dataclass +class CallableName: + name: str + kind: CallableKind + @staticmethod + def mk(c: CallableInfo) -> CallableName: + return CallableName(c.name, c.kind) + +class CallableInfo(abc.ABC): + """ + Class giving access to various properties of a function, method or constructor. + """ + def __init__(self, kind: CallableKind): + self.kind: CallableKind = kind + @property + @abc.abstractmethod + def name(self) -> str: + pass + @abc.abstractmethod + def getResultTypeLocation(self) -> Optional[Loc]: + pass + @abc.abstractmethod + def getParamSourceLocation(self, paramName: str) -> Optional[Loc]: + pass + +class StdCallableInfo(CallableInfo): + """ + Class giving access to various properties of a function + (arguments, result type etc.) + """ + def __init__(self, f: Callable, kind: CallableKind): + super().__init__(kind) + self.file = f.__code__.co_filename + self.__lineno = f.__code__.co_firstlineno + self.__name = f.__name__ + self.__ast = parsecache.getAST(self.file) + + def __repr__(self): + return f'StdCallableInfo({self.name}, {self.kind})' + + @property + def name(self): + return self.__name + + def _findDef(self) -> Optional[ast.FunctionDef | ast.AsyncFunctionDef]: + m = FunMatcher(self.__name, self.__lineno) + match self.kind: + case 'function': + return self.__ast.getFunDef(m) + case ClassMember('method', clsName): + return self.__ast.getMethodDef(clsName, m) + case k: + raise ValueError(f'Unexpected CallableKind {k} in StdCallableInfo') + + def getResultTypeLocation(self) -> Optional[Loc]: + """ + Returns the location of the result type + """ + node = self._findDef() + if not node: + return None + r = node.returns + if r: + return Loc(self.file, + r.lineno, + r.col_offset, + r.end_lineno, + r.end_col_offset) + else: + # There is no return type annotation + return None + + def getParamSourceLocation(self, paramName: str) -> Optional[Loc]: + """ + Returns the location of the parameter with the given name. + """ + node = self._findDef() + if not node: + return None + res = None + for arg in node.args.args + node.args.kwonlyargs: + if arg.arg == paramName: + res = arg + break + if res is None: + # Look in vararg and kwarg + if node.args.vararg and node.args.vararg.arg == paramName: + res = node.args.vararg + if node.args.kwarg and node.args.kwarg.arg == paramName: + res = node.args.kwarg + if res is None: + return None + else: + return Loc(self.file, + res.lineno, + res.col_offset, + res.end_lineno, + res.end_col_offset) + + +def classFilename(cls) -> str | None: + """Best-effort path to the file that defined `cls`.""" + try: + fn = inspect.getsourcefile(cls) or inspect.getfile(cls) + if fn: + return fn + except TypeError: + pass + # Fallback via the owning module (works for some frozen/zip cases) + mod = sys.modules.get(cls.__module__) + if mod is not None: + return getattr(mod, "__file__", None) or getattr(getattr(mod, "__spec__", None), "origin", None) + return None + +class RecordConstructorInfo(CallableInfo): + """ + Class giving access to various properties of a record constructor. + """ + def __init__(self, cls: type): + super().__init__(ClassMember('recordConstructor', cls.__name__)) + self.__cls = cls + def __repr__(self): + return f'RecordConstructorInfo({self.name})' + @property + def name(self): + return self.__cls.__name__ + def getResultTypeLocation(self) -> Optional[Loc]: + return None + def getParamSourceLocation(self, paramName: str) -> Optional[Loc]: + file = classFilename(self.__cls) + if not file: + return None + ast = parsecache.getAST(file) + node = ast.getRecordAttr(self.name, paramName) + if node: + return Loc(file, node.lineno, node.col_offset, node.end_lineno, node.end_col_offset) + else: + return None + +def locationOfArgument(fi: inspect.FrameInfo, idxOrName: int | str) -> Optional[Loc]: + """ + Given a stack frame with a function call f(arg1, arg2, ..., argN), returns + the source code location of the i-th argument. + """ + loc = Loc.fromFrameInfo(fi) + match loc.fullSpan(): + case (startLine, startCol, _endLine, _endCol): + codeOfCall = loc.code() + if codeOfCall is None: + return loc + try: + tree = ast.parse(codeOfCall) + except SyntaxError: + return loc + match tree: + case ast.Module([ast.Expr(ast.Call(_fun, args, kwArgs))]): + arg = None + if isinstance(idxOrName, int): + idx = idxOrName + if idx >= 0 and idx < len(args): + arg = args[idx] + else: + matching = [k.value for k in kwArgs if k.arg == idxOrName] + if matching: + arg = matching[0] + if arg is not None: + if arg.end_lineno is not None and arg.end_col_offset is not None: + callStartLine = startLine + arg.lineno - 1 + callStartCol = startCol + arg.col_offset + callEndLine = startLine + arg.end_lineno - 1 + if arg.lineno != arg.end_lineno: + callEndCol = arg.end_col_offset + else: + callEndCol = startCol + arg.end_col_offset + return Loc(loc.filename, callStartLine, callStartCol, + callEndLine, callEndCol) + return loc diff --git a/python/code/wypp/myLogging.py b/python/code/wypp/myLogging.py new file mode 100644 index 00000000..88f20469 --- /dev/null +++ b/python/code/wypp/myLogging.py @@ -0,0 +1,31 @@ +import utils +import sys + +VERBOSE = False # set via commandline +DEBUG = utils.getEnv("WYPP_DEBUG", bool, False) + +def isDebug() -> bool: + return DEBUG + +def enableVerbose(): + global VERBOSE + VERBOSE = True + +def enableDebug(): + global VERBOSE, DEBUG + VERBOSE = True + DEBUG = True + +def printStderr(s=''): + sys.stderr.write(s + '\n') + +def verbose(s): + if VERBOSE or DEBUG: + printStderr('[V] ' + str(s)) + +def debug(s): + if DEBUG: + printStderr('[D] ' + str(s)) + +def warn(s: str): + printStderr('WARN: ' + str(s)) diff --git a/python/code/wypp/myTypeguard.py b/python/code/wypp/myTypeguard.py new file mode 100644 index 00000000..b3854ee3 --- /dev/null +++ b/python/code/wypp/myTypeguard.py @@ -0,0 +1,45 @@ +# Wrapper module for typeguard. Do not import typeguard directly but always via myTypeguard +from __future__ import annotations +from typing import * +from dataclasses import dataclass +from myLogging import * + +# We externally adjust the PYTHONPATH so that the typeguard module can be resolved +import typeguard # type: ignore + +@dataclass(frozen=True) +class Namespaces: + globals: dict + locals: dict + @staticmethod + def empty() -> Namespaces: + return Namespaces({}, {}) + +@dataclass(frozen=True) +class MatchesTyFailure: + # This is potentially a failure because of an invalid type + exception: Exception + ty: Any + +type MatchesTyResult = bool | MatchesTyFailure + +def matchesTy(a: Any, ty: Any, ns: Namespaces) -> MatchesTyResult: + try: + typeguard.check_type(a, + ty, + collection_check_strategy=typeguard.CollectionCheckStrategy.ALL_ITEMS, + ns=(ns.globals, ns.locals)) + return True + except typeguard.TypeCheckError as e: + return False + except Exception as e: + debug(f'Exception when checking type, ns={ns}: {e}') + return MatchesTyFailure(e, ty) + +def getTypeName(t: Any) -> str: + res: str = typeguard._utils.get_type_name(t, ['__wypp__']) + wyppPrefixes = ['wypp.writeYourProgram.'] + for p in wyppPrefixes: + if res.startswith(p): + return 'wypp.' + res[len(p):] + return res diff --git a/python/code/wypp/parsecache.py b/python/code/wypp/parsecache.py new file mode 100644 index 00000000..94810c6e --- /dev/null +++ b/python/code/wypp/parsecache.py @@ -0,0 +1,147 @@ +import ast +import os +import linecache +from dataclasses import dataclass +from typing import * + +def _firstLineOfFun(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int: + allLinenos = [node.lineno] + for e in node.decorator_list: + allLinenos.append(e.lineno) + return min(allLinenos) + +@dataclass(frozen=True) +class FunMatcher: + name: str + firstlineno: Optional[int] = None + + def matches(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + if node.name == self.name: + if self.firstlineno is not None: + return self.firstlineno == _firstLineOfFun(node) + else: + return True + else: + return False + +class AST: + + def __init__(self, filename: str): + """ + Do not instantiate, use getAST + """ + self.__filename = filename + self.__tree = None + self.__treeResolved = False + self.__cache = {} + + def __ensureTree(self): + if self.__treeResolved: + return + lines = linecache.getlines(self.__filename) + try: + tree = ast.parse(''.join(lines)) + except Exception: + tree = None + self.__tree = tree + self.__treeResolved = True + + def _collectFuns(self, nodes: list[ast.stmt], m: FunMatcher, recursive: bool, + results: list[ast.FunctionDef | ast.AsyncFunctionDef]): + for node in nodes: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + if m.matches(node): + results.append(node) + if recursive: + self._collectFuns(node.body, m, True, results) + + def _findFun(self, nodes: list[ast.stmt], m: FunMatcher, recursive: bool) -> ast.FunctionDef | ast.AsyncFunctionDef | None: + """ + Finds the function from the list of statements that matches m. None is returned + if no such function is found or if multiple matches are found. + """ + results = [] + self._collectFuns(nodes, m, recursive, results) + if len(results) == 1: + return results[0] + else: + return None + + def getFunDef(self, m: FunMatcher) -> ast.FunctionDef | ast.AsyncFunctionDef | None: + if m in self.__cache: + return self.__cache[m] + self.__ensureTree() + if self.__tree: + resultNode = self._findFun(self.__tree.body, m, True) + self.__cache[m] = resultNode + return resultNode + + def _findClass(self, clsName: str) -> ast.ClassDef | None: + """ + Finds the toplevel class with the given name. Returns None if no such class or + multiple classes are found. + """ + targetClss = [] + if self.__tree: + for node in self.__tree.body: + if isinstance(node, ast.ClassDef) and node.name == clsName: + targetClss.append(node) + if len(targetClss) == 1: + return targetClss[0] + else: + return None + + def getRecordAttr(self, clsName: str, attrName: str) -> ast.AnnAssign | None: + """ + Finds the attribute of the format `A: T` or `A: T = ...` with name attrName + in a toplevel class with name clsName. Returns None if no such class/attribute + comnbination or mutiples are found. + """ + key = (clsName, attrName) + if key in self.__cache: + return self.__cache[key] + self.__ensureTree() + # Step 1: find the class + targetCls = self._findClass(clsName) + # Step 2: look for an AnnAssign like: A: T = ... + annNodes = [] + if targetCls: + for stmt in targetCls.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + if stmt.target.id == attrName: + annNodes.append(stmt) + annNode = None + if len(annNodes) == 1: + annNode = annNodes[0] + self.__cache[key] = annNode + return annNode + + def getMethodDef(self, clsName: str, m: FunMatcher) -> ast.FunctionDef | ast.AsyncFunctionDef | None: + """ + Finds the method in class clsName that matches m. None is returned if no such method + or multiple methods are found. + """ + key = (clsName, m) + if key in self.__cache: + return self.__cache[key] + self.__ensureTree() + # Step 1: find the class + targetCls = self._findClass(clsName) + # Step 2: look for the method + funNode = None + if targetCls: + funNode = self._findFun(targetCls.body, m, False) + self.__cache[key] = funNode + # print(f'clsName={clsName}, m={m}, funNode={funNode}') + return funNode + +_cache: dict[str, AST] = {} + +def getAST(filename: str) -> AST: + filename = os.path.normpath(filename) + if filename not in _cache: + a = AST(filename) + _cache[filename] = a + return a + else: + return _cache[filename] diff --git a/python/code/wypp/paths.py b/python/code/wypp/paths.py new file mode 100644 index 00000000..b8dc9161 --- /dev/null +++ b/python/code/wypp/paths.py @@ -0,0 +1,27 @@ +import os +from typing import * +from contextlib import contextmanager + +_projectDir: Optional[str] = None + +@contextmanager +def projectDir(d: str): + """Context manager to temporarily set the _relDir global variable.""" + global _projectDir + old = _projectDir + _projectDir = _normPath(d) + if _projectDir and not _projectDir[-1] == '/': + _projectDir = _projectDir + '/' + try: + yield + finally: + _projectDir = old + +def _normPath(s: str) -> str: + return os.path.normpath(os.path.abspath(s)) + +def canonicalizePath(s: str) -> str: + s = _normPath(s) + if _projectDir and s.startswith(_projectDir): + s = s[len(_projectDir):] + return s diff --git a/python/code/wypp/records.py b/python/code/wypp/records.py new file mode 100644 index 00000000..21eb3122 --- /dev/null +++ b/python/code/wypp/records.py @@ -0,0 +1,98 @@ +import typing +import dataclasses +import utils +import sys +import myTypeguard +import errors +import typecheck +import location +import stacktrace +from utils import _call_with_frames_removed + +EQ_ATTRS_ATTR = '__eqAttrs__' + +_typeCheckingEnabled = False + +def init(enableTypeChecking=True): + global _typeCheckingEnabled + _typeCheckingEnabled = enableTypeChecking + +def _collectDataClassAttributes(cls): + result = dict() + for c in cls.mro(): + if hasattr(c, '__kind') and c.__kind == 'record' and hasattr(c, '__annotations__'): + result = c.__annotations__ | result + return result + +def _checkRecordAttr(cls: typing.Any, + ns: myTypeguard.Namespaces, + name: str, + ty: typing.Any, + tyLoc: typing.Optional[location.Loc], + v: typing.Any): + if not typecheck.handleMatchesTyResult(myTypeguard.matchesTy(v, ty, ns), tyLoc): + fi = stacktrace.callerOutsideWypp() + if fi: + loc = location.Loc.fromFrameInfo(fi) + else: + loc = None + raise errors.WyppTypeError.recordAssignError(cls.__name__, + name, + ty, + tyLoc, + v, + loc) + return v + +def _patchDataClass(cls, mutable: bool, ns: myTypeguard.Namespaces): + fieldNames = [f.name for f in dataclasses.fields(cls)] + setattr(cls, EQ_ATTRS_ATTR, fieldNames) + + if hasattr(cls, '__annotations__'): + # add annotions for type checked constructor. + cls.__kind = 'record' + cls.__init__.__annotations__ = _collectDataClassAttributes(cls) + cls.__init__ = typecheck.wrapTypecheckRecordConstructor(cls, ns) + + if mutable: + # prevent new fields being added + fields = set(fieldNames) + info = location.RecordConstructorInfo(cls) + locs = {} + for name in fields: + if not name in cls.__annotations__: + raise errors.WyppTypeError.noTypeAnnotationForRecordAttribute(name, cls.__name__) + else: + locs[name] = info.getParamSourceLocation(name) + types = None # lazily initialized because forward references are not available at this point + oldSetattr = cls.__setattr__ + def _setattr(obj, name, v): + nonlocal types + if types is None: + types = typing.get_type_hints(cls, globalns=ns.globals, localns=ns.locals, + include_extras=True) + if name in types: + ty = types[name] + tyLoc = locs[name] + v = _checkRecordAttr(cls, ns, name, ty, tyLoc, v) + oldSetattr(obj, name, v) + else: + raise errors.WyppAttributeError(f'Unknown attribute {name} for record {cls.__name__}') + setattr(cls, "__setattr__", lambda obj, k, v: _call_with_frames_removed(_setattr, obj, k, v)) + return cls + +def record(cls=None, mutable=False, globals={}, locals={}): + ns = myTypeguard.Namespaces(globals, locals) + def wrap(cls: type): + newCls = dataclasses.dataclass(cls, frozen=not mutable) + if _typeCheckingEnabled: + return utils._call_with_frames_removed(_patchDataClass, newCls, mutable, ns) + else: + return newCls + # See if we're being called as @record or @record(). + if cls is None: + # We're called with parens. + return wrap + else: + # We're called as @dataclass without parens. + return _call_with_frames_removed(wrap, cls) diff --git a/python/code/wypp/renderTy.py b/python/code/wypp/renderTy.py new file mode 100644 index 00000000..0b337444 --- /dev/null +++ b/python/code/wypp/renderTy.py @@ -0,0 +1,85 @@ +import collections.abc +import types +from typing import * +import myTypeguard +#def renderTy(t: Any) -> str: +# if isinstance(t, str): +# return t +# return str(t) +# # return typeguard._utils.get_type_name(t, ['__wypp__']) + +_NoneType = type(None) + +def renderTy(tp: Any) -> str: + # Does not work: return typeguard._utils.get_type_name(t, ['__wypp__']) + # For example, Callable[[int, bool], str] is formated as "Callable[list, str]" + + if isinstance(tp, str): + # forward reference + return tp + + origin = get_origin(tp) + args = get_args(tp) + + # Simple / builtin / classes + if origin is None: + # e.g. int, str, custom classes, Any, typing.NoReturn, typing.Never + if tp is Any: return "Any" + if tp is _NoneType: return "None" + if tp is types.EllipsisType: return "..." + if isinstance(tp, list): return str(tp) + if isinstance(tp, tuple): return str(tp) + if isinstance(tp, dict): return str(tp) + return myTypeguard.getTypeName(tp) + + # Union / Optional (PEP 604) + if origin is Union or origin is types.UnionType: + if len(args) == 2: + if args[0] is _NoneType and args[1] is not _NoneType: + return f"Optional[{renderTy(args[1])}]" + elif args[1] is _NoneType and args[0] is not _NoneType: + return f"Optional[{renderTy(args[0])}]" + parts = [renderTy(a) for a in args] + return "Union[" + ", ".join(parts) + "]" + + # Annotated[T, ...] + if origin is Annotated: + base, *meta = args + if len(meta) >= 1 and isinstance(meta[-1], str): + return meta[-1] + metas = ", ".join(repr(m) for m in meta) + return f"Annotated[{renderTy(base)}, {metas}]" + + # Literal[...] + if origin is Literal: + return "Literal[" + ", ".join(repr(a) for a in args) + "]" + + # Callable[[A, B], R] or Callable[..., R] + if origin in (Callable, collections.abc.Callable): + params, ret = args + if params is Ellipsis: + params_s = "..." + else: + params_s = ", ".join(renderTy(p) for p in params) + return f"Callable[[{params_s}], {renderTy(ret)}]" + + # Tuple[T1, T2] or Tuple[T, ...] + if origin is tuple: + if len(args) == 2 and args[1] is Ellipsis: + return f"tuple[{renderTy(args[0])}, ...]" + elif len(args) == 0: + return "tuple[()]" + return "tuple[" + ", ".join(renderTy(a) for a in args) + "]" + + # ClassVar[T], Final[T] + if origin is ClassVar: + return f"ClassVar[{renderTy(args[0])}]" + if origin is Final: + return f"Final[{renderTy(args[0])}]" + + # Parametrized generics like list[T], dict[K, V], set[T], type[T], etc. + name = getattr(origin, "__name__", None) or getattr(origin, "__qualname__", repr(origin)) + if args: + return f"{name}[" + ", ".join(renderTy(a) for a in args) + "]" + else: + return name diff --git a/python/src/replTester.py b/python/code/wypp/replTester.py similarity index 88% rename from python/src/replTester.py rename to python/code/wypp/replTester.py index 426a2e2c..2c5f1bdd 100644 --- a/python/src/replTester.py +++ b/python/code/wypp/replTester.py @@ -1,9 +1,13 @@ import sys +import constants +sys.path.insert(0, constants.CODE_DIR) + import doctest import os import argparse from dataclasses import dataclass -from runner import runCode, importUntypy, verbose, enableVerbose +from myLogging import * +import runCode usage = """python3 replTester.py [ ARGUMENTS ] LIB_1 ... LIB_n --repl SAMPLE_1 ... SAMPLE_m @@ -56,8 +60,6 @@ def parseCmdlineArgs(): libDir = os.path.dirname(__file__) libFile = os.path.join(libDir, 'writeYourProgram.py') defs = globals() -importUntypy() -# runCode(libFile, defs, []) for lib in opts.libs: d = os.path.dirname(lib) @@ -66,7 +68,7 @@ def parseCmdlineArgs(): for lib in opts.libs: verbose(f"Loading lib {lib}") - runCode(lib, defs, []) + defs = runCode.runCode(lib, defs) totalFailures = 0 totalTests = 0 @@ -79,9 +81,9 @@ def parseCmdlineArgs(): # We use our own DocTestParser to replace exception names in stacktraces class MyDocTestParser(doctest.DocTestParser): def get_examples(self, string, name=''): - prefs = {'WyppTypeError: ': 'untypy.error.UntypyTypeError: ', - 'WyppNameError: ': 'untypy.error.UntypyNameError: ', - 'WyppAttributeError: ': 'untypy.error.UntypyAttributeError: '} + prefs = {'WyppTypeError: ': 'errors.WyppTypeError: ', + 'WyppNameError: ': 'errors.WyppNameError: ', + 'WyppAttributeError: ': 'errors.WyppAttributeError: '} lines = [] for l in string.split('\n'): for pref,repl in prefs.items(): @@ -111,7 +113,7 @@ def get_examples(self, string, name=''): print('ERROR: No tests found at all!') sys.exit(1) else: - print(f'All {totalTests} tests succeded. Great!') + print(f'All {totalTests} tests succeeded. Great!') else: print(f'ERROR: {failures} out of {tests} failed') sys.exit(1) diff --git a/python/code/wypp/runCode.py b/python/code/wypp/runCode.py new file mode 100644 index 00000000..e8014e43 --- /dev/null +++ b/python/code/wypp/runCode.py @@ -0,0 +1,111 @@ +import sys +import os +import importlib +import runpy +from dataclasses import dataclass + +# local imports +from constants import * +import stacktrace +import instrument +from myLogging import * +from exceptionHandler import handleCurrentException +import utils + +class Lib: + def __init__(self, mod, properlyImported): + self.properlyImported = properlyImported + if not properlyImported: + self.initModule = mod['initModule'] + self.resetTestCount = mod['resetTestCount'] + self.printTestResults = mod['printTestResults'] + self.dict = mod + else: + self.initModule = mod.initModule + self.resetTestCount = mod.resetTestCount + self.printTestResults = mod.printTestResults + d = {} + self.dict = d + for name in dir(mod): + if name and name[0] != '_': + d[name] = getattr(mod, name) + + +@dataclass +class RunSetup: + def __init__(self, pathDir: str, args: list[str]): + self.pathDir = pathDir + self.args = args + self.sysPathInserted = False + self.oldArgs = sys.argv + def __enter__(self): + if self.pathDir not in sys.path: + sys.path.insert(0, self.pathDir) + self.sysPathInserted = True + sys.argv = self.args + self.originalProfile = sys.getprofile() + stacktrace.installProfileHook() + def __exit__(self, exc_type, value, traceback): + sys.setprofile(self.originalProfile) + if self.sysPathInserted: + sys.path.remove(self.pathDir) + self.sysPathInserted = False + sys.argv = self.oldArgs + +def prepareLib(onlyCheckRunnable, enableTypeChecking): + libDefs = None + mod = 'wypp' + verbose('Attempting to import ' + mod) + wypp = importlib.import_module(mod) + libDefs = Lib(wypp, True) + verbose(f'Successfully imported module {mod} from file {wypp.__file__}') + libDefs.initModule(enableChecks=not onlyCheckRunnable, + enableTypeChecking=enableTypeChecking, + quiet=onlyCheckRunnable) + return libDefs + +def runCode(fileToRun, globals, doTypecheck=True, extraDirs=None) -> dict: + if not extraDirs: + extraDirs = [] + with instrument.setupFinder(os.path.dirname(fileToRun), extraDirs, doTypecheck): + modName = os.path.basename(os.path.splitext(fileToRun)[0]) + sys.dont_write_bytecode = True + res = runpy.run_module(modName, init_globals=globals, run_name='__wypp__', alter_sys=False) + return res + +def runStudentCode(fileToRun, globals, onlyCheckRunnable, doTypecheck=True, extraDirs=None) -> dict: + doRun = lambda: runCode(fileToRun, globals, doTypecheck=doTypecheck, extraDirs=extraDirs) + if onlyCheckRunnable: + try: + doRun() + except: + printStderr('Loading file %s crashed' % fileToRun) + handleCurrentException() + else: + utils.die(0) + return doRun() + +# globals already contain libDefs +def runTestsInFile(testFile, globals, libDefs, doTypecheck=True, extraDirs=[]): + printStderr() + printStderr(f"Running tutor's tests in {testFile}") + libDefs.resetTestCount() + try: + runCode(testFile, globals, doTypecheck=doTypecheck, extraDirs=extraDirs) + except: + handleCurrentException() + return libDefs.dict['printTestResults']('Tutor: ') + +# globals already contain libDefs +def performChecks(check, testFile, globals, libDefs, doTypecheck=True, extraDirs=None, loadingFailed=False): + prefix = '' + if check and testFile: + prefix = 'Student: ' + testResultsStudent = libDefs.printTestResults(prefix, loadingFailed) + if check: + testResultsInstr = {'total': 0, 'failing': 0} + if testFile: + testResultsInstr = runTestsInFile(testFile, globals, libDefs, doTypecheck=doTypecheck, + extraDirs=extraDirs) + failingSum = testResultsStudent['failing'] + testResultsInstr['failing'] + utils.die(0 if failingSum < 1 else 1) diff --git a/python/src/runYourProgram.py b/python/code/wypp/runYourProgram.py similarity index 80% rename from python/src/runYourProgram.py rename to python/code/wypp/runYourProgram.py index cd2dd616..672b88ba 100644 --- a/python/src/runYourProgram.py +++ b/python/code/wypp/runYourProgram.py @@ -1,7 +1,7 @@ # coding=utf-8 +# This is the entry point into running a python file via wypp. # NOTE: this file must have valid python 2 syntax. We want to display an error message # when running with python 2. - import sys pythonVersion = sys.version.split()[0] if not pythonVersion.startswith('3.'): @@ -13,5 +13,5 @@ sys.exit(1) if __name__ == '__main__': - import runner - runner.main(globals()) + import runner as r + r.main(globals()) diff --git a/python/code/wypp/runner.py b/python/code/wypp/runner.py new file mode 100644 index 00000000..9dd06ada --- /dev/null +++ b/python/code/wypp/runner.py @@ -0,0 +1,105 @@ +import sys +import constants +sys.path.insert(0, constants.CODE_DIR) + +import sys +import os + +# local imports +from constants import * +import i18n +import paths +from myLogging import * +import version as versionMod +import interactive +import runCode +import exceptionHandler +import cmdlineArgs + +requiredVersion = (3, 12, 0) +def pythonVersionOk(v): + (reqMajor, reqMinor, reqMicro) = requiredVersion + if v.major < reqMajor or v.minor < reqMinor: + return False + if v.major == reqMajor and v.minor == reqMinor and v.micro < reqMicro: + return False + else: + return True + +def printWelcomeString(file, version, doTypecheck): + cwd = os.getcwd() + "/" + if file.startswith(cwd): + file = file[len(cwd):] + versionStr = '' if not version else 'Version %s, ' % version + pythonVersion = sys.version.split()[0] + tycheck = '' + if not doTypecheck: + tycheck = ', no typechecking' + printStderr('=== WELCOME to "Write Your Python Program" ' + + '(%sPython %s, %s%s) ===' % (versionStr, pythonVersion, file, tycheck)) + +def main(globals, argList=None): + v = sys.version_info + if not pythonVersionOk(v): + vStr = sys.version.split()[0] + reqVStr = '.'.join([str(x) for x in requiredVersion]) + print(f""" +Python in version {reqVStr} or newer is required. You are still using version {vStr}, please upgrade! +""") + sys.exit(1) + + (args, restArgs) = cmdlineArgs.parseCmdlineArgs(argList) + if args.verbose: + enableVerbose() + if args.debug: + enableDebug() + + verbose(f'VERBOSE={args.verbose}, DEBUG={args.debug}') + + if args.lang: + if args.lang in i18n.allLanguages: + i18n.setLang(args.lang) + else: + printStderr(f'Unsupported language {args.lang}. Supported: ' + ', '.join(i18n.allLanguages)) + sys.exit(1) + + fileToRun: str = args.file + if args.changeDir: + os.chdir(os.path.dirname(fileToRun)) + fileToRun = os.path.basename(fileToRun) + + isInteractive = args.interactive + version = versionMod.readVersion() + if isInteractive: + interactive.prepareInteractive(reset=not args.noClear) + + if fileToRun is None: + return + + if not args.checkRunnable and (not args.quiet or args.verbose): + printWelcomeString(fileToRun, version, doTypecheck=args.checkTypes) + + libDefs = runCode.prepareLib(onlyCheckRunnable=args.checkRunnable, enableTypeChecking=args.checkTypes) + + with (runCode.RunSetup(os.path.dirname(fileToRun), [fileToRun] + restArgs), + paths.projectDir(os.path.abspath(os.getcwd()))): + globals['__name__'] = '__wypp__' + sys.modules['__wypp__'] = sys.modules['__main__'] + loadingFailed = False + try: + verbose(f'running code in {fileToRun}') + globals['__file__'] = fileToRun + globals = runCode.runStudentCode(fileToRun, globals, args.checkRunnable, + doTypecheck=args.checkTypes, extraDirs=args.extraDirs) + except Exception as e: + verbose(f'Error while running code in {fileToRun}: {e}') + exceptionHandler.handleCurrentException(exit=not isInteractive) + loadingFailed = True + + runCode.performChecks(args.check, args.testFile, globals, libDefs, doTypecheck=args.checkTypes, + extraDirs=args.extraDirs, loadingFailed=loadingFailed) + + if isInteractive: + interactive.enterInteractive(globals, args.checkTypes, loadingFailed) + + diff --git a/python/code/wypp/stacktrace.py b/python/code/wypp/stacktrace.py new file mode 100644 index 00000000..eec485c4 --- /dev/null +++ b/python/code/wypp/stacktrace.py @@ -0,0 +1,120 @@ +import types +import traceback +import utils +import inspect +from typing import Optional, Any +import os +import sys +from collections import deque + +def tbToFrameList(tb: types.TracebackType) -> list[types.FrameType]: + cur = tb + res: list[types.FrameType] = [] + while cur: + res.append(cur.tb_frame) + cur = cur.tb_next + return res + +def isCallWithFramesRemoved(frame: types.FrameType): + return frame.f_code.co_name == utils._call_with_frames_removed.__name__ + +def isCallWithNextFrameRemoved(frame: types.FrameType): + return frame.f_code.co_name == utils._call_with_next_frame_removed.__name__ + +def isWyppFrame(frame: types.FrameType): + modName = frame.f_globals.get("__name__") or '__wypp__' + fname = frame.f_code.co_filename + directDir = os.path.basename(os.path.dirname(fname)) + if '__wypp_runYourProgram' in frame.f_globals: + return True + for prefix in ['wypp', 'typeguard']: + if modName == prefix or modName.startswith(prefix + '.') or directDir == prefix: + return True + return False + +def isRunpyFrame(frame: types.FrameType) -> bool: + f = frame.f_code.co_filename + return f == '' or f.startswith(' traceback.StackSummary: + if filter: + # Step 1: remove all frames that appear after the first _call_with_frames_removed + endIdx = len(frameList) + for i in range(len(frameList)): + if isCallWithFramesRemoved(frameList[i]): + endIdx = i - 1 + break + frameList = frameList[:endIdx] + # Step 2: remove those frames directly after _call_with_next_frame_removed + toRemove = [] + for i in range(len(frameList)): + if isCallWithNextFrameRemoved(frameList[i]): + toRemove.append(i) + for i in reversed(toRemove): + frameList = frameList[:i-1] + frameList[i+1:] + # Step 3: remove leading wypp or typeguard frames + frameList = utils.dropWhile(frameList, lambda f: isWyppFrame(f) or isRunpyFrame(f)) + frameList = frameList + [f.frame for f in extraFrames] + frames = [(f, f.f_lineno) for f in frameList] + return traceback.StackSummary.extract(frames) + +def callerOutsideWypp() -> Optional[inspect.FrameInfo]: + stack = inspect.stack() + d = os.path.dirname(stack[0].filename) + for f in stack: + if not isWyppFrame(f.frame) and not d == os.path.dirname(f.filename): + return f + return None + +class ReturnTracker: + def __init__(self, entriesToKeep: int): + self.__returnFrames = deque(maxlen=entriesToKeep) # a ring buffer + def __call__(self, frame: types.FrameType, event: str, arg: Any): + # event is one of 'call', 'return', 'c_call', 'c_return', or 'c_exception' + match event: + case 'call': + pass + case 'return': + # print(f'appending {frame} to return tracker') + self.__returnFrames.append(frame) # overwrite oldest when full + case 'c_call': + pass + case 'c_return': + pass + case 'c_exception': + pass + def getReturnFrame(self, idx: int) -> Optional[inspect.FrameInfo]: + try: + f = self.__returnFrames[idx] + except IndexError: + return None + if f: + tb = inspect.getframeinfo(f, context=1) + fi = inspect.FrameInfo(f, tb.filename, tb.lineno, tb.function, tb.code_context, tb.index) + del f + return fi + else: + return None + +# when using _call_with_next_frame_removed, we have to take the second-to-last +# return. Hence, we keep the two most recent returns byn setting entriesToKeep = 2. +def installProfileHook(entriesToKeep: int=2) -> ReturnTracker: + obj = sys.getprofile() + if isinstance(obj, ReturnTracker): + return obj + obj = ReturnTracker(entriesToKeep) + sys.setprofile(obj) + return obj + +def getReturnTracker(): + obj = sys.getprofile() + if isinstance(obj, ReturnTracker): + return obj + else: + raise ValueError(f'No ReturnTracker set, must use installProfileHook before') diff --git a/python/code/wypp/typecheck.py b/python/code/wypp/typecheck.py new file mode 100644 index 00000000..bd598077 --- /dev/null +++ b/python/code/wypp/typecheck.py @@ -0,0 +1,196 @@ +from __future__ import annotations +from collections.abc import Callable +from typing import ParamSpec, TypeVar, Any, Optional, Literal +import inspect +from dataclasses import dataclass +import utils +from myTypeguard import matchesTy, MatchesTyResult, MatchesTyFailure, Namespaces +import stacktrace +import location +import errors +from myLogging import * + +def printVars(what: str, *l): + s = what + ": " + ', '.join([str(x) for x in l]) + print(s) + +def isEmptyAnnotation(t: Any) -> bool: + return t is inspect.Signature.empty or t is inspect.Parameter.empty + +def isEmptySignature(sig: inspect.Signature) -> bool: + for x in sig.parameters: + p = sig.parameters[x] + if not isEmptyAnnotation(p.annotation): + return False + return isEmptyAnnotation(sig.return_annotation) + +def handleMatchesTyResult(res: MatchesTyResult, tyLoc: Optional[location.Loc]) -> bool: + match res: + case MatchesTyFailure(exc, ty): + if isDebug(): + debug(f'Exception occurred while calling matchesTy with type {ty}, re-raising') + raise exc + else: + raise errors.WyppTypeError.invalidType(ty, tyLoc) + case b: + return b + +def getKind(cfg: CheckCfg, paramNames: list[str]) -> Literal['function', 'method', 'staticmethod']: + kind: Literal['function', 'method', 'staticmethod'] = 'function' + match cfg.kind: + case location.ClassMember('recordConstructor', _): + kind = 'method' + case location.ClassMember('method', _): + if len(paramNames) > 0 and paramNames[0] == 'self': + kind = 'method' + else: + kind = 'staticmethod' + case 'function': + kind = 'function' + return kind + +def checkSignature(sig: inspect.Signature, info: location.CallableInfo, cfg: CheckCfg) -> None: + paramNames = list(sig.parameters) + kind = getKind(cfg, paramNames) + for i in range(len(paramNames)): + name = paramNames[i] + p = sig.parameters[name] + ty = p.annotation + if isEmptyAnnotation(ty): + if i == 0 and kind == 'method': + pass + else: + locDecl = info.getParamSourceLocation(name) + raise errors.WyppTypeError.partialAnnotationError(location.CallableName.mk(info), name, locDecl) + if p.default is not inspect.Parameter.empty: + locDecl = info.getParamSourceLocation(name) + if not handleMatchesTyResult(matchesTy(p.default, ty, cfg.ns), locDecl): + raise errors.WyppTypeError.defaultError(location.CallableName.mk(info), name, locDecl, ty, p.default) + +def mandatoryArgCount(sig: inspect.Signature) -> int: + required_kinds = { + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + } + res = 0 + for p in sig.parameters.values(): + if p.kind in required_kinds and p.default is inspect._empty: + res = res + 1 + return res + +def checkArgument(p: inspect.Parameter, name: str, idx: Optional[int], a: Any, + locArg: Optional[location.Loc], info: location.CallableInfo, cfg: CheckCfg): + t = p.annotation + if not isEmptyAnnotation(t): + locDecl = info.getParamSourceLocation(name) + if not handleMatchesTyResult(matchesTy(a, t, cfg.ns), locDecl): + cn = location.CallableName.mk(info) + raise errors.WyppTypeError.argumentError(cn, + name, + idx, + locDecl, + t, + a, + locArg) +def checkArguments(sig: inspect.Signature, args: tuple, kwargs: dict, + info: location.CallableInfo, cfg: CheckCfg) -> None: + debug(f'Checking arguments when calling {info}') + paramNames = list(sig.parameters) + mandatory = mandatoryArgCount(sig) + kind = getKind(cfg, paramNames) + offset = 1 if kind == 'method' else 0 + fi = stacktrace.callerOutsideWypp() + callLoc = None if not fi else location.Loc.fromFrameInfo(fi) + cn = location.CallableName.mk(info) + def raiseArgMismatch(): + raise errors.WyppTypeError.argCountMismatch(cn, + callLoc, + len(paramNames) - offset, + mandatory - offset, + len(args) - offset) + if len(args) + len(kwargs) < mandatory: + raiseArgMismatch() + for i in range(len(args)): + if i >= len(paramNames): + raiseArgMismatch() + name = paramNames[i] + p = sig.parameters[name] + locArg = None if fi is None else location.locationOfArgument(fi, i) + checkArgument(p, name, i - offset, args[i], locArg, info, cfg) + for name in kwargs: + if name not in sig.parameters: + raise errors.WyppTypeError.unknownKeywordArgument(cn, callLoc, name) + locArg = None if fi is None else location.locationOfArgument(fi, name) + checkArgument(sig.parameters[name], name, None, kwargs[name], locArg, info, cfg) + +def checkReturn(sig: inspect.Signature, returnFrame: Optional[inspect.FrameInfo], + result: Any, info: location.CallableInfo, cfg: CheckCfg) -> None: + t = sig.return_annotation + if isEmptyAnnotation(t): + t = None + debug(f'Checking return value when calling {info}, return type: {t}') + locDecl = info.getResultTypeLocation() + if not handleMatchesTyResult(matchesTy(result, t, cfg.ns), locDecl): + fi = stacktrace.callerOutsideWypp() + if fi is not None: + locRes = location.Loc.fromFrameInfo(fi) + returnLoc = None + extraFrames = [] + if returnFrame: + returnLoc = location.Loc.fromFrameInfo(returnFrame) + extraFrames = [returnFrame] + raise errors.WyppTypeError.resultError(location.CallableName.mk(info), locDecl, t, returnLoc, result, + locRes, extraFrames) + + +@dataclass +class CheckCfg: + kind: location.CallableKind + ns: Namespaces + @staticmethod + def fromDict(d: dict) -> CheckCfg: + k = d['kind'] + match k: + case 'function': + kind = 'function' + case 'method': + kind = location.ClassMember('method', d['className']) + return CheckCfg(kind=kind, ns=Namespaces(d['globals'], d['locals'])) + +P = ParamSpec("P") +T = TypeVar("T") + +def wrapTypecheck(cfg: dict | CheckCfg, outerInfo: Optional[location.CallableInfo]=None) -> Callable[[Callable[P, T]], Callable[P, T]]: + if isinstance(cfg, CheckCfg): + checkCfg = cfg + else: + checkCfg = CheckCfg.fromDict(cfg) + def _wrap(f: Callable[P, T]) -> Callable[P, T]: + sig = inspect.signature(f) + if isEmptySignature(sig): + return f + if outerInfo is None: + # we have a regular function or method + info = location.StdCallableInfo(f, checkCfg.kind) + else: + # special case: constructor of a record + info = outerInfo + utils._call_with_frames_removed(checkSignature, sig, info, checkCfg) + def wrapped(*args, **kwargs) -> T: + utils._call_with_frames_removed(checkArguments, sig, args, kwargs, info, checkCfg) + returnTracker = stacktrace.getReturnTracker() + result = utils._call_with_next_frame_removed(f, *args, **kwargs) + retFrame = returnTracker.getReturnFrame(0) + utils._call_with_frames_removed( + checkReturn, sig, retFrame, result, info, checkCfg + ) + return result + return wrapped + return _wrap + +def wrapTypecheckRecordConstructor(cls: type, ns: Namespaces) -> Callable: + checkCfg = CheckCfg.fromDict({'kind': 'method', 'className': cls.__name__, + 'globals': ns.globals, 'locals': ns.locals}) + info = location.RecordConstructorInfo(cls) + return wrapTypecheck(checkCfg, info)(cls.__init__) diff --git a/python/code/wypp/utils.py b/python/code/wypp/utils.py new file mode 100644 index 00000000..736cbab9 --- /dev/null +++ b/python/code/wypp/utils.py @@ -0,0 +1,102 @@ +from typing import * +import os +import sys +from contextlib import contextmanager + +P = ParamSpec("P") +T = TypeVar("T") + +# The name of this function is magical. All stack frames that appear +# nested within this function are removed from tracebacks. +def _call_with_frames_removed( + f: Callable[P, T], *args: P.args, **kwargs: P.kwargs +) -> T: + return f(*args, **kwargs) + +# The name of this function is magical. The next stack frame that appears +# nested within this function is removed from tracebacks. +def _call_with_next_frame_removed( + f: Callable[P, T], *args: P.args, **kwargs: P.kwargs +) -> T: + return f(*args, **kwargs) + +def getEnv(name, conv, default): + s = os.getenv(name) + if s is None: + return default + try: + return conv(s) + except: + return default + +def dropWhile(l: list, f: Callable[[Any], bool]) -> list: + if not l: + return l + for i in range(len(l)): + if not f(l[i]): + break + else: + # All elements satisfied the condition, return empty list + return [] + return l[i:] + +def split(l: list, f: Callable[[Any], bool]) -> tuple[list, list]: + """ + span, applied to a list l and a predicate f, returns a tuple where first element is the + longest prefix (possibly empty) of l of elements that satisfy f and second element + is the remainder of the list. + """ + inFirst = True + first = [] + second = [] + for x in l: + if inFirst: + if f(x): + first.append(x) + else: + inFirst = False + second.append(x) + else: + second.append(x) + return (first, second) + + +def isUnderTest() -> bool: + x = os.getenv('WYPP_UNDER_TEST') + return x == 'True' + +@contextmanager +def underTest(value: bool = True): + """Context manager to temporarily set WYPP_UNDER_TEST environment variable.""" + oldValue = os.getenv('WYPP_UNDER_TEST') + os.environ['WYPP_UNDER_TEST'] = str(value) + try: + yield + finally: + if oldValue is None: + # Remove the environment variable if it didn't exist before + os.environ.pop('WYPP_UNDER_TEST', None) + else: + # Restore the original value + os.environ['WYPP_UNDER_TEST'] = oldValue + + +def die(ecode: str | int | None = 1): + if isinstance(ecode, str): + sys.stderr.write(ecode) + sys.stderr.write('\n') + ecode = 1 + elif ecode == None: + ecode = 0 + if sys.flags.interactive: + os._exit(ecode) + else: + sys.exit(ecode) + +def readFile(path): + try: + with open(path, encoding='utf-8') as f: + return f.read() + except UnicodeDecodeError: + with open(path) as f: + return f.read() diff --git a/python/code/wypp/version.py b/python/code/wypp/version.py new file mode 100644 index 00000000..c1c8ecef --- /dev/null +++ b/python/code/wypp/version.py @@ -0,0 +1,40 @@ +import os +import json +import subprocess +from dataclasses import dataclass + +# local imports +from constants import * +from myLogging import * +import utils + +def readGitVersion(): + thisDir = os.path.basename(SOURCE_DIR) + baseDir = os.path.join(SOURCE_DIR, '..', '..') + if thisDir == 'src' and os.path.isdir(os.path.join(baseDir, '.git')): + try: + h = subprocess.check_output(['git', '-C', baseDir, 'rev-parse', '--short', 'HEAD'], + encoding='UTF-8').strip() + changes = subprocess.check_output( + ['git', '-C', baseDir, 'status', '--porcelain', '--untracked-files=no'], + encoding='UTF-8').strip() + if changes: + return f'git-{h}-dirty' + else: + return f'git-{h}' + except subprocess.CalledProcessError: + return 'git-?' + else: + return None + +def readVersion(): + version = readGitVersion() + if version is not None: + return version + try: + content = utils.readFile(os.path.join(SOURCE_DIR, '..', '..', 'package.json')) + d = json.loads(content) + version = d['version'] + except: + pass + return version diff --git a/python/src/writeYourProgram.py b/python/code/wypp/writeYourProgram.py similarity index 72% rename from python/src/writeYourProgram.py rename to python/code/wypp/writeYourProgram.py index ce22370f..55902409 100644 --- a/python/src/writeYourProgram.py +++ b/python/code/wypp/writeYourProgram.py @@ -1,9 +1,14 @@ -# FIXME: make exceptions nicer -import untypy import typing import dataclasses import inspect -import types +import errors +import typecheck +import records +import stacktrace +import renderTy +import location +import paths +import utils _DEBUG = False def _debug(s): @@ -27,8 +32,7 @@ def _debug(s): Callable = typing.Callable dataclass = dataclasses.dataclass - -unchecked = untypy.unchecked +record = records.record intPositive = typing.Annotated[int, lambda i: i > 0, 'intPositive'] nat = typing.Annotated[int, lambda i: i >= 0, 'nat'] @@ -44,10 +48,8 @@ def _debug(s): class Lock(Protocol): def acquire(self, blocking: bool = True, timeout:int = -1) -> Any: pass - def release(self) -> Any: pass - def locked(self) -> Any: pass @@ -57,6 +59,8 @@ def locked(self) -> Any: U = typing.TypeVar('U') V = typing.TypeVar('V') +# Dirty hack ahead: we patch some methods of internal class of the typing module. + def _literalInstanceOf(self, value): for p in self.__args__: # typing.Literal checks for type(value) == type(arg), @@ -65,6 +69,11 @@ def _literalInstanceOf(self, value): return True return False +# This patch is needed to be able to use a literal L to check whether a value x +# is an instance of this literal: isinstance(x, L) +# pyright does not know about typing._LiteralGenericAlias, we do not typecheck the following line. +setattr(typing._LiteralGenericAlias, '__instancecheck__', _literalInstanceOf) # type: ignore + def _invalidCall(self, *args, **kwds): if hasattr(self, '__name__'): name = self.__name__ @@ -77,81 +86,35 @@ def formatArg(x): if name == 'Literal': return repr(x) else: - return x - argStr = ', '.join([formatArg(untypy.util.typehints.qualname(x)) for x in args]) - raise untypy.error.WyppTypeError(f"Cannot call {name} like a function. Did you mean {name}[{argStr}]?") - -# Dirty hack ahead: we patch some methods of internal class of the typing module. - -# This patch is needed to be able to use a literal L to check whether a value x -# is an instance of this literal: instanceof(x, L) -setattr(typing._LiteralGenericAlias, '__instancecheck__', _literalInstanceOf) + return renderTy.renderTy(x) + caller = stacktrace.callerOutsideWypp() + loc = None if caller is None else location.Loc.fromFrameInfo(caller) + argStr = ', '.join([formatArg(x) for x in args]) + tyStr = f'{name}({argStr})' + raise utils._call_with_frames_removed(errors.WyppTypeError.invalidType, tyStr, loc) # This patch is needed to provide better error messages if a student passes type arguments # with paranthesis instead of square brackets setattr(typing._SpecialForm, '__call__', _invalidCall) -def _collectDataClassAttributes(cls): - result = dict() - for c in cls.mro(): - if hasattr(c, '__kind') and c.__kind == 'record' and hasattr(c, '__annotations__'): - result = c.__annotations__ | result - return result - - -def _patchDataClass(cls, mutable): - fieldNames = [f.name for f in dataclasses.fields(cls)] - setattr(cls, EQ_ATTRS_ATTR, fieldNames) - - if hasattr(cls, '__annotations__'): - # add annotions for type checked constructor. - cls.__kind = 'record' - cls.__init__.__annotations__ = _collectDataClassAttributes(cls) - cls.__init__.__original = cls # mark class as source of annotation - cls.__init__ = untypy.typechecked(cls.__init__) - - if mutable: - # prevent new fields being added - fields = set(fieldNames) - - checker = {} - # Note: Partial annotations are disallowed by untypy.typechecked(cls.__init__) - # So no handling in this code is required. - for name in fields: - if name in cls.__annotations__: - # This the type is wrapped in an lambda expression to allow for Forward Ref. - # Would the lambda expression be called at this moment, it may cause an name error - # untypy.checker fetches the annotation lazily. - checker[name] = untypy.checker(\ - lambda cls=cls,name=name: typing.get_type_hints(cls, include_extras=True)[name], - cls) - - oldSetattr = cls.__setattr__ - def _setattr(obj, k, v): - # Note __setattr__ also gets called in the constructor. - if k in checker: - oldSetattr(obj, k, checker[k](v)) - elif k in fields: - oldSetattr(obj, k, v) +def typechecked(func=None): + def wrap(func): + if hasattr(func, '__qualname__'): + q = getattr(func, "__qualname__").split('.') + if len(q) >= 2: + className = q[-2] + cfg = {'kind': 'method', 'className': className} else: - raise untypy.error.WyppAttributeError(f'Unknown attribute {k} for record {cls.__name__}') - setattr(cls, "__setattr__", _setattr) - return cls - -def record(cls=None, mutable=False): - def wrap(cls): - newCls = dataclasses.dataclass(cls, frozen=not mutable) - if _typeCheckingEnabled: - return _patchDataClass(newCls, mutable) + cfg = {'kind': 'function'} else: - return newCls - # See if we're being called as @record or @record(). - if cls is None: + cfg = {'kind': 'function'} + return typecheck.wrapTypecheck(cfg)(func) + if func is None: # We're called with parens. return wrap else: # We're called as @dataclass without parens. - return wrap(cls) + return wrap(func) # Tests @@ -172,6 +135,7 @@ def initModule(enableChecks=True, enableTypeChecking=True, quiet=False): global _checksEnabled, _typeCheckingEnabled _checksEnabled = enableChecks _typeCheckingEnabled = enableTypeChecking + records.init(enableTypeChecking) resetTestCount() def resetTestCount(): @@ -232,7 +196,8 @@ def checkGeneric(actual, expected, *, structuralObjEq=True, floatEqWithDelta=Tru stack = inspect.stack() frame = stack[2] if len(stack) > 2 else None if frame: - caller = f"{frame.filename}:{frame.lineno}: " + filename = paths.canonicalizePath(frame.filename) + caller = f"Datei {filename}, Zeile {frame.lineno}: " else: caller = "" def fmt(x): @@ -259,8 +224,11 @@ def checkFail(msg: str): def uncoveredCase(): stack = inspect.stack() - caller = stack[1] if len(stack) > 1 else None - raise Exception(f"{caller.filename}, Zeile {caller.lineno}: ein Fall ist nicht abgedeckt") + if len(stack) > 1: + caller = stack[1] + raise Exception(f"{caller.filename}, Zeile {caller.lineno}: ein Fall ist nicht abgedeckt") + else: + raise Exception(f"Ein Fall ist nicht abgedeckt") # # Deep equality @@ -292,9 +260,9 @@ def _objToDict(o): d = {} attrs = dir(o) useAll = False - if hasattr(o, EQ_ATTRS_ATTR): + if hasattr(o, records.EQ_ATTRS_ATTR): useAll = True - attrs = getattr(o, EQ_ATTRS_ATTR) + attrs = getattr(o, records.EQ_ATTRS_ATTR) for n in attrs: if not useAll and n and n[0] == '_': continue @@ -307,7 +275,6 @@ def _objEq(o1, o2, flags): return _dictEq(_objToDict(o1), _objToDict(o2), flags) _EPSILON = 0.00001 -EQ_ATTRS_ATTR = '__eqAttrs__' def _useFloatEqWithDelta(flags): return flags.get('floatEqWithDelta', False) @@ -354,21 +321,15 @@ def deepEq(v1, v2, **flags): return False # v1 == v2 already checked return False -class TodoError(Exception, untypy.error.DeliberateError): - pass - def todo(msg=None): if msg is None: msg = 'TODO' - raise TodoError(msg) - -class ImpossibleError(Exception, untypy.error.DeliberateError): - pass + raise errors.TodoError(msg) def impossible(msg=None): if msg is None: - msg = 'the impossible happened' - raise ImpossibleError(msg) + msg = 'Das Unmögliche ist passiert!' + raise errors.ImpossibleError(msg) # Additional functions and aliases @@ -376,4 +337,4 @@ def impossible(msg=None): math = moduleMath - +wrapTypecheck = typecheck.wrapTypecheck diff --git a/python/debug.py b/python/debug.py index 5ffdb0cb..e2660da4 100644 --- a/python/debug.py +++ b/python/debug.py @@ -1,4 +1,4 @@ -# Run this file with the debugger to debug wypp and untypy. +# Run this file with the debugger to debug wypp. # Before running, set FILE to the input file that you want # to use for debugging. @@ -11,7 +11,7 @@ file = os.path.join(thisDir, FILE) args = [file] -sys.path.insert(0, os.path.join(thisDir, 'site-lib')) +sys.path.insert(0, os.path.join(thisDir, 'code')) -from wypp import runner +from wypp import runner # type: ignore runner.main(globals(), args) diff --git a/python/deps/untypy/.gitignore b/python/deps/untypy/.gitignore deleted file mode 100644 index c18dd8d8..00000000 --- a/python/deps/untypy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ diff --git a/python/deps/untypy/LICENSE b/python/deps/untypy/LICENSE deleted file mode 100644 index 45d7ebab..00000000 --- a/python/deps/untypy/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 CodeSteak - -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. diff --git a/python/deps/untypy/README.rst b/python/deps/untypy/README.rst deleted file mode 100644 index 741895cf..00000000 --- a/python/deps/untypy/README.rst +++ /dev/null @@ -1,43 +0,0 @@ -untypy -====== - -A library for dynamic type checking in python. - -Current Features: ------------------ -- Simple Types like int, str, bool ... -- Callable (e.g. lambda) -- None -- Any -- Literal -- Tuple -- Union -- list, dict, set -- Optional -- Iterator -- Generator -- Checking of class methods -- Protocols - - -Examples --------- - -run examples with - -.. code:: bash - - python -m examples.ex1 - -.. code:: bash - - python -m examples.ex9 - -Tests ------ - -run tests with: - -.. code:: bash - - python -m unittest discover diff --git a/python/deps/untypy/examples/ex01.py b/python/deps/untypy/examples/ex01.py deleted file mode 100644 index ec949b9e..00000000 --- a/python/deps/untypy/examples/ex01.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import untypy - -untypy.enable() - -from typing import Annotated - -IsEven = Annotated[int, lambda x: x % 2 == 0] - - -def divideBy2(x: IsEven) -> int: - return x // 2 - - -divideBy2(7) - -# IsEven = Annotated[int, lambda x: x % 2 == 0] diff --git a/python/deps/untypy/examples/ex02.py b/python/deps/untypy/examples/ex02.py deleted file mode 100644 index db5fd2ea..00000000 --- a/python/deps/untypy/examples/ex02.py +++ /dev/null @@ -1,26 +0,0 @@ -import untypy - -untypy.enable() - -from typing import * - - -class PointDisplay: - def show(self, x: int, y: str) -> str: - return f"({x}, {y})" - - -T = TypeVar('T') -K = TypeVar('K') - - -class MyProtocol(Protocol[T, K]): - def show(self, x: T, y: T) -> K: - pass - - -def use_my_protocol(fn: MyProtocol[int, str]) -> None: - print(fn.show(10, 10)) - - -use_my_protocol(PointDisplay()) diff --git a/python/deps/untypy/examples/ex03.py b/python/deps/untypy/examples/ex03.py deleted file mode 100644 index 291a12f4..00000000 --- a/python/deps/untypy/examples/ex03.py +++ /dev/null @@ -1,14 +0,0 @@ -import untypy - -untypy.enable() - -from typing import * - - -def foo(functions: list[Callable[[], str]]) -> NoReturn: - for fn in functions: - print(fn()) - - -foo([lambda: "Hello"]) # This should cause no type error -foo([lambda: 42]) # This is a type error diff --git a/python/deps/untypy/examples/ex04.py b/python/deps/untypy/examples/ex04.py deleted file mode 100644 index 063eaf86..00000000 --- a/python/deps/untypy/examples/ex04.py +++ /dev/null @@ -1,17 +0,0 @@ -import untypy - -untypy.enable() - -from typing import * -import io - -AppendOnlyFile = Annotated[io.TextIOBase, lambda file: file.mode == 'a', - 'A File that is opend with mode "a".'] - - -def log(file: AppendOnlyFile, message: str) -> NoReturn: - file.write(message + "\n") - - -file = open("mylogfile", "w") -log(file, "Programm Started") diff --git a/python/deps/untypy/examples/ex05.py b/python/deps/untypy/examples/ex05.py deleted file mode 100644 index 4559efed..00000000 --- a/python/deps/untypy/examples/ex05.py +++ /dev/null @@ -1,30 +0,0 @@ -import untypy - -untypy.enable() - -from typing import TypeVar, Protocol - -X = TypeVar('X') - - -class ProtoX(Protocol): - - @untypy.postcondition(lambda ret: ret.startswith("a")) - def foo(self) -> str: - pass - - -class Concrete: - def __init__(self): - self.x = "This is an attribute of Concrete" - - def foo(self) -> str: - return "bbb" - - -def meep(a: ProtoX) -> None: - print(a.x) - a.foo() - - -meep(Concrete()) diff --git a/python/deps/untypy/examples/ex06.py b/python/deps/untypy/examples/ex06.py deleted file mode 100644 index 7f338277..00000000 --- a/python/deps/untypy/examples/ex06.py +++ /dev/null @@ -1,15 +0,0 @@ -import untypy - -untypy.enable() -from typing import * - -EvenNumber = Annotated[int, lambda x: x % 2 == 0, "Number must be even."] - - -def foo(funtions: List[Callable[[], EvenNumber]]) -> NoReturn: - for fn in funtions: - print(fn()) - - -func = lambda: 41 -foo([func]) # This is a type error diff --git a/python/deps/untypy/examples/ex09.py b/python/deps/untypy/examples/ex09.py deleted file mode 100644 index e5afe101..00000000 --- a/python/deps/untypy/examples/ex09.py +++ /dev/null @@ -1,11 +0,0 @@ -import untypy - -untypy.enable() - - -def show_price_for_screw(amount: int) -> None: - base_price = 10 # cents per screw - print(f"Screws: {amount * base_price} cents") - - -show_price_for_screw("3") diff --git a/python/deps/untypy/examples/ex10.py b/python/deps/untypy/examples/ex10.py deleted file mode 100644 index c16affd6..00000000 --- a/python/deps/untypy/examples/ex10.py +++ /dev/null @@ -1,17 +0,0 @@ -import untypy - -untypy.enable() - -from typing import Callable - - -def showoperator(name: str, fn: Callable[[int, int], int]) -> None: - print(f"5 {name} 4 = {fn(5, 4)}") - - -def mul(x: int, y: float) -> int: - return x * y - - -showoperator("+", lambda x, y: x + y) -showoperator("*", mul) diff --git a/python/deps/untypy/runtests.sh b/python/deps/untypy/runtests.sh deleted file mode 100755 index ac2b2bb3..00000000 --- a/python/deps/untypy/runtests.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -python3 -m unittest discover "$@" - diff --git a/python/deps/untypy/setup.py b/python/deps/untypy/setup.py deleted file mode 100644 index c4bb0ea0..00000000 --- a/python/deps/untypy/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python - -from distutils.core import setup - -setup(name='untypy', - version='0.0.1', - description='Dynamic Type Checking For Python', - author='Codesteak', - author_email='Codesteak@shellf.art', - url='https://github.com/CodeSteak/untypy', - packages=[], - ) diff --git a/python/deps/untypy/test/__init__.py b/python/deps/untypy/test/__init__.py deleted file mode 100644 index 12cfdd65..00000000 --- a/python/deps/untypy/test/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import untypy - -untypy.GlobalConfig = untypy.GlobalConfig._replace(checkedprefixes=["test"]) diff --git a/python/deps/untypy/test/impl/test_any.py b/python/deps/untypy/test/impl/test_any.py deleted file mode 100644 index 98698a80..00000000 --- a/python/deps/untypy/test/impl/test_any.py +++ /dev/null @@ -1,15 +0,0 @@ -import unittest - -from test.util import DummyExecutionContext -from untypy.impl.any import AnyChecker - - -class TestAny(unittest.TestCase): - - def test_wrap(self): - checker = AnyChecker() - - a = [1, 2, 3] - res = checker.check_and_wrap(a, DummyExecutionContext()) - - self.assertIs(a, res) diff --git a/python/deps/untypy/test/impl/test_callable.py b/python/deps/untypy/test/impl/test_callable.py deleted file mode 100644 index bbe772c9..00000000 --- a/python/deps/untypy/test/impl/test_callable.py +++ /dev/null @@ -1,78 +0,0 @@ -import unittest -from typing import Callable - -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from untypy.error import UntypyTypeError -from untypy.impl.callable import CallableFactory -from untypy.impl.dummy_delayed import DummyDelayedType - - -class TestCallable(unittest.TestCase): - fn1: Callable - fn2: Callable - - def setUp(self) -> None: - self.checker = CallableFactory().create_from(Callable[[int, int], str], DummyDefaultCreationContext()) - self.fn1 = self.checker.check_and_wrap(lambda x, y: str(x // y), DummyExecutionContext()) - self.fn2 = self.checker.check_and_wrap(lambda x, y: x // y, DummyExecutionContext()) - - def test_normal(self): - self.assertEqual(self.fn1(100, 20), "5") - - def test_is_callable(self): - self.assertTrue(callable(self.fn1)) - - def test_arg_error(self): - with self.assertRaises(UntypyTypeError) as cm: - self.fn1(100, "20") - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Callable[[int, int], str]") - self.assertEqual(i, " ^^^") - - # This file is responsable - self.assertEqual(cm.exception.last_responsable().file, __file__) - - def test_ret_error(self): - with self.assertRaises(UntypyTypeError) as cm: - self.fn2(100, 20) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Callable[[int, int], str]") - self.assertEqual(i, " ^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_not_a_callable(self): - with self.assertRaises(UntypyTypeError) as cm: - self.checker.check_and_wrap("Hello", DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Callable[[int, int], str]") - self.assertEqual(i, "^^^^^^^^^^^^^^^^^^^^^^^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_error_delayed(self): - self.checker = CallableFactory().create_from(Callable[[int, int], DummyDelayedType], - DummyDefaultCreationContext()) - fn = self.checker.check_and_wrap(lambda x, y: x // y, DummyExecutionContext()) - res = fn(1, 2) - - with self.assertRaises(UntypyTypeError) as cm: - res.use() - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Callable[[int, int], DummyDelayedType]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") diff --git a/python/deps/untypy/test/impl/test_dict.py b/python/deps/untypy/test/impl/test_dict.py deleted file mode 100644 index 42851819..00000000 --- a/python/deps/untypy/test/impl/test_dict.py +++ /dev/null @@ -1,87 +0,0 @@ -import unittest -import collections - -# For running with the debugger -#import sys -#sys.path.insert(0, '/Users/swehr/devel/write-your-python-program/python/deps/untypy/') - -import untypy -from test.util_test.untypy_test_case import dummy_caller -import inspect - -class TestDict(unittest.TestCase): - def test_equiv_with_builtin_dict(self): - self.check(str) - self.check(repr) - def inPlaceAdd1(l): - l["foo"] = 3 - (l, len(l)) - self.check(inPlaceAdd1) - self.check(lambda l: "2" in l) - self.check(lambda l: "42" in l) - self.check(lambda l: l["2"]) - self.check(lambda l: l["42"]) - def update(l): - l.update({"foo": 3}) - (l, len(l)) - self.check(update) - def update2(l): - l.update({"foo": 3}, chaos=100) - (l, len(l)) - self.check(update2) - def delete(l): - del l["2"] - (l, len(l)) - self.check(delete) - def iter(d): - acc = [] - for x in d: - acc.append(x) - return x - self.check(iter) - self.check(lambda d: d.copy()) - self.check(lambda d: d | {"42": 42, "2": 100}) - self.check(lambda d: {"42": 42, "2": 100} | d) - self.check(lambda d: len(d)) - self.check(lambda d: d.pop("2")) - self.check(lambda d: d.pop("42", 100)) - self.check(lambda d: d.popitem()) - self.check(lambda d: d.clear()) - self.check(lambda d: d.setdefault("1")) - self.check(lambda d: d.setdefault("1", 50)) - self.check(lambda d: d.get("1")) - self.check(lambda d: d.get("1", 50)) - self.check(lambda d: d.get("100")) - self.check(lambda d: d.get("100", 50)) - self.check(lambda d: d.keys()) - self.check(lambda d: d.items()) - self.check(lambda d: d.values()) - self.check(lambda l: list(reversed(l))) - - def check(self, f): - l1 = {"1": 1, "2": 2} - l2 = l1.copy() - try: - refRes = f(l1) - except Exception as e: - refRes = e - checker = untypy.checker(lambda ty=dict[str, int]: ty, dummy_caller) - wrapped = checker(l2) - self.assertFalse(l2 is wrapped) - try: - res = f(wrapped) - except Exception as e: - res = e - if isinstance(refRes, Exception) and isinstance(res, Exception): - self.assertEqual(str(refRes), str(res)) - elif not isinstance(refRes, Exception) and not isinstance(res, Exception): - if isinstance(refRes, collections.abc.ValuesView): - refRes = list(refRes) - if isinstance(res, collections.abc.ValuesView): - res = list(res) - self.assertEqual(refRes, res) - else: - src = inspect.getsource(f) - self.fail(f"when checking {src.strip()}: reference result: {refRes}, real result: {res}") - self.assertEqual(l1, wrapped) - self.assertEqual(l1, l2) diff --git a/python/deps/untypy/test/impl/test_forwardRef.py b/python/deps/untypy/test/impl/test_forwardRef.py deleted file mode 100644 index 3db02492..00000000 --- a/python/deps/untypy/test/impl/test_forwardRef.py +++ /dev/null @@ -1,57 +0,0 @@ -import unittest -from typing import Union, ForwardRef - -import untypy -from untypy.error import UntypyTypeError - -Shape = Union['Square', 'Circle'] -Shape2 = Union[ForwardRef('Square'), ForwardRef('Circle')] - -class Square: - pass - -class Circle: - pass - -@untypy.typechecked -def useShape(s: Shape) -> None: - pass - -@untypy.typechecked -def useShape2(s: Shape2) -> None: - pass - -@untypy.typechecked -def useUnion(s: Union[Square, Circle]) -> None: - pass - -@untypy.typechecked -def useSquare(s: Square) -> None: - pass - -class TestForwardRef(unittest.TestCase): - - def test_failing(self): - with self.assertRaises(UntypyTypeError): - useShape(0) - with self.assertRaises(UntypyTypeError): - useShape2(0) - with self.assertRaises(UntypyTypeError): - useUnion(0) - with self.assertRaises(UntypyTypeError): - useSquare(Circle()) - - def test_useSquare(self): - useSquare(Square()) - - def test_useShape(self): - useShape(Square()) - useShape(Circle()) - - def test_useShape2(self): - useShape2(Square()) - useShape2(Circle()) - - def test_useUnion(self): - useUnion(Square()) - useUnion(Circle()) diff --git a/python/deps/untypy/test/impl/test_generator.py b/python/deps/untypy/test/impl/test_generator.py deleted file mode 100644 index 11585561..00000000 --- a/python/deps/untypy/test/impl/test_generator.py +++ /dev/null @@ -1,167 +0,0 @@ -import unittest -from typing import Generator - -from test.util import DummyDefaultCreationContext, DummyExecutionContext -from untypy.error import UntypyTypeError -from untypy.impl import GeneratorFactory -from untypy.impl.dummy_delayed import DummyDelayedType - - -def gen_normal() -> Generator[int, str, bool]: - assert "a" == (yield 1) - assert "b" == (yield 2) - assert "c" == (yield 3) - return True - - -def gen_use_sent(): - # use dummy type to raise type exception - (yield).use() - return None - - -def create_checker(annotation): - return GeneratorFactory().create_from(annotation, DummyDefaultCreationContext()) - - -class TestGenerator(unittest.TestCase): - - def test_normal(self): - checker = create_checker(Generator[int, str, bool]) - - not_wrapped = gen_normal() - wrapped = checker.check_and_wrap(gen_normal(), DummyExecutionContext()) - - def test_gen_normal(generator): - self.assertEqual(generator.send(None), 1) - self.assertEqual(generator.send("a"), 2) - self.assertEqual(generator.send("b"), 3) - with self.assertRaises(StopIteration) as cm: - generator.send("c") - - self.assertEqual(cm.exception.value, True) - - # both wrapped an unwrapped has the same behaviour - test_gen_normal(not_wrapped) - test_gen_normal(wrapped) - - def test_not_a_generator(self): - checker = create_checker(Generator[int, str, bool]) - with self.assertRaises(UntypyTypeError) as cm: - checker.check_and_wrap(42, DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[int, str, bool]") - self.assertEqual(i, "^^^^^^^^^^^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_yield_error(self): - # annotation incorrect V - checker = create_checker(Generator[str, str, bool]) - wrapped = checker.check_and_wrap(gen_normal(), DummyExecutionContext()) - - with self.assertRaises(UntypyTypeError) as cm: - next(wrapped) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[str, str, bool]") - self.assertEqual(i, " ^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_send_error(self): - # annotation incorrect V - checker = create_checker(Generator[int, int, bool]) - wrapped = checker.check_and_wrap(gen_normal(), DummyExecutionContext()) - - next(wrapped) - with self.assertRaises(UntypyTypeError) as cm: - wrapped.send("a") - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[int, int, bool]") - self.assertEqual(i, " ^^^") - - self.assertEqual(cm.exception.last_responsable().file, __file__) - - def test_return_error(self): - # annotation incorrect - # Fun-Fact: bools are also ints V - checker = create_checker(Generator[int, str, str]) - - wrapped = checker.check_and_wrap(gen_normal(), DummyExecutionContext()) - - wrapped.send(None) - wrapped.send("a") - wrapped.send("b") - with self.assertRaises(UntypyTypeError) as cm: - wrapped.send("c") - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[int, str, str]") - self.assertEqual(i, " ^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_yield_error_delayed(self): - checker = create_checker(Generator[DummyDelayedType, str, bool]) - wrapped = checker.check_and_wrap(gen_normal(), DummyExecutionContext()) - - res = next(wrapped) - with self.assertRaises(UntypyTypeError) as cm: - res.use() - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[DummyDelayedType, str, bool]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_send_error_delayed(self): - checker = create_checker(Generator[None, DummyDelayedType, None]) - wrapped = checker.check_and_wrap(gen_use_sent(), DummyExecutionContext()) - - wrapped.send(None) - with self.assertRaises(UntypyTypeError) as cm: - wrapped.send(42) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[None, DummyDelayedType, None]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - self.assertEqual(cm.exception.last_responsable().file, __file__) - - def test_return_error_delayed(self): - checker = create_checker(Generator[int, str, DummyDelayedType]) - wrapped = checker.check_and_wrap(gen_normal(), DummyExecutionContext()) - - wrapped.send(None) - wrapped.send("a") - wrapped.send("b") - with self.assertRaises(StopIteration) as si: - wrapped.send("c") - - with self.assertRaises(UntypyTypeError) as cm: - si.exception.value.use() # use dummy - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Generator[int, str, DummyDelayedType]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") diff --git a/python/deps/untypy/test/impl/test_interface_dict.py b/python/deps/untypy/test/impl/test_interface_dict.py deleted file mode 100644 index 540b3af4..00000000 --- a/python/deps/untypy/test/impl/test_interface_dict.py +++ /dev/null @@ -1,235 +0,0 @@ -from test.util_test.untypy_test_case import UntypyTestCase, dummy_caller -from untypy.error import UntypyTypeError - - -class TestInterfaceDict(UntypyTestCase): - """ - Test's that the signatures matches the implementation. - """ - - def setUp(self) -> None: - # A: No Errors - self.good = dummy_caller( - dict[int, str], - { - 1: "one", - 2: "two", - 3: "three", - } - ) - - # B: Value error on 2 - self.valerr = dummy_caller( - dict[int, str], - { - 1: "one", - 2: 2, - 3: "three", - } - ) - - # C: Key error on 2 - self.keyerr = dummy_caller( - dict[int, str], - { - 1: "one", - "two": 2, - 3: "three", - } - ) - - def test_blame(self): - with self.assertRaises(UntypyTypeError) as cm: - # 42 must be str - self.good.get(1, 42) - self.assertBlame(cm, TestInterfaceDict.test_blame) - - with self.assertRaises(UntypyTypeError) as cm: - # key must be int - self.good.get("two") - self.assertBlame(cm, TestInterfaceDict.test_blame) - - with self.assertRaises(UntypyTypeError) as cm: - # value err - self.valerr.get(2) - self.assertBlame(cm, dummy_caller) - - def test_get(self): - self.assertEqual(self.good.get(1), "one") - self.assertEqual(self.good.get(4), None) - self.assertEqual(self.good.get(4, "four"), "four") - - with self.assertRaises(UntypyTypeError): - # 42 must be str - self.good.get(1, 42) - - with self.assertRaises(UntypyTypeError): - # key must be int - self.good.get("two") - - with self.assertRaises(UntypyTypeError): - # value err - self.valerr.get(2) - - def test_items(self): - self.assertEqual( - list(self.good.items()), - [(1, "one"), (2, "two"), (3, "three")] - ) - - with self.assertRaises(UntypyTypeError): - list(self.keyerr.items()) - - with self.assertRaises(UntypyTypeError): - list(self.valerr.items()) - - def test_keys(self): - self.assertEqual( - list(self.good.keys()), - [1, 2, 3] - ) - - with self.assertRaises(UntypyTypeError): - list(self.keyerr.keys()) - - # values stay unchecked - self.assertEqual( - list(self.valerr.keys()), - [1, 2, 3] - ) - - def test_pop(self): - self.assertEqual(self.good.pop(1), "one") - self.assertEqual(self.good.pop(4), None) - self.assertEqual(self.good.pop(4, "four"), "four") - - with self.assertRaises(UntypyTypeError): - self.good.pop("one") - - with self.assertRaises(UntypyTypeError): - self.valerr.pop(2) - - def test_popitem(self): - self.assertEqual(self.good.popitem(), (3, "three")) - self.assertEqual(self.good.popitem(), (2, "two")) - self.assertEqual(self.good.popitem(), (1, "one")) - - with self.assertRaises(KeyError): - self.good.popitem() - - self.assertEqual(self.keyerr.popitem(), (3, "three")) - with self.assertRaises(UntypyTypeError): - self.keyerr.popitem() - - self.assertEqual(self.valerr.popitem(), (3, "three")) - with self.assertRaises(UntypyTypeError): - self.valerr.popitem() - - def test_setdefault(self): - self.assertEqual(self.good.setdefault(1, "xxx"), "one") - - with self.assertRaises(UntypyTypeError): - # Untypy does not support setdefault w/o default=XXX - # https://github.com/skogsbaer/write-your-python-program/issues/19 - self.good.setdefault(5) - - self.assertEqual(self.good.setdefault(4, "four"), "four") - self.assertEqual(self.good.get(4), "four") - - with self.assertRaises(UntypyTypeError): - # 42 must be str - self.good.setdefault(4, 42) - - with self.assertRaises(UntypyTypeError): - self.valerr.setdefault(2) - - def test_values(self): - self.assertEqual( - list(self.good.values()), - ["one", "two", "three"] - ) - - with self.assertRaises(UntypyTypeError): - # This failes also, but I don't know why. - # However the keys violate the annotation, - # So this is fine. - self.assertEqual( - list(self.keyerr.values()), - ["one", "two", "three"] - ) - - with self.assertRaises(UntypyTypeError): - list(self.valerr.values()) - - def test_contains(self): - self.assertTrue(1 in self.good) - self.assertFalse(4 in self.good) - self.assertFalse("42" in self.good) - - def test_delitem(self): - del self.good[1] - - with self.assertRaises(KeyError): - del self.good[4] - - with self.assertRaises(UntypyTypeError): - del self.good["four"] - - self.assertEqual(self.good, {2: "two", 3: "three"}) - - def test_update(self): - self.good.update({4: "four", 5: "five"}) - str_int = dummy_caller( - dict[str, int], - { - "one": 1, - } - ) - str_int.update(four=4, five=5) - - with self.assertRaises(UntypyTypeError): - self.good.update({"a": "b"}) - - with self.assertRaises(UntypyTypeError): - str_int.update(four="111") - - def test_iter(self): - for k in self.good: - pass - - for k in self.valerr: - pass - - with self.assertRaises(UntypyTypeError): - for k in self.keyerr: - pass - - def test_len(self): - self.assertEqual(len(self.good), 3) - - def test_reversed(self): - self.assertEqual(list(reversed(self.good)), [3, 2, 1]) - self.assertEqual(list(reversed(self.valerr)), [3, 2, 1]) - - with self.assertRaises(UntypyTypeError): - list(reversed(self.keyerr)) - - def test_getitem(self): - self.assertEqual(self.good[1], "one") - - with self.assertRaises(UntypyTypeError): - self.good["two"] - - with self.assertRaises(UntypyTypeError): - self.valerr[2] - - def test_setiem(self): - self.good[1] = "not one" - - with self.assertRaises(UntypyTypeError): - # key err - self.good["key"] = "one" - - with self.assertRaises(UntypyTypeError): - # val err - self.good[4] = 44 diff --git a/python/deps/untypy/test/impl/test_interface_set.py b/python/deps/untypy/test/impl/test_interface_set.py deleted file mode 100644 index 74ecb9b5..00000000 --- a/python/deps/untypy/test/impl/test_interface_set.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest -# For running with the debugger -#import sys -#sys.path.insert(0, '/Users/swehr/devel/write-your-python-program/python/deps/untypy/') - -from test.util_test.untypy_test_case import UntypyTestCase, dummy_caller -from untypy.error import UntypyTypeError - - -class TestInterfaceSet(UntypyTestCase): - """ - Test's that the signatures matches the implementation. - """ - - def setUp(self) -> None: - self.good = dummy_caller( - set[int], - {1, 2, 3} - ) - - self.bad = dummy_caller( - set[int], - {1, "two", 3} - ) - - def test_add(self): - self.good.add(4) - - with self.assertRaises(UntypyTypeError): - self.good.add("four") - - def test_discard(self): - self.good.discard(3) - - with self.assertRaises(UntypyTypeError): - self.good.discard("four") - - def test_pop(self): - self.good.pop() - self.good.pop() - self.good.pop() - - with self.assertRaises(KeyError): - self.good.pop() - pass - - with self.assertRaises(UntypyTypeError): - self.bad.pop() - self.bad.pop() - self.bad.pop() - - def test_remove(self): - self.good.remove(3) - - with self.assertRaises(UntypyTypeError): - self.good.remove("four") - - def test_update(self): - self.good.update({5, 6, 7}) - self.good.update({8}, [9], {24}) - - with self.assertRaises(UntypyTypeError): - self.good.update({"four"}) - - @unittest.skip("fails :/. Does not raise UntypyTypeError") - #FIXME: enable once all tests are running - def test_ior(self): - self.good |= {4} | {7} - self.assertEqual(self.good, {1, 2, 3, 4, 7}) - - with self.assertRaises(UntypyTypeError): - self.good |= {"four"} - - def test_contains(self): - self.assertEqual(1 in self.good, True) - self.assertFalse("four" in self.good) - - def test_iter(self): - for k in self.good: - pass - - with self.assertRaises(UntypyTypeError): - for k in self.bad: - pass -def _debug(): - t = TestInterfaceSet() - t.setUp() - t.test_ior() - -# _debug() diff --git a/python/deps/untypy/test/impl/test_list.py b/python/deps/untypy/test/impl/test_list.py deleted file mode 100644 index ec757edc..00000000 --- a/python/deps/untypy/test/impl/test_list.py +++ /dev/null @@ -1,348 +0,0 @@ -import unittest -import typing - -# For running with the debugger -#import sys -#sys.path.insert(0, '/Users/swehr/devel/write-your-python-program/python/deps/untypy/') - -import untypy -from test.util import DummyExecutionContext -from test.util_test.untypy_test_case import dummy_caller -from untypy.error import UntypyTypeError -from untypy.impl.dummy_delayed import DummyDelayedType - - -class TestList(unittest.TestCase): - - def setUp(self) -> None: - self.checker = untypy.checker(lambda ty=list[int]: ty, dummy_caller, DummyExecutionContext()) - - self.normal_list = [0, 1, 2, 3] - self.wrapped_list = self.checker(self.normal_list) - - self.faulty_normal_list = [0, 1, "2", 3] - self.faulty_wrapped_list = self.checker(self.faulty_normal_list) - - def test_side_effects(self): - self.assertEqual(self.normal_list, self.wrapped_list) - self.normal_list.append(4) - self.assertEqual(self.normal_list, self.wrapped_list) - self.wrapped_list.append(4) - self.assertEqual(self.normal_list, self.wrapped_list) - - def test_error_delayed(self): - checker = untypy.checker(lambda ty=list[DummyDelayedType]: ty, dummy_caller, DummyExecutionContext()) - lst = checker([1]) - - res = lst[0] - with self.assertRaises(UntypyTypeError) as cm: - res.use() - - (t, i) = cm.exception.next_type_and_indicator() - self.assertEqual(t, "list[DummyDelayedType]") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_not_a_list_fail(self): - checker = untypy.checker(lambda ty=list[list[int]]: ty, dummy_caller, DummyExecutionContext()) - with self.assertRaises(UntypyTypeError) as cm: - checker((1,2,3)) - - def test_getitem_fail(self): - checker = untypy.checker(lambda ty=list[list[DummyDelayedType]]: ty, dummy_caller, DummyExecutionContext()) - lst = checker([[1]]) - res1 = lst[0][0] - with self.assertRaises(UntypyTypeError) as cm: - res1.use() - res2 = lst[:][:] - self.assertEqual(res2, [[1]]) - - def test_getitem_ok(self): - checker = untypy.checker(lambda ty=list[list[int]]: ty, dummy_caller, DummyExecutionContext()) - lst = checker([[1]]) - res1 = lst[0][0] - self.assertIs(int, type(res1)) - self.assertEqual(1, res1) - res2 = lst[:][:] - self.assertTrue(isinstance(res2, list)) - self.assertEqual([[1]], res2) - - def test_list_or_tuple(self): - checker = untypy.checker(lambda ty=typing.Union[list[int], tuple[str, str]]: ty, dummy_caller, DummyExecutionContext()) - lst = checker([1,2,3]) - (x, y) = checker(("foo", "bar")) - self.assertEqual([1,2,3], [lst[0], lst[1], lst[2]]) - self.assertEqual("foo", x) - self.assertEqual("bar", y) - lst_fail = checker(["foo", "bar"]) - with self.assertRaises(UntypyTypeError) as cm: - lst_fail[0] - with self.assertRaises(UntypyTypeError) as cm: - checker((1, 2)) - - def test_wrapping_resp(self): - """ - The wrapping call is responsable for ensuring the list - wrapped is typed correctly - """ - with self.assertRaises(UntypyTypeError) as cm: - var = self.faulty_wrapped_list[2] - - (t, i) = cm.exception.next_type_and_indicator() - - self.assertEqual(t, "list[int]") - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_wrapping_resp_by_side_effects(self): - """ - The wrapping call is responsable for side effects not causing - type errors - """ - self.normal_list.append("4") - with self.assertRaises(UntypyTypeError) as cm: - var = self.wrapped_list[4] - - (t, i) = cm.exception.next_type_and_indicator() - - self.assertEqual(t, "list[int]") - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_self_resp(self): - """ - when appending the caller of append is responsable - """ - with self.assertRaises(UntypyTypeError) as cm: - self.wrapped_list.append("4") - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "append(self, object: int)") - self.assertEqual(i, " ^^^") - - self.assertEqual(cm.exception.last_responsable().file, __file__) - - def test_not_a_list(self): - with self.assertRaises(UntypyTypeError) as cm: - self.checker("Hello") - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "list[int]") - self.assertEqual(i, "^^^^^^^^^") - - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_iterator(self): - out = [] - for i in self.wrapped_list: - out.append(i) - - self.assertEqual(out, [0, 1, 2, 3]) - - with self.assertRaises(UntypyTypeError): - for i in self.faulty_wrapped_list: - out.append(i) - - def test_some_basic_ops(self): - self.assertEqual(self.wrapped_list[1], 1) - self.assertEqual(self.wrapped_list[:], [0, 1, 2, 3]) - self.assertEqual(self.wrapped_list[1:], [1, 2, 3]) - - # Index - self.assertEqual(self.wrapped_list.index(1), 1) - self.assertEqual(self.wrapped_list.index(1, 0, 2), 1) - self.assertRaises(ValueError, lambda: self.wrapped_list.index(2, start=3)) # Value Error '2' not in list - self.assertRaises(UntypyTypeError, lambda: self.wrapped_list.index("hello")) # Wrong Argument Type - - self.assertEqual(self.wrapped_list, [0, 1, 2, 3]) - self.wrapped_list.append(4) - self.assertEqual(self.wrapped_list, [0, 1, 2, 3, 4]) - - self.wrapped_list.pop() - self.assertEqual(self.wrapped_list, [0, 1, 2, 3]) - - self.assertEqual(f"{self.wrapped_list}", f"{self.normal_list}") - - self.wrapped_list.append(4) - self.assertEqual(self.wrapped_list + [5, 6], [0, 1, 2, 3, 4, 5, 6]) - self.assertEqual([5,6] + self.wrapped_list, [5, 6, 0, 1, 2, 3, 4]) - self.assertEqual(self.wrapped_list + self.wrapped_list, [0,1,2,3,4,0,1,2,3,4]) - - self.wrapped_list.extend([5, 6]) - self.assertEqual(self.wrapped_list, [0, 1, 2, 3, 4, 5, 6]) - - self.wrapped_list.insert(0, 42) - self.assertEqual(self.wrapped_list, [42, 0, 1, 2, 3, 4, 5, 6]) - - self.wrapped_list.remove(4) - self.assertEqual(self.wrapped_list, [42, 0, 1, 2, 3, 5, 6]) - self.wrapped_list.remove(42) - self.wrapped_list.remove(5) - self.wrapped_list.remove(6) - self.wrapped_list.remove(0) - self.assertEqual(self.wrapped_list, [1, 2, 3]) - - x = self.wrapped_list.pop(1) - self.assertEqual(x, 2) - self.assertEqual(self.wrapped_list, [1, 3]) - - self.assertTrue(self.wrapped_list == [1,3]) - self.assertTrue(self.wrapped_list == self.wrapped_list) - self.assertTrue([1,3] == self.wrapped_list) - - self.assertRaises(UntypyTypeError, lambda: self.wrapped_list.append("foo")) - l = self.wrapped_list.copy() - self.assertEqual(l, self.wrapped_list) - l.append("foo") # no more wrapper - - def test_equiv_with_builtin_list(self): - self.check(str) - self.check(repr) - self.check(lambda l: [42, 5] + l) - self.check(lambda l: l + [42, 5]) - self.check(lambda l: 4 * l) - self.check(lambda l: l * 3) - def inPlaceAdd1(l): - l += [5,6] - self.check(inPlaceAdd1) - def inPlaceAdd2(l): - x = [5,6] - x += l - return x - self.check(inPlaceAdd2) - self.check(lambda l: 2 in l) - self.check(lambda l: 42 in l) - self.check(lambda l: l[0]) - self.check(lambda l: l[-2]) - self.check(lambda l: l[1:2]) - def extend(l): - l.extend((5,6)) - self.check(extend) - def sliceAssign1(l): - l[0:2] = [5,6,7,8] - self.check(sliceAssign1) - def sliceAssign2(l): - x = [0,1,2,3,4,5] - x[1:4] = l - return x - self.check(sliceAssign2) - def append(l): - l.append(5) - self.check(append) - def extend1(l): - l.extend([5,6]) - self.check(extend1) - def extend2(l): - x = [1,2,3,4,5,6,7] - x.extend(l) - return x - self.check(extend2) - def iter(l): - acc = [] - for x in l: - acc.append(l) - return acc - self.check(iter) - self.check(lambda l: l.insert(1, 42)) - self.check(lambda l: l.remove(1)) - self.check(lambda l: l.pop()) - self.check(lambda l: l.pop(2)) - self.check(lambda l: l.clear()) - self.check(lambda l: l.index(4, 1, 3)) - self.check(lambda l: len(l)) - self.check(lambda l: l.count(1)) - self.check(lambda l: sorted(l)) - self.check(lambda l: l.sort()) - self.check(lambda l: l.sort(reverse=True)) - self.check(lambda l: l.sort(key=lambda i: -i)) - self.check(lambda l: list(reversed(l))) - self.check(lambda l: l.reverse()) - self.check(lambda l: l.copy()) - self.check(lambda l: repr(l)) - self.check(lambda l: str(l)) - # == - self.check(lambda l: l == [1,4,2,1]) - self.check(lambda l: [1,4,2,1] == l) - self.check2(lambda l1, l2: l1 == l2) - self.check2(lambda l1, l2: l2 == l1) - # != - self.check(lambda l: l != [1,4,2,1]) - self.check(lambda l: [1,4,2,1] != l) - self.check2(lambda l1, l2: l1 != l2) - self.check2(lambda l1, l2: l2 != l1) - # < - self.check(lambda l: l < [1,4,2]) - self.check(lambda l: [1,4,1] < l) - self.check2(lambda l1, l2: l1 < l2) - self.check2(lambda l1, l2: l2 < l1) - # <= - self.check(lambda l: l <= [1,4,2]) - self.check(lambda l: [1,4,1] <= l) - self.check2(lambda l1, l2: l1 <= l2) - self.check2(lambda l1, l2: l2 <= l1) - # > - self.check(lambda l: l > [1,4,2]) - self.check(lambda l: [1,4,1] > l) - self.check2(lambda l1, l2: l1 > l2) - self.check2(lambda l1, l2: l2 > l1) - # >= - self.check(lambda l: l >= [1,4,2]) - self.check(lambda l: [1,4,1] >= l) - self.check2(lambda l1, l2: l1 >= l2) - self.check2(lambda l1, l2: l2 >= l1) - - def check(self, f): - l1 = [1, 4, 2, 1] - l2 = l1.copy() - refRes = f(l1) - checker = untypy.checker(lambda ty=list[int]: ty, dummy_caller) - wrapped = checker(l2) - self.assertFalse(l1 is wrapped) - res = f(wrapped) - self.assertEqual(l1, wrapped) - self.assertEqual(l1, l2) - self.assertEqual(refRes, res) - - def check2(self, f): - self.check21(f) - self.check22(f) - - def check21(self, f): - l1 = [1, 4, 2, 1] - l11 = l1.copy() - l12 = l1.copy() - l1w = l1.copy() - checker = untypy.checker(lambda ty=list[int]: ty, dummy_caller) - wrapped1 = checker(l1w) - self.assertFalse(l1 is wrapped1) - refRes11 = f(l11, l12) - res11 = f(wrapped1, wrapped1) - self.assertEqual(l1, wrapped1) - self.assertEqual(refRes11, res11) - - def check22(self, f): - l1 = [1, 4, 2, 1] - l2 = [1, 4, 1] - l1w = l1.copy() - l2w = l2.copy() - refRes12 = f(l1, l2) - checker = untypy.checker(lambda ty=list[int]: ty, dummy_caller) - wrapped1 = checker(l1w) - wrapped2 = checker(l2w) - self.assertFalse(l1 is wrapped1) - self.assertFalse(l2 is wrapped2) - res12 = f(wrapped1, wrapped2) - self.assertEqual(l1, wrapped1) - self.assertEqual(l1, l1w) - self.assertEqual(l2, wrapped2) - self.assertEqual(l2, l2w) - self.assertEqual(refRes12, res12) - -# def _debug(): -# t = TestList() -# t.setUp() -# t.test_some_basic_ops() -# -# _debug() diff --git a/python/deps/untypy/test/impl/test_literal.py b/python/deps/untypy/test/impl/test_literal.py deleted file mode 100644 index ded683e9..00000000 --- a/python/deps/untypy/test/impl/test_literal.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -from typing import Literal - -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from untypy.error import UntypyTypeError -from untypy.impl.literal import LiteralFactory - - -class TestLiteral(unittest.TestCase): - - def setUp(self) -> None: - self.checker = LiteralFactory().create_from(Literal[1, 2, "3"], DummyDefaultCreationContext()) - - def test_positive_checking(self): - self.assertEqual(self.checker.check_and_wrap(1, DummyExecutionContext()), 1) - self.assertEqual(self.checker.check_and_wrap("3", DummyExecutionContext()), "3") - - def test_neg_checking(self): - with self.assertRaises(UntypyTypeError) as cm: - self.checker.check_and_wrap(4, DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Literal[1, 2, '3']") - self.assertEqual(i, "^^^^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") diff --git a/python/deps/untypy/test/impl/test_none.py b/python/deps/untypy/test/impl/test_none.py deleted file mode 100644 index 198bbbc5..00000000 --- a/python/deps/untypy/test/impl/test_none.py +++ /dev/null @@ -1,35 +0,0 @@ -import unittest - -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from untypy.error import UntypyTypeError -from untypy.impl.none import NoneFactory - - -class TestNone(unittest.TestCase): - - def test_wrap(self): - checker = NoneFactory().create_from(None, DummyDefaultCreationContext()) - - res = checker.check_and_wrap(None, DummyExecutionContext()) - self.assertEqual(None, res) - - def test_wrap_of_none_type(self): - checker = NoneFactory().create_from(type(None), DummyDefaultCreationContext()) - - res = checker.check_and_wrap(None, DummyExecutionContext()) - self.assertEqual(None, res) - - def test_wrap_negative(self): - checker = NoneFactory().create_from(None, DummyDefaultCreationContext()) - - with self.assertRaises(UntypyTypeError) as cm: - res = checker.check_and_wrap(12, DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "None") - self.assertEqual(i, "^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") diff --git a/python/deps/untypy/test/impl/test_optional.py b/python/deps/untypy/test/impl/test_optional.py deleted file mode 100644 index f64b0e71..00000000 --- a/python/deps/untypy/test/impl/test_optional.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -from typing import Optional - -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from untypy.error import UntypyTypeError -from untypy.impl.dummy_delayed import DummyDelayedType -from untypy.impl.optional import OptionalFactory - - -class TestOptional(unittest.TestCase): - - def test_wrap(self): - checker = OptionalFactory().create_from(Optional[int], DummyDefaultCreationContext()) - res = checker.check_and_wrap(1, DummyExecutionContext()) - self.assertEqual(1, res) - - res = checker.check_and_wrap(None, DummyExecutionContext()) - self.assertEqual(None, res) - - def test_wrap_negative(self): - checker = OptionalFactory().create_from(Optional[int], DummyDefaultCreationContext()) - with self.assertRaises(UntypyTypeError) as cm: - res = checker.check_and_wrap(23.5, DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Optional[int]") - self.assertEqual(i, " ^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_wrap_negative_delayed(self): - checker = OptionalFactory().create_from(Optional[DummyDelayedType], DummyDefaultCreationContext()) - - res = checker.check_and_wrap(1, DummyExecutionContext()) - - with self.assertRaises(UntypyTypeError) as cm: - res.use() - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Optional[DummyDelayedType]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") diff --git a/python/deps/untypy/test/impl/test_protocol.py b/python/deps/untypy/test/impl/test_protocol.py deleted file mode 100644 index 2afb8a79..00000000 --- a/python/deps/untypy/test/impl/test_protocol.py +++ /dev/null @@ -1,280 +0,0 @@ -import unittest -from typing import Protocol, Union, TypeVar, Generic, NoReturn - -import untypy -from test.util import DummyDefaultCreationContext, DummyExecutionContext, location_of -from untypy.error import UntypyTypeError -from untypy.impl import ProtocolFactory, GenericFactory -from untypy.impl.union import UnionFactory - - -class A: - pass - - -class ParrentB: - pass - - -class B(ParrentB): - pass - - -class ProtoReturnB(Protocol): - @untypy.patch - def meth(self) -> B: - raise NotImplementedError - - -class ProtoReceiveB(Protocol): - @untypy.patch - def meth(self, b: B) -> None: - raise NotImplementedError - -class UntypedProtocol(Protocol): - @untypy.patch - def meth(self, a, b): - raise NotImplementedError - -class TestProtocolTestCommon(unittest.TestCase): - - def setUp(self) -> None: - self.sig_b = "B" - self.ProtoReturnB = ProtoReturnB - self.ProtoReceiveB = ProtoReceiveB - self.ProtoReturnBName = "ProtoReturnB" - self.ProtoReceiveBName = "ProtoReceiveB" - self.checker_return = ProtocolFactory().create_from(ProtoReturnB, DummyDefaultCreationContext()) - self.checker_arg = ProtocolFactory().create_from(ProtoReceiveB, DummyDefaultCreationContext()) - - def test_not_implementing_methods(self): - class NoMeth: - def foo(self) -> None: - pass - - with self.assertRaises(UntypyTypeError) as cm: - self.checker_return.check_and_wrap(NoMeth(), DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, self.ProtoReturnBName) - self.assertEqual(i, "^" * len(self.ProtoReturnBName)) - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_receiving_wrong_arguments(self): - class Concrete: - @untypy.patch - def meth(self, b: ParrentB) -> None: - pass - - instance = self.checker_arg.check_and_wrap(Concrete(), DummyExecutionContext()) - with self.assertRaises(UntypyTypeError) as cm: - instance.meth(A()) - - (t, i) = cm.exception.next_type_and_indicator() - - self.assertEqual(t, f"meth(self: Self, b: {self.sig_b}) -> None") - self.assertEqual(cm.exception.last_responsable().file, __file__) - - self.assertEqual(cm.exception.last_declared(), location_of(self.ProtoReceiveB.meth)) - - def test_return_wrong_arguments(self): - class Concrete: - @untypy.patch - def meth(self) -> B: - return A() - - instance = self.checker_return.check_and_wrap(Concrete(), DummyExecutionContext()) - with self.assertRaises(UntypyTypeError) as cm: - instance.meth() - - (t, i) = cm.exception.next_type_and_indicator() - - self.assertEqual(t, f"meth(self: Self) -> B") - self.assertEqual(cm.exception.last_responsable().file, __file__) - self.assertEqual(cm.exception.last_declared(), location_of(Concrete.meth)) - - def test_concrete_wrong_argument_signature(self): - class Concrete: - @untypy.patch - def meth(self, b: A) -> NoReturn: - pass - - instance = self.checker_arg.check_and_wrap(Concrete(), DummyExecutionContext()) - with self.assertRaises(UntypyTypeError) as cm: - instance.meth(B()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "meth(self: Self, b: A) -> None") - self.assertEqual(i, " ^") - self.assertEqual(cm.exception.last_responsable(), location_of(Concrete.meth)) - self.assertEqual(cm.exception.last_declared(), location_of(self.ProtoReceiveB.meth)) - - def test_concrete_wrong_return_signature(self): - class Concrete: - @untypy.patch - def meth(self) -> A: - return A() - - instance = self.checker_return.check_and_wrap(Concrete(), DummyExecutionContext()) - with self.assertRaises(UntypyTypeError) as cm: - instance.meth() - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, f"meth(self: Self) -> {self.sig_b}") - self.assertEqual(cm.exception.last_responsable(), location_of(Concrete.meth)) - self.assertEqual(cm.exception.last_declared(), location_of(self.ProtoReturnB.meth)) - - def test_double_wrapping(self): - # there should only one layer of wrapping. - - class EmptyProto(Protocol): - pass - - class MProto(Protocol): - def m(self) -> int: - pass - - class ConcreteMProto: - @untypy.patch - def m(self) -> int: - return 42 - - @untypy.patch - def a(p: EmptyProto) -> None: - b(p) - - @untypy.patch - def b(p: MProto) -> None: - p.m() - - # must not fail - a(ConcreteMProto()) - - -class TestProtocolGenerics(TestProtocolTestCommon): - def setUp(self) -> None: - T = TypeVar("T") - - class ProtoReturnGeneric(Protocol[T]): - @untypy.patch - def meth(self) -> T: - raise NotImplementedError - - class ProtoReceiveGeneric(Protocol[T]): - @untypy.patch - def meth(self, b: T) -> None: - raise NotImplementedError - - self.sig_b = "B" - self.ProtoReturnB = ProtoReturnGeneric - self.ProtoReceiveB = ProtoReceiveGeneric - self.ProtoReturnBName = "ProtoReturnGeneric[B]" - self.ProtoReceiveBName = "ProtoReceiveGeneric(Protocol)" - self.checker_return = ProtocolFactory().create_from(ProtoReturnGeneric[B], DummyDefaultCreationContext()) - self.checker_arg = ProtocolFactory().create_from(ProtoReceiveGeneric[B], DummyDefaultCreationContext()) - - -class TestProtocolGenericTypeRepr(unittest.TestCase): - - def test_protocol_type_repr(self): - T = TypeVar("T") - - # There was a bug, causing T to be listet multiple Times. - @untypy.patch - class Proto(Generic[T]): - @untypy.patch - def do_it(self, a: T, b: T) -> None: - return - - fac = GenericFactory().create_from(Proto[int], DummyDefaultCreationContext()) - with self.assertRaises(UntypyTypeError) as cm: - fac.check_and_wrap(42, DummyExecutionContext()) - self.assertEqual(cm.exception.expected, "Proto[int]") - - -class TestProtocolSpecific(unittest.TestCase): - - def test_union_protocols(self): - class U1: - @untypy.patch - def meth(self) -> str: - return "s" - - class U2: - @untypy.patch - def meth(self) -> int: - return 42 - - @untypy.patch - def meth2(self) -> int: - return 42 - - # when wrapping order matters - UnionFactory() \ - .create_from(Union[U1, U2], DummyDefaultCreationContext()) \ - .check_and_wrap(U1(), DummyExecutionContext()) \ - .meth() - UnionFactory() \ - .create_from(Union[U1, U2], DummyDefaultCreationContext()) \ - .check_and_wrap(U2(), DummyExecutionContext()) \ - .meth() - UnionFactory() \ - .create_from(Union[U2, U1], DummyDefaultCreationContext()) \ - .check_and_wrap(U1(), DummyExecutionContext()) \ - .meth() - UnionFactory() \ - .create_from(Union[U2, U1], DummyDefaultCreationContext()) \ - .check_and_wrap(U2(), DummyExecutionContext()) \ - .meth() - - def test_untyped_no_match(self): - checker = ProtocolFactory().create_from(UntypedProtocol, DummyDefaultCreationContext()) - class U0: - @untypy.patch - def notmeth(self, a, b): - return 42 - - with self.assertRaises(UntypyTypeError) as cm: - checker.check_and_wrap(U0(), DummyExecutionContext()) - self.assertEqual(cm.exception.expected, "UntypedProtocol") - - def test_untyped_match(self): - checker = ProtocolFactory().create_from(UntypedProtocol, DummyDefaultCreationContext()) - class U1: - @untypy.patch - def meth(self, a, b): # matches - return 42 - - self.assertEqual(checker.check_and_wrap(U1(), DummyExecutionContext()).meth("a", "b"), 42) - - def test_untyped_nomatch_signature(self): - checker = ProtocolFactory().create_from(UntypedProtocol, DummyDefaultCreationContext()) - class U2: - @untypy.patch - def meth(self, a): # does not match - pass - - with self.assertRaises(UntypyTypeError) as cm: - checker.check_and_wrap(U2(), DummyExecutionContext()).meth("a", 42) - self.assertEqual(cm.exception.expected, "UntypedProtocol") - - def test_untyped_narrow_signature(self): - checker = ProtocolFactory().create_from(UntypedProtocol, DummyDefaultCreationContext()) - class U3: - @untypy.patch - def meth(self, a : int, b : int) -> int: # matches, but to specific - return a + b - - wrapped = checker.check_and_wrap(U3(), DummyExecutionContext()) - - self.assertEqual(wrapped.meth(10, 20), 30) - with self.assertRaises(UntypyTypeError) as cm: - wrapped.meth("a", 20) - - self.assertEqual(cm.exception.previous_chain.expected, "UntypedProtocol") diff --git a/python/deps/untypy/test/impl/test_set.py b/python/deps/untypy/test/impl/test_set.py deleted file mode 100644 index ae2562d9..00000000 --- a/python/deps/untypy/test/impl/test_set.py +++ /dev/null @@ -1,171 +0,0 @@ -import unittest -import collections - -# For running with the debugger -#import sys -#sys.path.insert(0, '/Users/swehr/devel/write-your-python-program/python/deps/untypy/') - -import untypy -from test.util_test.untypy_test_case import dummy_caller - - -class TestSet(unittest.TestCase): - def test_update(self): - checker = untypy.checker(lambda ty=set[str]: ty, dummy_caller) - s1 = set(["foo", "bar"]) - s2 = set(["bar", "spam"]) - s3 = s1.copy() - s4 = s2.copy() - s3w = checker(s3) - s4w = checker(s4) - refRes = s1.update(s2) - res = s3w.update(s4w) - self.assertEqual(refRes, res) - self.assertEqual(s1, s3) - self.assertEqual(s1, s3w) - self.assertEqual(s2, s4) - self.assertEqual(s2, s4w) - - def test_equiv_with_builtin_set(self): - self.check(str) - self.check(repr) - self.check(lambda s: s.add(3)) - self.check(lambda s: s.add(2)) - self.check(lambda s: s.clear()) - self.check(lambda s: s.copy()) - self.check(lambda s: s.discard(1)) - self.check(lambda s: s.discard(3)) - self.check(lambda s: s.pop()) - self.check(lambda s: s.remove(1)) - self.check(lambda s: s.remove(3)) - self.check(lambda s: 1 in s) - self.check(lambda s: 3 in s) - self.check(lambda s: len(s)) - self.checkM(lambda s, others: s.difference(*others)) - self.checkM(lambda s, others: s.difference_update(*others)) - self.checkM(lambda s, others: s.symmetric_difference(*others)) - self.checkM(lambda s, others: s.symmetric_difference_update(*others)) - self.checkM(lambda s, others: s.union(*others)) - self.checkM(lambda s, others: s.update(*others)) - self.checkSym(lambda s1, s2: s1.issubset(s2)) - self.checkSym(lambda s1, s2: s1.issuperset(s2)) - self.checkSym(lambda s1, s2: s1.isdisjoint(s2)) - self.checkSym(lambda s1, s2: s1 < s2) - self.checkSym(lambda s1, s2: s1 <= s2) - self.checkSym(lambda s1, s2: s1 > s2) - self.checkSym(lambda s1, s2: s1 >= s2) - self.checkSym(lambda s1, s2: s1 == s2) - self.checkSym(lambda s1, s2: s1 != s2) - self.checkSym(lambda s1, s2: s1 & s2) - def addAssign(s1, s2): s1 &= s2 - self.checkSym(addAssign) - self.checkSym(lambda s1, s2: s1 | s2) - def orAssign(s1, s2): s1 |= s2 - self.checkSym(orAssign) - self.checkSym(lambda s1, s2: s1 ^ s2) - def xorAssign(s1, s2): s1 ^= s2 - self.checkSym(xorAssign) - self.checkSym(lambda s1, s2: s1 - s2) - self.checkSym(lambda s1, s2: s1 - s2) - def subAssign(s1, s2): s1 -= s2 - self.checkSym(subAssign) - def iter(s): - acc = [] - for x in s: - acc.append(x) - return acc - self.check(iter) - - def _check(self, act1, act2, eqs): - try: - refRes = act1() - except Exception as e: - refRes = e - try: - res = act2() - except Exception as e: - res = e - if isinstance(refRes, Exception) and isinstance(res, Exception): - self.assertEqual(str(refRes), str(res)) - elif not isinstance(refRes, Exception) and not isinstance(res, Exception): - self.assertEqual(refRes, res) - else: - self.fail(f"resRef={refRes}, res={res}") - for (x, y) in eqs: - self.assertEqual(x, y) - - def check(self, f): - l1 = set([1,2]) - l2 = l1.copy() - checker = untypy.checker(lambda ty=set[int]: ty, dummy_caller) - wrapped = checker(l2) - self.assertFalse(l2 is wrapped) - self._check(lambda: f(l1), lambda: f(wrapped), [(l1, wrapped), (l1, l2)]) - - def checkSym(self, f): - s1 = None - s2 = None - s3 = None - s4 = None - s3w = None - s4w = None - eqs = None - def setup(): - nonlocal s1,s2,s3,s4,s3w,s4w,eqs - s1 = set(["foo", "bar"]) - s2 = set(["bar", "spam"]) - checker = untypy.checker(lambda ty=set[str]: ty, dummy_caller) - s3 = s1.copy() - s4 = s2.copy() - s3w = checker(s3) - s4w = checker(s4) - eqs = [(s1, s3), (s2, s4), (s1, s3w), (s2, s4w)] - setup() - self._check(lambda: f(s1, s2), lambda: f(s3w, s4w), eqs) - setup() - self._check(lambda: f(s2, s1), lambda: f(s4w, s3w), eqs) - setup() - self._check(lambda: f(s1, s2), lambda: f(s3, s4w), eqs) - setup() - self._check(lambda: f(s2, s1), lambda: f(s4w, s3), eqs) - setup() - self._check(lambda: f(s1, s2), lambda: f(s3w, s4), eqs) - setup() - self._check(lambda: f(s2, s1), lambda: f(s4, s3w), eqs) - - def checkM(self, f): - s1 = None - s2 = None - s3 = None - s4 = None - s5 = None - s6 = None - s4w = None - s5w = None - s6w = None - eqs = None - def setup(): - nonlocal s1,s2,s3,s4,s5,s6,s4w,s5w,s6w,eqs - s1 = set(["foo", "bar"]) - s2 = set(["bar", "spam"]) - s3 = set(["foo", "egg"]) - checker = untypy.checker(lambda ty=set[str]: ty, dummy_caller) - s4 = s1.copy() - s5 = s2.copy() - s6 = s3.copy() - s4w = checker(s4) - s5w = checker(s5) - s6w = checker(s6) - eqs = [(s1, s4), (s1, s4w), (s2, s5), (s2, s5w), (s3, s6), (s3, s6w)] - setup() - self._check(lambda: f(s1, [s2]), lambda: f(s4w, [s5w]), eqs) - setup() - self._check(lambda: f(s1, [s2]), lambda: f(s4w, [s5]), eqs) - setup() - self._check(lambda: f(s1, [s2]), lambda: f(s4, [s5w]), eqs) - setup() - self._check(lambda: f(s1, [s2,s3]), lambda: f(s4w, [s5w, s6w]), eqs) - setup() - self._check(lambda: f(s1, [s2,s3]), lambda: f(s4w, [s5, s6w]), eqs) - setup() - self._check(lambda: f(s1, [s2,s3]), lambda: f(s4, [s5w, s6]), eqs) diff --git a/python/deps/untypy/test/impl/test_simple.py b/python/deps/untypy/test/impl/test_simple.py deleted file mode 100644 index 6e70e37d..00000000 --- a/python/deps/untypy/test/impl/test_simple.py +++ /dev/null @@ -1,160 +0,0 @@ -import unittest -from typing import Union, Callable - -import untypy -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from untypy.error import UntypyTypeError, UntypyAttributeError -from untypy.impl.simple import SimpleFactory -from untypy.impl.union import UnionFactory - - -@untypy.patch -class A: - pass - - -@untypy.patch -class ChildOfA(A): - pass - - -@untypy.patch -class B: - pass - -@untypy.patch -class SomeParent: - @untypy.patch - def meth(self) -> str: - return "Hello" - - -@untypy.patch -class ChildOfSomeParent(SomeParent): - @untypy.patch - def meth(self) -> int: # Signature does not match. - return 42 - - -class TestSimple(unittest.TestCase): - - def test_wrap(self): - checker = SimpleFactory().create_from(A, DummyDefaultCreationContext()) - - a = A() - child_a = ChildOfA() - - res = checker.check_and_wrap(a, DummyExecutionContext()) - self.assertIs(a, res) - res = checker.check_and_wrap(child_a, DummyExecutionContext()) - self.assertIsNot(child_a, res) # Wrapped with AWrapper - - def test_attributes(self): - a = ChildOfA() - a.foo = 42 - - # Note: Attributes are not checked, but they need to be accessible - - @untypy.patch - def m(x: A) -> None: - self.assertEqual(x.foo, 42) - x.foo = 43 - self.assertEqual(x.foo, 43) - - m(a) - self.assertEqual(a.foo, 43) - - def test_wrap_negative(self): - checker = SimpleFactory().create_from(A, DummyDefaultCreationContext()) - - with self.assertRaises(UntypyTypeError) as cm: - res = checker.check_and_wrap(B(), DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "A") - self.assertEqual(i, "^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_wrap_inheritance(self): - checker = SimpleFactory().create_from(SomeParent, DummyDefaultCreationContext()) - - res = checker.check_and_wrap(ChildOfSomeParent(), DummyExecutionContext()) - with self.assertRaises(UntypyTypeError): - res.meth() - - def test_unions_simple_types_negative(self): - class U1: - def meth(self) -> None: - pass - - class U2: - def meth(self) -> None: - pass - - with self.assertRaises(UntypyAttributeError): - # Should fail because both have the same methods - # Wrapping cannot distinguish - UnionFactory().create_from(Union[U1, U2], DummyDefaultCreationContext()) - - def test_unions_simple_types_negative(self): - class U3: - def meth(self) -> None: - pass - - class U4: - def meth(self) -> None: - pass - - def somethingelse(self) -> None: - pass - - # this should also be fine. A and B don't have any signatures, - # so protocol like wrapping does not apply - UnionFactory().create_from(Union[A, B], DummyDefaultCreationContext()) - - # this must be fine: Names of Signatures differ. - UnionFactory().create_from(Union[U3, U4], DummyDefaultCreationContext()) - - def test_no_inheritance_checking_of_builtins(self): - class SubInt(int): - pass - - @untypy.patch - def take_number(number: int) -> None: - # SubInt should not be wrapped. - self.assertEqual(type(number), SubInt) - - take_number(SubInt("42")) - - def test_int_as_float(self): - @untypy.patch - def f(x: float) -> float: - return x + 1 - self.assertEqual(f(1), 2) - self.assertEqual(f(1.1), 2.1) - with self.assertRaises(UntypyTypeError): - f("x") - - def test_float_not_as_int(self): - @untypy.patch - def f(x: int) -> int: - return x + 1 - self.assertEqual(f(1), 2) - with self.assertRaises(UntypyTypeError): - f("x") - with self.assertRaises(UntypyTypeError): - f(3.14) - - def test_callable(self): - @untypy.patch - def f(g: Callable) -> int: - return g(1) - self.assertEqual(f(lambda x: x + 1), 2) - with self.assertRaises(TypeError): - f(lambda x: "x" + x) - with self.assertRaises(UntypyTypeError): - f(lambda x: "x" + str(x)) diff --git a/python/deps/untypy/test/impl/test_str.py b/python/deps/untypy/test/impl/test_str.py deleted file mode 100644 index a3b28d79..00000000 --- a/python/deps/untypy/test/impl/test_str.py +++ /dev/null @@ -1,96 +0,0 @@ -import unittest -from typing import Iterable - -from test.util_test.untypy_test_case import dummy_caller -import untypy - - -class TestStr(unittest.TestCase): - - def test_equiv_with_builtin_tuple(self): - self.check(str) - self.check(repr) - self.check(lambda l: "foo" + l) - self.check(lambda l: l + "foo") - self.check(lambda l: 4 * l) - self.check(lambda l: l * 3) - self.check(lambda l: "sp" in l) - self.check(lambda l: "xxx" in l) - self.check(lambda l: l[0]) - self.check(lambda l: l[-2]) - self.check(lambda l: l[1:2]) - def iter(l): - acc = [] - for x in l: - acc.append(l) - return acc - self.check(iter) - self.check(lambda l: len(l)) - self.check(lambda l: l.count("s")) - self.check(lambda l: sorted(l)) - self.check(lambda l: list(reversed(l))) - # == - self.check(lambda l: l == "spam") - self.check(lambda l: "spam" == l) - self.check2(lambda l1, l2: l1 == l2) - self.check2(lambda l1, l2: l2 == l1) - # != - self.check(lambda l: l != "spam") - self.check(lambda l: "spam" != l) - self.check2(lambda l1, l2: l1 != l2) - self.check2(lambda l1, l2: l2 != l1) - # < - self.check(lambda l: l < "egg") - self.check(lambda l: "egg" < l) - self.check2(lambda l1, l2: l1 < l2) - self.check2(lambda l1, l2: l2 < l1) - # <= - self.check(lambda l: l <= "egg") - self.check(lambda l: "egg" <= l) - self.check2(lambda l1, l2: l1 <= l2) - self.check2(lambda l1, l2: l2 <= l1) - # > - self.check(lambda l: l > "egg") - self.check(lambda l: "egg" > l) - self.check2(lambda l1, l2: l1 > l2) - self.check2(lambda l1, l2: l2 > l1) - # >= - self.check(lambda l: l >= "egg") - self.check(lambda l: "egg" >= l) - self.check2(lambda l1, l2: l1 >= l2) - self.check2(lambda l1, l2: l2 >= l1) - - def check(self, f): - l1 = "spam" - refRes = f(l1) - checker = untypy.checker(lambda ty=Iterable[str]: ty, dummy_caller) - wrapped = checker(l1) - self.assertFalse(l1 is wrapped) - res = f(wrapped) - self.assertEqual(l1, wrapped) - self.assertEqual(refRes, res) - - def check2(self, f): - self.check21(f) - self.check22(f) - - def check21(self, f): - l1 = "spam" - refRes11 = f(l1, l1) - checker = untypy.checker(lambda ty=Iterable[str]: ty, dummy_caller) - wrapped1 = checker(l1) - self.assertFalse(l1 is wrapped1) - res11 = f(wrapped1, wrapped1) - self.assertEqual(refRes11, res11) - - def check22(self, f): - l1 = "spam" - l2 = "spa" - refRes12 = f(l1, l2) - checker = untypy.checker(lambda ty=Iterable[str]: ty, dummy_caller) - wrapped1 = checker(l1) - wrapped2 = checker(l2) - self.assertFalse(l1 is wrapped1) - self.assertFalse(l2 is wrapped2) - res12 = f(wrapped1, wrapped2) - self.assertEqual(refRes12, res12) diff --git a/python/deps/untypy/test/impl/test_tuple.py b/python/deps/untypy/test/impl/test_tuple.py deleted file mode 100644 index a7edb3f9..00000000 --- a/python/deps/untypy/test/impl/test_tuple.py +++ /dev/null @@ -1,156 +0,0 @@ -import unittest -from typing import Tuple - -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from test.util_test.untypy_test_case import dummy_caller -import untypy -from untypy.error import UntypyTypeError -from untypy.impl.dummy_delayed import DummyDelayedType -from untypy.impl.tuple import TupleFactory - - -class TestTuple(unittest.TestCase): - - def test_wrap_lower_case(self): - checker = TupleFactory().create_from(tuple[int, str], DummyDefaultCreationContext()) - res = checker.check_and_wrap((1, "2"), DummyExecutionContext()) - self.assertEqual((1, "2"), res) - - def test_wrap_upper_case(self): - checker = TupleFactory().create_from(Tuple[int, str], DummyDefaultCreationContext()) - res = checker.check_and_wrap((1, "2"), DummyExecutionContext()) - self.assertEqual((1, "2"), res) - - def test_not_a_tuple(self): - checker = TupleFactory().create_from(tuple[int, str], DummyDefaultCreationContext()) - - with self.assertRaises(UntypyTypeError) as cm: - res = checker.check_and_wrap(1, DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "tuple[int, str]") - self.assertEqual(i, "^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_negative(self): - checker = TupleFactory().create_from(tuple[int, str], DummyDefaultCreationContext()) - - with self.assertRaises(UntypyTypeError) as cm: - res = checker.check_and_wrap((1, 2), DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "tuple[int, str]") - self.assertEqual(i, " ^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_negative_delayed(self): - checker = TupleFactory().create_from(tuple[int, DummyDelayedType], DummyDefaultCreationContext()) - - res = checker.check_and_wrap((1, 2), DummyExecutionContext()) - with self.assertRaises(UntypyTypeError) as cm: - res[1].use() - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "tuple[int, DummyDelayedType]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_equiv_with_builtin_tuple(self): - self.check(str) - self.check(repr) - self.check(lambda l: (42, 5) + l) - self.check(lambda l: l + (42, 5)) - self.check(lambda l: 4 * l) - self.check(lambda l: l * 3) - self.check(lambda l: 2 in l) - self.check(lambda l: 42 in l) - self.check(lambda l: l[0]) - self.check(lambda l: l[-2]) - self.check(lambda l: l[1:2]) - def iter(l): - acc = [] - for x in l: - acc.append(l) - return acc - self.check(iter) - self.check(lambda l: len(l)) - self.check(lambda l: l.count(1)) - self.check(lambda l: sorted(l)) - self.check(lambda l: list(reversed(l))) - # == - self.check(lambda l: l == (1,4,2,1)) - self.check(lambda l: (1,4,2,1) == l) - self.check2(lambda l1, l2: l1 == l2) - self.check2(lambda l1, l2: l2 == l1) - # != - self.check(lambda l: l != (1,4,2,1)) - self.check(lambda l: (1,4,2,1) != l) - self.check2(lambda l1, l2: l1 != l2) - self.check2(lambda l1, l2: l2 != l1) - # < - self.check(lambda l: l < (1,4,2)) - self.check(lambda l: (1,4,1) < l) - self.check2(lambda l1, l2: l1 < l2) - self.check2(lambda l1, l2: l2 < l1) - # <= - self.check(lambda l: l <= (1,4,2)) - self.check(lambda l: (1,4,1) <= l) - self.check2(lambda l1, l2: l1 <= l2) - self.check2(lambda l1, l2: l2 <= l1) - # > - self.check(lambda l: l > (1,4,2)) - self.check(lambda l: (1,4,1) > l) - self.check2(lambda l1, l2: l1 > l2) - self.check2(lambda l1, l2: l2 > l1) - # >= - self.check(lambda l: l >= (1,4,2)) - self.check(lambda l: (1,4,1) >= l) - self.check2(lambda l1, l2: l1 >= l2) - self.check2(lambda l1, l2: l2 >= l1) - - def check(self, f): - l1 = (1, 4, 2, 1) - refRes = f(l1) - checker = untypy.checker(lambda ty=tuple[int,...]: ty, dummy_caller) - wrapped = checker(l1) - self.assertFalse(l1 is wrapped) - res = f(wrapped) - self.assertEqual(l1, wrapped) - self.assertEqual(refRes, res) - - def check2(self, f): - self.check21(f) - self.check22(f) - - def check21(self, f): - l1 = (1, 4, 2, 1) - refRes11 = f(l1, l1) - checker = untypy.checker(lambda ty=tuple[int,...]: ty, dummy_caller) - wrapped1 = checker(l1) - self.assertFalse(l1 is wrapped1) - res11 = f(wrapped1, wrapped1) - self.assertEqual(refRes11, res11) - - def check22(self, f): - l1 = (1, 4, 2, 1) - l2 = (1, 4, 1) - refRes12 = f(l1, l2) - checker = untypy.checker(lambda ty=tuple[int, ...]: ty, dummy_caller) - wrapped1 = checker(l1) - wrapped2 = checker(l2) - self.assertFalse(l1 is wrapped1) - self.assertFalse(l2 is wrapped2) - res12 = f(wrapped1, wrapped2) - self.assertEqual(refRes12, res12) diff --git a/python/deps/untypy/test/impl/test_union.py b/python/deps/untypy/test/impl/test_union.py deleted file mode 100644 index 18e68e70..00000000 --- a/python/deps/untypy/test/impl/test_union.py +++ /dev/null @@ -1,71 +0,0 @@ -import unittest -from typing import Union, Callable - -from test.util import DummyExecutionContext, DummyDefaultCreationContext -from untypy.error import UntypyTypeError, UntypyAttributeError -from untypy.impl.dummy_delayed import DummyDelayedType -from untypy.impl.union import UnionFactory -import untypy - -class TestUnion(unittest.TestCase): - - def test_wrap(self): - checker = UnionFactory().create_from(Union[int, str], DummyDefaultCreationContext()) - res = checker.check_and_wrap(1, DummyExecutionContext()) - self.assertEqual(1, res) - - res = checker.check_and_wrap("2", DummyExecutionContext()) - self.assertEqual("2", res) - - def test_wrap_negative(self): - checker = UnionFactory().create_from(Union[int, str], DummyDefaultCreationContext()) - with self.assertRaises(UntypyTypeError) as cm: - res = checker.check_and_wrap(23.5, DummyExecutionContext()) - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Union[int, str]") - self.assertEqual(i, "^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_wrap_negative_delayed(self): - checker = UnionFactory().create_from(Union[DummyDelayedType, str], DummyDefaultCreationContext()) - - res = checker.check_and_wrap(1, DummyExecutionContext()) - - with self.assertRaises(UntypyTypeError) as cm: - res.use() - - (t, i) = cm.exception.next_type_and_indicator() - i = i.rstrip() - - self.assertEqual(t, "Union[DummyDelayedType, str]") - self.assertEqual(i, " ^^^^^^^^^^^^^^^^") - - # This DummyExecutionContext is responsable - self.assertEqual(cm.exception.last_responsable().file, "dummy") - - def test_not_allowing_multiple_callables(self): - with self.assertRaises(UntypyAttributeError): - checker = UnionFactory().create_from(Union[int, str, Callable[[int], str], Callable[[str], str]], - DummyDefaultCreationContext()) - - with self.assertRaises(UntypyAttributeError): - checker = UnionFactory().create_from(Union[int, Callable[[int], str], - Union[Callable[[str], str], list[int]]], - DummyDefaultCreationContext()) - - def test_union(self): - @untypy.patch - def f(x: Union[str, float]) -> Union[str, float]: - return x - self.assertEqual(f(1), 1) - self.assertEqual(f(3.14), 3.14) - self.assertEqual(f("x"), "x") - with self.assertRaises(UntypyTypeError): - f(None) - with self.assertRaises(UntypyTypeError): - f([1]) diff --git a/python/deps/untypy/test/patching_dummy/a/__init__.py b/python/deps/untypy/test/patching_dummy/a/__init__.py deleted file mode 100644 index 369e1ccc..00000000 --- a/python/deps/untypy/test/patching_dummy/a/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .a_sub import fn_two - - -def fn_one(x: int) -> None: - pass diff --git a/python/deps/untypy/test/patching_dummy/a/a_sub.py b/python/deps/untypy/test/patching_dummy/a/a_sub.py deleted file mode 100644 index 2745d71a..00000000 --- a/python/deps/untypy/test/patching_dummy/a/a_sub.py +++ /dev/null @@ -1,2 +0,0 @@ -def fn_two(x: int) -> None: - pass diff --git a/python/deps/untypy/test/patching_dummy/argument_types.py b/python/deps/untypy/test/patching_dummy/argument_types.py deleted file mode 100644 index da2f8106..00000000 --- a/python/deps/untypy/test/patching_dummy/argument_types.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Tuple - - -def kwargs(a: int, b: str, c: bool) -> Tuple[bool, int, str]: - return c, a, b - - -def default_args(a: int, b: str = "hello") -> str: - return b diff --git a/python/deps/untypy/test/patching_dummy/async_gen.py b/python/deps/untypy/test/patching_dummy/async_gen.py deleted file mode 100644 index 752ce8f0..00000000 --- a/python/deps/untypy/test/patching_dummy/async_gen.py +++ /dev/null @@ -1,16 +0,0 @@ -import asyncio - - -async def some_coroutine_generator(x: int) -> str: - await asyncio.sleep(0.0000001) - yield "1" - yield "2" - yield "3" - - -async def some_coroutine(x: int) -> str: - await asyncio.sleep(0.0000001) - return str(x) - - -def some_generator() -> int diff --git a/python/deps/untypy/test/patching_dummy/b/__init__.py b/python/deps/untypy/test/patching_dummy/b/__init__.py deleted file mode 100644 index 26da4915..00000000 --- a/python/deps/untypy/test/patching_dummy/b/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .b_sub import fn_two - - -def fn_one(x: int) -> None: - pass diff --git a/python/deps/untypy/test/patching_dummy/b/b_sub.py b/python/deps/untypy/test/patching_dummy/b/b_sub.py deleted file mode 100644 index 2745d71a..00000000 --- a/python/deps/untypy/test/patching_dummy/b/b_sub.py +++ /dev/null @@ -1,2 +0,0 @@ -def fn_two(x: int) -> None: - pass diff --git a/python/deps/untypy/test/patching_dummy/c.py b/python/deps/untypy/test/patching_dummy/c.py deleted file mode 100644 index 80f56399..00000000 --- a/python/deps/untypy/test/patching_dummy/c.py +++ /dev/null @@ -1,5 +0,0 @@ -def fn_one(x: int) -> None: - pass - - -untypy.enable() diff --git a/python/deps/untypy/test/patching_dummy/patching_classes.py b/python/deps/untypy/test/patching_dummy/patching_classes.py deleted file mode 100644 index a088ba5f..00000000 --- a/python/deps/untypy/test/patching_dummy/patching_classes.py +++ /dev/null @@ -1,6 +0,0 @@ -class A: - def __init__(self, y: int): - self.y = y - - def add(self, x: int) -> int: - return x + self.y diff --git a/python/deps/untypy/test/patching_dummy/patching_does_not_change_signature.py b/python/deps/untypy/test/patching_dummy/patching_does_not_change_signature.py deleted file mode 100644 index 5ba12547..00000000 --- a/python/deps/untypy/test/patching_dummy/patching_does_not_change_signature.py +++ /dev/null @@ -1,7 +0,0 @@ -def fun(x: int) -> str: - return str(x) - - -class SomeClass: - def meth(self, x: int) -> str: - return str(x) diff --git a/python/deps/untypy/test/patching_dummy/unpatched.py b/python/deps/untypy/test/patching_dummy/unpatched.py deleted file mode 100644 index 582d5317..00000000 --- a/python/deps/untypy/test/patching_dummy/unpatched.py +++ /dev/null @@ -1,2 +0,0 @@ -def fn_one(x: int) -> None: - pass diff --git a/python/deps/untypy/test/test_patching.py b/python/deps/untypy/test/test_patching.py deleted file mode 100644 index b1136e2d..00000000 --- a/python/deps/untypy/test/test_patching.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import unittest - -import untypy - - -@untypy.patch -class C: - @untypy.patch - def foo(self: C, d: D) -> C: - return C() - - -@untypy.patch -class D: - pass - - -class TestRecursion(unittest.TestCase): - - def test_recursion(self): - # should not fail - C().foo(D()) diff --git a/python/deps/untypy/test/test_standalone_checker.py b/python/deps/untypy/test/test_standalone_checker.py deleted file mode 100644 index 66190e8c..00000000 --- a/python/deps/untypy/test/test_standalone_checker.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest -import untypy -from untypy.error import UntypyTypeError, Location - - -class TestStandaloneChecker(unittest.TestCase): - - def test_standalone(self): - ch = untypy.checker(lambda: int, TestStandaloneChecker.test_standalone) - - self.assertEqual(ch(42), 42) - - def myfunc(x): - ch(x) - - with self.assertRaises(UntypyTypeError) as cm: - myfunc("hello") - - self.assertEqual(cm.exception.expected, 'int') - self.assertEqual(cm.exception.last_declared(), Location.from_code(TestStandaloneChecker.test_standalone)) - self.assertIn('myfunc("hello")', cm.exception.last_responsable().source_lines) \ No newline at end of file diff --git a/python/deps/untypy/test/util.py b/python/deps/untypy/test/util.py deleted file mode 100644 index 6e20f8f2..00000000 --- a/python/deps/untypy/test/util.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import TypeVar, Any - -from untypy.error import UntypyTypeError, Frame, Location -from untypy.impl import DefaultCreationContext -from untypy.interfaces import ExecutionContext, WrappedFunction - - -class DummyExecutionContext(ExecutionContext): - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - (t, i) = err.next_type_and_indicator() - - return err.with_frame(Frame( - t, - i, - declared=None, - responsable=Location( - file="dummy", - line_no=0, - line_span=1 - ) - )) - - -class DummyDefaultCreationContext(DefaultCreationContext): - - def __init__(self, typevars: dict[TypeVar, Any] = dict()): - super().__init__(typevars.copy(), Location( - file="dummy", - line_no=0, - line_span=1 - ), checkedpkgprefixes=["test"]) - - -def location_of(fn): - return WrappedFunction.find_location(fn) diff --git a/python/deps/untypy/test/util_test/test_return_traces.py b/python/deps/untypy/test/util_test/test_return_traces.py deleted file mode 100644 index 5c9e3337..00000000 --- a/python/deps/untypy/test/util_test/test_return_traces.py +++ /dev/null @@ -1,36 +0,0 @@ -import ast -import unittest - -from untypy.util.return_traces import ReturnTraceManager, ReturnTracesTransformer - - -class TestAstTransform(unittest.TestCase): - - def test_ast_transform(self): - src = """ -def foo(flag: bool) -> int: - print('Hello World') - if flag: - return 1 - else: - return 'you stupid' - """ - target = """ -def foo(flag: bool) -> int: - print('Hello World') - if flag: - untypy._before_return(0) - return 1 - else: - untypy._before_return(1) - return 'you stupid' - """ - - tree = ast.parse(src) - mgr = ReturnTraceManager() - ReturnTracesTransformer("", mgr).visit(tree) - ast.fix_missing_locations(tree) - self.assertEqual(ast.unparse(tree).strip(), target.strip()) - self.assertEqual(mgr.get(0), ("", 5)) - self.assertEqual(mgr.get(1), ("", 7)) - diff --git a/python/deps/untypy/test/util_test/test_wrapper.py b/python/deps/untypy/test/util_test/test_wrapper.py deleted file mode 100644 index 9d3d921d..00000000 --- a/python/deps/untypy/test/util_test/test_wrapper.py +++ /dev/null @@ -1,170 +0,0 @@ -from untypy.util.wrapper import wrap -import unittest -import collections - -class C: - def __init__(self, content): - self.content = content - def __eq__(self, other): - if not isinstance(other, C): - return False - else: - return self.content == other.content - def __repr__(self): - return f'C({self.content})' - def __str__(self): - return self.__repr__() - def foo(self): - return 1 - -class WrapperTests(unittest.TestCase): - def test_wrapObject(self): - c1 = C(42) - c2 = wrap(C(42), {'foo': lambda self: 0}) - self.assertEqual(str(c2), 'C(42)') - self.assertTrue(c1 == c2) - self.assertTrue(c2 == c1) - self.assertTrue(isinstance(c2, C)) - self.assertEqual(c2.foo(), 0) - self.assertEqual(c1.foo(), 1) - - def test_wrapList(self): - l = wrap([1,2,3], {'__str__': lambda self: 'XXX'}) - self.assertEqual(repr(l), '[1, 2, 3]') - self.assertEqual(str(l), 'XXX') - self.assertTrue(isinstance(l, list)) - acc = [] - for x in l: - acc.append(x+1) - self.assertEqual(acc, [2,3,4]) - self.assertEqual([0] + l, [0,1,2,3]) - self.assertEqual(l + [4], [1,2,3,4]) - self.assertTrue(l == [1,2,3]) - self.assertTrue([1,2,3] == l) - self.assertEqual(3, len(l)) - l.append(4) - self.assertEqual(4, len(l)) - self.assertEqual([1,2,3,4], l) - f = l.append - flag = False - def myAppend(x): - nonlocal flag - flag = True - f(x) - setattr(l, 'append', myAppend) - l.append(5) - self.assertEqual(5, len(l)) - self.assertEqual([1,2,3,4,5], l) - self.assertTrue(flag) - - def test_wrapList2(self): - # This is what happens in the protocol checker: the function is called on the original - # value - orig = [1,2,3] - l = wrap(orig, {'append': lambda self, x: orig.append(x)}) - self.assertTrue([1,2,3] == l) - self.assertEqual(3, len(l)) - l.append(4) - self.assertEqual(4, len(l)) - self.assertEqual([1,2,3,4], l) - - def _test_api_complete(self, obj, ignore=[]): - wrapped = wrap(obj, {}) - expectedModule = 'untypy.util.wrapper' - blacklist = ['__class__', '__delattr__', '__class_getitem__', '__dict__', '__dir__', - '__doc__', '__extra__', '__format__', '__getattribute__', '__init__', - '__init_subclass__', '__module__', '__setattr__', '__subclasshook__', - '__weakref__', '__wrapped__', '_DictWrapper__marker', '__setstate__', - '__getstate__', '__firstlineno__', '__static_attributes__' - ] + ignore - for x in dir(obj): - if x in blacklist: - continue - a = getattr(wrapped, x) - if not hasattr(a, '__module__'): - self.fail(f'Attribute {x} not defined. obj={obj}, a={a}, wrapped={wrapped}') - elif a.__module__ != expectedModule: - self.fail(f'Attrribute {x} not defined in {expectedModule}') - - def test_list_api_complete(self): - self._test_api_complete([]) - - def test_set_api_complete(self): - self._test_api_complete(set()) - - def test_dict_api_complete(self): - self._test_api_complete({}, ignore=['fromkeys']) - - def test_wrapTuple(self): - l = wrap((1,2,3), {'__str__': lambda self: 'XXX'}) - self.assertEqual(repr(l), '(1, 2, 3)') - self.assertEqual(str(l), 'XXX') - self.assertTrue(isinstance(l, tuple)) - acc = [] - for x in l: - acc.append(x+1) - self.assertEqual(acc, [2,3,4]) - self.assertEqual((0,) + l, (0,1,2,3)) - self.assertEqual(l + (4,), (1,2,3,4)) - self.assertTrue(l == (1,2,3)) - self.assertTrue((1,2,3) == l) - - def test_wrapString(self): - l = wrap("123", {'__str__': lambda self: 'XXX'}) - self.assertEqual(repr(l), "'123'") - self.assertEqual(str(l), 'XXX') - self.assertTrue(isinstance(l, str)) - acc = [] - for x in l: - acc.append(x) - self.assertEqual(acc, ['1', '2', '3']) - self.assertEqual('0' + l, '0123') - self.assertEqual(l + '4', '1234') - self.assertTrue(l == "123") - self.assertTrue("123" == l) - - def test_wrapDict(self): - d = wrap({'1': 1, '2': 2, '3': 3}, {'__str__': lambda self: 'YYY', 'foo': lambda self: 11}) - self.assertEqual(repr(d), "{'1': 1, '2': 2, '3': 3}") - self.assertEqual(str(d), 'YYY') - self.assertTrue(isinstance(d, dict)) - acc = [] - for x in d: - acc.append(x) - self.assertEqual(acc, ['1', '2', '3']) - self.assertEqual(len(d), 3) - self.assertTrue(d == {'1': 1, '2': 2, '3': 3}) - self.assertTrue({'1': 1, '2': 2, '3': 3} == d) - self.assertEqual(d.foo(), 11) - self.assertEqual(d.copy(), {'1': 1, '2': 2, '3': 3}) - - def test_wrapViews(self): - d = {'1': 1, '2': 2} - # keys - self.assertTrue(isinstance(d.keys(), collections.abc.KeysView)) - kv = wrap(d.keys(), {}) - self.assertTrue(isinstance(kv, collections.abc.KeysView)) - acc = [] - for x in kv: - acc.append(x) - self.assertEqual(['1','2'], acc) - # items - iv = wrap(d.items(), {}) - self.assertTrue(isinstance(iv, collections.abc.ItemsView)) - acc = [] - for x in iv: - acc.append(x) - self.assertEqual([('1', 1),('2', 2)], acc) - # values - vv = wrap(d.values(), {}) - self.assertTrue(isinstance(vv, collections.abc.ValuesView)) - acc = [] - for x in vv: - acc.append(x) - self.assertEqual([1,2], acc) - - def test_name(self): - l = wrap([1,2,3], {}, "NameOfWrapper") - self.assertEqual(str(type(l)), "") - c = wrap(C(1), {}, "blub") - self.assertEqual(str(type(c)), "") diff --git a/python/deps/untypy/test/util_test/untypy_test_case.py b/python/deps/untypy/test/util_test/untypy_test_case.py deleted file mode 100644 index 52228f37..00000000 --- a/python/deps/untypy/test/util_test/untypy_test_case.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest - -import untypy -from untypy.error import Location - - -def dummy_caller_2(ch, arg): - return ch(arg) - - -def dummy_caller(typ, arg): - """ - Sets up callstack so that this function is blamed for creation. - untypy.checker uses the function that called the function that - invokes the checker. - """ - ch = untypy.checker(lambda ty=typ: ty, dummy_caller) - return dummy_caller_2(ch, arg) - - -class UntypyTestCase(unittest.TestCase): - - def assertBlame(self, cm, blamed): - ok = cm.exception.last_responsable() in Location.from_code(blamed) - if not ok: - print(cm.exception.last_responsable()) - print("not in") - print(Location.from_code(blamed)) - self.assertTrue(ok) - diff --git a/python/deps/untypy/untypy/__init__.py b/python/deps/untypy/untypy/__init__.py deleted file mode 100644 index 5f451216..00000000 --- a/python/deps/untypy/untypy/__init__.py +++ /dev/null @@ -1,187 +0,0 @@ -import ast -import inspect -import sys -from types import ModuleType -from typing import Optional, Any, Union, Callable - -from .patching import wrap_function, patch_class, wrap_class, DefaultConfig -from .patching.ast_transformer import UntypyAstTransformer, did_no_code_run_before_untypy_enable, \ - UntypyAstImportTransformer -from .patching.import_hook import install_import_hook -from .patching.standalone_checker import StandaloneChecker -from .util import condition -from .util.return_traces import ReturnTracesTransformer, before_return, GlobalReturnTraceManager -from .util.tranformer_combinator import TransformerCombinator -from .util import debug - -GlobalConfig = DefaultConfig - -""" -This function is called before any return statement, to store which was the last return. -For this the AST is transformed using ReturnTracesTransformer. -Must be in untypy so it can be used in transformed module. -Must also be in other module, so it can be used from inside (No circular imports). -""" -_before_return = before_return - -_importhook_transformer_builder = lambda path, file: TransformerCombinator(UntypyAstTransformer(), - ReturnTracesTransformer(file)) - -def just_install_hook(prefixes=[]): - global GlobalConfig - - def predicate(module_name): - for p in prefixes: - if module_name == p: - return True - elif module_name.startswith(p + "."): - return True - return False - - GlobalConfig = DefaultConfig._replace(checkedprefixes=[*prefixes]) - install_import_hook(predicate, _importhook_transformer_builder) - - -def transform_tree(tree, file): - UntypyAstTransformer().visit(tree) - ReturnTracesTransformer(file).visit(tree) - ast.fix_missing_locations(tree) - - -def enable(*, recursive: bool = True, root: Union[ModuleType, str, None] = None, prefixes: list[str] = []) -> None: - global GlobalConfig - caller = _find_calling_module() - exit_after = False - - if root is None: - root = caller - exit_after = True - if caller is None: - raise Exception("Couldn't find loading Module. This is a Bug.") - - rootname = root - if hasattr(rootname, '__name__'): - rootname = root.__name__ - - GlobalConfig = DefaultConfig._replace(checkedprefixes=[rootname]) - - def predicate(module_name): - if recursive: - # Patch if Submodule - if module_name.startswith(rootname + "."): - return True - else: - for p in prefixes: - if module_name == p: - return True - elif module_name.startswith(p + "."): - return True - return False - else: - raise AssertionError("You cannot run 'untypy.enable()' twice!") - - transformer = _importhook_transformer_builder - install_import_hook(predicate, transformer) - _exec_module_patched(root, exit_after, transformer(caller.__name__.split("."), caller.__file__)) - - -def enable_on_imports(*prefixes): - print("!!!! THIS FEATURE HAS UNEXPECTED SIDE EFFECTS WHEN IMPORTING ABSOLUTE SUBMODULES !!!") - # TODO: Fix import of submodules should not change parent module. - global GlobalConfig - GlobalConfig = DefaultConfig._replace(checkedprefixes=[*prefixes]) - caller = _find_calling_module() - - def predicate(module_name: str): - module_name = module_name.replace('__main__.', '') - for p in prefixes: - if module_name == p: - return True - elif module_name.startswith(p + "."): - return True - else: - return False - - transformer = _importhook_transformer_builder - install_import_hook(predicate, transformer) - _exec_module_patched(caller, True, transformer(caller.__name__.split("."), caller.__file__)) - - -def _exec_module_patched(mod: ModuleType, exit_after: bool, transformer: ast.NodeTransformer): - source = inspect.getsource(mod) - tree = compile(source, mod.__file__, 'exec', ast.PyCF_ONLY_AST, - dont_inherit=True, optimize=-1) - - if not did_no_code_run_before_untypy_enable(tree): - raise AssertionError("Please put 'untypy.enable()' at the start of your module like so:\n" - "\timport untypy\n" - "\tuntypy.enable()") - - transformer.visit(tree) - ReturnTracesTransformer(mod.__file__).visit(tree) - ast.fix_missing_locations(tree) - patched_mod = compile(tree, mod.__file__, 'exec', dont_inherit=True, optimize=-1) - stack = list(map(lambda s: s.frame, inspect.stack())) - try: - exec(patched_mod, mod.__dict__) - except Exception as e: - while e.__traceback__.tb_frame in stack: - e.__traceback__ = e.__traceback__.tb_next - sys.excepthook(type(e), e, e.__traceback__) - if exit_after: sys.exit(-1) - if exit_after: sys.exit(0) - - -def _find_calling_module() -> Optional[ModuleType]: - for caller in inspect.stack(): - if caller.filename != __file__: - mod = inspect.getmodule(caller.frame) - if mod is not None: - return mod - return None - -precondition = condition.precondition -postcondition = condition.postcondition - -def unchecked(fn): - setattr(fn, "__unchecked", True) - return fn - - -def patch(a: Any) -> Any: - global GlobalConfig - if hasattr(a, '__unchecked'): - return a - - if inspect.isfunction(a): - return wrap_function(a, GlobalConfig) - elif inspect.isclass(a): - patch_class(a, GlobalConfig) - return a - else: - return a - - -typechecked = patch - - -def wrap_import(a: Any) -> Any: - global GlobalConfig - if inspect.isfunction(a): - return wrap_function(a, GlobalConfig) - elif inspect.isclass(a) or inspect.ismodule(a): - return wrap_class(a, GlobalConfig) - else: - return a - - -def checker(annotation: Callable[[], Any], location: Any, ctx=None) -> Callable[[Any], Any]: - """ - :param annotation: A functions that returns the annotation lazily. (Needed for FowardRefs) - :param location: Where was it declared? - :return: A type checker function - """ - return StandaloneChecker(annotation, location, DefaultConfig, ctx) - -def enableDebug(d: bool): - debug.enableDebug(d) diff --git a/python/deps/untypy/untypy/error.py b/python/deps/untypy/untypy/error.py deleted file mode 100644 index 0ffeda72..00000000 --- a/python/deps/untypy/untypy/error.py +++ /dev/null @@ -1,384 +0,0 @@ -from __future__ import annotations - -import inspect -from enum import Enum -import os -from typing import Any, Optional, Tuple, Iterable - - -def readFile(path): - try: - with open(path, encoding='utf-8') as f: - return f.read() - except UnicodeDecodeError: - with open(path) as f: - return f.read() - -def relpath(p): - # relpath might throw an exception, at least on windows - try: - return os.path.relpath(p) - except: - return p - -class Location: - file: str - line_no: int - line_span : int - source_lines: Optional[str] - - def __init__(self, file: str, line_no: int, line_span : int): - self.file = file - self.line_no = line_no - self.line_span = line_span - self.source_lines = None - - def source(self) -> Optional[str]: - if self.source_lines is None: - try: - self.source_lines = readFile(self.file) - except OSError: - self.source_lines = '' - return self.source_lines - - def source_lines_span(self) -> Optional[str]: - # This is still used for unit testing - - source = self.source() - if source is None: - return None - - buf = "" - - for i, line in enumerate(source.splitlines()): - if (i + 1) in range(self.line_no, self.line_no + self.line_span): - buf += f"\n{line}" - - return buf - - def __str__(self): - return f"{relpath(self.file)}:{self.line_no}" - - def formatWithCode(self): - buf = str(self) - source = self.source() - if not source: - return buf - lines = source.splitlines() - idx = self.line_no - 1 - if idx < 0 or idx > len(lines): - return buf - else: - return buf + '\n | ' + lines[idx] - - def __repr__(self): - return f"Location(file={self.file.__repr__()}, line_no={self.line_no.__repr__()}, line_span={self.line_span})" - - def __eq__(self, other): - if not isinstance(other, Location): - return False - return self.file == other.file and self.line_no == other.line_no - - @staticmethod - def from_code(obj) -> Location: - try: - return Location( - file=inspect.getfile(obj), - line_no=inspect.getsourcelines(obj)[1], - line_span=len(inspect.getsourcelines(obj)[0]), - ) - except Exception: - return Location( - file=inspect.getfile(obj), - line_no=1, - line_span=1, - ) - - @staticmethod - def from_stack(stack) -> Location: - if isinstance(stack, inspect.FrameInfo): - try: - return Location( - file=stack.filename, - line_no=stack.lineno, - line_span=1 - ) - except Exception: - return Location( - file=stack.filename, - line_no=stack.lineno, - line_span=1 - ) - else: # assume sys._getframe(...) - try: - return Location( - file=stack.f_code.co_filename, - line_no=stack.f_lineno, - line_span=1 - ) - except Exception: - return Location( - file=stack.f_code.co_filename, - line_no=stack.f_lineno, - line_span=1 - ) - - def __contains__(self, other: Location): - file, line = (other.file, other.line_no) - return self.file == file and line in range(self.line_no, self.line_no + self.line_span) - - def narrow_in_span(self, reti_loc: Tuple[str, int]): - """ - Use new Location if inside of span of this Location - :param reti_loc: filename and line_no - :return: a new Location, else self - """ - file, line = reti_loc - if self.file == file and line in range(self.line_no, self.line_no + self.line_span): - return Location( - file=file, - line_no=line, - line_span=1 - ) - else: - return self - - -class Frame: - type_declared: str - indicator_line: str - - declared: Optional[Location] - responsable: Optional[Location] - - responsibility_type: Optional[ResponsibilityType] - - def __init__(self, type_declared: str, indicator_line: Optional[str], - declared: Optional[Location], responsable: Optional[Location]): - - self.type_declared = type_declared - if indicator_line is None: - indicator_line = '^' * len(type_declared) - self.indicator_line = indicator_line - self.declared = declared - self.responsable = responsable - - def __repr__(self): - return f'Frame({self.type_declared}, {self.declared}, {self.responsable}, {self.responsibility_type})' - - def __str__(self): - buf = f"in: {self.type_declared}\n" \ - f" {self.indicator_line}\n" - - if self.responsable is not None: - buf += f"{self.responsable.file}:{self.responsable.line_no}:\n" \ - f"{self.responsable.source_lines}\n" \ - f"\n" - return buf - - -class ResponsibilityType(Enum): - IN = 0 - OUT = 1 - - def invert(self): - if self is ResponsibilityType.IN: - return ResponsibilityType.OUT - else: - return ResponsibilityType.IN - -def joinLines(l: Iterable[str]) -> str: - return '\n'.join([x.rstrip() for x in l]) - -# Note: the visual studio code plugin uses the prefixes "caused by: " and "declared at: " -# for finding source locations. Do not change without changing the plugin code!! -CAUSED_BY_PREFIX = "caused by: " -DECLARED_AT_PREFIX = "declared at: " - -def formatLocations(prefix: str, locs: list[Location]) -> str: - return joinLines(map(lambda s: prefix + str(s), locs)) - -# DeliberateError instances are not reported as bugs -class DeliberateError: - pass - -class WyppTypeError(TypeError, DeliberateError): - pass - -class WyppAttributeError(AttributeError, DeliberateError): - pass - -# All error types must be subclasses from UntypyError. -class UntypyError: - def simpleName(self): - raise Exception('abstract method') - -NO_GIVEN = object() - -class UntypyTypeError(TypeError, UntypyError): - given: Any # NO_GIVEN if not present - header: str - expected: Optional[str] - frames: list[Frame] - notes: list[str] - previous_chain: Optional[UntypyTypeError] - responsibility_type: ResponsibilityType - - def __init__(self, - given: Any = NO_GIVEN, - expected: str = None, - frames: list[Frame] = [], - notes: list[str] = [], - previous_chain: Optional[UntypyTypeError] = None, - responsibility_type: ResponsibilityType = ResponsibilityType.IN, - header: str = ''): - self.responsibility_type = responsibility_type - self.given = given - self.expected = expected - self.frames = frames.copy() - for frame in self.frames: - if frame.responsibility_type is None: - frame.responsibility_type = responsibility_type - self.notes = notes.copy() - self.previous_chain = previous_chain - self.header = header - super().__init__('\n' + self.__str__()) - - def simpleName(self): - return 'TypeError' - - def next_type_and_indicator(self) -> Tuple[str, str]: - if len(self.frames) >= 1: - frame = self.frames[-1] - return frame.type_declared, frame.indicator_line - else: - n = 0 - if self.expected: - n = len(self.expected) - return self.expected, "^" * n - - def with_frame(self, frame: Frame) -> UntypyTypeError: - frame.responsibility_type = self.responsibility_type - return UntypyTypeError(self.given, self.expected, self.frames + [frame], - self.notes, self.previous_chain, self.responsibility_type, - self.header) - - def with_previous_chain(self, previous_chain: UntypyTypeError): - return UntypyTypeError(self.given, self.expected, self.frames, - self.notes, previous_chain, self.responsibility_type, self.header) - - def with_note(self, note: str): - return UntypyTypeError(self.given, self.expected, self.frames, - self.notes + [note], self.previous_chain, self.responsibility_type, - self.header) - - def with_inverted_responsibility_type(self): - return UntypyTypeError(self.given, self.expected, self.frames, - self.notes, self.previous_chain, self.responsibility_type.invert(), - self.header) - - def with_header(self, header: str): - return UntypyTypeError(self.given, self.expected, self.frames, - self.notes, self.previous_chain, self.responsibility_type, header) - - def last_responsable(self): - for f in reversed(self.frames): - if f.responsable is not None and f.responsibility_type is ResponsibilityType.IN: - return f.responsable - return None - - def last_declared(self): - for f in reversed(self.frames): - if f.declared is not None: - return f.declared - return None - - def __str__(self): - declared_locs = [] - responsable_locs = [] - - for f in self.frames: - if f.responsable is not None and f.responsibility_type is ResponsibilityType.IN: - s = f.responsable.formatWithCode() - if s not in responsable_locs: - responsable_locs.append(s) - if f.declared is not None: - s = str(f.declared) - if s not in declared_locs: - declared_locs.append(str(f.declared)) - - cause = formatLocations(CAUSED_BY_PREFIX, responsable_locs) - declared = formatLocations(DECLARED_AT_PREFIX, declared_locs) - - (ty, ind) = self.next_type_and_indicator() - - notes = joinLines(self.notes) - if notes: - notes = notes + "\n" - - if self.previous_chain is None: - previous_chain = "" - preHeader = self.header or 'got value of wrong type' - postHeader = '' - else: - previous_chain = self.previous_chain.__str__().strip() - preHeader = '' - postHeader = self.header - if postHeader: - postHeader = postHeader + "\n" - if preHeader: - preHeader = preHeader + "\n" - if previous_chain: - previous_chain = previous_chain + "\n\n" - - ctx = "" - if self.expected != ty: - ctx = f"context: {ty.rstrip()}" - ind = ind.rstrip() - if ind: - ctx = f"{ctx}\n {ind}" - if ctx: - ctx = ctx + "\n" - given = None if self.given is NO_GIVEN else repr(self.given) - expected = None if self.expected is None else self.expected.strip() - if expected is not None and expected != 'None': - expected = f'value of type {expected}' - if given is not None: - given = f"given: {given.rstrip()}\n" - else: - given = "" - if expected is not None: - expected = f"expected: {expected}\n" - else: - expected = "" - if notes and (given or expected): - notes += "\n" - return (f"""{preHeader}{previous_chain}{postHeader}{notes}{given}{expected} -{ctx}{declared} -{cause}""") - - -class UntypyAttributeError(AttributeError, UntypyError): - - def __init__(self, message: str, locations: list[Location] = []): - self.message = message - self.locations = locations.copy() - super().__init__(self.__str__()) - - def simpleName(self): - return 'AttributeError' - - def with_location(self, loc: Location) -> UntypyAttributeError: - return type(self)(self.message, self.locations + [loc]) # preserve type - - def __str__(self): - return f"{self.message}\n{formatLocations(DECLARED_AT_PREFIX, self.locations)}" - - -class UntypyNameError(UntypyAttributeError, UntypyError): - def simpleName(self): - return 'NameError' - -class UntypyAnnotationError(UntypyAttributeError, UntypyError): - def simpleName(self): - return 'AnnotationError' diff --git a/python/deps/untypy/untypy/impl/__init__.py b/python/deps/untypy/untypy/impl/__init__.py deleted file mode 100644 index d147d692..00000000 --- a/python/deps/untypy/untypy/impl/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -import inspect -from typing import Any, Optional, TypeVar, List, Dict -import typing - -from untypy.interfaces import CreationContext, TypeChecker, ExecutionContext -from .alias import TypeAliasTypeFactory -from .annotated import AnnotatedFactory -from .any import AnyFactory -from .callable import CallableFactory -from .dummy_delayed import DummyDelayedFactory -from .generator import GeneratorFactory -from .generic import GenericFactory -from .interface import InterfaceFactory -from .choice import ChoiceFactory -from .literal import LiteralFactory -from .none import NoneFactory -from .optional import OptionalFactory -from .protocol import ProtocolFactory -from .simple import SimpleFactory -from .string_forward_refs import StringForwardRefFactory -from .tuple import TupleFactory -from .union import UnionFactory -from ..error import Location, UntypyAttributeError, UntypyAnnotationError -from ..util.debug import debug - -# More Specific Ones First -_FactoryList = [ - AnyFactory(), - NoneFactory(), - DummyDelayedFactory(), - AnnotatedFactory(), - TypeAliasTypeFactory(), - ProtocolFactory(), # must be higher then Generic - GenericFactory(), - CallableFactory(), - LiteralFactory(), - OptionalFactory(), # must be higher then Union - UnionFactory(), - TupleFactory(), - GeneratorFactory(), - InterfaceFactory(), - ChoiceFactory(), - StringForwardRefFactory(), # resolve types passed as strings - # must come last - SimpleFactory() -] - -def isAnnotationValid(annot: Any): - try: - hash(annot) - except TypeError: - return False - return True - -class DefaultCreationContext(CreationContext): - - def __init__(self, typevars: Dict[TypeVar, Any], declared_location: Location, - checkedpkgprefixes: List[str], eval_context: Optional[Any] = None): - self.typevars = typevars - self.declared = declared_location - self.checkedpkgprefixes = checkedpkgprefixes - self._eval_context = eval_context - - def declared_location(self) -> Location: - return self.declared - - def find_checker(self, annotation: Any) -> Optional[TypeChecker]: - if not isAnnotationValid(annotation): - raise UntypyAnnotationError(message="Invalid type annotation: " + str(annotation)) - for fac in _FactoryList: - res = fac.create_from(annotation=annotation, ctx=self) - if res is not None: - debug(f'Created checker for {annotation} from factory {fac}') - return res - return None - - def wrap(self, err: UntypyAttributeError) -> UntypyAttributeError: - return err.with_location(self.declared) - - def resolve_typevar(self, var: TypeVar) -> typing.Tuple[bool, Any]: - # Not result may be None - if var in self.typevars: - return True, self.typevars[var] - else: - return False, None - - def all_typevars(self) -> List[TypeVar]: - return list(self.typevars.keys()) - - def with_typevars(self, typevars: Dict[TypeVar, Any]) -> CreationContext: - tv = self.typevars.copy() - tv.update(typevars) - return DefaultCreationContext(tv, self.declared, self.checkedpkgprefixes, self._eval_context) - - def should_be_inheritance_checked(self, annotation: type) -> bool: - m = inspect.getmodule(annotation) - - for pkgs in self.checkedpkgprefixes: - # Inheritance should be checked on types - # when the type's module or its parent lies in the "user code". - # Inheritance of types of extern modules should be not be checked. - if m.__name__ == pkgs or m.__name__.startswith(pkgs + "."): - return True - - return False - - def eval_context(self): - return self._eval_context diff --git a/python/deps/untypy/untypy/impl/alias.py b/python/deps/untypy/untypy/impl/alias.py deleted file mode 100644 index 559a4c85..00000000 --- a/python/deps/untypy/untypy/impl/alias.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any, Optional -import typing - -from untypy.interfaces import TypeChecker, CreationContext, TypeCheckerFactory - -class TypeAliasTypeFactory(TypeCheckerFactory): - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - # TypeAliasType was introduced in python 3.12 - if hasattr(typing, 'TypeAliasType'): - if isinstance(annotation, typing.TypeAliasType): - return ctx.find_checker(annotation.__value__) - return None diff --git a/python/deps/untypy/untypy/impl/annotated.py b/python/deps/untypy/untypy/impl/annotated.py deleted file mode 100644 index f9119228..00000000 --- a/python/deps/untypy/untypy/impl/annotated.py +++ /dev/null @@ -1,137 +0,0 @@ -from typing import Optional, Any, Iterator - -from untypy.error import UntypyTypeError, UntypyAttributeError, Frame -from untypy.interfaces import TypeCheckerFactory, TypeChecker, ExecutionContext, CreationContext - - -class AnnotatedChecker: - def check(self, arg: Any, ctx: ExecutionContext) -> None: - pass - - -class AnnotatedCheckerCallable(AnnotatedChecker): - def __init__(self, annotated, callable): - self.callable = callable - self.annotated = annotated - - def check(self, arg: Any, ctx: ExecutionContext) -> None: - res = self.callable(arg) - if not res: - exp = self.annotated.describe() - # raise error on falsy value - err = UntypyTypeError( - given=arg, - expected=exp - ) - if self.annotated.is_anonymous: - err = err.with_note(f"condition in {exp} does not hold") - (t, i) = err.next_type_and_indicator() - for info in self.annotated.info: - err = err.with_note(" - " + info) - raise ctx.wrap(err) - - -class AnnotatedCheckerContainer(AnnotatedChecker): - def __init__(self, annotated, cont): - self.cont = cont - self.annotated = annotated - - def check(self, arg: Any, ctx: ExecutionContext) -> None: - if arg not in self.cont: - # raise error on falsy value - err = UntypyTypeError( - given=arg, - expected=self.annotated.describe() - ) - if self.annotated.is_anonymous: - err = err.with_note(f"{repr(arg)} is not in {repr(self.cont)}.") - - for info in self.annotated.info: - err = err.with_note(" - " + info) - - raise ctx.wrap(err) - - -class AnnotatedFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if hasattr(annotation, '__metadata__') and hasattr(annotation, '__origin__'): - inner = ctx.find_checker(annotation.__origin__) - if inner is None: - raise ctx.wrap(UntypyAttributeError(f"Could not resolve annotation " - f"'{repr(annotation.__origin__)}' " - f"inside of '{annotation}'.")) - - return AnnotatedChecker(annotation, inner, annotation.__metadata__, ctx) - else: - return None - - -class AnnotatedChecker(TypeChecker): - def __init__(self, annotated, inner: TypeChecker, metadata: Iterator, ctx: CreationContext): - self.annotated = annotated - self.inner = inner - - meta = [] - info = [] - for m in metadata: - if callable(m): - meta.append(AnnotatedCheckerCallable(self, m)) - elif isinstance(m, str): - info.append(m) - elif hasattr(m, '__contains__'): - meta.append(AnnotatedCheckerContainer(self, m)) - else: - raise ctx.wrap(UntypyAttributeError(f"Unsupported metadata '{repr(m)}' in '{repr(self.annotated)}'.\n" - f"Only callables or objects providing __contains__ are allowed.")) - self.meta = meta - self.info = info - if len(info) == 1: - self.name = info[0] - self.info = [] - else: - self.name = None - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - wrapped = self.inner.check_and_wrap(arg, AnnotatedCheckerExecutionContext(self, ctx)) - for ck in self.meta: - ck.check(wrapped, ctx) - return wrapped - - def describe(self) -> str: - if self.name: - return self.name - elif len(self.info) > 0: - text = ", ".join(map(lambda a: f"'{a}'", self.info)) - return f"Annotated[{text}]" - else: - return repr(self.annotated) - - def base_type(self): - return self.inner.base_type() - - @property - def is_anonymous(self): - return self.name is None - - -class AnnotatedCheckerExecutionContext(ExecutionContext): - def __init__(self, ch: AnnotatedChecker, upper: ExecutionContext): - self.ch = ch - self.upper = upper - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - offset = self.ch.describe().find("[") + 1 - - (t, i) = err.next_type_and_indicator() - if self.ch.name is not None: - i = "^" * len(self.ch.describe()) - - err = err.with_frame(Frame( - type_declared=self.ch.describe(), - indicator_line=(" " * offset) + i, - declared=None, - responsable=None - )) - - return self.upper.wrap(err) diff --git a/python/deps/untypy/untypy/impl/any.py b/python/deps/untypy/untypy/impl/any.py deleted file mode 100644 index 62671c8f..00000000 --- a/python/deps/untypy/untypy/impl/any.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, Optional - -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext - - -class AnyFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if annotation is Any or annotation is any: - return AnyChecker() - else: - return None - - -class AnyChecker(TypeChecker): - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - return arg - - def describe(self) -> str: - return "Any" - - def base_type(self) -> type: - return [Any] - - -class SelfChecker(TypeChecker): - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - return arg - - def describe(self) -> str: - return "Self" - - def base_type(self) -> type: - return [Any] diff --git a/python/deps/untypy/untypy/impl/callable.py b/python/deps/untypy/untypy/impl/callable.py deleted file mode 100644 index 2d092e12..00000000 --- a/python/deps/untypy/untypy/impl/callable.py +++ /dev/null @@ -1,277 +0,0 @@ -import inspect -import sys -from collections.abc import Callable as AbcCallable -from typing import Any, Optional, Callable, Union, Tuple - -from untypy.error import UntypyTypeError, UntypyAttributeError, Frame, Location -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext, WrappedFunction, \ - WrappedFunctionContextProvider -# These Types are prefixed with an underscore... -from untypy.util import ArgumentExecutionContext, ReturnExecutionContext -import untypy.util.typedfunction as typedfun - -CallableTypeOne = type(Callable[[], None]) -CallableTypeTwo = type(AbcCallable[[], None]) - - -class CallableFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if type(annotation) == CallableTypeOne or type(annotation) == CallableTypeTwo: - return CallableChecker(annotation, ctx) - else: - return None - - -class CallableChecker(TypeChecker): - return_checker: TypeChecker - argument_checker: list[TypeChecker] - - def __init__(self, annotation: Union[CallableTypeOne, CallableTypeTwo], ctx: CreationContext): - arguments_ty = annotation.__args__[:-1] - return_ty = annotation.__args__[-1] - - return_checker = ctx.find_checker(return_ty) - if return_checker is None: - raise ctx.wrap(UntypyAttributeError(f"Return Type Annotation not found. {return_ty}")) - - argument_checker = [] - for arg in arguments_ty: - checker = ctx.find_checker(arg) - if checker is None: - raise ctx.wrap(UntypyAttributeError(f"Argument Type Annotation not found. {arg}")) - argument_checker.append(checker) - - self.return_checker = return_checker - self.argument_checker = argument_checker - - def may_be_wrapped(self) -> bool: - return True - - def base_type(self) -> list[Any]: - return [Callable] - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if callable(arg): - return TypedCallable(arg, self.return_checker, self.argument_checker, ctx) - else: - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - - def describe(self) -> str: - arguments = ", ".join(map(lambda e: e.describe(), self.argument_checker)) - return f"Callable[[{arguments}], {self.return_checker.describe()}]" - - -class TypedCallable(Callable, WrappedFunction): - return_checker: TypeChecker - argument_checker: list[TypeChecker] - inner: Callable - fn: Callable - ctx: ExecutionContext - - def __init__(self, inner: Callable, return_checker: TypeChecker, - argument_checker: list[TypeChecker], ctx: ExecutionContext): - # unwrap if wrapped function - self.ResponsibilityType = None - if hasattr(inner, '__wf'): - inner = getattr(inner, '__wf') - self.inner = inner - self.return_checker = return_checker - self.argument_checker = argument_checker - self.ctx = ctx - self.fn = WrappedFunction.find_original(self.inner) - setattr(self, '__wf', self) - - def __call__(self, *args, **kwargs): - caller = sys._getframe(1) - - (args, kwargs, bindings) = self.wrap_arguments( - lambda i: TypedCallableArgumentExecutionContext(self, caller, i, self.ctx), args, kwargs) - - bind2 = None - if isinstance(self.inner, WrappedFunction): - (args, kwargs, bind2) = self.inner.wrap_arguments(lambda n: - TypedCallableIncompatibleSingature(self, n, - caller, - self.ctx), - args, kwargs) - - ret = self.fn(*args, **kwargs) - if isinstance(self.inner, WrappedFunction): - ret = self.inner.wrap_return(args, kwargs, ret, bind2, TypedCallableReturnExecutionContext(self.ctx, self, True)) - - ret = self.wrap_return(args, kwargs, ret, bindings, TypedCallableReturnExecutionContext(self.ctx, self, False)) - return ret - - def get_original(self): - return self.inner - - def wrap_arguments(self, ctxprv: WrappedFunctionContextProvider, args, kwargs): - new_args = [] - i = 0 - for (arg, checker) in zip(args, self.argument_checker): - res = checker.check_and_wrap(arg, ctxprv(i)) - new_args.append(res) - i += 1 - - if len(kwargs) > 0: - raise self.ctx.wrap(UntypyTypeError( - kwargs, - self.describe() - ).with_note("Keyword arguments are not supported in callable types.")) - - bindings = None - return new_args, kwargs, bindings - - def wrap_return(self, args, kwargs, ret, bindings, ctx: ExecutionContext): - fc_pair = None - return typedfun.wrap_return(self.return_checker, args, kwargs, ret, fc_pair, ctx) - - def describe(self) -> str: - arguments = ", ".join(map(lambda e: e.describe(), self.argument_checker)) - return f"Callable[[{arguments}], {self.return_checker.describe()}]" - - def checker_for(self, name: str) -> TypeChecker: - if name == 'return': - return self.return_checker - spec = inspect.getfullargspec(self.fn) - try: - ix = spec.args.index(name) - except ValueError: - raise self.ctx.wrap(UntypyAttributeError( - f"No annotation found for argument {name}" - )) - if ix >= len(self.argument_checker): - raise self.ctx.wrap(UntypyAttributeError( - f"No annotation found for argument {name}" - )) - return self.argument_checker[ix] - - -class TypedCallableIncompatibleSingature(ExecutionContext): - - def __init__(self, tc: TypedCallable, arg_name: str, caller, upper: ExecutionContext): - self.arg_name = arg_name - self.tc = tc - self.upper = upper - self.caller = caller - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - (original_expected, _ind) = err.next_type_and_indicator() - err = ArgumentExecutionContext(self.tc.inner, None, self.arg_name).wrap(err) - - func_decl = WrappedFunction.find_location(self.tc.inner) - - name = WrappedFunction.find_original(self.tc.inner).__name__ - - (decl, ind) = err.next_type_and_indicator() - err = err.with_frame(Frame( - decl, - ind, - declared=None, - responsable=func_decl - )) - - previous_chain = UntypyTypeError( - f"def {name}{self.tc.inner.describe()}", - f"{self.tc.describe()}" - ) - (decl, ind) = previous_chain.next_type_and_indicator() - previous_chain = previous_chain.with_frame(Frame( - decl, - ind, - declared=func_decl, - responsable=None - )) - - err = err.with_note( - f"Argument '{self.arg_name}' of method '{name}' violates the signature of {self.tc.describe()}.") - - previous_chain = self.upper.wrap(previous_chain) - - return err.with_previous_chain(previous_chain) - - -class TypedCallableReturnExecutionContext(ExecutionContext): - upper: ExecutionContext - fn: TypedCallable - - def __init__(self, upper: ExecutionContext, fn: TypedCallable, invert: bool): - self.upper = upper - self.fn = fn - self.invert = invert - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - desc = lambda s: s.describe() - front_str = f"Callable[[{', '.join(map(desc, self.fn.argument_checker))}], " - responsable = WrappedFunction.find_location(self.fn.inner) - - if self.invert: - (next_ty, indicator) = err.next_type_and_indicator() - err = ReturnExecutionContext(self.fn.inner).wrap(err) - err = err.with_frame(Frame( - f"{front_str}{next_ty}]", - (" " * len(front_str)) + indicator, - declared=None, - responsable=responsable - )) - err = err.with_inverted_responsibility_type() - return self.upper.wrap(err) - - (next_ty, indicator) = err.next_type_and_indicator() - err = err.with_frame(Frame( - f"{front_str}{next_ty}]", - (" " * len(front_str)) + indicator, - declared=None, - responsable=responsable - )) - - return self.upper.wrap(err) - - -class TypedCallableArgumentExecutionContext(ExecutionContext): - upper: ExecutionContext - fn: TypedCallable - stack: inspect.FrameInfo - argument_num: int - - def __init__(self, fn: TypedCallable, stack: inspect.FrameInfo, argument_num: int, upper: ExecutionContext): - self.fn = fn - self.stack = stack - self.argument_num = argument_num - self.upper = upper - - def declared_and_indicator(self, err: UntypyTypeError) -> Tuple[str, str]: - (next_ty, indicator) = err.next_type_and_indicator() - front_types = [] - back_types = [] - for n, ch in enumerate(self.fn.argument_checker): - if n < self.argument_num: - front_types.append(ch.describe()) - elif n > self.argument_num: - back_types.append(ch.describe()) - - l = len(f"Callable[[{', '.join(front_types)}") - if len(front_types) > 0: - l += len(', ') - - return f"Callable[[{', '.join(front_types + [next_ty] + back_types)}], {self.fn.return_checker.describe()}]", \ - (" " * l) + indicator - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - (type_declared, indicator_line) = self.declared_and_indicator(err) - - declared = WrappedFunction.find_location(self.fn.inner) - - responsable = Location.from_stack(self.stack) - - frame = Frame( - type_declared, - indicator_line, - declared=declared, - responsable=responsable, - ) - - err = err.with_frame(frame) - err = err.with_inverted_responsibility_type() - return self.upper.wrap(err) diff --git a/python/deps/untypy/untypy/impl/choice.py b/python/deps/untypy/untypy/impl/choice.py deleted file mode 100644 index fefd5776..00000000 --- a/python/deps/untypy/untypy/impl/choice.py +++ /dev/null @@ -1,66 +0,0 @@ - -from typing import Any, Optional -from untypy.interfaces import TypeCheckerFactory, CreationContext, TypeChecker, ExecutionContext -from untypy.util.debug import debug, isDebug -import untypy.error - -# A type depending on the arguments passed to a function. Can only be used as the return type -# of a function. -# Written Choice[T1, ..., Tn, fun]. fun is called with the arguments of the function -# call (possible including self and empty kwargs) and the type checkers for T1, ..., Tn. -# It then returns a type checker. -class Choice: - def __init__(self, types, select_fun): - self.types = types - self.select_fun = select_fun - - def __class_getitem__(cls, args): - if len(args) < 2: - raise untypy.error.WyppTypeError(f'Choice requires at least two arguments: Choice[TYPE_1, ..., FUN]') - return Choice(args[:-1], args[-1]) - -class ChoiceFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if isinstance(annotation, Choice): - checkers = [] - for t in annotation.types: - checker = ctx.find_checker(t) - if checker is None: - return None - checkers.append(checker) - return ChoiceChecker(checkers, annotation.select_fun) - else: - return None - -class ChoiceChecker(TypeChecker): - - def __init__(self, checkers: list[TypeChecker], select_fun): - self.checkers = checkers - self.select_fun = select_fun - - def describe(self) -> str: - l = [c.describe() for c in self.checkers] - return f"Choice[{', '.join(l)}, ]" - - def may_be_wrapped(self) -> bool: - return True - - def __illegal_use(self): - raise untypy.error.WyppTypeError(f"{self.describe()} can only be used as the return type of a function or method") - - def base_type(self) -> list[Any]: - self.__illegal_use() - - def base_type_priority(self) -> int: - return 0 - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - self.__illegal_use() - - def get_checker(self, args: list[Any], kwds: dict[str, Any]) -> TypeChecker: - c = self.select_fun(*args, kwds, *self.checkers) - if isDebug: - all = [c.describe() for c in self.checkers] - debug(f'Choice returned checker {c.describe()} for args={args}, kwds={kwds}. All checkers: {",".join(all)}') - return c diff --git a/python/deps/untypy/untypy/impl/dummy_delayed.py b/python/deps/untypy/untypy/impl/dummy_delayed.py deleted file mode 100644 index 81b07704..00000000 --- a/python/deps/untypy/untypy/impl/dummy_delayed.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Any, Optional - -from untypy.error import UntypyTypeError -from untypy.interfaces import TypeChecker, CreationContext, TypeCheckerFactory, ExecutionContext - - -class DummyDelayedType: - """ - This class is used for raising delayed type checking errors. - """ - pass - - -class DummyDelayedFactory(TypeCheckerFactory): - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if annotation is DummyDelayedType: - return DummyDelayedChecker() - else: - return None - - -class DummyDelayedChecker(TypeChecker): - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - return DummyDelayedWrapper(ctx) - - def describe(self) -> str: - return "DummyDelayedType" - - def base_type(self) -> list[Any]: - return [] - - -class DummyDelayedWrapper: - upper: ExecutionContext - - def __init__(self, upper: ExecutionContext): - self.upper = upper - - def use(self): - raise self.upper.wrap(UntypyTypeError( - "", - "DummyDelayedType" - )) diff --git a/python/deps/untypy/untypy/impl/generator.py b/python/deps/untypy/untypy/impl/generator.py deleted file mode 100644 index 6551bdb3..00000000 --- a/python/deps/untypy/untypy/impl/generator.py +++ /dev/null @@ -1,128 +0,0 @@ -import collections.abc -import inspect -import sys -from collections.abc import Generator -from typing import Any, Optional -from typing import Generator as OtherGenerator - -from untypy.error import UntypyTypeError, UntypyAttributeError, Location -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext -from untypy.util import CompoundTypeExecutionContext, NoResponsabilityWrapper - -GeneratorTypeA = type(Generator[None, None, None]) -GeneratorTypeB = type(OtherGenerator[None, None, None]) - - -class GeneratorFactory(TypeCheckerFactory): - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if type(annotation) in [GeneratorTypeA, GeneratorTypeB] and annotation.__origin__ == collections.abc.Generator: - if len(annotation.__args__) != 3: - raise ctx.wrap(UntypyAttributeError(f"Expected 3 type arguments for Generator.")) - - (yield_checker, send_checker, return_checker) = list( - map(lambda a: ctx.find_checker(a), annotation.__args__)) - - if yield_checker is None: - raise ctx.wrap(UntypyAttributeError(f"The Yield Annotation of the Generator could not be resolved.")) - if send_checker is None: - raise ctx.wrap(UntypyAttributeError(f"The Send Annotation of the Generator could not be resolved.")) - if return_checker is None: - raise ctx.wrap(UntypyAttributeError(f"The Return Annotation of the Generator could not be resolved.")) - - return GeneratorChecker(yield_checker, send_checker, return_checker) - else: - return None - - -class GeneratorChecker(TypeChecker): - yield_checker: TypeChecker - send_checker: TypeChecker - return_checker: TypeChecker - - def __init__(self, yield_checker: TypeChecker, send_checker: TypeChecker, return_checker: TypeChecker): - self.yield_checker = yield_checker - self.send_checker = send_checker - self.return_checker = return_checker - - def may_be_wrapped(self) -> bool: - return True - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if not inspect.isgenerator(arg): - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - - me = self - yield_ctx = TypedGeneratorYieldReturnContext(arg, self, True, ctx) - return_ctx = TypedGeneratorYieldReturnContext(arg, self, False, ctx) - - def wrapped(): - try: - sent = None - while True: - value_yield = arg.send(sent) - # check value_yield (arg is responsable) - value_yield = me.yield_checker.check_and_wrap(value_yield, yield_ctx) - - sent = yield value_yield - - caller = sys._getframe(1) - - # check sent value (caller is responsable) - sent = me.send_checker.check_and_wrap(sent, TypedGeneratorSendContext(caller, me, ctx)) - - except StopIteration as e: - # check value_returned (arg is responsable) - ret = me.return_checker.check_and_wrap(e.value, return_ctx) - return ret - - return wrapped() - - def describe(self) -> str: - return f"Generator[{self.yield_checker.describe()}, {self.send_checker.describe()}, {self.return_checker.describe()}]" - - def base_type(self) -> Any: - return [GeneratorType] - - -class TypedGeneratorYieldReturnContext(CompoundTypeExecutionContext): - generator: Generator[Any, Any, Any] - - def __init__(self, generator: Generator[Any, Any, Any], checker: GeneratorChecker, is_yield: bool, - upper: ExecutionContext): - self.generator = generator - # index in checkers list - if is_yield: - idx = 0 - else: - idx = 2 - super().__init__(upper, [checker.yield_checker, checker.send_checker, checker.return_checker], idx) - - def name(self) -> str: - return "Generator" - - def responsable(self) -> Optional[Location]: - try: - if hasattr(self.generator, 'gi_frame'): - return Location( - file=inspect.getfile(self.generator.gi_frame), - line_no=inspect.getsourcelines(self.generator.gi_frame)[1], - line_span=len(inspect.getsourcelines(self.generator.gi_frame)[0]), - ) - except OSError: # this call does not work all the time - pass - except TypeError: - pass - return None - - -class TypedGeneratorSendContext(CompoundTypeExecutionContext): - def __init__(self, stack: inspect.FrameInfo, checker: GeneratorChecker, upper: ExecutionContext): - self.stack = stack - super().__init__(NoResponsabilityWrapper(upper), - [checker.yield_checker, checker.send_checker, checker.return_checker], 1) - - def name(self) -> str: - return "Generator" - - def responsable(self) -> Optional[Location]: - return Location.from_stack(self.stack) diff --git a/python/deps/untypy/untypy/impl/generic.py b/python/deps/untypy/untypy/impl/generic.py deleted file mode 100644 index 0f0e65b5..00000000 --- a/python/deps/untypy/untypy/impl/generic.py +++ /dev/null @@ -1,98 +0,0 @@ -import typing -from typing import Optional, TypeVar, Any - -from untypy.error import UntypyTypeError -from untypy.impl.protocol import ProtocolChecker -from untypy.interfaces import TypeCheckerFactory, CreationContext, TypeChecker, ExecutionContext - -class GenericProtocolChecker(ProtocolChecker): - def protocol_type(self) -> str: - return "generic" - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if isinstance(arg, self.proto): - return super().check_and_wrap(arg, ctx) - else: - raise ctx.wrap(UntypyTypeError( - expected=self.describe(), - given=arg - )).with_note(f"Type '{type(arg).__name__}' does not inherit from '{self.proto.__name__}'") - - -class GenericFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - # TODO: Support other typevar features - if type(annotation) is TypeVar: - (found, replacement_annotation) = ctx.resolve_typevar(annotation) - if isinstance(replacement_annotation, TypeChecker): - inner = replacement_annotation - elif found: - inner = ctx.find_checker(replacement_annotation) - else: - inner = None - if found: - if inner is not None: - return BoundTypeVar(inner, annotation) - else: - return None - else: - return UnboundTypeVar(annotation) - elif hasattr(annotation, '__args__') and \ - hasattr(annotation, '__origin__') and \ - hasattr(annotation.__origin__, '__mro__') and \ - typing.Generic in annotation.__origin__.__mro__: - return GenericProtocolChecker(annotation, ctx) - else: - return None - - -class BoundTypeVar(TypeChecker): - def __init__(self, inner: TypeChecker, typevar: TypeVar): - self.inner = inner - self.typevar = typevar - - def describe(self) -> str: - return self.inner.describe() - - def may_be_wrapped(self) -> bool: - return self.inner.may_be_wrapped() - - def base_type(self) -> list[Any]: - return self.inner.base_type() - - def base_type_priority(self) -> int: - return self.inner.base_type_priority() - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - return self.inner.check_and_wrap(arg, BoundTypeVarCtx(self, ctx)) - - -class BoundTypeVarCtx(ExecutionContext): - - def __init__(self, bv: BoundTypeVar, ctx: ExecutionContext): - self.bv = bv - self.upper = ctx - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - (nt, ni) = err.next_type_and_indicator() - - if nt == err.expected and nt == self.bv.inner.describe(): - err.expected = self.bv.describe() - - return self.upper.wrap(err) - - -class UnboundTypeVar(TypeChecker): - - def __init__(self, typevar: TypeVar): - self.typevar = typevar - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - return arg - - def describe(self) -> str: - return str(self.typevar) - - def base_type(self) -> list[Any]: - return [self.typevar] diff --git a/python/deps/untypy/untypy/impl/interface.py b/python/deps/untypy/untypy/impl/interface.py deleted file mode 100644 index 9ecb5957..00000000 --- a/python/deps/untypy/untypy/impl/interface.py +++ /dev/null @@ -1,87 +0,0 @@ -import typing -from collections.abc import Iterable as ABCIterable -from collections.abc import Iterator as ABCIterator -from collections.abc import Sequence as ABCSequence -from typing import Optional, Any - -from untypy.error import UntypyAttributeError -from untypy.impl.interfaces.iterable import Iterable, Iterator -from untypy.impl.interfaces.sequence import Sequence -from untypy.impl.interfaces.dict import Dict, DictLike -from untypy.impl.interfaces.list import List -from untypy.impl.interfaces.set import Set -from untypy.impl.protocol import ProtocolChecker -from untypy.interfaces import TypeCheckerFactory, TypeChecker, CreationContext - -InterfaceMapping = { - dict: Dict, - typing.Dict: Dict, - DictLike: DictLike, - list: List, - typing.List: List, - set: Set, - typing.Set: Set, - ABCIterable: Iterable, - typing.Iterable: Iterable, - typing.Iterator: Iterator, - ABCIterator: Iterator, - ABCSequence: Sequence, - typing.Sequence: Sequence -} - -class InterfaceFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext, omit_tyargs=False) -> Optional[TypeChecker]: - if annotation in InterfaceMapping: - # Assume Any if no parameters are given - protocol = InterfaceMapping[annotation] - bindings = protocol.__parameters__ - if len(bindings) == 0: - raise AssertionError(f"This is a BUG. {annotation} has no generic params.") - anys = (Any, ) * len(bindings) - # handle Python inconsistency - if hasattr(annotation, '__class_getitem__'): - return self.create_from( - annotation.__class_getitem__(anys), - ctx, - omit_tyargs=True - ) - elif hasattr(annotation, '__getitem__'): - return self.create_from( - annotation.__getitem__(anys), - ctx, - omit_tyargs=True - ) - - elif hasattr(annotation, '__origin__') and \ - hasattr(annotation, '__args__') and annotation.__origin__ in InterfaceMapping: - protocol = InterfaceMapping[annotation.__origin__] - bindings = protocol.__parameters__ # args of Generic super class - origin = annotation.__origin__ - inner_checkers = [] - for param in annotation.__args__: - ch = ctx.find_checker(param) - if ch is None: - raise UntypyAttributeError(f"Could not resolve annotation {param} inside of {annotation}") - inner_checkers.append(ch) - if len(inner_checkers) != len(bindings): - raise UntypyAttributeError(f"Expected {len(bindings)} type arguments inside of {annotation}") - if omit_tyargs: - name = f"{origin.__name__}" - else: - name = f"{origin.__name__}[" + (', '.join(map(lambda t: t.describe(), inner_checkers))) + "]" - bindings = dict(zip(bindings, inner_checkers)) - ctx = ctx.with_typevars(bindings) - return ProtocolChecker(protocol, ctx, altname=name, omit_tyargs=omit_tyargs, ty=origin) - - # Non Generic - elif annotation in InterfaceMapping: - protocol = InterfaceMapping[annotation] - # Edge-Case `TypingSequence has no __name__, like every other class` - if annotation == typing.Sequence: - name = 'Sequence' - else: - name = annotation.__name__ - return ProtocolChecker(protocol, ctx, altname=name, omit_tyargs=omit_tyargs, ty=annotation) - else: - return None diff --git a/python/deps/untypy/untypy/impl/interfaces/dict.py b/python/deps/untypy/untypy/impl/interfaces/dict.py deleted file mode 100644 index fdb76902..00000000 --- a/python/deps/untypy/untypy/impl/interfaces/dict.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Iterator, Protocol, TypeVar, Generic, Optional, Iterable, Tuple, Any - -A = TypeVar("A") -B = TypeVar("B") - - -class DictLike(Protocol[A, B]): - """ - This protocol implements a subset of dict. - It exists solly to prevent an recursion issue - inside of WDict - """ - - def __iter__(self) -> Iterator[A]: - pass - - def __getitem__(self, key: A) -> B: - pass - - -K = TypeVar("K") -V = TypeVar("V") - - -# See: https://docs.python.org/3/library/stdtypes.html#typesmapping -class Dict(Generic[K, V], dict): - def clear(self) -> None: pass - - # Cannot Typecheck Copy -> Leads to endless recursion in "UntypyInterfaces" - # def copy(self) -> dict[K,V]: - # pass - - def get(self, key: K, default: Optional[V] = None) -> Optional[V]: pass - - def items(self) -> Iterable[Tuple[K, V]]: pass - - def keys(self) -> Iterable[K]: pass - - def pop(self, k: K, default: Optional[V] = None) -> Optional[V]: pass - - def popitem(self) -> Tuple[K, V]: pass - - # Miss-match See: https://github.com/skogsbaer/write-your-python-program/issues/19 - def setdefault(self, key: K, default: Optional[V]=None) -> V: pass - - def update(self, *E: Iterable[DictLike[K, V]], **F: Optional[DictLike[K, V]]) -> Any: pass - - def values(self) -> Iterable[V]: pass - - def __contains__(self, key: Any) -> bool: pass - - def __delitem__(self, key: K) -> None: pass - - def __iter__(self) -> Iterator[K]: pass - - def __len__(self) -> int: pass - - # Untypy does not support generic functions :/ - def __or__(self, other : dict) -> dict: pass - - def __reversed__(self) -> Iterator[K]: pass - - # Untypy does not support generic functions :/ - def __ror__(self, other : dict) -> dict: pass - def __getitem__(self, key: K) -> V: pass - - def __setitem__(self, key: K, value: V) -> None: pass - # FIXME: complete methods diff --git a/python/deps/untypy/untypy/impl/interfaces/iterable.py b/python/deps/untypy/untypy/impl/interfaces/iterable.py deleted file mode 100644 index b52c7db1..00000000 --- a/python/deps/untypy/untypy/impl/interfaces/iterable.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations -from typing import Generic, TypeVar, Protocol -import typing - -I = TypeVar("I") - -# Note: using typing.Iterator as the result creates an indirection that avoids an infinite -# loop when constructing checkers. -class Iterator(Generic[I]): - def __next__(self) -> I: - pass - def __iter__(self) -> typing.Iterator[I]: - pass - -class Iterable(Generic[I]): - def __iter__(self) -> typing.Iterator[I]: - pass - -class OnlyIterable(Generic[I], Protocol): - # The __protocol_only__ flag signals that an object wrapped with the protocol should - # only provide the methods of the protocol. - __protocol_only__ = True - - def __iter__(self) -> typing.Iterator[I]: - pass diff --git a/python/deps/untypy/untypy/impl/interfaces/list.py b/python/deps/untypy/untypy/impl/interfaces/list.py deleted file mode 100644 index d624d8f3..00000000 --- a/python/deps/untypy/untypy/impl/interfaces/list.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import TypeVar, Generic, Iterable, Optional, Union, Any, Iterator -import typing -from untypy.impl.interfaces.util import overwrite -from untypy.impl.choice import Choice -from untypy.interfaces import CreationContext -from untypy.util import ReturnExecutionContext -from untypy.util.condition import postcondition - -I = TypeVar("I") - -class List(Generic[I], list): - # doc @ https://docs.python.org/3/tutorial/datastructures.html - # and https://docs.python.org/3/library/stdtypes.html#common-sequence-operations - # Exact signatures are undocumented :/ - # HINT: Argument names must match. - - def append(self, object: I) -> None: pass - - def extend(self, iterable: Iterable[I]) -> None: pass - - def insert(self, i: int, x: I) -> None: pass - - def remove(self, x: I) -> None: pass - - def pop(self, i: int = -1) -> Optional[I]: pass - - def clear(self) -> None: pass - - def index(self, value: I, start: Optional[int] = 0, stop: Optional[int] = 9223372036854775807) -> int: - # get index of list - pass - - def count(self, value: I) -> int: pass - - # inner list will check type of key. - def sort(self, *, key: Any = None, reverse: bool = False) -> None: pass - - def __contains__(self, key: Any) -> bool: pass - - def __delitem__(self, i: Union[int, slice]): pass - - def __getitem__(self, i: Union[int, slice]) -> \ - Choice[I, Any, lambda self, i, kws, ti, tl: ti if isinstance(i, int) else tl]: pass - - def __add__(self, other: Iterable) -> Any: pass - - def __mul__(self, n: int) -> Any: pass - - def __iadd__(self, other: Iterable[I]) -> Any: pass - - def __imul__(self, n: int) -> Any: pass - - def __setitem__(self, key: Union[int, slice], value: Any) -> None: pass - - def __iter__(self) -> Iterator[I]: pass - # FIXME: complete methods diff --git a/python/deps/untypy/untypy/impl/interfaces/sequence.py b/python/deps/untypy/untypy/impl/interfaces/sequence.py deleted file mode 100644 index 08d58371..00000000 --- a/python/deps/untypy/untypy/impl/interfaces/sequence.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import TypeVar, Generic, Optional, Iterator, Optional, Union, Any -from untypy.impl.choice import Choice -I = TypeVar("I") - - -class Sequence(Generic[I]): - # See https://docs.python.org/3/library/collections.abc.html - - def __getitem__(self, i: Union[int, slice]) -> \ - Choice[I, Any, lambda self, i, kws, ti, tl: ti if isinstance(i, int) else tl]: - pass - - def __len__(self) -> int: - pass - - def __contains__(self, key: Any) -> bool: - pass - - def index(self, value: I, start: Optional[int] = 0, stop: Optional[int] = 9223372036854775807) -> int: - pass - - def count(self, value: I) -> int: - pass - - # For these Methods: `iter` and `reversed` will fallback to `__getitem__` and `__len__` - # This is just an optimization for some types. - # Maybe for a future feature. - - def __iter__(self) -> Iterator[I]: - pass - diff --git a/python/deps/untypy/untypy/impl/interfaces/set.py b/python/deps/untypy/untypy/impl/interfaces/set.py deleted file mode 100644 index 145cb9b1..00000000 --- a/python/deps/untypy/untypy/impl/interfaces/set.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Generic, TypeVar, Optional, Tuple, Iterable, Any, Iterator -from untypy.impl.interfaces.iterable import OnlyIterable - -I = TypeVar("I") - -class Set(Generic[I]): - def add(self, other: I) -> None: pass - def clear(self) -> None: pass - def copy(self) -> Any: pass - def difference(self, *others: Tuple[Iterable, ...]) -> Any: pass - def difference_update(self, *others: Tuple[Iterable[I], ...]) -> None: pass - def discard(self, elem: I) -> None: pass - def intersection(self, *others: Tuple[Iterable, ...]) -> Any: pass - def intersection_update(self, *others: Tuple[Iterable[I], ...]) -> None: pass - def isdisjoint(self, other: set) -> bool: pass - def issubset(self, other: set) -> bool: pass - def issuperset(self, other: set) -> bool: pass - def pop(self) -> Optional[I]: pass - def remove(self, elem: I) -> None: pass - def symmetric_difference(self, *others: Tuple[Iterable, ...]) -> Any: pass - def symmetric_difference_update(self, *others: Tuple[Iterable[I], ...]) -> None: pass - def union(self, *others: Tuple[Iterable, ...]) -> Any: pass - - # Using OnlyIterable here makes no other methods than the one provide in Iterable - # available. This is required because the implementation of update has shortcuts - # bypassing type checks if an element is of type set. - def update(self, *others: Tuple[OnlyIterable[I], ...]) -> None: - pass - - def __contains__(self, x: Any) -> bool: pass - def __delattr__(self, name: str) -> None: pass - - def __le__(self, other: Any) -> bool: pass - def __lt__(self, other: Any) -> bool: pass - def __ge__(self, other: Any) -> bool: pass - def __gt__(self, other: Any) -> bool: pass - - def __and__(self, other: set) -> Any: pass - def __rand__(self, other: set) -> Any: pass - def __iand__(self, other: set) -> Any: pass - def __ior__(self, other: set) -> Any: pass # FIXME: result should be set[I] - def __isub__(self, other: set) -> Any: pass - def __ixor__(self, other: set) -> Any: pass - def __or__(self, other: set) -> Any: pass - def __ror__(self, other: set) -> Any: pass - def __rxor__(self, other: set) -> Any: pass - def __xor__(self, other: set) -> Any: pass - def __rsub__(self, other: set) -> Any: pass - def __sub__(self, other: set) -> Any: pass - - def __iter__(self) -> Iterator[I]: pass - def __len__(self) -> int: pass - def __setattr__(self, name: str, x: Any) -> None: pass diff --git a/python/deps/untypy/untypy/impl/interfaces/util.py b/python/deps/untypy/untypy/impl/interfaces/util.py deleted file mode 100644 index 42e5f170..00000000 --- a/python/deps/untypy/untypy/impl/interfaces/util.py +++ /dev/null @@ -1,6 +0,0 @@ -def overwrite(typ): - def inner(fn): - setattr(fn, '__overwrite', typ) - return fn - - return inner diff --git a/python/deps/untypy/untypy/impl/literal.py b/python/deps/untypy/untypy/impl/literal.py deleted file mode 100644 index 03de3e53..00000000 --- a/python/deps/untypy/untypy/impl/literal.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any, Optional, Literal - -from untypy.error import UntypyTypeError -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext - - -class LiteralFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if hasattr(annotation, '__origin__') and hasattr(annotation, '__args__') and annotation.__origin__ == Literal: - return LiteralChecker(annotation.__args__) - else: - return None - - -class LiteralChecker(TypeChecker): - inner: list[Any] - - def __init__(self, inner: list[Any]): - self.inner = inner - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if arg in self.inner: - return arg - else: - raise ctx.wrap(UntypyTypeError( - arg, - self.describe() - )) - - def base_type(self) -> list[Any]: - return self.inner[:] - - def describe(self) -> str: - strings = map(lambda v: "%r" % v, self.inner) - return f"Literal[{', '.join(strings)}]" diff --git a/python/deps/untypy/untypy/impl/none.py b/python/deps/untypy/untypy/impl/none.py deleted file mode 100644 index 31180286..00000000 --- a/python/deps/untypy/untypy/impl/none.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any, Optional, NoReturn - -from untypy.error import UntypyTypeError -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext - - -class NoneFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if annotation is None or annotation is type(None) or annotation == NoReturn: - return NoneChecker() - else: - return None - - -class NoneChecker(TypeChecker): - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if arg is None: - return arg - else: - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - - def describe(self) -> str: - return "None" - - def base_type(self) -> list[Any]: - return [None] diff --git a/python/deps/untypy/untypy/impl/optional.py b/python/deps/untypy/untypy/impl/optional.py deleted file mode 100644 index b1577c6b..00000000 --- a/python/deps/untypy/untypy/impl/optional.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Any, Optional, Union - -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext -from untypy.util import CompoundTypeExecutionContext - -UnionType = type(Union[int, str]) - - -class OptionalFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if type(annotation) is UnionType and len(annotation.__args__) == 2 and annotation.__args__[1] is type(None): - checker = ctx.find_checker(annotation.__args__[0]) - if checker is None: - return None - else: - return OptionalChecker(checker) - else: - return None - - -class OptionalChecker(TypeChecker): - inner: TypeChecker - - def __init__(self, inner: TypeChecker): - self.inner = inner - - def check_and_wrap(self, arg: Any, upper: ExecutionContext) -> Any: - if arg is None: - return arg - else: - ctx = OptionalExecutionContext(upper, [self.inner], 0) - return self.inner.check_and_wrap(arg, ctx) - - def describe(self) -> str: - return f"Optional[{self.inner.describe()}]" - - def base_type(self) -> list[Any]: - return [self.inner.base_type()] - - -class OptionalExecutionContext(CompoundTypeExecutionContext): - def name(self): - return "Optional" diff --git a/python/deps/untypy/untypy/impl/protocol.py b/python/deps/untypy/untypy/impl/protocol.py deleted file mode 100644 index e98d0385..00000000 --- a/python/deps/untypy/untypy/impl/protocol.py +++ /dev/null @@ -1,524 +0,0 @@ -import abc -import inspect -import sys -import typing -from typing import Protocol, Any, Optional, Callable, Union, TypeVar, Dict, Tuple - -import untypy.util.display as display -from untypy.error import UntypyTypeError, UntypyAttributeError, Frame, Location, ResponsibilityType -from untypy.impl.any import SelfChecker, AnyChecker -from untypy.interfaces import TypeCheckerFactory, CreationContext, TypeChecker, ExecutionContext, \ - WrappedFunctionContextProvider -from untypy.util import WrappedFunction, ArgumentExecutionContext, ReturnExecutionContext -from untypy.util.condition import FunctionCondition -from untypy.util.typehints import get_type_hints -import untypy.util.wrapper as wrapper -import untypy.util.typedfunction as typedfun - -class ProtocolFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if isinstance(annotation, type) and Protocol in annotation.mro(): - return ProtocolChecker(annotation, ctx) - elif hasattr(annotation, '__args__') and \ - hasattr(annotation, '__origin__') and \ - hasattr(annotation.__origin__, '__mro__') and \ - typing.Protocol in annotation.__origin__.__mro__: - return ProtocolChecker(annotation, ctx) - else: - return None - - -def _find_bound_typevars(clas: type) -> Tuple[type, Dict[TypeVar, Any]]: - if not hasattr(clas, '__args__') or not hasattr(clas, '__origin__'): - return (clas, dict()) - if not hasattr(clas.__origin__, '__parameters__'): - return (clas, dict()) - - keys = clas.__origin__.__parameters__ - values = clas.__args__ - - if len(keys) != len(values): - raise UntypyAttributeError(f"Some unbound Parameters in {clas.__name__}. " - f"keys={keys} do not match values={values}.", - [Location( - file=inspect.getfile(clas), - line_no=inspect.getsourcelines(clas)[1], - line_span=len(inspect.getsourcelines(clas)[0]))]) - return (clas.__origin__, dict(zip(keys, values))) - - -def get_proto_members(proto: type, ctx: CreationContext) -> dict[ - str, Tuple[inspect.Signature, dict[str, TypeChecker], FunctionCondition]]: - blacklist = ['__init__', '__class__', '__delattr__', '__dict__', '__dir__', - '__doc__', '__getattribute__', '__getattr__', '__init_subclass__', - '__new__', '__setattr__', '__subclasshook__', '__weakref__', - '__abstractmethods__', '__class_getitem__', - '__firstlineno__', '__static_attributes__'] - - member_dict = {} - for [name, member] in inspect.getmembers(proto): - if name in blacklist: - continue - - if inspect.isfunction(member): - member = WrappedFunction.find_original(member) - signature = inspect.signature(member) - - is_typed = len(inspect.getfullargspec(member).annotations) != 0 - - checkers = {} - if not is_typed: - # Use Any for any type - for key in signature.parameters: - if key == 'self': - checkers[key] = SelfChecker() - else: - checkers[key] = AnyChecker() - checkers['return'] = AnyChecker() - else: - annotations = get_type_hints(member, ctx) - for key in signature.parameters: - if key == 'self': - checkers[key] = SelfChecker() - else: - param = signature.parameters[key] - if param.annotation is inspect.Parameter.empty: - raise ctx.wrap(UntypyAttributeError( - f"Missing annotation for argument '{key}' of function {member.__name__} " - f"in protocol {proto.__name__}\n")) - - param_anot = annotations[key] - if param_anot is proto: - checker = SimpleInstanceOfChecker(proto, None) - else: - checker = ctx.find_checker(param_anot) - if checker is None: - raise ctx.wrap(UntypyAttributeError(f"\n\tUnsupported type annotation: {param.annotation}\n" - f"for argument '{key}' of function {member.__name__} " - f"in protocol {proto.__name__}.\n")) - checkers[key] = checker - - if signature.return_annotation is inspect.Parameter.empty: - return_annotation = None - else: - return_annotation = annotations['return'] - if return_annotation is proto: # Self as Return Type would led to endless recursion - return_checker = SimpleInstanceOfChecker(proto, None) - else: - return_checker = ctx.find_checker(return_annotation) - - if return_checker is None: - raise ctx.wrap(UntypyAttributeError(f"\n\tUnsupported type annotation: {signature.return_annotation}\n" - f"for return value of function {member.__name__} " - f"in protocol-like {proto.__name__}.\n")) - checkers['return'] = return_checker - - fc = None - if hasattr(member, '__fc'): - fc = getattr(member, '__fc') - member_dict[name] = (signature, checkers, fc) - return member_dict - - -class ProtocolChecker(TypeChecker): - def __init__(self, annotation: type, ctx: CreationContext, *, altname : Optional[str] = None, - omit_tyargs=False, ty: Optional[type]=None): - (proto, typevars) = _find_bound_typevars(annotation) - self.ctx = ctx.with_typevars(typevars) - self.proto = proto - self._members = None - self.typevars = typevars - self.altname = altname - self.omit_tyargs = omit_tyargs - self.ty = ty - - @property - def members(self): - if not self._members: - self._members = get_proto_members(self.proto, self.ctx) - return self._members - - def may_change_identity(self) -> bool: - return True - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if hasattr(arg, '__wrapped__'): - # no double wrapping - arg = getattr(arg, '__wrapped__') - simple = False - if hasattr(self.proto, '__protocol_only__'): - simple = getattr(self.proto, '__protocol_only__') - if self.ty is not None and not isinstance(arg, self.ty): - err = ctx.wrap(UntypyTypeError( - expected=self.describe(), - given=arg - )) - raise err - return wrapForProtocol(self, arg, self.members, ctx, simple=simple) - - def base_type(self) -> list[Any]: - # Prevent Classes implementing multiple Protocols in one Union by accident. - if self.ty: - return [self.ty] - else: - return [Protocol] - - def describe(self) -> str: - if self.altname is not None: - return self.altname - - desc = set([]) - if not self.omit_tyargs: - for name in self.members: - (sig, binds, cond) = self.members[name] - for argname in sig.parameters: - if isinstance(sig.parameters[argname].annotation, TypeVar): - desc.add(binds[argname].describe()) - if isinstance(sig.return_annotation, TypeVar): - desc.add(binds['return'].describe()) - if len(desc) > 0: - # FIXME: what about the ordering of tyvars? - return f"{self.proto.__name__}[" + (', '.join(desc)) + "]" - else: - return f"{self.proto.__name__}" - - def protocol_type(self) -> str: - return f"protocol" - - def protoname(self): - return self.describe() - -def isInternalProtocol(p: Any): - if isinstance(p, ProtocolChecker): - p = p.proto - if hasattr(p, '__module__'): - return 'untypy.' in p.__module__ - else: - return False - -def protoMismatchErrorMessage(what: str, proto: Any): - if isinstance(proto, ProtocolChecker): - kind = proto.protocol_type() - name = proto.protoname() - else: - kind = 'protocol' - name = proto.__name__ - isUserDefined = True - if isInternalProtocol(proto): - isUserDefined = False - if isUserDefined: - return f"{what} does not implement {kind} {name}" - else: - return f"{what} is not {display.withIndefiniteArticle(name)}" - -def wrapForProtocol(protocolchecker: ProtocolChecker, - originalValue: Any, - members: dict[str, Tuple[inspect.Signature, dict[str, TypeChecker], FunctionCondition]], - ctx: ExecutionContext, - simple: bool): - list_of_attr = dict() - original = type(originalValue) - for fnname in members: - if not hasattr(original, fnname): - err = ctx.wrap(UntypyTypeError( - expected=protocolchecker.describe(), - given=originalValue - )).with_header( - protoMismatchErrorMessage(original.__name__, protocolchecker.proto) - ) - missing = [] - for fnname in members: - if not hasattr(original, fnname): - missing.append(fnname) - if len(missing) == 2: - err = err.with_note(f"It is missing the functions '{missing[0]}' and '{missing[1]}'") - elif len(missing) == 1: - err = err.with_note(f"It is missing the function '{missing[0]}'") - raise err - - original_fn = getattr(original, fnname) - try: - # fails on built ins - YEAH - original_fn_signature = inspect.signature(original_fn) - except: - original_fn_signature = None - - if hasattr(original_fn, '__wf'): - original_fn = getattr(original_fn, '__wf') - (sig, baseArgDict, fc) = members[fnname] - - if original_fn_signature is not None: - err = None - if len(sig.parameters) > len(original_fn_signature.parameters): - err = f"The signature of '{fnname}' does not match. Missing required parameters." - # Special check for self - if 'self' in sig.parameters and 'self' not in original_fn_signature.parameters: - err = f"The signature of '{fnname}' does not match. Missing required parameter self." - if err is not None: - raise ctx.wrap(UntypyTypeError( - expected=protocolchecker.describe(), - given=originalValue - )).with_header( - protoMismatchErrorMessage(original.__name__, protocolchecker.proto) - ).with_note(err) - paramDict = dict(zip(original_fn_signature.parameters, sig.parameters)) - else: - paramDict = {} - for k in sig.parameters: - paramDict[k] = k - - list_of_attr[fnname] = ProtocolWrappedFunction(original_fn, - sig, - baseArgDict, - protocolchecker, - paramDict, - fc).build() - - name = f"WyppTypeCheck({original.__name__}, {protocolchecker.describe()})" - return wrapper.wrap(originalValue, list_of_attr, name, extra={'ctx': ctx}, simple=simple) - -def _getWrappedName(fn): - if hasattr(fn, '__name__'): - return fn.__name__ - elif hasattr(fn, 'fget'): - return fn.fget.__name__ - else: - raise ValueError(f'Cannot determine name of {fn}') - -def _isProperty(fn): - return isinstance(fn, property) - -class ProtocolWrappedFunction(WrappedFunction): - - def __init__(self, - inner: Union[Callable, WrappedFunction], - signature: inspect.Signature, - checker: Dict[str, TypeChecker], # maps argument names from the protocol to checkers - protocol: ProtocolChecker, - baseArgs: Dict[str, str], # maps arguments names of the implementing class to argument names of the protocol - fc: FunctionCondition): - self.inner = inner - self.signature = signature - self.parameters = list(self.signature.parameters.values()) - self.checker = checker - self.baseArgs = baseArgs - self.protocol = protocol - self.fc = fc - self.fast_sig = typedfun.is_fast_sig(self.parameters, self.fc) - - def build(self): - fn = WrappedFunction.find_original(self.inner) - name = _getWrappedName(fn) - fn_of_protocol = getattr(self.protocol.proto, name) - if hasattr(fn_of_protocol, '__wf'): - fn_of_protocol = getattr(fn_of_protocol, '__wf') - - def wrapper(me, *args, **kwargs): - inner_object = me.__wrapped__ - inner_ctx = me.__extra__['ctx'] - - caller = sys._getframe(1) - (args, kwargs, bind1) = self.wrap_arguments(lambda n: ArgumentExecutionContext(fn_of_protocol, caller, n), - (inner_object, *args), kwargs) - if isinstance(self.inner, WrappedFunction): - (args, kwargs, bind2) = self.inner.wrap_arguments( - lambda n: ProtocolArgumentExecutionContext(self, self.baseArgs[n], n, - inner_object, - inner_ctx), - args, kwargs) - if _isProperty(fn): - ret = fn - else: - try: - ret = fn(*args, **kwargs) - except Exception as e: - # If an exception occurs here, we find an additional stack frame - # on top of the traceback. We add the __wrapped__ attribute so that - # traceback formatting mechanism can remove this additional frame. - e.__wrapped__ = True - tb = e.__traceback__ - if tb: - tb = tb.tb_next - raise e.with_traceback(tb) - if isinstance(self.inner, WrappedFunction): - ret = self.inner.wrap_return(args, kwargs, ret, bind2, - ProtocolReturnExecutionContext(self, ResponsibilityType.IN, inner_object, inner_ctx)) - return self.wrap_return(args, kwargs, ret, bind1, - ProtocolReturnExecutionContext(self, ResponsibilityType.OUT, inner_object, inner_ctx)) - - async def async_wrapper(*args, **kwargs): - raise AssertionError("Not correctly implemented see wrapper") - - if inspect.iscoroutine(self.inner): - w = async_wrapper - else: - w = wrapper - - setattr(w, '__wrapped__', fn) - setattr(w, '__name__', name) - setattr(w, '__signature__', self.signature) - setattr(w, '__wf', self) - return w - - def get_original(self): - return self.inner - - def wrap_arguments(self, ctxprv: WrappedFunctionContextProvider, args, kwargs): - return typedfun.wrap_arguments(self.parameters, self.checker, self.signature, - self.fc, self.fast_sig, ctxprv, args, kwargs, expectSelf=True) - - def wrap_return(self, args, kwargs, ret, bindings, ctx: ExecutionContext): - fc_pair = None - if self.fc is not None: - fc_pair = (self.fc, bindings) - return typedfun.wrap_return(self.checker['return'], args, kwargs, ret, fc_pair, ctx) - - def describe(self) -> str: - fn = WrappedFunction.find_original(self.inner) - return f"{fn.__name__}" + str(self.signature) - - def checker_for(self, name: str) -> TypeChecker: - if name == 'return': - k = 'return' - else: - k = self.baseArgs[name] - return self.checker[k] - - def declared(self) -> Location: - fn = WrappedFunction.find_original(self.inner) - return WrappedFunction.find_location(getattr(self.protocol.proto, fn.__name__)) - - -class ProtocolReturnExecutionContext(ExecutionContext): - def __init__(self, wf: ProtocolWrappedFunction, invert: ResponsibilityType, me: Any, ctx: ExecutionContext): - self.wf = wf - self.invert = invert - self.me = me - self.ctx = ctx - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - err = ReturnExecutionContext(self.wf).wrap(err) - - if err.responsibility_type is self.invert: - return err - responsable = WrappedFunction.find_location(self.wf) - (decl, ind) = err.next_type_and_indicator() - err = err.with_inverted_responsibility_type() - err = err.with_frame(Frame( - decl, - ind, - declared=self.wf.declared(), - responsable=responsable - )) - - inner = self.wf.inner - if isinstance(inner, WrappedFunction): - err = err.with_note( - f"The return value of method '{WrappedFunction.find_original(self.wf).__name__}' does violate the {self.wf.protocol.protocol_type()} '{self.wf.protocol.proto.__name__}'.") - err = err.with_note( - f"The annotation '{inner.checker_for('return').describe()}' is incompatible with the {self.wf.protocol.protocol_type()}'s annotation '{self.wf.checker_for('return').describe()}'\nwhen checking against the following value:") - - previous_chain = UntypyTypeError( - self.me, - f"{self.wf.protocol.protoname()}" - ).with_header( - protoMismatchErrorMessage(type(self.me).__name__, self.wf.protocol) - ) - - previous_chain = self.ctx.wrap(previous_chain) - if isInternalProtocol(self.wf.protocol): - return previous_chain - else: - return err.with_previous_chain(previous_chain) - - - -class ProtocolArgumentExecutionContext(ExecutionContext): - def __init__(self, wf: ProtocolWrappedFunction, - base_arg: str, this_arg: str, me: Any, ctx: ExecutionContext): - self.wf = wf - self.base_arg = base_arg - self.this_arg = this_arg - self.me = me - self.ctx = ctx - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - protocol = self.wf.protocol.proto - - (original_expected, _ind) = err.next_type_and_indicator() - err = ArgumentExecutionContext(self.wf, None, self.base_arg).wrap(err) - - responsable = WrappedFunction.find_location(self.wf) - - (decl, ind) = err.next_type_and_indicator() - err = err.with_frame(Frame( - decl, - ind, - declared=self.wf.declared(), - responsable=responsable - )) - - base_expected = self.wf.checker_for(self.this_arg).describe() - if self.base_arg == self.this_arg: - err = err.with_note( - f"Argument {self.this_arg} of method {WrappedFunction.find_original(self.wf).__name__} " - f"violates the type declared by the " - f"{self.wf.protocol.protocol_type()} {self.wf.protocol.proto.__name__}.") - else: - err = err.with_note( - f"Argument {self.this_arg} of method {WrappedFunction.find_original(self.wf).__name__} " - f"violates the type declared for {self.base_arg} in " - f"{self.wf.protocol.protocol_type()} {self.wf.protocol.proto.__name__}.") - err = err.with_note( - f"Annotation {original_expected} is incompatible with the " - f"{self.wf.protocol.protocol_type()}'s annotation " - f"{base_expected}.") - - previous_chain = UntypyTypeError( - self.me, - f"{self.wf.protocol.protoname()}" - ).with_header( - protoMismatchErrorMessage(type(self.me).__name__, self.wf.protocol) - ) - - # Protocols can either be declared explicit or implicit. - if protocol in type(self.me).__mro__: - # If it is declared explicit (e.g. Inheritance) the - # declaration of the inheritance has to be blamed. - previous_chain = previous_chain.with_frame(Frame( - # /- Could also be `self.wf.describe()`, which would put "right" signature as "context:". - # v But this info may be better suited in the note. - *previous_chain.next_type_and_indicator(), - declared=Location.from_code(type(self.me)), - responsable=Location.from_code(type(self.me)) - )).with_frame(Frame( - *previous_chain.next_type_and_indicator(), - declared=self.wf.declared(), - responsable=responsable - )) - else: - # Else: We need to explain how this protocol was declared. - previous_chain = self.ctx.wrap(previous_chain) - - if isInternalProtocol(self.wf.protocol): - return previous_chain - else: - return err.with_previous_chain(previous_chain) - - -class SimpleInstanceOfChecker(TypeChecker): - def __init__(self, annotation: type, ctx: CreationContext): - self.annotation = annotation - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if isinstance(arg, self.annotation): - return arg - else: - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - - def describe(self) -> str: - return self.annotation.__name__ - - def base_type(self) -> Any: - return [self.annotation] diff --git a/python/deps/untypy/untypy/impl/simple.py b/python/deps/untypy/untypy/impl/simple.py deleted file mode 100644 index 4cb39da7..00000000 --- a/python/deps/untypy/untypy/impl/simple.py +++ /dev/null @@ -1,68 +0,0 @@ -import abc -from typing import Any, Optional, Callable - -from untypy.error import UntypyTypeError -from untypy.impl.protocol import ProtocolChecker -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext - - -class SimpleFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - ta = type(annotation) - if ta is type or ta is abc.ABCMeta or annotation is Callable: - return SimpleChecker(annotation, ctx) - else: - return None - - -class ParentProtocolChecker(ProtocolChecker): - def protocol_type(self) -> str: - return "parent" - -def simpleTypeCompat(x: Any, ty: type): - xTy = type(x) - return xTy is ty or (ty is float and xTy is int) or \ - (ty is complex and (xTy is int or xTy is float)) - -class SimpleChecker(TypeChecker): - annotation: type - always_wrap: bool = False - parent_checker: Optional[Callable[[Any, ExecutionContext], Any]] - - def __init__(self, annotation: type, ctx: CreationContext): - self.annotation = annotation - self.always_wrap = False - - # use protocol like wrapping only if there are some signatures - if ctx.should_be_inheritance_checked(annotation): - if hasattr(annotation, '__patched'): - p = ParentProtocolChecker(annotation, ctx) - self.parent_checker = p.check_and_wrap - else: - self.parent_checker = None - - - def may_be_wrapped(self) -> bool: - return True - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if simpleTypeCompat(arg, self.annotation) and not self.always_wrap: - return arg - if isinstance(arg, self.annotation): - if self.parent_checker is None: - return arg - else: - return self.parent_checker(arg, ctx) - else: - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - - def describe(self) -> str: - a = self.annotation - if hasattr(a, '__name__'): - return a.__name__ - else: - return str(a) - - def base_type(self) -> Any: - return [self.annotation] diff --git a/python/deps/untypy/untypy/impl/string_forward_refs.py b/python/deps/untypy/untypy/impl/string_forward_refs.py deleted file mode 100644 index 85fa8a21..00000000 --- a/python/deps/untypy/untypy/impl/string_forward_refs.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Any, Optional - -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext -from untypy.util.typehints import get_type_hints - - -class StringForwardRefFactory(TypeCheckerFactory): - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if type(annotation) is str: - eval_args = [annotation, globals()] - local = ctx.eval_context() - if local is not None: - eval_args.append(local) - - def resolver(eval_args): - return eval(*eval_args) - - annotation = get_type_hints(eval_args, ctx, resolver=resolver) - return ctx.find_checker(annotation) diff --git a/python/deps/untypy/untypy/impl/tuple.py b/python/deps/untypy/untypy/impl/tuple.py deleted file mode 100644 index dd5836bc..00000000 --- a/python/deps/untypy/untypy/impl/tuple.py +++ /dev/null @@ -1,103 +0,0 @@ -from typing import Any, Optional, Tuple - -from untypy.error import UntypyTypeError, Frame -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext -from untypy.util import CompoundTypeExecutionContext - -TupleType = type(Tuple[str, int]) -TupleTypeB = type(tuple[str, int]) - - -class TupleFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - if (type(annotation) is TupleType or type(annotation) is TupleTypeB) and annotation.__origin__ == tuple: - args = annotation.__args__ - if len(args) == 2 and args[1] == Ellipsis: - checker = ctx.find_checker(args[0]) - if checker is None: - return None - else: - return VariadicTupleChecker(checker) - inner = [] - for arg in args: - checker = ctx.find_checker(arg) - if checker is None: - return None - else: - inner.append(checker) - - return TupleChecker(inner) - else: - return None - -class VariadicTupleChecker(TypeChecker): - inner: TypeChecker - - def __init__(self, inner: TypeChecker): - self.inner = inner - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if hasattr(arg, '__wrapped__'): - # no double wrapping - arg = getattr(arg, '__wrapped__') - if not type(arg) is tuple: - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - out = [] - for elm in arg: - out.append(self.inner.check_and_wrap(elm, VariadicTupleExecutionContext(ctx))) - return tuple(out) - - def base_type(self) -> Any: - return [tuple] - - def describe(self) -> str: - desc = self.inner.describe() - return f"tuple[{desc}, ...]" - - -class VariadicTupleExecutionContext(ExecutionContext): - upper: ExecutionContext - - def __init__(self, upper: ExecutionContext): - self.upper = upper - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - next_type, indicator = err.next_type_and_indicator() - err = err.with_frame(Frame( - f"tuple[{next_type}, ...]", - (" " * len("tuple[") + indicator), - None, - None - )) - return self.upper.wrap(err) - -class TupleChecker(TypeChecker): - inner: list[TypeChecker] - - def __init__(self, inner: list[TypeChecker]): - self.inner = inner - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - if not type(arg) is tuple or len(arg) != len(self.inner): - raise ctx.wrap(UntypyTypeError(arg, self.describe())) - - out = [] - idx = 0 - for elm, checker in zip(arg, self.inner): - out.append(checker.check_and_wrap(elm, TupleExecutionContext(ctx, self.inner, idx))) - idx += 1 - - return tuple(out) - - def base_type(self) -> Any: - return [tuple] - - def describe(self) -> str: - desc = lambda s: s.describe() - return f"tuple[{', '.join(map(desc, self.inner))}]" - - -class TupleExecutionContext(CompoundTypeExecutionContext): - def name(self): - return "tuple" diff --git a/python/deps/untypy/untypy/impl/union.py b/python/deps/untypy/untypy/impl/union.py deleted file mode 100644 index b6c973e1..00000000 --- a/python/deps/untypy/untypy/impl/union.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Any, Optional, Union - -from untypy.error import UntypyTypeError, UntypyAttributeError -from untypy.interfaces import TypeChecker, TypeCheckerFactory, CreationContext, ExecutionContext -from untypy.util import CompoundTypeExecutionContext - -import sys -pythonVersion = sys.version_info - -unionTypes = [type(Union[int, str])] -if pythonVersion >= (3, 10): - unionTypes.append(type(int | str)) - -class UnionFactory(TypeCheckerFactory): - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - t = type(annotation) - if t in unionTypes: - inner = [] - for arg in annotation.__args__: - checker = ctx.find_checker(arg) - if checker is None: - return None - else: - inner.append(checker) - - return UnionChecker(inner, ctx) - else: - return None - - -class UnionChecker(TypeChecker): - inner: list[TypeChecker] - - def __init__(self, inner: list[TypeChecker], ctx: CreationContext): - # especially Protocols must be checked in a specific order. - self.inner = sorted(inner, key=lambda t: -t.base_type_priority()) - dups = dict() - for checker in inner: - for base_type in checker.base_type(): - if base_type in dups: - raise ctx.wrap(UntypyAttributeError(f"{checker.describe()} is in conflict with " - f"{dups[base_type].describe()} " - f"in {self.describe()}. " - f"Types must be distinguishable inside a Union." - f"\nNote: Only one protocol is allowed inside a Union. " - f"Classes could implement multiple Protocols by accident." - f"\nNote: Multiple callables or generics inside a Union are also unsupported.")) - else: - dups[base_type] = checker - - def check_and_wrap(self, arg: Any, upper: ExecutionContext) -> Any: - idx = 0 - for checker in self.inner: - ctx = UnionExecutionContext(upper, self.inner, idx) - idx += 1 - try: - return checker.check_and_wrap(arg, ctx) - except UntypyTypeError as _e: - pass - - raise upper.wrap(UntypyTypeError( - arg, - self.describe() - )) - - def describe(self) -> str: - desc = lambda s: s.describe() - return f"Union[{', '.join(map(desc, self.inner))}]" - - def base_type(self) -> list[Any]: - out = [] - for checker in self.inner: - out.extend(checker.base_type()) - return out - - -class UnionExecutionContext(CompoundTypeExecutionContext): - def name(self): - return "Union" diff --git a/python/deps/untypy/untypy/interfaces.py b/python/deps/untypy/untypy/interfaces.py deleted file mode 100644 index d6ad5e17..00000000 --- a/python/deps/untypy/untypy/interfaces.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import inspect -from typing import Optional, Any, Callable, TypeVar, List, Tuple - -from untypy.error import UntypyTypeError, Location, UntypyAttributeError - - -class CreationContext: - def find_checker(self, annotation: Any) -> Optional[TypeChecker]: - raise NotImplementedError - - def declared_location(self) -> Location: - raise NotImplementedError - - def wrap(self, err: UntypyAttributeError) -> UntypyAttributeError: - raise NotImplementedError - - def resolve_typevar(self, var: TypeVar) -> Tuple[bool, Any]: - raise NotImplementedError - - def all_typevars(self) -> List[TypeVar]: - raise NotImplementedError - - def with_typevars(self, typevars: dict[TypeVar, Any]) -> CreationContext: - raise NotImplementedError - - def should_be_inheritance_checked(self, annotation: type) -> bool: - raise NotImplementedError - - def eval_context(self): - raise NotImplementedError - - -class ExecutionContext: - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - raise NotImplementedError - - -class TypeChecker: - - def describe(self) -> str: - raise NotImplementedError - - def may_be_wrapped(self) -> bool: - return False - - def base_type(self) -> list[Any]: - raise NotImplementedError(f'base_type({self})') - - # Higher Priority => checked first inside Union. - def base_type_priority(self) -> int: - return 0 - - def check_and_wrap(self, arg: Any, ctx: ExecutionContext) -> Any: - raise NotImplementedError - - -class TypeCheckerFactory: - - def create_from(self, annotation: Any, ctx: CreationContext) -> Optional[TypeChecker]: - raise NotImplementedError - - -WrappedFunctionContextProvider = Callable[[str], ExecutionContext] - - -class WrappedFunction: - def get_original(self): - raise NotImplementedError - - def wrap_arguments(self, ctxprv: WrappedFunctionContextProvider, args, kwargs): - raise NotImplementedError - - def wrap_return(self, ret, bindings, ctx: ExecutionContext): - raise NotImplementedError - - def describe(self) -> str: - raise NotImplementedError - - def checker_for(self, name: str) -> TypeChecker: - raise NotImplementedError - - @staticmethod - def find_original(fn): - if hasattr(fn, '__original'): - return WrappedFunction.find_original(getattr(fn, '__original')) - elif isinstance(fn, WrappedFunction): - return WrappedFunction.find_original(fn.get_original()) - elif hasattr(fn, '__wf'): - return WrappedFunction.find_original(getattr(fn, '__wf').get_original()) - else: - return fn - - @staticmethod - def find_location(fn) -> Optional[Location]: - fn = WrappedFunction.find_original(fn) - try: - return Location( - file=inspect.getfile(fn), - line_no=inspect.getsourcelines(fn)[1], - line_span=len(inspect.getsourcelines(fn)[0]), - ) - except: # Failes on builtins - return None diff --git a/python/deps/untypy/untypy/patching/__init__.py b/python/deps/untypy/untypy/patching/__init__.py deleted file mode 100644 index 35a1e6c8..00000000 --- a/python/deps/untypy/untypy/patching/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -import inspect -from collections import namedtuple -from types import FunctionType -from typing import Callable - -from untypy.error import Location -from untypy.impl import DefaultCreationContext -from untypy.interfaces import WrappedFunction -from untypy.util.typedfunction import TypedFunctionBuilder - -Config = namedtuple('PatchConfig', ['verbose', 'checkedprefixes']) -DefaultConfig = Config(verbose=False, checkedprefixes=[""]) -not_patching = ['__class__'] - -GlobalPatchedList = set() - - -def patch_class(clas: type, cfg: Config): - if clas in GlobalPatchedList: - return clas - GlobalPatchedList.add(clas) - - try: - ctx = DefaultCreationContext( - typevars=dict(), - declared_location=Location( - file=inspect.getfile(clas), - line_no=inspect.getsourcelines(clas)[1], - line_span=len(inspect.getsourcelines(clas)[0]), - ), checkedpkgprefixes=cfg.checkedprefixes) - except (TypeError, OSError) as e: # Built in types - ctx = DefaultCreationContext( - typevars=dict(), - declared_location=Location( - file="", - line_no=0, - line_span=1 - ), checkedpkgprefixes=cfg.checkedprefixes, - ) - - setattr(clas, '__patched', True) - -def wrap_function(fn: FunctionType, cfg: Config) -> Callable: - if len(inspect.getfullargspec(fn).annotations) > 0: - if cfg.verbose: - print(f"Patching Function: {fn.__name__}") - return TypedFunctionBuilder(fn, DefaultCreationContext( - typevars=dict(), - declared_location=WrappedFunction.find_location(fn), - checkedpkgprefixes=cfg.checkedprefixes, eval_context=fn.__globals__)).build() - else: - return fn - - -def wrap_class(a: type, cfg: Config) -> Callable: - return WrappedType(a, DefaultCreationContext( - typevars=dict(), - declared_location=Location.from_code(a), - checkedpkgprefixes=cfg.checkedprefixes)) diff --git a/python/deps/untypy/untypy/patching/ast_transformer.py b/python/deps/untypy/untypy/patching/ast_transformer.py deleted file mode 100644 index f48ee49b..00000000 --- a/python/deps/untypy/untypy/patching/ast_transformer.py +++ /dev/null @@ -1,165 +0,0 @@ -import ast -from typing import Callable, List, Optional, Any - - -class UntypyAstTransformer(ast.NodeTransformer): - def visit_Module(self, node: ast.Module): - for i, child in enumerate(node.body): - if isinstance(child, ast.ImportFrom) and child.module == '__future__': - continue # from __future__ import ... - elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant): - continue # module docstring - elif _is_untypy_import(node): - break - else: - node.body.insert(i, ast.Import(names=[ast.alias('untypy', None)])) - break - - self.generic_visit(node) - return node - - def visit_FunctionDef(self, node: ast.FunctionDef): - node.decorator_list.insert(0, ast.Attribute(ast.Name("untypy", ast.Load()), "patch", ast.Load())) - self.generic_visit(node) - return node - - def visit_ClassDef(self, node: ast.FunctionDef): - node.decorator_list.insert(0, ast.Attribute(ast.Name("untypy", ast.Load()), "patch", ast.Load())) - self.generic_visit(node) - return node - - def visit_Expr(self, node: ast.Expr): - val = node.value - if _is_untypy_patch_call(val): - return ast.Expr(ast.Constant("# untypy.enable()")) - else: - self.generic_visit(node) - return node - - -class UntypyAstImportTransformer(ast.NodeTransformer): - def __init__(self, predicate: Callable[[str], bool], module_path: List[str]): - self.predicate = predicate - self.module_path = module_path - - def resolve_relative_name(self, name: str, levels: Optional[int]) -> str: - if levels is None: - return name - elif levels == 1: - return '.'.join(self.module_path + list(name.split('.'))) - else: - prefix = self.module_path[:-(levels - 1)] - return '.'.join(prefix + list(name.split('.'))) - - def visit_Module(self, node: ast.Module): - inserts = [] - need_insert_utypy_imp = True - need_insert_utypy_idx = 0 - for i, child in enumerate(node.body): - if _is_untypy_import(node): - need_insert_utypy_imp = False - elif isinstance(child, ast.ImportFrom) and child.module == '__future__': - need_insert_utypy_idx += 1 - elif isinstance(child, ast.ImportFrom) and self.predicate( - self.resolve_relative_name(child.module, child.level)): - for alias in child.names: - if alias.asname is None: - destname = alias.name - else: - destname = alias.asname - # $destname = untypy.wrap_import($destname) - call = ast.Call(ast.Attribute(ast.Name("untypy", ast.Load()), "wrap_import", ast.Load()), - [ast.Name(destname, ast.Load())], []) - expr = ast.Assign([ast.Name(destname, ast.Store())], call) - node.body.insert(i + 1, expr) - - elif isinstance(child, ast.Import): - for alias in child.names: - if self.predicate(alias.name): - if alias.asname is None: - destname = alias.name - else: - destname = alias.asname - - # $destname = untypy.wrap_import($destname) - # python ast weirdness - def create_attribute(levels: List[str], op=ast.Store()): - if len(levels) == 1: - return ast.Name(levels[0], ctx=op) - else: - return ast.Attribute(create_attribute(levels[:-1], ast.Load()), levels[-1], ctx=op) - - destpath = destname.split(".") - call = ast.Call(ast.Attribute(ast.Name("untypy", ast.Load()), "wrap_import", ast.Load()), - [create_attribute(destpath, ast.Load())], []) - expr = ast.Assign([create_attribute(destpath)], call) - # inserts.append((i + 1, expr)) # insert after - node.body.insert(i + 1, expr) - - # TODO BUG: Multiple imports index mix up - # TODO BUG: - for (idx, expr) in inserts: - node.body.insert(idx, expr) - - if need_insert_utypy_imp: - node.body.insert(need_insert_utypy_idx, ast.Import(names=[ast.alias('untypy', None)])) - - self.generic_visit(node) - return node - - def visit_Expr(self, node: ast.Expr): - val = node.value - if _is_untypy_patch_call(val): - return ast.Expr(ast.Constant("# untypy.enable()")) - else: - self.generic_visit(node) - return node - -def _is_untypy_patch_call(node): - if isinstance(node, ast.Expr): - node = node.value - - return (isinstance(node, ast.Call) - and isinstance(node.func, ast.Attribute) - and isinstance(node.func.value, ast.Name) - and node.func.value.id == 'untypy' - and (node.func.attr == 'enable' or node.func.attr == 'enable_on_imports')) - - -def _is_untypy_import(node): - return (isinstance(node, ast.Import) - and len(node.names) == 1 - and isinstance(node.names[0], ast.alias) - and node.names[0].name == 'untypy') - - -def did_no_code_run_before_untypy_enable(node: ast.Module) -> bool: - for child in node.body: - if isinstance(child, ast.ImportFrom) and child.module == '__future__': - continue # from __future__ import ... - elif _is_untypy_import(child): - continue - elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant): - continue # module docstring - elif _is_untypy_patch_call(child): - return True - else: - break - - # untypy.enable() was not first. It is still okay if this call does not exist. - class DidNoCodeRunVisitor(ast.NodeVisitor): - def __init__(self): - self.has_untypycall = False - - def visit_Call(self, node): - if _is_untypy_patch_call(node): - self.has_untypycall = True - self.generic_visit(node) - return node - - visitor = DidNoCodeRunVisitor() - visitor.visit(node) - if visitor.has_untypycall: - return False - else: - return True diff --git a/python/deps/untypy/untypy/patching/import_hook.py b/python/deps/untypy/untypy/patching/import_hook.py deleted file mode 100644 index ac44cb02..00000000 --- a/python/deps/untypy/untypy/patching/import_hook.py +++ /dev/null @@ -1,65 +0,0 @@ -import ast -import importlib -from collections.abc import Callable -from importlib.abc import MetaPathFinder -from importlib.machinery import SourceFileLoader -from importlib.util import decode_source - - -def install_import_hook(should_patch_predicate: Callable[[str, str], bool], - transformer: Callable[[str], ast.NodeTransformer]): - import sys - - already_patched = next((f for f in sys.meta_path if isinstance(f, UntypyFinder)), None) - if already_patched is not None: - return - - original_finder = next(f for f in sys.meta_path if hasattr(f, '__name__') and f.__name__ == 'PathFinder' and hasattr(f, 'find_spec')) - sys.meta_path.insert(0, UntypyFinder(original_finder, should_patch_predicate, transformer)) - - -class UntypyFinder(MetaPathFinder): - - def __init__(self, inner_finder: MetaPathFinder, should_patch_predicate: Callable[[str, str], bool], - transformer: Callable[[str], ast.NodeTransformer]): - self.inner_finder = inner_finder - self.should_patch_predicate = should_patch_predicate - self.transformer = transformer - - def find_spec(self, fullname, path=None, target=None): - if not self.should_instrument(fullname): - return None - - inner_spec = self.inner_finder.find_spec(fullname, path, target) - if inner_spec is not None and isinstance(inner_spec.loader, SourceFileLoader): - inner_spec.loader = UntypyLoader(inner_spec.loader.name, inner_spec.loader.path, self.transformer) - return inner_spec - - def should_instrument(self, module_name: str) -> bool: - return self.should_patch_predicate(module_name) - - -class UntypyLoader(SourceFileLoader): - - def __init__(self, fullname, path, transformer: Callable[[str, str], ast.NodeTransformer]): - super().__init__(fullname, path) - self.transformer = transformer - - def source_to_code(self, data, path, *, _optimize=-1): - source = decode_source(data) - tree = compile(source, path, 'exec', ast.PyCF_ONLY_AST, - dont_inherit=True, optimize=_optimize) - self.transformer(self.name.split('.'), self.path).visit(tree) - ast.fix_missing_locations(tree) - return compile(tree, path, 'exec', dont_inherit=True, optimize=_optimize) - - def exec_module(self, module) -> None: - # cache_from_source has to be patched to prevent load from cache - # this enables patching of AST - # See https://github.com/agronholm/typeguard/blob/89c1478bd33bcf9a7cccc2c962ebeaa034e51908/src/typeguard/importhook.py#L12 - original = getattr(importlib._bootstrap_external, 'cache_from_source') - try: - setattr(importlib._bootstrap_external, 'cache_from_source', lambda *args: None) - return super().exec_module(module) - finally: - setattr(importlib._bootstrap_external, 'cache_from_source', original) diff --git a/python/deps/untypy/untypy/patching/standalone_checker.py b/python/deps/untypy/untypy/patching/standalone_checker.py deleted file mode 100644 index e31428c2..00000000 --- a/python/deps/untypy/untypy/patching/standalone_checker.py +++ /dev/null @@ -1,59 +0,0 @@ -import sys -from typing import Any, Callable - -from untypy.error import Location, UntypyAttributeError, UntypyTypeError, Frame, UntypyNameError -from untypy.impl import DefaultCreationContext -from untypy.interfaces import ExecutionContext - - -class StandaloneChecker: - def __init__(self, annotation: Callable[[], Any], declared: Any, cfg, ctx=None): - self._checker = None - self.annotation = annotation - self.declared = declared - self.cfg = cfg - self.ctx = ctx - - def get_checker(self): - if self._checker: - return self._checker - - ctx = DefaultCreationContext( - typevars=dict(), - declared_location=Location.from_code(self.declared), - checkedpkgprefixes=self.cfg.checkedprefixes) - try: - annotation = self.annotation() - except NameError as ne: - raise ctx.wrap(UntypyNameError( - f"{ne}.\nType annotation could not be resolved." - )) - - checker = ctx.find_checker(annotation) - if checker is None: - raise ctx.wrap(UntypyAttributeError(f"\n\tUnsupported type annotation: {self.annotation}\n")) - self._checker = checker - return checker - - def __call__(self, val): - frame = sys._getframe(2) - checker = self.get_checker() - ctx = self.ctx or StandaloneCheckerContext(frame, self.declared) - return checker.check_and_wrap(val, ctx) - - def __repr__(self): - return f"" - - -class StandaloneCheckerContext(ExecutionContext): - def __init__(self, caller, declared): - self.caller = caller - self.declared = declared - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - t, i = err.next_type_and_indicator() - return err.with_frame(Frame( - t, i, - responsable=Location.from_stack(self.caller), - declared=Location.from_code(self.declared) - )) diff --git a/python/deps/untypy/untypy/util/__init__.py b/python/deps/untypy/untypy/util/__init__.py deleted file mode 100644 index b9e3a845..00000000 --- a/python/deps/untypy/untypy/util/__init__.py +++ /dev/null @@ -1,260 +0,0 @@ -import inspect -import types -from typing import Optional, Union, List - -from untypy.error import UntypyTypeError, Frame, Location -from untypy.interfaces import ExecutionContext, TypeChecker, WrappedFunction -from untypy.util.display import IndicatorStr -from untypy.util.return_traces import get_last_return - - -class ReplaceTypeExecutionContext(ExecutionContext): - - def __init__(self, upper: Optional[ExecutionContext], name: str): - self.upper = upper - self.name = name - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - err = err.with_frame(Frame( - self.name, - None, None, None - )) - - if self.upper is not None: - err = self.upper.wrap(err) - return err - - -class CompoundTypeExecutionContext(ExecutionContext): - upper: ExecutionContext - checkers: list[TypeChecker] - idx: int - - def __init__(self, upper: ExecutionContext, checkers: list[TypeChecker], idx: int): - self.upper = upper - self.checkers = checkers - self.idx = idx - - def declared(self) -> Optional[Location]: - return None - - def responsable(self) -> Optional[Location]: - return None - - def name(self) -> str: - raise NotImplementedError - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - type_declared = self.name() + "[" - indicator = " " * len(type_declared) - - for i, checker in enumerate(self.checkers): - if i == self.idx: - next_type, next_indicator = err.next_type_and_indicator() - type_declared += next_type - indicator += next_indicator - else: - type_declared += checker.describe() - indicator += " " * len(checker.describe()) - - if i != len(self.checkers) - 1: # not last element - type_declared += ", " - indicator += " " - - type_declared += "]" - - err = err.with_frame(Frame( - type_declared, - indicator, - declared=self.declared(), - responsable=self.responsable(), - )) - - return self.upper.wrap(err) - - -class NoResponsabilityWrapper(ExecutionContext): - upper: ExecutionContext - - def __init__(self, upper: ExecutionContext): - self.upper = upper - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - full = self.upper.wrap(err) - - # now remove responsability in frames: - frames_to_add = [] - for frame in full.frames: - if frame not in err.frames: - frame.responsable = None - frames_to_add.append(frame) - - for frame in frames_to_add: - err = err.with_frame(frame) - - for note in full.notes: - err = err.with_note(note) - - if full.previous_chain is not None: - err = err.with_previous_chain(full.previous_chain) - - return err - - -class ReturnExecutionContext(ExecutionContext): - fn: WrappedFunction - - def __init__(self, fn: WrappedFunction): - self.reti_loc = get_last_return() - self.fn = fn - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - (next_ty, indicator) = err.next_type_and_indicator() - return_id = IndicatorStr(next_ty, indicator) - - original = WrappedFunction.find_original(self.fn) - - try: - signature = inspect.signature(original) # TODO:!!! FIX BUILTINS - front_sig = [] - for name in signature.parameters: - front_sig.append(f"{name}: {self.fn.checker_for(name).describe()}") - front_sig = f"{format_name(original)}(" + (", ".join(front_sig)) + ") -> " - return_id = IndicatorStr(front_sig) + return_id - except: - return_id = IndicatorStr("???") - - declared = WrappedFunction.find_location(self.fn) - responsable = declared - - if responsable is not None: - if err.expected is not None and err.given is None: - # Missing Return-Value? - err = err.with_note("Did you miss a return statement?") - last_line = responsable.line_no + responsable.line_span - 1 - responsable = responsable.narrow_in_span((responsable.file, last_line)) - else: - responsable = responsable.narrow_in_span(self.reti_loc) - - return err.with_frame(Frame( - return_id.ty, - return_id.indicator, - declared=declared, - responsable=responsable, - )) - -def format_name(orig): - n = orig.__name__ - if inspect.isclass(orig): - k = 'class' - if hasattr(orig, '__kind'): - k = getattr(orig, '__kind') - if k: - return f"{k} constructor {n}" - return n - - -class ArgumentExecutionContext(ExecutionContext): - n: WrappedFunction - stack: inspect.FrameInfo - argument_name: str - - def __init__(self, - fn: Union[WrappedFunction, types.FunctionType], - stack: Optional[inspect.FrameInfo], - argument_name: str, - declared: Optional[Location] = None, - upper: ExecutionContext = None): - self.fn = fn - self.stack = stack - self.argument_name = argument_name - self.declared = declared - self.upper = upper - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - (next_ty, indicator) = err.next_type_and_indicator() - error_id = IndicatorStr(next_ty, indicator) - - original = WrappedFunction.find_original(self.fn) - try: - signature = inspect.signature(original) - except ValueError: - # fails on some built-ins - signature = inspect.signature(self.fn) - - wf = None - if (hasattr(self.fn, '__wf')): - wf = getattr(self.fn, '__wf') - elif isinstance(self.fn, WrappedFunction): - wf = self.fn - - arglist = [] - for name in signature.parameters: - if name is self.argument_name: - arglist.append(IndicatorStr(f"{name}: ") + error_id) - else: - if wf is not None: - arglist.append(IndicatorStr(f"{name}: {wf.checker_for(name).describe()}")) - else: - arglist.append(IndicatorStr(f"{name}")) - - id = IndicatorStr(f"{format_name(original)}(") + IndicatorStr(", ").join(arglist) - - if wf is not None: - id += IndicatorStr(f") -> {wf.checker_for('return').describe()}") - else: - id += IndicatorStr(f")") - - if self.declared is None: - declared = WrappedFunction.find_location(self.fn) - else: - declared = self.declared - - if self.stack is not None: - responsable = Location.from_stack(self.stack) - else: - responsable = None - - frame = Frame( - id.ty, - id.indicator, - declared=declared, - responsable=responsable - ) - if self.upper: - err = self.upper.wrap(err) - return err.with_frame(frame) - - -class GenericExecutionContext(ExecutionContext): - def __init__(self, *, declared: Union[None, Location, List[Location]] = None, - responsable: Union[None, Location, List[Location]] = None, - upper_ctx: Optional[ExecutionContext] = None): - self.declared = declared - self.responsable = responsable - self.upper_ctx = upper_ctx - - def wrap(self, err: UntypyTypeError) -> UntypyTypeError: - declared = [] - if isinstance(self.declared, Location): - declared.append(self.declared) - if isinstance(self.declared, list): - declared.extend(self.declared) - - responsable = [] - if isinstance(self.responsable, Location): - responsable.append(self.responsable) - if isinstance(self.responsable, list): - responsable.extend(self.responsable) - - while len(declared) < len(responsable): declared.append(None) - while len(declared) > len(responsable): responsable.append(None) - - for (d, r) in zip(declared, responsable): - (t, i) = err.next_type_and_indicator() - err = err.with_frame(Frame(t, i, d, r)) - - if self.upper_ctx is not None: - return self.upper_ctx.wrap(err) - else: - return err diff --git a/python/deps/untypy/untypy/util/condition.py b/python/deps/untypy/untypy/util/condition.py deleted file mode 100644 index 7a053740..00000000 --- a/python/deps/untypy/untypy/util/condition.py +++ /dev/null @@ -1,130 +0,0 @@ -import inspect -import re -from collections.abc import Callable -from typing import Optional - -from untypy.error import UntypyAttributeError, UntypyTypeError, Frame -from untypy.interfaces import ExecutionContext, WrappedFunction, TypeChecker - -WrappedFunctionContextProvider = Callable[[str], ExecutionContext] - - -class FunctionCondition: - - def __init__(self): - self.precondition = [] - self.postcondition = [] - self.func = None - pass - - def prehook(self, boundargs, ctx: WrappedFunctionContextProvider): - for p in self.precondition: - bindings = {} - for name in inspect.signature(p).parameters: - if name in boundargs.arguments: - bindings[name] = boundargs.arguments[name] - else: - raise UntypyAttributeError( - f"Did not find argument {name} of precondition in function.", - locations=[ - WrappedFunction.find_location(p), - WrappedFunction.find_location(self.func), - ] - ) - if not p(**bindings): - lsource = find_lambdasource(p) - if lsource is not None: - expected = f"passing: {lsource}" - else: - expected = "passing precondition" - - err = UntypyTypeError( - bindings, - expected - ) - err = err.with_note("Failed precondition.") - err = err.with_frame(Frame( - expected, - "", - declared=WrappedFunction.find_location(p), - responsable=None, - )) - raise ctx(0).wrap(err) - - def posthook(self, ret, boundargs, ctx: ExecutionContext, checker: TypeChecker): - for p in self.postcondition: - bindings = {} - for name in inspect.signature(p).parameters: - if name == "ret": - bindings["ret"] = ret - elif name == "checker": - bindings["checker"] = checker - elif name == "ctx": - bindings["ctx"] = ctx - elif name in boundargs.arguments: - bindings[name] = boundargs.arguments[name] - else: - raise UntypyAttributeError( - f"Did not find argument {name} of postcondition in function.", - locations=[ - WrappedFunction.find_location(p), - WrappedFunction.find_location(self.func), - ] - ) - if not p(**bindings): - lsource = find_lambdasource(p) - if lsource is not None: - expected = f"passing: {lsource}" - else: - expected = "passing postcondition" - - given = ret.__repr__() - err = UntypyTypeError( - given, - expected, - ).with_note("Failed postcondition").with_frame(Frame( - expected, - "", - declared=WrappedFunction.find_location(p), - responsable=None, - )) - raise ctx.wrap(err) - - -def find_lambdasource(fn) -> Optional[str]: - """ - tries to retuns body of precondition or postcondition annotation - """ - try: - fn = WrappedFunction.find_original(fn) - source = inspect.getsource(fn).split('\n') - m = re.match(r'@[a-zA-Z_\.]+\((.*)\)', source[0]) - if m is not None and len(m.groups()) == 1: - return m.group(1) - except: - return None - -def _condgetfc(func): - if hasattr(func, "__fc"): - return getattr(func, "__fc") - else: - fc = FunctionCondition() - setattr(func, "__fc", fc) - fc.func = func - return fc - - -def precondition(cond): - def decorator(func): - fc = _condgetfc(func) - fc.precondition.append(cond) - return func - return decorator - - -def postcondition(cond): - def decorator(func): - fc = _condgetfc(func) - fc.postcondition.append(cond) - return func - return decorator diff --git a/python/deps/untypy/untypy/util/debug.py b/python/deps/untypy/untypy/util/debug.py deleted file mode 100644 index 9f2293be..00000000 --- a/python/deps/untypy/untypy/util/debug.py +++ /dev/null @@ -1,24 +0,0 @@ -import sys -import os - -def _getEnv(name, conv, default): - s = os.getenv(name) - if s is None: - return default - try: - return conv(s) - except: - return default - -_DEBUG = _getEnv("WYPP_DEBUG", bool, False) - -def enableDebug(debug: bool): - global _DEBUG - _DEBUG = debug - -def isDebug(): - return _DEBUG - -def debug(s): - if _DEBUG: - sys.stderr.write('[DEBUG] ' + s + '\n') diff --git a/python/deps/untypy/untypy/util/display.py b/python/deps/untypy/untypy/util/display.py deleted file mode 100644 index 8e73c940..00000000 --- a/python/deps/untypy/untypy/util/display.py +++ /dev/null @@ -1,44 +0,0 @@ -import inspect - - -class IndicatorStr: - ty: str - indicator: str - - def __init__(self, ty: str, indicator: str = ""): - n = 0 if ty is None else len(ty) - while len(indicator) < n: - indicator += " " - - self.ty = ty - self.indicator = indicator - - def __add__(self, other): - return IndicatorStr(self.ty + other.ty, self.indicator + other.indicator) - - def join(self, lst): - return IndicatorStr( - self.ty.join(map(lambda s: s.ty, lst)), - self.indicator.join(map(lambda s: s.indicator, lst)), - ) - - -def format_argument_values(args, kwargs): - allargs = [] - for a in args: - allargs.append(a.__repr__()) - for k,v in kwargs.items(): - allargs.append(k + "=" + v.__repr__()) - - return "(" + ", ".join(allargs) + ")" - - -def withIndefiniteArticle(s): - if s: - first = s[0] - if first in "aeiouyAEIOUY": - return 'an ' + s - else: - return 'a ' + s - else: - return s diff --git a/python/deps/untypy/untypy/util/return_traces.py b/python/deps/untypy/untypy/util/return_traces.py deleted file mode 100644 index 84eca8b0..00000000 --- a/python/deps/untypy/untypy/util/return_traces.py +++ /dev/null @@ -1,75 +0,0 @@ -import ast -from typing import * - - -class ReturnTraceManager: - """ - Stores file & line_no to every return idx - """ - def __init__(self): - self.lst = [] - - def next_id(self, reti : ast.Return, file: str) -> int: - i = len(self.lst) - self.lst.append((file, reti.lineno)) - return i - - def get(self, idx : int) -> (str, int): - return self.lst[idx] - -GlobalReturnTraceManager = ReturnTraceManager() -reti_loc: int = -1 - - -def before_return(idx : int): - global reti_loc - reti_loc = idx - - -def get_last_return() -> (str, int): - global reti_loc - if reti_loc < 0: - return ("", 0) # this will never match any real location - - # Note: this location is only used if it is in the span of the located function. - # See ReturnExecutionContext - return GlobalReturnTraceManager.get(reti_loc) - - -class ReturnTracesTransformer(ast.NodeTransformer): - - def __init__(self, file: str, manager=GlobalReturnTraceManager): - self.file = file - self.manager = manager - - def generic_visit(self, node) -> Any: - # See https://docs.python.org/3/library/ast.html - stmt_types = ["body", "orelse", "finalbody"] - - for stmt_type in stmt_types: - if not hasattr(node, stmt_type): - continue - statements : list = getattr(node, stmt_type) - - if not isinstance(statements, Iterable): - # skip lambda expressions - continue - - inserts = [] - for i,s in enumerate(statements): - if type(s) is ast.Return: - inserts.append((i, s)) - - # start at end so the early indexes still fit - inserts.reverse() - - for index, reti in inserts: - n = ast.Expr(value=ast.Call( - func=ast.Attribute(value=ast.Name(id='untypy', ctx=ast.Load()), attr='_before_return', ctx=ast.Load()), - args=[ast.Constant(value=self.manager.next_id(reti, self.file))], - keywords=[]) - ) - statements.insert(index, n) - - super(ast.NodeTransformer, self).generic_visit(node) - diff --git a/python/deps/untypy/untypy/util/source_utils.py b/python/deps/untypy/untypy/util/source_utils.py deleted file mode 100644 index 6847c21a..00000000 --- a/python/deps/untypy/untypy/util/source_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Tuple - - -class DisplayMatrix: - chars: dict - - def __init__(self, src: str, max_lines=5): - self.chars = {} - self.max_lines = max_lines - for y, line in enumerate(src.splitlines()): - for x, char in enumerate(line): - self.chars[(x, y)] = char - - def write(self, pos: Tuple[int, int], message: str): - for y, line in enumerate(message.splitlines()): - for x, char in enumerate(line): - self.chars[(x + pos[0], y + pos[1])] = char - - def __str__(self): - # Slow, but this is only in the "error" path - max_y = 0 - max_x = 0 - for x, y in self.chars: - max_y = max(max_y, y) - max_x = max(max_x, x) - - max_y = min(max_y, self.max_lines) - out = "" - for y in range(max_y + 1): - for x in range(max_x + 1): - if (x, y) in self.chars: - out += self.chars[(x, y)] - else: - out += " " - out += "\n" - return out diff --git a/python/deps/untypy/untypy/util/tranformer_combinator.py b/python/deps/untypy/untypy/util/tranformer_combinator.py deleted file mode 100644 index c86de398..00000000 --- a/python/deps/untypy/untypy/util/tranformer_combinator.py +++ /dev/null @@ -1,11 +0,0 @@ -import ast - - -class TransformerCombinator(ast.NodeTransformer): - - def __init__(self, *inner: ast.NodeTransformer): - self.inner = inner - - def visit(self, node): - for inner in self.inner: - inner.visit(node) diff --git a/python/deps/untypy/untypy/util/typedfunction.py b/python/deps/untypy/untypy/util/typedfunction.py deleted file mode 100644 index 8c6624a5..00000000 --- a/python/deps/untypy/untypy/util/typedfunction.py +++ /dev/null @@ -1,218 +0,0 @@ -import inspect -import sys -from typing import Callable, Dict, Optional, Any - -from untypy.error import UntypyAttributeError, UntypyNameError, UntypyTypeError -from untypy.impl.any import SelfChecker -from untypy.impl.choice import ChoiceChecker -from untypy.interfaces import WrappedFunction, TypeChecker, CreationContext, WrappedFunctionContextProvider, \ - ExecutionContext -from untypy.util import ArgumentExecutionContext, ReturnExecutionContext -from untypy.util.typehints import get_type_hints -from untypy.util.condition import FunctionCondition - -class FastTypeError(TypeError): - pass - -class TypedFunctionBuilder(WrappedFunction): - inner: Callable - signature: inspect.Signature - _checkers: Optional[Dict[str, TypeChecker]] - - special_args = ['self', 'cls'] - method_name_ignore_return = ['__init__'] - - def __init__(self, inner: Callable, ctx: CreationContext): - self.inner = inner - self.signature = inspect.signature(inner) - self.parameters = list(self.signature.parameters.values()) - self.ctx = ctx - self.fc = None - self._checkers = None - - try: - # try to detect errors like missing arguments as early as possible. - # but not all type annotations are resolvable yet. - # so ignore `UntypyNameError`s - self.checkers() - except UntypyNameError: - pass - except NameError: - pass - - # The self.fast_sig flags tells wether the signature supports fast matching of arguments. - # We identified (2022-05-05) that self.wrap_arguments is a performence bottleneck. - # With type annotations, the performance was factor 8.8 slower than without type - # annotations - # So we now have a fastlane for the common case where there are no kw and no variable - # arguments. For the fastlane, performance is only factor 3.7 slower. - if hasattr(self.inner, "__fc"): - self.fc = getattr(self.inner, "__fc") - - self.fast_sig = is_fast_sig(self.parameters, self.fc) - - def checkers(self) -> Dict[str, TypeChecker]: - if self._checkers is not None: - return self._checkers - - annotations = get_type_hints(self.inner, self.ctx) - - checkers = {} - checked_keys = list(self.signature.parameters) - - # Remove self and cls from checking - if len(checked_keys) > 0 and checked_keys[0] in self.special_args: - checkers[checked_keys[0]] = SelfChecker() - checked_keys = checked_keys[1:] - - for key in checked_keys: - if self.signature.parameters[key].annotation is inspect.Parameter.empty: - raise self.ctx.wrap( - UntypyAttributeError(f"Missing annotation for argument '{key}' of function {self.inner.__name__}\n" - "Partial annotation are not supported.")) - annotation = annotations[key] - checker = self.ctx.find_checker(annotation) - if checker is None: - raise self.ctx.wrap(UntypyAttributeError(f"\n\tUnsupported type annotation: {annotation}\n" - f"\tin argument '{key}'")) - else: - checkers[key] = checker - - if self.inner.__name__ in self.method_name_ignore_return: - checkers['return'] = SelfChecker() - else: - if not 'return' in annotations: - annotation = None - else: - annotation = annotations['return'] - return_checker = self.ctx.find_checker(annotation) - if return_checker is None: - raise self.ctx.wrap(UntypyAttributeError(f"\n\tUnsupported type annotation: {annotation}\n" - f"\tin return")) - - checkers['return'] = return_checker - - self._checkers = checkers - return checkers - - def build(self): - def wrapper(*args, **kwargs): - # first is this fn - caller = sys._getframe(1) - (args, kwargs, bindings) = self.wrap_arguments(lambda n: ArgumentExecutionContext(self, caller, n), args, - kwargs) - ret = self.inner(*args, **kwargs) - ret = self.wrap_return(args, kwargs, ret, bindings, ReturnExecutionContext(self)) - return ret - - if inspect.iscoroutine(self.inner): - raise UntypyAttributeError("Async functions are currently not supported.") - else: - w = wrapper - - setattr(w, '__wrapped__', self.inner) - setattr(w, '__name__', self.inner.__name__) - setattr(w, '__signature__', self.signature) - setattr(w, '__wf', self) - - # Copy useful attributes - # This is need for the detection of abstract classes - for attr in ['__isabstractmethod__']: - if hasattr(self.inner, attr): - setattr(w, attr, getattr(self.inner, attr)) - - return w - - def wrap_arguments(self, ctxprv: WrappedFunctionContextProvider, args, kwargs): - return wrap_arguments(self.parameters, self.checkers(), self.signature, self.fc, self.fast_sig, - ctxprv, args, kwargs) - - def wrap_return(self, args, kwargs, ret, bindings, ctx: ExecutionContext): - fc_pair = None - if self.fc is not None: - fc_pair = (self.fc, bindings) - return wrap_return(self.checkers()['return'], args, kwargs, ret, fc_pair, ctx) - - def describe(self): - return str(self.signature) - - def get_original(self): - return self.inner - - def checker_for(self, name: str) -> TypeChecker: - return self.checkers()[name] - -def _wrap_arguments_fast(parameters, checkers, ctxprv: WrappedFunctionContextProvider, args): - # Fast case: no kwargs, no rest args, self.fc is None. - # (self.fc is used for pre- and postconditions, a rarely used feature.) - # In this case, the order parameter names in args and self.parameters is the - # same, so we can simply match them by position. As self.fc is None, we do not - # need to build a inspect.BoundArguments object. - params = parameters - n = len(params) - n_args = len(args) - if n_args > n: - raise FastTypeError(f"too many positional arguments") - wrapped_args = [None] * n - for i in range(n): - p = params[i] - name = params[i].name - if i < n_args: - a = args[i] - else: - a = p.default - if a == inspect._empty: - raise FastTypeError(f"missing a required argument: {name!r}") - check = checkers[name] - ctx = ctxprv(name) - wrapped = check.check_and_wrap(a, ctx) - wrapped_args[i] = wrapped - return (wrapped_args, {}, None) - -def wrap_arguments(parameters, checkers, signature, fc, fast_sig, - ctxprv: WrappedFunctionContextProvider, args, kwargs, expectSelf=False): - if not kwargs and fast_sig: - try: - return _wrap_arguments_fast(parameters, checkers, ctxprv, args) - except FastTypeError as e: - err = UntypyTypeError(header=str(e)) - if expectSelf and "self" not in parameters: - err = err.with_note("Hint: 'self'-parameter was omitted in declaration.") - raise ctxprv("").wrap(err) - try: - bindings = signature.bind(*args, **kwargs) - except TypeError as e: - err = UntypyTypeError(header=str(e)) - if expectSelf and "self" not in parameters: - err = err.with_note("Hint: 'self'-parameter was omitted in declaration.") - raise ctxprv("").wrap(err) - bindings.apply_defaults() - if fc is not None: - fc.prehook(bindings, ctxprv) - for name in bindings.arguments: - check = checkers[name] - ctx = ctxprv(name) - bindings.arguments[name] = check.check_and_wrap(bindings.arguments[name], ctx) - return bindings.args, bindings.kwargs, bindings - -def is_fast_sig(parameters, fc): - if fc: - return False - fast_sig = True - for p in parameters: - # See https://docs.python.org/3/glossary.html#term-parameter for the - # different kinds of parameters - if p.kind != inspect._POSITIONAL_ONLY and p.kind != inspect._POSITIONAL_OR_KEYWORD: - fast_sig = False - break - return fast_sig - -def wrap_return(check: TypeChecker, args: list[Any], kwds: dict[str, Any], ret: Any, - fc_pair: tuple[FunctionCondition, inspect.BoundArguments], ctx: ExecutionContext): - if isinstance(check, ChoiceChecker): - check = check.get_checker(args, kwds) - result = check.check_and_wrap(ret, ctx) - if fc_pair is not None: - fc, bindings = fc_pair - fc.posthook(result, bindings, ctx, check) - return result diff --git a/python/deps/untypy/untypy/util/typehints.py b/python/deps/untypy/untypy/util/typehints.py deleted file mode 100644 index 8f9947c7..00000000 --- a/python/deps/untypy/untypy/util/typehints.py +++ /dev/null @@ -1,133 +0,0 @@ -import ast -import inspect -import typing - -from untypy.error import UntypyNameError, UntypyAttributeError -from untypy.interfaces import CreationContext, WrappedFunction -from untypy.util.source_utils import DisplayMatrix - -RULES = [] - - -def _default_resolver(item): - return typing.get_type_hints(item, include_extras=True) - - -def get_type_hints(item, ctx: CreationContext, resolver=_default_resolver): - try: - # SEE: https://www.python.org/dev/peps/pep-0563/#id7 - return resolver(item) - except NameError as ne: - org = WrappedFunction.find_original(item) - if inspect.isclass(org): - raise ctx.wrap(UntypyNameError( - f"{ne}.\nType annotation inside of class '{qualname(org)}' could not be resolved." - )) - else: - raise ctx.wrap(UntypyNameError( - f"{ne}.\nType annotation of function '{qualname(org)}' could not be resolved." - )) - except Exception as e: - # Try to find better cause in analyse - raise analyse(item, ctx, e) - - -def analyse(item, ctx: CreationContext, e) -> UntypyAttributeError: - org = WrappedFunction.find_original(item) - what = 'function' - if inspect.isclass(org): - what = 'class' - try: - source = inspect.getsource(item) - except OSError: - return ctx.wrap( - UntypyAttributeError(f"Type annotation of {what} '{qualname(org)}' could not be resolved.") - ) - fn_ast = ast.parse(source) - - for node in map_str_to_ast(fn_ast.body[0].args, fn_ast.body[0].returns): - for rule in RULES: - rule_result = rule(node) - if rule_result: - # Got a Match - (n, message) = rule_result - display = DisplayMatrix(source) - display.write((n.col_offset - 1, n.lineno), - " " + "^" * (n.end_col_offset - n.col_offset) + " - " + message) - return ctx.wrap( - UntypyAttributeError(f"Type annotation of {what} '{qualname(org)}' could not be resolved:\n" - f"{e}\n" - f"\n{display}") - ) - - return ctx.wrap( - UntypyAttributeError(f"Type annotation of {what} '{qualname(org)}' could not be resolved:\n" - f"{e}\n")) - - -# some type annotations may repr. as strings -def map_str_to_ast(*nodes): - for outer_node in nodes: - for node in ast.walk(outer_node): - yield node - if isinstance(node, ast.Constant) and isinstance(node.value, str): - try: - for inode in ast.walk(ast.parse(node.value, mode='eval').body): - if hasattr(inode, 'lineno'): - inode.lineno += node.lineno - 1 - inode.col_offset += node.col_offset + 1 - inode.end_col_offset += node.col_offset + 1 - yield inode - except SyntaxError: - # may not a forward ref - pass - - -# Rules -# For Ast-Nodes see: https://docs.python.org/3/library/ast.html -def rule_wrong_parentheses(item): - if isinstance(item, ast.Call) and _traverse(item, 'func.id') and _traverse(item, 'args.0'): - inner = ", ".join(map(ast.unparse, _traverse(item, 'args'))) - return (item, f"Did you mean: '{_traverse(item, 'func.id')}[{inner}]'?") - - -RULES.append(rule_wrong_parentheses) - - -# Helpers -def _traverse(item, path): - """ - Traverse item. - Else return None. - Path is a str separated by '.' - """ - - if isinstance(path, str): - return _traverse(item, path.split(".")) - - if len(path) == 0: - return item - - head = path[0] - tail = path[1:] - - if hasattr(item, head): - return _traverse(getattr(item, head), tail) - elif isinstance(item, list): - try: - idx = int(head) - if len(item) > idx >= 0: - return _traverse(item[idx], tail) - except ValueError: - return None - else: - return None - - -def qualname(typ): - if hasattr(typ, '__qualname__'): - return typ.__qualname__ - elif hasattr(typ, '__name__'): - return typ.__name__ - else: - return str(typ) diff --git a/python/deps/untypy/untypy/util/wrapper.py b/python/deps/untypy/untypy/util/wrapper.py deleted file mode 100644 index 1ffe3eae..00000000 --- a/python/deps/untypy/untypy/util/wrapper.py +++ /dev/null @@ -1,275 +0,0 @@ -import typing -import collections -from untypy.util.debug import debug - -def _f(): - yield 0 -generatorType = type(_f()) - -class WrapperBase: - def __eq__(self, other): - if hasattr(other, '__wrapped__'): - return self.__wrapped__ == other.__wrapped__ - else: - return self.__wrapped__ == other - def __ne__(self, other): - return not self.__eq__(other) - def __hash__(self): - return hash(self.__wrapped__) - def __repr__(self): - return repr(self.__wrapped__) - def __str__(self): - return str(self.__wrapped__) - def __cast__(self, other): - if type(other) == type(self) and hasattr(other, '__wrapped__'): - return other.__wrapped__ - else: - return other - def __reduce__(self): return self.__wrapped__.__reduce__() - def __reduce_ex__(self): return self.__wrapped__.__reduce_ex__() - def __sizeof__(self): return self.__wrapped__.__sizeof__() - -# A wrapper for list such that the class is a subclass of the builtin list class. -class ListWrapper(WrapperBase, list): # important: inherit from WrapperBase first - def __new__(cls, content): - # the constructor of list copies the list passed to it. Thus, we use an empty list. - # IMPORTANT: we need to override *all* methods provided by list so that - # all methods operate on the list being wrapped. - self = super().__new__(cls, []) - self.__wrapped__ = content - return self - # defined in WrapperBase: __repr__, __str__, __eq__, __hash__ - def __lt__(self, other): return self.__wrapped__.__lt__(self.__cast__(other)) - def __le__(self, other): return self.__wrapped__.__le__(self.__cast__(other)) - def __gt__(self, other): return self.__wrapped__.__gt__(self.__cast__(other)) - def __ge__(self, other): return self.__wrapped__.__ge__(self.__cast__(other)) - def __contains__(self, item): return self.__wrapped__.contains(item) - def __len__(self): return self.__wrapped__.__len__() - def __getitem__(self, i): return self.__wrapped__.__getitem__(i) - def __setitem__(self, i, x): return self.__wrapped__.__setitem__(i, x) - def __delitem__(self, i): return self.__wrapped__.__delitem__(i) - def __add__(self, other): return self.__wrapped__.__add__(self.__cast__(other)) - def __radd__(self, other): return self.__cast__(other) + self.__wrapped__ - def __iadd__(self, other): return self.__wrapped__.__iadd__(self.__cast__(other)) - def __mul__(self, n): return self.__wrapped__.__mul__(n) - __rmul__ = __mul__ - def __imul__(self, n): return self.__wrapped__.__imul__(n) - def __iter__(self): return self.__wrapped__.__iter__() - def __copy__(self): return self.__wrapped__.copy() - def __reversed__(self): return self.__wrapped__.__reversed__() - def append(self, item): return self.__wrapped__.append(item) - def extend(self, iter): return self.__wrapped__.extend(iter) - def insert(self, i, item): return self.__wrapped__.insert(i, item) - def pop(self, i=-1): return self.__wrapped__.pop(i) - def remove(self, item): return self.__wrapped__.remove(item) - def clear(self): return self.__wrapped__.clear() - def copy(self): return self.__wrapped__.copy() - def count(self, item): return self.__wrapped__.count(item) - def index(self, item, *args): return self.__wrapped__.index(item, *args) - def reverse(self): return self.__wrapped__.reverse() - def sort(self, /, *args, **kwds): return self.__wrapped__.sort(*args, **kwds) - -class SetWrapper(WrapperBase, set): - def __new__(cls, content): - self = super().__new__(cls, set()) - self.__wrapped__ = content - return self - def add(self, x): return self.__wrapped__.add(x) - def clear(self): return self.__wrapped__.clear() - def copy(self): return self.__wrapped__.copy() - def difference(self, *others): return self.__wrapped__.difference(*others) - def difference_update(self, *others): return self.__wrapped__.difference_update(*others) - def discard(self, elem): return self.__wrapped__.discard(elem) - def intersection(self, *others): return self.__wrapped__.intersection(*others) - def intersection_update(self, *others): return self.__wrapped__.intersection_update(*others) - def isdisjoint(self, other): return self.__wrapped__.isdisjoint(other) - def issubset(self, other): return self.__wrapped__.issubset(other) - def issuperset(self, other): return self.__wrapped__.issuperset(other) - def pop(self): return self.__wrapped__.pop() - def remove(self, elem): return self.__wrapped__.remove(elem) - def symmetric_difference(self, *others): return self.__wrapped__.symmetric_difference(*others) - def symmetric_difference_update(self, *others): return self.__wrapped__.symmetric_difference_update(*others) - def union(self, *others): return self.__wrapped__.union(*others) - def update(self, *others): return self.__wrapped__.update(*others) - def __contains__(self, x): return self.__wrapped__.__contains__(x) - def __le__(self, other): return self.__wrapped__.__le__(self.__cast__(other)) - def __lt__(self, other): return self.__wrapped__.__lt__(self.__cast__(other)) - def __ge__(self, other): return self.__wrapped__.__ge__(self.__cast__(other)) - def __gt__(self, other): return self.__wrapped__.__gt__(self.__cast__(other)) - def __and__(self, other): return self.__wrapped__.__and__(self.__cast__(other)) - def __rand__(self, other): return self.__wrapped__.__rand__(self.__cast__(other)) - def __iand__(self, other): return self.__wrapped__.__iand__(self.__cast__(other)) - def __ior__(self, other): return self.__wrapped__.__ior__(self.__cast__(other)) - def __ixor__(self, other): return self.__wrapped__.__ixor__(self.__cast__(other)) - def __or__(self, other): return self.__wrapped__.__or__(self.__cast__(other)) - def __ror__(self, other): return self.__wrapped__.__ror__(self.__cast__(other)) - def __rxor__(self, other): return self.__wrapped__.__rxor__(self.__cast__(other)) - def __xor__(self, other): return self.__wrapped__.__xor__(self.__cast__(other)) - def __rsub__(self, other): return self.__wrapped__.__rsub__(self.__cast__(other)) - def __sub__(self, other): return self.__wrapped__.__sub__(self.__cast__(other)) - def __isub__(self, other): return self.__wrapped__.__isub__(self.__cast__(other)) - def __iter__(self): return self.__wrapped__.__iter__() - def __len__(self): return self.__wrapped__.__len__() - -class DictWrapper(WrapperBase, dict): - def __new__(cls, content): - self = super().__new__(cls, {}) - self.__wrapped__ = content - return self - def __len__(self): return self.__wrapped__.__len__() - def __getitem__(self, key): return self.__wrapped__.__getitem__(key) - def __setitem__(self, key, item): return self.__wrapped__.__setitem__(key, item) - def __delitem__(self, key): return self.__wrapped__.__delitem__(key) - def __iter__(self): return self.__wrapped__.__iter__() - def __contains__(self, key): return self.__wrapped__.__contains__(key) - def __or__(self, other): return self.__wrapped__.__or__(self.__cast__(other)) - def __ror__(self, other): return self.__wrapped__.__ror__(self.__cast__(other)) - def __ior__(self, other): return self.__wrapped__.__ior__(self.__cast__(other)) - def __copy__(self): return self.__wrapped__.__copy__() - def __lt__(self, other): return self.__wrapped__.__lt__(self.__cast__(other)) - def __le__(self, other): return self.__wrapped__.__le__(self.__cast__(other)) - def __gt__(self, other): return self.__wrapped__.__gt__(self.__cast__(other)) - def __ge__(self, other): return self.__wrapped__.__ge__(self.__cast__(other)) - def copy(self): return self.__wrapped__.copy() - def __reversed__(self): return self.__wrapped__.__reversed__() - __marker = object() - def pop(self, key, default=__marker): - if default == self.__marker: - return self.__wrapped__.pop(key) - else: - return self.__wrapped__.pop(key, default) - def popitem(self): return self.__wrapped__.popitem() - def clear(self): return self.__wrapped__.clear() - def update(self, other=(), /, **kwds): return self.__wrapped__.update(self.__cast__(other), **kwds) - def setdefault(self, key, default=None): return self.__wrapped__.setdefault(key, default=default) - def get(self, key, default=None): return self.__wrapped__.get(key, default) - def keys(self): return self.__wrapped__.keys() - def items(self): return self.__wrapped__.items() - def values(self): return self.__wrapped__.values() - -# Tuple and string wrapper are simpler because these types are immutable -class StringWrapper(str, WrapperBase): - def __new__(cls, content): - self = super().__new__(cls, content) - self.__wrapped__ = content - return self - -class TupleWrapper(tuple, WrapperBase): - def __new__(cls, content): - self = super().__new__(cls, content) - self.__wrapped__ = content - return self - -# SimpleWrapper is a fallback for types that cannot be used as base types -class SimpleWrapper(WrapperBase): - def __init__(self, wrapped): - self.__wrapped__ = wrapped - -class ValuesViewWrapper(SimpleWrapper): - pass -collections.abc.ValuesView.register(ValuesViewWrapper) - -class ItemsViewWrapper(SimpleWrapper): - pass -collections.abc.ItemsView.register(ItemsViewWrapper) - -class KeysViewWrapper(SimpleWrapper): - pass -collections.abc.KeysView.register(KeysViewWrapper) - -def _wrap(wrapped, methods, mod, name, extra, cls): - if extra is None: - extra = {} - # Dynamically create a new class: - # type(class_name, base_classes, class_dict) - WrapperClass = type( - name, - (cls,), - methods - ) - WrapperClass.__module__ = mod - w = WrapperClass(wrapped) - w.__extra__ = extra - return w - -def wrapSimple(wrapped, methods, name, extra, cls=SimpleWrapper): - if name is None: - name = cls.__name__ - mod = None - else: - if hasattr(wrapped, '__module__'): - mod = wrapped.__module__ - else: - mod = None - for x in ['__next__', '__iter__']: - if x not in methods and hasattr(wrapped, x): - attr = getattr(wrapped, x) - methods[x] = attr - return _wrap(wrapped, methods, mod, name, extra, cls) - -def wrapObj(wrapped, methods, name, extra): - class BaseWrapper(WrapperBase, wrapped.__class__): - def __init__(self, wrapped): - self.__dict__ = wrapped.__dict__ - self.__wrapped__ = wrapped - if name is None: - name = 'ObjectWrapper' - if hasattr(wrapped, '__module__'): - mod = getattr(wrapped, '__module__') - else: - mod = None - return _wrap(wrapped, methods, mod, name, extra, BaseWrapper) - -def wrapBuiltin(wrapped, methods, name, extra, cls): - if name is None: - name = cls.__name__ - return _wrap(wrapped, methods, None, name, extra, cls) - -def wrap(obj, methods, name=None, extra=None, simple=False): - if extra is None: - extra = {} - wrapper = None - if simple: - w = wrapSimple(obj, methods, name, extra) - wrapper = 'SimpleWrapper' - elif isinstance(obj, list): - w = wrapBuiltin(obj, methods, name, extra, ListWrapper) - wrapper = 'ListWrapper' - elif isinstance(obj, tuple): - w = wrapBuiltin(obj, methods, name, extra, TupleWrapper) - wrapper = 'TupleWrapper' - elif isinstance(obj, dict): - w = wrapBuiltin(obj, methods, name, extra, DictWrapper) - wrapper = 'DictWrapper' - elif isinstance(obj, str): - w = wrapBuiltin(obj, methods, name, extra, StringWrapper) - wrapper = 'StringWrapper' - elif isinstance(obj, set): - w = wrapBuiltin(obj, methods, name, extra, SetWrapper) - wrapper = 'SetWrapper' - elif isinstance(obj, collections.abc.ValuesView): - w = wrapSimple(obj, methods, name, extra, ValuesViewWrapper) - wrapper = 'ValuesViewWrapper' - elif isinstance(obj, collections.abc.KeysView): - w = wrapSimple(obj, methods, name, extra, KeysViewWrapper) - wrapper = 'KeysViewWrapper' - elif isinstance(obj, collections.abc.ItemsView): - w = wrapSimple(obj, methods, name, extra, ItemsViewWrapper) - wrapper = 'ItemsViewWrapper' - elif isinstance(obj, typing.Generic): - w = wrapSimple(obj, methods, name, extra) - wrapper = 'SimpleWrapper' - elif isinstance(obj, generatorType): - w = wrapSimple(obj, methods, name, extra) - wrapper = 'SimpleWrapper' - elif hasattr(obj, '__dict__'): - w = wrapObj(obj, methods, name, extra) - wrapper = 'ObjectWrapper' - else: - w = wrapSimple(obj, methods, name, extra) - wrapper = 'SimpleWrapper' - wname = name - if wname is None: - wname = str(type(w)) - debug(f"Wrapping {obj} at 0x{id(obj):09x} as {wname}, simple={simple}, wrapper=0x{id(w):09x} ({wrapper})") - return w diff --git a/python/file-test-data/basics/async.err b/python/file-test-data/basics/async.err new file mode 100644 index 00000000..a17f2494 --- /dev/null +++ b/python/file-test-data/basics/async.err @@ -0,0 +1,25 @@ +Traceback (most recent call last): + File "file-test-data/basics/async.py", line 10, in + asyncio.run(main()) + File "runners.py", line ?, in run + return runner.run(main) + File "runners.py", line ?, in run + signal.signal(signal.SIGINT, signal.default_int_handler) + File "base_events.py", line ?, in run_until_complete + return future.result() + File "file-test-data/basics/async.py", line 7, in main + x = await foo("blub") + +WyppTypeError: "blub" + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/async.py +## Fehlerhafter Aufruf in Zeile 7: + + x = await foo("blub") + +## Typ deklariert in Zeile 3: + +async def foo(i: int): diff --git a/python/file-test-data/basics/async.err_en b/python/file-test-data/basics/async.err_en new file mode 100644 index 00000000..e4a7d64e --- /dev/null +++ b/python/file-test-data/basics/async.err_en @@ -0,0 +1,25 @@ +Traceback (most recent call last): + File "file-test-data/basics/async.py", line 10, in + asyncio.run(main()) + File "runners.py", line ?, in run + return runner.run(main) + File "runners.py", line ?, in run + signal.signal(signal.SIGINT, signal.default_int_handler) + File "base_events.py", line ?, in run_until_complete + return future.result() + File "file-test-data/basics/async.py", line 7, in main + x = await foo("blub") + +WyppTypeError: "blub" + +The call of function `foo` expects value of type `int` as 1st argument. +But the value given has type `str`. + +## File file-test-data/basics/async.py +## Problematic call in line 7: + + x = await foo("blub") + +## Type declared in line 3: + +async def foo(i: int): diff --git a/python/deps/untypy/requirements.txt b/python/file-test-data/basics/async.out similarity index 100% rename from python/deps/untypy/requirements.txt rename to python/file-test-data/basics/async.out diff --git a/python/deps/untypy/test-requirements.txt b/python/file-test-data/basics/async.out_en similarity index 100% rename from python/deps/untypy/test-requirements.txt rename to python/file-test-data/basics/async.out_en diff --git a/python/file-test-data/basics/async.py b/python/file-test-data/basics/async.py new file mode 100644 index 00000000..bcc83ba0 --- /dev/null +++ b/python/file-test-data/basics/async.py @@ -0,0 +1,10 @@ +import asyncio + +async def foo(i: int): + return i + 1 + +async def main(): + x = await foo("blub") + print(x) + +asyncio.run(main()) diff --git a/python/file-test-data/basics/constructor.err b/python/file-test-data/basics/constructor.err new file mode 100644 index 00000000..9d8fb5ad --- /dev/null +++ b/python/file-test-data/basics/constructor.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/constructor.py", line 5, in + c = C("1") + +WyppTypeError: "1" + +Der Aufruf der Methode `__init__` aus Klasse `C` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/constructor.py +## Fehlerhafter Aufruf in Zeile 5: + +c = C("1") + +## Typ deklariert in Zeile 2: + + def __init__(self, x: int): \ No newline at end of file diff --git a/python/file-test-data/basics/constructor.err_en b/python/file-test-data/basics/constructor.err_en new file mode 100644 index 00000000..44f7d2ce --- /dev/null +++ b/python/file-test-data/basics/constructor.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/constructor.py", line 5, in + c = C("1") + +WyppTypeError: "1" + +The call of method `__init__` of class `C` expects value of type `int` as 1st argument. +But the value given has type `str`. + +## File file-test-data/basics/constructor.py +## Problematic call in line 5: + +c = C("1") + +## Type declared in line 2: + + def __init__(self, x: int): \ No newline at end of file diff --git a/python/deps/untypy/test/impl/__init__.py b/python/file-test-data/basics/constructor.out similarity index 100% rename from python/deps/untypy/test/impl/__init__.py rename to python/file-test-data/basics/constructor.out diff --git a/python/deps/untypy/test/patching_dummy/__init__.py b/python/file-test-data/basics/constructor.out_en similarity index 100% rename from python/deps/untypy/test/patching_dummy/__init__.py rename to python/file-test-data/basics/constructor.out_en diff --git a/python/file-test-data/basics/constructor.py b/python/file-test-data/basics/constructor.py new file mode 100644 index 00000000..bd9ca484 --- /dev/null +++ b/python/file-test-data/basics/constructor.py @@ -0,0 +1,5 @@ +class C: + def __init__(self, x: int): + self.x = x + +c = C("1") diff --git a/python/file-test-data/basics/constructorDataclass_ok.py b/python/file-test-data/basics/constructorDataclass_ok.py new file mode 100644 index 00000000..5f2c3bec --- /dev/null +++ b/python/file-test-data/basics/constructorDataclass_ok.py @@ -0,0 +1,8 @@ +#from dataclasses import dataclass +from wypp import * + +@dataclass +class C: + x: int + +c = C("1") diff --git a/python/deps/untypy/test/util_test/__init__.py b/python/file-test-data/basics/constructor_ok.err similarity index 100% rename from python/deps/untypy/test/util_test/__init__.py rename to python/file-test-data/basics/constructor_ok.err diff --git a/python/file-test-data/basics/constructor_ok.out b/python/file-test-data/basics/constructor_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/constructor_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/constructor_ok.py b/python/file-test-data/basics/constructor_ok.py new file mode 100644 index 00000000..57132fce --- /dev/null +++ b/python/file-test-data/basics/constructor_ok.py @@ -0,0 +1,6 @@ +class C: + def __init__(self, x: int): + self.x = x + +c = C(1) +print('ok') diff --git a/python/file-test-data/basics/forwardRefs.err b/python/file-test-data/basics/forwardRefs.err new file mode 100644 index 00000000..e98cc51d --- /dev/null +++ b/python/file-test-data/basics/forwardRefs.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/forwardRefs.py", line 12, in + a.foo(a) + +WyppTypeError: <__wypp__.A object at 0x00> + +Der Aufruf der Methode `foo` aus Klasse `A` erwartet Wert vom Typ `B` als erstes Argument. +Aber der übergebene Wert hat Typ `A`. + +## Datei file-test-data/basics/forwardRefs.py +## Fehlerhafter Aufruf in Zeile 12: + +a.foo(a) + +## Typ deklariert in Zeile 4: + + def foo(self, b: B): \ No newline at end of file diff --git a/python/file-test-data/basics/forwardRefs.err_en b/python/file-test-data/basics/forwardRefs.err_en new file mode 100644 index 00000000..20d22f20 --- /dev/null +++ b/python/file-test-data/basics/forwardRefs.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/forwardRefs.py", line 12, in + a.foo(a) + +WyppTypeError: <__wypp__.A object at 0x00> + +The call of method `foo` of class `A` expects value of type `B` as 1st argument. +But the value given has type `A`. + +## File file-test-data/basics/forwardRefs.py +## Problematic call in line 12: + +a.foo(a) + +## Type declared in line 4: + + def foo(self, b: B): \ No newline at end of file diff --git a/python/deps/untypy/untypy/impl/interfaces/__init__.py b/python/file-test-data/basics/forwardRefs.out similarity index 100% rename from python/deps/untypy/untypy/impl/interfaces/__init__.py rename to python/file-test-data/basics/forwardRefs.out diff --git a/python/test-data/admin.err b/python/file-test-data/basics/forwardRefs.out_en similarity index 100% rename from python/test-data/admin.err rename to python/file-test-data/basics/forwardRefs.out_en diff --git a/python/file-test-data/basics/forwardRefs.py b/python/file-test-data/basics/forwardRefs.py new file mode 100644 index 00000000..cd11bd03 --- /dev/null +++ b/python/file-test-data/basics/forwardRefs.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +class A: + def foo(self, b: B): + pass + +class B: + def bar(self, a: A): + pass + +a = A() +a.foo(a) diff --git a/python/test-data/declared-at-missing.out b/python/file-test-data/basics/forwardRefs_ok.err similarity index 100% rename from python/test-data/declared-at-missing.out rename to python/file-test-data/basics/forwardRefs_ok.err diff --git a/python/file-test-data/basics/forwardRefs_ok.out b/python/file-test-data/basics/forwardRefs_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/forwardRefs_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/forwardRefs_ok.py b/python/file-test-data/basics/forwardRefs_ok.py new file mode 100644 index 00000000..c3f7fc81 --- /dev/null +++ b/python/file-test-data/basics/forwardRefs_ok.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +class A: + def foo(self, b: B): + pass + +class B: + def bar(self, a: A): + pass + +a = A() +b = B() +a.foo(b) +b.bar(a) +print('ok') diff --git a/python/file-test-data/basics/functionArg.err b/python/file-test-data/basics/functionArg.err new file mode 100644 index 00000000..003a56f3 --- /dev/null +++ b/python/file-test-data/basics/functionArg.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionArg.py", line 8, in + print(foo(foo(1, "1"), 42)) + +WyppTypeError: "1" + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `int` als zweites Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/functionArg.py +## Fehlerhafter Aufruf in Zeile 8: + + print(foo(foo(1, "1"), 42)) + +## Typ deklariert in Zeile 4: + +def foo(i: int, j: int) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/functionArg.err_en b/python/file-test-data/basics/functionArg.err_en new file mode 100644 index 00000000..8f4994f8 --- /dev/null +++ b/python/file-test-data/basics/functionArg.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionArg.py", line 8, in + print(foo(foo(1, "1"), 42)) + +WyppTypeError: "1" + +The call of function `foo` expects value of type `int` as 2nd argument. +But the value given has type `str`. + +## File file-test-data/basics/functionArg.py +## Problematic call in line 8: + + print(foo(foo(1, "1"), 42)) + +## Type declared in line 4: + +def foo(i: int, j: int) -> int: \ No newline at end of file diff --git a/python/test-data/modules/A/main.out b/python/file-test-data/basics/functionArg.out similarity index 100% rename from python/test-data/modules/A/main.out rename to python/file-test-data/basics/functionArg.out diff --git a/python/test-data/printModuleName.err b/python/file-test-data/basics/functionArg.out_en similarity index 100% rename from python/test-data/printModuleName.err rename to python/file-test-data/basics/functionArg.out_en diff --git a/python/file-test-data/basics/functionArg.py b/python/file-test-data/basics/functionArg.py new file mode 100644 index 00000000..fa3619c6 --- /dev/null +++ b/python/file-test-data/basics/functionArg.py @@ -0,0 +1,8 @@ +def bar(s: str) -> str: + return s + +def foo(i: int, j: int) -> int: + return i + 1 + +if True: + print(foo(foo(1, "1"), 42)) diff --git a/python/test-data/printModuleNameImport.err b/python/file-test-data/basics/functionArg_ok.err similarity index 100% rename from python/test-data/printModuleNameImport.err rename to python/file-test-data/basics/functionArg_ok.err diff --git a/python/test-data/testABC.err b/python/file-test-data/basics/functionArg_ok.err_None similarity index 100% rename from python/test-data/testABC.err rename to python/file-test-data/basics/functionArg_ok.err_None diff --git a/python/file-test-data/basics/functionArg_ok.out b/python/file-test-data/basics/functionArg_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/functionArg_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/functionArg_ok.out_None b/python/file-test-data/basics/functionArg_ok.out_None new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/functionArg_ok.out_None @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/functionArg_ok.py b/python/file-test-data/basics/functionArg_ok.py new file mode 100644 index 00000000..9f86377b --- /dev/null +++ b/python/file-test-data/basics/functionArg_ok.py @@ -0,0 +1,5 @@ +def foo(i: int) -> int: + return i + 1 + +foo(1) +print('ok') diff --git a/python/file-test-data/basics/functionNoResult.err b/python/file-test-data/basics/functionNoResult.err new file mode 100644 index 00000000..11e69b2c --- /dev/null +++ b/python/file-test-data/basics/functionNoResult.err @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionNoResult.py", line 4, in + foo(1) + File "file-test-data/basics/functionNoResult.py", line 2, in foo + return "x" + +WyppTypeError: "x" + +Kein Rückgabewert erwartet bei Aufruf der Funktion `foo`. +Aber der Aufruf gibt einen Wert vom Typ `str` zurück. + +## Datei file-test-data/basics/functionNoResult.py +## Rückgabetyp deklariert in Zeile 1: + +def foo(i: int) -> None: + +## Fehlerhaftes return in Zeile 2: + + return "x" + +## Aufruf in Zeile 4 verursacht das fehlerhafte return: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/basics/functionNoResult.err_en b/python/file-test-data/basics/functionNoResult.err_en new file mode 100644 index 00000000..304cff85 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult.err_en @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionNoResult.py", line 4, in + foo(1) + File "file-test-data/basics/functionNoResult.py", line 2, in foo + return "x" + +WyppTypeError: "x" + +Expecting no return value when calling function `foo`. +But the call returns a value of type `str`. + +## File file-test-data/basics/functionNoResult.py +## Result type declared in line 1: + +def foo(i: int) -> None: + +## Problematic return in line 2: + + return "x" + +## Call in line 4 causes the problematic return: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testABCMeta.out b/python/file-test-data/basics/functionNoResult.out similarity index 100% rename from python/test-data/testABCMeta.out rename to python/file-test-data/basics/functionNoResult.out diff --git a/python/test-data/testArgs.err b/python/file-test-data/basics/functionNoResult.out_en similarity index 100% rename from python/test-data/testArgs.err rename to python/file-test-data/basics/functionNoResult.out_en diff --git a/python/file-test-data/basics/functionNoResult.py b/python/file-test-data/basics/functionNoResult.py new file mode 100644 index 00000000..75085123 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult.py @@ -0,0 +1,4 @@ +def foo(i: int) -> None: + return "x" + +foo(1) diff --git a/python/file-test-data/basics/functionNoResult2.err b/python/file-test-data/basics/functionNoResult2.err new file mode 100644 index 00000000..c890beaa --- /dev/null +++ b/python/file-test-data/basics/functionNoResult2.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionNoResult2.py", line 4, in + foo(1) + File "file-test-data/basics/functionNoResult2.py", line 2, in foo + pass + +WyppTypeError: kein Rückgabewert vorhanden + +Rückgabewert vom Typ `int` erwartet bei Aufruf der Funktion `foo`. +Aber kein Rückgabewert vorhanden. + +## Datei file-test-data/basics/functionNoResult2.py +## Rückgabetyp deklariert in Zeile 1: + +def foo(i: int) -> int: + +## Aufruf in Zeile 4 führt dazu, dass die Funktion keinen Wert zurückgibt: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/basics/functionNoResult2.err_en b/python/file-test-data/basics/functionNoResult2.err_en new file mode 100644 index 00000000..627f5d4c --- /dev/null +++ b/python/file-test-data/basics/functionNoResult2.err_en @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionNoResult2.py", line 4, in + foo(1) + File "file-test-data/basics/functionNoResult2.py", line 2, in foo + pass + +WyppTypeError: no result returned + +Expecting return value of type `int` when calling function `foo`. +But no value returned. + +## File file-test-data/basics/functionNoResult2.py +## Result type declared in line 1: + +def foo(i: int) -> int: + +## Call in line 4 causes the function to return no value: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testBugSliceIndices.err b/python/file-test-data/basics/functionNoResult2.out similarity index 100% rename from python/test-data/testBugSliceIndices.err rename to python/file-test-data/basics/functionNoResult2.out diff --git a/python/test-data/testCheck.err b/python/file-test-data/basics/functionNoResult2.out_en similarity index 100% rename from python/test-data/testCheck.err rename to python/file-test-data/basics/functionNoResult2.out_en diff --git a/python/file-test-data/basics/functionNoResult2.py b/python/file-test-data/basics/functionNoResult2.py new file mode 100644 index 00000000..167c5b3a --- /dev/null +++ b/python/file-test-data/basics/functionNoResult2.py @@ -0,0 +1,4 @@ +def foo(i: int) -> int: + pass + +foo(1) diff --git a/python/test-data/testCheck2.out b/python/file-test-data/basics/functionNoResult2_ok.err similarity index 100% rename from python/test-data/testCheck2.out rename to python/file-test-data/basics/functionNoResult2_ok.err diff --git a/python/file-test-data/basics/functionNoResult2_ok.out b/python/file-test-data/basics/functionNoResult2_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/functionNoResult2_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/functionNoResult2_ok.py b/python/file-test-data/basics/functionNoResult2_ok.py new file mode 100644 index 00000000..5b658665 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult2_ok.py @@ -0,0 +1,5 @@ +def foo(i: int) -> int: + return 1 + +foo(1) +print('ok') diff --git a/python/file-test-data/basics/functionNoResult3.err b/python/file-test-data/basics/functionNoResult3.err new file mode 100644 index 00000000..887347a5 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult3.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionNoResult3.py", line 4, in + foo(1) + File "file-test-data/basics/functionNoResult3.py", line 2, in foo + return "x" + +WyppTypeError: "x" + +Kein Rückgabewert erwartet bei Aufruf der Funktion `foo`. +Aber der Aufruf gibt einen Wert vom Typ `str` zurück. + +## Datei file-test-data/basics/functionNoResult3.py +## Fehlerhaftes return in Zeile 2: + + return "x" + +## Aufruf in Zeile 4 verursacht das fehlerhafte return: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/basics/functionNoResult3.err_en b/python/file-test-data/basics/functionNoResult3.err_en new file mode 100644 index 00000000..d4f2bad8 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult3.err_en @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionNoResult3.py", line 4, in + foo(1) + File "file-test-data/basics/functionNoResult3.py", line 2, in foo + return "x" + +WyppTypeError: "x" + +Expecting no return value when calling function `foo`. +But the call returns a value of type `str`. + +## File file-test-data/basics/functionNoResult3.py +## Problematic return in line 2: + + return "x" + +## Call in line 4 causes the problematic return: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testCheckFail.err b/python/file-test-data/basics/functionNoResult3.out similarity index 100% rename from python/test-data/testCheckFail.err rename to python/file-test-data/basics/functionNoResult3.out diff --git a/python/test-data/testClassHierarchy.err b/python/file-test-data/basics/functionNoResult3.out_en similarity index 100% rename from python/test-data/testClassHierarchy.err rename to python/file-test-data/basics/functionNoResult3.out_en diff --git a/python/test-data/testInferReturnType.py b/python/file-test-data/basics/functionNoResult3.py similarity index 60% rename from python/test-data/testInferReturnType.py rename to python/file-test-data/basics/functionNoResult3.py index f3358c0d..bb14b13b 100644 --- a/python/test-data/testInferReturnType.py +++ b/python/file-test-data/basics/functionNoResult3.py @@ -1,4 +1,4 @@ def foo(i: int): - return None + return "x" foo(1) diff --git a/python/test-data/testClassRecursion.err b/python/file-test-data/basics/functionNoResult3_ok.err similarity index 100% rename from python/test-data/testClassRecursion.err rename to python/file-test-data/basics/functionNoResult3_ok.err diff --git a/python/file-test-data/basics/functionNoResult3_ok.out b/python/file-test-data/basics/functionNoResult3_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/functionNoResult3_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/functionNoResult3_ok.py b/python/file-test-data/basics/functionNoResult3_ok.py new file mode 100644 index 00000000..c67b93a6 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult3_ok.py @@ -0,0 +1,5 @@ +def foo(i: int): + pass + +foo(1) +print('ok') diff --git a/python/test-data/testComplex.err b/python/file-test-data/basics/functionNoResult_ok.err similarity index 100% rename from python/test-data/testComplex.err rename to python/file-test-data/basics/functionNoResult_ok.err diff --git a/python/file-test-data/basics/functionNoResult_ok.out b/python/file-test-data/basics/functionNoResult_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/functionNoResult_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/functionNoResult_ok.py b/python/file-test-data/basics/functionNoResult_ok.py new file mode 100644 index 00000000..c67b93a6 --- /dev/null +++ b/python/file-test-data/basics/functionNoResult_ok.py @@ -0,0 +1,5 @@ +def foo(i: int): + pass + +foo(1) +print('ok') diff --git a/python/file-test-data/basics/functionResult.err b/python/file-test-data/basics/functionResult.err new file mode 100644 index 00000000..fccc0a10 --- /dev/null +++ b/python/file-test-data/basics/functionResult.err @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionResult.py", line 7, in + foo(1) + File "file-test-data/basics/functionResult.py", line 5, in foo + return "foo_" + str(i) + +WyppTypeError: "foo_1" + +Rückgabewert vom Typ `int` erwartet bei Aufruf der Funktion `foo`. +Aber der Aufruf gibt einen Wert vom Typ `str` zurück. + +## Datei file-test-data/basics/functionResult.py +## Rückgabetyp deklariert in Zeile 4: + +def foo(i: int) -> int: + +## Fehlerhaftes return in Zeile 5: + + return "foo_" + str(i) + +## Aufruf in Zeile 7 verursacht das fehlerhafte return: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/basics/functionResult.err_en b/python/file-test-data/basics/functionResult.err_en new file mode 100644 index 00000000..56189aa0 --- /dev/null +++ b/python/file-test-data/basics/functionResult.err_en @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionResult.py", line 7, in + foo(1) + File "file-test-data/basics/functionResult.py", line 5, in foo + return "foo_" + str(i) + +WyppTypeError: "foo_1" + +Expecting return value of type `int` when calling function `foo`. +But the call returns a value of type `str`. + +## File file-test-data/basics/functionResult.py +## Result type declared in line 4: + +def foo(i: int) -> int: + +## Problematic return in line 5: + + return "foo_" + str(i) + +## Call in line 7 causes the problematic return: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testComplex.out b/python/file-test-data/basics/functionResult.out similarity index 100% rename from python/test-data/testComplex.out rename to python/file-test-data/basics/functionResult.out diff --git a/python/test-data/testConcat.err b/python/file-test-data/basics/functionResult.out_en similarity index 100% rename from python/test-data/testConcat.err rename to python/file-test-data/basics/functionResult.out_en diff --git a/python/file-test-data/basics/functionResult.py b/python/file-test-data/basics/functionResult.py new file mode 100644 index 00000000..52439efb --- /dev/null +++ b/python/file-test-data/basics/functionResult.py @@ -0,0 +1,7 @@ +def bar(s: str) -> int: + return len(s) + +def foo(i: int) -> int: + return "foo_" + str(i) + +foo(1) diff --git a/python/file-test-data/basics/functionResult2.err b/python/file-test-data/basics/functionResult2.err new file mode 100644 index 00000000..ab040019 --- /dev/null +++ b/python/file-test-data/basics/functionResult2.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionResult2.py", line 7, in + foo(1) + File "file-test-data/basics/functionResult2.py", line 5, in foo + return "foo_" + str(i) + +WyppTypeError: "foo_1" + +Kein Rückgabewert erwartet bei Aufruf der Funktion `foo`. +Aber der Aufruf gibt einen Wert vom Typ `str` zurück. + +## Datei file-test-data/basics/functionResult2.py +## Fehlerhaftes return in Zeile 5: + + return "foo_" + str(i) + +## Aufruf in Zeile 7 verursacht das fehlerhafte return: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/basics/functionResult2.err_en b/python/file-test-data/basics/functionResult2.err_en new file mode 100644 index 00000000..1d4c4f31 --- /dev/null +++ b/python/file-test-data/basics/functionResult2.err_en @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/functionResult2.py", line 7, in + foo(1) + File "file-test-data/basics/functionResult2.py", line 5, in foo + return "foo_" + str(i) + +WyppTypeError: "foo_1" + +Expecting no return value when calling function `foo`. +But the call returns a value of type `str`. + +## File file-test-data/basics/functionResult2.py +## Problematic return in line 5: + + return "foo_" + str(i) + +## Call in line 7 causes the problematic return: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testCopy.err b/python/file-test-data/basics/functionResult2.out similarity index 100% rename from python/test-data/testCopy.err rename to python/file-test-data/basics/functionResult2.out diff --git a/python/test-data/testDeepEqBug.err b/python/file-test-data/basics/functionResult2.out_en similarity index 100% rename from python/test-data/testDeepEqBug.err rename to python/file-test-data/basics/functionResult2.out_en diff --git a/python/file-test-data/basics/functionResult2.py b/python/file-test-data/basics/functionResult2.py new file mode 100644 index 00000000..fd81e2bf --- /dev/null +++ b/python/file-test-data/basics/functionResult2.py @@ -0,0 +1,7 @@ +def bar(s: str) -> int: + return len(s) + +def foo(i: int): + return "foo_" + str(i) + +foo(1) diff --git a/python/test-data/testDict.err b/python/file-test-data/basics/functionResult_ok.err similarity index 100% rename from python/test-data/testDict.err rename to python/file-test-data/basics/functionResult_ok.err diff --git a/python/file-test-data/basics/functionResult_ok.out b/python/file-test-data/basics/functionResult_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/functionResult_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/functionResult_ok.py b/python/file-test-data/basics/functionResult_ok.py new file mode 100644 index 00000000..9f86377b --- /dev/null +++ b/python/file-test-data/basics/functionResult_ok.py @@ -0,0 +1,5 @@ +def foo(i: int) -> int: + return i + 1 + +foo(1) +print('ok') diff --git a/python/file-test-data/basics/iterator.err b/python/file-test-data/basics/iterator.err new file mode 100644 index 00000000..461a166c --- /dev/null +++ b/python/file-test-data/basics/iterator.err @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/basics/iterator.py", line 6, in + g = my_generator() + File "file-test-data/basics/iterator.py", line 4, in my_generator + return 1 + +WyppTypeError: 1 + +Rückgabewert vom Typ `Iterator[int]` erwartet bei Aufruf der Funktion `my_generator`. +Aber der Aufruf gibt einen Wert vom Typ `int` zurück. + +## Datei file-test-data/basics/iterator.py +## Rückgabetyp deklariert in Zeile 3: + +def my_generator() -> Iterator[int]: + +## Fehlerhaftes return in Zeile 4: + + return 1 + +## Aufruf in Zeile 6 verursacht das fehlerhafte return: + +g = my_generator() \ No newline at end of file diff --git a/python/file-test-data/basics/iterator.err_en b/python/file-test-data/basics/iterator.err_en new file mode 100644 index 00000000..ce4a4c16 --- /dev/null +++ b/python/file-test-data/basics/iterator.err_en @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/basics/iterator.py", line 6, in + g = my_generator() + File "file-test-data/basics/iterator.py", line 4, in my_generator + return 1 + +WyppTypeError: 1 + +Expecting return value of type `Iterator[int]` when calling function `my_generator`. +But the call returns a value of type `int`. + +## File file-test-data/basics/iterator.py +## Result type declared in line 3: + +def my_generator() -> Iterator[int]: + +## Problematic return in line 4: + + return 1 + +## Call in line 6 causes the problematic return: + +g = my_generator() \ No newline at end of file diff --git a/python/test-data/testDisappearingObject_01.err b/python/file-test-data/basics/iterator.out similarity index 100% rename from python/test-data/testDisappearingObject_01.err rename to python/file-test-data/basics/iterator.out diff --git a/python/test-data/testDisappearingObject_02.err b/python/file-test-data/basics/iterator.out_en similarity index 100% rename from python/test-data/testDisappearingObject_02.err rename to python/file-test-data/basics/iterator.out_en diff --git a/python/file-test-data/basics/iterator.py b/python/file-test-data/basics/iterator.py new file mode 100644 index 00000000..cc0c7e4b --- /dev/null +++ b/python/file-test-data/basics/iterator.py @@ -0,0 +1,8 @@ +from typing import * + +def my_generator() -> Iterator[int]: + return 1 + +g = my_generator() +for x in my_generator(): + print(x) diff --git a/python/test-data/testDisappearingObject_02.out b/python/file-test-data/basics/iterator_ok.err similarity index 100% rename from python/test-data/testDisappearingObject_02.out rename to python/file-test-data/basics/iterator_ok.err diff --git a/python/file-test-data/basics/iterator_ok.out b/python/file-test-data/basics/iterator_ok.out new file mode 100644 index 00000000..7e3104bf --- /dev/null +++ b/python/file-test-data/basics/iterator_ok.out @@ -0,0 +1,3 @@ +6 +7 +ok diff --git a/python/file-test-data/basics/iterator_ok.py b/python/file-test-data/basics/iterator_ok.py new file mode 100644 index 00000000..8baf1cb4 --- /dev/null +++ b/python/file-test-data/basics/iterator_ok.py @@ -0,0 +1,9 @@ +from wypp import * + +def my_generator() -> Iterator[int]: + yield 6 + yield "7" + +for x in my_generator(): + print(x) +print('ok') diff --git a/python/file-test-data/basics/kwargs.err b/python/file-test-data/basics/kwargs.err new file mode 100644 index 00000000..d7c8f199 --- /dev/null +++ b/python/file-test-data/basics/kwargs.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/kwargs.py", line 4, in + foo(1, y=1, z=2) + +WyppTypeError: 2 + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `str` als Argument `z`. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/basics/kwargs.py +## Fehlerhafter Aufruf in Zeile 4: + +foo(1, y=1, z=2) + +## Typ deklariert in Zeile 1: + +def foo(x: int, y: int, z: str) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/kwargs.err_en b/python/file-test-data/basics/kwargs.err_en new file mode 100644 index 00000000..7a482090 --- /dev/null +++ b/python/file-test-data/basics/kwargs.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/kwargs.py", line 4, in + foo(1, y=1, z=2) + +WyppTypeError: 2 + +The call of function `foo` expects value of type `str` as argument `z`. +But the value given has type `int`. + +## File file-test-data/basics/kwargs.py +## Problematic call in line 4: + +foo(1, y=1, z=2) + +## Type declared in line 1: + +def foo(x: int, y: int, z: str) -> int: \ No newline at end of file diff --git a/python/test-data/testDoubleWrappingDicts.err b/python/file-test-data/basics/kwargs.out similarity index 100% rename from python/test-data/testDoubleWrappingDicts.err rename to python/file-test-data/basics/kwargs.out diff --git a/python/test-data/testForwardRef.err b/python/file-test-data/basics/kwargs.out_en similarity index 100% rename from python/test-data/testForwardRef.err rename to python/file-test-data/basics/kwargs.out_en diff --git a/python/file-test-data/basics/kwargs.py b/python/file-test-data/basics/kwargs.py new file mode 100644 index 00000000..224f67ba --- /dev/null +++ b/python/file-test-data/basics/kwargs.py @@ -0,0 +1,4 @@ +def foo(x: int, y: int, z: str) -> int: + return x + y + len(z) + +foo(1, y=1, z=2) diff --git a/python/file-test-data/basics/kwargs2.err b/python/file-test-data/basics/kwargs2.err new file mode 100644 index 00000000..0fbc3b16 --- /dev/null +++ b/python/file-test-data/basics/kwargs2.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/kwargs2.py", line 4, in + foo(1, z=2, y='foo') + +WyppTypeError: 2 + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `str` als Argument `z`. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/basics/kwargs2.py +## Fehlerhafter Aufruf in Zeile 4: + +foo(1, z=2, y='foo') + +## Typ deklariert in Zeile 1: + +def foo(x: int, y: int, z: str) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/kwargs2.err_en b/python/file-test-data/basics/kwargs2.err_en new file mode 100644 index 00000000..67437dae --- /dev/null +++ b/python/file-test-data/basics/kwargs2.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/kwargs2.py", line 4, in + foo(1, z=2, y='foo') + +WyppTypeError: 2 + +The call of function `foo` expects value of type `str` as argument `z`. +But the value given has type `int`. + +## File file-test-data/basics/kwargs2.py +## Problematic call in line 4: + +foo(1, z=2, y='foo') + +## Type declared in line 1: + +def foo(x: int, y: int, z: str) -> int: \ No newline at end of file diff --git a/python/test-data/testForwardRef.out b/python/file-test-data/basics/kwargs2.out similarity index 100% rename from python/test-data/testForwardRef.out rename to python/file-test-data/basics/kwargs2.out diff --git a/python/test-data/testForwardRef1.err b/python/file-test-data/basics/kwargs2.out_en similarity index 100% rename from python/test-data/testForwardRef1.err rename to python/file-test-data/basics/kwargs2.out_en diff --git a/python/file-test-data/basics/kwargs2.py b/python/file-test-data/basics/kwargs2.py new file mode 100644 index 00000000..95eda6c6 --- /dev/null +++ b/python/file-test-data/basics/kwargs2.py @@ -0,0 +1,4 @@ +def foo(x: int, y: int, z: str) -> int: + return x + y + len(z) + +foo(1, z=2, y='foo') diff --git a/python/test-data/testForwardRef2.out b/python/file-test-data/basics/kwargs_ok.err similarity index 100% rename from python/test-data/testForwardRef2.out rename to python/file-test-data/basics/kwargs_ok.err diff --git a/python/file-test-data/basics/kwargs_ok.out b/python/file-test-data/basics/kwargs_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/kwargs_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/kwargs_ok.py b/python/file-test-data/basics/kwargs_ok.py new file mode 100644 index 00000000..2851e3bc --- /dev/null +++ b/python/file-test-data/basics/kwargs_ok.py @@ -0,0 +1,5 @@ +def foo(x: int, y: int, z: str) -> int: + return x + y + len(z) + +foo(1, z='foo', y=2) +print('ok') diff --git a/python/file-test-data/basics/listArg.err b/python/file-test-data/basics/listArg.err new file mode 100644 index 00000000..57676ddc --- /dev/null +++ b/python/file-test-data/basics/listArg.err @@ -0,0 +1,16 @@ +Traceback (most recent call last): + File "file-test-data/basics/listArg.py", line 4, in + foo([1, 2, "3"]) + +WyppTypeError: [1, 2, '3'] + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `list[int]` als erstes Argument. + +## Datei file-test-data/basics/listArg.py +## Fehlerhafter Aufruf in Zeile 4: + +foo([1, 2, "3"]) + +## Typ deklariert in Zeile 1: + +def foo(l: list[int]) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/listArg.err_en b/python/file-test-data/basics/listArg.err_en new file mode 100644 index 00000000..b6fc16a9 --- /dev/null +++ b/python/file-test-data/basics/listArg.err_en @@ -0,0 +1,16 @@ +Traceback (most recent call last): + File "file-test-data/basics/listArg.py", line 4, in + foo([1, 2, "3"]) + +WyppTypeError: [1, 2, '3'] + +The call of function `foo` expects value of type `list[int]` as 1st argument. + +## File file-test-data/basics/listArg.py +## Problematic call in line 4: + +foo([1, 2, "3"]) + +## Type declared in line 1: + +def foo(l: list[int]) -> int: \ No newline at end of file diff --git a/python/test-data/testForwardRef3.err b/python/file-test-data/basics/listArg.out similarity index 100% rename from python/test-data/testForwardRef3.err rename to python/file-test-data/basics/listArg.out diff --git a/python/test-data/testForwardRef4.out b/python/file-test-data/basics/listArg.out_en similarity index 100% rename from python/test-data/testForwardRef4.out rename to python/file-test-data/basics/listArg.out_en diff --git a/python/file-test-data/basics/listArg.py b/python/file-test-data/basics/listArg.py new file mode 100644 index 00000000..2917bc3e --- /dev/null +++ b/python/file-test-data/basics/listArg.py @@ -0,0 +1,4 @@ +def foo(l: list[int]) -> int: + return len(l) + +foo([1, 2, "3"]) diff --git a/python/test-data/testForwardRef5.out b/python/file-test-data/basics/listArg_ok.err similarity index 100% rename from python/test-data/testForwardRef5.out rename to python/file-test-data/basics/listArg_ok.err diff --git a/python/file-test-data/basics/listArg_ok.out b/python/file-test-data/basics/listArg_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/listArg_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/listArg_ok.py b/python/file-test-data/basics/listArg_ok.py new file mode 100644 index 00000000..b5edecae --- /dev/null +++ b/python/file-test-data/basics/listArg_ok.py @@ -0,0 +1,5 @@ +def foo(l: list[int]) -> int: + return len(l) + +foo([1, 2, 3]) +print('ok') diff --git a/python/file-test-data/basics/listResult.err b/python/file-test-data/basics/listResult.err new file mode 100644 index 00000000..3330f737 --- /dev/null +++ b/python/file-test-data/basics/listResult.err @@ -0,0 +1,22 @@ +Traceback (most recent call last): + File "file-test-data/basics/listResult.py", line 5, in + foo([1, 2, 3]) + File "file-test-data/basics/listResult.py", line 3, in foo + return l + +WyppTypeError: [1, 2, 3, '4'] + +Rückgabewert vom Typ `list[int]` erwartet bei Aufruf der Funktion `foo`. + +## Datei file-test-data/basics/listResult.py +## Rückgabetyp deklariert in Zeile 1: + +def foo(l: list[int]) -> list[int]: + +## Fehlerhaftes return in Zeile 3: + + return l + +## Aufruf in Zeile 5 verursacht das fehlerhafte return: + +foo([1, 2, 3]) \ No newline at end of file diff --git a/python/file-test-data/basics/listResult.err_en b/python/file-test-data/basics/listResult.err_en new file mode 100644 index 00000000..e41debdf --- /dev/null +++ b/python/file-test-data/basics/listResult.err_en @@ -0,0 +1,22 @@ +Traceback (most recent call last): + File "file-test-data/basics/listResult.py", line 5, in + foo([1, 2, 3]) + File "file-test-data/basics/listResult.py", line 3, in foo + return l + +WyppTypeError: [1, 2, 3, '4'] + +Expecting return value of type `list[int]` when calling function `foo`. + +## File file-test-data/basics/listResult.py +## Result type declared in line 1: + +def foo(l: list[int]) -> list[int]: + +## Problematic return in line 3: + + return l + +## Call in line 5 causes the problematic return: + +foo([1, 2, 3]) \ No newline at end of file diff --git a/python/test-data/testForwardRef6.err b/python/file-test-data/basics/listResult.out similarity index 100% rename from python/test-data/testForwardRef6.err rename to python/file-test-data/basics/listResult.out diff --git a/python/test-data/testForwardTypeInRecord.err b/python/file-test-data/basics/listResult.out_en similarity index 100% rename from python/test-data/testForwardTypeInRecord.err rename to python/file-test-data/basics/listResult.out_en diff --git a/python/file-test-data/basics/listResult.py b/python/file-test-data/basics/listResult.py new file mode 100644 index 00000000..c1797727 --- /dev/null +++ b/python/file-test-data/basics/listResult.py @@ -0,0 +1,5 @@ +def foo(l: list[int]) -> list[int]: + l.append("4") + return l + +foo([1, 2, 3]) diff --git a/python/test-data/testForwardTypeInRecord2.err b/python/file-test-data/basics/listResult_ok.err similarity index 100% rename from python/test-data/testForwardTypeInRecord2.err rename to python/file-test-data/basics/listResult_ok.err diff --git a/python/file-test-data/basics/listResult_ok.out b/python/file-test-data/basics/listResult_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/listResult_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/listResult_ok.py b/python/file-test-data/basics/listResult_ok.py new file mode 100644 index 00000000..df81782e --- /dev/null +++ b/python/file-test-data/basics/listResult_ok.py @@ -0,0 +1,6 @@ +def foo(l: list[int]) -> list[int]: + l.append(4) + return l + +foo([1, 2, 3]) +print('ok') diff --git a/python/file-test-data/basics/method.err b/python/file-test-data/basics/method.err new file mode 100644 index 00000000..33d6ee80 --- /dev/null +++ b/python/file-test-data/basics/method.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/method.py", line 9, in + c.method("2") + +WyppTypeError: "2" + +Der Aufruf der Methode `method` aus Klasse `C` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/method.py +## Fehlerhafter Aufruf in Zeile 9: + +c.method("2") + +## Typ deklariert in Zeile 5: + + def method(self, y: int) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/method.err_en b/python/file-test-data/basics/method.err_en new file mode 100644 index 00000000..458cc43a --- /dev/null +++ b/python/file-test-data/basics/method.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/method.py", line 9, in + c.method("2") + +WyppTypeError: "2" + +The call of method `method` of class `C` expects value of type `int` as 1st argument. +But the value given has type `str`. + +## File file-test-data/basics/method.py +## Problematic call in line 9: + +c.method("2") + +## Type declared in line 5: + + def method(self, y: int) -> int: \ No newline at end of file diff --git a/python/test-data/testFunEq.err b/python/file-test-data/basics/method.out similarity index 100% rename from python/test-data/testFunEq.err rename to python/file-test-data/basics/method.out diff --git a/python/test-data/testGetSource.out b/python/file-test-data/basics/method.out_en similarity index 100% rename from python/test-data/testGetSource.out rename to python/file-test-data/basics/method.out_en diff --git a/python/file-test-data/basics/method.py b/python/file-test-data/basics/method.py new file mode 100644 index 00000000..5229e620 --- /dev/null +++ b/python/file-test-data/basics/method.py @@ -0,0 +1,9 @@ +class C: + def __init__(self, x: int): + self.x = x + + def method(self, y: int) -> int: + return self.x + +c = C(1) +c.method("2") diff --git a/python/test-data/testHintParentheses1.out b/python/file-test-data/basics/method_ok.err similarity index 100% rename from python/test-data/testHintParentheses1.out rename to python/file-test-data/basics/method_ok.err diff --git a/python/file-test-data/basics/method_ok.out b/python/file-test-data/basics/method_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/method_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/method_ok.py b/python/file-test-data/basics/method_ok.py new file mode 100644 index 00000000..7b7d1546 --- /dev/null +++ b/python/file-test-data/basics/method_ok.py @@ -0,0 +1,10 @@ +class C: + def __init__(self, x: int): + self.x = x + + def method(self, y: int) -> int: + return self.x + y + +c = C(1) +c.method(2) +print('ok') diff --git a/python/file-test-data/basics/mutable.err b/python/file-test-data/basics/mutable.err new file mode 100644 index 00000000..8569f41e --- /dev/null +++ b/python/file-test-data/basics/mutable.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/mutable.py", line 10, in + p.y = "foo" + +WyppTypeError: "foo" + +Attribut `y` des Records `Point` deklariert als Typ `int`. +Das Attribute kann nicht auf einen Wert vom Typ `str` gesetzt werden. + +## Datei file-test-data/basics/mutable.py +## Fehlerhafte Zuweisung in Zeile 10: + +p.y = "foo" + +## Typ deklariert in Zeile 6: + + y: int \ No newline at end of file diff --git a/python/file-test-data/basics/mutable.err_en b/python/file-test-data/basics/mutable.err_en new file mode 100644 index 00000000..1c39452c --- /dev/null +++ b/python/file-test-data/basics/mutable.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/mutable.py", line 10, in + p.y = "foo" + +WyppTypeError: "foo" + +Attribute `y` of record `Point` declared with type `int.` +Cannot set attribute to value of type `str`. + +## File file-test-data/basics/mutable.py +## Problematic assignment in line 10: + +p.y = "foo" + +## Type declared in line 6: + + y: int \ No newline at end of file diff --git a/python/test-data/testHintParentheses2.out b/python/file-test-data/basics/mutable.out similarity index 100% rename from python/test-data/testHintParentheses2.out rename to python/file-test-data/basics/mutable.out diff --git a/python/test-data/testHintParentheses3.out b/python/file-test-data/basics/mutable.out_en similarity index 100% rename from python/test-data/testHintParentheses3.out rename to python/file-test-data/basics/mutable.out_en diff --git a/python/file-test-data/basics/mutable.py b/python/file-test-data/basics/mutable.py new file mode 100644 index 00000000..fdb88c67 --- /dev/null +++ b/python/file-test-data/basics/mutable.py @@ -0,0 +1,10 @@ +from wypp import * + +@record(mutable=True) +class Point: + x: int + y: int + +p = Point(1, 2) +p.x = 5 +p.y = "foo" diff --git a/python/file-test-data/basics/mutable2.err b/python/file-test-data/basics/mutable2.err new file mode 100644 index 00000000..e3ab1fb4 --- /dev/null +++ b/python/file-test-data/basics/mutable2.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/mutable2.py", line 8, in + p = Point(1, '2') + +WyppTypeError: '2' + +Der Aufruf des Konstruktors des Records `Point` erwartet Wert vom Typ `int` als zweites Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/mutable2.py +## Fehlerhafter Aufruf in Zeile 8: + +p = Point(1, '2') + +## Typ deklariert in Zeile 6: + + y: int \ No newline at end of file diff --git a/python/file-test-data/basics/mutable2.err_en b/python/file-test-data/basics/mutable2.err_en new file mode 100644 index 00000000..515ba4ec --- /dev/null +++ b/python/file-test-data/basics/mutable2.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/mutable2.py", line 8, in + p = Point(1, '2') + +WyppTypeError: '2' + +The call of the constructor of record `Point` expects value of type `int` as 2nd argument. +But the value given has type `str`. + +## File file-test-data/basics/mutable2.py +## Problematic call in line 8: + +p = Point(1, '2') + +## Type declared in line 6: + + y: int \ No newline at end of file diff --git a/python/test-data/testHof.err b/python/file-test-data/basics/mutable2.out similarity index 100% rename from python/test-data/testHof.err rename to python/file-test-data/basics/mutable2.out diff --git a/python/test-data/testImpossible.out b/python/file-test-data/basics/mutable2.out_en similarity index 100% rename from python/test-data/testImpossible.out rename to python/file-test-data/basics/mutable2.out_en diff --git a/python/file-test-data/basics/mutable2.py b/python/file-test-data/basics/mutable2.py new file mode 100644 index 00000000..8e5f2161 --- /dev/null +++ b/python/file-test-data/basics/mutable2.py @@ -0,0 +1,8 @@ +from wypp import * + +@record(mutable=True) +class Point: + x: int + y: int + +p = Point(1, '2') diff --git a/python/test-data/testIndexError.out b/python/file-test-data/basics/mutable2_ok.err similarity index 100% rename from python/test-data/testIndexError.out rename to python/file-test-data/basics/mutable2_ok.err diff --git a/python/file-test-data/basics/mutable2_ok.out b/python/file-test-data/basics/mutable2_ok.out new file mode 100644 index 00000000..337e5e81 --- /dev/null +++ b/python/file-test-data/basics/mutable2_ok.out @@ -0,0 +1 @@ +CourseM(name='Grundlagen der Informatik', teacher='Oelke', students=[]) diff --git a/python/file-test-data/basics/mutable2_ok.py b/python/file-test-data/basics/mutable2_ok.py new file mode 100644 index 00000000..5300f178 --- /dev/null +++ b/python/file-test-data/basics/mutable2_ok.py @@ -0,0 +1,10 @@ +from wypp import * + +@record(mutable=True) +class CourseM: + name: str + teacher: str + students: list[str] + +x = CourseM('Grundlagen der Informatik', 'Oelke', []) +print(x) diff --git a/python/test-data/testIndexSeq.err b/python/file-test-data/basics/mutable_ok.err similarity index 100% rename from python/test-data/testIndexSeq.err rename to python/file-test-data/basics/mutable_ok.err diff --git a/python/file-test-data/basics/mutable_ok.out b/python/file-test-data/basics/mutable_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/mutable_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/mutable_ok.py b/python/file-test-data/basics/mutable_ok.py new file mode 100644 index 00000000..a1eb6aee --- /dev/null +++ b/python/file-test-data/basics/mutable_ok.py @@ -0,0 +1,10 @@ +from wypp import * + +@record(mutable=True) +class Point: + x: int + y: int + +p = Point(1, 2) +p.x = 5 +print('ok') diff --git a/python/file-test-data/basics/nested.err b/python/file-test-data/basics/nested.err new file mode 100644 index 00000000..d814332f --- /dev/null +++ b/python/file-test-data/basics/nested.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/nested.py", line 4, in + foo(42) + +WyppTypeError: 42 + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `list[list[int]]` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/basics/nested.py +## Fehlerhafter Aufruf in Zeile 4: + +foo(42) + +## Typ deklariert in Zeile 1: + +def foo(l: list[list[int]]) -> int: \ No newline at end of file diff --git a/python/test-data/testInferReturnType.err b/python/file-test-data/basics/nested.out similarity index 100% rename from python/test-data/testInferReturnType.err rename to python/file-test-data/basics/nested.out diff --git a/python/file-test-data/basics/nested.py b/python/file-test-data/basics/nested.py new file mode 100644 index 00000000..7287f825 --- /dev/null +++ b/python/file-test-data/basics/nested.py @@ -0,0 +1,4 @@ +def foo(l: list[list[int]]) -> int: + return len(l) + +foo(42) diff --git a/python/file-test-data/basics/nestedFun.err b/python/file-test-data/basics/nestedFun.err new file mode 100644 index 00000000..84ff8062 --- /dev/null +++ b/python/file-test-data/basics/nestedFun.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/nestedFun.py", line 9, in + foo(1) + File "file-test-data/basics/nestedFun.py", line 7, in foo + return bar("foo") + +WyppTypeError: "foo" + +Der Aufruf der Funktion `bar` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/nestedFun.py +## Fehlerhafter Aufruf in Zeile 7: + + return bar("foo") + +## Typ deklariert in Zeile 5: + + def bar(j: int) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/nestedFun.err_en b/python/file-test-data/basics/nestedFun.err_en new file mode 100644 index 00000000..e9d766fd --- /dev/null +++ b/python/file-test-data/basics/nestedFun.err_en @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/basics/nestedFun.py", line 9, in + foo(1) + File "file-test-data/basics/nestedFun.py", line 7, in foo + return bar("foo") + +WyppTypeError: "foo" + +The call of function `bar` expects value of type `int` as 1st argument. +But the value given has type `str`. + +## File file-test-data/basics/nestedFun.py +## Problematic call in line 7: + + return bar("foo") + +## Type declared in line 5: + + def bar(j: int) -> int: \ No newline at end of file diff --git a/python/test-data/testInferReturnType.out b/python/file-test-data/basics/nestedFun.out similarity index 100% rename from python/test-data/testInferReturnType.out rename to python/file-test-data/basics/nestedFun.out diff --git a/python/test-data/testInferReturnType2.out b/python/file-test-data/basics/nestedFun.out_en similarity index 100% rename from python/test-data/testInferReturnType2.out rename to python/file-test-data/basics/nestedFun.out_en diff --git a/python/file-test-data/basics/nestedFun.py b/python/file-test-data/basics/nestedFun.py new file mode 100644 index 00000000..0cd20063 --- /dev/null +++ b/python/file-test-data/basics/nestedFun.py @@ -0,0 +1,9 @@ +def bar(s: str) -> int: + return len(s) + +def foo(i: int) -> int: + def bar(j: int) -> int: + return j + 1 + return bar("foo") + +foo(1) diff --git a/python/test-data/testInferReturnType3.err b/python/file-test-data/basics/nosig_ok.err similarity index 100% rename from python/test-data/testInferReturnType3.err rename to python/file-test-data/basics/nosig_ok.err diff --git a/python/file-test-data/basics/nosig_ok.out b/python/file-test-data/basics/nosig_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/nosig_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/nosig_ok.py b/python/file-test-data/basics/nosig_ok.py new file mode 100644 index 00000000..3b934537 --- /dev/null +++ b/python/file-test-data/basics/nosig_ok.py @@ -0,0 +1,14 @@ +def foo(i, j): + return i + j + +class C: + def bar(self, x): + return x + 1 + +k = foo(1, 2) +c = C() +r = c.bar(k) +if r == 4: + print('ok') +else: + raise ValueError('unexpected result') diff --git a/python/file-test-data/basics/optionalArgs.err b/python/file-test-data/basics/optionalArgs.err new file mode 100644 index 00000000..100e8290 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs.py", line 1, in + def foo(i: int, s: str=2): + +WyppTypeError: 2 + +Default-Wert des Parameters `s` der Funktion `foo` muss vom Typ `str` sein. +Aber der Default-Wert hat Typ `int`. + +## Datei file-test-data/basics/optionalArgs.py +## Parameter deklariert in Zeile 1: + +def foo(i: int, s: str=2): \ No newline at end of file diff --git a/python/file-test-data/basics/optionalArgs.err_en b/python/file-test-data/basics/optionalArgs.err_en new file mode 100644 index 00000000..0c7ceff6 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs.py", line 1, in + def foo(i: int, s: str=2): + +WyppTypeError: 2 + +Default value for parameter `s` of function `foo` must have type `str`. +But the default value has type `int`. + +## File file-test-data/basics/optionalArgs.py +## Parameter declared in line 1: + +def foo(i: int, s: str=2): \ No newline at end of file diff --git a/python/test-data/testInferReturnType3.out b/python/file-test-data/basics/optionalArgs.out similarity index 100% rename from python/test-data/testInferReturnType3.out rename to python/file-test-data/basics/optionalArgs.out diff --git a/python/test-data/testInferReturnType4.out b/python/file-test-data/basics/optionalArgs.out_en similarity index 100% rename from python/test-data/testInferReturnType4.out rename to python/file-test-data/basics/optionalArgs.out_en diff --git a/python/file-test-data/basics/optionalArgs.py b/python/file-test-data/basics/optionalArgs.py new file mode 100644 index 00000000..96a209d1 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs.py @@ -0,0 +1,4 @@ +def foo(i: int, s: str=2): + pass + +foo(1) diff --git a/python/file-test-data/basics/optionalArgs2.err b/python/file-test-data/basics/optionalArgs2.err new file mode 100644 index 00000000..70409aa7 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs2.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs2.py", line 4, in + foo(1) + +WyppTypeError: Anzahl der Argument passt nicht + +Funktion `foo` benötigt mindestens 2 Argumente. +Gegeben: 1 Argument + +## Datei file-test-data/basics/optionalArgs2.py +## Aufruf in Zeile 4: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/basics/optionalArgs2.err_en b/python/file-test-data/basics/optionalArgs2.err_en new file mode 100644 index 00000000..f2505277 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs2.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs2.py", line 4, in + foo(1) + +WyppTypeError: argument count mismatch + +Function `foo` takes at least 2 arguments. +Given: 1 argument + +## File file-test-data/basics/optionalArgs2.py +## Call in line 4: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testInvalidLiteral.out b/python/file-test-data/basics/optionalArgs2.out similarity index 100% rename from python/test-data/testInvalidLiteral.out rename to python/file-test-data/basics/optionalArgs2.out diff --git a/python/test-data/testIterable1.err b/python/file-test-data/basics/optionalArgs2.out_en similarity index 100% rename from python/test-data/testIterable1.err rename to python/file-test-data/basics/optionalArgs2.out_en diff --git a/python/file-test-data/basics/optionalArgs2.py b/python/file-test-data/basics/optionalArgs2.py new file mode 100644 index 00000000..3a145537 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs2.py @@ -0,0 +1,4 @@ +def foo(i: int, j: int, s: int=2): + pass + +foo(1) diff --git a/python/file-test-data/basics/optionalArgs3.err b/python/file-test-data/basics/optionalArgs3.err new file mode 100644 index 00000000..7d626a42 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs3.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs3.py", line 4, in + foo(1, 2, 3, 4) + +WyppTypeError: Anzahl der Argument passt nicht + +Funktion `foo` akzeptiert höchstens 3 Argumente. +Gegeben: 4 Argumente + +## Datei file-test-data/basics/optionalArgs3.py +## Aufruf in Zeile 4: + +foo(1, 2, 3, 4) \ No newline at end of file diff --git a/python/file-test-data/basics/optionalArgs3.err_en b/python/file-test-data/basics/optionalArgs3.err_en new file mode 100644 index 00000000..26e80eb9 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs3.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs3.py", line 4, in + foo(1, 2, 3, 4) + +WyppTypeError: argument count mismatch + +Function `foo` takes at most 3 arguments. +Given: 4 arguments + +## File file-test-data/basics/optionalArgs3.py +## Call in line 4: + +foo(1, 2, 3, 4) \ No newline at end of file diff --git a/python/test-data/testIterable2.err b/python/file-test-data/basics/optionalArgs3.out similarity index 100% rename from python/test-data/testIterable2.err rename to python/file-test-data/basics/optionalArgs3.out diff --git a/python/test-data/testIterable3.err b/python/file-test-data/basics/optionalArgs3.out_en similarity index 100% rename from python/test-data/testIterable3.err rename to python/file-test-data/basics/optionalArgs3.out_en diff --git a/python/file-test-data/basics/optionalArgs3.py b/python/file-test-data/basics/optionalArgs3.py new file mode 100644 index 00000000..7c9c48d0 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs3.py @@ -0,0 +1,4 @@ +def foo(i: int, j: int, s: int=2): + pass + +foo(1, 2, 3, 4) diff --git a/python/file-test-data/basics/optionalArgs4.err b/python/file-test-data/basics/optionalArgs4.err new file mode 100644 index 00000000..642f35ba --- /dev/null +++ b/python/file-test-data/basics/optionalArgs4.err @@ -0,0 +1,15 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs4.py", line 1, in + class C: + File "file-test-data/basics/optionalArgs4.py", line 2, in C + def __init__(i: int, s: str=2): + +WyppTypeError: 2 + +Default-Wert des Parameters `s` der Methode `__init__` aus Klasse `C` muss vom Typ `str` sein. +Aber der Default-Wert hat Typ `int`. + +## Datei file-test-data/basics/optionalArgs4.py +## Parameter deklariert in Zeile 2: + + def __init__(i: int, s: str=2): \ No newline at end of file diff --git a/python/file-test-data/basics/optionalArgs4.err_en b/python/file-test-data/basics/optionalArgs4.err_en new file mode 100644 index 00000000..bef9dca7 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs4.err_en @@ -0,0 +1,15 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalArgs4.py", line 1, in + class C: + File "file-test-data/basics/optionalArgs4.py", line 2, in C + def __init__(i: int, s: str=2): + +WyppTypeError: 2 + +Default value for parameter `s` of method `__init__` in class `C` must have type `str`. +But the default value has type `int`. + +## File file-test-data/basics/optionalArgs4.py +## Parameter declared in line 2: + + def __init__(i: int, s: str=2): \ No newline at end of file diff --git a/python/test-data/testIterable4.err b/python/file-test-data/basics/optionalArgs4.out similarity index 100% rename from python/test-data/testIterable4.err rename to python/file-test-data/basics/optionalArgs4.out diff --git a/python/test-data/testIterable5.err b/python/file-test-data/basics/optionalArgs4.out_en similarity index 100% rename from python/test-data/testIterable5.err rename to python/file-test-data/basics/optionalArgs4.out_en diff --git a/python/file-test-data/basics/optionalArgs4.py b/python/file-test-data/basics/optionalArgs4.py new file mode 100644 index 00000000..6174209d --- /dev/null +++ b/python/file-test-data/basics/optionalArgs4.py @@ -0,0 +1,5 @@ +class C: + def __init__(i: int, s: str=2): + pass + +C(1) diff --git a/python/test-data/testIterable6.err b/python/file-test-data/basics/optionalArgs_ok.err similarity index 100% rename from python/test-data/testIterable6.err rename to python/file-test-data/basics/optionalArgs_ok.err diff --git a/python/file-test-data/basics/optionalArgs_ok.out b/python/file-test-data/basics/optionalArgs_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/optionalArgs_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/optionalArgs_ok.py b/python/file-test-data/basics/optionalArgs_ok.py new file mode 100644 index 00000000..3abb6630 --- /dev/null +++ b/python/file-test-data/basics/optionalArgs_ok.py @@ -0,0 +1,5 @@ +def foo(i: int, s: str='2'): + pass + +foo(1) +print('ok') diff --git a/python/file-test-data/basics/optionalAttr.err b/python/file-test-data/basics/optionalAttr.err new file mode 100644 index 00000000..43d19e81 --- /dev/null +++ b/python/file-test-data/basics/optionalAttr.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalAttr.py", line 3, in + @record + +WyppTypeError: 'foo' + +Default-Wert des Attributs `y` des Records `C` muss vom Typ `int` sein. +Aber der Default-Wert hat Typ `str`. + +## Datei file-test-data/basics/optionalAttr.py +## Parameter deklariert in Zeile 6: + + y: int = 'foo' \ No newline at end of file diff --git a/python/file-test-data/basics/optionalAttr.err_en b/python/file-test-data/basics/optionalAttr.err_en new file mode 100644 index 00000000..21401c73 --- /dev/null +++ b/python/file-test-data/basics/optionalAttr.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalAttr.py", line 3, in + @record + +WyppTypeError: 'foo' + +Default value for attribute `y` of record `C` must have type `int`. +But the default value has type `str`. + +## File file-test-data/basics/optionalAttr.py +## Parameter declared in line 6: + + y: int = 'foo' \ No newline at end of file diff --git a/python/test-data/testIterator.err b/python/file-test-data/basics/optionalAttr.out similarity index 100% rename from python/test-data/testIterator.err rename to python/file-test-data/basics/optionalAttr.out diff --git a/python/test-data/testIterator2.err b/python/file-test-data/basics/optionalAttr.out_en similarity index 100% rename from python/test-data/testIterator2.err rename to python/file-test-data/basics/optionalAttr.out_en diff --git a/python/file-test-data/basics/optionalAttr.py b/python/file-test-data/basics/optionalAttr.py new file mode 100644 index 00000000..dbfbae84 --- /dev/null +++ b/python/file-test-data/basics/optionalAttr.py @@ -0,0 +1,6 @@ +from wypp import * + +@record +class C: + x: int + y: int = 'foo' diff --git a/python/file-test-data/basics/optionalAttr2.err b/python/file-test-data/basics/optionalAttr2.err new file mode 100644 index 00000000..98e5620a --- /dev/null +++ b/python/file-test-data/basics/optionalAttr2.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalAttr2.py", line 9, in + c = C(1) + +WyppTypeError: Anzahl der Argument passt nicht + +Konstruktor des Records `C` benötigt mindestens 2 Argumente. +Gegeben: 1 Argument + +## Datei file-test-data/basics/optionalAttr2.py +## Aufruf in Zeile 9: + +c = C(1) \ No newline at end of file diff --git a/python/file-test-data/basics/optionalAttr2.err_en b/python/file-test-data/basics/optionalAttr2.err_en new file mode 100644 index 00000000..030f5e0a --- /dev/null +++ b/python/file-test-data/basics/optionalAttr2.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalAttr2.py", line 9, in + c = C(1) + +WyppTypeError: argument count mismatch + +Constructor of record `C` takes at least 2 arguments. +Given: 1 argument + +## File file-test-data/basics/optionalAttr2.py +## Call in line 9: + +c = C(1) \ No newline at end of file diff --git a/python/test-data/testIterator3.err b/python/file-test-data/basics/optionalAttr2.out similarity index 100% rename from python/test-data/testIterator3.err rename to python/file-test-data/basics/optionalAttr2.out diff --git a/python/test-data/testIterator4.err b/python/file-test-data/basics/optionalAttr2.out_en similarity index 100% rename from python/test-data/testIterator4.err rename to python/file-test-data/basics/optionalAttr2.out_en diff --git a/python/file-test-data/basics/optionalAttr2.py b/python/file-test-data/basics/optionalAttr2.py new file mode 100644 index 00000000..3eef77fe --- /dev/null +++ b/python/file-test-data/basics/optionalAttr2.py @@ -0,0 +1,9 @@ +from wypp import * + +@record +class C: + x: int + y: int + z: int = 2 + +c = C(1) diff --git a/python/file-test-data/basics/optionalAttr3.err b/python/file-test-data/basics/optionalAttr3.err new file mode 100644 index 00000000..9b406713 --- /dev/null +++ b/python/file-test-data/basics/optionalAttr3.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalAttr3.py", line 9, in + c = C(1, 2, 3, 4) + +WyppTypeError: Anzahl der Argument passt nicht + +Konstruktor des Records `C` akzeptiert höchstens 3 Argumente. +Gegeben: 4 Argumente + +## Datei file-test-data/basics/optionalAttr3.py +## Aufruf in Zeile 9: + +c = C(1, 2, 3, 4) \ No newline at end of file diff --git a/python/file-test-data/basics/optionalAttr3.err_en b/python/file-test-data/basics/optionalAttr3.err_en new file mode 100644 index 00000000..17976c56 --- /dev/null +++ b/python/file-test-data/basics/optionalAttr3.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/optionalAttr3.py", line 9, in + c = C(1, 2, 3, 4) + +WyppTypeError: argument count mismatch + +Constructor of record `C` takes at most 3 arguments. +Given: 4 arguments + +## File file-test-data/basics/optionalAttr3.py +## Call in line 9: + +c = C(1, 2, 3, 4) \ No newline at end of file diff --git a/python/test-data/testLiteral1.out b/python/file-test-data/basics/optionalAttr3.out similarity index 100% rename from python/test-data/testLiteral1.out rename to python/file-test-data/basics/optionalAttr3.out diff --git a/python/test-data/testLiteralInstanceOf.err b/python/file-test-data/basics/optionalAttr3.out_en similarity index 100% rename from python/test-data/testLiteralInstanceOf.err rename to python/file-test-data/basics/optionalAttr3.out_en diff --git a/python/file-test-data/basics/optionalAttr3.py b/python/file-test-data/basics/optionalAttr3.py new file mode 100644 index 00000000..735db5cf --- /dev/null +++ b/python/file-test-data/basics/optionalAttr3.py @@ -0,0 +1,9 @@ +from wypp import * + +@record +class C: + x: int + y: int + z: int = 2 + +c = C(1, 2, 3, 4) diff --git a/python/test-data/testLockFactory.out b/python/file-test-data/basics/optionalAttr_ok.err similarity index 100% rename from python/test-data/testLockFactory.out rename to python/file-test-data/basics/optionalAttr_ok.err diff --git a/python/file-test-data/basics/optionalAttr_ok.out b/python/file-test-data/basics/optionalAttr_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/optionalAttr_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/optionalAttr_ok.py b/python/file-test-data/basics/optionalAttr_ok.py new file mode 100644 index 00000000..eb566fc6 --- /dev/null +++ b/python/file-test-data/basics/optionalAttr_ok.py @@ -0,0 +1,10 @@ +from wypp import * + +@record +class C: + x: int + y: int = 0 + +c1 = C(1) +c2 = C(1, 2) +print('ok') diff --git a/python/file-test-data/basics/partial.err b/python/file-test-data/basics/partial.err new file mode 100644 index 00000000..23401695 --- /dev/null +++ b/python/file-test-data/basics/partial.err @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/basics/partial.py", line 1, in + def foo(i: int, j) -> None: + +WyppTypeError: Parameter `j` der Funktion `foo` benötigt eine Typannotation. + +## Datei file-test-data/basics/partial.py +## Parameter deklariert in Zeile 1: + +def foo(i: int, j) -> None: \ No newline at end of file diff --git a/python/file-test-data/basics/partial.err_en b/python/file-test-data/basics/partial.err_en new file mode 100644 index 00000000..6f3cb523 --- /dev/null +++ b/python/file-test-data/basics/partial.err_en @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/basics/partial.py", line 1, in + def foo(i: int, j) -> None: + +WyppTypeError: Parameter `j` of function `foo` requires a type annotation. + +## File file-test-data/basics/partial.py +## Parameter declared in line 1: + +def foo(i: int, j) -> None: \ No newline at end of file diff --git a/python/test-data/testLockFactory2.out b/python/file-test-data/basics/partial.out similarity index 100% rename from python/test-data/testLockFactory2.out rename to python/file-test-data/basics/partial.out diff --git a/python/test-data/testMissingReturn.out b/python/file-test-data/basics/partial.out_en similarity index 100% rename from python/test-data/testMissingReturn.out rename to python/file-test-data/basics/partial.out_en diff --git a/python/file-test-data/basics/partial.py b/python/file-test-data/basics/partial.py new file mode 100644 index 00000000..5ed50475 --- /dev/null +++ b/python/file-test-data/basics/partial.py @@ -0,0 +1,4 @@ +def foo(i: int, j) -> None: + pass + +foo(1, 2) diff --git a/python/file-test-data/basics/record.err b/python/file-test-data/basics/record.err new file mode 100644 index 00000000..b2daf4fd --- /dev/null +++ b/python/file-test-data/basics/record.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/record.py", line 8, in + p = Person("Alice", "30") + +WyppTypeError: "30" + +Der Aufruf des Konstruktors des Records `Person` erwartet Wert vom Typ `int` als zweites Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/record.py +## Fehlerhafter Aufruf in Zeile 8: + +p = Person("Alice", "30") + +## Typ deklariert in Zeile 6: + + age: int \ No newline at end of file diff --git a/python/file-test-data/basics/record.err_en b/python/file-test-data/basics/record.err_en new file mode 100644 index 00000000..d3e78da1 --- /dev/null +++ b/python/file-test-data/basics/record.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/record.py", line 8, in + p = Person("Alice", "30") + +WyppTypeError: "30" + +The call of the constructor of record `Person` expects value of type `int` as 2nd argument. +But the value given has type `str`. + +## File file-test-data/basics/record.py +## Problematic call in line 8: + +p = Person("Alice", "30") + +## Type declared in line 6: + + age: int \ No newline at end of file diff --git a/python/test-data/testNameErrorBug.err b/python/file-test-data/basics/record.out similarity index 100% rename from python/test-data/testNameErrorBug.err rename to python/file-test-data/basics/record.out diff --git a/python/test-data/testNameErrorBug.out b/python/file-test-data/basics/record.out_en similarity index 100% rename from python/test-data/testNameErrorBug.out rename to python/file-test-data/basics/record.out_en diff --git a/python/file-test-data/basics/record.py b/python/file-test-data/basics/record.py new file mode 100644 index 00000000..5adf60f5 --- /dev/null +++ b/python/file-test-data/basics/record.py @@ -0,0 +1,8 @@ +from wypp import * + +@record +class Person: + name: str + age: int + +p = Person("Alice", "30") diff --git a/python/test-data/testNums.out b/python/file-test-data/basics/record_ok.err similarity index 100% rename from python/test-data/testNums.out rename to python/file-test-data/basics/record_ok.err diff --git a/python/file-test-data/basics/record_ok.out b/python/file-test-data/basics/record_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/record_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/record_ok.py b/python/file-test-data/basics/record_ok.py new file mode 100644 index 00000000..3b5eb905 --- /dev/null +++ b/python/file-test-data/basics/record_ok.py @@ -0,0 +1,9 @@ +from wypp import * + +@record +class Person: + name: str + age: int + +p = Person("Alice", 30) +print('ok') diff --git a/python/test-data/testOriginalTypeNames.err b/python/file-test-data/basics/sample_ok.err similarity index 100% rename from python/test-data/testOriginalTypeNames.err rename to python/file-test-data/basics/sample_ok.err diff --git a/python/file-test-data/basics/sample_ok.out b/python/file-test-data/basics/sample_ok.out new file mode 100644 index 00000000..aaf9a472 --- /dev/null +++ b/python/file-test-data/basics/sample_ok.out @@ -0,0 +1 @@ +14 Tests, alle erfolgreich 😀 diff --git a/python/file-test-data/basics/sample_ok.py b/python/file-test-data/basics/sample_ok.py new file mode 100644 index 00000000..d117e9fb --- /dev/null +++ b/python/file-test-data/basics/sample_ok.py @@ -0,0 +1,132 @@ +from wypp import * + +Drink = Literal["Tea", "Coffee"] + +# berechnet wieviele Tassen ich von einem Getränk trinken darf +def canDrink(d: Drink) -> int: + if d == "Tea": + return 5 + elif d == "Coffee": + return 1 + +# A shape is one of the following: +# - a circle (Circle) +# - a square (Square) +# - an overlay of two shapes (Overlay) +type Shape = Union['Circle', 'Square', 'Overlay'] + +# A point consists of +# - x (float) +# - y (float) +@record +class Point: + x: float + y: float +# Point: (float, float) -> Point +# For some Point p +# p.x: float +# p.y: float + +# point at x=10, y=20 +p1 = Point(10, 20) + +# point at x=30, y=50 +p2 = Point(30, 50) + +# point at x=40, y=30 +p3 = Point(40, 30) + +# A circle consists of +# - center (Point) +# - radius (float) +@record +class Circle: + center: Point + radius: float + +# Circle: (Point, float) -> Circle +# For some circle c +# c.center: Point +# c.radius: float + +# circle at p2 with radius=20 +c1 = Circle(p2, 20) + +# circle at p3 with radius=15 +c2 = Circle(p3, 15) + +# A square (parallel to the coordinate system) consists of +# - lower-left corner (Point) +# - size (float) +@record +class Square: + corner: Point + size: float + +# square at p1 with size=40 +s1 = Square(p1, 40) + +# Square: (Point, float) -> Square +# For some square s +# s.corner: Point +# s.size: float + +# An overlay consists of +# - top (Shape) +# - bottom (Shape) +@record +class Overlay: + top: Shape + bottom: Shape + +# Overlay: (Shape, Shape) -> Overlay +# For some overlay: +# o.top: Shape +# o.bottom: Shape + +# overlay of circle c1 and square s1 +o1 = Overlay(c1, s1) +# Overlay of overlay o1 and circle c2 +o2 = Overlay(o1, c2) + +# Calculate the distance between two points +def distance(p1: Point, p2: Point) -> float: + w = p1.x - p2.x + h = p1.y - p2.y + dist = math.sqrt(w**2 + h**2) + return dist + +# Is a point within a shape? +def pointInShape(point: Point, shape: Shape) -> bool: + px = point.x + py = point.y + if type(shape) == Circle: + return distance(point, shape.center) <= shape.radius + elif type(shape) == Square: + corner = shape.corner + size = shape.size + return ( + px >= corner.x and + px <= corner.x + size and + py >= corner.y and + py <= corner.y + size + ) + elif type(shape) == Overlay: + return pointInShape(point, shape.top) or pointInShape(point, shape.bottom) + else: + return impossible() + +check(pointInShape(p2, c1), True) +check(pointInShape(p3, c2), True) +check(pointInShape(Point(51, 50), c1), False) +check(pointInShape(Point(11, 21), s1), True) +check(pointInShape(Point(49, 59), s1), True) +check(pointInShape(Point(9, 21), s1), False) +check(pointInShape(Point(11, 19), s1), False) +check(pointInShape(Point(51, 59), s1), False) +check(pointInShape(Point(49, 61), s1), False) +check(pointInShape(Point(40, 30), c2), True) +check(pointInShape(Point(40, 30), o2), True) +check(pointInShape(Point(0, 0), o2), False) +check(pointInShape(Point(30, 65), o2), True) +check(pointInShape(Point(40, 17), o2), True) diff --git a/python/file-test-data/basics/stack.err b/python/file-test-data/basics/stack.err new file mode 100644 index 00000000..b99a6f58 --- /dev/null +++ b/python/file-test-data/basics/stack.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/stack.py", line 7, in + factorial(5) + File "file-test-data/basics/stack.py", line 5, in factorial + return factorial(n - 1) * n + File "file-test-data/basics/stack.py", line 5, in factorial + return factorial(n - 1) * n + File "file-test-data/basics/stack.py", line 5, in factorial + return factorial(n - 1) * n + [Previous line repeated 2 more times] + File "file-test-data/basics/stack.py", line 3, in factorial + raise ValueError('kein Bock') +ValueError: kein Bock \ No newline at end of file diff --git a/python/test-data/testRecordSetTypeForwardRef.out b/python/file-test-data/basics/stack.out similarity index 100% rename from python/test-data/testRecordSetTypeForwardRef.out rename to python/file-test-data/basics/stack.out diff --git a/python/file-test-data/basics/stack.py b/python/file-test-data/basics/stack.py new file mode 100644 index 00000000..96c2f3bf --- /dev/null +++ b/python/file-test-data/basics/stack.py @@ -0,0 +1,7 @@ +def factorial(n: int) -> int: + if n == 0: + raise ValueError('kein Bock') + else: + return factorial(n - 1) * n + +factorial(5) diff --git a/python/file-test-data/basics/staticmethod.err b/python/file-test-data/basics/staticmethod.err new file mode 100644 index 00000000..dbffb013 --- /dev/null +++ b/python/file-test-data/basics/staticmethod.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/staticmethod.py", line 6, in + C.method("2") + +WyppTypeError: "2" + +Der Aufruf der Methode `method` aus Klasse `C` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/staticmethod.py +## Fehlerhafter Aufruf in Zeile 6: + +C.method("2") + +## Typ deklariert in Zeile 3: + + def method(y: int) -> int: \ No newline at end of file diff --git a/python/file-test-data/basics/staticmethod.err_en b/python/file-test-data/basics/staticmethod.err_en new file mode 100644 index 00000000..70642fac --- /dev/null +++ b/python/file-test-data/basics/staticmethod.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/staticmethod.py", line 6, in + C.method("2") + +WyppTypeError: "2" + +The call of method `method` of class `C` expects value of type `int` as 1st argument. +But the value given has type `str`. + +## File file-test-data/basics/staticmethod.py +## Problematic call in line 6: + +C.method("2") + +## Type declared in line 3: + + def method(y: int) -> int: \ No newline at end of file diff --git a/python/test-data/testRecordSetTypes.out b/python/file-test-data/basics/staticmethod.out similarity index 100% rename from python/test-data/testRecordSetTypes.out rename to python/file-test-data/basics/staticmethod.out diff --git a/python/test-data/testRecordTypes.out b/python/file-test-data/basics/staticmethod.out_en similarity index 100% rename from python/test-data/testRecordTypes.out rename to python/file-test-data/basics/staticmethod.out_en diff --git a/python/file-test-data/basics/staticmethod.py b/python/file-test-data/basics/staticmethod.py new file mode 100644 index 00000000..c177637c --- /dev/null +++ b/python/file-test-data/basics/staticmethod.py @@ -0,0 +1,6 @@ +class C: + @staticmethod + def method(y: int) -> int: + return y + +C.method("2") diff --git a/python/test-data/testTodo.out b/python/file-test-data/basics/staticmethod_ok.err similarity index 100% rename from python/test-data/testTodo.out rename to python/file-test-data/basics/staticmethod_ok.err diff --git a/python/file-test-data/basics/staticmethod_ok.out b/python/file-test-data/basics/staticmethod_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/basics/staticmethod_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/basics/staticmethod_ok.py b/python/file-test-data/basics/staticmethod_ok.py new file mode 100644 index 00000000..90b56179 --- /dev/null +++ b/python/file-test-data/basics/staticmethod_ok.py @@ -0,0 +1,7 @@ +class C: + @staticmethod + def method(y: int) -> int: + return y + +C.method(2) +print('ok') diff --git a/python/file-test-data/basics/testCallable.err b/python/file-test-data/basics/testCallable.err new file mode 100644 index 00000000..5fe14904 --- /dev/null +++ b/python/file-test-data/basics/testCallable.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/testCallable.py", line 6, in + foo(42) + +WyppTypeError: 42 + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `Callable[[int, bool], str]` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/basics/testCallable.py +## Fehlerhafter Aufruf in Zeile 6: + +foo(42) + +## Typ deklariert in Zeile 3: + +def foo(f: Callable[[int, bool], str]) -> int: \ No newline at end of file diff --git a/python/test-data/testTraceback2.out b/python/file-test-data/basics/testCallable.out similarity index 100% rename from python/test-data/testTraceback2.out rename to python/file-test-data/basics/testCallable.out diff --git a/python/file-test-data/basics/testCallable.py b/python/file-test-data/basics/testCallable.py new file mode 100644 index 00000000..a5776112 --- /dev/null +++ b/python/file-test-data/basics/testCallable.py @@ -0,0 +1,6 @@ +from typing import Callable + +def foo(f: Callable[[int, bool], str]) -> int: + return 1 + +foo(42) diff --git a/python/file-test-data/basics/tooFewArgs.err b/python/file-test-data/basics/tooFewArgs.err new file mode 100644 index 00000000..6d7678ef --- /dev/null +++ b/python/file-test-data/basics/tooFewArgs.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/tooFewArgs.py", line 4, in + foo(1) + +WyppTypeError: Anzahl der Argument passt nicht + +Funktion `foo` benötigt 2 Argumente. +Gegeben: 1 Argument + +## Datei file-test-data/basics/tooFewArgs.py +## Aufruf in Zeile 4: + +foo(1) \ No newline at end of file diff --git a/python/test-data/testTraceback3.out b/python/file-test-data/basics/tooFewArgs.out similarity index 100% rename from python/test-data/testTraceback3.out rename to python/file-test-data/basics/tooFewArgs.out diff --git a/python/file-test-data/basics/tooFewArgs.py b/python/file-test-data/basics/tooFewArgs.py new file mode 100644 index 00000000..846d7580 --- /dev/null +++ b/python/file-test-data/basics/tooFewArgs.py @@ -0,0 +1,4 @@ +def foo(i: int, j: int): + pass + +foo(1) diff --git a/python/file-test-data/basics/tooFewAttrs.err b/python/file-test-data/basics/tooFewAttrs.err new file mode 100644 index 00000000..37b19e92 --- /dev/null +++ b/python/file-test-data/basics/tooFewAttrs.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/tooFewAttrs.py", line 8, in + c = C(1) + +WyppTypeError: Anzahl der Argument passt nicht + +Konstruktor des Records `C` benötigt 2 Argumente. +Gegeben: 1 Argument + +## Datei file-test-data/basics/tooFewAttrs.py +## Aufruf in Zeile 8: + +c = C(1) \ No newline at end of file diff --git a/python/test-data/testTypeKeyword.err b/python/file-test-data/basics/tooFewAttrs.out similarity index 100% rename from python/test-data/testTypeKeyword.err rename to python/file-test-data/basics/tooFewAttrs.out diff --git a/python/file-test-data/basics/tooFewAttrs.py b/python/file-test-data/basics/tooFewAttrs.py new file mode 100644 index 00000000..a62dcce3 --- /dev/null +++ b/python/file-test-data/basics/tooFewAttrs.py @@ -0,0 +1,8 @@ +from wypp import * + +@record +class C: + x: int + y: int + +c = C(1) diff --git a/python/file-test-data/basics/tooManyArgs.err b/python/file-test-data/basics/tooManyArgs.err new file mode 100644 index 00000000..c9a0ba95 --- /dev/null +++ b/python/file-test-data/basics/tooManyArgs.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/tooManyArgs.py", line 4, in + foo(1, 2, 3) + +WyppTypeError: Anzahl der Argument passt nicht + +Funktion `foo` benötigt 2 Argumente. +Gegeben: 3 Argumente + +## Datei file-test-data/basics/tooManyArgs.py +## Aufruf in Zeile 4: + +foo(1, 2, 3) \ No newline at end of file diff --git a/python/file-test-data/basics/tooManyArgs.err_en b/python/file-test-data/basics/tooManyArgs.err_en new file mode 100644 index 00000000..31e349f4 --- /dev/null +++ b/python/file-test-data/basics/tooManyArgs.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/tooManyArgs.py", line 4, in + foo(1, 2, 3) + +WyppTypeError: argument count mismatch + +Function `foo` takes 2 arguments. +Given: 3 arguments + +## File file-test-data/basics/tooManyArgs.py +## Call in line 4: + +foo(1, 2, 3) \ No newline at end of file diff --git a/python/test-data/testTypes1.out b/python/file-test-data/basics/tooManyArgs.out similarity index 100% rename from python/test-data/testTypes1.out rename to python/file-test-data/basics/tooManyArgs.out diff --git a/python/test-data/testTypes2.err-notypes b/python/file-test-data/basics/tooManyArgs.out_en similarity index 100% rename from python/test-data/testTypes2.err-notypes rename to python/file-test-data/basics/tooManyArgs.out_en diff --git a/python/file-test-data/basics/tooManyArgs.py b/python/file-test-data/basics/tooManyArgs.py new file mode 100644 index 00000000..f3c9e7a4 --- /dev/null +++ b/python/file-test-data/basics/tooManyArgs.py @@ -0,0 +1,4 @@ +def foo(i: int, j: int): + pass + +foo(1, 2, 3) diff --git a/python/file-test-data/basics/tooManyAttrs.err b/python/file-test-data/basics/tooManyAttrs.err new file mode 100644 index 00000000..8747fc40 --- /dev/null +++ b/python/file-test-data/basics/tooManyAttrs.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/tooManyAttrs.py", line 8, in + c = C(1, 2, 3) + +WyppTypeError: Anzahl der Argument passt nicht + +Konstruktor des Records `C` benötigt 2 Argumente. +Gegeben: 3 Argumente + +## Datei file-test-data/basics/tooManyAttrs.py +## Aufruf in Zeile 8: + +c = C(1, 2, 3) \ No newline at end of file diff --git a/python/file-test-data/basics/tooManyAttrs.err_en b/python/file-test-data/basics/tooManyAttrs.err_en new file mode 100644 index 00000000..af5fe59b --- /dev/null +++ b/python/file-test-data/basics/tooManyAttrs.err_en @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/basics/tooManyAttrs.py", line 8, in + c = C(1, 2, 3) + +WyppTypeError: argument count mismatch + +Constructor of record `C` takes 2 arguments. +Given: 3 arguments + +## File file-test-data/basics/tooManyAttrs.py +## Call in line 8: + +c = C(1, 2, 3) \ No newline at end of file diff --git a/python/test-data/testTypes2.out b/python/file-test-data/basics/tooManyAttrs.out similarity index 100% rename from python/test-data/testTypes2.out rename to python/file-test-data/basics/tooManyAttrs.out diff --git a/python/test-data/testTypesCollections1.out b/python/file-test-data/basics/tooManyAttrs.out_en similarity index 100% rename from python/test-data/testTypesCollections1.out rename to python/file-test-data/basics/tooManyAttrs.out_en diff --git a/python/file-test-data/basics/tooManyAttrs.py b/python/file-test-data/basics/tooManyAttrs.py new file mode 100644 index 00000000..d6230a7f --- /dev/null +++ b/python/file-test-data/basics/tooManyAttrs.py @@ -0,0 +1,8 @@ +from wypp import * + +@record +class C: + x: int + y: int + +c = C(1, 2, 3) diff --git a/python/file-test-data/basics/typeAlias.err b/python/file-test-data/basics/typeAlias.err new file mode 100644 index 00000000..c7216234 --- /dev/null +++ b/python/file-test-data/basics/typeAlias.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/typeAlias.py", line 7, in + foo('foo') + +WyppTypeError: 'foo' + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `T` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/basics/typeAlias.py +## Fehlerhafter Aufruf in Zeile 7: + +foo('foo') + +## Typ deklariert in Zeile 3: + +def foo(i: T) -> T: \ No newline at end of file diff --git a/python/file-test-data/basics/typeAlias.err_en b/python/file-test-data/basics/typeAlias.err_en new file mode 100644 index 00000000..62b6d3f5 --- /dev/null +++ b/python/file-test-data/basics/typeAlias.err_en @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/typeAlias.py", line 7, in + foo('foo') + +WyppTypeError: 'foo' + +The call of function `foo` expects value of type `T` as 1st argument. +But the value given has type `str`. + +## File file-test-data/basics/typeAlias.py +## Problematic call in line 7: + +foo('foo') + +## Type declared in line 3: + +def foo(i: T) -> T: \ No newline at end of file diff --git a/python/test-data/testTypesCollections3.out b/python/file-test-data/basics/typeAlias.out similarity index 100% rename from python/test-data/testTypesCollections3.out rename to python/file-test-data/basics/typeAlias.out diff --git a/python/test-data/testTypesCollections4.out b/python/file-test-data/basics/typeAlias.out_en similarity index 100% rename from python/test-data/testTypesCollections4.out rename to python/file-test-data/basics/typeAlias.out_en diff --git a/python/file-test-data/basics/typeAlias.py b/python/file-test-data/basics/typeAlias.py new file mode 100644 index 00000000..04a0bcd3 --- /dev/null +++ b/python/file-test-data/basics/typeAlias.py @@ -0,0 +1,7 @@ +type T = int + +def foo(i: T) -> T: + return i + +foo(1) +foo('foo') diff --git a/python/file-test-data/basics/typeAlias2.err b/python/file-test-data/basics/typeAlias2.err new file mode 100644 index 00000000..45d2db1d --- /dev/null +++ b/python/file-test-data/basics/typeAlias2.err @@ -0,0 +1,16 @@ +Traceback (most recent call last): + File "file-test-data/basics/typeAlias2.py", line 7, in + foo(['foo']) + +WyppTypeError: ['foo'] + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `list[T]` als erstes Argument. + +## Datei file-test-data/basics/typeAlias2.py +## Fehlerhafter Aufruf in Zeile 7: + +foo(['foo']) + +## Typ deklariert in Zeile 3: + +def foo(x: list[T]): \ No newline at end of file diff --git a/python/file-test-data/basics/typeAlias2.err_en b/python/file-test-data/basics/typeAlias2.err_en new file mode 100644 index 00000000..457dd101 --- /dev/null +++ b/python/file-test-data/basics/typeAlias2.err_en @@ -0,0 +1,16 @@ +Traceback (most recent call last): + File "file-test-data/basics/typeAlias2.py", line 7, in + foo(['foo']) + +WyppTypeError: ['foo'] + +The call of function `foo` expects value of type `list[T]` as 1st argument. + +## File file-test-data/basics/typeAlias2.py +## Problematic call in line 7: + +foo(['foo']) + +## Type declared in line 3: + +def foo(x: list[T]): \ No newline at end of file diff --git a/python/test-data/testTypesCollections5.out b/python/file-test-data/basics/typeAlias2.out similarity index 100% rename from python/test-data/testTypesCollections5.out rename to python/file-test-data/basics/typeAlias2.out diff --git a/python/test-data/testTypesDict1.out b/python/file-test-data/basics/typeAlias2.out_en similarity index 100% rename from python/test-data/testTypesDict1.out rename to python/file-test-data/basics/typeAlias2.out_en diff --git a/python/file-test-data/basics/typeAlias2.py b/python/file-test-data/basics/typeAlias2.py new file mode 100644 index 00000000..69881c0a --- /dev/null +++ b/python/file-test-data/basics/typeAlias2.py @@ -0,0 +1,7 @@ +type T = int + +def foo(x: list[T]): + pass + +foo([1,2]) +foo(['foo']) diff --git a/python/test-data/testTypesDict3.out b/python/file-test-data/extras/admin_ok.err similarity index 100% rename from python/test-data/testTypesDict3.out rename to python/file-test-data/extras/admin_ok.err diff --git a/python/test-data/admin.out b/python/file-test-data/extras/admin_ok.out similarity index 100% rename from python/test-data/admin.out rename to python/file-test-data/extras/admin_ok.out diff --git a/python/test-data/admin.py b/python/file-test-data/extras/admin_ok.py similarity index 100% rename from python/test-data/admin.py rename to python/file-test-data/extras/admin_ok.py diff --git a/python/file-test-data/extras/declared-at-missing.err b/python/file-test-data/extras/declared-at-missing.err new file mode 100644 index 00000000..4393d1f0 --- /dev/null +++ b/python/file-test-data/extras/declared-at-missing.err @@ -0,0 +1,16 @@ +Traceback (most recent call last): + File "file-test-data/extras/declared-at-missing.py", line 22, in + semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) + +WyppTypeError: (Course(name='Programmierung 1', teacher='Wehr', students=()),) + +Der Aufruf des Konstruktors des Records `Semester` erwartet Wert vom Typ `tuple[CourseM, ...]` als drittes Argument. + +## Datei file-test-data/extras/declared-at-missing.py +## Fehlerhafter Aufruf in Zeile 22: + +semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) + +## Typ deklariert in Zeile 19: + + courses: tuple[CourseM, ...] \ No newline at end of file diff --git a/python/test-data/testTypesDict4.out b/python/file-test-data/extras/declared-at-missing.out similarity index 100% rename from python/test-data/testTypesDict4.out rename to python/file-test-data/extras/declared-at-missing.out diff --git a/python/test-data/declared-at-missing.py b/python/file-test-data/extras/declared-at-missing.py similarity index 100% rename from python/test-data/declared-at-missing.py rename to python/file-test-data/extras/declared-at-missing.py diff --git a/python/file-test-data/extras/invalidRecord.err b/python/file-test-data/extras/invalidRecord.err new file mode 100644 index 00000000..4665becc --- /dev/null +++ b/python/file-test-data/extras/invalidRecord.err @@ -0,0 +1,7 @@ + +WyppTypeError: ungültige Record-Definition + +## Datei file-test-data/extras/invalidRecord.py +## Zeile 1: + +@record(mut=True) diff --git a/python/test-data/testTypesHigherOrderFuns2.err b/python/file-test-data/extras/invalidRecord.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns2.err rename to python/file-test-data/extras/invalidRecord.out diff --git a/python/file-test-data/extras/invalidRecord.py b/python/file-test-data/extras/invalidRecord.py new file mode 100644 index 00000000..9b14e61b --- /dev/null +++ b/python/file-test-data/extras/invalidRecord.py @@ -0,0 +1,4 @@ +@record(mut=True) +class C: + pass + diff --git a/python/file-test-data/extras/invalidType.err b/python/file-test-data/extras/invalidType.err new file mode 100644 index 00000000..739a7f86 --- /dev/null +++ b/python/file-test-data/extras/invalidType.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/invalidType.py", line 9, in + foo() + +WyppTypeError: ungültiger Typ `Union(list(int), list[float])` + +Wolltest du `Union[list(int), list[float]]` schreiben? + +## Datei file-test-data/extras/invalidType.py +## Typ deklariert in Zeile 6: + +def foo() -> Union(list(int), list[float]): \ No newline at end of file diff --git a/python/test-data/testTypesHigherOrderFuns3.out b/python/file-test-data/extras/invalidType.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns3.out rename to python/file-test-data/extras/invalidType.out diff --git a/python/file-test-data/extras/invalidType.py b/python/file-test-data/extras/invalidType.py new file mode 100644 index 00000000..77595170 --- /dev/null +++ b/python/file-test-data/extras/invalidType.py @@ -0,0 +1,9 @@ +from __future__ import annotations +from wypp import * +# See https://github.com/skogsbaer/write-your-python-program/issues/61 + +# Tests 'return' +def foo() -> Union(list(int), list[float]): + pass + +foo() diff --git a/python/file-test-data/extras/invalidType2.err b/python/file-test-data/extras/invalidType2.err new file mode 100644 index 00000000..abf47854 --- /dev/null +++ b/python/file-test-data/extras/invalidType2.err @@ -0,0 +1,4 @@ +Traceback (most recent call last): + File "file-test-data/extras/invalidType2.py", line 5, in + T = Union(list(int), list[float]) +TypeError: 'type' object is not iterable \ No newline at end of file diff --git a/python/test-data/testTypesHigherOrderFuns4.err b/python/file-test-data/extras/invalidType2.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns4.err rename to python/file-test-data/extras/invalidType2.out diff --git a/python/file-test-data/extras/invalidType2.py b/python/file-test-data/extras/invalidType2.py new file mode 100644 index 00000000..1757ecb7 --- /dev/null +++ b/python/file-test-data/extras/invalidType2.py @@ -0,0 +1,11 @@ +from __future__ import annotations +from wypp import * +# See https://github.com/skogsbaer/write-your-python-program/issues/61 + +T = Union(list(int), list[float]) + +# Tests 'return' +def foo() -> T: + pass + +foo() diff --git a/python/file-test-data/extras/invalidType3.err b/python/file-test-data/extras/invalidType3.err new file mode 100644 index 00000000..8a772f48 --- /dev/null +++ b/python/file-test-data/extras/invalidType3.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/invalidType3.py", line 9, in + foo() + +WyppTypeError: ungültiger Typ `Optional(list(int), list[float])` + +Wolltest du `Optional[list(int), list[float]]` schreiben? + +## Datei file-test-data/extras/invalidType3.py +## Typ deklariert in Zeile 6: + +def foo() -> Optional(list(int), list[float]): \ No newline at end of file diff --git a/python/test-data/testTypesHigherOrderFuns5.err b/python/file-test-data/extras/invalidType3.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns5.err rename to python/file-test-data/extras/invalidType3.out diff --git a/python/file-test-data/extras/invalidType3.py b/python/file-test-data/extras/invalidType3.py new file mode 100644 index 00000000..cf8418ba --- /dev/null +++ b/python/file-test-data/extras/invalidType3.py @@ -0,0 +1,9 @@ +from __future__ import annotations +from wypp import * +# See https://github.com/skogsbaer/write-your-python-program/issues/61 + +# Tests 'return' +def foo() -> Optional(list(int), list[float]): + pass + +foo() diff --git a/python/file-test-data/extras/invalidType4.err b/python/file-test-data/extras/invalidType4.err new file mode 100644 index 00000000..b261d290 --- /dev/null +++ b/python/file-test-data/extras/invalidType4.err @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/extras/invalidType4.py", line 9, in + foo() + +WyppTypeError: ungültiger Typ `Optional[list[int], list[float]]` + +## Datei file-test-data/extras/invalidType4.py +## Typ deklariert in Zeile 6: + +def foo() -> Optional[list[int], list[float]]: \ No newline at end of file diff --git a/python/test-data/testTypesProtos1.out b/python/file-test-data/extras/invalidType4.out similarity index 100% rename from python/test-data/testTypesProtos1.out rename to python/file-test-data/extras/invalidType4.out diff --git a/python/file-test-data/extras/invalidType4.py b/python/file-test-data/extras/invalidType4.py new file mode 100644 index 00000000..36f511df --- /dev/null +++ b/python/file-test-data/extras/invalidType4.py @@ -0,0 +1,9 @@ +from __future__ import annotations +from wypp import * +# See https://github.com/skogsbaer/write-your-python-program/issues/61 + +# Tests 'return' +def foo() -> Optional[list[int], list[float]]: + pass + +foo() diff --git a/python/file-test-data/extras/main.err b/python/file-test-data/extras/main.err new file mode 100644 index 00000000..4e925967 --- /dev/null +++ b/python/file-test-data/extras/main.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/main.py", line 4, in + print(mod.foo(1)) + File "file-test-data/modules/B/mod.py", line 5, in foo + return bar(i) + +WyppTypeError: 1 + +Der Aufruf der Funktion `bar` erwartet Wert vom Typ `str` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/modules/B/mod.py +## Fehlerhafter Aufruf in Zeile 5: + + return bar(i) + +## Typ deklariert in Zeile 1: + +def bar(s: str) -> int: \ No newline at end of file diff --git a/python/test-data/testTypesProtos2.out b/python/file-test-data/extras/main.out similarity index 100% rename from python/test-data/testTypesProtos2.out rename to python/file-test-data/extras/main.out diff --git a/python/file-test-data/extras/main.py b/python/file-test-data/extras/main.py new file mode 100644 index 00000000..175213cc --- /dev/null +++ b/python/file-test-data/extras/main.py @@ -0,0 +1,4 @@ +# WYPP_TEST_CONFIG: {"args": ["--extra-dir", "file-test-data/modules/B"], "pythonPath": "file-test-data/modules/B"} +import mod + +print(mod.foo(1)) diff --git a/python/test-data/testTypesProtos3.out b/python/file-test-data/extras/printModuleNameImport_ok.err similarity index 100% rename from python/test-data/testTypesProtos3.out rename to python/file-test-data/extras/printModuleNameImport_ok.err diff --git a/python/test-data/printModuleNameImport.out b/python/file-test-data/extras/printModuleNameImport_ok.out similarity index 55% rename from python/test-data/printModuleNameImport.out rename to python/file-test-data/extras/printModuleNameImport_ok.out index c23481b5..fbf92154 100644 --- a/python/test-data/printModuleNameImport.out +++ b/python/file-test-data/extras/printModuleNameImport_ok.out @@ -1,4 +1,4 @@ -printModuleName +printModuleName_ok True __wypp__ True diff --git a/python/file-test-data/extras/printModuleNameImport_ok.py b/python/file-test-data/extras/printModuleNameImport_ok.py new file mode 100644 index 00000000..aeff565d --- /dev/null +++ b/python/file-test-data/extras/printModuleNameImport_ok.py @@ -0,0 +1,12 @@ +# WYPP_TEST_CONFIG: {"typecheck": "both"} +import wypp +import printModuleName_ok +print(__name__) + +class C: + pass + +import sys +print(sys.modules.get(C.__module__) is not None) +print(sys.modules.get(printModuleName_ok.C.__module__) is not None) + diff --git a/python/test-data/testTypesProtos5.err b/python/file-test-data/extras/printModuleName_ok.err similarity index 100% rename from python/test-data/testTypesProtos5.err rename to python/file-test-data/extras/printModuleName_ok.err diff --git a/python/test-data/printModuleName.out b/python/file-test-data/extras/printModuleName_ok.out similarity index 100% rename from python/test-data/printModuleName.out rename to python/file-test-data/extras/printModuleName_ok.out diff --git a/python/test-data/printModuleName.py b/python/file-test-data/extras/printModuleName_ok.py similarity index 83% rename from python/test-data/printModuleName.py rename to python/file-test-data/extras/printModuleName_ok.py index 82c7cf94..cb049d40 100644 --- a/python/test-data/printModuleName.py +++ b/python/file-test-data/extras/printModuleName_ok.py @@ -1,5 +1,6 @@ # Should print 'wypp' when loaded via the RUN button. # When imported, it should print 'printModuleName' +# WYPP_TEST_CONFIG: {"typecheck": "both"} import wypp print(__name__) diff --git a/python/test-data/testABCMeta.err b/python/file-test-data/extras/testABCMeta.err similarity index 54% rename from python/test-data/testABCMeta.err rename to python/file-test-data/extras/testABCMeta.err index 2bba158d..4ffca11e 100644 --- a/python/test-data/testABCMeta.err +++ b/python/file-test-data/extras/testABCMeta.err @@ -1,4 +1,4 @@ Traceback (most recent call last): - File "test-data/testABCMeta.py", line 28, in + File "file-test-data/extras/testABCMeta.py", line 28, in Circle(Point(0, 0), 1) -TypeError: Can't instantiate abstract class Circle without an implementation for abstract method 'area' +TypeError: Can't instantiate abstract class Circle without an implementation for abstract method 'area' \ No newline at end of file diff --git a/python/test-data/testTypesProtos6.out b/python/file-test-data/extras/testABCMeta.out similarity index 100% rename from python/test-data/testTypesProtos6.out rename to python/file-test-data/extras/testABCMeta.out diff --git a/python/test-data/testABCMeta.py b/python/file-test-data/extras/testABCMeta.py similarity index 100% rename from python/test-data/testABCMeta.py rename to python/file-test-data/extras/testABCMeta.py diff --git a/python/test-data/testTypesProtos7.out b/python/file-test-data/extras/testABC_ok.err similarity index 100% rename from python/test-data/testTypesProtos7.out rename to python/file-test-data/extras/testABC_ok.err diff --git a/python/test-data/testABC.out b/python/file-test-data/extras/testABC_ok.out similarity index 100% rename from python/test-data/testABC.out rename to python/file-test-data/extras/testABC_ok.out diff --git a/python/test-data/testABC.py b/python/file-test-data/extras/testABC_ok.py similarity index 100% rename from python/test-data/testABC.py rename to python/file-test-data/extras/testABC_ok.py diff --git a/python/test-data/testTypesProtos8.out b/python/file-test-data/extras/testArgs_ok.err similarity index 100% rename from python/test-data/testTypesProtos8.out rename to python/file-test-data/extras/testArgs_ok.err diff --git a/python/file-test-data/extras/testArgs_ok.out b/python/file-test-data/extras/testArgs_ok.out new file mode 100644 index 00000000..d6d31d2c --- /dev/null +++ b/python/file-test-data/extras/testArgs_ok.out @@ -0,0 +1 @@ +['file-test-data/extras/testArgs_ok.py', 'ARG_1', 'ARG_2'] diff --git a/python/file-test-data/extras/testArgs_ok.py b/python/file-test-data/extras/testArgs_ok.py new file mode 100644 index 00000000..1b88936f --- /dev/null +++ b/python/file-test-data/extras/testArgs_ok.py @@ -0,0 +1,4 @@ +# WYPP_TEST_CONFIG: {"args": ["ARG_1", "ARG_2"]} +import wypp +import sys +print(sys.argv) diff --git a/python/test-data/testTypesProtos9.out b/python/file-test-data/extras/testBugSliceIndices_ok.err similarity index 100% rename from python/test-data/testTypesProtos9.out rename to python/file-test-data/extras/testBugSliceIndices_ok.err diff --git a/python/test-data/testBugSliceIndices.out b/python/file-test-data/extras/testBugSliceIndices_ok.out similarity index 100% rename from python/test-data/testBugSliceIndices.out rename to python/file-test-data/extras/testBugSliceIndices_ok.out diff --git a/python/test-data/testBugSliceIndices.py b/python/file-test-data/extras/testBugSliceIndices_ok.py similarity index 100% rename from python/test-data/testBugSliceIndices.py rename to python/file-test-data/extras/testBugSliceIndices_ok.py diff --git a/python/test-data/testTypesSet1.out b/python/file-test-data/extras/testCheckFail_ok.err similarity index 100% rename from python/test-data/testTypesSet1.out rename to python/file-test-data/extras/testCheckFail_ok.err diff --git a/python/test-data/testCheckFail.out b/python/file-test-data/extras/testCheckFail_ok.out similarity index 100% rename from python/test-data/testCheckFail.out rename to python/file-test-data/extras/testCheckFail_ok.out diff --git a/python/test-data/testCheckFail.py b/python/file-test-data/extras/testCheckFail_ok.py similarity index 57% rename from python/test-data/testCheckFail.py rename to python/file-test-data/extras/testCheckFail_ok.py index 1c9981ca..1ea3c05c 100644 --- a/python/test-data/testCheckFail.py +++ b/python/file-test-data/extras/testCheckFail_ok.py @@ -1,3 +1,4 @@ +# WYPP_TEST_CONFIG: {"typecheck": "both"} from wypp import * checkFail('something bad happened') diff --git a/python/test-data/testTypesSet3.out b/python/file-test-data/extras/testCheck_ok.err similarity index 100% rename from python/test-data/testTypesSet3.out rename to python/file-test-data/extras/testCheck_ok.err diff --git a/python/file-test-data/extras/testCheck_ok.out b/python/file-test-data/extras/testCheck_ok.out new file mode 100644 index 00000000..b4ed27cd --- /dev/null +++ b/python/file-test-data/extras/testCheck_ok.out @@ -0,0 +1,5 @@ +FEHLER in Datei file-test-data/extras/testCheck_ok.py, Zeile 13: Erwartet wird 2, aber das Ergebnis ist 1 +FEHLER in Datei file-test-data/extras/testCheck_ok.py, Zeile 14: Erwartet wird 'xyz', aber das Ergebnis ist 'abc' +FEHLER in Datei file-test-data/extras/testCheck_ok.py, Zeile 15: Erwartet wird Point(x=1, y=3), aber das Ergebnis ist Point(x=1, y=2) +FEHLER in Datei file-test-data/extras/testCheck_ok.py, Zeile 16: Erwartet wird Point(x=1, y=3), aber das Ergebnis ist Name(firstName='Max', lastName='Müller') +4 Tests, 4 Fehler 🙁 diff --git a/python/test-data/testCheck.py b/python/file-test-data/extras/testCheck_ok.py similarity index 100% rename from python/test-data/testCheck.py rename to python/file-test-data/extras/testCheck_ok.py diff --git a/python/test-data/testTypesSubclassing1.out b/python/file-test-data/extras/testClassHierarchy_ok.err similarity index 100% rename from python/test-data/testTypesSubclassing1.out rename to python/file-test-data/extras/testClassHierarchy_ok.err diff --git a/python/test-data/testClassHierarchy.out b/python/file-test-data/extras/testClassHierarchy_ok.out similarity index 100% rename from python/test-data/testClassHierarchy.out rename to python/file-test-data/extras/testClassHierarchy_ok.out diff --git a/python/test-data/testClassHierarchy.py b/python/file-test-data/extras/testClassHierarchy_ok.py similarity index 100% rename from python/test-data/testClassHierarchy.py rename to python/file-test-data/extras/testClassHierarchy_ok.py diff --git a/python/test-data/testTypesWrapperEq.err b/python/file-test-data/extras/testClassRecursion_ok.err similarity index 100% rename from python/test-data/testTypesWrapperEq.err rename to python/file-test-data/extras/testClassRecursion_ok.err diff --git a/python/test-data/testClassRecursion.out b/python/file-test-data/extras/testClassRecursion_ok.out similarity index 100% rename from python/test-data/testClassRecursion.out rename to python/file-test-data/extras/testClassRecursion_ok.out diff --git a/python/test-data/testClassRecursion.py b/python/file-test-data/extras/testClassRecursion_ok.py similarity index 100% rename from python/test-data/testClassRecursion.py rename to python/file-test-data/extras/testClassRecursion_ok.py diff --git a/python/test-data/testUnion.err b/python/file-test-data/extras/testComplex_ok.err similarity index 100% rename from python/test-data/testUnion.err rename to python/file-test-data/extras/testComplex_ok.err diff --git a/python/test-data/testUnion2.err b/python/file-test-data/extras/testComplex_ok.out similarity index 100% rename from python/test-data/testUnion2.err rename to python/file-test-data/extras/testComplex_ok.out diff --git a/python/test-data/testComplex.py b/python/file-test-data/extras/testComplex_ok.py similarity index 100% rename from python/test-data/testComplex.py rename to python/file-test-data/extras/testComplex_ok.py diff --git a/python/test-data/testUnion3.err b/python/file-test-data/extras/testConcat_ok.err similarity index 100% rename from python/test-data/testUnion3.err rename to python/file-test-data/extras/testConcat_ok.err diff --git a/python/test-data/testConcat.out b/python/file-test-data/extras/testConcat_ok.out similarity index 100% rename from python/test-data/testConcat.out rename to python/file-test-data/extras/testConcat_ok.out diff --git a/python/test-data/testConcat.py b/python/file-test-data/extras/testConcat_ok.py similarity index 100% rename from python/test-data/testConcat.py rename to python/file-test-data/extras/testConcat_ok.py diff --git a/python/test-data/testUnionLiteral.err b/python/file-test-data/extras/testCopy_ok.err similarity index 100% rename from python/test-data/testUnionLiteral.err rename to python/file-test-data/extras/testCopy_ok.err diff --git a/python/test-data/testCopy.out b/python/file-test-data/extras/testCopy_ok.out similarity index 100% rename from python/test-data/testCopy.out rename to python/file-test-data/extras/testCopy_ok.out diff --git a/python/test-data/testCopy.py b/python/file-test-data/extras/testCopy_ok.py similarity index 100% rename from python/test-data/testCopy.py rename to python/file-test-data/extras/testCopy_ok.py diff --git a/python/test-data/testUnionLiteral.out b/python/file-test-data/extras/testDeepEqBug_ok.err similarity index 100% rename from python/test-data/testUnionLiteral.out rename to python/file-test-data/extras/testDeepEqBug_ok.err diff --git a/python/file-test-data/extras/testDeepEqBug_ok.out b/python/file-test-data/extras/testDeepEqBug_ok.out new file mode 100644 index 00000000..bdcb1c6b --- /dev/null +++ b/python/file-test-data/extras/testDeepEqBug_ok.out @@ -0,0 +1,2 @@ +FEHLER in Datei file-test-data/extras/testDeepEqBug_ok.py, Zeile 61: Erwartet wird SemesterM(degreeProgram='AKI', semester='1. Semester 2021/22', courses=[CourseM(name='Programmierung 1', teacher='Wehr', students=[]), CourseM(name='Grundlagen der Informatik', teacher='Oelke', students=[])]), aber das Ergebnis ist SemesterM(degreeProgram='AKI', semester='1. Semester 2021/22', courses=[CourseM(name='Programmierung 1', teacher='Wehr', students=['1234', '9876']), CourseM(name='Grundlagen der Informatik', teacher='Oelke', students=['1234'])]) +1 Test, 1 Fehler 🙁 diff --git a/python/test-data/testDeepEqBug.py b/python/file-test-data/extras/testDeepEqBug_ok.py similarity index 100% rename from python/test-data/testDeepEqBug.py rename to python/file-test-data/extras/testDeepEqBug_ok.py diff --git a/python/test-data/testUnionOfUnion.err b/python/file-test-data/extras/testDict_ok.err similarity index 100% rename from python/test-data/testUnionOfUnion.err rename to python/file-test-data/extras/testDict_ok.err diff --git a/python/test-data/testDict.out b/python/file-test-data/extras/testDict_ok.out similarity index 100% rename from python/test-data/testDict.out rename to python/file-test-data/extras/testDict_ok.out diff --git a/python/test-data/testDict.py b/python/file-test-data/extras/testDict_ok.py similarity index 100% rename from python/test-data/testDict.py rename to python/file-test-data/extras/testDict_ok.py diff --git a/python/test-data/testUnsortableDicts.err b/python/file-test-data/extras/testDoubleWrappingDicts_ok.err similarity index 100% rename from python/test-data/testUnsortableDicts.err rename to python/file-test-data/extras/testDoubleWrappingDicts_ok.err diff --git a/python/test-data/testDoubleWrappingDicts.out b/python/file-test-data/extras/testDoubleWrappingDicts_ok.out similarity index 100% rename from python/test-data/testDoubleWrappingDicts.out rename to python/file-test-data/extras/testDoubleWrappingDicts_ok.out diff --git a/python/test-data/testDoubleWrappingDicts.py b/python/file-test-data/extras/testDoubleWrappingDicts_ok.py similarity index 100% rename from python/test-data/testDoubleWrappingDicts.py rename to python/file-test-data/extras/testDoubleWrappingDicts_ok.py diff --git a/python/test-data/testWrap.err b/python/file-test-data/extras/testForwardRef1_ok.err similarity index 100% rename from python/test-data/testWrap.err rename to python/file-test-data/extras/testForwardRef1_ok.err diff --git a/python/test-data/testForwardRef1.out b/python/file-test-data/extras/testForwardRef1_ok.out similarity index 100% rename from python/test-data/testForwardRef1.out rename to python/file-test-data/extras/testForwardRef1_ok.out diff --git a/python/test-data/testForwardRef1.py b/python/file-test-data/extras/testForwardRef1_ok.py similarity index 100% rename from python/test-data/testForwardRef1.py rename to python/file-test-data/extras/testForwardRef1_ok.py diff --git a/python/file-test-data/extras/testForwardRef2.err b/python/file-test-data/extras/testForwardRef2.err new file mode 100644 index 00000000..87cf3893 --- /dev/null +++ b/python/file-test-data/extras/testForwardRef2.err @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/extras/testForwardRef2.py", line 10, in + t = Test(FooX()) + +WyppTypeError: ungültiger Typ `Foo` + +## Datei file-test-data/extras/testForwardRef2.py +## Typ deklariert in Zeile 2: + + def __init__(self, foo: 'Foo'): \ No newline at end of file diff --git a/python/test-data/testWrap2.err b/python/file-test-data/extras/testForwardRef2.out similarity index 100% rename from python/test-data/testWrap2.err rename to python/file-test-data/extras/testForwardRef2.out diff --git a/python/test-data/testForwardRef2.py b/python/file-test-data/extras/testForwardRef2.py similarity index 100% rename from python/test-data/testForwardRef2.py rename to python/file-test-data/extras/testForwardRef2.py diff --git a/python/test-data/testWrapperError.out b/python/file-test-data/extras/testForwardRef3_ok.err similarity index 100% rename from python/test-data/testWrapperError.out rename to python/file-test-data/extras/testForwardRef3_ok.err diff --git a/python/test-data/testForwardRef3.out b/python/file-test-data/extras/testForwardRef3_ok.out similarity index 100% rename from python/test-data/testForwardRef3.out rename to python/file-test-data/extras/testForwardRef3_ok.out diff --git a/python/test-data/testForwardRef3.py b/python/file-test-data/extras/testForwardRef3_ok.py similarity index 100% rename from python/test-data/testForwardRef3.py rename to python/file-test-data/extras/testForwardRef3_ok.py diff --git a/python/file-test-data/extras/testForwardRef4.err b/python/file-test-data/extras/testForwardRef4.err new file mode 100644 index 00000000..b644f3f6 --- /dev/null +++ b/python/file-test-data/extras/testForwardRef4.err @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/extras/testForwardRef4.py", line 11, in + t = Test(Foo()) + +WyppTypeError: ungültiger Typ `FooX` + +## Datei file-test-data/extras/testForwardRef4.py +## Typ deklariert in Zeile 5: + + foo: 'FooX' diff --git a/python/test-data/testWrongKeywordArg.out b/python/file-test-data/extras/testForwardRef4.out similarity index 100% rename from python/test-data/testWrongKeywordArg.out rename to python/file-test-data/extras/testForwardRef4.out diff --git a/python/test-data/testForwardRef4.py b/python/file-test-data/extras/testForwardRef4.py similarity index 100% rename from python/test-data/testForwardRef4.py rename to python/file-test-data/extras/testForwardRef4.py diff --git a/python/file-test-data/extras/testForwardRef5.err b/python/file-test-data/extras/testForwardRef5.err new file mode 100644 index 00000000..c13aa2dd --- /dev/null +++ b/python/file-test-data/extras/testForwardRef5.err @@ -0,0 +1,16 @@ +Traceback (most recent call last): + File "file-test-data/extras/testForwardRef5.py", line 22, in + garage = Garage(cars=[Car(color='red'), "Not A Car"]) + +WyppTypeError: [Car(color='red'), 'Not A Car'] + +Der Aufruf des Konstruktors des Records `Garage` erwartet Wert vom Typ `list[Car]` als Argument `cars`. + +## Datei file-test-data/extras/testForwardRef5.py +## Fehlerhafter Aufruf in Zeile 22: + +garage = Garage(cars=[Car(color='red'), "Not A Car"]) + +## Typ deklariert in Zeile 11: + + cars: list['Car'] \ No newline at end of file diff --git a/python/test-data/testWrongKeywordArg2.out b/python/file-test-data/extras/testForwardRef5.out similarity index 100% rename from python/test-data/testWrongKeywordArg2.out rename to python/file-test-data/extras/testForwardRef5.out diff --git a/python/test-data/testForwardRef5.py b/python/file-test-data/extras/testForwardRef5.py similarity index 100% rename from python/test-data/testForwardRef5.py rename to python/file-test-data/extras/testForwardRef5.py diff --git a/python/test-data/testWrongNumOfArguments.out b/python/file-test-data/extras/testForwardRef6_ok.err similarity index 100% rename from python/test-data/testWrongNumOfArguments.out rename to python/file-test-data/extras/testForwardRef6_ok.err diff --git a/python/test-data/testForwardRef6.out b/python/file-test-data/extras/testForwardRef6_ok.out similarity index 100% rename from python/test-data/testForwardRef6.out rename to python/file-test-data/extras/testForwardRef6_ok.out diff --git a/python/test-data/testForwardRef6.py b/python/file-test-data/extras/testForwardRef6_ok.py similarity index 100% rename from python/test-data/testForwardRef6.py rename to python/file-test-data/extras/testForwardRef6_ok.py diff --git a/python/test-data/testWrongNumOfArguments2.out b/python/file-test-data/extras/testForwardRef_ok.err similarity index 100% rename from python/test-data/testWrongNumOfArguments2.out rename to python/file-test-data/extras/testForwardRef_ok.err diff --git a/python/test-data/wrong-caused-by.out b/python/file-test-data/extras/testForwardRef_ok.out similarity index 100% rename from python/test-data/wrong-caused-by.out rename to python/file-test-data/extras/testForwardRef_ok.out diff --git a/python/test-data/testForwardRef.py b/python/file-test-data/extras/testForwardRef_ok.py similarity index 100% rename from python/test-data/testForwardRef.py rename to python/file-test-data/extras/testForwardRef_ok.py diff --git a/python/file-test-data/extras/testForwardTypeInRecord2_ok.err b/python/file-test-data/extras/testForwardTypeInRecord2_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testForwardTypeInRecord.out b/python/file-test-data/extras/testForwardTypeInRecord2_ok.out similarity index 100% rename from python/test-data/testForwardTypeInRecord.out rename to python/file-test-data/extras/testForwardTypeInRecord2_ok.out diff --git a/python/test-data/testForwardTypeInRecord2.py b/python/file-test-data/extras/testForwardTypeInRecord2_ok.py similarity index 100% rename from python/test-data/testForwardTypeInRecord2.py rename to python/file-test-data/extras/testForwardTypeInRecord2_ok.py diff --git a/python/file-test-data/extras/testForwardTypeInRecord_ok.err b/python/file-test-data/extras/testForwardTypeInRecord_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testForwardTypeInRecord2.out b/python/file-test-data/extras/testForwardTypeInRecord_ok.out similarity index 100% rename from python/test-data/testForwardTypeInRecord2.out rename to python/file-test-data/extras/testForwardTypeInRecord_ok.out diff --git a/python/test-data/testForwardTypeInRecord.py b/python/file-test-data/extras/testForwardTypeInRecord_ok.py similarity index 100% rename from python/test-data/testForwardTypeInRecord.py rename to python/file-test-data/extras/testForwardTypeInRecord_ok.py diff --git a/python/file-test-data/extras/testFunEq_ok.err b/python/file-test-data/extras/testFunEq_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testFunEq_ok.out b/python/file-test-data/extras/testFunEq_ok.out new file mode 100644 index 00000000..7ec80606 --- /dev/null +++ b/python/file-test-data/extras/testFunEq_ok.out @@ -0,0 +1,2 @@ +FEHLER in Datei file-test-data/extras/testFunEq_ok.py, Zeile 9: Erwartet wird , aber das Ergebnis ist +1 Test, 1 Fehler 🙁 diff --git a/python/test-data/testFunEq.py b/python/file-test-data/extras/testFunEq_ok.py similarity index 100% rename from python/test-data/testFunEq.py rename to python/file-test-data/extras/testFunEq_ok.py diff --git a/python/file-test-data/extras/testGetSource.err b/python/file-test-data/extras/testGetSource.err new file mode 100644 index 00000000..4a8b3cb7 --- /dev/null +++ b/python/file-test-data/extras/testGetSource.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testGetSource.py", line 11, in + Art = Literal('klein','mittag') # <= problem is here + +WyppTypeError: ungültiger Typ `Literal('klein', 'mittag')` + +Wolltest du `Literal['klein', 'mittag']` schreiben? + +## Datei file-test-data/extras/testGetSource.py +## Typ deklariert in Zeile 11: + +Art = Literal('klein','mittag') # <= problem is here diff --git a/python/file-test-data/extras/testGetSource.out b/python/file-test-data/extras/testGetSource.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testGetSource.py b/python/file-test-data/extras/testGetSource.py similarity index 100% rename from python/test-data/testGetSource.py rename to python/file-test-data/extras/testGetSource.py diff --git a/python/file-test-data/extras/testHintParentheses1.err b/python/file-test-data/extras/testHintParentheses1.err new file mode 100644 index 00000000..7cf31405 --- /dev/null +++ b/python/file-test-data/extras/testHintParentheses1.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testHintParentheses1.py", line 8, in + check(foo([1,2,3]), 3) + +WyppTypeError: ungültiger Typ `list(int)` + +Wolltest du `list[int]` schreiben? + +## Datei file-test-data/extras/testHintParentheses1.py +## Typ deklariert in Zeile 5: + +def foo(l: list(int)) -> int: \ No newline at end of file diff --git a/python/file-test-data/extras/testHintParentheses1.out b/python/file-test-data/extras/testHintParentheses1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testHintParentheses1.py b/python/file-test-data/extras/testHintParentheses1.py similarity index 100% rename from python/test-data/testHintParentheses1.py rename to python/file-test-data/extras/testHintParentheses1.py diff --git a/python/file-test-data/extras/testHintParentheses2.err b/python/file-test-data/extras/testHintParentheses2.err new file mode 100644 index 00000000..7f253075 --- /dev/null +++ b/python/file-test-data/extras/testHintParentheses2.err @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/extras/testHintParentheses2.py", line 8, in + foo(1, {}) + +WyppTypeError: ungültiger Typ `dict[1, list(int)]` + +## Datei file-test-data/extras/testHintParentheses2.py +## Typ deklariert in Zeile 5: + +def foo(a: 'int', b: 'dict[1, list(int)]') -> int: \ No newline at end of file diff --git a/python/file-test-data/extras/testHintParentheses2.out b/python/file-test-data/extras/testHintParentheses2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testHintParentheses2.py b/python/file-test-data/extras/testHintParentheses2.py similarity index 85% rename from python/test-data/testHintParentheses2.py rename to python/file-test-data/extras/testHintParentheses2.py index 6f05ce0d..98456c55 100644 --- a/python/test-data/testHintParentheses2.py +++ b/python/file-test-data/extras/testHintParentheses2.py @@ -3,4 +3,6 @@ # Uses types wrapped in strings def foo(a: 'int', b: 'dict[1, list(int)]') -> int: - return len(l) \ No newline at end of file + return len(b) + +foo(1, {}) diff --git a/python/file-test-data/extras/testHintParentheses3.err b/python/file-test-data/extras/testHintParentheses3.err new file mode 100644 index 00000000..e5d29b1b --- /dev/null +++ b/python/file-test-data/extras/testHintParentheses3.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testHintParentheses3.py", line 9, in + foo() + +WyppTypeError: ungültiger Typ `Union(list, str)` + +Wolltest du `Union[list, str]` schreiben? + +## Datei file-test-data/extras/testHintParentheses3.py +## Typ deklariert in Zeile 6: + +def foo() -> Union(list, str): \ No newline at end of file diff --git a/python/file-test-data/extras/testHintParentheses3.out b/python/file-test-data/extras/testHintParentheses3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testHintParentheses3.py b/python/file-test-data/extras/testHintParentheses3.py similarity index 96% rename from python/test-data/testHintParentheses3.py rename to python/file-test-data/extras/testHintParentheses3.py index 225f0b11..d93e038e 100644 --- a/python/test-data/testHintParentheses3.py +++ b/python/file-test-data/extras/testHintParentheses3.py @@ -5,3 +5,5 @@ # Tests 'return' def foo() -> Union(list, str): pass + +foo() diff --git a/python/file-test-data/extras/testHof_ok.err b/python/file-test-data/extras/testHof_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testHof.out b/python/file-test-data/extras/testHof_ok.out similarity index 100% rename from python/test-data/testHof.out rename to python/file-test-data/extras/testHof_ok.out diff --git a/python/test-data/testHof.py b/python/file-test-data/extras/testHof_ok.py similarity index 100% rename from python/test-data/testHof.py rename to python/file-test-data/extras/testHof_ok.py diff --git a/python/file-test-data/extras/testImpossible.err b/python/file-test-data/extras/testImpossible.err new file mode 100644 index 00000000..19e40e8d --- /dev/null +++ b/python/file-test-data/extras/testImpossible.err @@ -0,0 +1,7 @@ +Traceback (most recent call last): + File "file-test-data/extras/testImpossible.py", line 3, in + impossible() + File "code/wypp/writeYourProgram.py", line 332, in impossible + raise errors.ImpossibleError(msg) + +Das Unmögliche ist passiert! diff --git a/python/file-test-data/extras/testImpossible.out b/python/file-test-data/extras/testImpossible.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testImpossible.py b/python/file-test-data/extras/testImpossible.py similarity index 100% rename from python/test-data/testImpossible.py rename to python/file-test-data/extras/testImpossible.py diff --git a/python/file-test-data/extras/testIndexError.err b/python/file-test-data/extras/testIndexError.err new file mode 100644 index 00000000..81e0bc3a --- /dev/null +++ b/python/file-test-data/extras/testIndexError.err @@ -0,0 +1,6 @@ +Traceback (most recent call last): + File "file-test-data/extras/testIndexError.py", line 6, in + foo([1,2,3]) + File "file-test-data/extras/testIndexError.py", line 3, in foo + x = l[42] +IndexError: list index out of range \ No newline at end of file diff --git a/python/file-test-data/extras/testIndexError.out b/python/file-test-data/extras/testIndexError.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIndexError.py b/python/file-test-data/extras/testIndexError.py similarity index 100% rename from python/test-data/testIndexError.py rename to python/file-test-data/extras/testIndexError.py diff --git a/python/file-test-data/extras/testIndexSeq_ok.err b/python/file-test-data/extras/testIndexSeq_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIndexSeq.out b/python/file-test-data/extras/testIndexSeq_ok.out similarity index 100% rename from python/test-data/testIndexSeq.out rename to python/file-test-data/extras/testIndexSeq_ok.out diff --git a/python/test-data/testIndexSeq.py b/python/file-test-data/extras/testIndexSeq_ok.py similarity index 100% rename from python/test-data/testIndexSeq.py rename to python/file-test-data/extras/testIndexSeq_ok.py diff --git a/python/file-test-data/extras/testInvalidLiteral.err b/python/file-test-data/extras/testInvalidLiteral.err new file mode 100644 index 00000000..85d327ec --- /dev/null +++ b/python/file-test-data/extras/testInvalidLiteral.err @@ -0,0 +1,10 @@ +Traceback (most recent call last): + File "file-test-data/extras/testInvalidLiteral.py", line 8, in + gameFull([['x']]) + +WyppTypeError: ungültiger Typ `list[list[['x', 'o', '-']]]` + +## Datei file-test-data/extras/testInvalidLiteral.py +## Typ deklariert in Zeile 5: + +def gameFull(game:Game)->bool: \ No newline at end of file diff --git a/python/file-test-data/extras/testInvalidLiteral.out b/python/file-test-data/extras/testInvalidLiteral.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testInvalidLiteral.py b/python/file-test-data/extras/testInvalidLiteral.py similarity index 84% rename from python/test-data/testInvalidLiteral.py rename to python/file-test-data/extras/testInvalidLiteral.py index 3ef2938e..20a311a3 100644 --- a/python/test-data/testInvalidLiteral.py +++ b/python/file-test-data/extras/testInvalidLiteral.py @@ -4,3 +4,5 @@ Game = list[list[Mark]] def gameFull(game:Game)->bool: pass + +gameFull([['x']]) diff --git a/python/file-test-data/extras/testIterable1_ok.err b/python/file-test-data/extras/testIterable1_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterable1.out b/python/file-test-data/extras/testIterable1_ok.out similarity index 100% rename from python/test-data/testIterable1.out rename to python/file-test-data/extras/testIterable1_ok.out diff --git a/python/test-data/testIterable1.py b/python/file-test-data/extras/testIterable1_ok.py similarity index 100% rename from python/test-data/testIterable1.py rename to python/file-test-data/extras/testIterable1_ok.py diff --git a/python/file-test-data/extras/testIterable2_ok.err b/python/file-test-data/extras/testIterable2_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterable2.out b/python/file-test-data/extras/testIterable2_ok.out similarity index 100% rename from python/test-data/testIterable2.out rename to python/file-test-data/extras/testIterable2_ok.out diff --git a/python/test-data/testIterable2.py b/python/file-test-data/extras/testIterable2_ok.py similarity index 100% rename from python/test-data/testIterable2.py rename to python/file-test-data/extras/testIterable2_ok.py diff --git a/python/file-test-data/extras/testIterable3_ok.err b/python/file-test-data/extras/testIterable3_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterable3.out b/python/file-test-data/extras/testIterable3_ok.out similarity index 100% rename from python/test-data/testIterable3.out rename to python/file-test-data/extras/testIterable3_ok.out diff --git a/python/test-data/testIterable3.py b/python/file-test-data/extras/testIterable3_ok.py similarity index 100% rename from python/test-data/testIterable3.py rename to python/file-test-data/extras/testIterable3_ok.py diff --git a/python/file-test-data/extras/testIterable4_ok.err b/python/file-test-data/extras/testIterable4_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterable4.out b/python/file-test-data/extras/testIterable4_ok.out similarity index 100% rename from python/test-data/testIterable4.out rename to python/file-test-data/extras/testIterable4_ok.out diff --git a/python/test-data/testIterable4.py b/python/file-test-data/extras/testIterable4_ok.py similarity index 100% rename from python/test-data/testIterable4.py rename to python/file-test-data/extras/testIterable4_ok.py diff --git a/python/file-test-data/extras/testIterable5_ok.err b/python/file-test-data/extras/testIterable5_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterable5.out b/python/file-test-data/extras/testIterable5_ok.out similarity index 100% rename from python/test-data/testIterable5.out rename to python/file-test-data/extras/testIterable5_ok.out diff --git a/python/test-data/testIterable5.py b/python/file-test-data/extras/testIterable5_ok.py similarity index 100% rename from python/test-data/testIterable5.py rename to python/file-test-data/extras/testIterable5_ok.py diff --git a/python/file-test-data/extras/testIterable6_ok.err b/python/file-test-data/extras/testIterable6_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterable6.out b/python/file-test-data/extras/testIterable6_ok.out similarity index 100% rename from python/test-data/testIterable6.out rename to python/file-test-data/extras/testIterable6_ok.out diff --git a/python/test-data/testIterable6.py b/python/file-test-data/extras/testIterable6_ok.py similarity index 100% rename from python/test-data/testIterable6.py rename to python/file-test-data/extras/testIterable6_ok.py diff --git a/python/file-test-data/extras/testIterable7_ok.err b/python/file-test-data/extras/testIterable7_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testIterable7_ok.out b/python/file-test-data/extras/testIterable7_ok.out new file mode 100644 index 00000000..db562fb0 --- /dev/null +++ b/python/file-test-data/extras/testIterable7_ok.out @@ -0,0 +1,3 @@ +start of foo +end of foo +[15] diff --git a/python/test-data/testIterable7.py b/python/file-test-data/extras/testIterable7_ok.py similarity index 100% rename from python/test-data/testIterable7.py rename to python/file-test-data/extras/testIterable7_ok.py diff --git a/python/file-test-data/extras/testIterableImplicitAny.err b/python/file-test-data/extras/testIterableImplicitAny.err new file mode 100644 index 00000000..33552986 --- /dev/null +++ b/python/file-test-data/extras/testIterableImplicitAny.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testIterableImplicitAny.py", line 13, in + foo(NotIterable()) + +WyppTypeError: NotIterable + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `Iterable` als erstes Argument. +Aber der übergebene Wert hat Typ `NotIterable`. + +## Datei file-test-data/extras/testIterableImplicitAny.py +## Fehlerhafter Aufruf in Zeile 13: + +foo(NotIterable()) + +## Typ deklariert in Zeile 7: + +def foo(it: Iterable) -> int: \ No newline at end of file diff --git a/python/test-data/testIterableImplicitAny.out b/python/file-test-data/extras/testIterableImplicitAny.out similarity index 100% rename from python/test-data/testIterableImplicitAny.out rename to python/file-test-data/extras/testIterableImplicitAny.out diff --git a/python/test-data/testIterableImplicitAny.py b/python/file-test-data/extras/testIterableImplicitAny.py similarity index 100% rename from python/test-data/testIterableImplicitAny.py rename to python/file-test-data/extras/testIterableImplicitAny.py diff --git a/python/file-test-data/extras/testIterator2_ok.err b/python/file-test-data/extras/testIterator2_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterator.out b/python/file-test-data/extras/testIterator2_ok.out similarity index 100% rename from python/test-data/testIterator.out rename to python/file-test-data/extras/testIterator2_ok.out diff --git a/python/test-data/testIterator2.py b/python/file-test-data/extras/testIterator2_ok.py similarity index 100% rename from python/test-data/testIterator2.py rename to python/file-test-data/extras/testIterator2_ok.py diff --git a/python/file-test-data/extras/testIterator3_ok.err b/python/file-test-data/extras/testIterator3_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterator3.out b/python/file-test-data/extras/testIterator3_ok.out similarity index 100% rename from python/test-data/testIterator3.out rename to python/file-test-data/extras/testIterator3_ok.out diff --git a/python/test-data/testIterator3.py b/python/file-test-data/extras/testIterator3_ok.py similarity index 100% rename from python/test-data/testIterator3.py rename to python/file-test-data/extras/testIterator3_ok.py diff --git a/python/file-test-data/extras/testIterator4_ok.err b/python/file-test-data/extras/testIterator4_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterator4.out b/python/file-test-data/extras/testIterator4_ok.out similarity index 100% rename from python/test-data/testIterator4.out rename to python/file-test-data/extras/testIterator4_ok.out diff --git a/python/test-data/testIterator4.py b/python/file-test-data/extras/testIterator4_ok.py similarity index 100% rename from python/test-data/testIterator4.py rename to python/file-test-data/extras/testIterator4_ok.py diff --git a/python/file-test-data/extras/testIterator_ok.err b/python/file-test-data/extras/testIterator_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testIterator2.out b/python/file-test-data/extras/testIterator_ok.out similarity index 100% rename from python/test-data/testIterator2.out rename to python/file-test-data/extras/testIterator_ok.out diff --git a/python/test-data/testIterator.py b/python/file-test-data/extras/testIterator_ok.py similarity index 100% rename from python/test-data/testIterator.py rename to python/file-test-data/extras/testIterator_ok.py diff --git a/python/file-test-data/extras/testLiteral1.err b/python/file-test-data/extras/testLiteral1.err new file mode 100644 index 00000000..14bb807e --- /dev/null +++ b/python/file-test-data/extras/testLiteral1.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testLiteral1.py", line 3, in + T = Literal('a', 'b') + +WyppTypeError: ungültiger Typ `Literal('a', 'b')` + +Wolltest du `Literal['a', 'b']` schreiben? + +## Datei file-test-data/extras/testLiteral1.py +## Typ deklariert in Zeile 3: + +T = Literal('a', 'b') diff --git a/python/file-test-data/extras/testLiteral1.out b/python/file-test-data/extras/testLiteral1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testLiteral1.py b/python/file-test-data/extras/testLiteral1.py similarity index 100% rename from python/test-data/testLiteral1.py rename to python/file-test-data/extras/testLiteral1.py diff --git a/python/file-test-data/extras/testLiteralInstanceOf_ok.err b/python/file-test-data/extras/testLiteralInstanceOf_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testLiteralInstanceOf.out b/python/file-test-data/extras/testLiteralInstanceOf_ok.out similarity index 74% rename from python/test-data/testLiteralInstanceOf.out rename to python/file-test-data/extras/testLiteralInstanceOf_ok.out index 483b67ec..34be1684 100644 --- a/python/test-data/testLiteralInstanceOf.out +++ b/python/file-test-data/extras/testLiteralInstanceOf_ok.out @@ -1,8 +1,9 @@ True +False True True True -True +False True True True diff --git a/python/test-data/testLiteralInstanceOf.py b/python/file-test-data/extras/testLiteralInstanceOf_ok.py similarity index 84% rename from python/test-data/testLiteralInstanceOf.py rename to python/file-test-data/extras/testLiteralInstanceOf_ok.py index 9f653f11..32b97afb 100644 --- a/python/test-data/testLiteralInstanceOf.py +++ b/python/file-test-data/extras/testLiteralInstanceOf_ok.py @@ -1,12 +1,13 @@ from wypp import * print(isinstance(1, Literal[1,2,3])) -print(not isinstance(1, Literal["1", "2"])) +print(isinstance(1, Literal["1", "2"])) # nested print(isinstance(1, Literal[1, Literal[Literal[2], 3]])) print(isinstance(2, Literal[1, Literal[Literal[2], 3]])) print(isinstance(3, Literal[1, Literal[Literal[2], 3]])) +print(isinstance(4, Literal[1, Literal[Literal[2], 3]])) print(Literal[1, Literal[Literal[2], 3]] == Literal[1, 2, 3]) print(Literal[1, Literal[Literal[2], 3]] != Literal[1, 2, 3, 4]) diff --git a/python/file-test-data/extras/testLockFactory.err b/python/file-test-data/extras/testLockFactory.err new file mode 100644 index 00000000..933f618c --- /dev/null +++ b/python/file-test-data/extras/testLockFactory.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testLockFactory.py", line 15, in + foo("not a lock") + +WyppTypeError: "not a lock" + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `LockFactory` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testLockFactory.py +## Fehlerhafter Aufruf in Zeile 15: + +foo("not a lock") + +## Typ deklariert in Zeile 6: + +def foo(lock: wypp.LockFactory) -> None: \ No newline at end of file diff --git a/python/file-test-data/extras/testLockFactory.out b/python/file-test-data/extras/testLockFactory.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testLockFactory.py b/python/file-test-data/extras/testLockFactory.py similarity index 100% rename from python/test-data/testLockFactory.py rename to python/file-test-data/extras/testLockFactory.py diff --git a/python/file-test-data/extras/testLockFactory2.err b/python/file-test-data/extras/testLockFactory2.err new file mode 100644 index 00000000..be3a9241 --- /dev/null +++ b/python/file-test-data/extras/testLockFactory2.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testLockFactory2.py", line 14, in + foo("not a lock") + +WyppTypeError: "not a lock" + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `wypp.Lock` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testLockFactory2.py +## Fehlerhafter Aufruf in Zeile 14: + +foo("not a lock") + +## Typ deklariert in Zeile 6: + +def foo(l: wypp.Lock) -> None: \ No newline at end of file diff --git a/python/file-test-data/extras/testLockFactory2.out b/python/file-test-data/extras/testLockFactory2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testLockFactory2.py b/python/file-test-data/extras/testLockFactory2.py similarity index 100% rename from python/test-data/testLockFactory2.py rename to python/file-test-data/extras/testLockFactory2.py diff --git a/python/file-test-data/extras/testLockFactory_ok.err b/python/file-test-data/extras/testLockFactory_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testLockFactory_ok.out b/python/file-test-data/extras/testLockFactory_ok.out new file mode 100644 index 00000000..9766475a --- /dev/null +++ b/python/file-test-data/extras/testLockFactory_ok.out @@ -0,0 +1 @@ +ok diff --git a/python/file-test-data/extras/testLockFactory_ok.py b/python/file-test-data/extras/testLockFactory_ok.py new file mode 100644 index 00000000..3d1e1613 --- /dev/null +++ b/python/file-test-data/extras/testLockFactory_ok.py @@ -0,0 +1,14 @@ +from wypp import * +import wypp +import threading +# See: https://github.com/skogsbaer/write-your-python-program/issues/77 + +def foo(lock: wypp.LockFactory) -> None: + l = lock() + l.acquire() + # ... + l.release() + pass + +foo(threading.Lock) +print('ok') diff --git a/python/file-test-data/extras/testMissingReturn.err b/python/file-test-data/extras/testMissingReturn.err new file mode 100644 index 00000000..14babc70 --- /dev/null +++ b/python/file-test-data/extras/testMissingReturn.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testMissingReturn.py", line 6, in + print(billigStrom(500)) + File "file-test-data/extras/testMissingReturn.py", line 4, in billigStrom + pass + +WyppTypeError: kein Rückgabewert vorhanden + +Rückgabewert vom Typ `float` erwartet bei Aufruf der Funktion `billigStrom`. +Aber kein Rückgabewert vorhanden. + +## Datei file-test-data/extras/testMissingReturn.py +## Rückgabetyp deklariert in Zeile 3: + +def billigStrom(kwh: float) -> float: + +## Aufruf in Zeile 6 führt dazu, dass die Funktion keinen Wert zurückgibt: + +print(billigStrom(500)) \ No newline at end of file diff --git a/python/file-test-data/extras/testMissingReturn.out b/python/file-test-data/extras/testMissingReturn.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testMissingReturn.py b/python/file-test-data/extras/testMissingReturn.py similarity index 100% rename from python/test-data/testMissingReturn.py rename to python/file-test-data/extras/testMissingReturn.py diff --git a/python/file-test-data/extras/testNameErrorBug_ok.err b/python/file-test-data/extras/testNameErrorBug_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testNameErrorBug_ok.out b/python/file-test-data/extras/testNameErrorBug_ok.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testNameErrorBug.py b/python/file-test-data/extras/testNameErrorBug_ok.py similarity index 100% rename from python/test-data/testNameErrorBug.py rename to python/file-test-data/extras/testNameErrorBug_ok.py diff --git a/python/file-test-data/extras/testOriginalTypeNames_ok.err b/python/file-test-data/extras/testOriginalTypeNames_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testOriginalTypeNames_ok.out b/python/file-test-data/extras/testOriginalTypeNames_ok.out new file mode 100644 index 00000000..614cfd32 --- /dev/null +++ b/python/file-test-data/extras/testOriginalTypeNames_ok.out @@ -0,0 +1 @@ + diff --git a/python/test-data/testOriginalTypeNames.py b/python/file-test-data/extras/testOriginalTypeNames_ok.py similarity index 100% rename from python/test-data/testOriginalTypeNames.py rename to python/file-test-data/extras/testOriginalTypeNames_ok.py diff --git a/python/file-test-data/extras/testRecordSetTypeForwardRef.err b/python/file-test-data/extras/testRecordSetTypeForwardRef.err new file mode 100644 index 00000000..a4b6d328 --- /dev/null +++ b/python/file-test-data/extras/testRecordSetTypeForwardRef.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testRecordSetTypeForwardRef.py", line 15, in + m() + File "file-test-data/extras/testRecordSetTypeForwardRef.py", line 13, in m + r.x = "hello" + +WyppTypeError: "hello" + +Attribut `x` des Records `Record` deklariert als Typ `A`. +Das Attribute kann nicht auf einen Wert vom Typ `str` gesetzt werden. + +## Datei file-test-data/extras/testRecordSetTypeForwardRef.py +## Fehlerhafte Zuweisung in Zeile 13: + + r.x = "hello" + +## Typ deklariert in Zeile 6: + + x: A \ No newline at end of file diff --git a/python/file-test-data/extras/testRecordSetTypeForwardRef.out b/python/file-test-data/extras/testRecordSetTypeForwardRef.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testRecordSetTypeForwardRef.py b/python/file-test-data/extras/testRecordSetTypeForwardRef.py similarity index 100% rename from python/test-data/testRecordSetTypeForwardRef.py rename to python/file-test-data/extras/testRecordSetTypeForwardRef.py diff --git a/python/file-test-data/extras/testRecordSetTypes.err b/python/file-test-data/extras/testRecordSetTypes.err new file mode 100644 index 00000000..ce400994 --- /dev/null +++ b/python/file-test-data/extras/testRecordSetTypes.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testRecordSetTypes.py", line 12, in + m() + File "file-test-data/extras/testRecordSetTypes.py", line 10, in m + r.x = "hello" + +WyppTypeError: "hello" + +Attribut `x` des Records `Record` deklariert als Typ `int`. +Das Attribute kann nicht auf einen Wert vom Typ `str` gesetzt werden. + +## Datei file-test-data/extras/testRecordSetTypes.py +## Fehlerhafte Zuweisung in Zeile 10: + + r.x = "hello" + +## Typ deklariert in Zeile 5: + + x : int \ No newline at end of file diff --git a/python/file-test-data/extras/testRecordSetTypes.out b/python/file-test-data/extras/testRecordSetTypes.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testRecordSetTypes.py b/python/file-test-data/extras/testRecordSetTypes.py similarity index 100% rename from python/test-data/testRecordSetTypes.py rename to python/file-test-data/extras/testRecordSetTypes.py diff --git a/python/file-test-data/extras/testRecordTypes.err b/python/file-test-data/extras/testRecordTypes.err new file mode 100644 index 00000000..aac97f56 --- /dev/null +++ b/python/file-test-data/extras/testRecordTypes.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testRecordTypes.py", line 8, in + p = Point(1, '5') + +WyppTypeError: '5' + +Der Aufruf des Konstruktors des Records `Point` erwartet Wert vom Typ `int` als zweites Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testRecordTypes.py +## Fehlerhafter Aufruf in Zeile 8: + +p = Point(1, '5') + +## Typ deklariert in Zeile 6: + + y: int \ No newline at end of file diff --git a/python/file-test-data/extras/testRecordTypes.out b/python/file-test-data/extras/testRecordTypes.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testRecordTypes.py b/python/file-test-data/extras/testRecordTypes.py similarity index 100% rename from python/test-data/testRecordTypes.py rename to python/file-test-data/extras/testRecordTypes.py diff --git a/python/file-test-data/extras/testTodo.err b/python/file-test-data/extras/testTodo.err new file mode 100644 index 00000000..3764a715 --- /dev/null +++ b/python/file-test-data/extras/testTodo.err @@ -0,0 +1,7 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTodo.py", line 3, in + todo() + File "code/wypp/writeYourProgram.py", line 327, in todo + raise errors.TodoError(msg) + +TODO diff --git a/python/file-test-data/extras/testTodo.out b/python/file-test-data/extras/testTodo.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTodo.py b/python/file-test-data/extras/testTodo.py similarity index 100% rename from python/test-data/testTodo.py rename to python/file-test-data/extras/testTodo.py diff --git a/python/file-test-data/extras/testTraceback.err b/python/file-test-data/extras/testTraceback.err new file mode 100644 index 00000000..df26c7f1 --- /dev/null +++ b/python/file-test-data/extras/testTraceback.err @@ -0,0 +1,6 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTraceback.py", line 9, in + foo(lst) + File "file-test-data/extras/testTraceback.py", line 7, in foo + print(lst[10]) +IndexError: list index out of range \ No newline at end of file diff --git a/python/test-data/testTraceback.out b/python/file-test-data/extras/testTraceback.out similarity index 100% rename from python/test-data/testTraceback.out rename to python/file-test-data/extras/testTraceback.out diff --git a/python/test-data/testTraceback.py b/python/file-test-data/extras/testTraceback.py similarity index 100% rename from python/test-data/testTraceback.py rename to python/file-test-data/extras/testTraceback.py diff --git a/python/test-data/testTraceback2.err b/python/file-test-data/extras/testTraceback2.err similarity index 58% rename from python/test-data/testTraceback2.err rename to python/file-test-data/extras/testTraceback2.err index 82834546..d6703684 100644 --- a/python/test-data/testTraceback2.err +++ b/python/file-test-data/extras/testTraceback2.err @@ -1,4 +1,4 @@ - File "test-data/testTraceback2.py", line 3 + File "testTraceback2.py", line ? lst = [1,2,3 ^ SyntaxError: '[' was never closed diff --git a/python/file-test-data/extras/testTraceback2.out b/python/file-test-data/extras/testTraceback2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTraceback2.py b/python/file-test-data/extras/testTraceback2.py similarity index 100% rename from python/test-data/testTraceback2.py rename to python/file-test-data/extras/testTraceback2.py diff --git a/python/file-test-data/extras/testTraceback3.err b/python/file-test-data/extras/testTraceback3.err new file mode 100644 index 00000000..7bc652e2 --- /dev/null +++ b/python/file-test-data/extras/testTraceback3.err @@ -0,0 +1,4 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTraceback3.py", line 2, in + print([1,2,3][10]) +IndexError: list index out of range \ No newline at end of file diff --git a/python/file-test-data/extras/testTraceback3.out b/python/file-test-data/extras/testTraceback3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTraceback3.py b/python/file-test-data/extras/testTraceback3.py similarity index 100% rename from python/test-data/testTraceback3.py rename to python/file-test-data/extras/testTraceback3.py diff --git a/python/file-test-data/extras/testTypeKeyword_ok.err b/python/file-test-data/extras/testTypeKeyword_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypeKeyword.out b/python/file-test-data/extras/testTypeKeyword_ok.out similarity index 100% rename from python/test-data/testTypeKeyword.out rename to python/file-test-data/extras/testTypeKeyword_ok.out diff --git a/python/test-data/testTypeKeyword.py b/python/file-test-data/extras/testTypeKeyword_ok.py similarity index 82% rename from python/test-data/testTypeKeyword.py rename to python/file-test-data/extras/testTypeKeyword_ok.py index a9125795..d98ceb0b 100644 --- a/python/test-data/testTypeKeyword.py +++ b/python/file-test-data/extras/testTypeKeyword_ok.py @@ -1,3 +1,4 @@ +# WYPP_TEST_CONFIG: {"typecheck": "both"} from __future__ import annotations from wypp import * diff --git a/python/file-test-data/extras/testTypes1.err b/python/file-test-data/extras/testTypes1.err new file mode 100644 index 00000000..ca748a76 --- /dev/null +++ b/python/file-test-data/extras/testTypes1.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypes1.py", line 4, in + inc("1") + +WyppTypeError: "1" + +Der Aufruf der Funktion `inc` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testTypes1.py +## Fehlerhafter Aufruf in Zeile 4: + +inc("1") + +## Typ deklariert in Zeile 1: + +def inc(x: int) -> int: \ No newline at end of file diff --git a/python/test-data/testTypes1.err-notypes b/python/file-test-data/extras/testTypes1.err-notypes similarity index 100% rename from python/test-data/testTypes1.err-notypes rename to python/file-test-data/extras/testTypes1.err-notypes diff --git a/python/file-test-data/extras/testTypes1.out b/python/file-test-data/extras/testTypes1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypes1.py b/python/file-test-data/extras/testTypes1.py similarity index 100% rename from python/test-data/testTypes1.py rename to python/file-test-data/extras/testTypes1.py diff --git a/python/file-test-data/extras/testTypes2.err b/python/file-test-data/extras/testTypes2.err new file mode 100644 index 00000000..fa96aea6 --- /dev/null +++ b/python/file-test-data/extras/testTypes2.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypes2.py", line 4, in + inc("1") + +WyppTypeError: "1" + +Der Aufruf der Funktion `inc` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testTypes2.py +## Fehlerhafter Aufruf in Zeile 4: + +inc("1") + +## Typ deklariert in Zeile 1: + +def inc(x: int) -> int: \ No newline at end of file diff --git a/python/file-test-data/extras/testTypes2.err-notypes b/python/file-test-data/extras/testTypes2.err-notypes new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testTypes2.out b/python/file-test-data/extras/testTypes2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypes2.py b/python/file-test-data/extras/testTypes2.py similarity index 100% rename from python/test-data/testTypes2.py rename to python/file-test-data/extras/testTypes2.py diff --git a/python/file-test-data/extras/testTypes2_ok.py b/python/file-test-data/extras/testTypes2_ok.py new file mode 100644 index 00000000..c99dca5c --- /dev/null +++ b/python/file-test-data/extras/testTypes2_ok.py @@ -0,0 +1,5 @@ +# WYPP_TEST_CONFIG: {"typecheck": false} +def inc(x: int) -> int: + return x + +inc("1") diff --git a/python/file-test-data/extras/testTypesDict3.err b/python/file-test-data/extras/testTypesDict3.err new file mode 100644 index 00000000..09717b3d --- /dev/null +++ b/python/file-test-data/extras/testTypesDict3.err @@ -0,0 +1,22 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesDict3.py", line 11, in + foo({'y': func}) + File "file-test-data/extras/testTypesDict3.py", line 8, in foo + return res + +WyppTypeError: ['xxx', 42] + +Rückgabewert vom Typ `list[str]` erwartet bei Aufruf der Funktion `foo`. + +## Datei file-test-data/extras/testTypesDict3.py +## Rückgabetyp deklariert in Zeile 3: + +def foo(d: dict[str, Callable[[], str]]) -> list[str]: + +## Fehlerhaftes return in Zeile 8: + + return res + +## Aufruf in Zeile 11 verursacht das fehlerhafte return: + +foo({'y': func}) \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesDict3.out b/python/file-test-data/extras/testTypesDict3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesDict3.py b/python/file-test-data/extras/testTypesDict3.py similarity index 100% rename from python/test-data/testTypesDict3.py rename to python/file-test-data/extras/testTypesDict3.py diff --git a/python/file-test-data/extras/testTypesDict4.err b/python/file-test-data/extras/testTypesDict4.err new file mode 100644 index 00000000..195ea6af --- /dev/null +++ b/python/file-test-data/extras/testTypesDict4.err @@ -0,0 +1,24 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesDict4.py", line 14, in + bar({'y': func}) # error + File "file-test-data/extras/testTypesDict4.py", line 11, in bar + return foo(d) + File "file-test-data/extras/testTypesDict4.py", line 8, in foo + return res + +WyppTypeError: [42, 'x'] + +Rückgabewert vom Typ `list[str]` erwartet bei Aufruf der Funktion `foo`. + +## Datei file-test-data/extras/testTypesDict4.py +## Rückgabetyp deklariert in Zeile 3: + +def foo(d: dict[str, Callable[[], str]]) -> list[str]: + +## Fehlerhaftes return in Zeile 8: + + return res + +## Aufruf in Zeile 11 verursacht das fehlerhafte return: + + return foo(d) \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesDict4.out b/python/file-test-data/extras/testTypesDict4.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesDict4.py b/python/file-test-data/extras/testTypesDict4.py similarity index 100% rename from python/test-data/testTypesDict4.py rename to python/file-test-data/extras/testTypesDict4.py diff --git a/python/file-test-data/extras/testTypesHigherOrderFuns.err b/python/file-test-data/extras/testTypesHigherOrderFuns.err new file mode 100644 index 00000000..ab4217f9 --- /dev/null +++ b/python/file-test-data/extras/testTypesHigherOrderFuns.err @@ -0,0 +1,22 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesHigherOrderFuns.py", line 10, in + map(["hello", "1"], lambda x: x) + File "file-test-data/extras/testTypesHigherOrderFuns.py", line 7, in map + return res + +WyppTypeError: ['hello', '1'] + +Rückgabewert vom Typ `list[int]` erwartet bei Aufruf der Funktion `map`. + +## Datei file-test-data/extras/testTypesHigherOrderFuns.py +## Rückgabetyp deklariert in Zeile 3: + +def map(container: Iterable[str], fun: Callable[[str], int]) -> list[int]: + +## Fehlerhaftes return in Zeile 7: + + return res + +## Aufruf in Zeile 10 verursacht das fehlerhafte return: + +map(["hello", "1"], lambda x: x) \ No newline at end of file diff --git a/python/test-data/testTypesHigherOrderFuns.out b/python/file-test-data/extras/testTypesHigherOrderFuns.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns.out rename to python/file-test-data/extras/testTypesHigherOrderFuns.out diff --git a/python/test-data/testTypesHigherOrderFuns.py b/python/file-test-data/extras/testTypesHigherOrderFuns.py similarity index 100% rename from python/test-data/testTypesHigherOrderFuns.py rename to python/file-test-data/extras/testTypesHigherOrderFuns.py diff --git a/python/file-test-data/extras/testTypesHigherOrderFuns2_ok.err b/python/file-test-data/extras/testTypesHigherOrderFuns2_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesHigherOrderFuns2.out b/python/file-test-data/extras/testTypesHigherOrderFuns2_ok.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns2.out rename to python/file-test-data/extras/testTypesHigherOrderFuns2_ok.out diff --git a/python/test-data/testTypesHigherOrderFuns2.py b/python/file-test-data/extras/testTypesHigherOrderFuns2_ok.py similarity index 100% rename from python/test-data/testTypesHigherOrderFuns2.py rename to python/file-test-data/extras/testTypesHigherOrderFuns2_ok.py diff --git a/python/file-test-data/extras/testTypesHigherOrderFuns3.err b/python/file-test-data/extras/testTypesHigherOrderFuns3.err new file mode 100644 index 00000000..28bdb129 --- /dev/null +++ b/python/file-test-data/extras/testTypesHigherOrderFuns3.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesHigherOrderFuns3.py", line 38, in + homePoints: Callable[[GameResult], int] = mkGamePoints(42) + +WyppTypeError: 42 + +Der Aufruf der Funktion `mkGamePoints` erwartet Wert vom Typ `Callable[[int, int], bool]` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/extras/testTypesHigherOrderFuns3.py +## Fehlerhafter Aufruf in Zeile 38: + +homePoints: Callable[[GameResult], int] = mkGamePoints(42) + +## Typ deklariert in Zeile 35: + +def mkGamePoints(cmp: Callable[[int, int], bool]) -> Callable[[GameResult], int]: \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesHigherOrderFuns3.out b/python/file-test-data/extras/testTypesHigherOrderFuns3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesHigherOrderFuns3.py b/python/file-test-data/extras/testTypesHigherOrderFuns3.py similarity index 94% rename from python/test-data/testTypesHigherOrderFuns3.py rename to python/file-test-data/extras/testTypesHigherOrderFuns3.py index 15319d0a..4777f85c 100644 --- a/python/test-data/testTypesHigherOrderFuns3.py +++ b/python/file-test-data/extras/testTypesHigherOrderFuns3.py @@ -35,7 +35,7 @@ def gamePoints(game: GameResult, cmp: Callable[[int, int], bool]) -> int: def mkGamePoints(cmp: Callable[[int, int], bool]) -> Callable[[GameResult], int]: return lambda game: gamePoints(game, cmp) -homePoints: Callable[[GameResult], int] = mkGamePoints(lambda g, h: "foo") +homePoints: Callable[[GameResult], int] = mkGamePoints(42) guestPoints: Callable[[GameResult], int] = mkGamePoints(lambda g, h: h > g) check(homePoints(game1), 0) diff --git a/python/file-test-data/extras/testTypesHigherOrderFuns4_ok.err b/python/file-test-data/extras/testTypesHigherOrderFuns4_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesHigherOrderFuns4.out b/python/file-test-data/extras/testTypesHigherOrderFuns4_ok.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns4.out rename to python/file-test-data/extras/testTypesHigherOrderFuns4_ok.out diff --git a/python/test-data/testTypesHigherOrderFuns4.py b/python/file-test-data/extras/testTypesHigherOrderFuns4_ok.py similarity index 100% rename from python/test-data/testTypesHigherOrderFuns4.py rename to python/file-test-data/extras/testTypesHigherOrderFuns4_ok.py diff --git a/python/file-test-data/extras/testTypesHigherOrderFuns5_ok.err b/python/file-test-data/extras/testTypesHigherOrderFuns5_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesHigherOrderFuns5.out b/python/file-test-data/extras/testTypesHigherOrderFuns5_ok.out similarity index 100% rename from python/test-data/testTypesHigherOrderFuns5.out rename to python/file-test-data/extras/testTypesHigherOrderFuns5_ok.out diff --git a/python/test-data/testTypesHigherOrderFuns5.py b/python/file-test-data/extras/testTypesHigherOrderFuns5_ok.py similarity index 100% rename from python/test-data/testTypesHigherOrderFuns5.py rename to python/file-test-data/extras/testTypesHigherOrderFuns5_ok.py diff --git a/python/file-test-data/extras/testTypesProtos1.err b/python/file-test-data/extras/testTypesProtos1.err new file mode 100644 index 00000000..1dbb65d6 --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos1.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos1.py", line 21, in + doSomething(Dog()) + File "file-test-data/extras/testTypesProtos1.py", line 19, in doSomething + print(a.makeSound(3.14)) + +WyppTypeError: 3.14 + +Der Aufruf der Methode `makeSound` aus Klasse `Dog` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `float`. + +## Datei file-test-data/extras/testTypesProtos1.py +## Fehlerhafter Aufruf in Zeile 19: + + print(a.makeSound(3.14)) + +## Typ deklariert in Zeile 13: + + def makeSound(self, loadness: int) -> str: \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos1.out b/python/file-test-data/extras/testTypesProtos1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos1.py b/python/file-test-data/extras/testTypesProtos1.py similarity index 100% rename from python/test-data/testTypesProtos1.py rename to python/file-test-data/extras/testTypesProtos1.py diff --git a/python/file-test-data/extras/testTypesProtos2.err b/python/file-test-data/extras/testTypesProtos2.err new file mode 100644 index 00000000..f25564ef --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos2.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos2.py", line 21, in + doSomething(Dog()) + File "file-test-data/extras/testTypesProtos2.py", line 19, in doSomething + print(a.makeSound(3.14)) + +WyppTypeError: 3.14 + +Der Aufruf der Methode `makeSound` aus Klasse `Dog` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `float`. + +## Datei file-test-data/extras/testTypesProtos2.py +## Fehlerhafter Aufruf in Zeile 19: + + print(a.makeSound(3.14)) + +## Typ deklariert in Zeile 13: + + def makeSound(self, loadness: int) -> str: \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos2.out b/python/file-test-data/extras/testTypesProtos2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos2.py b/python/file-test-data/extras/testTypesProtos2.py similarity index 100% rename from python/test-data/testTypesProtos2.py rename to python/file-test-data/extras/testTypesProtos2.py diff --git a/python/file-test-data/extras/testTypesProtos3.err b/python/file-test-data/extras/testTypesProtos3.err new file mode 100644 index 00000000..e8d33de9 --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos3.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos3.py", line 21, in + doSomething(Dog()) + File "file-test-data/extras/testTypesProtos3.py", line 19, in doSomething + print(a.makeSound(3.14)) + +WyppTypeError: + +Der Aufruf der Methode `makeSound` aus Klasse `Dog` erwartet Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat Typ `Dog`. + +## Datei file-test-data/extras/testTypesProtos3.py +## Fehlerhafter Aufruf in Zeile 19: + + print(a.makeSound(3.14)) + +## Typ deklariert in Zeile 13: + + def makeSound(loadness: int) -> str: # self parameter omitted! \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos3.out b/python/file-test-data/extras/testTypesProtos3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos3.py b/python/file-test-data/extras/testTypesProtos3.py similarity index 100% rename from python/test-data/testTypesProtos3.py rename to python/file-test-data/extras/testTypesProtos3.py diff --git a/python/file-test-data/extras/testTypesProtos4.err b/python/file-test-data/extras/testTypesProtos4.err new file mode 100644 index 00000000..a41011f4 --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos4.err @@ -0,0 +1,21 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos4.py", line 27, in + print(foo(ConcreteWrong())) + File "file-test-data/extras/testTypesProtos4.py", line 24, in foo + return fn(2) + File "file-test-data/extras/testTypesProtos4.py", line 20, in + return lambda x: bar(x) # invalid call of bar with argument of type int + +WyppTypeError: 2 + +Der Aufruf der Funktion `bar` erwartet Wert vom Typ `str` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/extras/testTypesProtos4.py +## Fehlerhafter Aufruf in Zeile 20: + + return lambda x: bar(x) # invalid call of bar with argument of type int + +## Typ deklariert in Zeile 15: + +def bar(s: str) -> int: \ No newline at end of file diff --git a/python/test-data/testTypesProtos4.out b/python/file-test-data/extras/testTypesProtos4.out similarity index 100% rename from python/test-data/testTypesProtos4.out rename to python/file-test-data/extras/testTypesProtos4.out diff --git a/python/test-data/testTypesProtos4.py b/python/file-test-data/extras/testTypesProtos4.py similarity index 100% rename from python/test-data/testTypesProtos4.py rename to python/file-test-data/extras/testTypesProtos4.py diff --git a/python/file-test-data/extras/testTypesProtos5_ok.err b/python/file-test-data/extras/testTypesProtos5_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos5.out b/python/file-test-data/extras/testTypesProtos5_ok.out similarity index 100% rename from python/test-data/testTypesProtos5.out rename to python/file-test-data/extras/testTypesProtos5_ok.out diff --git a/python/test-data/testTypesProtos5.py b/python/file-test-data/extras/testTypesProtos5_ok.py similarity index 100% rename from python/test-data/testTypesProtos5.py rename to python/file-test-data/extras/testTypesProtos5_ok.py diff --git a/python/file-test-data/extras/testTypesProtos6.err b/python/file-test-data/extras/testTypesProtos6.err new file mode 100644 index 00000000..e20d49fd --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos6.err @@ -0,0 +1,25 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos6.py", line 57, in + print(computeTotalSize(root)) + File "file-test-data/extras/testTypesProtos6.py", line 50, in computeTotalSize + fs.accept(visitor) + File "file-test-data/extras/testTypesProtos6.py", line 19, in accept + visitor.visitDirectory(self) + File "file-test-data/extras/testTypesProtos6.py", line 41, in visitDirectory + c.accept(self) + File "file-test-data/extras/testTypesProtos6.py", line 28, in accept + visitor.visitFile(self) + +WyppTypeError: <__wypp__.File object at 0x00> + +Der Aufruf der Methode `visitFile` aus Klasse `TotalSizeVisitor` erwartet Wert vom Typ `str` als erstes Argument. +Aber der übergebene Wert hat Typ `File`. + +## Datei file-test-data/extras/testTypesProtos6.py +## Fehlerhafter Aufruf in Zeile 28: + + visitor.visitFile(self) + +## Typ deklariert in Zeile 42: + + def visitFile(self, file: str): \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos6.out b/python/file-test-data/extras/testTypesProtos6.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos6.py b/python/file-test-data/extras/testTypesProtos6.py similarity index 100% rename from python/test-data/testTypesProtos6.py rename to python/file-test-data/extras/testTypesProtos6.py diff --git a/python/file-test-data/extras/testTypesProtos7.err b/python/file-test-data/extras/testTypesProtos7.err new file mode 100644 index 00000000..d61b751e --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos7.err @@ -0,0 +1,25 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos7.py", line 76, in + print(computeTotalSize(root)) + File "file-test-data/extras/testTypesProtos7.py", line 69, in computeTotalSize + fs.accept(visitor) + File "file-test-data/extras/testTypesProtos7.py", line 36, in accept + visitor.visitDirectory(self) + File "file-test-data/extras/testTypesProtos7.py", line 60, in visitDirectory + c.accept(self) + File "file-test-data/extras/testTypesProtos7.py", line 47, in accept + visitor.visitFile(self) + +WyppTypeError: File('notes.txt') + +Der Aufruf der Methode `visitFile` aus Klasse `TotalSizeVisitor` erwartet Wert vom Typ `str` als erstes Argument. +Aber der übergebene Wert hat Typ `File`. + +## Datei file-test-data/extras/testTypesProtos7.py +## Fehlerhafter Aufruf in Zeile 47: + + visitor.visitFile(self) + +## Typ deklariert in Zeile 61: + + def visitFile(self, f: str): \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos7.out b/python/file-test-data/extras/testTypesProtos7.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos7.py b/python/file-test-data/extras/testTypesProtos7.py similarity index 100% rename from python/test-data/testTypesProtos7.py rename to python/file-test-data/extras/testTypesProtos7.py diff --git a/python/file-test-data/extras/testTypesProtos8.err b/python/file-test-data/extras/testTypesProtos8.err new file mode 100644 index 00000000..a6ff1267 --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos8.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos8.py", line 12, in + bar(Sub()) + File "file-test-data/extras/testTypesProtos8.py", line 10, in bar + b.foo(1, "foo") + +WyppTypeError: "foo" + +Der Aufruf der Methode `foo` aus Klasse `Sub` erwartet Wert vom Typ `float` als zweites Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testTypesProtos8.py +## Fehlerhafter Aufruf in Zeile 10: + + b.foo(1, "foo") + +## Typ deklariert in Zeile 6: + + def foo(self, y: int, x: float): \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos8.out b/python/file-test-data/extras/testTypesProtos8.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos8.py b/python/file-test-data/extras/testTypesProtos8.py similarity index 100% rename from python/test-data/testTypesProtos8.py rename to python/file-test-data/extras/testTypesProtos8.py diff --git a/python/file-test-data/extras/testTypesProtos9.err b/python/file-test-data/extras/testTypesProtos9.err new file mode 100644 index 00000000..5d271c3d --- /dev/null +++ b/python/file-test-data/extras/testTypesProtos9.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesProtos9.py", line 12, in + bar(Sub()) + File "file-test-data/extras/testTypesProtos9.py", line 10, in bar + b.foo(1, "foo") + +WyppTypeError: "foo" + +Der Aufruf der Methode `foo` aus Klasse `Sub` erwartet Wert vom Typ `float` als zweites Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testTypesProtos9.py +## Fehlerhafter Aufruf in Zeile 10: + + b.foo(1, "foo") + +## Typ deklariert in Zeile 6: + + def foo(self, subX: int, subY: float): \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesProtos9.out b/python/file-test-data/extras/testTypesProtos9.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos9.py b/python/file-test-data/extras/testTypesProtos9.py similarity index 100% rename from python/test-data/testTypesProtos9.py rename to python/file-test-data/extras/testTypesProtos9.py diff --git a/python/file-test-data/extras/testTypesRecordInheritance.err b/python/file-test-data/extras/testTypesRecordInheritance.err new file mode 100644 index 00000000..cb44a0b4 --- /dev/null +++ b/python/file-test-data/extras/testTypesRecordInheritance.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesRecordInheritance.py", line 18, in + Point3D(1,2, "foo") + +WyppTypeError: "foo" + +Der Aufruf des Konstruktors des Records `Point3D` erwartet Wert vom Typ `int` als drittes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testTypesRecordInheritance.py +## Fehlerhafter Aufruf in Zeile 18: + +Point3D(1,2, "foo") + +## Typ deklariert in Zeile 11: + + z: int \ No newline at end of file diff --git a/python/test-data/testTypesRecordInheritance.out b/python/file-test-data/extras/testTypesRecordInheritance.out similarity index 100% rename from python/test-data/testTypesRecordInheritance.out rename to python/file-test-data/extras/testTypesRecordInheritance.out diff --git a/python/test-data/testTypesRecordInheritance.py b/python/file-test-data/extras/testTypesRecordInheritance.py similarity index 100% rename from python/test-data/testTypesRecordInheritance.py rename to python/file-test-data/extras/testTypesRecordInheritance.py diff --git a/python/file-test-data/extras/testTypesReturn.err b/python/file-test-data/extras/testTypesReturn.err new file mode 100644 index 00000000..cea0d411 --- /dev/null +++ b/python/file-test-data/extras/testTypesReturn.err @@ -0,0 +1,23 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesReturn.py", line 8, in + foo(False) + File "file-test-data/extras/testTypesReturn.py", line 6, in foo + return 'you stupid' + +WyppTypeError: 'you stupid' + +Rückgabewert vom Typ `int` erwartet bei Aufruf der Funktion `foo`. +Aber der Aufruf gibt einen Wert vom Typ `str` zurück. + +## Datei file-test-data/extras/testTypesReturn.py +## Rückgabetyp deklariert in Zeile 1: + +def foo(flag: bool) -> int: + +## Fehlerhaftes return in Zeile 6: + + return 'you stupid' + +## Aufruf in Zeile 8 verursacht das fehlerhafte return: + +foo(False) \ No newline at end of file diff --git a/python/test-data/testTypesReturn.out b/python/file-test-data/extras/testTypesReturn.out similarity index 100% rename from python/test-data/testTypesReturn.out rename to python/file-test-data/extras/testTypesReturn.out diff --git a/python/test-data/testTypesReturn.py b/python/file-test-data/extras/testTypesReturn.py similarity index 100% rename from python/test-data/testTypesReturn.py rename to python/file-test-data/extras/testTypesReturn.py diff --git a/python/file-test-data/extras/testTypesSequence1.err b/python/file-test-data/extras/testTypesSequence1.err new file mode 100644 index 00000000..cd7927a8 --- /dev/null +++ b/python/file-test-data/extras/testTypesSequence1.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesSequence1.py", line 10, in + foo(1) # should fail + +WyppTypeError: 1 + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `Sequence` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/extras/testTypesSequence1.py +## Fehlerhafter Aufruf in Zeile 10: + +foo(1) # should fail + +## Typ deklariert in Zeile 3: + +def foo(seq: Sequence) -> None: \ No newline at end of file diff --git a/python/test-data/testTypesSequence1.out b/python/file-test-data/extras/testTypesSequence1.out similarity index 100% rename from python/test-data/testTypesSequence1.out rename to python/file-test-data/extras/testTypesSequence1.out diff --git a/python/test-data/testTypesSequence1.py b/python/file-test-data/extras/testTypesSequence1.py similarity index 100% rename from python/test-data/testTypesSequence1.py rename to python/file-test-data/extras/testTypesSequence1.py diff --git a/python/file-test-data/extras/testTypesSequence2.err b/python/file-test-data/extras/testTypesSequence2.err new file mode 100644 index 00000000..de4b451b --- /dev/null +++ b/python/file-test-data/extras/testTypesSequence2.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesSequence2.py", line 11, in + foo("Hello!") # should fail + +WyppTypeError: "Hello!" + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `Sequence[int]` als erstes Argument. +Aber der übergebene Wert hat Typ `str`. + +## Datei file-test-data/extras/testTypesSequence2.py +## Fehlerhafter Aufruf in Zeile 11: + +foo("Hello!") # should fail + +## Typ deklariert in Zeile 3: + +def foo(seq: Sequence[int]) -> None: \ No newline at end of file diff --git a/python/test-data/testTypesSequence2.out b/python/file-test-data/extras/testTypesSequence2.out similarity index 73% rename from python/test-data/testTypesSequence2.out rename to python/file-test-data/extras/testTypesSequence2.out index d19f48dc..5e77e5dc 100644 --- a/python/test-data/testTypesSequence2.out +++ b/python/file-test-data/extras/testTypesSequence2.out @@ -7,5 +7,3 @@ (4, 5) 4 5 -'Hello!' -Hello! diff --git a/python/test-data/testTypesSequence2.py b/python/file-test-data/extras/testTypesSequence2.py similarity index 100% rename from python/test-data/testTypesSequence2.py rename to python/file-test-data/extras/testTypesSequence2.py diff --git a/python/file-test-data/extras/testTypesSubclassing1.err b/python/file-test-data/extras/testTypesSubclassing1.err new file mode 100644 index 00000000..ecfbeb53 --- /dev/null +++ b/python/file-test-data/extras/testTypesSubclassing1.err @@ -0,0 +1,19 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesSubclassing1.py", line 29, in + feedAnimal(dog) + File "file-test-data/extras/testTypesSubclassing1.py", line 26, in feedAnimal + a.feed(AnimalFood('some cat food')) + +WyppTypeError: + +Der Aufruf der Methode `feed` aus Klasse `Dog` erwartet Wert vom Typ `DogFood` als erstes Argument. +Aber der übergebene Wert hat Typ `AnimalFood`. + +## Datei file-test-data/extras/testTypesSubclassing1.py +## Fehlerhafter Aufruf in Zeile 26: + + a.feed(AnimalFood('some cat food')) + +## Typ deklariert in Zeile 22: + + def feed(self, food: DogFood) -> None: \ No newline at end of file diff --git a/python/file-test-data/extras/testTypesSubclassing1.out b/python/file-test-data/extras/testTypesSubclassing1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesSubclassing1.py b/python/file-test-data/extras/testTypesSubclassing1.py similarity index 100% rename from python/test-data/testTypesSubclassing1.py rename to python/file-test-data/extras/testTypesSubclassing1.py diff --git a/python/file-test-data/extras/testTypesTuple1.err b/python/file-test-data/extras/testTypesTuple1.err new file mode 100644 index 00000000..71647939 --- /dev/null +++ b/python/file-test-data/extras/testTypesTuple1.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/testTypesTuple1.py", line 5, in + foo(1) + +WyppTypeError: 1 + +Der Aufruf der Funktion `foo` erwartet Wert vom Typ `tuple[int, ...]` als erstes Argument. +Aber der übergebene Wert hat Typ `int`. + +## Datei file-test-data/extras/testTypesTuple1.py +## Fehlerhafter Aufruf in Zeile 5: + +foo(1) + +## Typ deklariert in Zeile 1: + +def foo(l: tuple[int, ...]) -> int: \ No newline at end of file diff --git a/python/test-data/testTypesTuple1.out b/python/file-test-data/extras/testTypesTuple1.out similarity index 100% rename from python/test-data/testTypesTuple1.out rename to python/file-test-data/extras/testTypesTuple1.out diff --git a/python/test-data/testTypesTuple1.py b/python/file-test-data/extras/testTypesTuple1.py similarity index 100% rename from python/test-data/testTypesTuple1.py rename to python/file-test-data/extras/testTypesTuple1.py diff --git a/python/file-test-data/extras/testTypesWrapperEq_ok.err b/python/file-test-data/extras/testTypesWrapperEq_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesWrapperEq.out b/python/file-test-data/extras/testTypesWrapperEq_ok.out similarity index 100% rename from python/test-data/testTypesWrapperEq.out rename to python/file-test-data/extras/testTypesWrapperEq_ok.out diff --git a/python/test-data/testTypesWrapperEq.py b/python/file-test-data/extras/testTypesWrapperEq_ok.py similarity index 100% rename from python/test-data/testTypesWrapperEq.py rename to python/file-test-data/extras/testTypesWrapperEq_ok.py diff --git a/python/file-test-data/extras/testUnion2_ok.err b/python/file-test-data/extras/testUnion2_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testUnion2.out b/python/file-test-data/extras/testUnion2_ok.out similarity index 100% rename from python/test-data/testUnion2.out rename to python/file-test-data/extras/testUnion2_ok.out diff --git a/python/test-data/testUnion2.py b/python/file-test-data/extras/testUnion2_ok.py similarity index 100% rename from python/test-data/testUnion2.py rename to python/file-test-data/extras/testUnion2_ok.py diff --git a/python/file-test-data/extras/testUnion3_ok.err b/python/file-test-data/extras/testUnion3_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testUnion3.out b/python/file-test-data/extras/testUnion3_ok.out similarity index 100% rename from python/test-data/testUnion3.out rename to python/file-test-data/extras/testUnion3_ok.out diff --git a/python/test-data/testUnion3.py b/python/file-test-data/extras/testUnion3_ok.py similarity index 100% rename from python/test-data/testUnion3.py rename to python/file-test-data/extras/testUnion3_ok.py diff --git a/python/file-test-data/extras/testUnionLiteral_ok.err b/python/file-test-data/extras/testUnionLiteral_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testUnionLiteral_ok.out b/python/file-test-data/extras/testUnionLiteral_ok.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testUnionLiteral.py b/python/file-test-data/extras/testUnionLiteral_ok.py similarity index 100% rename from python/test-data/testUnionLiteral.py rename to python/file-test-data/extras/testUnionLiteral_ok.py diff --git a/python/file-test-data/extras/testUnionOfUnion_ok.err b/python/file-test-data/extras/testUnionOfUnion_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testUnionOfUnion.out b/python/file-test-data/extras/testUnionOfUnion_ok.out similarity index 100% rename from python/test-data/testUnionOfUnion.out rename to python/file-test-data/extras/testUnionOfUnion_ok.out diff --git a/python/test-data/testUnionOfUnion.py b/python/file-test-data/extras/testUnionOfUnion_ok.py similarity index 100% rename from python/test-data/testUnionOfUnion.py rename to python/file-test-data/extras/testUnionOfUnion_ok.py diff --git a/python/file-test-data/extras/testUnion_ok.err b/python/file-test-data/extras/testUnion_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testUnion.out b/python/file-test-data/extras/testUnion_ok.out similarity index 100% rename from python/test-data/testUnion.out rename to python/file-test-data/extras/testUnion_ok.out diff --git a/python/test-data/testUnion.py b/python/file-test-data/extras/testUnion_ok.py similarity index 100% rename from python/test-data/testUnion.py rename to python/file-test-data/extras/testUnion_ok.py diff --git a/python/file-test-data/extras/testWrongKeywordArg.err b/python/file-test-data/extras/testWrongKeywordArg.err new file mode 100644 index 00000000..77c95bd0 --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongKeywordArg.py", line 4, in + foo(x=4) + +WyppTypeError: unbekanntes Schlüsselwort-Argument + +Funktion `foo` akzeptiert kein Schlüsselwort-Argument `x`. + +## Datei file-test-data/extras/testWrongKeywordArg.py +## Fehlerhafter Aufruf in Zeile 4: + +foo(x=4) \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongKeywordArg.out b/python/file-test-data/extras/testWrongKeywordArg.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testWrongKeywordArg.py b/python/file-test-data/extras/testWrongKeywordArg.py similarity index 100% rename from python/test-data/testWrongKeywordArg.py rename to python/file-test-data/extras/testWrongKeywordArg.py diff --git a/python/file-test-data/extras/testWrongKeywordArg2.err b/python/file-test-data/extras/testWrongKeywordArg2.err new file mode 100644 index 00000000..c759db46 --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg2.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongKeywordArg2.py", line 8, in + p = Point(foo=3, y=4) + +WyppTypeError: unbekanntes Schlüsselwort-Argument + +Konstruktor des Records `Point` akzeptiert kein Schlüsselwort-Argument `foo`. + +## Datei file-test-data/extras/testWrongKeywordArg2.py +## Fehlerhafter Aufruf in Zeile 8: + +p = Point(foo=3, y=4) \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongKeywordArg2.out b/python/file-test-data/extras/testWrongKeywordArg2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testWrongKeywordArg2.py b/python/file-test-data/extras/testWrongKeywordArg2.py similarity index 100% rename from python/test-data/testWrongKeywordArg2.py rename to python/file-test-data/extras/testWrongKeywordArg2.py diff --git a/python/file-test-data/extras/testWrongKeywordArg3.err b/python/file-test-data/extras/testWrongKeywordArg3.err new file mode 100644 index 00000000..1053346e --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg3.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongKeywordArg3.py", line 8, in + p = Point(x=3, y=4, z=4) + +WyppTypeError: unbekanntes Schlüsselwort-Argument + +Konstruktor des Records `Point` akzeptiert kein Schlüsselwort-Argument `z`. + +## Datei file-test-data/extras/testWrongKeywordArg3.py +## Fehlerhafter Aufruf in Zeile 8: + +p = Point(x=3, y=4, z=4) \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongKeywordArg3.out b/python/file-test-data/extras/testWrongKeywordArg3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testWrongKeywordArg3.py b/python/file-test-data/extras/testWrongKeywordArg3.py new file mode 100644 index 00000000..54940e6d --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg3.py @@ -0,0 +1,8 @@ +from wypp import * + +@record +class Point: + x: int + y: int + +p = Point(x=3, y=4, z=4) diff --git a/python/file-test-data/extras/testWrongKeywordArg4.err b/python/file-test-data/extras/testWrongKeywordArg4.err new file mode 100644 index 00000000..2163bbd2 --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg4.err @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongKeywordArg4.py", line 4, in + foo(kw=1, x=4) + +WyppTypeError: unbekanntes Schlüsselwort-Argument + +Funktion `foo` akzeptiert kein Schlüsselwort-Argument `x`. + +## Datei file-test-data/extras/testWrongKeywordArg4.py +## Fehlerhafter Aufruf in Zeile 4: + +foo(kw=1, x=4) \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongKeywordArg4.out b/python/file-test-data/extras/testWrongKeywordArg4.out new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testWrongKeywordArg4.py b/python/file-test-data/extras/testWrongKeywordArg4.py new file mode 100644 index 00000000..5eae9fd0 --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg4.py @@ -0,0 +1,4 @@ +def foo(kw: int) -> int: + return kw + +foo(kw=1, x=4) diff --git a/python/file-test-data/extras/testWrongKeywordArg5.err b/python/file-test-data/extras/testWrongKeywordArg5.err new file mode 100644 index 00000000..0c2390bf --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg5.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongKeywordArg5.py", line 4, in + foo() + +WyppTypeError: Anzahl der Argument passt nicht + +Funktion `foo` benötigt 1 Argument. +Gegeben: keine Argumente + +## Datei file-test-data/extras/testWrongKeywordArg5.py +## Aufruf in Zeile 4: + +foo() \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongKeywordArg5.out b/python/file-test-data/extras/testWrongKeywordArg5.out new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/extras/testWrongKeywordArg5.py b/python/file-test-data/extras/testWrongKeywordArg5.py new file mode 100644 index 00000000..05c01a30 --- /dev/null +++ b/python/file-test-data/extras/testWrongKeywordArg5.py @@ -0,0 +1,4 @@ +def foo(kw: int) -> int: + return kw + +foo() diff --git a/python/file-test-data/extras/testWrongNumOfArguments.err b/python/file-test-data/extras/testWrongNumOfArguments.err new file mode 100644 index 00000000..6e4ffebd --- /dev/null +++ b/python/file-test-data/extras/testWrongNumOfArguments.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongNumOfArguments.py", line 4, in + foo(1) + +WyppTypeError: Anzahl der Argument passt nicht + +Funktion `foo` benötigt 2 Argumente. +Gegeben: 1 Argument + +## Datei file-test-data/extras/testWrongNumOfArguments.py +## Aufruf in Zeile 4: + +foo(1) \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongNumOfArguments.out b/python/file-test-data/extras/testWrongNumOfArguments.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testWrongNumOfArguments.py b/python/file-test-data/extras/testWrongNumOfArguments.py similarity index 100% rename from python/test-data/testWrongNumOfArguments.py rename to python/file-test-data/extras/testWrongNumOfArguments.py diff --git a/python/file-test-data/extras/testWrongNumOfArguments2.err b/python/file-test-data/extras/testWrongNumOfArguments2.err new file mode 100644 index 00000000..3c767145 --- /dev/null +++ b/python/file-test-data/extras/testWrongNumOfArguments2.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/extras/testWrongNumOfArguments2.py", line 6, in + c.foo(1) + +WyppTypeError: Anzahl der Argument passt nicht + +Methode `foo` der Klasse `C` benötigt 2 Argumente. +Gegeben: 1 Argument + +## Datei file-test-data/extras/testWrongNumOfArguments2.py +## Aufruf in Zeile 6: + +c.foo(1) \ No newline at end of file diff --git a/python/file-test-data/extras/testWrongNumOfArguments2.out b/python/file-test-data/extras/testWrongNumOfArguments2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testWrongNumOfArguments2.py b/python/file-test-data/extras/testWrongNumOfArguments2.py similarity index 100% rename from python/test-data/testWrongNumOfArguments2.py rename to python/file-test-data/extras/testWrongNumOfArguments2.py diff --git a/python/file-test-data/extras/wrong-caused-by.err b/python/file-test-data/extras/wrong-caused-by.err new file mode 100644 index 00000000..ca0e627c --- /dev/null +++ b/python/file-test-data/extras/wrong-caused-by.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/wrong-caused-by.py", line 31, in + mainStreetM.turnIntoStreet(redCarM) + +WyppTypeError: CarM(licensePlate='OG PY 123', color='rot') + +Der Aufruf der Methode `turnIntoStreet` aus Klasse `StreetM` erwartet Wert vom Typ `Car` als erstes Argument. +Aber der übergebene Wert hat Typ `CarM`. + +## Datei file-test-data/extras/wrong-caused-by.py +## Fehlerhafter Aufruf in Zeile 31: + +mainStreetM.turnIntoStreet(redCarM) + +## Typ deklariert in Zeile 10: + + def turnIntoStreet(self: StreetM, car: Car) -> None: \ No newline at end of file diff --git a/python/file-test-data/extras/wrong-caused-by.out b/python/file-test-data/extras/wrong-caused-by.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/wrong-caused-by.py b/python/file-test-data/extras/wrong-caused-by.py similarity index 100% rename from python/test-data/wrong-caused-by.py rename to python/file-test-data/extras/wrong-caused-by.py diff --git a/python/test-data/fileWithBothImports.py b/python/file-test-data/imports/fileWithBothImports.py similarity index 100% rename from python/test-data/fileWithBothImports.py rename to python/file-test-data/imports/fileWithBothImports.py diff --git a/python/test-data/fileWithImport.py b/python/file-test-data/imports/fileWithImport.py similarity index 100% rename from python/test-data/fileWithImport.py rename to python/file-test-data/imports/fileWithImport.py diff --git a/python/test-data/fileWithoutImport.py b/python/file-test-data/imports/fileWithoutImport.py similarity index 100% rename from python/test-data/fileWithoutImport.py rename to python/file-test-data/imports/fileWithoutImport.py diff --git a/python/test-data/localMod.py b/python/file-test-data/imports/localMod.py similarity index 100% rename from python/test-data/localMod.py rename to python/file-test-data/imports/localMod.py diff --git a/python/test-data/modules/B/mod.py b/python/file-test-data/modules/B/mod.py similarity index 100% rename from python/test-data/modules/B/mod.py rename to python/file-test-data/modules/B/mod.py diff --git a/python/fileTests b/python/fileTests deleted file mode 100755 index 807f8523..00000000 --- a/python/fileTests +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env bash - -# FIXME (sw, 2021-09-16): we should migrate this mess to tests/integrationTests.py! - -set -e - -cd $(dirname $0) - -d=$(pwd) -t=$(mktemp) - -if [ "$1" == "--help" ]; then - echo "USAGE: $0 [--start-at] [FILE]" - exit 1 -fi - -startAt=false -if [ "$1" == "--start-at" ]; then - startAt=true - shift -fi -FILE="$1" - -started=false - -function skip() -{ - # return 1 if test should be run - # return 0 if test should be skipped - if [ ! -z "$FILE" ]; then - if [ "$startAt" == true -a "$started" == true ]; then - return 1 - fi - if [ "$FILE" == "$1" ]; then - started=true - return 1 - fi - return 0 - else - return 1 - fi -} - -echo "Running file tests, siteDir=$siteDir ..." -echo "Writing logs to $t" - -#PY="coverage run --append" -PY=python3 - -function check() -{ - if skip "$1"; then - return - fi - echo "Checking with $1" - pushd /tmp > /dev/null - dir=$(mktemp -d) - echo "Installing into $dir ..." - echo "Run 1" - PYTHONPATH=$dir WYPP_INSTALL_DIR=$dir $PY $d/src/runYourProgram.py --install-mode install --quiet --check $d/"$1" >> "$t" - rm -rf "$dir" - mkdir "$dir" - echo "Run 2" - PYTHONPATH=$dir WYPP_INSTALL_DIR=$dir $PY $d/src/runYourProgram.py --install-mode install --quiet --check $d/"$1" >> "$t" - echo "Run 3" - PYTHONPATH=$dir WYPP_INSTALL_DIR=$dir $PY $d/src/runYourProgram.py --check --install-mode assertInstall --quiet $d/"$1" >> "$t" - rm -f "$dir/untypy/__init__.py" - echo "Run 4" - PYTHONPATH=$dir WYPP_INSTALL_DIR=$dir $PY $d/src/runYourProgram.py --install-mode install --quiet --check $d/"$1" >> "$t" - echo "Run 5" - PYTHONPATH=$dir WYPP_INSTALL_DIR=$dir $PY $d/src/runYourProgram.py --check --install-mode assertInstall --quiet $d/"$1" >> "$t" - popd > /dev/null - rm -rf "$dir" -} -check test-data/fileWithImport.py -check test-data/fileWithoutImport.py -check test-data/fileWithBothImports.py -check test-data/fileWithRecursiveTypes.py - -version=$(python3 -V | sed 's/Python //g' | sed 's/\.[^.]*$//g') -fullVersion=$(python3 -V | sed 's/Python //g') - -function fix_output() -{ - sed 's/at 0x[0-9a-f][0-9a-f]*>/at 0x00>/g' | \ - sed 's| File "/[^"]*"| File ""|g' -} - -# First argument: whether to do type checking or not -# Second argument: expected exit code. If given as X:Y, then X is the exit code with active -# type checking, and Y is the exit code without type checking. -# Third argument: input file -# The remaining arguments are passed to src/runYourProgram.py -function checkWithOutputAux() -{ - local tycheck="$1" - local expectedEcode=$2 - local file="$3" - if skip "$file"; then - return - fi - echo "Checking $file" - shift 3 - tycheckOpt="" - suffixes="${fullVersion} ${version}" - if [ "$tycheck" == "no" ]; then - tycheckOpt="--no-typechecking" - suffixes="${fullVersion}-notypes ${fullVersion} ${version}-notypes ${version} notypes" - fi - if echo "$expectedEcode" | grep ':' > /dev/null; then - if [ "$tycheck" == "no" ]; then - expectedEcode=$(echo "$expectedEcode" | sed 's/^.*://g') - else - expectedEcode=$(echo "$expectedEcode" | sed 's/:.*$//g') - fi - fi - local expectedOut="${file%.py}.out" - if [ ! -f "$expectedOut" ]; then - echo "File $expectedOut does not exist" - exit 1 - fi - local expectedErr="${file%.py}.err" - if [ ! -f "$expectedErr" ]; then - echo "File $expectedErr does not exist" - exit 1 - fi - for suf in $suffixes; do - if [ -e "${expectedOut}-${suf}" ]; then - expectedOut="${expectedOut}-${suf}" - fi - if [ -e "${expectedErr}-${suf}" ]; then - expectedErr="${expectedErr}-${suf}" - fi - done - local t=$(mktemp) - local out=$t.out - local err=$t.err - local errtemp=$t.errtemp - local outtemp=$t.outtemp - set +e - echo "Checking $file (typecheck: $tycheck)" - local p=$d/site-lib - if [ ! -z $PP ]; then - p=$p:$PP - fi - PYTHONPATH=$p $PY $d/src/runYourProgram.py --quiet $tycheckOpt "$file" "$@" 2>> "$errtemp" > "$outtemp" - ecode=$? - set -e - cat "$errtemp" | fix_output > "$err" - cat "$outtemp" | fix_output > "$out" - if [ $ecode != $expectedEcode ]; then - echo "Expected exit code $expectedEcode got $ecode for test $file" - echo "Stderr:" - cat "$err" - echo "Stdout:" - cat "$out" - exit 1 - fi - if ! diff -u $expectedOut $out; then - echo "Wrong output on stdout for $file ($expectedOut contains the expected output)" - echo "Full output: $out" - exit 1 - fi - if ! diff -u $expectedErr $err; then - echo "Wrong output on stderr for $file ($expectedErr contains the expected output)" - echo "Full output: $err" - exit 1 - fi - rm -f "$out" - rm -f "$err" -} - -function checkWithOutput() -{ - checkWithOutputAux yes "$@" - checkWithOutputAux no "$@" -} - -checkWithOutput 1 test-data/testTraceback.py -checkWithOutput 1 test-data/testTraceback2.py -checkWithOutput 1 test-data/testTraceback3.py -checkWithOutput 0 test-data/testArgs.py ARG_1 ARG_2 -checkWithOutput 0 test-data/printModuleName.py -checkWithOutput 0 test-data/printModuleNameImport.py -checkWithOutput 1 test-data/testTypes1.py -checkWithOutput 1 test-data/testIndexError.py -checkWithOutput 1 test-data/testWrapperError.py -checkWithOutput 0 test-data/testCheckFail.py -checkWithOutput 1:0 test-data/testTypes2.py -checkWithOutputAux yes 1 test-data/testABCMeta.py -checkWithOutputAux yes 0 test-data/testClassHierarchy.py -checkWithOutputAux yes 1 test-data/testTypesCollections1.py -checkWithOutputAux yes 1 test-data/testTypesCollections2.py -checkWithOutputAux yes 1 test-data/testTypesCollections3.py # see #5 -checkWithOutputAux yes 1 test-data/testTypesCollections4.py -checkWithOutputAux yes 1 test-data/testTypesProtos1.py -checkWithOutputAux yes 1 test-data/testTypesProtos2.py -checkWithOutputAux yes 1 test-data/testTypesProtos3.py -checkWithOutputAux yes 1 test-data/testTypesProtos4.py -checkWithOutputAux yes 1 test-data/testTypesSubclassing1.py -checkWithOutputAux yes 1 test-data/testTypesHigherOrderFuns.py -checkWithOutputAux yes 0 test-data/testTypesHigherOrderFuns2.py # see #6 -checkWithOutputAux yes 1 test-data/testTypesHigherOrderFuns3.py -checkWithOutputAux yes 0 test-data/testTypesHigherOrderFuns4.py -checkWithOutputAux yes 0 test-data/testTypesHigherOrderFuns5.py -checkWithOutputAux yes 1 test-data/testTypesRecordInheritance.py -checkWithOutputAux yes 1 test-data/testRecordSetTypes.py -checkWithOutputAux yes 1 test-data/testRecordSetTypeForwardRef.py -checkWithOutputAux yes 0 test-data/testForwardRef.py -checkWithOutputAux yes 0 test-data/testForwardRef1.py -checkWithOutputAux yes 1 test-data/testForwardRef2.py -checkWithOutputAux yes 0 test-data/testForwardRef3.py -checkWithOutputAux yes 1 test-data/testForwardRef4.py -checkWithOutputAux yes 1 test-data/testForwardRef5.py -checkWithOutputAux yes 0 test-data/testForwardRef6.py -checkWithOutputAux yes 1 test-data/testHintParentheses1.py -checkWithOutputAux yes 1 test-data/testHintParentheses2.py -checkWithOutputAux yes 1 test-data/testHintParentheses3.py -checkWithOutputAux yes 1 test-data/testTypesReturn.py -checkWithOutputAux yes 1 test-data/testMissingReturn.py -checkWithOutputAux yes 1 test-data/testTypesSequence1.py -checkWithOutputAux yes 1 test-data/testTypesSequence2.py -checkWithOutputAux yes 1 test-data/testTypesTuple1.py -checkWithOutputAux yes 1 test-data/wrong-caused-by.py -checkWithOutputAux yes 1 test-data/declared-at-missing.py -checkWithOutputAux yes 1 test-data/testTypesSet1.py -checkWithOutputAux yes 1 test-data/testTypesSet2.py -checkWithOutputAux yes 1 test-data/testTypesSet3.py -checkWithOutputAux yes 1 test-data/testTypesSet4.py -checkWithOutputAux yes 1 test-data/testTypesDict1.py -checkWithOutputAux yes 1 test-data/testTypesDict2.py -checkWithOutputAux yes 1 test-data/testTypesDict3.py -checkWithOutputAux yes 1 test-data/testTypesDict4.py -checkWithOutputAux yes 1 test-data/testIterableImplicitAny.py -checkWithOutputAux yes 0 test-data/testDoubleWrappingDicts.py -checkWithOutputAux yes 0 test-data/testClassRecursion.py -checkWithOutputAux yes 1 test-data/testWrongNumOfArguments.py -checkWithOutputAux yes 1 test-data/testWrongNumOfArguments2.py -checkWithOutputAux yes 0 test-data/testLiteralInstanceOf.py -checkWithOutputAux yes 1 test-data/testWrongKeywordArg.py -checkWithOutputAux yes 1 test-data/testWrongKeywordArg2.py -checkWithOutputAux yes 0 test-data/testComplex.py -checkWithOutputAux yes 0 test-data/testUnionLiteral.py -checkWithOutputAux yes 0 test-data/testCheck.py -checkWithOutputAux yes 0 test-data/testNameErrorBug.py -checkWithOutputAux yes 0 test-data/testOriginalTypeNames.py -checkWithOutputAux yes 0 test-data/testDeepEqBug.py -checkWithOutputAux yes 1 test-data/testLockFactory.py -checkWithOutputAux yes 1 test-data/testLockFactory2.py -checkWithOutputAux yes 1 test-data/testGetSource.py -checkWithOutputAux yes 0 test-data/testDict.py #see #87 -checkWithOutputAux yes 0 test-data/testFunEq.py #see #78 -checkWithOutputAux yes 0 test-data/testBugSliceIndices.py #see #92 -checkWithOutputAux yes 0 test-data/testABC.py -checkWithOutputAux yes 0 test-data/testTypesWrapperEq.py -checkWithOutputAux yes 0 test-data/testTypesProtos5.py -checkWithOutputAux yes 1 test-data/testTypesProtos6.py -checkWithOutputAux yes 1 test-data/testTypesProtos7.py -checkWithOutputAux yes 1 test-data/testTypesProtos8.py -checkWithOutputAux yes 1 test-data/testTypesProtos9.py -checkWithOutputAux yes 0 test-data/testIterable1.py -checkWithOutputAux yes 0 test-data/testIterable2.py -checkWithOutputAux yes 0 test-data/testIterable3.py -checkWithOutputAux yes 0 test-data/testIterable4.py -checkWithOutputAux yes 0 test-data/testIterable5.py -checkWithOutputAux yes 0 test-data/testIterable6.py -checkWithOutputAux yes 1 test-data/testIterable7.py -checkWithOutputAux yes 0 test-data/testIterator.py -checkWithOutputAux yes 0 test-data/testIterator2.py -checkWithOutputAux yes 0 test-data/testIterator3.py -checkWithOutputAux yes 0 test-data/testIterator4.py -checkWithOutputAux yes 0 test-data/testConcat.py -checkWithOutputAux yes 0 test-data/testCopy.py -checkWithOutputAux yes 0 test-data/testHof.py -checkWithOutputAux yes 0 test-data/testIndexSeq.py -checkWithOutputAux yes 0 test-data/testWrap.py -checkWithOutputAux yes 0 test-data/testWrap2.py -checkWithOutputAux yes 1 test-data/testTodo.py -checkWithOutputAux yes 1 test-data/testImpossible.py -checkWithOutputAux yes 1 test-data/testInvalidLiteral.py -checkWithOutputAux yes 0 test-data/admin.py -PP=test-data/modules/B checkWithOutputAux yes 1 test-data/modules/A/main.py \ - --extra-dir test-data/modules/B -checkWithOutputAux yes 0 test-data/testUnion.py -checkWithOutputAux yes 0 test-data/testUnion2.py -checkWithOutputAux yes 0 test-data/testUnion3.py -checkWithOutputAux yes 1 test-data/testLiteral1.py -checkWithOutputAux yes 0 test-data/testForwardTypeInRecord.py -checkWithOutputAux yes 0 test-data/testForwardTypeInRecord2.py -checkWithOutputAux yes 0 test-data/testUnionOfUnion.py -checkWithOutputAux yes 1 test-data/testRecordTypes.py -checkWithOutputAux yes 0 test-data/testDisappearingObject_01.py -checkWithOutputAux yes 0 test-data/testDisappearingObject_02.py - -function is_min_version() -{ - min="$1" - v="$2" - smaller=$(echo -e "$min\n$v" | sort -V | head -1) - if [ "$smaller" == "$min" ]; then - return 0; - else - return 1; - fi -} -version=$(python3 --version | sed 's/Python //g') - -if is_min_version "3.12" "$version"; then - checkWithOutput 0 test-data/testTypeKeyword.py -fi diff --git a/python/fileTests.py b/python/fileTests.py new file mode 100644 index 00000000..32f7b345 --- /dev/null +++ b/python/fileTests.py @@ -0,0 +1,17 @@ +from pathlib import Path +from fileTestsLib import * + +directories = [Path("file-test-data/basics"), + Path("file-test-data/extras")] + +#directories = [Path("file-test-data/basics")] +#directories = [Path("file-test-data/extras")] + +for d in directories: + for file in d.iterdir(): + if file.is_file(): + name = file.as_posix() + if name.endswith('.py'): + check(name) + +globalCtx.results.finish() diff --git a/python/fileTestsLib.py b/python/fileTestsLib.py new file mode 100644 index 00000000..a889445e --- /dev/null +++ b/python/fileTestsLib.py @@ -0,0 +1,448 @@ +from __future__ import annotations +from dataclasses import dataclass +import os +from typing import * +import sys +import subprocess +import tempfile +import argparse +import re +import shutil +import json +import re + +GLOBAL_CHECK_OUTPUTS = True + +GLOBAL_RECORD_ALL = False # Should be False, write actual output to all expected output files + +@dataclass(frozen=True) +class TestOpts: + cmd: str + baseDir: str + startAt: Optional[str] + only: Optional[str] + keepGoing: bool + record: Optional[str] + lang: Optional[str] + +def parseArgs() -> TestOpts: + parser = argparse.ArgumentParser( + description="Run tests with specified options.", + usage="USAGE: ./fileTests OPTIONS" + ) + + # Define the command-line arguments + parser.add_argument("--start-at", type=str, help="Start with test in FILE") + parser.add_argument("--only", type=str, help="Run only the test in FILE") + parser.add_argument("--continue", action="store_true", + dest="keepGoing", default=False, + help="Continue with tests after first error") + parser.add_argument('--record', dest='record', + type=str, help='Record the expected output for the given file.') + parser.add_argument('--lang', dest='lang', + type=str, help='Display error messages in this language (either en or de, only for recording).') + + # Parse the arguments + args = parser.parse_args() + + scriptDir = os.path.dirname(__file__) + return TestOpts( + cmd=f'{scriptDir}/code/wypp/runYourProgram.py', + baseDir=scriptDir, + startAt=args.start_at, + only=args.only, + keepGoing=args.keepGoing, + record=args.record, + lang=args.lang + ) + +defaultLang = 'de' + +TestStatus = Literal['passed', 'failed', 'skipped'] + +@dataclass +class TestResults: + passed: list[str] + failed: list[str] + skipped: list[str] + def storeTestResult(self, testFail: str, result: TestStatus): + if result == 'passed': + self.passed.append(testFail) + elif result == 'failed': + self.failed.append(testFail) + elif result == 'skipped': + self.skipped.append(testFail) + def finish(self): + total = len(self.passed) + len(self.skipped) + len(self.failed) + print() + print(80 * '-') + print("Tests finished") + print() + print(f"Total: {total}") + print(f"Passed: {len(self.passed)}") + print(f"Skipped: {len(self.skipped)}") + print(f"Failed: {len(self.failed)}") + if self.failed: + print() + print("Failed tests:") + for test in self.failed: + print(f" {test}") + sys.exit(1) + +@dataclass(frozen=True) +class TestContext: + opts: TestOpts + results: TestResults + +globalCtx = TestContext( + opts=parseArgs(), + results=TestResults(passed=[], failed=[], skipped=[]) +) + +def readFile(filePath: str) -> str: + with open(filePath, "r") as f: + return f.read() + +def readFileIfExists(filePath: str) -> str: + if not os.path.exists(filePath): + return '' + else: + return readFile(filePath) + +def getVersionedFile(base: str, typcheck: bool, lang: Optional[str]) -> str: + if lang is None: + lang = defaultLang + if lang != defaultLang: + base = f'{base}_{lang}' + v = sys.version_info + suffixes = [f'{v.major}.{v.minor}', f'{v.major}.{v.minor}.{v.micro}'] + if not typcheck: + l = [] + for x in suffixes: + l.append(f'{x}-notypes') + l.append(x) + l.append('notypes') + suffixes = l + for suffix in suffixes: + filePath = f"{base}-{suffix}" + if os.path.exists(filePath): + return filePath + return base + +_started = False +def shouldSkip(testFile: str, ctx: TestContext, minVersion: Optional[tuple[int, int]]) -> bool: + """ + Determines if a test should be skipped based on the context and minimum version. + """ + global _started + opts = ctx.opts + if opts.startAt: + if _started: + return False + elif testFile == opts.startAt: + _started = True + return False + else: + return True + if opts.only and testFile != opts.only: + return True + if minVersion: + v = sys.version_info + if (v.major, v.minor) < minVersion: + return True + return False + +def checkOutputOk(testFile: str, outputType: str, expectedFile: str, actualFile: str) -> bool: + expected = readFileIfExists(expectedFile).strip() + actual = readFileIfExists(actualFile).strip() + if expected != actual: + print(f"Test {testFile} {outputType} output mismatch:") + subprocess.run(['diff', '-u', expectedFile, actualFile]) + if GLOBAL_RECORD_ALL: + with open(expectedFile, 'w') as f: + f.write(actual) + return True + else: + return False + else: + return True + +def checkInstall(testFile: str, ctx: TestContext=globalCtx): + if shouldSkip(testFile, ctx, None): + ctx.results.storeTestResult(testFile, 'skipped') + return + with tempfile.TemporaryDirectory() as d: + def run(args: list[str]): + cmd = [sys.executable, ctx.opts.cmd, '--quiet'] + cmd.extend(args) + cmd.append(os.path.join(ctx.opts.baseDir, testFile)) + env = os.environ.copy() + env['PYTHONPATH'] = d + env['WYPP_INSTALL_DIR'] = d + subprocess.run( + cmd, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=env + ) + sys.stdout.write(f'Install test {testFile} ...') + run(['--install-mode', 'install', '--check']) + subprocess.run(f'rm -rf {d} && mkdir {d}', shell=True, check=True) + run(['--install-mode', 'install', '--check']) + run(['--check', '--install-mode', 'assertInstall']) + subprocess.run(f'rm -f {d}/untypy/__init__.py', shell=True, check=True) + run(['--install-mode', 'install', '--check']) + run(['--check', '--install-mode', 'assertInstall']) + sys.stdout.write(' OK\n') + +def fixOutput(filePath: str): + """ + Fixes the output file by removing specific lines and patterns. + """ + content = readFile(filePath) + content = re.sub(r'at 0x[0-9a-f][0-9a-f]*>', 'at 0x00>', content, flags=re.MULTILINE) # Remove memory addresses + content = re.sub(r' File "/[^"]*/([^"/]+)", line \d+', ' File "\\1", line ?', content, flags=re.MULTILINE) # Remove absolute file paths from traceback + with open(filePath, 'w') as f: + f.write(content) + +def readAnswer(question: str, allowed: list[str]) -> str: + while True: + answer = input(question) + if answer in allowed: + return answer + print(f'Answer must be one of {allowed}. Try again!') + +def _runTest(testFile: str, + exitCode: int, + typecheck: bool, + args: list[str], + actualStdoutFile: str, + actualStderrFile: str, + pythonPath: list[str], + what: str, + lang: str, + ctx: TestContext) -> Literal['failed'] | None: + # Prepare the command + cmd = [sys.executable, ctx.opts.cmd, '--quiet'] + if not typecheck: + cmd.append('--no-typechecking') + cmd.append(testFile) + cmd.append('--lang') + cmd.append(lang) + cmd.extend(args) + env = os.environ.copy() + env['PYTHONPATH'] = os.pathsep.join([os.path.join(ctx.opts.baseDir, 'code')] + pythonPath) + env['WYPP_UNDER_TEST'] = 'True' + with open(actualStdoutFile, 'w') as stdoutFile, \ + open(actualStderrFile, 'w') as stderrFile: + # Run the command + result = subprocess.run( + cmd, + stdout=stdoutFile, + stderr=stderrFile, + text=True, + env=env + ) + # Check exit code + if result.returncode != exitCode: + print(f"Test {testFile}{what} failed: Expected exit code {exitCode}, got {result.returncode}") + return 'failed' + +def _checkForLang(testFile: str, + exitCode: int, + typecheck: bool, + args: list[str], + pythonPath: list[str], + checkOutputs: bool, + lang: str, + ctx: TestContext, + what: str) -> TestStatus: + # Prepare expected output files + baseFile = os.path.splitext(testFile)[0] + expectedStdoutFile = getVersionedFile(f"{baseFile}.out", typcheck=typecheck, lang=lang) + expectedStderrFile = getVersionedFile(f"{baseFile}.err", typcheck=typecheck, lang=lang) + + with tempfile.TemporaryDirectory() as d: + actualStdoutFile = os.path.join(d, 'stdout.txt') + actualStderrFile = os.path.join(d, 'stderr.txt') + r = _runTest(testFile, exitCode, typecheck, args, actualStdoutFile, actualStderrFile, + pythonPath, what, lang, ctx) + if r is not None: + return r + + fixOutput(actualStdoutFile) + fixOutput(actualStderrFile) + + # Checkout outputs + if checkOutputs and GLOBAL_CHECK_OUTPUTS: + if not checkOutputOk(testFile + what, 'stdout', expectedStdoutFile, actualStdoutFile): + return 'failed' + if not checkOutputOk(testFile + what, 'stderr', expectedStderrFile, actualStderrFile): + return 'failed' + + # If all checks passed + whatLang = '' + if lang != defaultLang: + whatLang = f' ({lang})' + print(f"{testFile}{what}{whatLang} OK") + return 'passed' + +def _check(testFile: str, + exitCode: int, + typecheck: bool, + args: list[str], + pythonPath: list[str], + minVersion: Optional[tuple[int, int]], + checkOutputs: bool, + ctx: TestContext, + what: str) -> TestStatus: + status1 = _checkForLang(testFile, exitCode, typecheck, args, pythonPath, checkOutputs, defaultLang, ctx, what) + baseFile = os.path.splitext(testFile)[0] + enOut = getVersionedFile(f"{baseFile}.out", typcheck=typecheck, lang='en') + enErr = getVersionedFile(f"{baseFile}.err", typcheck=typecheck, lang='en') + if os.path.exists(enOut) or os.path.exists(enErr): + status2 = _checkForLang(testFile, exitCode, typecheck, args, pythonPath, checkOutputs, 'en', ctx, what) + else: + status2 = 'passed' + if status1 != 'passed': + return status1 + elif status2 != 'passed': + return status2 + else: + return 'passed' + +def guessExitCode(testFile: str) -> int: + return 0 if testFile.endswith('_ok.py') else 1 + +_CONFIG_RE = re.compile(r'^# WYPP_TEST_CONFIG:\s*(\{.*\})\s*$') + +@dataclass +class WyppTestConfig: + typecheck: Literal[True, False, "both"] + args: list[str] + pythonPath: Optional[str] + @staticmethod + def default() -> WyppTestConfig: + return WyppTestConfig(typecheck=True, args=[], pythonPath=None) + +def readWyppTestConfig(path: str, *, max_lines: int = 5) -> WyppTestConfig: + """ + Read a line like `# WYPP_TEST_CONFIG: {"typecheck": false}` from the first + `max_lines` lines of the file at `path` and return it as a dict. + Returns {} if not present. + """ + validKeys = ['typecheck', 'args', 'pythonPath'] + with open(path, "r", encoding="utf-8") as f: + for lineno in range(1, max_lines + 1): + line = f.readline() + if not line: + break + m = _CONFIG_RE.match(line) + if m: + payload = m.group(1) + j = json.loads(payload) + for k in j: + if k not in validKeys: + raise ValueError(f'Unknown key {k} in config for file {path}') + typecheck = j.get('typecheck', True) + args = j.get('args', []) + pythonPath = j.get('pythonPath') + return WyppTestConfig(typecheck=typecheck, args=args, pythonPath=pythonPath) + return WyppTestConfig.default() + +def checkNoConfig(testFile: str, + exitCode: int = 1, + typecheck: bool = True, + args: list[str] = [], + pythonPath: list[str] = [], + minVersion: Optional[tuple[int, int]] = None, + checkOutputs: bool = True, + ctx: TestContext = globalCtx, + what: str = ''): + if guessExitCode(testFile) == 0: + exitCode = 0 + status = _check(testFile, exitCode, typecheck, args, pythonPath, minVersion, checkOutputs, ctx, what) + ctx.results.storeTestResult(testFile, status) + if status == 'failed': + if not ctx.opts.keepGoing: + ctx.results.finish() + +def check(testFile: str, + exitCode: int = 1, + minVersion: Optional[tuple[int, int]] = None, + checkOutputs: bool = True, + ctx: TestContext = globalCtx,): + if shouldSkip(testFile, ctx, minVersion): + return 'skipped' + cfg = readWyppTestConfig(testFile) + args = cfg.args + pythonPath = [] + if cfg.pythonPath: + pythonPath = cfg.pythonPath.split(':') + if cfg.typecheck == 'both': + checkNoConfig(testFile, exitCode, typecheck=True, args=args, + pythonPath=pythonPath, minVersion=minVersion, checkOutputs=checkOutputs, + ctx=ctx, what=' (typecheck)') + checkNoConfig(testFile, exitCode, typecheck=False, args=args, + pythonPath=pythonPath, minVersion=minVersion, checkOutputs=checkOutputs, + ctx=ctx, what=' (no typecheck)') + else: + what = ' (no typecheck)' if not cfg.typecheck else '' + checkNoConfig(testFile, exitCode, typecheck=cfg.typecheck, args=args, + pythonPath=pythonPath, minVersion=minVersion, checkOutputs=checkOutputs, + ctx=ctx, what=what) + +def checkBasic(testFile: str, ctx: TestContext = globalCtx): + check(testFile, checkOutputs=False, ctx=ctx) + +def record(testFile: str): + """ + Runs filePath and stores the output in the expected files. + """ + baseFile = os.path.splitext(testFile)[0] + exitCode = guessExitCode(testFile) + cfg = readWyppTestConfig(testFile) + typecheck = cfg.typecheck + if typecheck == 'both': + typecheck = True + args = cfg.args + pythonPath = [] + if cfg.pythonPath: + pythonPath = cfg.pythonPath.split(':') + what = '' + ctx = globalCtx + def display(filename: str, where: str): + x = readFile(filename) + if x: + print(f'--- Output on {where} ---') + print(x) + print('------------------------') + else: + print(f'No output on {where}') + with tempfile.TemporaryDirectory() as d: + actualStdoutFile = os.path.join(d, 'stdout.txt') + actualStderrFile = os.path.join(d, 'stderr.txt') + result = _runTest(testFile, exitCode, typecheck, args, actualStdoutFile, actualStderrFile, + pythonPath, what, ctx.opts.lang or defaultLang, ctx) + if result is not None: + print(f'Test did not produce the expected exit code. Aborting') + sys.exit(1) + display(actualStdoutFile, 'stdout') + display(actualStderrFile, 'stderr') + answer = readAnswer('Store the output as the new expected output? (y/n) ', ['y', 'n']) + if answer: + fixOutput(actualStdoutFile) + fixOutput(actualStderrFile) + expectedStdoutFile = getVersionedFile(f"{baseFile}.out", typcheck=typecheck, lang=ctx.opts.lang) + expectedStderrFile = getVersionedFile(f"{baseFile}.err", typcheck=typecheck, lang=ctx.opts.lang) + shutil.copy(actualStdoutFile, expectedStdoutFile) + shutil.copy(actualStderrFile, expectedStderrFile) + print(f'Stored expected output in {expectedStdoutFile} and {expectedStderrFile}') + else: + print('Aborting') + +if __name__ == '__main__': + if globalCtx.opts.record is not None: + record(globalCtx.opts.record) + sys.exit(0) diff --git a/python/test-data/repl-test-checks.py b/python/integration-test-data/repl-test-checks.py similarity index 100% rename from python/test-data/repl-test-checks.py rename to python/integration-test-data/repl-test-checks.py diff --git a/python/test-data/repl-test-lib.py b/python/integration-test-data/repl-test-lib.py similarity index 100% rename from python/test-data/repl-test-lib.py rename to python/integration-test-data/repl-test-lib.py diff --git a/python/test-data/scope-bug-peter.py b/python/integration-test-data/scope-bug-peter.py similarity index 100% rename from python/test-data/scope-bug-peter.py rename to python/integration-test-data/scope-bug-peter.py diff --git a/python/test-data/student-submission-bad.py b/python/integration-test-data/student-submission-bad.py similarity index 100% rename from python/test-data/student-submission-bad.py rename to python/integration-test-data/student-submission-bad.py diff --git a/python/test-data/student-submission-tests-tyerror.py b/python/integration-test-data/student-submission-tests-tyerror.py similarity index 100% rename from python/test-data/student-submission-tests-tyerror.py rename to python/integration-test-data/student-submission-tests-tyerror.py diff --git a/python/test-data/student-submission-tests.py b/python/integration-test-data/student-submission-tests.py similarity index 100% rename from python/test-data/student-submission-tests.py rename to python/integration-test-data/student-submission-tests.py diff --git a/python/test-data/student-submission-tyerror.py b/python/integration-test-data/student-submission-tyerror.py similarity index 100% rename from python/test-data/student-submission-tyerror.py rename to python/integration-test-data/student-submission-tyerror.py diff --git a/python/test-data/student-submission.py b/python/integration-test-data/student-submission.py similarity index 100% rename from python/test-data/student-submission.py rename to python/integration-test-data/student-submission.py diff --git a/python/integration-test-data/testTypes2.py b/python/integration-test-data/testTypes2.py new file mode 100644 index 00000000..593392d3 --- /dev/null +++ b/python/integration-test-data/testTypes2.py @@ -0,0 +1,4 @@ +def inc(x: int) -> int: + return x + +inc("1") diff --git a/python/test-data/testTypes3.py b/python/integration-test-data/testTypes3.py similarity index 100% rename from python/test-data/testTypes3.py rename to python/integration-test-data/testTypes3.py diff --git a/python/test-data/testTypesInteractive.py b/python/integration-test-data/testTypesInteractive.py similarity index 100% rename from python/test-data/testTypesInteractive.py rename to python/integration-test-data/testTypesInteractive.py diff --git a/python/test-data/testTypesSet1.py b/python/integration-test-data/testTypesSet1_ok.py similarity index 100% rename from python/test-data/testTypesSet1.py rename to python/integration-test-data/testTypesSet1_ok.py diff --git a/python/integration-tests/shell.py b/python/integration-tests/shell.py index 321cbb0c..fac20c29 100644 --- a/python/integration-tests/shell.py +++ b/python/integration-tests/shell.py @@ -407,6 +407,18 @@ def f(): debug('Not running exit action') atexit.register(f) +def safeRm(f): + try: + rm(f) + except Exception: + pass + +def safeRmdir(f, b): + try: + rmdir(f, b) + except Exception: + pass + # deleteAtExit is one of the following: # - True: the file is deleted unconditionally # - 'ifSuccess': the file is deleted if the program exists with code 0 @@ -414,13 +426,13 @@ def f(): def mkTempFile(suffix='', prefix='', dir=None, deleteAtExit=True): f = tempfile.mktemp(suffix, prefix, dir) if deleteAtExit: - registerAtExit(lambda: rm(f), deleteAtExit) + registerAtExit(lambda: safeRm(f), deleteAtExit) return f def mkTempDir(suffix='', prefix='tmp', dir=None, deleteAtExit=True): d = tempfile.mkdtemp(suffix, prefix, dir) if deleteAtExit: - registerAtExit(lambda: rmdir(d, True), deleteAtExit) + registerAtExit(lambda: safeRmdir(d, True), deleteAtExit) return d class tempDir: diff --git a/python/integration-tests/testIntegration.py b/python/integration-tests/testIntegration.py index 3a1b0341..49082b25 100644 --- a/python/integration-tests/testIntegration.py +++ b/python/integration-tests/testIntegration.py @@ -3,9 +3,9 @@ import os def runWithFlags(path, flags, onError, input=''): - cmd = f'python3 src/runYourProgram.py {" ".join(flags)} {path}' + cmd = f'python3 code/wypp/runYourProgram.py {" ".join(flags)} {path}' res = shell.run(cmd, input=input, captureStdout=True, stderrToStdout=True, onError=onError, - env={'PYTHONPATH': f'./site-lib'}) + env={'PYTHONPATH': f'./code'}) return res def run(path, input='', tycheck=True, ecode=0): @@ -33,156 +33,62 @@ def stripTrailingWs(s): print(f'Output of integration tests goes to {LOG_FILE}') LOG_REDIR = f'>> {LOG_FILE} 2>&1' -class TypeTests(unittest.TestCase): - def test_enumOk(self): - out = runInteractive('test-data/typeEnums.py', 'colorToNumber("red")') - self.assertEqual(['0'], out) - - def test_enumTypeError(self): - out = runInteractive('test-data/typeEnums.py', 'colorToNumber(1)')[0] - self.assertIn("expected: value of type Literal['red', 'yellow', 'green']", out) - - def test_recordOk(self): - rec = 'test-data/typeRecords.py' - out1 = runInteractive(rec, 'Person("stefan", 42)') - self.assertEqual(["Person(name='stefan', age=42)"], out1) - out2 = runInteractive(rec, 'incAge(Person("stefan", 42))') - self.assertEqual(["Person(name='stefan', age=43)"], out2) - - def test_recordFail1(self): - rec = 'test-data/typeRecords.py' - out = runInteractive(rec, 'Person("stefan", 42.3)') - expected = f"""Traceback (most recent call last): - File "", line 1, in -WyppTypeError: got value of wrong type -given: 42.3 -expected: value of type int - -context: record constructor Person(name: str, age: int) -> Self - ^^^""" - - real = '\n'.join(out) - self.assertTrue(real.startswith(expected)) - - def test_recordFail2(self): - rec = 'test-data/typeRecords.py' - out = runInteractive(rec, 'mutableIncAge(Person("stefan", 42))')[0] - self.assertIn('expected: value of type MutablePerson', out) - - def test_recordMutableOk(self): - rec = 'test-data/typeRecords.py' - out1 = runInteractive(rec, 'MutablePerson("stefan", 42)') - self.assertEqual(["MutablePerson(name='stefan', age=42)"], out1) - out2 = runInteractive(rec, 'p = MutablePerson("stefan", 42)\nmutableIncAge(p)\np') - self.assertEqual(['', '', "MutablePerson(name='stefan', age=43)"], out2) - - def test_mutableRecordFail1(self): - rec = 'test-data/typeRecords.py' - out = runInteractive(rec, 'MutablePerson("stefan", 42.3)')[0] - self.assertIn('expected: value of type int', out) - - def test_mutableRecordFail2(self): - rec = 'test-data/typeRecords.py' - out = runInteractive(rec, 'incAge(MutablePerson("stefan", 42))')[0] - self.assertIn('expected: value of type Person', out) - - @unittest.skip - def test_mutableRecordFail3(self): - rec = 'test-data/typeRecords.py' - out = runInteractive(rec, 'p = MutablePerson("stefan", 42)\np.age = 42.4') - self.assertIn('expected: value of type int', out) - - def test_union(self): - out = runInteractive('test-data/typeUnion.py', """formatAnimal(myCat) -formatAnimal(myParrot) -formatAnimal(None) - """) - self.assertEqual("'Cat Pumpernickel'", out[0]) - self.assertEqual("\"Parrot Mike says: Let's go to the punkrock show\"", out[1]) - self.assertIn('given: None\nexpected: value of type Union[Cat, Parrot]', out[2]) - class StudentSubmissionTests(unittest.TestCase): def check(self, file, testFile, ecode, tycheck=True): flags = ['--check'] if not tycheck: flags.append('--no-typechecking') - cmd = f"python3 src/runYourProgram.py {' '.join(flags)} --test-file {testFile} {file} {LOG_REDIR}" + cmd = f"python3 code/wypp/runYourProgram.py {' '.join(flags)} --test-file {testFile} {file} {LOG_REDIR}" res = shell.run(cmd, onError='ignore') self.assertEqual(ecode, res.exitcode) def test_goodSubmission(self): - self.check("test-data/student-submission.py", "test-data/student-submission-tests.py", 0) - self.check("test-data/student-submission.py", "test-data/student-submission-tests.py", 0, + self.check("integration-test-data/student-submission.py", "integration-test-data/student-submission-tests.py", 0) + self.check("integration-test-data/student-submission.py", "integration-test-data/student-submission-tests.py", 0, tycheck=False) def test_badSubmission(self): - self.check("test-data/student-submission-bad.py", - "test-data/student-submission-tests.py", 1) - self.check("test-data/student-submission-bad.py", - "test-data/student-submission-tests.py", 1, tycheck=False) + self.check("integration-test-data/student-submission-bad.py", + "integration-test-data/student-submission-tests.py", 1) + self.check("integration-test-data/student-submission-bad.py", + "integration-test-data/student-submission-tests.py", 1, tycheck=False) def test_submissionWithTypeErrors(self): - self.check("test-data/student-submission-tyerror.py", - "test-data/student-submission-tests.py", 1) - self.check("test-data/student-submission-tyerror.py", - "test-data/student-submission-tests.py", 0, tycheck=False) - self.check("test-data/student-submission.py", - "test-data/student-submission-tests-tyerror.py", 1) - self.check("test-data/student-submission.py", - "test-data/student-submission-tests-tyerror.py", 0, tycheck=False) + self.check("integration-test-data/student-submission-tyerror.py", + "integration-test-data/student-submission-tests.py", 1) + self.check("integration-test-data/student-submission-tyerror.py", + "integration-test-data/student-submission-tests.py", 0, tycheck=False) + self.check("integration-test-data/student-submission.py", + "integration-test-data/student-submission-tests-tyerror.py", 1) + self.check("integration-test-data/student-submission.py", + "integration-test-data/student-submission-tests-tyerror.py", 0, tycheck=False) class InteractiveTests(unittest.TestCase): def test_scopeBugPeter(self): - out = runInteractive('test-data/scope-bug-peter.py', 'local_test()\nprint(spam)') + out = runInteractive('integration-test-data/scope-bug-peter.py', 'local_test()\nprint(spam)') self.assertIn('IT WORKS', out) def test_types1(self): - out = runInteractive('test-data/testTypesInteractive.py', 'inc(3)') + out = runInteractive('integration-test-data/testTypesInteractive.py', 'inc(3)') self.assertEqual(['4'], out) def test_types2(self): - out = runInteractive('test-data/testTypesInteractive.py', 'inc("3")')[0] - expected = """Traceback (most recent call last): - File "", line 1, in -WyppTypeError: got value of wrong type -given: '3' -expected: value of type int - -context: inc(x: int) -> int - ^^^ -declared at: test-data/testTypesInteractive.py:1 -caused by: :1""" - self.assertEqual(expected, out) - - def test_types3(self): - out = runInteractive('test-data/testTypesInteractive.py', - 'def f(x: int) -> int: return x\n\nf("x")')[1] - self.assertIn('expected: value of type int', out) - - def test_types4(self): - out = runInteractive('test-data/testTypesInteractive.py', - 'def f(x: int) -> int: return x\n\nf(3)') - self.assertEqual(['...', '3'], out) - - def test_types5(self): - out = runInteractive('test-data/testTypesInteractive.py', - 'def f(x: int) -> int: return x\n\nf("x")', - tycheck=False) - self.assertEqual(['...', "'x'"], out) + out = runInteractive('integration-test-data/testTypesInteractive.py', 'inc("3")')[0] + self.assertIn('The call of function `inc` expects value of type `int` as 1st argument', out) def test_typesInImportedModule1(self): - out = run('test-data/testTypes3.py', ecode=1) - self.assertIn('expected: value of type int', out) + out = run('integration-test-data/testTypes3.py', ecode=1) + self.assertIn('The call of function `inc` expects value of type `int` as 1st argument', out) def test_typesInImportedModule2(self): - out = run('test-data/testTypes3.py', tycheck=False) + out = run('integration-test-data/testTypes3.py', tycheck=False) self.assertEqual('END', out) class ReplTesterTests(unittest.TestCase): def test_replTester(self): d = shell.pwd() - cmd = f'python3 {d}/src/replTester.py {d}/test-data/repl-test-lib.py --repl {d}/test-data/repl-test-checks.py' + cmd = f'python3 {d}/code/wypp/replTester.py {d}/integration-test-data/repl-test-lib.py --repl {d}/integration-test-data/repl-test-checks.py' res = shell.run(cmd, captureStdout=True, onError='die', cwd='/tmp') - self.assertIn('All 1 tests succeded. Great!', res.stdout) + self.assertIn('All 1 tests succeeded. Great!', res.stdout) diff --git a/python/pyrightconfig.json b/python/pyrightconfig.json new file mode 100644 index 00000000..1aeadf45 --- /dev/null +++ b/python/pyrightconfig.json @@ -0,0 +1,10 @@ +{ + // See https://microsoft.github.io/pyright/#/configuration?id=pyright-configuration + "reportWildcardImportFromLibrary": false, + "reportMissingTypeStubs": false, + "exclude": ["file-test-data", "integration-test-data"], + "ignore": ["code/typing_extensions.py", "code/typeguard"], + "extraPaths": ["code/wypp"], + "typeCheckingMode": "basic" +} + diff --git a/python/run b/python/run index e61be5c5..91861067 100755 --- a/python/run +++ b/python/run @@ -1,11 +1,13 @@ #!/bin/bash -PY=python3.13 +if [ -z "$PY" ]; then + PY=python3.13 +fi SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" OPTS="--quiet" # OPTS="--verbose" -PYTHONPATH="$SCRIPT_DIR"/site-lib:"$PYTHONPATH" $PY "$SCRIPT_DIR"/src/runYourProgram.py \ +PYTHONPATH="$SCRIPT_DIR"/code:"$PYTHONPATH" $PY "$SCRIPT_DIR"/code/wypp/runYourProgram.py \ --no-clear $OPTS "$@" exit $? diff --git a/python/run-repl-tester b/python/run-repl-tester index 3321433d..7ff5b149 100755 --- a/python/run-repl-tester +++ b/python/run-repl-tester @@ -2,4 +2,4 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -PYTHONPATH="$SCRIPT_DIR"/site-lib/ python3 "$SCRIPT_DIR"/src/replTester.py "$@" +PYTHONPATH="$SCRIPT_DIR"/code/ python3 "$SCRIPT_DIR"/code/wypp/replTester.py "$@" diff --git a/python/setup.py b/python/setup.py index bc0e9e21..0bb8a63d 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -from distutils.core import setup -import setuptools +from setuptools import setup, find_packages import os import json @@ -32,8 +31,12 @@ def readVersion(): author='Stefan Wehr', author_email='stefan.wehr@hs-offenburg.de', url='https://github.com/skogsbaer/write-your-python-program', - package_dir={'wypp': 'src', 'untypy': 'deps/untypy/untypy'}, - packages=['wypp'] + setuptools.find_packages("deps/untypy", exclude=['test', 'test.*']), + package_dir={ + 'wypp': 'code/wypp', + 'typeguard': 'code/typeguard', + 'typing_extensions': 'code' + }, + packages=['wypp', 'typing_extensions', 'typeguard'], python_requires='>=3.12.0', scripts=['wypp'] ) diff --git a/python/site-lib/README b/python/site-lib/README deleted file mode 100644 index d2769afc..00000000 --- a/python/site-lib/README +++ /dev/null @@ -1,3 +0,0 @@ -This directory contains symlinks for wypp and untypy. - -Add this directory to PYTHONPATH if you want to import wypp and untypy in-place. diff --git a/python/site-lib/untypy b/python/site-lib/untypy deleted file mode 120000 index db337ec2..00000000 --- a/python/site-lib/untypy +++ /dev/null @@ -1 +0,0 @@ -../deps/untypy/untypy/ \ No newline at end of file diff --git a/python/site-lib/wypp b/python/site-lib/wypp deleted file mode 120000 index e057607e..00000000 --- a/python/site-lib/wypp +++ /dev/null @@ -1 +0,0 @@ -../src/ \ No newline at end of file diff --git a/python/src/runner.py b/python/src/runner.py deleted file mode 100644 index 8e2f514e..00000000 --- a/python/src/runner.py +++ /dev/null @@ -1,604 +0,0 @@ -import sys -import os -import os.path -import argparse -import json -import traceback -import shutil -import site -import importlib -import re -import code -import ast -from modulefinder import ModuleFinder -from pathlib import Path -import subprocess - -__wypp_runYourProgram = 1 - -def die(ecode=1): - if sys.flags.interactive: - os._exit(ecode) - else: - sys.exit(ecode) - -def getEnv(name, conv, default): - s = os.getenv(name) - if s is None: - return default - try: - return conv(s) - except: - return default - -VERBOSE = False # set via commandline -DEBUG = getEnv("WYPP_DEBUG", bool, False) - -def enableVerbose(): - global VERBOSE - VERBOSE = True - -LIB_DIR = os.path.dirname(__file__) -INSTALLED_MODULE_NAME = 'wypp' -FILES_TO_INSTALL = ['writeYourProgram.py', 'drawingLib.py', '__init__.py'] - -UNTYPY_DIR = os.path.join(LIB_DIR, "..", "deps", "untypy", "untypy") -UNTYPY_MODULE_NAME = 'untypy' -SITELIB_DIR = os.path.join(LIB_DIR, "..", "site-lib") - -def verbose(s): - if VERBOSE or DEBUG: - printStderr('[V] ' + str(s)) - -def printStderr(s=''): - sys.stderr.write(s + '\n') - -class InstallMode: - dontInstall = 'dontInstall' - installOnly = 'installOnly' - install = 'install' - assertInstall = 'assertInstall' - allModes = [dontInstall, installOnly, install, assertInstall] - -def parseCmdlineArgs(argList): - parser = argparse.ArgumentParser(description='Run Your Program!', - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('--check-runnable', dest='checkRunnable', action='store_const', - const=True, default=False, - help='Abort with exit code 1 if loading the file raises errors') - parser.add_argument('--check', dest='check', action='store_const', - const=True, default=False, - help='Abort with exit code 1 if there are test errors.') - parser.add_argument('--install-mode', dest='installMode', type=str, - default=InstallMode.dontInstall, - help="""The install mode can be one of the following: -- dontInstall do not install the wypp library (default) -- installOnly install the wypp library and quit -- install install the wypp library and continue even if installation fails -- assertInstall check whether wypp is installed -""") - parser.add_argument('--verbose', dest='verbose', action='store_const', - const=True, default=False, - help='Be verbose') - parser.add_argument('--debug', dest='debug', action='store_const', - const=True, default=False, - help='Enable debugging') - parser.add_argument('--quiet', dest='quiet', action='store_const', - const=True, default=False, help='Be extra quiet') - parser.add_argument('--no-clear', dest='noClear', action='store_const', - const=True, default=False, help='Do not clear the terminal') - parser.add_argument('--test-file', dest='testFile', - type=str, help='Run additional tests contained in this file.') - parser.add_argument('--extra-dir', dest='extraDirs', action='append', type=str, - help='Also typechecks files contained in the given directory.\n' \ - 'By default, only files in the same directory as the main file are\n' \ - 'checked.') - parser.add_argument('--change-directory', dest='changeDir', action='store_const', - const=True, default=False, - help='Change to the directory of FILE before running') - parser.add_argument('--interactive', dest='interactive', action='store_const', - const=True, default=False, - help='Run REPL after the programm has finished') - parser.add_argument('--no-typechecking', dest='checkTypes', action='store_const', - const=False, default=True, - help='Do not check type annotations') - parser.add_argument('file', metavar='FILE', - help='The file to run', nargs='?') - if argList is None: - argList = sys.argv[1:] - try: - args, restArgs = parser.parse_known_args(argList) - except SystemExit as ex: - die(ex.code) - if args.file and not args.file.endswith('.py'): - printStderr(f'ERROR: file {args.file} is not a python file') - die() - if args.installMode not in InstallMode.allModes: - printStderr(f'ERROR: invalid install mode {args.installMode}.') - die() - return (args, restArgs) - -def readFile(path): - try: - with open(path, encoding='utf-8') as f: - return f.read() - except UnicodeDecodeError: - with open(path) as f: - return f.read() - -def readGitVersion(): - thisDir = os.path.basename(LIB_DIR) - baseDir = os.path.join(LIB_DIR, '..', '..') - if thisDir == 'src' and os.path.isdir(os.path.join(baseDir, '.git')): - try: - h = subprocess.check_output(['git', '-C', baseDir, 'rev-parse', '--short', 'HEAD'], - encoding='UTF-8').strip() - changes = subprocess.check_output( - ['git', '-C', baseDir, 'status', '--porcelain', '--untracked-files=no'], - encoding='UTF-8').strip() - if changes: - return f'git-{h}-dirty' - else: - return f'git-{h}' - except subprocess.CalledProcessError: - return 'git-?' - else: - return None - -def readVersion(): - version = readGitVersion() - if version is not None: - return version - try: - content = readFile(os.path.join(LIB_DIR, '..', '..', 'package.json')) - d = json.loads(content) - version = d['version'] - except: - pass - return version - -def printWelcomeString(file, version, useUntypy): - cwd = os.getcwd() + "/" - if file.startswith(cwd): - file = file[len(cwd):] - versionStr = '' if not version else 'Version %s, ' % version - pythonVersion = sys.version.split()[0] - tycheck = '' - if not useUntypy: - tycheck = ', no typechecking' - printStderr('=== WELCOME to "Write Your Python Program" ' + - '(%sPython %s, %s%s) ===' % (versionStr, pythonVersion, file, tycheck)) - -def isSameFile(f1, f2): - x = readFile(f1) - y = readFile(f2) - return x == y - -def installFromDir(srcDir, targetDir, mod, files=None): - verbose(f'Installing from {srcDir} to {targetDir}/{mod}') - if files is None: - files = [p.relative_to(srcDir) for p in Path(srcDir).rglob('*.py')] - else: - files = [Path(f) for f in files] - installDir = os.path.join(targetDir, mod) - os.makedirs(installDir, exist_ok=True) - installedFiles = sorted([p.relative_to(installDir) for p in Path(installDir).rglob('*.py')]) - wantedFiles = sorted(files) - if installedFiles == wantedFiles: - for i in range(len(installedFiles)): - f1 = os.path.join(installDir, installedFiles[i]) - f2 = os.path.join(srcDir, wantedFiles[i]) - if not isSameFile(f1, f2): - verbose(f'{f1} and {f2} differ') - break - else: - # no break, all files equal - verbose(f'All files from {srcDir} already installed in {targetDir}/{mod}') - return True - else: - verbose(f'Installed files {installedFiles} and wanted files {wantedFiles} are different') - for f in installedFiles: - p = os.path.join(installDir, f) - os.remove(p) - d = os.path.join(installDir, mod) - for f in wantedFiles: - src = os.path.join(srcDir, f) - tgt = os.path.join(installDir, f) - os.makedirs(os.path.dirname(tgt), exist_ok=True) - shutil.copyfile(src, tgt) - verbose(f'Finished installation from {srcDir} to {targetDir}/{mod}') - return False - -def installLib(mode): - verbose("installMode=" + mode) - if mode == InstallMode.dontInstall: - verbose("No installation of WYPP should be performed") - return - targetDir = os.getenv('WYPP_INSTALL_DIR', site.USER_SITE) - try: - allEq1 = installFromDir(LIB_DIR, targetDir, INSTALLED_MODULE_NAME, FILES_TO_INSTALL) - allEq2 = installFromDir(UNTYPY_DIR, targetDir, UNTYPY_MODULE_NAME) - if allEq1 and allEq2: - verbose(f'WYPP library in {targetDir} already up to date') - if mode == InstallMode.installOnly: - printStderr(f'WYPP library in {targetDir} already up to date') - return - else: - if mode == InstallMode.assertInstall: - printStderr("The WYPP library was not installed before running this command.") - die(1) - printStderr(f'The WYPP library has been successfully installed in {targetDir}') - except Exception as e: - printStderr('Installation of the WYPP library failed: ' + str(e)) - if mode == InstallMode.installOnly: - raise e - if mode == InstallMode.installOnly: - printStderr('Exiting after installation of the WYPP library') - die(0) - -class Lib: - def __init__(self, mod, properlyImported): - self.properlyImported = properlyImported - if not properlyImported: - self.initModule = mod['initModule'] - self.resetTestCount = mod['resetTestCount'] - self.printTestResults = mod['printTestResults'] - self.dict = mod - else: - self.initModule = mod.initModule - self.resetTestCount = mod.resetTestCount - self.printTestResults = mod.printTestResults - d = {} - self.dict = d - for name in dir(mod): - if name and name[0] != '_': - d[name] = getattr(mod, name) - -def prepareLib(onlyCheckRunnable, enableTypeChecking): - libDefs = None - mod = INSTALLED_MODULE_NAME - verbose('Attempting to import ' + mod) - wypp = importlib.import_module(mod) - libDefs = Lib(wypp, True) - verbose('Successfully imported module ' + mod + ' from file ' + wypp.__file__) - libDefs.initModule(enableChecks=not onlyCheckRunnable, - enableTypeChecking=enableTypeChecking, - quiet=onlyCheckRunnable) - return libDefs - -importRe = importRe = re.compile(r'^import\s+wypp\s*$|^from\s+wypp\s+import\s+\*\s*$') - -def findWyppImport(fileName): - lines = [] - try: - with open(fileName) as f: - lines = f.readlines() - except Exception as e: - verbose('Failed to read code from %s: %s' % (fileName, e)) - for l in lines: - if importRe.match(l): - return True - return False - -def findImportedModules(path, file): - finder = ModuleFinder(path=path) - try: - finder.run_script(file) - except: - return [] - realdirs = [os.path.realpath(p) for p in path] - res = [] - for name, mod in finder.modules.items(): - if name != '__main__' and mod.__file__: - realp = os.path.realpath(mod.__file__) - good = False - for d in realdirs: - if realp.startswith(d): - good = True - break - if good: - res.append(name) - return res - -class RunSetup: - def __init__(self, sysPath): - self.sysPath = sysPath - self.sysPathInserted = False - def __enter__(self): - if self.sysPath not in sys.path: - sys.path.insert(0, self.sysPath) - self.sysPathInserted = True - def __exit__(self, exc_type, value, traceback): - if self.sysPathInserted: - sys.path.remove(self.sysPath) - self.sysPathInserted = False - -def runCode(fileToRun, globals, args, useUntypy=True, extraDirs=None): - if not extraDirs: - extraDirs = [] - localDir = os.path.dirname(fileToRun) - - with RunSetup(localDir): - codeTxt = readFile(fileToRun) - flags = 0 - if useUntypy: - verbose(f'finding modules imported by {fileToRun}') - importedMods = findImportedModules([localDir] + extraDirs, fileToRun) - verbose('finished finding modules, now installing import hook on ' + repr(importedMods)) - untypy.enableDebug(DEBUG) - untypy.just_install_hook(importedMods + ['__wypp__']) - verbose(f"transforming {fileToRun} for typechecking") - tree = compile(codeTxt, fileToRun, 'exec', flags=(flags | ast.PyCF_ONLY_AST), - dont_inherit=True, optimize=-1) - untypy.transform_tree(tree, fileToRun) - verbose(f'done with transformation of {fileToRun}') - code = tree - else: - code = codeTxt - compiledCode = compile(code, fileToRun, 'exec', flags=flags, dont_inherit=True) - oldArgs = sys.argv - try: - sys.argv = [fileToRun] + args - exec(compiledCode, globals) - finally: - sys.argv = oldArgs - -def runStudentCode(fileToRun, globals, onlyCheckRunnable, args, useUntypy=True, extraDirs=None): - doRun = lambda: runCode(fileToRun, globals, args, useUntypy=useUntypy, extraDirs=extraDirs) - if onlyCheckRunnable: - try: - doRun() - except: - printStderr('Loading file %s crashed' % fileToRun) - handleCurrentException() - else: - die(0) - doRun() - -# globals already contain libDefs -def runTestsInFile(testFile, globals, libDefs, useUntypy=True, extraDirs=[]): - printStderr() - printStderr(f"Running tutor's tests in {testFile}") - libDefs.resetTestCount() - try: - runCode(testFile, globals, [], useUntypy=useUntypy, extraDirs=extraDirs) - except: - handleCurrentException() - return libDefs.dict['printTestResults']('Tutor: ') - -# globals already contain libDefs -def performChecks(check, testFile, globals, libDefs, useUntypy=True, extraDirs=None, loadingFailed=False): - prefix = '' - if check and testFile: - prefix = 'Student: ' - testResultsStudent = libDefs.printTestResults(prefix, loadingFailed) - if check: - testResultsInstr = {'total': 0, 'failing': 0} - if testFile: - testResultsInstr = runTestsInFile(testFile, globals, libDefs, useUntypy=useUntypy, - extraDirs=extraDirs) - failingSum = testResultsStudent['failing'] + testResultsInstr['failing'] - die(0 if failingSum < 1 else 1) - -def prepareInteractive(reset=True): - print() - if reset: - if os.name == 'nt': - # clear the terminal - os.system('cls') - else: - # On linux & mac use ANSI Sequence for this - print('\033[2J\033[H') - -def enterInteractive(userDefs): - for k, v in userDefs.items(): - globals()[k] = v - print() - -def tbToFrameList(tb): - cur = tb - res = [] - while cur: - res.append(cur.tb_frame) - cur = cur.tb_next - return res - -def isWyppFrame(frame): - modName = frame.f_globals["__name__"] - return '__wypp_runYourProgram' in frame.f_globals or \ - modName == 'untypy' or modName.startswith('untypy.') or \ - modName == 'wypp' or modName.startswith('wypp.') - -def ignoreFrame(frame): - if DEBUG: - return False - return isWyppFrame(frame) - -# Returns a StackSummary object -def limitTraceback(frameList, isBug): - frames = [(f, f.f_lineno) for f in frameList if isBug or not ignoreFrame(f)] - return traceback.StackSummary.extract(frames) - -def handleCurrentException(exit=True, removeFirstTb=False, file=sys.stderr): - (etype, val, tb) = sys.exc_info() - if isinstance(val, SystemExit): - die(val.code) - frameList = tbToFrameList(tb) - if frameList and removeFirstTb: - frameList = frameList[1:] - if frameList and getattr(val, '__wrapped__', False): - # We remove the stack frame of the wrapper - frameList = frameList[:-1] - isWyppError = isinstance(val, untypy.error.UntypyError) - isBug = not isWyppError and not isinstance(val, SyntaxError) and \ - not isinstance(val, untypy.error.DeliberateError) and frameList \ - and isWyppFrame(frameList[-1]) - stackSummary = limitTraceback(frameList, isBug) - header = False - for x in stackSummary.format(): - if not header: - file.write('Traceback (most recent call last):\n') - header = True - file.write(x) - if isinstance(val, untypy.error.UntypyError): - name = 'Wypp' + val.simpleName() - file.write(name) - s = str(val) - if s and s[0] != '\n': - file.write(': ') - file.write(s) - file.write('\n') - else: - for x in traceback.format_exception_only(etype, val): - file.write(x) - if isBug: - file.write(f'BUG: the error above is most likely a bug in WYPP!') - if exit: - die(1) - -HISTORY_SIZE = 1000 - -def getHistoryFilePath(): - envVar = 'HOME' - if os.name == 'nt': - envVar = 'USERPROFILE' - d = os.getenv(envVar, None) - if d: - return os.path.join(d, ".wypp_history") - else: - return None - -# We cannot import untypy at the top of the file because we might have to install it first. -def importUntypy(): - global untypy - try: - import untypy - except ModuleNotFoundError as e: - printStderr(f"Module untypy not found, sys.path={sys.path}: {e}") - die(1) - -requiredVersion = (3, 12, 0) -def versionOk(v): - (reqMajor, reqMinor, reqMicro) = requiredVersion - if v.major < reqMajor or v.minor < reqMinor: - return False - if v.major == reqMajor and v.minor == reqMinor and v.micro < reqMicro: - return False - else: - return True - -def main(globals, argList=None): - v = sys.version_info - if not versionOk(v): - vStr = sys.version.split()[0] - reqVStr = '.'.join([str(x) for x in requiredVersion]) - print(f""" -Python in version {reqVStr} or newer is required. You are still using version {vStr}, please upgrade! -""") - sys.exit(1) - - (args, restArgs) = parseCmdlineArgs(argList) - global VERBOSE - if args.verbose: - VERBOSE = True - global DEBUG - if args.debug: - DEBUG = True - - verbose(f'VERBOSE={VERBOSE}, DEBUG={DEBUG}') - - installLib(args.installMode) - if args.installMode == InstallMode.dontInstall: - if SITELIB_DIR not in sys.path: - sys.path.insert(0, SITELIB_DIR) - else: - if site.USER_SITE not in sys.path: - if not site.ENABLE_USER_SITE: - printStderr(f"User site-packages disabled ({site.USER_SITE}. This might cause problems importing wypp or untypy.") - else: - verbose(f"Adding user site-package directory {site.USER_SITE} to sys.path") - sys.path.append(site.USER_SITE) - importUntypy() - - fileToRun = args.file - if args.changeDir: - os.chdir(os.path.dirname(fileToRun)) - fileToRun = os.path.basename(fileToRun) - - isInteractive = args.interactive - version = readVersion() - if isInteractive: - prepareInteractive(reset=not args.noClear) - - if fileToRun is None: - return - if not args.checkRunnable and (not args.quiet or args.verbose): - printWelcomeString(fileToRun, version, useUntypy=args.checkTypes) - - libDefs = prepareLib(onlyCheckRunnable=args.checkRunnable, enableTypeChecking=args.checkTypes) - - globals['__name__'] = '__wypp__' - sys.modules['__wypp__'] = sys.modules['__main__'] - loadingFailed = False - try: - verbose(f'running code in {fileToRun}') - globals['__file__'] = fileToRun - runStudentCode(fileToRun, globals, args.checkRunnable, restArgs, - useUntypy=args.checkTypes, extraDirs=args.extraDirs) - except Exception as e: - verbose(f'Error while running code in {fileToRun}: {e}') - handleCurrentException(exit=not isInteractive) - loadingFailed = True - - performChecks(args.check, args.testFile, globals, libDefs, useUntypy=args.checkTypes, - extraDirs=args.extraDirs, loadingFailed=loadingFailed) - - if isInteractive: - enterInteractive(globals) - if loadingFailed: - print('NOTE: running the code failed, some definitions might not be available!') - print() - if args.checkTypes: - consoleClass = TypecheckedInteractiveConsole - else: - consoleClass = code.InteractiveConsole - historyFile = getHistoryFilePath() - try: - import readline - readline.parse_and_bind('tab: complete') - if historyFile and os.path.exists(historyFile): - readline.read_history_file(historyFile) - except: - pass - try: - consoleClass(locals=globals).interact(banner="", exitmsg='') - finally: - if readline and historyFile: - readline.set_history_length(HISTORY_SIZE) - readline.write_history_file(historyFile) - -class TypecheckedInteractiveConsole(code.InteractiveConsole): - - def showtraceback(self) -> None: - handleCurrentException(exit=False, removeFirstTb=True, file=sys.stdout) - - def runsource(self, source, filename="", symbol="single"): - try: - code = self.compile(source, filename, symbol) - except (OverflowError, SyntaxError, ValueError): - self.showsyntaxerror(filename) - return False - if code is None: - return True - try: - import ast - tree = compile("\n".join(self.buffer), filename, symbol, flags=ast.PyCF_ONLY_AST, dont_inherit=True, optimize=-1) - untypy.transform_tree(tree, filename) - code = compile(tree, filename, symbol) - except Exception as e: - if hasattr(e, 'text') and e.text == "": - pass - else: - traceback.print_tb(e.__traceback__) - self.runcode(code) - return False diff --git a/python/test-data/declared-at-missing.err b/python/test-data/declared-at-missing.err deleted file mode 100644 index 937cd8db..00000000 --- a/python/test-data/declared-at-missing.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/declared-at-missing.py", line 22, in - semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) -WyppTypeError: got value of wrong type -given: Course(name='Programmierung 1', teacher='Wehr', students=()) -expected: value of type CourseM - -context: record constructor Semester(degreeProgram: str, semester: str, courses: tuple[CourseM, ...]) -> Self - ^^^^^^^ -declared at: test-data/declared-at-missing.py:16 -caused by: test-data/declared-at-missing.py:22 - | semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) diff --git a/python/test-data/declared-at-missing.err-3.10 b/python/test-data/declared-at-missing.err-3.10 deleted file mode 100644 index 92b7295e..00000000 --- a/python/test-data/declared-at-missing.err-3.10 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/declared-at-missing.py", line 22, in - semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) -WyppTypeError: got value of wrong type -given: Course(name='Programmierung 1', teacher='Wehr', students=()) -expected: value of type CourseM - -context: record constructor Semester(degreeProgram: str, semester: str, courses: tuple[CourseM, ...]) -> Self - ^^^^^^^ -declared at: test-data/declared-at-missing.py:15 -caused by: test-data/declared-at-missing.py:22 - | semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) diff --git a/python/test-data/declared-at-missing.err-3.11 b/python/test-data/declared-at-missing.err-3.11 deleted file mode 100644 index 92b7295e..00000000 --- a/python/test-data/declared-at-missing.err-3.11 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/declared-at-missing.py", line 22, in - semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) -WyppTypeError: got value of wrong type -given: Course(name='Programmierung 1', teacher='Wehr', students=()) -expected: value of type CourseM - -context: record constructor Semester(degreeProgram: str, semester: str, courses: tuple[CourseM, ...]) -> Self - ^^^^^^^ -declared at: test-data/declared-at-missing.py:15 -caused by: test-data/declared-at-missing.py:22 - | semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) diff --git a/python/test-data/declared-at-missing.err-3.12 b/python/test-data/declared-at-missing.err-3.12 deleted file mode 100644 index 92b7295e..00000000 --- a/python/test-data/declared-at-missing.err-3.12 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/declared-at-missing.py", line 22, in - semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) -WyppTypeError: got value of wrong type -given: Course(name='Programmierung 1', teacher='Wehr', students=()) -expected: value of type CourseM - -context: record constructor Semester(degreeProgram: str, semester: str, courses: tuple[CourseM, ...]) -> Self - ^^^^^^^ -declared at: test-data/declared-at-missing.py:15 -caused by: test-data/declared-at-missing.py:22 - | semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) diff --git a/python/test-data/fileWithRecursiveTypes.py b/python/test-data/fileWithRecursiveTypes.py deleted file mode 100644 index cd7bfd59..00000000 --- a/python/test-data/fileWithRecursiveTypes.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import annotations - -class C: - def foo(self: C, d: D) -> C: - return C - -class D: - pass diff --git a/python/test-data/fileWithStarImport.py b/python/test-data/fileWithStarImport.py deleted file mode 100644 index 2be1696e..00000000 --- a/python/test-data/fileWithStarImport.py +++ /dev/null @@ -1,7 +0,0 @@ -from wypp import * - -def use(x): - pass - -use(record) -use(list[int]) diff --git a/python/test-data/modules/A/main.err b/python/test-data/modules/A/main.err deleted file mode 100644 index 0060cc41..00000000 --- a/python/test-data/modules/A/main.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/modules/A/main.py", line 3, in - mod.foo(1) - File "", line 5, in foo - return bar(i) -WyppTypeError: got value of wrong type -given: 1 -expected: value of type str - -context: bar(s: str) -> int - ^^^ -declared at: test-data/modules/B/mod.py:1 -caused by: test-data/modules/B/mod.py:5 - | return bar(i) diff --git a/python/test-data/modules/A/main.py b/python/test-data/modules/A/main.py deleted file mode 100644 index f641d76c..00000000 --- a/python/test-data/modules/A/main.py +++ /dev/null @@ -1,3 +0,0 @@ -import mod - -mod.foo(1) diff --git a/python/test-data/perf.py b/python/test-data/perf.py deleted file mode 100644 index 6b3ce38e..00000000 --- a/python/test-data/perf.py +++ /dev/null @@ -1,125 +0,0 @@ -from __future__ import annotations -import unittest -from wypp import * - -# Measured 2022-05-05 on a Macbook pro with a 2,3 GHz 8-Core Intel Core i9 -# -# * Without type annotations -# -# Run 1 -# ['0.004', '0.258', '0.054'] -# Run 2 -# ['0.004', '0.258', '0.054'] -# Run 3 -# ['0.005', '0.262', '0.055'] -# Median of total: 0.317 -# -# * With type annotations (slow way) -# -# Run 1 -# ['0.005', '0.271', '2.548'] -# Run 2 -# ['0.005', '0.272', '2.509'] -# Run 3 -# ['0.005', '0.271', '2.526'] -# Median of total: 2.803 -# Factor: 8.8 -# -# * With type annotations (fast way) -# Run 1 -# ['0.005', '0.126', '1.128'] -# Run 2 -# ['0.005', '0.120', '1.072'] -# Run 3 -# ['0.005', '0.119', '1.073'] -# Median of total: 1.197 -# Factor: 3.7 - -@dataclass -class SearchTreeNode: - key: any - value: any - left: Optional[SearchTreeNode] - right: Optional[SearchTreeNode] - -class SearchTree: - def __init__(self, root = None): - self.root = root - - # SEITENEFFEKT: verändert den Suchbaum - def insert(self, key: any, value: any): - self.root = self.__insertAux(self.root, key, value) - - def __insertAux(self, node: Optional[SearchTreeNode], key: any, value: any) -> SearchTreeNode: - if node is None: - return SearchTreeNode(key, value, None, None) - elif node.key == key: - node.value = value - return node - elif key < node.key: - node.left = self.__insertAux(node.left, key, value) - return node - else: # key > node.key - node.right = self.__insertAux(node.right, key, value) - return node - -def inorder(node: any): - if node is not None: - inorder(node.left) - inorder(node.right) - -# Tests -import random - -def randomInts(n, low, high): - res = [] - for _ in range(n): - i = random.randint(low, high) - res.append(i) - return res - -def assertSearchTree(node): - if node == None: - return - inorder(node.left) - inorder(node.right) - assertSearchTree(node.left) - assertSearchTree(node.right) - return - -def measure(start, times, i): - import time - end = time.time() - times[i] = times[i] + (end - start) - return end - -def run(f): - times = [0,0,0] - import time - for _ in range(100): - start = time.time() - l = randomInts(30, 0, 50) - t = SearchTree() - for i in l: - start = measure(start, times, 0) - t.insert(i, "value_" + str(i)) - start = measure(start, times, 1) - f(t.root) - start = measure(start, times, 2) - print([f'{x:.3f}' for x in times]) - -def bench(f): - import time - times = [] - for i in [1,2,3]: - print(f'Run {i}') - start = time.time() - run(f) - end = time.time() - times.append(end - start) - times.sort() - print(f'Median of total: {times[1]:.3f}') - -import cProfile -#cProfile.run('run()', 'profile') -bench(assertSearchTree) diff --git a/python/test-data/printModuleNameImport.py b/python/test-data/printModuleNameImport.py deleted file mode 100644 index bae36788..00000000 --- a/python/test-data/printModuleNameImport.py +++ /dev/null @@ -1,11 +0,0 @@ -import wypp -import printModuleName -print(__name__) - -class C: - pass - -import sys -print(sys.modules.get(C.__module__) is not None) -print(sys.modules.get(printModuleName.C.__module__) is not None) - diff --git a/python/test-data/testABCMeta.err-3.10 b/python/test-data/testABCMeta.err-3.10 deleted file mode 100644 index c6c172cb..00000000 --- a/python/test-data/testABCMeta.err-3.10 +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testABCMeta.py", line 28, in - Circle(Point(0, 0), 1) -TypeError: Can't instantiate abstract class Circle with abstract method area diff --git a/python/test-data/testABCMeta.err-3.11 b/python/test-data/testABCMeta.err-3.11 deleted file mode 100644 index c6c172cb..00000000 --- a/python/test-data/testABCMeta.err-3.11 +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testABCMeta.py", line 28, in - Circle(Point(0, 0), 1) -TypeError: Can't instantiate abstract class Circle with abstract method area diff --git a/python/test-data/testArgs.out b/python/test-data/testArgs.out deleted file mode 100644 index 982a2823..00000000 --- a/python/test-data/testArgs.out +++ /dev/null @@ -1 +0,0 @@ -['test-data/testArgs.py', 'ARG_1', 'ARG_2'] diff --git a/python/test-data/testArgs.py b/python/test-data/testArgs.py deleted file mode 100644 index 1174435a..00000000 --- a/python/test-data/testArgs.py +++ /dev/null @@ -1,3 +0,0 @@ -import wypp -import sys -print(sys.argv) diff --git a/python/test-data/testCheck.out b/python/test-data/testCheck.out deleted file mode 100644 index 29568d00..00000000 --- a/python/test-data/testCheck.out +++ /dev/null @@ -1,5 +0,0 @@ -FEHLER in test-data/testCheck.py:13: Erwartet wird 2, aber das Ergebnis ist 1 -FEHLER in test-data/testCheck.py:14: Erwartet wird 'xyz', aber das Ergebnis ist 'abc' -FEHLER in test-data/testCheck.py:15: Erwartet wird Point(x=1, y=3), aber das Ergebnis ist Point(x=1, y=2) -FEHLER in test-data/testCheck.py:16: Erwartet wird Point(x=1, y=3), aber das Ergebnis ist Name(firstName='Max', lastName='Müller') -4 Tests, 4 Fehler 🙁 diff --git a/python/test-data/testCheck2.err b/python/test-data/testCheck2.err deleted file mode 100644 index 3ee8955f..00000000 --- a/python/test-data/testCheck2.err +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testCheck2.py", line 3, in - check(1, 2, 3) -TypeError: check() takes 2 positional arguments but 3 were given diff --git a/python/test-data/testCheck2.py b/python/test-data/testCheck2.py deleted file mode 100644 index 6afe96bb..00000000 --- a/python/test-data/testCheck2.py +++ /dev/null @@ -1,3 +0,0 @@ -from wypp import * - -check(1, 2, 3) diff --git a/python/test-data/testDeepEqBug.out b/python/test-data/testDeepEqBug.out deleted file mode 100644 index 7cdbb33e..00000000 --- a/python/test-data/testDeepEqBug.out +++ /dev/null @@ -1,2 +0,0 @@ -FEHLER in test-data/testDeepEqBug.py:61: Erwartet wird SemesterM(degreeProgram='AKI', semester='1. Semester 2021/22', courses=[CourseM(name='Programmierung 1', teacher='Wehr', students=[]), CourseM(name='Grundlagen der Informatik', teacher='Oelke', students=[])]), aber das Ergebnis ist SemesterM(degreeProgram='AKI', semester='1. Semester 2021/22', courses=[CourseM(name='Programmierung 1', teacher='Wehr', students=['1234', '9876']), CourseM(name='Grundlagen der Informatik', teacher='Oelke', students=['1234'])]) -1 Test, 1 Fehler 🙁 diff --git a/python/test-data/testDisappearingObject_01.out b/python/test-data/testDisappearingObject_01.out deleted file mode 100644 index 3252461b..00000000 --- a/python/test-data/testDisappearingObject_01.out +++ /dev/null @@ -1,2 +0,0 @@ -['subdir'] -Directory('stefan') diff --git a/python/test-data/testDisappearingObject_01.py b/python/test-data/testDisappearingObject_01.py deleted file mode 100644 index aabc05fa..00000000 --- a/python/test-data/testDisappearingObject_01.py +++ /dev/null @@ -1,77 +0,0 @@ -# This test comes from a bug reported by a student, 2025-05-8 -from __future__ import annotations -from wypp import * -import gc - -class FileSystemEntry: - def __init__(self, name: str): - self.name = name - def getName(self) -> str: - return self.name - def getContent(self) -> str: - raise Exception('No content') - def getChildren(self) -> list[FileSystemEntry]: - return [] - def findChild(self, name: str) -> FileSystemEntry: - for c in self.getChildren(): - if c.getName() == name: - return c - raise Exception('No child with name ' + name) - def addChild(self, child: FileSystemEntry) -> None: - raise Exception('Cannot add child to ' + repr(self)) - -class Directory(FileSystemEntry): - def __init__(self, name: str, children: list[FileSystemEntry] = []): - super().__init__(name) - self.__children = children - def getChildren(self) -> list[FileSystemEntry]: - return self.__children - def addChild(self, child: FileSystemEntry): - self.__children.append(child) - def __repr__(self): - return 'Directory(' + repr(self.getName()) + ')' - -class File: - def __init__(self, name: str, content: str): - raise ValueError() - super().__init__(name) - self.content = content - def getContent(self) -> str: - return self.content - -class Link(FileSystemEntry): - def __init__(self, name: str, linkTarget: FileSystemEntry): - super().__init__(name) - self.__linkTarget = linkTarget - def getChildren(self) -> list[FileSystemEntry]: - return self.__linkTarget.getChildren() - def getContent(self) -> str: - return self.__linkTarget.getContent() - def addChild(self, child: FileSystemEntry): - self.__linkTarget.addChild(child) - def __repr__(self): - return ('Link(' + repr(self.getName()) + ' -> ' + - repr(self.__linkTarget) + ')') - def getLinkTarget(self) -> FileSystemEntry: - return self.__linkTarget - -class CryptoFile: - def __init__(self, name: str, content: str): - raise ValueError() - super().__init__(name) - self.content = content - def getContent(self) -> str: - return 'CRYPTO_' + 'X'*len(self.content) - def __repr__(self): - return 'CryptoFile(' + repr(self.getName()) - -def test_link(): - stefan = Directory('stefan', []) - wehr = Link('wehr', stefan) - l1 = [x.getName() for x in wehr.getChildren()] - wehr.addChild(Directory('subdir', [])) - l2 = [x.getName() for x in wehr.getChildren()] - print(l2) - print(stefan) - -test_link() diff --git a/python/test-data/testDisappearingObject_02.py b/python/test-data/testDisappearingObject_02.py deleted file mode 100644 index 97d8daca..00000000 --- a/python/test-data/testDisappearingObject_02.py +++ /dev/null @@ -1,68 +0,0 @@ -# This test comes from a bug reported by a student, 2025-05-8 -from __future__ import annotations -from wypp import * -from abc import ABC, abstractmethod - -class FileSystemEntry(ABC): - def __init__(self, name: str): - self.__name = name - def getName(self) -> str: - return self.__name - def getContent(self) -> str: - raise Exception('No content') - def getChildren(self) -> list[FileSystemEntry]: - return [] - def findChild(self, name: str) -> FileSystemEntry: - for c in self.getChildren(): - if c.getName() == name: - return c - raise Exception('No child with name ' + name) - def addChild(self, child: FileSystemEntry) -> None: - raise Exception('Cannot add child to ' + repr(self)) - -class Directory(FileSystemEntry): - def __init__(self, name: str, children: list[FileSystemEntry] = []): - super().__init__(name) - self.__children = children[:] - def getChildren(self) -> list[FileSystemEntry]: - return self.__children[:] - def addChild(self, child: FileSystemEntry): - self.__children.append(child) - def __repr__(self): - return 'Directory(' + repr(self.getName()) + ')' - -class File(FileSystemEntry): - def __init__(self, name: str, content: str): - super().__init__(name) - self.__content = content - def getContent(self) -> str: - return self.__content - def __repr__(self): - return 'File(' + repr(self.getName()) + ')' - -class Link(FileSystemEntry): - def __init__(self, name: str, linkTarget: FileSystemEntry): - super().__init__(name) - self.__linkTarget = linkTarget - def getChildren(self) -> list[FileSystemEntry]: - return self.__linkTarget.getChildren() - def getContent(self) -> str: - return self.__linkTarget.getContent() - def addChild(self, child: FileSystemEntry): - self.__linkTarget.addChild(child) - def __repr__(self): - return ('Link(' + repr(self.getName()) + ' -> ' + - repr(self.__linkTarget) + ')') - def getLinkTarget(self) -> FileSystemEntry: - return self.__linkTarget - -def test_link(): - stefan = Directory('stefan', [File('notes.txt', 'Notiz')]) - wehr = Link('wehr', stefan) - l1 = [x.getName() for x in wehr.getChildren()] - wehr.addChild(Directory('subdir', [])) - l2 = [x.getName() for x in stefan.getChildren()] - l3 = [x.getName() for x in wehr.getChildren()] - - -test_link() diff --git a/python/test-data/testForwardRef2.err b/python/test-data/testForwardRef2.err deleted file mode 100644 index dadfaeb9..00000000 --- a/python/test-data/testForwardRef2.err +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef2.py", line 10, in - t = Test(FooX()) -WyppNameError: name 'Foo' is not defined. -Type annotation of function 'Test.__init__' could not be resolved. -declared at: test-data/testForwardRef2.py:2 diff --git a/python/test-data/testForwardRef4.err b/python/test-data/testForwardRef4.err deleted file mode 100644 index 261721fc..00000000 --- a/python/test-data/testForwardRef4.err +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef4.py", line 11, in - t = Test(Foo()) -WyppNameError: name 'FooX' is not defined. -Type annotation inside of class 'Test' could not be resolved. -declared at: test-data/testForwardRef4.py:4 diff --git a/python/test-data/testForwardRef4.err-3.10 b/python/test-data/testForwardRef4.err-3.10 deleted file mode 100644 index 41fc248c..00000000 --- a/python/test-data/testForwardRef4.err-3.10 +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef4.py", line 11, in - t = Test(Foo()) -WyppNameError: name 'FooX' is not defined. -Type annotation inside of class 'Test' could not be resolved. -declared at: test-data/testForwardRef4.py:3 diff --git a/python/test-data/testForwardRef4.err-3.11 b/python/test-data/testForwardRef4.err-3.11 deleted file mode 100644 index 41fc248c..00000000 --- a/python/test-data/testForwardRef4.err-3.11 +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef4.py", line 11, in - t = Test(Foo()) -WyppNameError: name 'FooX' is not defined. -Type annotation inside of class 'Test' could not be resolved. -declared at: test-data/testForwardRef4.py:3 diff --git a/python/test-data/testForwardRef4.err-3.12 b/python/test-data/testForwardRef4.err-3.12 deleted file mode 100644 index 41fc248c..00000000 --- a/python/test-data/testForwardRef4.err-3.12 +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef4.py", line 11, in - t = Test(Foo()) -WyppNameError: name 'FooX' is not defined. -Type annotation inside of class 'Test' could not be resolved. -declared at: test-data/testForwardRef4.py:3 diff --git a/python/test-data/testForwardRef5.err b/python/test-data/testForwardRef5.err deleted file mode 100644 index 67974d66..00000000 --- a/python/test-data/testForwardRef5.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef5.py", line 23, in - for car in garage.cars: -WyppTypeError: list is not a list[Car] -given: [Car(color='red'), 'Not A Car'] -expected: value of type list[Car] - -context: record constructor Garage(cars: list[Car]) -> Self - ^^^^^^^^^ -declared at: test-data/testForwardRef5.py:10 -caused by: test-data/testForwardRef5.py:23 - | for car in garage.cars: diff --git a/python/test-data/testForwardRef5.err-3.10 b/python/test-data/testForwardRef5.err-3.10 deleted file mode 100644 index 45c30a41..00000000 --- a/python/test-data/testForwardRef5.err-3.10 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef5.py", line 23, in - for car in garage.cars: -WyppTypeError: list is not a list[Car] -given: [Car(color='red'), 'Not A Car'] -expected: value of type list[Car] - -context: record constructor Garage(cars: list[Car]) -> Self - ^^^^^^^^^ -declared at: test-data/testForwardRef5.py:9 -caused by: test-data/testForwardRef5.py:23 - | for car in garage.cars: diff --git a/python/test-data/testForwardRef5.err-3.11 b/python/test-data/testForwardRef5.err-3.11 deleted file mode 100644 index 45c30a41..00000000 --- a/python/test-data/testForwardRef5.err-3.11 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef5.py", line 23, in - for car in garage.cars: -WyppTypeError: list is not a list[Car] -given: [Car(color='red'), 'Not A Car'] -expected: value of type list[Car] - -context: record constructor Garage(cars: list[Car]) -> Self - ^^^^^^^^^ -declared at: test-data/testForwardRef5.py:9 -caused by: test-data/testForwardRef5.py:23 - | for car in garage.cars: diff --git a/python/test-data/testForwardRef5.err-3.12 b/python/test-data/testForwardRef5.err-3.12 deleted file mode 100644 index 45c30a41..00000000 --- a/python/test-data/testForwardRef5.err-3.12 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testForwardRef5.py", line 23, in - for car in garage.cars: -WyppTypeError: list is not a list[Car] -given: [Car(color='red'), 'Not A Car'] -expected: value of type list[Car] - -context: record constructor Garage(cars: list[Car]) -> Self - ^^^^^^^^^ -declared at: test-data/testForwardRef5.py:9 -caused by: test-data/testForwardRef5.py:23 - | for car in garage.cars: diff --git a/python/test-data/testFunEq.out b/python/test-data/testFunEq.out deleted file mode 100644 index 45440caf..00000000 --- a/python/test-data/testFunEq.out +++ /dev/null @@ -1,2 +0,0 @@ -FEHLER in test-data/testFunEq.py:9: Erwartet wird , aber das Ergebnis ist -1 Test, 1 Fehler 🙁 diff --git a/python/test-data/testGetSource.err b/python/test-data/testGetSource.err deleted file mode 100644 index 4e07caab..00000000 --- a/python/test-data/testGetSource.err +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testGetSource.py", line 11, in - Art = Literal('klein','mittag') # <= problem is here -untypy.error.WyppTypeError: Cannot call Literal like a function. Did you mean Literal['klein', 'mittag']? diff --git a/python/test-data/testHintParentheses1.err b/python/test-data/testHintParentheses1.err deleted file mode 100644 index ed624e35..00000000 --- a/python/test-data/testHintParentheses1.err +++ /dev/null @@ -1,10 +0,0 @@ -Traceback (most recent call last): - File "test-data/testHintParentheses1.py", line 5, in - def foo(l: list(int)) -> int: -WyppAttributeError: Type annotation of function 'foo' could not be resolved: -'type' object is not iterable - -def foo(l: list(int)) -> int: - return ^^^^^^^^^ - Did you mean: 'list[int]'? - -declared at: test-data/testHintParentheses1.py:5 diff --git a/python/test-data/testHintParentheses2.err b/python/test-data/testHintParentheses2.err deleted file mode 100644 index 9bba0373..00000000 --- a/python/test-data/testHintParentheses2.err +++ /dev/null @@ -1,10 +0,0 @@ -Traceback (most recent call last): - File "test-data/testHintParentheses2.py", line 5, in - def foo(a: 'int', b: 'dict[1, list(int)]') -> int: -WyppAttributeError: Type annotation of function 'foo' could not be resolved: -'type' object is not iterable - -def foo(a: 'int', b: 'dict[1, list(int)]') -> int: - return len(l) ^^^^^^^^^ - Did you mean: 'list[int]'? - -declared at: test-data/testHintParentheses2.py:5 diff --git a/python/test-data/testHintParentheses3.err b/python/test-data/testHintParentheses3.err deleted file mode 100644 index 5ac7b6ba..00000000 --- a/python/test-data/testHintParentheses3.err +++ /dev/null @@ -1,10 +0,0 @@ -Traceback (most recent call last): - File "test-data/testHintParentheses3.py", line 6, in - def foo() -> Union(list, str): -WyppAttributeError: Type annotation of function 'foo' could not be resolved: -Cannot call Union like a function. Did you mean Union[list, str]? - -def foo() -> Union(list, str): - pass ^^^^^^^^^^^^^^^^ - Did you mean: 'Union[list, str]'? - -declared at: test-data/testHintParentheses3.py:6 diff --git a/python/test-data/testImpossible.err b/python/test-data/testImpossible.err deleted file mode 100644 index a9195b86..00000000 --- a/python/test-data/testImpossible.err +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testImpossible.py", line 3, in - impossible() -wypp.writeYourProgram.ImpossibleError: the impossible happened diff --git a/python/test-data/testIndexError.err b/python/test-data/testIndexError.err deleted file mode 100644 index e39537b0..00000000 --- a/python/test-data/testIndexError.err +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testIndexError.py", line 6, in - foo([1,2,3]) - File "test-data/testIndexError.py", line 3, in foo - x = l[42] -IndexError: list index out of range diff --git a/python/test-data/testInferReturnType2.err b/python/test-data/testInferReturnType2.err deleted file mode 100644 index 8f93d5b4..00000000 --- a/python/test-data/testInferReturnType2.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testInferReturnType2.py", line 6, in - foo(2) # not OK -WyppTypeError: got value of wrong type -given: 'yuck' -expected: None - -context: foo(i: int) -> None - ^^^^ -declared at: test-data/testInferReturnType2.py:1 -caused by: test-data/testInferReturnType2.py:3 - | return "yuck" diff --git a/python/test-data/testInferReturnType2.py b/python/test-data/testInferReturnType2.py deleted file mode 100644 index e9405bba..00000000 --- a/python/test-data/testInferReturnType2.py +++ /dev/null @@ -1,6 +0,0 @@ -def foo(i: int): - if i == 2: - return "yuck" - -foo(1) # OK -foo(2) # not OK diff --git a/python/test-data/testInferReturnType3.py b/python/test-data/testInferReturnType3.py deleted file mode 100644 index 043c28a5..00000000 --- a/python/test-data/testInferReturnType3.py +++ /dev/null @@ -1,22 +0,0 @@ -from wypp import * -from typing import Protocol -import abc - -class Animal(Protocol): - @abc.abstractmethod - def makeSound(self, loadness: float): - pass - -class Dog: - def makeSound(self, loadness: float) -> None: - pass - -class Cat: - def makeSound(self, loadness: float): - pass - -def foo(animal: Animal): - animal.makeSound(1.0) - -foo(Dog()) -foo(Cat()) diff --git a/python/test-data/testInferReturnType4.err b/python/test-data/testInferReturnType4.err deleted file mode 100644 index 65f963fe..00000000 --- a/python/test-data/testInferReturnType4.err +++ /dev/null @@ -1,28 +0,0 @@ -Traceback (most recent call last): - File "test-data/testInferReturnType4.py", line 17, in - foo(Dog()) - File "test-data/testInferReturnType4.py", line 15, in foo - animal.makeSound(1.0) -WyppTypeError: Dog does not implement protocol Animal -given: <__wypp__.Dog object at 0x10b81e710> -expected: value of type Animal - -context: foo(animal: Animal) -> None - ^^^^^^ -declared at: test-data/testInferReturnType4.py:14 -caused by: test-data/testInferReturnType4.py:17 - | foo(Dog()) - -The return value of method 'makeSound' does violate the protocol 'Animal'. -The annotation 'int' is incompatible with the protocol's annotation 'None' -when checking against the following value: - -given: 1 -expected: None - -context: makeSound(self: Self, loadness: float) -> None - ^^^^ -declared at: test-data/testInferReturnType4.py:11 -declared at: test-data/testInferReturnType4.py:7 -caused by: test-data/testInferReturnType4.py:12 - | return 1 diff --git a/python/test-data/testInferReturnType4.py b/python/test-data/testInferReturnType4.py deleted file mode 100644 index e083d14e..00000000 --- a/python/test-data/testInferReturnType4.py +++ /dev/null @@ -1,17 +0,0 @@ -from wypp import * -from typing import Protocol -import abc - -class Animal(Protocol): - @abc.abstractmethod - def makeSound(self, loadness: float): - pass - -class Dog: - def makeSound(self, loadness: float) -> int: - return 1 - -def foo(animal: Animal): - animal.makeSound(1.0) - -foo(Dog()) diff --git a/python/test-data/testInvalidLiteral.err b/python/test-data/testInvalidLiteral.err deleted file mode 100644 index 084153ae..00000000 --- a/python/test-data/testInvalidLiteral.err +++ /dev/null @@ -1,5 +0,0 @@ -Traceback (most recent call last): - File "test-data/testInvalidLiteral.py", line 5, in - def gameFull(game:Game)->bool: -WyppAnnotationError: Invalid type annotation: list[list[['x', 'o', '-']]] - diff --git a/python/test-data/testIterable7.err b/python/test-data/testIterable7.err deleted file mode 100644 index 7cea4234..00000000 --- a/python/test-data/testIterable7.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testIterable7.py", line 10, in - print(list(i)) -WyppTypeError: generator is not an Iterable[str] -given: -expected: value of type Iterable[str] - -context: foo(i: int) -> Iterable[str] - ^^^^^^^^^^^^^ -declared at: test-data/testIterable7.py:4 -caused by: test-data/testIterable7.py:4 - | def foo(i: int) -> Iterable[str]: diff --git a/python/test-data/testIterable7.out b/python/test-data/testIterable7.out deleted file mode 100644 index 82a4ce68..00000000 --- a/python/test-data/testIterable7.out +++ /dev/null @@ -1 +0,0 @@ -start of foo diff --git a/python/test-data/testIterableImplicitAny.err b/python/test-data/testIterableImplicitAny.err deleted file mode 100644 index 46f37bc8..00000000 --- a/python/test-data/testIterableImplicitAny.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testIterableImplicitAny.py", line 13, in - foo(NotIterable()) -WyppTypeError: got value of wrong type -given: NotIterable -expected: value of type Iterable - -context: foo(it: Iterable) -> int - ^^^^^^^^ -declared at: test-data/testIterableImplicitAny.py:7 -caused by: test-data/testIterableImplicitAny.py:13 - | foo(NotIterable()) diff --git a/python/test-data/testLiteral1.err b/python/test-data/testLiteral1.err deleted file mode 100644 index 1805a4e8..00000000 --- a/python/test-data/testLiteral1.err +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testLiteral1.py", line 3, in - T = Literal('a', 'b') -untypy.error.WyppTypeError: Cannot call Literal like a function. Did you mean Literal['a', 'b']? diff --git a/python/test-data/testLockFactory.err b/python/test-data/testLockFactory.err deleted file mode 100644 index dd8c2e9d..00000000 --- a/python/test-data/testLockFactory.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testLockFactory.py", line 15, in - foo("not a lock") -WyppTypeError: got value of wrong type -given: 'not a lock' -expected: value of type Callable[[], Lock] - -context: foo(lock: LockFactory) -> None - ^^^^^^^^^^^ -declared at: test-data/testLockFactory.py:6 -caused by: test-data/testLockFactory.py:15 - | foo("not a lock") diff --git a/python/test-data/testLockFactory2.err b/python/test-data/testLockFactory2.err deleted file mode 100644 index dea8d5ad..00000000 --- a/python/test-data/testLockFactory2.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testLockFactory2.py", line 14, in - foo("not a lock") -WyppTypeError: str does not implement protocol Lock -given: 'not a lock' -expected: value of type Lock - -context: foo(l: Lock) -> None - ^^^^ -declared at: test-data/testLockFactory2.py:6 -caused by: test-data/testLockFactory2.py:14 - | foo("not a lock") diff --git a/python/test-data/testMissingReturn.err b/python/test-data/testMissingReturn.err deleted file mode 100644 index b3d3300b..00000000 --- a/python/test-data/testMissingReturn.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testMissingReturn.py", line 6, in - print(billigStrom(500)) -WyppTypeError: got value of wrong type -Did you miss a return statement? - -given: None -expected: value of type float - -context: billigStrom(kwh: float) -> float - ^^^^^ -declared at: test-data/testMissingReturn.py:3 -caused by: test-data/testMissingReturn.py:4 - | pass diff --git a/python/test-data/testNums.err b/python/test-data/testNums.err deleted file mode 100644 index 6ae51a3b..00000000 --- a/python/test-data/testNums.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testNums.py", line 8, in - f(-4) -WyppTypeError: got value of wrong type -given: -4 -expected: value of type floatNonNegative - -context: f(x: floatNonNegative) -> float - ^^^^^^^^^^^^^^^^ -declared at: test-data/testNums.py:3 -caused by: test-data/testNums.py:8 - | f(-4) diff --git a/python/test-data/testNums.py b/python/test-data/testNums.py deleted file mode 100644 index 3e8cc62f..00000000 --- a/python/test-data/testNums.py +++ /dev/null @@ -1,8 +0,0 @@ -from wypp import * - -def f(x: floatNonNegative) -> float: - return x - -f(0.0) -f(1) -f(-4) diff --git a/python/test-data/testOriginalTypeNames.out b/python/test-data/testOriginalTypeNames.out deleted file mode 100644 index cff31dc1..00000000 --- a/python/test-data/testOriginalTypeNames.out +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python/test-data/testRecordSetTypeForwardRef.err b/python/test-data/testRecordSetTypeForwardRef.err deleted file mode 100644 index f1494801..00000000 --- a/python/test-data/testRecordSetTypeForwardRef.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypeForwardRef.py", line 15, in - m() - File "test-data/testRecordSetTypeForwardRef.py", line 13, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type A - -declared at: test-data/testRecordSetTypeForwardRef.py:5 -caused by: test-data/testRecordSetTypeForwardRef.py:13 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypeForwardRef.err-3.10 b/python/test-data/testRecordSetTypeForwardRef.err-3.10 deleted file mode 100644 index 361ff706..00000000 --- a/python/test-data/testRecordSetTypeForwardRef.err-3.10 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypeForwardRef.py", line 14, in - m() - File "test-data/testRecordSetTypeForwardRef.py", line 12, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type A - -declared at: test-data/testRecordSetTypeForwardRef.py:3 -caused by: test-data/testRecordSetTypeForwardRef.py:12 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypeForwardRef.err-3.11 b/python/test-data/testRecordSetTypeForwardRef.err-3.11 deleted file mode 100644 index 361ff706..00000000 --- a/python/test-data/testRecordSetTypeForwardRef.err-3.11 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypeForwardRef.py", line 14, in - m() - File "test-data/testRecordSetTypeForwardRef.py", line 12, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type A - -declared at: test-data/testRecordSetTypeForwardRef.py:3 -caused by: test-data/testRecordSetTypeForwardRef.py:12 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypeForwardRef.err-3.12 b/python/test-data/testRecordSetTypeForwardRef.err-3.12 deleted file mode 100644 index 3068bc74..00000000 --- a/python/test-data/testRecordSetTypeForwardRef.err-3.12 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypeForwardRef.py", line 15, in - m() - File "test-data/testRecordSetTypeForwardRef.py", line 13, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type A - -declared at: test-data/testRecordSetTypeForwardRef.py:4 -caused by: test-data/testRecordSetTypeForwardRef.py:13 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypes.err b/python/test-data/testRecordSetTypes.err deleted file mode 100644 index 04ffa90e..00000000 --- a/python/test-data/testRecordSetTypes.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypes.py", line 12, in - m() - File "test-data/testRecordSetTypes.py", line 10, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type int - -declared at: test-data/testRecordSetTypes.py:4 -caused by: test-data/testRecordSetTypes.py:10 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypes.err-3.10 b/python/test-data/testRecordSetTypes.err-3.10 deleted file mode 100644 index 3a5b1593..00000000 --- a/python/test-data/testRecordSetTypes.err-3.10 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypes.py", line 12, in - m() - File "test-data/testRecordSetTypes.py", line 10, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type int - -declared at: test-data/testRecordSetTypes.py:3 -caused by: test-data/testRecordSetTypes.py:10 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypes.err-3.11 b/python/test-data/testRecordSetTypes.err-3.11 deleted file mode 100644 index 3a5b1593..00000000 --- a/python/test-data/testRecordSetTypes.err-3.11 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypes.py", line 12, in - m() - File "test-data/testRecordSetTypes.py", line 10, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type int - -declared at: test-data/testRecordSetTypes.py:3 -caused by: test-data/testRecordSetTypes.py:10 - | r.x = "hello" diff --git a/python/test-data/testRecordSetTypes.err-3.12 b/python/test-data/testRecordSetTypes.err-3.12 deleted file mode 100644 index 3a5b1593..00000000 --- a/python/test-data/testRecordSetTypes.err-3.12 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordSetTypes.py", line 12, in - m() - File "test-data/testRecordSetTypes.py", line 10, in m - r.x = "hello" -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type int - -declared at: test-data/testRecordSetTypes.py:3 -caused by: test-data/testRecordSetTypes.py:10 - | r.x = "hello" diff --git a/python/test-data/testRecordTypes.err b/python/test-data/testRecordTypes.err deleted file mode 100644 index 84554590..00000000 --- a/python/test-data/testRecordTypes.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordTypes.py", line 8, in - p = Point(1, '5') -WyppTypeError: got value of wrong type -given: '5' -expected: value of type int - -context: record constructor Point(x: int, y: int) -> Self - ^^^ -declared at: test-data/testRecordTypes.py:4 -caused by: test-data/testRecordTypes.py:8 - | p = Point(1, '5') diff --git a/python/test-data/testRecordTypes.err-3.12 b/python/test-data/testRecordTypes.err-3.12 deleted file mode 100644 index 37fbf43c..00000000 --- a/python/test-data/testRecordTypes.err-3.12 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testRecordTypes.py", line 8, in - p = Point(1, '5') -WyppTypeError: got value of wrong type -given: '5' -expected: value of type int - -context: record constructor Point(x: int, y: int) -> Self - ^^^ -declared at: test-data/testRecordTypes.py:3 -caused by: test-data/testRecordTypes.py:8 - | p = Point(1, '5') diff --git a/python/test-data/testTodo.err b/python/test-data/testTodo.err deleted file mode 100644 index c19527ae..00000000 --- a/python/test-data/testTodo.err +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTodo.py", line 3, in - todo() -wypp.writeYourProgram.TodoError: TODO diff --git a/python/test-data/testTraceback.err b/python/test-data/testTraceback.err deleted file mode 100644 index cd5a0661..00000000 --- a/python/test-data/testTraceback.err +++ /dev/null @@ -1,6 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTraceback.py", line 9, in - foo(lst) - File "test-data/testTraceback.py", line 7, in foo - print(lst[10]) -IndexError: list index out of range diff --git a/python/test-data/testTraceback2.err-3.10.0 b/python/test-data/testTraceback2.err-3.10.0 deleted file mode 100644 index 2ac0a7ee..00000000 --- a/python/test-data/testTraceback2.err-3.10.0 +++ /dev/null @@ -1,4 +0,0 @@ - File "test-data/testTraceback2.py", line 3 - lst = [1,2,3 - -SyntaxError: '[' was never closed diff --git a/python/test-data/testTraceback2.err-3.9 b/python/test-data/testTraceback2.err-3.9 deleted file mode 100644 index 714d0a88..00000000 --- a/python/test-data/testTraceback2.err-3.9 +++ /dev/null @@ -1,4 +0,0 @@ - File "test-data/testTraceback2.py", line 3 - lst = [1,2,3 - ^ -SyntaxError: unexpected EOF while parsing diff --git a/python/test-data/testTraceback3.err b/python/test-data/testTraceback3.err deleted file mode 100644 index 6a207b3e..00000000 --- a/python/test-data/testTraceback3.err +++ /dev/null @@ -1,4 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTraceback3.py", line 2, in - print([1,2,3][10]) -IndexError: list index out of range diff --git a/python/test-data/testTypes1.err b/python/test-data/testTypes1.err deleted file mode 100644 index bd64b177..00000000 --- a/python/test-data/testTypes1.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypes1.py", line 4, in - inc("1") -WyppTypeError: got value of wrong type -given: '1' -expected: value of type int - -context: inc(x: int) -> int - ^^^ -declared at: test-data/testTypes1.py:1 -caused by: test-data/testTypes1.py:4 - | inc("1") diff --git a/python/test-data/testTypes2.err b/python/test-data/testTypes2.err deleted file mode 100644 index 18c2778b..00000000 --- a/python/test-data/testTypes2.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypes2.py", line 4, in - inc("1") -WyppTypeError: got value of wrong type -given: '1' -expected: value of type int - -context: inc(x: int) -> int - ^^^ -declared at: test-data/testTypes2.py:1 -caused by: test-data/testTypes2.py:4 - | inc("1") diff --git a/python/test-data/testTypesCollections1.err b/python/test-data/testTypesCollections1.err deleted file mode 100644 index 773e0b8e..00000000 --- a/python/test-data/testTypesCollections1.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesCollections1.py", line 7, in - appendSomething(l) - File "test-data/testTypesCollections1.py", line 4, in appendSomething - l.append("foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: append(self, object: int) - ^^^ -declared at: site-lib/untypy/impl/interfaces/list.py:17 -caused by: test-data/testTypesCollections1.py:4 - | l.append("foo") diff --git a/python/test-data/testTypesCollections1.py b/python/test-data/testTypesCollections1.py deleted file mode 100644 index be569e10..00000000 --- a/python/test-data/testTypesCollections1.py +++ /dev/null @@ -1,8 +0,0 @@ -from wypp import * - -def appendSomething(l: list[int]) -> None: - l.append("foo") - -l = [1,2,3] -appendSomething(l) -print(l) diff --git a/python/test-data/testTypesCollections2.err b/python/test-data/testTypesCollections2.err deleted file mode 100644 index 472a9d3a..00000000 --- a/python/test-data/testTypesCollections2.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesCollections2.py", line 10, in - foo([lambda: "1", lambda: 42]) # error because the 2nd functions returns an int - File "test-data/testTypesCollections2.py", line 6, in foo - res.append(f()) -WyppTypeError: list is not a list[Callable[[], str]] -given: [ at 0x00>, at 0x00>] -expected: value of type list[Callable[[], str]] - -context: foo(l: list[Callable[[], str]]) -> list[str] - ^^^^^^^^^^^^^^^^^^^^^^^ -declared at: test-data/testTypesCollections2.py:3 -caused by: test-data/testTypesCollections2.py:10 - | foo([lambda: "1", lambda: 42]) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesCollections2.out b/python/test-data/testTypesCollections2.out deleted file mode 100644 index 189744a2..00000000 --- a/python/test-data/testTypesCollections2.out +++ /dev/null @@ -1 +0,0 @@ -['1', '2'] diff --git a/python/test-data/testTypesCollections2.py b/python/test-data/testTypesCollections2.py deleted file mode 100644 index 4e9c4997..00000000 --- a/python/test-data/testTypesCollections2.py +++ /dev/null @@ -1,10 +0,0 @@ -from wypp import * - -def foo(l: list[Callable[[], str]]) -> list[str]: - res = [] - for f in l: - res.append(f()) - return res - -print(foo([lambda: "1", lambda: "2"])) -foo([lambda: "1", lambda: 42]) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesCollections3.err b/python/test-data/testTypesCollections3.err deleted file mode 100644 index b6d40c46..00000000 --- a/python/test-data/testTypesCollections3.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesCollections3.py", line 7, in - appendSomething(l, []) - File "test-data/testTypesCollections3.py", line 4, in appendSomething - l.append("foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: append(self, object: int) - ^^^ -declared at: site-lib/untypy/impl/interfaces/list.py:17 -caused by: test-data/testTypesCollections3.py:4 - | l.append("foo") diff --git a/python/test-data/testTypesCollections3.py b/python/test-data/testTypesCollections3.py deleted file mode 100644 index 0bd390ec..00000000 --- a/python/test-data/testTypesCollections3.py +++ /dev/null @@ -1,8 +0,0 @@ -from wypp import * - -def appendSomething(l: list[int], l2: list[int]) -> None: - l.append("foo") - -l = [1,2,3] -appendSomething(l, []) -print(l) diff --git a/python/test-data/testTypesCollections4.err b/python/test-data/testTypesCollections4.err deleted file mode 100644 index 2b2ba1b3..00000000 --- a/python/test-data/testTypesCollections4.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesCollections4.py", line 11, in - foo([func]) - File "test-data/testTypesCollections4.py", line 7, in foo - res.append(f()) -WyppTypeError: got value of wrong type -given: 42 -expected: value of type str - -context: __next__(self: Self) -> Callable[[], str] - ^^^ -declared at: test-data/testTypesCollections4.py:4 -caused by: test-data/testTypesCollections4.py:4 - | l.append(lambda: 42) # error diff --git a/python/test-data/testTypesCollections4.py b/python/test-data/testTypesCollections4.py deleted file mode 100644 index 6e410ed2..00000000 --- a/python/test-data/testTypesCollections4.py +++ /dev/null @@ -1,11 +0,0 @@ -from wypp import * - -def foo(l: list[Callable[[], str]]) -> list[str]: - l.append(lambda: 42) # error - res = [] - for f in l: - res.append(f()) - return res - -func = lambda: "xxx" -foo([func]) diff --git a/python/test-data/testTypesCollections5.err b/python/test-data/testTypesCollections5.err deleted file mode 100644 index ff67384d..00000000 --- a/python/test-data/testTypesCollections5.err +++ /dev/null @@ -1,16 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesCollections5.py", line 11, in - foo([func]) # error - File "test-data/testTypesCollections5.py", line 7, in foo - res.append(f()) -WyppTypeError: got value of wrong type -given: 42 -expected: value of type str - -context: foo(l: list[Callable[[], str]]) -> list[str] - ^^^^^^^^^^^^^^^^^^^^^^^ -declared at: test-data/testTypesCollections5.py:3 -caused by: test-data/testTypesCollections5.py:10 - | func = lambda: 42 -caused by: test-data/testTypesCollections5.py:11 - | foo([func]) # error diff --git a/python/test-data/testTypesCollections5.py b/python/test-data/testTypesCollections5.py deleted file mode 100644 index bbc5001f..00000000 --- a/python/test-data/testTypesCollections5.py +++ /dev/null @@ -1,11 +0,0 @@ -from wypp import * - -def foo(l: list[Callable[[], str]]) -> list[str]: - l.append(lambda: 'x') # ok - res = [] - for f in l: - res.append(f()) - return res - -func = lambda: 42 -foo([func]) # error diff --git a/python/test-data/testTypesDict1.err b/python/test-data/testTypesDict1.err deleted file mode 100644 index 4abbc36a..00000000 --- a/python/test-data/testTypesDict1.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesDict1.py", line 7, in - appendSomething(d) - File "test-data/testTypesDict1.py", line 4, in appendSomething - d['x'] = "foo" -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: __setitem__(self, key, value: int) - ^^^ -declared at: site-lib/untypy/impl/interfaces/dict.py:67 -caused by: test-data/testTypesDict1.py:4 - | d['x'] = "foo" diff --git a/python/test-data/testTypesDict1.py b/python/test-data/testTypesDict1.py deleted file mode 100644 index 09866b3f..00000000 --- a/python/test-data/testTypesDict1.py +++ /dev/null @@ -1,8 +0,0 @@ -from wypp import * - -def appendSomething(d: dict[str, int]) -> None: - d['x'] = "foo" - -d = {'1': 1, '2': 2} -appendSomething(d) -print(d) diff --git a/python/test-data/testTypesDict2.err b/python/test-data/testTypesDict2.err deleted file mode 100644 index 0887b3ac..00000000 --- a/python/test-data/testTypesDict2.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesDict2.py", line 10, in - foo({'x': lambda: "1", 'y': lambda: 42}) # error because the 2nd functions returns an int - File "test-data/testTypesDict2.py", line 6, in foo - res.append(f()) -WyppTypeError: dict is not a dict[str, Callable[[], str]] -given: {'x': at 0x00>, 'y': at 0x00>} -expected: value of type dict[str, Callable[[], str]] - -context: foo(d: dict[str, Callable[[], str]]) -> list[str] - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -declared at: test-data/testTypesDict2.py:3 -caused by: test-data/testTypesDict2.py:10 - | foo({'x': lambda: "1", 'y': lambda: 42}) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesDict2.out b/python/test-data/testTypesDict2.out deleted file mode 100644 index 189744a2..00000000 --- a/python/test-data/testTypesDict2.out +++ /dev/null @@ -1 +0,0 @@ -['1', '2'] diff --git a/python/test-data/testTypesDict2.py b/python/test-data/testTypesDict2.py deleted file mode 100644 index b2d4f129..00000000 --- a/python/test-data/testTypesDict2.py +++ /dev/null @@ -1,10 +0,0 @@ -from wypp import * - -def foo(d: dict[str, Callable[[], str]]) -> list[str]: - res = [] - for _, f in d.items(): - res.append(f()) - return res - -print(foo({'x': lambda: "1", 'y': lambda: "2"})) -foo({'x': lambda: "1", 'y': lambda: 42}) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesDict3.err b/python/test-data/testTypesDict3.err deleted file mode 100644 index 3cb5e1ea..00000000 --- a/python/test-data/testTypesDict3.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesDict3.py", line 11, in - foo({'y': func}) - File "test-data/testTypesDict3.py", line 7, in foo - res.append(f()) -WyppTypeError: got value of wrong type -given: 42 -expected: value of type str - -context: __next__(self: Self) -> tuple[str, Callable[[], str]] - ^^^ -declared at: test-data/testTypesDict3.py:4 -caused by: test-data/testTypesDict3.py:4 - | d['x'] = lambda: 42 # error diff --git a/python/test-data/testTypesDict4.err b/python/test-data/testTypesDict4.err deleted file mode 100644 index 14446596..00000000 --- a/python/test-data/testTypesDict4.err +++ /dev/null @@ -1,16 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesDict4.py", line 14, in - bar({'y': func}) # error - File "test-data/testTypesDict4.py", line 11, in bar - return foo(d) - File "test-data/testTypesDict4.py", line 7, in foo - res.append(f()) -WyppTypeError: dict is not a dict[str, Callable[[], str]] -given: {'y': at 0x00>, 'x': } -expected: value of type dict[str, Callable[[], str]] - -context: foo(d: dict[str, Callable[[], str]]) -> list[str] - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -declared at: test-data/testTypesDict4.py:3 -caused by: test-data/testTypesDict4.py:11 - | return foo(d) diff --git a/python/test-data/testTypesHigherOrderFuns.err b/python/test-data/testTypesHigherOrderFuns.err deleted file mode 100644 index 977eef08..00000000 --- a/python/test-data/testTypesHigherOrderFuns.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesHigherOrderFuns.py", line 10, in - map(["hello", "1"], lambda x: x) - File "test-data/testTypesHigherOrderFuns.py", line 6, in map - res.append(fun(x)) -WyppTypeError: got value of wrong type -given: 'hello' -expected: value of type int - -context: map(container: Iterable[str], fun: Callable[[str], int]) -> list[int] - ^^^ -declared at: test-data/testTypesHigherOrderFuns.py:3 -caused by: test-data/testTypesHigherOrderFuns.py:10 - | map(["hello", "1"], lambda x: x) diff --git a/python/test-data/testTypesHigherOrderFuns3.err b/python/test-data/testTypesHigherOrderFuns3.err deleted file mode 100644 index 7600c7ea..00000000 --- a/python/test-data/testTypesHigherOrderFuns3.err +++ /dev/null @@ -1,17 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesHigherOrderFuns3.py", line 41, in - check(homePoints(game1), 0) - File "test-data/testTypesHigherOrderFuns3.py", line 36, in - return lambda game: gamePoints(game, cmp) - File "test-data/testTypesHigherOrderFuns3.py", line 30, in gamePoints - elif cmp(h, g): -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type bool - -context: gamePoints(game: GameResult, cmp: Callable[[int, int], bool]) -> int - ^^^^ -declared at: test-data/testTypesHigherOrderFuns3.py:38 -declared at: test-data/testTypesHigherOrderFuns3.py:25 -caused by: test-data/testTypesHigherOrderFuns3.py:38 - | homePoints: Callable[[GameResult], int] = mkGamePoints(lambda g, h: "foo") diff --git a/python/test-data/testTypesProtos1.err b/python/test-data/testTypesProtos1.err deleted file mode 100644 index bcc00914..00000000 --- a/python/test-data/testTypesProtos1.err +++ /dev/null @@ -1,27 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos1.py", line 21, in - doSomething(Dog()) - File "test-data/testTypesProtos1.py", line 19, in doSomething - print(a.makeSound(3.14)) -WyppTypeError: Dog does not implement protocol Animal -given: -expected: value of type Animal - -context: doSomething(a: Animal) -> None - ^^^^^^ -declared at: test-data/testTypesProtos1.py:18 -caused by: test-data/testTypesProtos1.py:21 - | doSomething(Dog()) - -Argument loadness of method makeSound violates the type declared by the protocol Animal. -Annotation int is incompatible with the protocol's annotation float. - -given: 3.14 -expected: value of type int - -context: makeSound(self: Self, loadness: int) -> str - ^^^ -declared at: test-data/testTypesProtos1.py:13 -declared at: test-data/testTypesProtos1.py:8 -caused by: test-data/testTypesProtos1.py:13 - | def makeSound(self, loadness: int) -> str: diff --git a/python/test-data/testTypesProtos2.err b/python/test-data/testTypesProtos2.err deleted file mode 100644 index 2af72e27..00000000 --- a/python/test-data/testTypesProtos2.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos2.py", line 21, in - doSomething(Dog()) - File "test-data/testTypesProtos2.py", line 19, in doSomething - print(a.makeSound(3.14)) -WyppTypeError: too many positional arguments -Hint: 'self'-parameter was omitted in declaration. - -context: makeSound(loadness: float) -> str -declared at: test-data/testTypesProtos2.py:8 -caused by: test-data/testTypesProtos2.py:19 - | print(a.makeSound(3.14)) diff --git a/python/test-data/testTypesProtos3.err b/python/test-data/testTypesProtos3.err deleted file mode 100644 index 0a3b18e1..00000000 --- a/python/test-data/testTypesProtos3.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos3.py", line 21, in - doSomething(Dog()) -WyppTypeError: Dog does not implement protocol Animal -The signature of 'makeSound' does not match. Missing required parameter self. - -given: -expected: value of type Animal - -context: doSomething(a: Animal) -> None - ^^^^^^ -declared at: test-data/testTypesProtos3.py:18 -caused by: test-data/testTypesProtos3.py:21 - | doSomething(Dog()) diff --git a/python/test-data/testTypesProtos4.err b/python/test-data/testTypesProtos4.err deleted file mode 100644 index d47cc1ff..00000000 --- a/python/test-data/testTypesProtos4.err +++ /dev/null @@ -1,16 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos4.py", line 27, in - print(foo(ConcreteWrong())) - File "test-data/testTypesProtos4.py", line 24, in foo - return fn(2) - File "test-data/testTypesProtos4.py", line 20, in - return lambda x: bar(x) # invalid call of bar with argument of type int -WyppTypeError: got value of wrong type -given: 2 -expected: value of type str - -context: bar(s: str) -> int - ^^^ -declared at: test-data/testTypesProtos4.py:15 -caused by: test-data/testTypesProtos4.py:20 - | return lambda x: bar(x) # invalid call of bar with argument of type int diff --git a/python/test-data/testTypesProtos6.err b/python/test-data/testTypesProtos6.err deleted file mode 100644 index 20a107ce..00000000 --- a/python/test-data/testTypesProtos6.err +++ /dev/null @@ -1,34 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos6.py", line 57, in - print(computeTotalSize(root)) - File "test-data/testTypesProtos6.py", line 50, in computeTotalSize - fs.accept(visitor) - File "test-data/testTypesProtos6.py", line 19, in accept - visitor.visitDirectory(self) - File "test-data/testTypesProtos6.py", line 41, in visitDirectory - c.accept(self) - File "test-data/testTypesProtos6.py", line 28, in accept - visitor.visitFile(self) -WyppTypeError: TotalSizeVisitor does not implement parent FileSystemVisitor -given: <__wypp__.TotalSizeVisitor object at 0x00> -expected: value of type FileSystemVisitor - -declared at: test-data/testTypesProtos6.py:36 -declared at: test-data/testTypesProtos6.py:33 -caused by: test-data/testTypesProtos6.py:36 - | class TotalSizeVisitor(FileSystemVisitor): -caused by: test-data/testTypesProtos6.py:42 - | def visitFile(self, file: str): - -Argument file of method visitFile violates the type declared by the parent FileSystemVisitor. -Annotation str is incompatible with the parent's annotation File. - -given: <__wypp__.File object at 0x00> -expected: value of type str - -context: visitFile(self: Self, file: str) -> None - ^^^ -declared at: test-data/testTypesProtos6.py:42 -declared at: test-data/testTypesProtos6.py:33 -caused by: test-data/testTypesProtos6.py:42 - | def visitFile(self, file: str): diff --git a/python/test-data/testTypesProtos7.err b/python/test-data/testTypesProtos7.err deleted file mode 100644 index a15b77bc..00000000 --- a/python/test-data/testTypesProtos7.err +++ /dev/null @@ -1,33 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos7.py", line 76, in - print(computeTotalSize(root)) - File "test-data/testTypesProtos7.py", line 69, in computeTotalSize - fs.accept(visitor) - File "test-data/testTypesProtos7.py", line 36, in accept - visitor.visitDirectory(self) - File "test-data/testTypesProtos7.py", line 60, in visitDirectory - c.accept(self) - File "test-data/testTypesProtos7.py", line 47, in accept - visitor.visitFile(self) -WyppTypeError: TotalSizeVisitor does not implement parent FileSystemVisitor -given: <__wypp__.TotalSizeVisitor object at 0x00> -expected: value of type FileSystemVisitor - -declared at: test-data/testTypesProtos7.py:55 -declared at: test-data/testTypesProtos7.py:52 -caused by: test-data/testTypesProtos7.py:55 - | class TotalSizeVisitor(FileSystemVisitor): -caused by: test-data/testTypesProtos7.py:61 - | def visitFile(self, f: str): - -Argument f of method visitFile violates the type declared for file in parent FileSystemVisitor. -Annotation str is incompatible with the parent's annotation File. - -given: File('notes.txt') -expected: value of type str - -context: visitFile(self: Self, f: File) -> None -declared at: test-data/testTypesProtos7.py:61 -declared at: test-data/testTypesProtos7.py:52 -caused by: test-data/testTypesProtos7.py:61 - | def visitFile(self, f: str): diff --git a/python/test-data/testTypesProtos8.err b/python/test-data/testTypesProtos8.err deleted file mode 100644 index e7375274..00000000 --- a/python/test-data/testTypesProtos8.err +++ /dev/null @@ -1,28 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos8.py", line 12, in - bar(Sub()) - File "test-data/testTypesProtos8.py", line 10, in bar - b.foo(1, "foo") -WyppTypeError: Sub does not implement parent Base -given: <__wypp__.Sub object at 0x00> -expected: value of type Base - -declared at: test-data/testTypesProtos8.py:5 -declared at: test-data/testTypesProtos8.py:2 -caused by: test-data/testTypesProtos8.py:5 - | class Sub(Base): -caused by: test-data/testTypesProtos8.py:6 - | def foo(self, y: int, x: float): - -Argument x of method foo violates the type declared for y in parent Base. -Annotation float is incompatible with the parent's annotation str. - -given: 'foo' -expected: value of type float - -context: foo(self: Self, y: float, x: str) -> None - ^^^^^ -declared at: test-data/testTypesProtos8.py:6 -declared at: test-data/testTypesProtos8.py:2 -caused by: test-data/testTypesProtos8.py:6 - | def foo(self, y: int, x: float): diff --git a/python/test-data/testTypesProtos9.err b/python/test-data/testTypesProtos9.err deleted file mode 100644 index 9785e612..00000000 --- a/python/test-data/testTypesProtos9.err +++ /dev/null @@ -1,27 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesProtos9.py", line 12, in - bar(Sub()) - File "test-data/testTypesProtos9.py", line 10, in bar - b.foo(1, "foo") -WyppTypeError: Sub does not implement parent Base -given: <__wypp__.Sub object at 0x00> -expected: value of type Base - -declared at: test-data/testTypesProtos9.py:5 -declared at: test-data/testTypesProtos9.py:2 -caused by: test-data/testTypesProtos9.py:5 - | class Sub(Base): -caused by: test-data/testTypesProtos9.py:6 - | def foo(self, subX: int, subY: float): - -Argument subY of method foo violates the type declared for y in parent Base. -Annotation float is incompatible with the parent's annotation str. - -given: 'foo' -expected: value of type float - -context: foo(self: Self, subX: int, subY: str) -> None -declared at: test-data/testTypesProtos9.py:6 -declared at: test-data/testTypesProtos9.py:2 -caused by: test-data/testTypesProtos9.py:6 - | def foo(self, subX: int, subY: float): diff --git a/python/test-data/testTypesRecordInheritance.err b/python/test-data/testTypesRecordInheritance.err deleted file mode 100644 index 34a56d0b..00000000 --- a/python/test-data/testTypesRecordInheritance.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesRecordInheritance.py", line 18, in - Point3D(1,2, "foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: record constructor Point3D(x: int, y: int, z: int) -> Self - ^^^ -declared at: test-data/testTypesRecordInheritance.py:10 -caused by: test-data/testTypesRecordInheritance.py:18 - | Point3D(1,2, "foo") diff --git a/python/test-data/testTypesRecordInheritance.err-3.10 b/python/test-data/testTypesRecordInheritance.err-3.10 deleted file mode 100644 index b86d1393..00000000 --- a/python/test-data/testTypesRecordInheritance.err-3.10 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesRecordInheritance.py", line 18, in - Point3D(1,2, "foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: record constructor Point3D(x: int, y: int, z: int) -> Self - ^^^ -declared at: test-data/testTypesRecordInheritance.py:9 -caused by: test-data/testTypesRecordInheritance.py:18 - | Point3D(1,2, "foo") diff --git a/python/test-data/testTypesRecordInheritance.err-3.11 b/python/test-data/testTypesRecordInheritance.err-3.11 deleted file mode 100644 index b86d1393..00000000 --- a/python/test-data/testTypesRecordInheritance.err-3.11 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesRecordInheritance.py", line 18, in - Point3D(1,2, "foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: record constructor Point3D(x: int, y: int, z: int) -> Self - ^^^ -declared at: test-data/testTypesRecordInheritance.py:9 -caused by: test-data/testTypesRecordInheritance.py:18 - | Point3D(1,2, "foo") diff --git a/python/test-data/testTypesRecordInheritance.err-3.12 b/python/test-data/testTypesRecordInheritance.err-3.12 deleted file mode 100644 index b86d1393..00000000 --- a/python/test-data/testTypesRecordInheritance.err-3.12 +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesRecordInheritance.py", line 18, in - Point3D(1,2, "foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: record constructor Point3D(x: int, y: int, z: int) -> Self - ^^^ -declared at: test-data/testTypesRecordInheritance.py:9 -caused by: test-data/testTypesRecordInheritance.py:18 - | Point3D(1,2, "foo") diff --git a/python/test-data/testTypesReturn.err b/python/test-data/testTypesReturn.err deleted file mode 100644 index 6325880e..00000000 --- a/python/test-data/testTypesReturn.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesReturn.py", line 8, in - foo(False) -WyppTypeError: got value of wrong type -given: 'you stupid' -expected: value of type int - -context: foo(flag: bool) -> int - ^^^ -declared at: test-data/testTypesReturn.py:1 -caused by: test-data/testTypesReturn.py:6 - | return 'you stupid' diff --git a/python/test-data/testTypesSequence1.err b/python/test-data/testTypesSequence1.err deleted file mode 100644 index a24c922c..00000000 --- a/python/test-data/testTypesSequence1.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSequence1.py", line 10, in - foo(1) # should fail -WyppTypeError: got value of wrong type -given: 1 -expected: value of type Sequence - -context: foo(seq: Sequence) -> None - ^^^^^^^^ -declared at: test-data/testTypesSequence1.py:3 -caused by: test-data/testTypesSequence1.py:10 - | foo(1) # should fail diff --git a/python/test-data/testTypesSequence2.err b/python/test-data/testTypesSequence2.err deleted file mode 100644 index f44365fc..00000000 --- a/python/test-data/testTypesSequence2.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSequence2.py", line 11, in - foo("Hello!") # should fail - File "test-data/testTypesSequence2.py", line 6, in foo - for x in seq: -WyppTypeError: str is not a Sequence[int] -given: 'Hello!' -expected: value of type Sequence[int] - -context: foo(seq: Sequence[int]) -> None - ^^^^^^^^^^^^^ -declared at: test-data/testTypesSequence2.py:3 -caused by: test-data/testTypesSequence2.py:11 - | foo("Hello!") # should fail diff --git a/python/test-data/testTypesSet1.err b/python/test-data/testTypesSet1.err deleted file mode 100644 index 8e8d6626..00000000 --- a/python/test-data/testTypesSet1.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSet1.py", line 7, in - appendSomething(l) - File "test-data/testTypesSet1.py", line 4, in appendSomething - l.add("foo") -WyppTypeError: got value of wrong type -given: 'foo' -expected: value of type int - -context: add(self, other: int) - ^^^ -declared at: site-lib/untypy/impl/interfaces/set.py:7 -caused by: test-data/testTypesSet1.py:4 - | l.add("foo") diff --git a/python/test-data/testTypesSet2.err b/python/test-data/testTypesSet2.err deleted file mode 100644 index d082c7c7..00000000 --- a/python/test-data/testTypesSet2.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSet2.py", line 10, in - foo(set([lambda: "1", lambda: 42])) # error because the 2nd functions returns an int - File "test-data/testTypesSet2.py", line 6, in foo - res.append(f()) -WyppTypeError: set is not a set[Callable[[], str]] -given: { at 0x00>, at 0x00>} -expected: value of type set[Callable[[], str]] - -context: foo(l: set[Callable[[], str]]) -> list[str] - ^^^^^^^^^^^^^^^^^^^^^^ -declared at: test-data/testTypesSet2.py:3 -caused by: test-data/testTypesSet2.py:10 - | foo(set([lambda: "1", lambda: 42])) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesSet2.out b/python/test-data/testTypesSet2.out deleted file mode 100644 index 189744a2..00000000 --- a/python/test-data/testTypesSet2.out +++ /dev/null @@ -1 +0,0 @@ -['1', '2'] diff --git a/python/test-data/testTypesSet2.py b/python/test-data/testTypesSet2.py deleted file mode 100644 index 24579212..00000000 --- a/python/test-data/testTypesSet2.py +++ /dev/null @@ -1,10 +0,0 @@ -from wypp import * - -def foo(l: set[Callable[[], str]]) -> list[str]: - res = [] - for f in l: - res.append(f()) - return res - -print(sorted(list(foo(set([lambda: "1", lambda: "2"]))))) -foo(set([lambda: "1", lambda: 42])) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesSet3.err b/python/test-data/testTypesSet3.err deleted file mode 100644 index 0185212d..00000000 --- a/python/test-data/testTypesSet3.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSet3.py", line 11, in - foo(set([func])) - File "test-data/testTypesSet3.py", line 7, in foo - res.append(f()) -WyppTypeError: got value of wrong type -given: 42 -expected: value of type str - -context: __next__(self: Self) -> Callable[[], str] - ^^^ -declared at: test-data/testTypesSet3.py:4 -caused by: test-data/testTypesSet3.py:4 - | l.add(lambda: 42) # error diff --git a/python/test-data/testTypesSet3.py b/python/test-data/testTypesSet3.py deleted file mode 100644 index 5d183bdd..00000000 --- a/python/test-data/testTypesSet3.py +++ /dev/null @@ -1,11 +0,0 @@ -from wypp import * - -def foo(l: set[Callable[[], str]]) -> list[str]: - l.add(lambda: 42) # error - res = [] - for f in l: - res.append(f()) - return res - -func = lambda: "xxx" -foo(set([func])) diff --git a/python/test-data/testTypesSet4.err b/python/test-data/testTypesSet4.err deleted file mode 100644 index d5955512..00000000 --- a/python/test-data/testTypesSet4.err +++ /dev/null @@ -1,14 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSet4.py", line 11, in - foo(set([func])) # error - File "test-data/testTypesSet4.py", line 6, in foo - res.append(f()) -WyppTypeError: set is not a set[Callable[[], str]] -given: { at 0x00>} -expected: value of type set[Callable[[], str]] - -context: foo(l: set[Callable[[], str]]) -> list[str] - ^^^^^^^^^^^^^^^^^^^^^^ -declared at: test-data/testTypesSet4.py:3 -caused by: test-data/testTypesSet4.py:11 - | foo(set([func])) # error diff --git a/python/test-data/testTypesSet4.out b/python/test-data/testTypesSet4.out deleted file mode 100644 index e56d69a9..00000000 --- a/python/test-data/testTypesSet4.out +++ /dev/null @@ -1 +0,0 @@ -['x'] diff --git a/python/test-data/testTypesSet4.py b/python/test-data/testTypesSet4.py deleted file mode 100644 index f8d7f7be..00000000 --- a/python/test-data/testTypesSet4.py +++ /dev/null @@ -1,11 +0,0 @@ -from wypp import * - -def foo(l: set[Callable[[], str]]) -> list[str]: - res = [] - for f in l: - res.append(f()) - return res - -print( foo(set([lambda: 'x']))) # ok -func = lambda: 42 -foo(set([func])) # error diff --git a/python/test-data/testTypesSubclassing1.err b/python/test-data/testTypesSubclassing1.err deleted file mode 100644 index 3c9aa638..00000000 --- a/python/test-data/testTypesSubclassing1.err +++ /dev/null @@ -1,28 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesSubclassing1.py", line 29, in - feedAnimal(dog) - File "test-data/testTypesSubclassing1.py", line 26, in feedAnimal - a.feed(AnimalFood('some cat food')) -WyppTypeError: Dog does not implement parent Animal -given: -expected: value of type Animal - -declared at: test-data/testTypesSubclassing1.py:20 -declared at: test-data/testTypesSubclassing1.py:10 -caused by: test-data/testTypesSubclassing1.py:20 - | class Dog(Animal): -caused by: test-data/testTypesSubclassing1.py:22 - | def feed(self, food: DogFood) -> None: - -Argument food of method feed violates the type declared by the parent Animal. -Annotation DogFood is incompatible with the parent's annotation AnimalFood. - -given: -expected: value of type DogFood - -context: feed(self: Self, food: DogFood) -> None - ^^^^^^^ -declared at: test-data/testTypesSubclassing1.py:22 -declared at: test-data/testTypesSubclassing1.py:10 -caused by: test-data/testTypesSubclassing1.py:22 - | def feed(self, food: DogFood) -> None: diff --git a/python/test-data/testTypesTuple1.err b/python/test-data/testTypesTuple1.err deleted file mode 100644 index 1e6e9a50..00000000 --- a/python/test-data/testTypesTuple1.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/testTypesTuple1.py", line 5, in - foo(1) -WyppTypeError: got value of wrong type -given: 1 -expected: value of type tuple[int, ...] - -context: foo(l: tuple[int, ...]) -> int - ^^^^^^^^^^^^^^^ -declared at: test-data/testTypesTuple1.py:1 -caused by: test-data/testTypesTuple1.py:5 - | foo(1) diff --git a/python/test-data/testUnsortableDicts.out b/python/test-data/testUnsortableDicts.out deleted file mode 100644 index 5b149202..00000000 --- a/python/test-data/testUnsortableDicts.out +++ /dev/null @@ -1 +0,0 @@ -1 erfolgreicher Test 😀 diff --git a/python/test-data/testUnsortableDicts.py b/python/test-data/testUnsortableDicts.py deleted file mode 100644 index ae06eb96..00000000 --- a/python/test-data/testUnsortableDicts.py +++ /dev/null @@ -1,8 +0,0 @@ -from wypp import * - -def foo(): - pass -def bar(): - pass - -check({bar: 2, foo: 1}, {foo: 1, bar: 2.00000000001}) diff --git a/python/test-data/testWrap.out b/python/test-data/testWrap.out deleted file mode 100644 index c5277e9c..00000000 --- a/python/test-data/testWrap.out +++ /dev/null @@ -1 +0,0 @@ -Street(name='Hauptstraße', cars=(Car(licensePlate='OG PY 123', color='rot'),)) diff --git a/python/test-data/testWrap.py b/python/test-data/testWrap.py deleted file mode 100644 index f21cc68d..00000000 --- a/python/test-data/testWrap.py +++ /dev/null @@ -1,28 +0,0 @@ -from wypp import * - -@record -class Car: - licensePlate: str - color: str - -@record -class Street: - name: str - cars: tuple[Car, ...] # Tuple ist unveränderbar - -def removeElem(seq: Sequence, x: Any) -> Sequence: - i = seq.index(x) - return seq[:i] + seq[i+1:] - -rotesAuto = Car('OG PY 123', 'rot') -blauesAuto = Car(licensePlate='OG HS 130', color='blau') -grünesAuto = Car('FR XJ 252', 'grün') - -hauptstraße = Street('Hauptstraße', (rotesAuto, blauesAuto)) - -def leaveStreet(street: Street, car: Car) -> Street: - newCars = removeElem(street.cars, car) - return Street(street.name, newCars) - -s = leaveStreet(hauptstraße, blauesAuto) -print(s) diff --git a/python/test-data/testWrap2.out b/python/test-data/testWrap2.out deleted file mode 100644 index ef9a4b5d..00000000 --- a/python/test-data/testWrap2.out +++ /dev/null @@ -1 +0,0 @@ -Street(name='Hauptstraße', cars=[Car(licensePlate='OG PY 123', color='rot')]) diff --git a/python/test-data/testWrap2.py b/python/test-data/testWrap2.py deleted file mode 100644 index 90bfb494..00000000 --- a/python/test-data/testWrap2.py +++ /dev/null @@ -1,28 +0,0 @@ -from wypp import * - -@record -class Car: - licensePlate: str - color: str - -@record -class Street: - name: str - cars: list[Car] - -def removeElem(seq: Sequence, x: Any) -> Sequence: - i = seq.index(x) - return seq[:i] + seq[i+1:] - -rotesAuto = Car('OG PY 123', 'rot') -blauesAuto = Car(licensePlate='OG HS 130', color='blau') -grünesAuto = Car('FR XJ 252', 'grün') - -hauptstraße = Street('Hauptstraße', [rotesAuto, blauesAuto]) - -def leaveStreet(street: Street, car: Car) -> Street: - newCars = removeElem(street.cars, car) - return Street(street.name, newCars) - -s = leaveStreet(hauptstraße, blauesAuto) -print(s) diff --git a/python/test-data/testWrapperError.err b/python/test-data/testWrapperError.err deleted file mode 100644 index eec6efcd..00000000 --- a/python/test-data/testWrapperError.err +++ /dev/null @@ -1,8 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrapperError.py", line 9, in - foo(bar) - File "test-data/testWrapperError.py", line 4, in foo - return f(42) - File "test-data/testWrapperError.py", line 7, in bar - raise ValueError(str(i)) -ValueError: 42 diff --git a/python/test-data/testWrapperError.py b/python/test-data/testWrapperError.py deleted file mode 100644 index d01b874f..00000000 --- a/python/test-data/testWrapperError.py +++ /dev/null @@ -1,9 +0,0 @@ -from wypp import * - -def foo(f: Callable[[int], int]) -> int: - return f(42) - -def bar(i: int) -> int: - raise ValueError(str(i)) - -foo(bar) diff --git a/python/test-data/testWrongKeywordArg.err b/python/test-data/testWrongKeywordArg.err deleted file mode 100644 index e33de87b..00000000 --- a/python/test-data/testWrongKeywordArg.err +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongKeywordArg.py", line 4, in - foo(x=4) -WyppTypeError: missing a required argument: 'kw' - -context: foo(kw: int) -> int -declared at: test-data/testWrongKeywordArg.py:1 -caused by: test-data/testWrongKeywordArg.py:4 - | foo(x=4) diff --git a/python/test-data/testWrongKeywordArg2.err b/python/test-data/testWrongKeywordArg2.err deleted file mode 100644 index 95915241..00000000 --- a/python/test-data/testWrongKeywordArg2.err +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongKeywordArg2.py", line 8, in - p = Point(foo=3, y=4) -WyppTypeError: missing a required argument: 'x' - -context: record constructor Point(x: int, y: int) -> Self -declared at: test-data/testWrongKeywordArg2.py:4 -caused by: test-data/testWrongKeywordArg2.py:8 - | p = Point(foo=3, y=4) diff --git a/python/test-data/testWrongKeywordArg2.err-3.10 b/python/test-data/testWrongKeywordArg2.err-3.10 deleted file mode 100644 index 2daa463d..00000000 --- a/python/test-data/testWrongKeywordArg2.err-3.10 +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongKeywordArg2.py", line 8, in - p = Point(foo=3, y=4) -WyppTypeError: missing a required argument: 'x' - -context: record constructor Point(x: int, y: int) -> Self -declared at: test-data/testWrongKeywordArg2.py:3 -caused by: test-data/testWrongKeywordArg2.py:8 - | p = Point(foo=3, y=4) diff --git a/python/test-data/testWrongKeywordArg2.err-3.11 b/python/test-data/testWrongKeywordArg2.err-3.11 deleted file mode 100644 index 2daa463d..00000000 --- a/python/test-data/testWrongKeywordArg2.err-3.11 +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongKeywordArg2.py", line 8, in - p = Point(foo=3, y=4) -WyppTypeError: missing a required argument: 'x' - -context: record constructor Point(x: int, y: int) -> Self -declared at: test-data/testWrongKeywordArg2.py:3 -caused by: test-data/testWrongKeywordArg2.py:8 - | p = Point(foo=3, y=4) diff --git a/python/test-data/testWrongKeywordArg2.err-3.12 b/python/test-data/testWrongKeywordArg2.err-3.12 deleted file mode 100644 index 2daa463d..00000000 --- a/python/test-data/testWrongKeywordArg2.err-3.12 +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongKeywordArg2.py", line 8, in - p = Point(foo=3, y=4) -WyppTypeError: missing a required argument: 'x' - -context: record constructor Point(x: int, y: int) -> Self -declared at: test-data/testWrongKeywordArg2.py:3 -caused by: test-data/testWrongKeywordArg2.py:8 - | p = Point(foo=3, y=4) diff --git a/python/test-data/testWrongNumOfArguments.err b/python/test-data/testWrongNumOfArguments.err deleted file mode 100644 index 162142c8..00000000 --- a/python/test-data/testWrongNumOfArguments.err +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongNumOfArguments.py", line 4, in - foo(1) -WyppTypeError: missing a required argument: 'y' - -context: foo(x: int, y: float) -> float -declared at: test-data/testWrongNumOfArguments.py:1 -caused by: test-data/testWrongNumOfArguments.py:4 - | foo(1) diff --git a/python/test-data/testWrongNumOfArguments2.err b/python/test-data/testWrongNumOfArguments2.err deleted file mode 100644 index 21a479e3..00000000 --- a/python/test-data/testWrongNumOfArguments2.err +++ /dev/null @@ -1,9 +0,0 @@ -Traceback (most recent call last): - File "test-data/testWrongNumOfArguments2.py", line 6, in - c.foo(1) -WyppTypeError: missing a required argument: 'y' - -context: foo(self: Self, x: int, y: float) -> float -declared at: test-data/testWrongNumOfArguments2.py:2 -caused by: test-data/testWrongNumOfArguments2.py:6 - | c.foo(1) diff --git a/python/test-data/testWrongNumOfArguments3.py b/python/test-data/testWrongNumOfArguments3.py deleted file mode 100644 index f7ea712f..00000000 --- a/python/test-data/testWrongNumOfArguments3.py +++ /dev/null @@ -1,6 +0,0 @@ -class C: - def foo(x: int, y: float) -> float: - return x + y - -c = C() -c.foo(1, 2, 3) diff --git a/python/test-data/typeEnums.py b/python/test-data/typeEnums.py deleted file mode 100644 index b1a305a2..00000000 --- a/python/test-data/typeEnums.py +++ /dev/null @@ -1,12 +0,0 @@ -from wypp import * - -Color = Literal['red', 'yellow', 'green'] - -def colorToNumber(c: Color) -> int: - if c == 'red': - return 0 - elif c == 'yellow': - return 1 - else: - return 2 - diff --git a/python/test-data/typeRecords.py b/python/test-data/typeRecords.py deleted file mode 100644 index a8ff1102..00000000 --- a/python/test-data/typeRecords.py +++ /dev/null @@ -1,17 +0,0 @@ -from wypp import * - -@record -class Person: - name: str - age: int - -def incAge(p: Person) -> Person: - return Person(p.name, p.age + 1) - -@record(mutable=True) -class MutablePerson: - name: str - age: int - -def mutableIncAge(p: MutablePerson) -> None: - p.age = p.age + 1 diff --git a/python/test-data/typeUnion.py b/python/test-data/typeUnion.py deleted file mode 100644 index a1c41364..00000000 --- a/python/test-data/typeUnion.py +++ /dev/null @@ -1,23 +0,0 @@ -from wypp import * - -@record -class Cat: - name: str - weight: int - -myCat = Cat('Pumpernickel', 2) - -@record -class Parrot: - name: str - sentence: str - -myParrot = Parrot('Mike', "Let's go to the punkrock show") - -Animal = Union[Cat, Parrot] - -def formatAnimal(a: Animal) -> str: - if type(a) == Cat: - return "Cat " + a.name - else: - return "Parrot " + a.name + " says: " + a.sentence diff --git a/python/test-data/wrong-caused-by.err b/python/test-data/wrong-caused-by.err deleted file mode 100644 index 6b363385..00000000 --- a/python/test-data/wrong-caused-by.err +++ /dev/null @@ -1,12 +0,0 @@ -Traceback (most recent call last): - File "test-data/wrong-caused-by.py", line 31, in - mainStreetM.turnIntoStreet(redCarM) -WyppTypeError: got value of wrong type -given: CarM(licensePlate='OG PY 123', color='rot') -expected: value of type Car - -context: turnIntoStreet(self: Self, car: Car) -> None - ^^^ -declared at: test-data/wrong-caused-by.py:10 -caused by: test-data/wrong-caused-by.py:31 - | mainStreetM.turnIntoStreet(redCarM) diff --git a/python/tests/locationTestData.py b/python/tests/locationTestData.py new file mode 100644 index 00000000..29a159d0 --- /dev/null +++ b/python/tests/locationTestData.py @@ -0,0 +1,40 @@ +import inspect +# no other imports here (put them below) to keep the line number for myFun stable + +lineNoMyFunDef = 12 +lineNoMyFunCall = lineNoMyFunDef + 2 +lineNoMyFunNoResultDef = 19 +lineNoTestRecordDef = 26 + +def blub(i: int) -> int: + return i + 1 + +def myFun(a: str, b: list[int], depth: int=0) -> inspect.FrameInfo: + if depth == 0: + return myFun("blub", [42, 0], 1) # that's the call + else: + stack = inspect.stack() + return stack[1] + +def myFunNoResult(depth: int=0): + if depth == 0: + return myFunNoResult(1) + else: + stack = inspect.stack() + return stack[1] + +class TestRecord: + x: int + y: str + z: float = 3.14 + +lineFooBase = 35 +lineFooSub = 39 + +class Base: + def foo(self, x: int, y: str): + pass + +class Sub(Base): + def foo(self, y: int, x: float): + pass diff --git a/python/tests/parsecacheTestData.py b/python/tests/parsecacheTestData.py new file mode 100644 index 00000000..7cf95236 --- /dev/null +++ b/python/tests/parsecacheTestData.py @@ -0,0 +1,25 @@ +# Keep line numbers intact, otherwise you have to adjust the tests. + +def bar(): + pass + +def foo(): + pass + +class C: + x: int + y: str + +class D: + x: int + z: str + def foo(self): + pass + +def spam() -> int: + return 1 + +def spam() -> int: + def foo(): + pass + return 2 diff --git a/python/tests/sample.py b/python/tests/sample.py index f020c989..d117e9fb 100644 --- a/python/tests/sample.py +++ b/python/tests/sample.py @@ -1,12 +1,8 @@ -from writeYourProgram import * -import math -from untypy import typechecked -import typing +from wypp import * Drink = Literal["Tea", "Coffee"] # berechnet wieviele Tassen ich von einem Getränk trinken darf -@typechecked def canDrink(d: Drink) -> int: if d == "Tea": return 5 @@ -17,8 +13,7 @@ def canDrink(d: Drink) -> int: # - a circle (Circle) # - a square (Square) # - an overlay of two shapes (Overlay) -Shape = typing.Union[typing.ForwardRef('Circle'), typing.ForwardRef('Square'), typing.ForwardRef('Overlay')] -Shape = typing.Union['Circle', 'Square', 'Overlay'] +type Shape = Union['Circle', 'Square', 'Overlay'] # A point consists of # - x (float) @@ -95,7 +90,6 @@ class Overlay: o2 = Overlay(o1, c2) # Calculate the distance between two points -@typechecked def distance(p1: Point, p2: Point) -> float: w = p1.x - p2.x h = p1.y - p2.y @@ -103,7 +97,6 @@ def distance(p1: Point, p2: Point) -> float: return dist # Is a point within a shape? -@typechecked def pointInShape(point: Point, shape: Shape) -> bool: px = point.x py = point.y @@ -121,4 +114,19 @@ def pointInShape(point: Point, shape: Shape) -> bool: elif type(shape) == Overlay: return pointInShape(point, shape.top) or pointInShape(point, shape.bottom) else: - uncoveredCase() + return impossible() + +check(pointInShape(p2, c1), True) +check(pointInShape(p3, c2), True) +check(pointInShape(Point(51, 50), c1), False) +check(pointInShape(Point(11, 21), s1), True) +check(pointInShape(Point(49, 59), s1), True) +check(pointInShape(Point(9, 21), s1), False) +check(pointInShape(Point(11, 19), s1), False) +check(pointInShape(Point(51, 59), s1), False) +check(pointInShape(Point(49, 61), s1), False) +check(pointInShape(Point(40, 30), c2), True) +check(pointInShape(Point(40, 30), o2), True) +check(pointInShape(Point(0, 0), o2), False) +check(pointInShape(Point(30, 65), o2), True) +check(pointInShape(Point(40, 17), o2), True) diff --git a/python/tests/stacktraceTestData.py b/python/tests/stacktraceTestData.py new file mode 100644 index 00000000..7d083dc4 --- /dev/null +++ b/python/tests/stacktraceTestData.py @@ -0,0 +1,13 @@ +f1ReturnLine = 6 +f2ReturnLine = 9 +f3ReturnLine = 13 + +def f1(): + return 42 + +def f2(): + f1() + +def f3(): + f1() + return 0 diff --git a/python/tests/testSample.py b/python/tests/testSample.py deleted file mode 100644 index 65ab52b0..00000000 --- a/python/tests/testSample.py +++ /dev/null @@ -1,22 +0,0 @@ -from writeYourProgram import * -import unittest -from sample import * - -setDieOnCheckFailures(True) - -class TestSample(unittest.TestCase): - def test_sample(self): - check(pointInShape(p2, c1), True) - check(pointInShape(p3, c2), True) - check(pointInShape(Point(51, 50), c1), False) - check(pointInShape(Point(11, 21), s1), True) - check(pointInShape(Point(49, 59), s1), True) - check(pointInShape(Point(9, 21), s1), False) - check(pointInShape(Point(11, 19), s1), False) - check(pointInShape(Point(51, 59), s1), False) - check(pointInShape(Point(49, 61), s1), False) - check(pointInShape(Point(40, 30), c2), True) - check(pointInShape(Point(40, 30), o2), True) - check(pointInShape(Point(0, 0), o2), False) - check(pointInShape(Point(30, 65), o2), True) - check(pointInShape(Point(40, 17), o2), True) diff --git a/python/tests/testDeepEq.py b/python/tests/test_deepEq.py similarity index 100% rename from python/tests/testDeepEq.py rename to python/tests/test_deepEq.py diff --git a/python/tests/testDrawing.py b/python/tests/test_drawing.py similarity index 100% rename from python/tests/testDrawing.py rename to python/tests/test_drawing.py diff --git a/python/tests/test_i18n.py b/python/tests/test_i18n.py new file mode 100644 index 00000000..90f3eaea --- /dev/null +++ b/python/tests/test_i18n.py @@ -0,0 +1,70 @@ +import unittest +import i18n +import location +import inspect +import utils + +class TestI18nTranslations(unittest.TestCase): + def setUp(self): + # Get all functions that return strings + blacklist = ['tr'] + self.stringFunctions = [] + for name, func in inspect.getmembers(i18n, inspect.isfunction): + if not name in blacklist and \ + hasattr(func, '__annotations__') and func.__annotations__.get('return') == str: + self.stringFunctions.append((name, func)) + + def _getCallableNameVariants(self): + return [ + location.CallableName('foo', 'function'), + location.CallableName('foo', location.ClassMember('method', 'foo')), + location.CallableName('foo', location.ClassMember('recordConstructor', 'foo')) + ] + + def _getTestArgs(self, func): + sig = inspect.signature(func) + args = [] + hasCallableName = False + + for paramName, param in sig.parameters.items(): + if param.annotation == str: + args.append('foo') + elif param.annotation == int: + args.append(1) + elif param.annotation == location.CallableName: + args.append(None) # Placeholder + hasCallableName = True + else: + args.append('foo') # Default fallback + + if hasCallableName: + # Generate variants for each CallableName parameter + variants = [] + for variant in self._getCallableNameVariants(): + testArgs = [] + for i, arg in enumerate(args): + if arg is None: # CallableName placeholder + testArgs.append(variant) + else: + testArgs.append(arg) + variants.append(testArgs) + return variants + else: + return [args] if args else [[]] + + def testAllI18NFunctions(self): + with utils.underTest(True): + for lang in i18n.allLanguages: + with i18n.explicitLang(lang): + with self.subTest(lang=lang): + for funcName, func in self.stringFunctions: + with self.subTest(function=funcName): + argVariants = self._getTestArgs(func) + for args in argVariants: + with self.subTest(args=args): + result = func(*args) + self.assertIsInstance(result, str) + self.assertGreater(len(result), 0) + +if __name__ == '__main__': + unittest.main() diff --git a/python/tests/test_location.py b/python/tests/test_location.py new file mode 100644 index 00000000..e4ed81ab --- /dev/null +++ b/python/tests/test_location.py @@ -0,0 +1,72 @@ +import unittest +import os +import location +from locationTestData import * + +class TestLocation(unittest.TestCase): + + def assertLocation(self, + loc: location.Loc | None, + startLine: int, startCol: int, endLine: int, endCol: int): + if loc is None: + self.fail("loc is None") + self.assertEqual('locationTestData.py', os.path.basename(loc.filename)) + self.assertEqual(startLine, loc.startLine) + self.assertEqual(endLine, loc.endLine) + self.assertEqual(startCol, loc.startCol) + self.assertEqual(endCol, loc.endCol) + + def test_locationOfArgument(self): + fi = myFun("foo", [1,2,3]) + loc = location.locationOfArgument(fi, 0) + colNo = 21 + self.assertLocation(loc, lineNoMyFunCall, + colNo, + lineNoMyFunCall, + colNo + len("blub") + 2) + + def test_StdCallableInfo(self): + info = location.StdCallableInfo(myFun, 'function') + self.assertEqual('locationTestData.py', os.path.basename(info.file)) + resultLoc = info.getResultTypeLocation() + argLoc = info.getParamSourceLocation("depth") + self.assertLocation(resultLoc, lineNoMyFunDef, 49, lineNoMyFunDef, 66) + self.assertLocation(argLoc, lineNoMyFunDef, 32, lineNoMyFunDef, 42) + + def test_StdCallableInfo2(self): + info = location.StdCallableInfo(myFunNoResult, 'function') + self.assertEqual('locationTestData.py', os.path.basename(info.file)) + resultLoc = info.getResultTypeLocation() + self.assertIsNone(resultLoc) + argLoc = info.getParamSourceLocation("depth") + self.assertLocation(argLoc, lineNoMyFunNoResultDef, 18, lineNoMyFunNoResultDef, 28) + + def test_StdCallableInfoSub(self): + sub = Sub() + info = location.StdCallableInfo(sub.foo, location.ClassMember('method', 'Sub')) + self.assertEqual('locationTestData.py', os.path.basename(info.file)) + argLoc = info.getParamSourceLocation("y") + self.assertLocation(argLoc, lineFooSub, 18, lineFooSub, 24) + + def test_StdCallableInfoBase(self): + b = Base() + info = location.StdCallableInfo(b.foo, location.ClassMember('method', 'Base')) + self.assertEqual('locationTestData.py', os.path.basename(info.file)) + argLoc = info.getParamSourceLocation("y") + self.assertLocation(argLoc, lineFooBase, 26, lineFooBase, 32) + + def test_RecordConstructorInfo(self): + info = location.RecordConstructorInfo(TestRecord) + # Test getting parameter source locations with precise line/column numbers + argLocX = info.getParamSourceLocation("x") + argLocY = info.getParamSourceLocation("y") + argLocZ = info.getParamSourceLocation("z") + self.assertLocation(argLocX, lineNoTestRecordDef + 1, 4, lineNoTestRecordDef + 1, 10) + self.assertLocation(argLocY, lineNoTestRecordDef + 2, 4, lineNoTestRecordDef + 2, 10) + self.assertLocation(argLocZ, lineNoTestRecordDef + 3, 4, lineNoTestRecordDef + 3, 19) + # Test non-existent parameter + nonExistentLoc = info.getParamSourceLocation("nonexistent") + self.assertIsNone(nonExistentLoc) + # Test result type location (should be None for constructors) + resultLoc = info.getResultTypeLocation() + self.assertIsNone(resultLoc) diff --git a/python/tests/testMisc.py b/python/tests/test_misc.py similarity index 100% rename from python/tests/testMisc.py rename to python/tests/test_misc.py diff --git a/python/tests/test_parsecache.py b/python/tests/test_parsecache.py new file mode 100644 index 00000000..d9c0d6ec --- /dev/null +++ b/python/tests/test_parsecache.py @@ -0,0 +1,75 @@ +import unittest +import parsecache +from parsecache import FunMatcher +import ast + +class TestParseCache(unittest.TestCase): + + def test_parseCacheFunDef(self): + a = parsecache.AST('tests/parsecacheTestData.py') + assert(a is not None) + defFoo = a.getFunDef(FunMatcher('foo', 6)) + assert(defFoo is not None) + self.assertEqual(defFoo.lineno, 6) + defBar = a.getFunDef(FunMatcher('bar')) + assert(defBar is not None) + self.assertEqual(defBar.lineno, 3) + x = a.getFunDef(FunMatcher('spam')) # conflicting def + self.assertIsNone(x) + defSpam = a.getFunDef(FunMatcher('spam', 22)) + assert(defSpam is not None) + self.assertEqual(defSpam.lineno, 22) + match defSpam.body[1]: + case ast.Return(ast.Constant(2)): + pass + case stmt: + self.fail(f'Invalid first statement of spam: {stmt}') + x = a.getFunDef(FunMatcher('egg')) + self.assertIsNone(x) + + def test_parseCacheNestedFunDef(self): + a = parsecache.AST('tests/parsecacheTestData.py') + assert(a is not None) + defFoo = a.getFunDef(FunMatcher('foo', 23)) + assert(defFoo is not None) + self.assertEqual(defFoo.lineno, 23) + + def test_parseCacheMethod(self): + a = parsecache.AST('tests/parsecacheTestData.py') + assert(a is not None) + defFoo = a.getMethodDef('D', FunMatcher('foo', 16)) + assert(defFoo is not None) + self.assertEqual(defFoo.lineno, 16) + + def test_parseCacheRecordAttr(self): + a = parsecache.AST('tests/parsecacheTestData.py') + assert(a is not None) + + # Test getting attributes from class C + attrX_C = a.getRecordAttr('C', 'x') + assert(attrX_C is not None) + self.assertEqual(attrX_C.lineno, 10) + + attrY_C = a.getRecordAttr('C', 'y') + assert(attrY_C is not None) + self.assertEqual(attrY_C.lineno, 11) + + # Test getting attributes from class D + attrX_D = a.getRecordAttr('D', 'x') + assert(attrX_D is not None) + self.assertEqual(attrX_D.lineno, 14) + + attrZ_D = a.getRecordAttr('D', 'z') + assert(attrZ_D is not None) + self.assertEqual(attrZ_D.lineno, 15) + + # Test non-existent attribute + nonExistent = a.getRecordAttr('C', 'z') + self.assertIsNone(nonExistent) + + # Test non-existent class + nonExistentClass = a.getRecordAttr('NonExistentClass', 'x') + self.assertIsNone(nonExistentClass) + + + diff --git a/python/tests/testRecord.py b/python/tests/test_record.py similarity index 89% rename from python/tests/testRecord.py rename to python/tests/test_record.py index 20a8b3db..d0113e9c 100644 --- a/python/tests/testRecord.py +++ b/python/tests/test_record.py @@ -3,9 +3,9 @@ import sys import traceback import dataclasses +import stacktrace initModule() -setDieOnCheckFailures(True) @record class Point: @@ -28,6 +28,16 @@ class Box: class TestRecords(unittest.TestCase): + def setUp(self): + setDieOnCheckFailures(True) + self.original_profile = sys.getprofile() + stacktrace.installProfileHook() + + def tearDown(self): + # Restore original profile function + sys.setprofile(self.original_profile) + setDieOnCheckFailures(False) + def test_create(self): p1 = Point(1, 2) p2 = Point(3, 4) @@ -79,7 +89,6 @@ def test_eq2(self): # check should use special logic for floats check(p1, p2) - def test_hash(self): p1 = Point(1, 2) p2 = Point(1, 4) @@ -117,7 +126,7 @@ def test_addField(self): b = Box(5) try: b.foobar = 'foobar' - self.fail("Expected AttributeError") + self.fail(f"Expected AttributeError, b={b}") except AttributeError: pass p1 = Point(1, 2) diff --git a/python/tests/test_renderTy.py b/python/tests/test_renderTy.py new file mode 100644 index 00000000..0b6f4c78 --- /dev/null +++ b/python/tests/test_renderTy.py @@ -0,0 +1,75 @@ +import unittest +from typing import * +from renderTy import renderTy + +class MyClass: + pass + +class TestRenderTy(unittest.TestCase): + + def test_basics(self): + self.assertEqual(renderTy(int), 'int') + self.assertEqual(renderTy(list[str]), 'list[str]') + self.assertEqual(renderTy(dict[str, list[int]]), 'dict[str, list[int]]') + self.assertEqual(renderTy(Optional[dict[str, int]]), 'Optional[dict[str, int]]') + self.assertEqual(renderTy(tuple[int, ...]), 'tuple[int, ...]') + self.assertEqual(renderTy(Union[int, str, bool]), 'Union[int, str, bool]') + self.assertEqual(renderTy(Callable[[int, str, list[float]], bool]), + 'Callable[[int, str, list[float]], bool]') + + def test_advanced_types(self): + # Nested generics + self.assertEqual(renderTy(dict[str, list[tuple[int, str]]]), 'dict[str, list[tuple[int, str]]]') + self.assertEqual(renderTy(list[dict[str, Optional[int]]]), 'list[dict[str, Optional[int]]]') + + # Complex Union types + self.assertEqual(renderTy(Union[int, str, None]), 'Union[int, str, None]') + self.assertEqual(renderTy(Union[list[int], dict[str, int], tuple[int, ...]]), + 'Union[list[int], dict[str, int], tuple[int, ...]]') + self.assertEqual(renderTy(int | str), 'Union[int, str]') + self.assertEqual(renderTy(list[int] | dict[str, int] | None), + 'Union[list[int], dict[str, int], None]') + + # Callable with complex signatures + self.assertEqual(renderTy(Callable[[dict[str, int], Optional[list[str]]], Union[int, str]]), + 'Callable[[dict[str, int], Optional[list[str]]], Union[int, str]]') + self.assertEqual(renderTy(Callable[[], None]), 'Callable[[], None]') + + # Literal types + self.assertEqual(renderTy(Literal['red', 'green', 'blue']), "Literal['red', 'green', 'blue']") + self.assertEqual(renderTy(Literal[1, 2, 3]), 'Literal[1, 2, 3]') + + def test_special_forms(self): + # Any and NoReturn + self.assertEqual(renderTy(Any), 'Any') + self.assertEqual(renderTy(NoReturn), 'NoReturn') + + # Forward references (if supported) + self.assertEqual(renderTy(ForwardRef('SomeClass')), 'SomeClass') + + + def test_edge_cases(self): + # Empty tuple + self.assertEqual(renderTy(tuple[()]), 'tuple[()]') + + # Single element tuple + self.assertEqual(renderTy(tuple[int]), 'tuple[int]') + + # Very nested types + self.assertEqual(renderTy(dict[str, dict[str, dict[str, int]]]), + 'dict[str, dict[str, dict[str, int]]]') + + # Union with single type + self.assertEqual(renderTy(Union[int]), 'int') + + # Complex Callable with nested types + self.assertEqual(renderTy(Callable[[Callable[[int], str], list[int]], dict[str, Any]]), + 'Callable[[Callable[[int], str], list[int]], dict[str, Any]]') + + def test_custom_classes(self): + # Custom class types + + self.assertEqual(renderTy(MyClass), 'tests.test_renderTy.MyClass') + self.assertEqual(renderTy(list[MyClass]), 'list[tests.test_renderTy.MyClass]') + self.assertEqual(renderTy(Optional[MyClass]), 'Optional[tests.test_renderTy.MyClass]') + diff --git a/python/tests/test_stacktrace.py b/python/tests/test_stacktrace.py new file mode 100644 index 00000000..a39b7522 --- /dev/null +++ b/python/tests/test_stacktrace.py @@ -0,0 +1,35 @@ +import unittest +import sys +import types +import stacktrace +from stacktraceTestData import * +import os + +class TestReturnTracker(unittest.TestCase): + + def setUp(self): + self.original_profile = sys.getprofile() + + def tearDown(self): + sys.setprofile(self.original_profile) + + def assertReturnFrame(self, tracker: stacktrace.ReturnTracker, line: int): + frame = tracker.getReturnFrame(0) + assert(frame is not None) + self.assertEqual(os.path.basename(frame.filename), 'stacktraceTestData.py') + self.assertEqual(frame.lineno, line) + + def test_returnTracker1(self): + tracker = stacktrace.installProfileHook(1) + f1() + self.assertReturnFrame(tracker, f1ReturnLine) + + def test_returnTracker2(self): + tracker = stacktrace.installProfileHook(1) + f2() + self.assertReturnFrame(tracker, f2ReturnLine) + + def test_returnTracker3(self): + tracker = stacktrace.installProfileHook(1) + f3() + self.assertReturnFrame(tracker, f3ReturnLine) diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py new file mode 100644 index 00000000..47460980 --- /dev/null +++ b/python/tests/test_utils.py @@ -0,0 +1,12 @@ +import unittest +from utils import dropWhile + +class TestUtils(unittest.TestCase): + + def test_dropWhile(self): + self.assertEqual(dropWhile([1, 2, 3, 4, 5], lambda x: x < 4), [4, 5]) + self.assertEqual(dropWhile([], lambda x: x < 10), []) + # Test where no elements satisfy the condition + self.assertEqual(dropWhile([5, 6, 7, 8], lambda x: x < 3), [5, 6, 7, 8]) + # Test where all elements satisfy the condition + self.assertEqual(dropWhile([1, 2, 3, 4], lambda x: x < 10), []) diff --git a/pytrace-generator/main.py b/pytrace-generator/main.py index 3bff2807..1d8d545b 100644 --- a/pytrace-generator/main.py +++ b/pytrace-generator/main.py @@ -254,7 +254,7 @@ class TraceStep: stack: Stack heap: Heap stdout: str - traceback_text: str + traceback_text: str | None def format(self): step = { @@ -358,7 +358,8 @@ def trace_dispatch(self, frame, event, arg): skip = filename != self.skip_until elif not filename.startswith(os.path.dirname(self.filename)): skip = True - self.skip_until = frame.f_back.f_code.co_filename + if frame.f_back: + self.skip_until = frame.f_back.f_code.co_filename if skip: return self.trace_dispatch else: @@ -403,7 +404,8 @@ def trace_dispatch(self, frame, event, arg): traceback_text = traceback_text_tmp.getvalue() - step = TraceStep(line, filename, copy.deepcopy(self.stack), copy.deepcopy(heap), accumulated_stdout, traceback_text) + step = TraceStep(line, filename, copy.deepcopy(self.stack), copy.deepcopy(heap), + accumulated_stdout, traceback_text) is_annotation = next_source_line.startswith("@") should_display_step = not is_annotation @@ -468,6 +470,13 @@ def run_script(self, filename, script_str): # It's a bit hacky but probably the easiest solution script_str += "\npass\n" +# Add code directory of wypp to path +thisDir = os.path.normpath(os.path.dirname(__file__)) +codeDir = os.path.normpath(os.path.join(thisDir, '..', 'python', 'code')) +wyppDir = os.path.join(codeDir, 'wypp') +sys.path.insert(0, wyppDir) +sys.path.insert(0, codeDir) + # Add script directory to path sys.path.insert(0, os.path.dirname(filename)) diff --git a/screenshot.jpg b/screenshot.jpg deleted file mode 100644 index 09d521c0..00000000 Binary files a/screenshot.jpg and /dev/null differ diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 00000000..5dc05612 Binary files /dev/null and b/screenshot.png differ diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 00000000..4d591a8a Binary files /dev/null and b/screenshot2.png differ diff --git a/src/extension.ts b/src/extension.ts index ade85008..81812b05 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -217,20 +217,43 @@ function disableTypechecking(context: vscode.ExtensionContext): boolean { return !!config[disableTypecheckingConfigKey]; } -function fixPythonConfig(context: vscode.ExtensionContext) { - const libDir = context.asAbsolutePath('python/src/'); - const pyComplConfig = vscode.workspace.getConfiguration("python.autoComplete"); - const oldPath: string[] = pyComplConfig.get("extraPaths") || []; - const newPath = oldPath.filter(v => { - return !v.includes(extensionId); - }); - if (!newPath.includes(libDir)) { - newPath.push(libDir); +async function fixPylanceConfig( + context: vscode.ExtensionContext, + folder?: vscode.WorkspaceFolder +) { + // disable warnings about wildcard imports, add wypp to pylance's extraPaths + const libDir = context.asAbsolutePath('python/code/'); + + // Use the "python" section; Pylance contributes python.analysis.* + const cfg = vscode.workspace.getConfiguration('python', folder?.uri); + const target = folder ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + + // Read existing overrides (don’t clobber other rules) + const keyOverride = 'analysis.diagnosticSeverityOverrides'; + const overrides = cfg.get>(keyOverride) ?? {}; + if (overrides.reportWildcardImportFromLibrary !== 'none') { + const updated = { + ...overrides, + reportWildcardImportFromLibrary: 'none', + }; + await cfg.update( + 'analysis.diagnosticSeverityOverrides', + updated, + target + ); } - pyComplConfig.update("extraPaths", newPath); - // const pyLintConfig = vscode.workspace.getConfiguration("python.linting"); - // pyLintConfig.update("enabled", false); + const keyExtraPaths = 'analysis.extraPaths'; + const extra = cfg.get(keyExtraPaths) ?? []; + if (!extra.includes(libDir)) { + const updated = [...extra, libDir]; + await cfg.update( + keyExtraPaths, + [...extra, libDir], + target + ); + } } class Location implements vscode.TerminalLink { @@ -329,7 +352,7 @@ export class PythonExtension { // this method is called when your extension is activated // your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { disposables.forEach(d => d.dispose()); console.log('Activating extension ' + extensionId); @@ -337,16 +360,15 @@ export function activate(context: vscode.ExtensionContext) { const outChannel = vscode.window.createOutputChannel("Write Your Python Program"); disposables.push(outChannel); - fixPythonConfig(context); + await fixPylanceConfig(context); const terminals: { [name: string]: TerminalContext } = {}; installButton("Write Your Python Program", undefined); const linkProvider = new TerminalLinkProvider(terminals); const pyExt = new PythonExtension(); + const runProg = context.asAbsolutePath('python/code/wypp/runYourProgram.py'); - // Run - const runProg = context.asAbsolutePath('python/src/runYourProgram.py'); installCmd( context, "run", @@ -383,7 +405,6 @@ export function activate(context: vscode.ExtensionContext) { "WYPP - RUN", pythonCmd + " " + fileToCommandArgument(runProg) + verboseOpt + disableOpt + - " --install-mode install" + " --interactive " + " --change-directory " + fileToCommandArgument(file) diff --git a/src/programflow-visualization/backend/backend.ts b/src/programflow-visualization/backend/backend.ts index cdba9bd5..fde26ba1 100644 --- a/src/programflow-visualization/backend/backend.ts +++ b/src/programflow-visualization/backend/backend.ts @@ -20,9 +20,6 @@ export function startBackend(context: ExtensionContext, file: Uri, outChannel: O break; } - // Get WYPP path - const runYourProgramPath = context.asAbsolutePath("python/src/runYourProgram.py"); - const mainPath = context.asAbsolutePath("pytrace-generator/main.py"); const workerPath = path.resolve(__dirname, 'trace_generator.js'); const worker = new Worker(workerPath); @@ -34,7 +31,6 @@ export function startBackend(context: ExtensionContext, file: Uri, outChannel: O mainPath: mainPath, logPort: logChannel.port1, pythonCmd: pythonCmd, - runYourProgramPath: runYourProgramPath, tracePort: traceChannel.port1 }, [logChannel.port1, traceChannel.port1]); return traceChannel.port2; diff --git a/src/programflow-visualization/backend/trace_generator.ts b/src/programflow-visualization/backend/trace_generator.ts index 5fae4985..ec9f8686 100644 --- a/src/programflow-visualization/backend/trace_generator.ts +++ b/src/programflow-visualization/backend/trace_generator.ts @@ -3,17 +3,13 @@ import { AddressInfo, createServer } from 'net'; import { dirname } from 'path'; import { MessagePort, isMainThread, parentPort } from 'worker_threads'; -function installWypp(pythonCmd: string[], runYourProgramPath: string, callback: (error: ExecFileException | null, stdout: string, stderr: string) => void) { - const args = pythonCmd.slice(1).concat([runYourProgramPath, '--install-mode', 'installOnly']); - execFile(pythonCmd[0], args, { windowsHide: true }, callback); -} - -export function generateTrace(pythonCmd: string[], mainPath: string, filePath: string, tracePort: MessagePort, logPort: MessagePort) { +export function generateTrace(pythonCmd: string[], traceGenPath: string, inputFilePath: string, tracePort: MessagePort, logPort: MessagePort) { + // traceGenPath is something like /Users/swehr/devel/write-your-python-program/pytrace-generator/main.py let childRef: ChildProcessWithoutNullStreams[] = []; let buffer = Buffer.alloc(0); const server = createServer((socket) => { socket.on('data', (data) => { - buffer = Buffer.concat([buffer, data]); + buffer = Buffer.concat([buffer as Uint8Array, data as Uint8Array]); while (buffer.length >= 4) { const dataLen = buffer.readUint32BE(0); if (buffer.length >= 4 + dataLen) { @@ -41,9 +37,12 @@ export function generateTrace(pythonCmd: string[], mainPath: string, filePath: s }); server.listen(0, '127.0.0.1', () => { const port = (server.address() as AddressInfo)?.port; - const traceArgs = [mainPath, filePath, port.toString()]; + const traceArgs = [traceGenPath, inputFilePath, port.toString()]; const args = pythonCmd.slice(1).concat(traceArgs); - const child = spawn(pythonCmd[0], args, { cwd: dirname(filePath), windowsHide: true }); + const child = spawn(pythonCmd[0], args, { + cwd: dirname(inputFilePath), + windowsHide: true + }); childRef.push(child); let err = ""; @@ -79,20 +78,6 @@ if (!isMainThread && parentPort) { initParams.tracePort.close(); return; } - installWypp(initParams.pythonCmd, initParams.runYourProgramPath, (error, stdout, stderr) => { - if (stdout.length > 0) { - initParams.logPort.postMessage(`${stdout}\n`); - } - if (stderr.length > 0) { - initParams.logPort.postMessage(`${stderr}\n`); - } - if (error !== null) { - initParams.logPort.postMessage(`${error}\n`); - initParams.logPort.close(); - initParams.tracePort.close(); - return; - } - generateTrace(initParams.pythonCmd, initParams.mainPath, initParams.file, initParams.tracePort, initParams.logPort); - }); + generateTrace(initParams.pythonCmd, initParams.mainPath, initParams.file, initParams.tracePort, initParams.logPort); }); }