diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 1a8a31c..40fe808 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -2,7 +2,7 @@ name: Question or consultation about: Ask anything about this project title: '' -labels: guestion +labels: question assignees: pomponchik --- diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 792555a..502743a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,36 +1,50 @@ name: Lint -on: - push +on: push jobs: build: - runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - shell: bash - run: pip install -r requirements_dev.txt - - - name: Install the library - shell: bash - run: pip install . - - - name: Run ruff - shell: bash - run: ruff check transfunctions - - - name: Run ruff for tests - shell: bash - run: ruff check tests + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.workflow }}-${{ hashFiles('requirements_dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip-${{ github.workflow }}- + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Install the library + shell: bash + run: pip install . + + - name: Run ruff + shell: bash + run: ruff check transfunctions + + - name: Run ruff for tests + shell: bash + run: ruff check tests + + - name: Run mypy + shell: bash + run: mypy --strict transfunctions + + - name: Run mypy for tests + shell: bash + run: mypy tests --exclude typing diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd1689b..8b3e1df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +15,12 @@ jobs: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python-version }} - name: Install dependencies shell: bash diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index 48e1774..5cd691f 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -1,49 +1,55 @@ name: Tests -on: - push +on: push jobs: build: - runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: python-version: ${{ matrix.python-version }} - - name: Install the library - shell: bash - run: pip install . - - - name: Install dependencies - shell: bash - run: pip install -r requirements_dev.txt - - - name: Print all libs - shell: bash - run: pip list - - - name: Run tests and show coverage on the command line - run: | - coverage run --source=transfunctions --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=90 - coverage xml - - - name: Upload coverage to Coveralls - if: runner.os == 'Linux' - env: - COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}} - uses: coverallsapp/github-action@v2 - with: - format: cobertura - file: coverage.xml - - - name: Run tests and show the branch coverage on the command line - run: coverage run --branch --source=transfunctions --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=90 + - name: Install the library + shell: bash + run: pip install . + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.workflow }}-${{ hashFiles('requirements_dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip-${{ github.workflow }}- + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Print all libs + shell: bash + run: pip list + + - name: Run tests and show coverage on the command line + run: | + coverage run --source=transfunctions --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=94 + coverage xml + + - name: Upload coverage to Coveralls + if: runner.os == 'Linux' + env: + COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}} + uses: coverallsapp/github-action@v2 + with: + format: cobertura + file: coverage.xml + + - name: Run tests and show the branch coverage on the command line + run: coverage run --branch --source=transfunctions --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=94 diff --git a/.gitignore b/.gitignore index ab45b6d..a9fafeb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ test.py dist venv .venv +venv3 build .ruff_cache .mypy_cache @@ -14,4 +15,5 @@ uv.lock .history .vscode/ .idea/ -temp* \ No newline at end of file +.ropeproject +node_modules diff --git a/.ruff.toml b/.ruff.toml deleted file mode 100644 index 353bb74..0000000 --- a/.ruff.toml +++ /dev/null @@ -1,4 +0,0 @@ -lint.ignore = ['E501', 'E712'] - -[format] -quote-style = "single" diff --git a/README.md b/README.md index e365f0c..8ecd4a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -![logo](https://raw.githubusercontent.com/pomponchik/transfunctions/develop/docs/assets/logo_2.svg) +
+ [![Downloads](https://static.pepy.tech/badge/transfunctions/month)](https://pepy.tech/project/transfunctions) [![Downloads](https://static.pepy.tech/badge/transfunctions)](https://pepy.tech/project/transfunctions) @@ -11,6 +12,10 @@ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +
+ +![logo](https://raw.githubusercontent.com/pomponchik/transfunctions/develop/docs/assets/logo_2.svg) + This library is designed to solve one of the most important problems in python programming - dividing all written code into 2 camps: sync and async. We get rid of code duplication by using templates. @@ -21,6 +26,7 @@ This library is designed to solve one of the most important problems in python p - [**Code generation**](#code-generation) - [**Markers**](#markers) - [**Superfunctions**](#superfunctions) +- [**Typing**](#typing) ## Quick start @@ -297,3 +303,34 @@ However, it is not completely free. The fact is that this mode uses a special tr - Exceptions will not work normally inside this function. Rather, they can be picked up and intercepted in [`sys.unraisablehook`](https://docs.python.org/3/library/sys.html#sys.unraisablehook), but they will not go up the stack above this function. This is due to a feature of CPython: exceptions that occur inside callbacks for finalizing objects are completely escaped. This mode is well suited for functions such as logging or sending statistics from your code: simple functions from which no exceptions or return values are expected. In all other cases, I recommend using the tilde syntax. + + +## Typing + +Typing is the most difficult problem we faced when developing this library. In most situations, it has already been solved, but in some cases you may still notice flaws when using `mypy` or other static type analyzers. If you encounter similar problems, please [report](https://github.com/pomponchik/transfunctions/issues) them. + +There are 2 main difficulties in developing typing here: + +- Code generation creates code in runtime that is not in the source files of your project. Whereas most type analyzers look at your code statically, at what is actually present in your files. +- We mix several types of syntax in a single template function, but the static analyzer does not know that this is a template and part of the code will be deleted from here. In its opinion, this is the final function that will continue to be used in your project. + +As you can see, typing in Python is not well suited for metaprogramming. However, in this project, almost all the problems with typing turned out to be solved in one way or another. The main reason why this is so is that we mostly *remove* code from functions, but hardly *add* it there during code generation. In other words, we almost never encounter the problem of how to type the *added* code. This makes the solution to most typing problems accessible. However! Unfortunately, we were not able to completely hide all the typing problems under the hood, but you should still be aware of some of them if you use `mypy` or another analyzer. + +If you use the keyword `yield from`, you need to call the function `yield_from_it` instead: + +```python +from transfunctions import yield_it + +@superfunction +def my_superfunction(): + print('so, ', end='') + with sync_context: + print("it's just usual function!") + with async_context: + print("it's an async function!") + with generator_context: + print("it's a generator function!") + yield_from_it([1, 2, 3]) +``` + +The keywords yield or yield from are available to you and work perfectly, but from the point of view of a static type checker, they turn the function into a generator, which should also mean a special type annotation. By replacing this fragment with a function call, we hack it. diff --git a/pyproject.toml b/pyproject.toml index ab2b6c4..04e2d19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,15 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "transfunctions" -version = "0.0.8" -authors = [ - { name="Evgeniy Blinov", email="zheni-b@yandex.ru" }, -] +version = "0.0.9" +authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = 'Say NO to Python fragmentation on sync and async' readme = "README.md" requires-python = ">=3.8" dependencies = [ - 'displayhooks>=0.0.4', + 'displayhooks>=0.0.5', 'dill==0.4.0', 'typing_extensions ; python_version <= "3.10"', ] @@ -30,6 +28,9 @@ classifiers = [ 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', + 'Programming Language :: Python :: Free Threading', + 'Programming Language :: Python :: Free Threading :: 3 - Stable', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', @@ -50,13 +51,16 @@ keywords = [ "transfunctions" = ["py.typed"] [tool.mutmut] -paths_to_mutate="transfunctions" -runner="pytest" +paths_to_mutate = "transfunctions" +runner = "pytest" [tool.pytest.ini_options] -markers = [ - "mypy_testing", -] +markers = ["mypy_testing"] + +[tool.ruff] +lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503', 'PLR0912', 'C901', 'RUF001'] +lint.select = ["ERA001", "YTT", "ASYNC", "BLE", "B", "A", "COM", "INP", "PIE", "T20", "PT", "RSE", "RET", "SIM", "SLOT", "TID252", "ARG", "PTH", "I", "C90", "N", "E", "W", "D201", "D202", "D419", "F", "PL", "PLE", "PLR", "PLW", "RUF", "TRY201", "TRY400", "TRY401"] +format.quote-style = "single" [project.urls] 'Source' = 'https://github.com/pomponchik/transfunctions' diff --git a/requirements_dev.txt b/requirements_dev.txt index d097412..2dd4729 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,4 +6,4 @@ mypy==1.14.1 pytest-mypy-testing==0.1.3 ruff==0.9.9 mutmut==3.2.3 -full_match==0.0.2 +full_match==0.0.3 diff --git a/tests/documentation/test_readme.py b/tests/documentation/test_readme.py index 47f01ab..221796f 100644 --- a/tests/documentation/test_readme.py +++ b/tests/documentation/test_readme.py @@ -3,23 +3,23 @@ from contextlib import redirect_stdout from transfunctions import ( - transfunction, - sync_context, async_context, generator_context, + sync_context, + transfunction, ) def test_quick_start(): @transfunction def template(): - print('so, ', end='') + print('so, ', end='') # noqa: T201 with sync_context: - print("it's just usual function!") + print("it's just usual function!") # noqa: T201 with async_context: - print("it's an async function!") + print("it's an async function!") # noqa: T201 with generator_context: - print("it's a generator function!") + print("it's a generator function!") # noqa: T201 yield buffer = io.StringIO() diff --git a/tests/typing/__init__.py b/tests/typing/__init__.py index e69de29..dc9fd4c 100644 --- a/tests/typing/__init__.py +++ b/tests/typing/__init__.py @@ -0,0 +1 @@ +# noqa: A005 diff --git a/tests/typing/decorators/test_superfunction_typing.py b/tests/typing/decorators/test_superfunction_typing.py index 65db39f..4e07e44 100644 --- a/tests/typing/decorators/test_superfunction_typing.py +++ b/tests/typing/decorators/test_superfunction_typing.py @@ -1,5 +1,5 @@ -import sys import asyncio +import sys from contextlib import suppress if sys.version_info <= (3, 11): @@ -9,13 +9,19 @@ import pytest -from transfunctions import superfunction, sync_context, async_context, generator_context, yield_from_it - +from transfunctions import ( + async_context, + generator_context, + superfunction, + sync_context, + yield_from_it, +) """ Что нужно проверить: 1. Что await_it, yield_from_it и yield_it типизированы. +2. Нельзя сохранять возвращаемое значение суперфункции в переменную. Что проверено: @@ -25,7 +31,7 @@ @pytest.mark.mypy_testing def test_superfunction_deduced_return_type_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -37,7 +43,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_deduced_return_type_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -49,7 +55,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_fail_on_incorrect_arg_type_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -61,7 +67,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_fail_on_incorrect_kwarg_type_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -73,7 +79,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_on_correct_args_types_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -85,7 +91,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_fail_on_incorrect_arg_type_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -97,7 +103,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_fail_on_incorrect_kwarg_type_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -109,7 +115,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_on_correct_args_types_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -121,7 +127,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_fail_on_missing_args_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -135,7 +141,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_superfunction_param_spec_fail_on_extra_args_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -149,7 +155,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_superfunction_param_spec_fail_on_extra_kwargs_sync() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -162,7 +168,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_superfunction_param_spec_fail_on_missing_args_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -176,7 +182,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_superfunction_param_spec_fail_on_extra_args_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -190,7 +196,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_superfunction_param_spec_fail_on_extra_kwargs_async() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -204,7 +210,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail # it shouldn't work because typed_superfunction is a generator function, gut it's not returning a generator object according to it's typing. def test_simple_using_of_generator_function_with_simple_yield_from() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -220,7 +226,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_wrong_using_of_generator_function_with_simple_yield_from() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -234,7 +240,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_simple_using_of_generator_function_with_yield_from_it_marker_function() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -250,7 +256,7 @@ def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_using_of_generator_function_with_yield_from_it_marker_function_with_wrong_return_value() -> None: @superfunction - def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_superfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: diff --git a/tests/typing/decorators/test_transfunction_typing.py b/tests/typing/decorators/test_transfunction_typing.py index 8c10822..f49d297 100644 --- a/tests/typing/decorators/test_transfunction_typing.py +++ b/tests/typing/decorators/test_transfunction_typing.py @@ -9,13 +9,13 @@ import pytest -from transfunctions import transfunction, sync_context, async_context +from transfunctions import async_context, sync_context, transfunction @pytest.mark.mypy_testing def test_transfunction_deduced_return_type_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -28,7 +28,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_deduced_return_type_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -40,7 +40,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_fail_on_incorrect_arg_type_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -52,7 +52,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_fail_on_incorrect_kwarg_type_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -64,7 +64,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_fail_on_missing_args_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -78,7 +78,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_transfunction_param_spec_fail_on_extra_args_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -92,7 +92,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_transfunction_param_spec_fail_on_extra_kwargs_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -105,7 +105,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_on_correct_args_types_sync(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -117,7 +117,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_fail_on_incorrect_arg_type_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -129,7 +129,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_fail_on_incorrect_kwarg_type_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -141,7 +141,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.mypy_testing def test_transfunction_param_spec_fail_on_missing_args_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -154,7 +154,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_transfunction_param_spec_fail_on_extra_args_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -168,7 +168,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_transfunction_param_spec_fail_on_extra_kwargs_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: @@ -182,7 +182,7 @@ def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: @pytest.mark.xfail def test_transfunction_param_spec_on_correct_args_types_async(): @transfunction - def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: + def typed_transfunction(arg: float, *, kwarg: int = 0) -> int: # noqa: ARG001 with sync_context: return 1 with async_context: diff --git a/tests/units/decorators/test_superfunction.py b/tests/units/decorators/test_superfunction.py index 91cb701..073252a 100644 --- a/tests/units/decorators/test_superfunction.py +++ b/tests/units/decorators/test_superfunction.py @@ -4,9 +4,19 @@ from contextlib import redirect_stdout import pytest -import full_match - -from transfunctions import superfunction, sync_context, async_context, generator_context, await_it, yield_from_it, WrongDecoratorSyntaxError, WrongTransfunctionSyntaxError, WrongMarkerSyntaxError +from full_match import match + +from transfunctions import ( + WrongDecoratorSyntaxError, + WrongMarkerSyntaxError, + WrongTransfunctionSyntaxError, + async_context, + await_it, + generator_context, + superfunction, + sync_context, + yield_from_it, +) """ Что нужно проверить: @@ -29,9 +39,9 @@ def test_just_sync_call_without_breackets(): @superfunction def function(): with sync_context: - print(1) + print(1) # noqa: T201 with async_context: - print(2) + print(2) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -45,9 +55,9 @@ def test_just_sync_call_without_tilde_syntax(): @superfunction(tilde_syntax=False) def function(): with sync_context: - print(1) + print(1) # noqa: T201 with async_context: - print(2) + print(2) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -61,9 +71,9 @@ def test_just_sync_call_with_tilde_syntax(): @superfunction(tilde_syntax=True) def function(): with sync_context: - print(1) + print(1) # noqa: T201 with async_context: - print(2) + print(2) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -77,9 +87,9 @@ def test_just_async_call(): @superfunction def function(): with sync_context: - print(1) + print(1) # noqa: T201 with async_context: - print(2) + print(2) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -93,9 +103,9 @@ def test_just_generator_iteration(): @superfunction def function(): with sync_context: - print(1) + print(1) # noqa: T201 with async_context: - print(2) + print(2) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -111,9 +121,9 @@ def test_just_sync_call_with_arguments(): @superfunction def function(a, b): with sync_context: - print(a) + print(a) # noqa: T201 with async_context: - print(b) + print(b) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -127,9 +137,9 @@ def test_just_async_call_with_arguments(): @superfunction def function(a, b): with sync_context: - print(a) + print(a) # noqa: T201 with async_context: - print(b) + print(b) # noqa: T201 with generator_context: yield from [1, 2, 3] @@ -143,9 +153,9 @@ def test_just_generator_with_arguments_iteration(): @superfunction def function(a, b): with sync_context: - print(a) + print(a) # noqa: T201 with async_context: - print(b) + print(b) # noqa: T201 with generator_context: yield from [a, b, 3] @@ -176,18 +186,18 @@ def function(a, b, c=4, d=3): def test_tilda_syntax_for_function_call_without_arguments_raise_exception(): @superfunction def function(): - raise ValueError + raise ValueError('some text') - with pytest.raises(ValueError): - ~function() == 124 + with pytest.raises(ValueError, match=match('some text')): + ~function() def test_tilda_syntax_for_function_call_with_arguments_raise_exception(): @superfunction - def function(a, b, c=4, d=3): - raise ValueError + def function(a, b, c=4, d=3): # noqa: ARG001 + raise ValueError('some text') - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=match('some text')): ~function(2, 3, d=5) @@ -268,7 +278,7 @@ def other_decorator(function): def template(): pass - with pytest.raises(WrongDecoratorSyntaxError, match=full_match('The @superfunction decorator cannot be used in conjunction with other decorators.')): + with pytest.raises(WrongDecoratorSyntaxError, match=match('The @superfunction decorator cannot be used in conjunction with other decorators.')): ~template() @@ -281,24 +291,24 @@ def other_decorator(function): def template(): pass - with pytest.raises(WrongDecoratorSyntaxError, match=full_match('The @superfunction decorator cannot be used in conjunction with other decorators.')): + with pytest.raises(WrongDecoratorSyntaxError, match=match('The @superfunction decorator cannot be used in conjunction with other decorators.')): ~template() def test_pass_coroutine_function_to_decorator(): - with pytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @superfunction. You can't use async functions.")): + with pytest.raises(ValueError, match=match("Only regular or generator functions can be used as a template for @superfunction. You can't use async functions.")): @superfunction async def function_maker(): return 4 def test_pass_not_function_to_decorator(): - with pytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @superfunction.")): + with pytest.raises(ValueError, match=match("Only regular or generator functions can be used as a template for @superfunction.")): superfunction(1) def test_try_to_pass_lambda_to_decorator(): - with pytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @superfunction. Don't use lambdas here.")): + with pytest.raises(ValueError, match=match("Only regular or generator functions can be used as a template for @superfunction. Don't use lambdas here.")): superfunction(lambda x: x) @@ -307,7 +317,7 @@ def test_choose_tilde_syntax_off_and_use_tilde(): def function(): pass - with pytest.raises(NotImplementedError, match=full_match('The syntax with ~ is disabled for this superfunction. Call it with simple breackets.')): + with pytest.raises(NotImplementedError, match=match('The syntax with ~ is disabled for this superfunction. Call it with simple breackets.')): ~function() @@ -325,7 +335,7 @@ def function(): function() - assert 'The tilde-syntax is enabled for the "function" function. Call it like this: ~function().' == exception_message + assert exception_message == 'The tilde-syntax is enabled for the "function" function. Call it like this: ~function().' sys.unraisablehook = old_hook @@ -344,27 +354,27 @@ def function(): function() - assert 'The tilde-syntax is enabled for the "function" function. Call it like this: ~function().' == exception_message + assert exception_message == 'The tilde-syntax is enabled for the "function" function. Call it like this: ~function().' sys.unraisablehook = old_hook def test_there_is_exception_if_not_tilde_mode_and_in_function_is_empty_return_in_common_block(): - with pytest.raises(WrongTransfunctionSyntaxError, match=full_match('A superfunction cannot contain a return statement.')): + with pytest.raises(WrongTransfunctionSyntaxError, match=match('A superfunction cannot contain a return statement.')): @superfunction(tilde_syntax=False) def function(): return def test_there_is_exception_if_not_tilde_mode_and_in_function_is_return_true_in_common_block(): - with pytest.raises(WrongTransfunctionSyntaxError, match=full_match('A superfunction cannot contain a return statement.')): + with pytest.raises(WrongTransfunctionSyntaxError, match=match('A superfunction cannot contain a return statement.')): @superfunction(tilde_syntax=False) def function(): return True def test_there_is_exception_if_not_tilde_mode_and_in_function_is_empty_return_in_sync_block(): - with pytest.raises(WrongTransfunctionSyntaxError, match=full_match('A superfunction cannot contain a return statement.')): + with pytest.raises(WrongTransfunctionSyntaxError, match=match('A superfunction cannot contain a return statement.')): @superfunction(tilde_syntax=False) def function(): with sync_context: @@ -372,7 +382,7 @@ def function(): def test_there_is_exception_if_not_tilde_mode_and_in_function_is_return_true_in_sync_block(): - with pytest.raises(WrongTransfunctionSyntaxError, match=full_match('A superfunction cannot contain a return statement.')): + with pytest.raises(WrongTransfunctionSyntaxError, match=match('A superfunction cannot contain a return statement.')): @superfunction(tilde_syntax=False) def function(): with sync_context: @@ -465,19 +475,6 @@ def function(): assert list(function()) == [1, 2, 3] - - - - - - - - - - - - - def test_await_it_with_two_arguments(): async def another_function(): return None @@ -487,7 +484,7 @@ def template(): with async_context: return await_it(another_function(), another_function()) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "await_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "await_it" marker can be used with only one positional argument.')): run(template()) @@ -497,7 +494,7 @@ def template(): with async_context: return await_it() - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "await_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "await_it" marker can be used with only one positional argument.')): run(template()) @@ -510,7 +507,7 @@ def template(): with async_context: return await_it(another_function(), kek=another_function()) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "await_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "await_it" marker can be used with only one positional argument.')): run(template()) @@ -520,7 +517,7 @@ def template(): with generator_context: return yield_from_it([1, 2, 3], [1, 2, 3]) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "yield_from_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "yield_from_it" marker can be used with only one positional argument.')): list(template()) @@ -530,7 +527,7 @@ def template(): with generator_context: return yield_from_it() - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "yield_from_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "yield_from_it" marker can be used with only one positional argument.')): list(template()) @@ -540,7 +537,7 @@ def template(): with generator_context: return yield_from_it([1, 2, 3], kek=[1, 2, 3]) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "yield_from_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "yield_from_it" marker can be used with only one positional argument.')): list(template()) @@ -562,7 +559,7 @@ def function(number=123): def test_list_literal_default_value_for_usual_function_with_tilde(): @superfunction - def function(number, lst=[]): + def function(number, lst=[]): # noqa: B006 lst.append(number) return lst @@ -572,7 +569,7 @@ def function(number, lst=[]): def test_list_literal_default_value_it_the_same_for_all_types_of_functions_when_usual_one_is_with_tilde(): @superfunction - def function(number, lst=[]): + def function(number, lst=[]): # noqa: B006 lst.append(number) with async_context: return lst @@ -609,7 +606,7 @@ def function(number=123): def test_list_literal_default_value_for_async_function(): @superfunction - def function(number, lst=[]): + def function(number, lst=[]): # noqa: B006 lst.append(number) return lst @@ -635,7 +632,7 @@ def function(number=123): def test_list_literal_default_value_for_generator_function(): @superfunction - def function(number, lst=[]): + def function(number, lst=[]): # noqa: B006 lst.append(number) yield from lst @@ -763,3 +760,19 @@ def function(number=global_variable): yield number assert list(function()) == ['kek'] + + +def test_use_decorator_without_at(): + def template(): + pass + + function = superfunction(template) + + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @superfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + ~function() + + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @superfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + run(function()) + + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @superfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + list(function()) diff --git a/tests/units/decorators/test_transfunction.py b/tests/units/decorators/test_transfunction.py index 6f6785f..d70031e 100644 --- a/tests/units/decorators/test_transfunction.py +++ b/tests/units/decorators/test_transfunction.py @@ -1,15 +1,24 @@ import traceback -from inspect import isfunction, iscoroutinefunction, isgeneratorfunction, getsourcelines from asyncio import run from contextlib import contextmanager +from inspect import getsourcelines, iscoroutinefunction, isfunction, isgeneratorfunction import pytest -import full_match - -from transfunctions import transfunction, CallTransfunctionDirectlyError, WrongDecoratorSyntaxError, DualUseOfDecoratorError, WrongMarkerSyntaxError +from full_match import match + +from transfunctions import ( + CallTransfunctionDirectlyError, + DualUseOfDecoratorError, + WrongDecoratorSyntaxError, + WrongMarkerSyntaxError, + async_context, + await_it, + generator_context, + sync_context, + transfunction, + yield_from_it, +) from transfunctions.transformer import FunctionTransformer -from transfunctions import async_context, sync_context, generator_context, yield_from_it, await_it - SOME_GLOBAL = 777 @@ -95,7 +104,7 @@ def function(): @pytest.mark.parametrize( - ['args', 'kwargs'], + ('args', 'kwargs'), [ ((), {}), (('lol', 'kek'), {}), @@ -108,19 +117,19 @@ def test_direct_call_or_transformer(args, kwargs): def function_maker(*args, **kwargs): pass - with pytest.raises(CallTransfunctionDirectlyError, match=full_match("You can't call a transfunction object directly, create a function, a generator function or a coroutine function from it.")): + with pytest.raises(CallTransfunctionDirectlyError, match=match("You can't call a transfunction object directly, create a function, a generator function or a coroutine function from it.")): function_maker(*args, **kwargs) def test_pass_coroutine_function_to_decorator(): - with pytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @transfunction. You can't use async functions.")): + with pytest.raises(ValueError, match=match("Only regular or generator functions can be used as a template for @transfunction. You can't use async functions.")): @transfunction async def function_maker(): return 4 def test_pass_not_function_to_decorator(): - with pytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @transfunction.")): + with pytest.raises(ValueError, match=match("Only regular or generator functions can be used as a template for @transfunction.")): transfunction(1) @@ -187,7 +196,7 @@ def function_maker(a, b, c=3): def test_try_to_pass_lambda_to_decorator(): - with pytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @transfunction. Don't use lambdas here.")): + with pytest.raises(ValueError, match=match("Only regular or generator functions can be used as a template for @transfunction. Don't use lambdas here.")): transfunction(lambda x: x) @@ -226,7 +235,7 @@ def make(): try: function() - assert False + raise AssertionError except ValueError as e: certain_traceback = list(traceback.extract_tb(e.__traceback__)) @@ -243,7 +252,7 @@ def make(): try: run(function()) - assert False + raise AssertionError except ValueError as e: certain_traceback = list(traceback.extract_tb(e.__traceback__)) @@ -261,7 +270,7 @@ def make(): try: [x for x in function()] - assert False + raise AssertionError except ValueError as e: certain_traceback = list(traceback.extract_tb(e.__traceback__)) @@ -279,7 +288,7 @@ def make(): try: function() - assert False + raise AssertionError except ValueError as e: certain_traceback = list(traceback.extract_tb(e.__traceback__)) @@ -297,7 +306,7 @@ def make(): try: run(function()) - assert False + raise AssertionError except ValueError as e: certain_traceback = list(traceback.extract_tb(e.__traceback__)) @@ -316,7 +325,7 @@ def make(): try: [x for x in function()] - assert False + raise AssertionError except ValueError as e: certain_traceback = list(traceback.extract_tb(e.__traceback__)) @@ -332,12 +341,12 @@ def function(): make = transfunction(function) - with pytest.raises(WrongDecoratorSyntaxError, match=full_match("The @transfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @transfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): function = make.get_generator_function() def test_double_use_of_decorator(): - with pytest.raises(DualUseOfDecoratorError, match=full_match("You cannot use the 'transfunction' decorator twice for the same function.")): + with pytest.raises(DualUseOfDecoratorError, match=match("You cannot use the 'transfunction' decorator twice for the same function.")): @transfunction @transfunction def make(): @@ -578,11 +587,11 @@ def make(number): def test_write_global_variable_from_usual_function_without_arguments(): @transfunction def make(): - global SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 SOME_GLOBAL += 1 - global SOME_GLOBAL - SOME_GLOBAL_BEFORE = SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 + SOME_GLOBAL_BEFORE = SOME_GLOBAL # noqa: N806 function = make.get_usual_function() function() @@ -594,11 +603,11 @@ def make(): def test_write_global_variable_from_usual_function_with_arguments(): @transfunction def make(number): - global SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 SOME_GLOBAL += number - global SOME_GLOBAL - SOME_GLOBAL_BEFORE = SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 + SOME_GLOBAL_BEFORE = SOME_GLOBAL # noqa: N806 function = make.get_usual_function() function(3) @@ -610,11 +619,11 @@ def make(number): def test_write_global_variable_from_async_function_without_arguments(): @transfunction def make(): - global SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 SOME_GLOBAL += 1 - global SOME_GLOBAL - SOME_GLOBAL_BEFORE = SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 + SOME_GLOBAL_BEFORE = SOME_GLOBAL # noqa: N806 function = make.get_async_function() run(function()) @@ -626,11 +635,11 @@ def make(): def test_write_global_variable_from_async_function_with_arguments(): @transfunction def make(number): - global SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 SOME_GLOBAL += number - global SOME_GLOBAL - SOME_GLOBAL_BEFORE = SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 + SOME_GLOBAL_BEFORE = SOME_GLOBAL # noqa: N806 function = make.get_async_function() run(function(3)) @@ -642,12 +651,12 @@ def make(number): def test_write_global_variable_from_generator_function_without_arguments(): @transfunction def make(): - global SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 SOME_GLOBAL += 1 yield None - global SOME_GLOBAL - SOME_GLOBAL_BEFORE = SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 + SOME_GLOBAL_BEFORE = SOME_GLOBAL # noqa: N806 function = make.get_generator_function() list(function()) @@ -659,12 +668,12 @@ def make(): def test_write_global_variable_from_generator_function_with_arguments(): @transfunction def make(number): - global SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 SOME_GLOBAL += number yield None - global SOME_GLOBAL - SOME_GLOBAL_BEFORE = SOME_GLOBAL + global SOME_GLOBAL # noqa: PLW0603 + SOME_GLOBAL_BEFORE = SOME_GLOBAL # noqa: N806 function = make.get_generator_function() list(function(3)) @@ -778,7 +787,7 @@ def other_decorator(function): def template(): pass - with pytest.raises(WrongDecoratorSyntaxError, match=full_match('The @transfunction decorator cannot be used in conjunction with other decorators.')): + with pytest.raises(WrongDecoratorSyntaxError, match=match('The @transfunction decorator cannot be used in conjunction with other decorators.')): template.get_usual_function() @@ -791,7 +800,7 @@ def other_decorator(function): def template(): pass - with pytest.raises(WrongDecoratorSyntaxError, match=full_match('The @transfunction decorator cannot be used in conjunction with other decorators.')): + with pytest.raises(WrongDecoratorSyntaxError, match=match('The @transfunction decorator cannot be used in conjunction with other decorators.')): template.get_usual_function() @@ -1026,7 +1035,7 @@ def context_manager_with_parentnes(): @transfunction def template(): - with sync_context: + with sync_context: # noqa: SIM117 with context_manager_with_parentnes() as something: return something @@ -1042,7 +1051,7 @@ def context_manager_with_parentnes(): @transfunction def template(a, b): - with sync_context: + with sync_context: # noqa: SIM117 with context_manager_with_parentnes() as something: return something + a + b @@ -1058,7 +1067,7 @@ def context_manager_with_parentnes(c): @transfunction def template(): - with sync_context: + with sync_context: # noqa: SIM117 with context_manager_with_parentnes(4) as something: return something @@ -1074,7 +1083,7 @@ def context_manager_with_parentnes(c): @transfunction def template(a, b): - with sync_context: + with sync_context: # noqa: SIM117 with context_manager_with_parentnes(4) as something: return something + a + b @@ -1090,7 +1099,7 @@ def context_manager_with_parentnes(): @transfunction def template(): - with async_context: + with async_context: # noqa: SIM117 with context_manager_with_parentnes() as something: return something @@ -1106,7 +1115,7 @@ def context_manager_with_parentnes(): @transfunction def template(a, b): - with async_context: + with async_context: # noqa: SIM117 with context_manager_with_parentnes() as something: return something + a + b @@ -1122,7 +1131,7 @@ def context_manager_with_parentnes(c): @transfunction def template(): - with async_context: + with async_context: # noqa: SIM117 with context_manager_with_parentnes(4) as something: return something @@ -1138,7 +1147,7 @@ def context_manager_with_parentnes(c): @transfunction def template(a, b): - with async_context: + with async_context: # noqa: SIM117 with context_manager_with_parentnes(4) as something: return something + a + b @@ -1154,7 +1163,7 @@ def context_manager_with_parentnes(): @transfunction def template(): - with generator_context: + with generator_context: # noqa: SIM117 with context_manager_with_parentnes() as something: yield something @@ -1170,7 +1179,7 @@ def context_manager_with_parentnes(): @transfunction def template(a, b): - with generator_context: + with generator_context: # noqa: SIM117 with context_manager_with_parentnes() as something: yield something + a + b @@ -1186,7 +1195,7 @@ def context_manager_with_parentnes(c): @transfunction def template(): - with generator_context: + with generator_context: # noqa: SIM117 with context_manager_with_parentnes(4) as something: yield something @@ -1202,7 +1211,7 @@ def context_manager_with_parentnes(c): @transfunction def template(a, b): - with generator_context: + with generator_context: # noqa: SIM117 with context_manager_with_parentnes(4) as something: yield something + a + b @@ -1245,7 +1254,7 @@ def template(): with async_context: return await_it(another_function(), another_function()) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "await_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "await_it" marker can be used with only one positional argument.')): template.get_async_function() @@ -1255,7 +1264,7 @@ def template(): with async_context: return await_it() - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "await_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "await_it" marker can be used with only one positional argument.')): template.get_async_function() @@ -1268,7 +1277,7 @@ def template(): with async_context: return await_it(another_function(), kek=another_function()) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "await_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "await_it" marker can be used with only one positional argument.')): template.get_async_function() @@ -1278,7 +1287,7 @@ def template(): with generator_context: return yield_from_it([1, 2, 3], [1, 2, 3]) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "yield_from_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "yield_from_it" marker can be used with only one positional argument.')): template.get_generator_function() @@ -1288,7 +1297,7 @@ def template(): with generator_context: return yield_from_it() - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "yield_from_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "yield_from_it" marker can be used with only one positional argument.')): template.get_generator_function() @@ -1298,7 +1307,7 @@ def template(): with generator_context: return yield_from_it([1, 2, 3], kek=[1, 2, 3]) - with pytest.raises(WrongMarkerSyntaxError, match=full_match('The "yield_from_it" marker can be used with only one positional argument.')): + with pytest.raises(WrongMarkerSyntaxError, match=match('The "yield_from_it" marker can be used with only one positional argument.')): template.get_generator_function() @@ -1324,7 +1333,7 @@ def template(number=123): def test_list_literal_default_value_for_usual_function(): @transfunction - def template(number, lst=[]): + def template(number, lst=[]): # noqa: B006 lst.append(number) return lst @@ -1336,7 +1345,7 @@ def template(number, lst=[]): def test_list_literal_default_value_it_the_same_for_all_types_of_functions(): @transfunction - def template(number, lst=[]): + def template(number, lst=[]): # noqa: B006 lst.append(number) with async_context: return lst @@ -1383,7 +1392,7 @@ def template(number=123): def test_list_literal_default_value_for_async_function(): @transfunction - def template(number, lst=[]): + def template(number, lst=[]): # noqa: B006 lst.append(number) return lst @@ -1415,7 +1424,7 @@ def template(number=123): def test_list_literal_default_value_for_generator_function(): @transfunction - def template(number, lst=[]): + def template(number, lst=[]): # noqa: B006 lst.append(number) yield from lst @@ -1454,7 +1463,7 @@ def template(number=SOME_GLOBAL): def test_resetted_global_variable_default_value_for_usual_function(): container = [] - SOME_GLOBAL = 'kek' + SOME_GLOBAL = 'kek' # noqa: N806 @transfunction def template(number=SOME_GLOBAL): @@ -1489,7 +1498,7 @@ def template(number=SOME_GLOBAL): def test_resetted_global_variable_default_value_for_async_function(): - SOME_GLOBAL = 'kek' + SOME_GLOBAL = 'kek' # noqa: N806 @transfunction def template(number=SOME_GLOBAL): @@ -1523,7 +1532,7 @@ def template(number=SOME_GLOBAL): def test_resetted_global_variable_default_value_for_generator_function(): - SOME_GLOBAL = 'kek' + SOME_GLOBAL = 'kek' # noqa: N806 @transfunction def template(number=SOME_GLOBAL): @@ -1532,3 +1541,19 @@ def template(number=SOME_GLOBAL): function = template.get_generator_function() assert list(function()) == ['kek'] + + +def test_use_decorator_without_at(): + def template(): + pass + + template = transfunction(template) + + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @transfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + template.get_usual_function() + + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @transfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + template.get_async_function() + + with pytest.raises(WrongDecoratorSyntaxError, match=match("The @transfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")): + template.get_generator_function() diff --git a/tests/units/test_universal_namespace.py b/tests/units/test_universal_namespace.py index 58318cc..910151e 100644 --- a/tests/units/test_universal_namespace.py +++ b/tests/units/test_universal_namespace.py @@ -1,11 +1,10 @@ -from inspect import currentframe import builtins +from inspect import currentframe import pytest from transfunctions.universal_namespace import UniversalNamespaceAroundFunction - some_global = 321 def test_set_something_and_get(): diff --git a/transfunctions/__init__.py b/transfunctions/__init__.py index 523d168..3edddc6 100644 --- a/transfunctions/__init__.py +++ b/transfunctions/__init__.py @@ -1,18 +1,36 @@ -from transfunctions.decorators.transfunction import transfunction as transfunction -from transfunctions.decorators.superfunction import superfunction as superfunction - -from transfunctions.markers import ( - async_context as async_context, - sync_context as sync_context, - generator_context as generator_context, - await_it as await_it, - yield_from_it as yield_from_it, +from transfunctions.decorators.superfunction import ( + superfunction as superfunction, # noqa: PLC0414 +) +from transfunctions.decorators.transfunction import ( + transfunction as transfunction, # noqa: PLC0414 +) +from transfunctions.errors import ( + CallTransfunctionDirectlyError as CallTransfunctionDirectlyError, # noqa: PLC0414 +) +from transfunctions.errors import ( + DualUseOfDecoratorError as DualUseOfDecoratorError, # noqa: PLC0414 +) +from transfunctions.errors import ( + WrongDecoratorSyntaxError as WrongDecoratorSyntaxError, # noqa: PLC0414 +) +from transfunctions.errors import ( + WrongMarkerSyntaxError as WrongMarkerSyntaxError, # noqa: PLC0414 ) - from transfunctions.errors import ( - CallTransfunctionDirectlyError as CallTransfunctionDirectlyError, - DualUseOfDecoratorError as DualUseOfDecoratorError, - WrongDecoratorSyntaxError as WrongDecoratorSyntaxError, - WrongTransfunctionSyntaxError as WrongTransfunctionSyntaxError, - WrongMarkerSyntaxError as WrongMarkerSyntaxError, + WrongTransfunctionSyntaxError as WrongTransfunctionSyntaxError, # noqa: PLC0414 +) +from transfunctions.markers import ( + async_context as async_context, # noqa: PLC0414 +) +from transfunctions.markers import ( + await_it as await_it, # noqa: PLC0414 +) +from transfunctions.markers import ( + generator_context as generator_context, # noqa: PLC0414 +) +from transfunctions.markers import ( + sync_context as sync_context, # noqa: PLC0414 +) +from transfunctions.markers import ( + yield_from_it as yield_from_it, # noqa: PLC0414 ) diff --git a/transfunctions/decorators/superfunction.py b/transfunctions/decorators/superfunction.py index 5adb252..1d53608 100644 --- a/transfunctions/decorators/superfunction.py +++ b/transfunctions/decorators/superfunction.py @@ -2,13 +2,20 @@ from ast import AST, NodeTransformer, Return from functools import wraps from inspect import currentframe -from typing import Any, Dict, Generic, List, Optional, Union, overload +from types import FrameType, TracebackType +from typing import Any, Dict, Generic, List, Optional, Type, Union, cast, overload from displayhooks import not_display from transfunctions.errors import WrongTransfunctionSyntaxError from transfunctions.transformer import FunctionTransformer -from transfunctions.typing import Callable, Coroutine, ReturnType, FunctionParams, Generator +from transfunctions.typing import ( + Callable, + Coroutine, + FunctionParams, + Generator, + ReturnType, +) class ParamSpecContainer(Generic[FunctionParams]): @@ -60,7 +67,7 @@ def __invert__(self) -> ReturnType: def send(self, value: Any) -> Any: return self.coroutine.send(value) - def throw(self, exception_type: Any, value: Any = None, traceback: Any = None) -> None: # pragma: no cover + def throw(self, exception_type: Type[BaseException], value: Optional[BaseException] = None, traceback: Optional[TracebackType] = None) -> None: # type: ignore[override] # pragma: no cover pass def close(self) -> None: # pragma: no cover @@ -78,8 +85,7 @@ def sync_option( wrapped_coroutine.close() if not tilde_syntax: return transformer.get_usual_function()(*param_spec.args, **param_spec.kwargs) - else: - raise NotImplementedError(f'The tilde-syntax is enabled for the "{transformer.function.__name__}" function. Call it like this: ~{transformer.function.__name__}().') + raise NotImplementedError(f'The tilde-syntax is enabled for the "{transformer.function.__name__}" function. Call it like this: ~{transformer.function.__name__}().') return None @staticmethod @@ -98,12 +104,12 @@ def superfunction(function: Callable[FunctionParams, ReturnType]) -> Callable[Fu @overload def superfunction( - *, tilde_syntax: bool = True + *, tilde_syntax: bool = True, check_decorators: bool = True, ) -> Callable[[Callable[FunctionParams, ReturnType]], Callable[FunctionParams, UsageTracer[FunctionParams, ReturnType]]]: ... def superfunction( # type: ignore[misc] - *args: Callable[FunctionParams, ReturnType], tilde_syntax: bool = True + *args: Callable[FunctionParams, ReturnType], tilde_syntax: bool = True, check_decorators: bool = True, ) -> Union[ Callable[FunctionParams, UsageTracer[FunctionParams, ReturnType]], Callable[[Callable[FunctionParams, ReturnType]], Callable[FunctionParams, UsageTracer[FunctionParams, ReturnType]]], @@ -111,15 +117,15 @@ def superfunction( # type: ignore[misc] def decorator(function: Callable[FunctionParams, ReturnType]) -> Callable[FunctionParams, UsageTracer[FunctionParams, ReturnType]]: transformer = FunctionTransformer( function, - currentframe().f_back.f_lineno, # type: ignore[union-attr] + cast(FrameType, cast(FrameType, currentframe()).f_back).f_lineno, "superfunction", - currentframe().f_back, + cast(FrameType, cast(FrameType, currentframe()).f_back), + check_decorators, ) if not tilde_syntax: - class NoReturns(NodeTransformer): - def visit_Return(self, node: Return) -> Optional[Union[AST, List[AST]]]: + def visit_Return(self, node: Return) -> Optional[Union[AST, List[AST]]]: # noqa: ARG002, N802 raise WrongTransfunctionSyntaxError('A superfunction cannot contain a return statement.') transformer.get_usual_function(addictional_transformers=[NoReturns()]) @@ -127,7 +133,7 @@ def visit_Return(self, node: Return) -> Optional[Union[AST, List[AST]]]: def wrapper(*args: FunctionParams.args, **kwargs: FunctionParams.kwargs) -> UsageTracer[FunctionParams, ReturnType]: return UsageTracer(ParamSpecContainer(*args, **kwargs), transformer, tilde_syntax) - setattr(wrapper, "__is_superfunction__", True) + wrapper.__is_superfunction__ = True # type: ignore[attr-defined] return wrapper diff --git a/transfunctions/decorators/transfunction.py b/transfunctions/decorators/transfunction.py index c302398..3b361d2 100644 --- a/transfunctions/decorators/transfunction.py +++ b/transfunctions/decorators/transfunction.py @@ -1,10 +1,38 @@ from inspect import currentframe +from types import FrameType +from typing import Union, cast, overload from transfunctions.transformer import FunctionTransformer from transfunctions.typing import Callable, FunctionParams, ReturnType +@overload +def transfunction(function: Callable[FunctionParams, ReturnType]) -> FunctionTransformer[FunctionParams, ReturnType]: ... + + +@overload def transfunction( - function: Callable[FunctionParams, ReturnType], -) -> FunctionTransformer[FunctionParams, ReturnType]: - return FunctionTransformer(function, currentframe().f_back.f_lineno, "transfunction", currentframe().f_back) # type: ignore[union-attr] + *, check_decorators: bool = True, +) -> Callable[[Callable[FunctionParams, ReturnType]], FunctionTransformer[FunctionParams, ReturnType]]: ... + + +def transfunction( # type: ignore[misc] + *args: Callable[FunctionParams, ReturnType], check_decorators: bool = True, +) -> Union[Callable[[Callable[FunctionParams, ReturnType]], FunctionTransformer[FunctionParams, ReturnType]], FunctionTransformer[FunctionParams, ReturnType]]: + frame = currentframe() + + def decorator( + function: Callable[FunctionParams, ReturnType], + ) -> FunctionTransformer[FunctionParams, ReturnType]: + return FunctionTransformer( + function, + cast(FrameType, cast(FrameType, frame).f_back).f_lineno, + "transfunction", + cast(FrameType, cast(FrameType, frame).f_back), + check_decorators, + ) + + if args: + return decorator(args[0]) + + return decorator diff --git a/transfunctions/markers.py b/transfunctions/markers.py index d7ae788..b48d426 100644 --- a/transfunctions/markers.py +++ b/transfunctions/markers.py @@ -1,5 +1,5 @@ -from typing import Any, NoReturn, Generator from contextlib import contextmanager +from typing import Any, Generator, NoReturn from transfunctions.typing import IterableWithResults @@ -25,6 +25,6 @@ def create_generator_context() -> Generator[NoReturn, None, None]: def await_it(some_expression: Any) -> Any: pass # pragma: no cover -def yield_from_it(some_iterable: IterableWithResults) -> NoReturn: # type: ignore[misc] +def yield_from_it(some_iterable: IterableWithResults) -> NoReturn: # type: ignore[misc, type-arg] for value in some_iterable: # pragma: no cover return value # type: ignore[misc] diff --git a/transfunctions/transformer.py b/transfunctions/transformer.py index b5e9a65..7c8e744 100644 --- a/transfunctions/transformer.py +++ b/transfunctions/transformer.py @@ -8,41 +8,48 @@ Constant, FunctionDef, Load, + Module, Name, NodeTransformer, Pass, Return, Store, With, + YieldFrom, arguments, increment_lineno, parse, - YieldFrom, ) from functools import update_wrapper, wraps from inspect import getfile, getsource, iscoroutinefunction, isfunction from sys import version_info -from types import FunctionType, MethodType, FrameType -from typing import Any, Dict, Generic, List, Optional, Union, cast +from types import FrameType, FunctionType, MethodType +from typing import Any, Dict, Generic, List, Optional, Type, Union, cast from dill.source import getsource as dill_getsource # type: ignore[import-untyped] from transfunctions.errors import ( - AliasedDecoratorSyntaxError, CallTransfunctionDirectlyError, DualUseOfDecoratorError, WrongDecoratorSyntaxError, WrongMarkerSyntaxError, ) -from transfunctions.typing import Coroutine, Callable, Generator, FunctionParams, ReturnType +from transfunctions.typing import ( + Callable, + Coroutine, + FunctionParams, + Generator, + ReturnType, + SomeClassInstance, +) from transfunctions.universal_namespace import UniversalNamespaceAroundFunction class FunctionTransformer(Generic[FunctionParams, ReturnType]): def __init__( - self, function: Callable[FunctionParams, ReturnType], decorator_lineno: int, decorator_name: str, frame: FrameType, + self, function: Callable[FunctionParams, ReturnType], decorator_lineno: int, decorator_name: str, frame: FrameType, check_decorators: bool, ) -> None: - if isinstance(function, type(self)): + if isinstance(function, type(self)) and check_decorators: raise DualUseOfDecoratorError(f"You cannot use the '{decorator_name}' decorator twice for the same function.") if not isfunction(function): raise ValueError(f"Only regular or generator functions can be used as a template for @{decorator_name}.") @@ -55,32 +62,37 @@ def __init__( self.decorator_lineno = decorator_lineno self.decorator_name = decorator_name self.frame = frame - self.base_object = None - self.cache: Dict[str, Callable] = {} + self.check_decorators = check_decorators + self.base_object: Optional[SomeClassInstance] = None # type: ignore[valid-type] + self.cache: Dict[str, Callable[FunctionParams, ReturnType]] = {} - def __call__(self, *args: Any, **kwargs: Any) -> None: + def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 raise CallTransfunctionDirectlyError("You can't call a transfunction object directly, create a function, a generator function or a coroutine function from it.") - def __get__(self, base_object, type=None): + def __get__( + self, + base_object: SomeClassInstance, + owner: Type[SomeClassInstance], + ) -> 'FunctionTransformer[FunctionParams, ReturnType]': self.base_object = base_object return self @staticmethod - def is_lambda(function: Callable) -> bool: + def is_lambda(function: Callable[FunctionParams, ReturnType]) -> bool: # https://stackoverflow.com/a/3655857/14522393 lambda_example = lambda: 0 # noqa: E731 return isinstance(function, type(lambda_example)) and function.__name__ == lambda_example.__name__ def get_usual_function(self, addictional_transformers: Optional[List[NodeTransformer]] = None) -> Callable[FunctionParams, ReturnType]: - return self.extract_context('sync_context', addictional_transformers=addictional_transformers) + return cast(Callable[FunctionParams, ReturnType], self.extract_context('sync_context', addictional_transformers=addictional_transformers)) def get_async_function(self) -> Callable[FunctionParams, Coroutine[Any, Any, ReturnType]]: original_function = self.function class ConvertSyncFunctionToAsync(NodeTransformer): - def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]]]: + def visit_FunctionDef(self, node: FunctionDef) -> Union[FunctionDef, AsyncFunctionDef]: # noqa: N802 if node.name == original_function.__name__: - return AsyncFunctionDef( + return AsyncFunctionDef( # type: ignore[no-any-return, call-overload, unused-ignore] name=original_function.__name__, args=node.args, body=node.body, @@ -93,7 +105,7 @@ def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]] return node class ExtractAwaitExpressions(NodeTransformer): - def visit_Call(self, node: Call) -> Optional[Union[AST, List[AST]]]: + def visit_Call(self, node: Call) -> Union[Call, Await]: # noqa: N802 if isinstance(node.func, Name) and node.func.id == 'await_it': if len(node.args) != 1 or node.keywords: raise WrongMarkerSyntaxError('The "await_it" marker can be used with only one positional argument.') @@ -107,17 +119,20 @@ def visit_Call(self, node: Call) -> Optional[Union[AST, List[AST]]]: ) return node - return self.extract_context( - 'async_context', - addictional_transformers=[ - ConvertSyncFunctionToAsync(), - ExtractAwaitExpressions(), - ], + return cast( + Callable[FunctionParams, Coroutine[Any, Any, ReturnType]], + self.extract_context( + 'async_context', + addictional_transformers=[ + ConvertSyncFunctionToAsync(), + ExtractAwaitExpressions(), + ], + ), ) def get_generator_function(self) -> Callable[FunctionParams, Generator[ReturnType, None, None]]: class ConvertYieldFroms(NodeTransformer): - def visit_Call(self, node: Call) -> Optional[Union[AST, List[AST]]]: + def visit_Call(self, node: Call) -> Optional[Union[AST, List[AST]]]: # noqa: N802 if isinstance(node.func, Name) and node.func.id == 'yield_from_it': if len(node.args) != 1 or node.keywords: raise WrongMarkerSyntaxError('The "yield_from_it" marker can be used with only one positional argument.') @@ -131,11 +146,14 @@ def visit_Call(self, node: Call) -> Optional[Union[AST, List[AST]]]: ) return node - return self.extract_context( - 'generator_context', - addictional_transformers=[ - ConvertYieldFroms(), - ], + return cast( + Callable[FunctionParams, Generator[ReturnType, None, None]], + self.extract_context( + 'generator_context', + addictional_transformers=[ + ConvertYieldFroms(), + ], + ), ) @staticmethod @@ -154,7 +172,7 @@ def clear_spaces_from_source_code(source_code: str) -> str: return '\n'.join(new_splitted_source_code) - def extract_context(self, context_name: str, addictional_transformers: Optional[List[NodeTransformer]] = None): + def extract_context(self, context_name: str, addictional_transformers: Optional[List[NodeTransformer]] = None) -> Callable[FunctionParams, Union[Coroutine[Any, Any, ReturnType], Generator[ReturnType, None, None], ReturnType]]: if context_name in self.cache: return self.cache[context_name] try: @@ -165,11 +183,12 @@ def extract_context(self, context_name: str, addictional_transformers: Optional[ converted_source_code = self.clear_spaces_from_source_code(source_code) tree = parse(converted_source_code) original_function = self.function - transfunction_decorator = None + transfunction_decorator: Optional[Name] = None decorator_name = self.decorator_name + check_decorators = self.check_decorators class RewriteContexts(NodeTransformer): - def visit_With(self, node: With) -> Optional[Union[AST, List[AST]]]: + def visit_With(self, node: With) -> Optional[Union[AST, List[AST]]]: # noqa: N802 if len(node.items) == 1: if isinstance(node.items[0].context_expr, Name): context_expr = node.items[0].context_expr @@ -183,27 +202,27 @@ def visit_With(self, node: With) -> Optional[Union[AST, List[AST]]]: return node class DeleteDecorator(NodeTransformer): - def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]]]: + def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]]]: # noqa: N802 if node.name == original_function.__name__: nonlocal transfunction_decorator transfunction_decorator = None - if not node.decorator_list: + if (not node.decorator_list) and check_decorators: raise WrongDecoratorSyntaxError(f"The @{decorator_name} decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.") for decorator in node.decorator_list: if isinstance(decorator, Call): - decorator = decorator.func + decorator = decorator.func # noqa: PLW2901 if ( isinstance(decorator, Name) and decorator.id != decorator_name + and check_decorators ): raise WrongDecoratorSyntaxError(f'The @{decorator_name} decorator cannot be used in conjunction with other decorators.') - else: - if transfunction_decorator is not None: - raise DualUseOfDecoratorError(f"You cannot use the '{decorator_name}' decorator twice for the same function.") - transfunction_decorator = decorator + if transfunction_decorator is not None: + raise DualUseOfDecoratorError(f"You cannot use the @{decorator_name} decorator twice for the same function.") + transfunction_decorator = cast(Name, decorator) node.decorator_list = [] return node @@ -211,11 +230,6 @@ def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]] RewriteContexts().visit(tree) DeleteDecorator().visit(tree) - if transfunction_decorator is None: - raise AliasedDecoratorSyntaxError( - "The transfunction decorator must have been renamed." - ) - function_def = cast(FunctionDef, tree.body[0]) if not function_def.body: function_def.body.append( @@ -230,10 +244,10 @@ def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]] tree = self.wrap_ast_by_closures(tree) - if version_info.minor > 10: - increment_lineno(tree, n=(self.decorator_lineno - transfunction_decorator.lineno)) + if version_info.minor > 10: # noqa: YTT204 + increment_lineno(tree, n=(self.decorator_lineno - cast(Name, transfunction_decorator).lineno)) else: - increment_lineno(tree, n=(self.decorator_lineno - transfunction_decorator.lineno - 1)) + increment_lineno(tree, n=(self.decorator_lineno - cast(Name, transfunction_decorator).lineno - 1)) code = compile(tree, filename=getfile(self.function), mode='exec') namespace = UniversalNamespaceAroundFunction(self.function, self.frame) @@ -251,12 +265,12 @@ def visit_FunctionDef(self, node: FunctionDef) -> Optional[Union[AST, List[AST]] self.cache[context_name] = result - return result + return result # type: ignore[no-any-return] - def wrap_ast_by_closures(self, tree): + def wrap_ast_by_closures(self, tree: Module) -> Module: old_functiondef = tree.body[0] - tree.body[0] = FunctionDef( + tree.body[0] = FunctionDef( # type: ignore[call-overload, unused-ignore] name='wrapper', body=[Assign(targets=[Name(id=name, ctx=Store(), col_offset=0)], value=Constant(value=None, col_offset=0), col_offset=0) for name in self.function.__code__.co_freevars] + [ old_functiondef, @@ -276,7 +290,7 @@ def wrap_ast_by_closures(self, tree): return tree - def rewrite_globals_and_closure(self, function): + def rewrite_globals_and_closure(self, function: FunctionType) -> FunctionType: # https://stackoverflow.com/a/13503277/14522393 all_new_closure_names = set(self.function.__code__.co_freevars) @@ -297,6 +311,6 @@ def rewrite_globals_and_closure(self, function): closure=filtered_closure, ) - new_function = update_wrapper(new_function, function) + new_function = cast(FunctionType, update_wrapper(new_function, function)) new_function.__kwdefaults__ = function.__kwdefaults__ return new_function diff --git a/transfunctions/typing.py b/transfunctions/typing.py index bf3fc5c..695986b 100644 --- a/transfunctions/typing.py +++ b/transfunctions/typing.py @@ -1,6 +1,7 @@ +# noqa: A005 import sys -from typing import TypeVar from collections.abc import Iterable +from typing import TypeVar if sys.version_info >= (3, 10): from typing import ParamSpec @@ -20,10 +21,11 @@ ReturnType = TypeVar('ReturnType') FunctionParams = ParamSpec('FunctionParams') +SomeClassInstance = TypeVar('SomeClassInstance') if sys.version_info >= (3, 9): IterableWithResults = Iterable[ReturnType] else: - IterableWithResults = Iterable + IterableWithResults = Iterable # pragma: no cover -__all__ = ('ParamSpec', 'TypeAlias', 'Callable', 'Coroutine', 'Generator', 'ReturnType', 'FunctionParams', 'IterableWithResults') +__all__ = ('Callable', 'Coroutine', 'FunctionParams', 'Generator', 'IterableWithResults', 'ParamSpec', 'ReturnType', 'SomeClassInstance', 'TypeAlias') diff --git a/transfunctions/universal_namespace.py b/transfunctions/universal_namespace.py index 0223fe2..cbebc71 100644 --- a/transfunctions/universal_namespace.py +++ b/transfunctions/universal_namespace.py @@ -1,13 +1,15 @@ -from typing import Dict, Any -from types import FrameType import builtins +from types import FrameType +from typing import Any, Dict, Optional + +from transfunctions.typing import Callable, FunctionParams, ReturnType class Nothing: pass -class UniversalNamespaceAroundFunction(dict): - def __init__(self, function, frame: FrameType) -> None: +class UniversalNamespaceAroundFunction(Dict[str, Any]): + def __init__(self, function: Callable[FunctionParams, ReturnType], frame: Optional[FrameType]) -> None: self.function = function self.frame = frame self.results: Dict[str, Any] = {} @@ -19,9 +21,9 @@ def __getitem__(self, key: str) -> Any: frame = self.frame while frame: - locals = frame.f_locals - if key in locals: - return locals[key] + locals_from_frame = frame.f_locals + if key in locals_from_frame: + return locals_from_frame[key] frame = frame.f_back if key in self.function.__globals__: