diff --git a/.github/workflows/actions/prepare/action.yml b/.github/workflows/actions/prepare/action.yml new file mode 100755 index 00000000..e3970843 --- /dev/null +++ b/.github/workflows/actions/prepare/action.yml @@ -0,0 +1,77 @@ +name: 'Prepare environment' +description: 'Prepare environment' + +inputs: + python-version: + description: 'Python version to use' + required: true + +runs: + using: "composite" + steps: + - name: Set up Python paths (Unix) + if: runner.os != 'Windows' + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + shell: bash + + - name: Set up Python paths (Windows) + if: runner.os == 'Windows' + run: echo "$env:APPDATA\Python\Scripts" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + shell: pwsh + + - name: Get Poetry version + id: poetry-version + run: | + [ -f .poetry-version ] && echo "version=$(head -n 1 .poetry-version)" >> $GITHUB_OUTPUT + shell: bash + + - name: Install Poetry (Unix) + if: runner.os != 'Windows' + run: pipx install poetry==${{ steps.poetry-version.outputs.version }} + shell: bash + + - name: Install Poetry (Windows) + if: runner.os == 'Windows' + run: pipx install poetry==${{ steps.poetry-version.outputs.version }} + shell: pwsh + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + cache: 'poetry' + + - name: Clear Poetry cache (Unix) + if: runner.os != 'Windows' + run: poetry cache clear --all pypi + shell: bash + + - name: Clear Poetry cache (Windows) + if: runner.os == 'Windows' + run: poetry cache clear --all pypi + shell: pwsh + + - name: Install dependencies and package (Unix) + if: runner.os != 'Windows' + run: poetry install --no-interaction --no-ansi + shell: bash + + - name: Install dependencies and package (Windows) + if: runner.os == 'Windows' + run: poetry install --no-interaction --no-ansi + shell: pwsh + + # Setup Node.js for JavaScript tests + - uses: actions/setup-node@v4 + with: + node-version: '20' + + # Install Node.js dependencies + - name: Install Node.js dependencies + run: npm install + shell: bash + + # Setup Go for Go language tests + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml new file mode 100644 index 00000000..4694f847 --- /dev/null +++ b/.github/workflows/auto-author-assign.yml @@ -0,0 +1,16 @@ +name: Auto Author Assign + +on: + pull_request_target: + types: [ opened, reopened ] + +permissions: + pull-requests: write + +jobs: + assign-author: + runs-on: arc-runners-small + timeout-minutes: 30 + if: ${{ !github.event.pull_request.assignee }} + steps: + - uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml new file mode 100644 index 00000000..a2169de9 --- /dev/null +++ b/.github/workflows/auto-format.yml @@ -0,0 +1,46 @@ +name: Auto format + +on: + push: + branches: + - master + - release + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + auto_format: + name: Auto format with ruff + runs-on: arc-runners-small + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Get Python version + id: python_version + run: echo "python_version=$(head -n 1 .python-version)" >> $GITHUB_OUTPUT + shell: bash + + - uses: ./.github/workflows/actions/prepare + with: + python-version: ${{ steps.python_version.outputs.python_version }} + + - name: Check files using the ruff formatter + run: | + poetry run ruff check --fix --unsafe-fixes --preview --exit-zero . + poetry run ruff format . + shell: bash + + - name: Commit changes + uses: EndBug/add-and-commit@v9 + with: + fetch: false + default_author: github_actions + message: 'Backend: Auto format' + add: '.' diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml new file mode 100755 index 00000000..cb9916bc --- /dev/null +++ b/.github/workflows/build-wheels.yml @@ -0,0 +1,118 @@ +name: Build Wheels + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build_wheels: + name: Build psutil wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + permissions: + contents: write + strategy: + matrix: + os: [ "ubuntu-latest", "windows-latest", "macos-14" ] + python-version: [ "3.10", "3.11", "3.12" ] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install build dependencies + run: | + python -m pip install pip wheel + shell: bash + + - name: Build psutil wheel + run: | + # Create dist directory + mkdir -p dist + + # Build psutil wheel + pip wheel psutil==5.8.0 --wheel-dir dist/ + shell: bash + + - name: Upload to GitHub Actions + uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.os }}-py${{ matrix.python-version }} + path: dist/* + + release: + needs: build_wheels + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: release + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + + - name: Get tag version + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + shell: bash + + - name: Update wheel URLs in pyproject.toml + run: | + # Get the version without the 'v' prefix + VERSION="${{ steps.get_version.outputs.VERSION }}" + + # Update URLs in pyproject.toml + sed -i "s|/releases/download/v[0-9]\+\.[0-9]\+\.[0-9]\+/|/releases/download/${VERSION}/|g" pyproject.toml + + # Update poetry.lock + poetry lock --no-update + + # Commit changes + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git add pyproject.toml poetry.lock + git commit -m "Update wheel URLs to ${VERSION}" + + # Force update the release branch + git push origin release --force + + # Create/update tag to point to the new commit + git tag -f "${VERSION}" + git push origin "${VERSION}" --force + shell: bash + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/* + pyproject.toml + body: | + Release ${{ steps.get_version.outputs.VERSION }} + + This release includes: + - Updated wheel builds for Python 3.10, 3.11, and 3.12 + - Updated pyproject.toml with correct wheel URLs + draft: false + prerelease: false + target_commitish: release + + - name: Merge release into master + run: | + git checkout master + git pull origin master + git merge release --no-ff -m "Merge release ${VERSION} into master" + git push origin master + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 00000000..104ef55b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: + - master + - release + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + lint: + name: Lint with ruff + runs-on: arc-runners-small + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Get Python version + id: python_version + run: echo "python_version=$(head -n 1 .python-version)" >> $GITHUB_OUTPUT + shell: bash + + - uses: ./.github/workflows/actions/prepare + with: + python-version: ${{ steps.python_version.outputs.python_version }} + + - name: Check files using the ruff formatter + run: | + poetry run ruff check --fix --unsafe-fixes --preview . + shell: bash + - name: Commit changes + uses: EndBug/add-and-commit@v9 + with: + fetch: false + default_author: github_actions + message: 'Backend: Auto format' + add: '.' + + mypy: + name: Static Type Checking + needs: lint + runs-on: arc-runners-small + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Get Python version + id: python_version + run: echo "python_version=$(head -n 1 .python-version)" >> $GITHUB_OUTPUT + shell: bash + + - uses: ./.github/workflows/actions/prepare + with: + python-version: ${{ steps.python_version.outputs.python_version }} + + - name: Mypy + run: poetry run mypy . + shell: bash + + test: + name: Run unit test on ${{ matrix.os }} with Python ${{ matrix.python-version }} + needs: lint + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest", "windows-latest", "macos-14" ] + python-version: [ "3.10", "3.11", "3.12", "3.13" ] + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/actions/prepare + with: + python-version: ${{ matrix.python-version }} + + # Set environment variables for Windows + - name: Configure Windows environment + if: runner.os == 'Windows' + run: | + echo "PYTHONIOENCODING=utf-8" >> $GITHUB_ENV + echo "PYTHONUTF8=1" >> $GITHUB_ENV + echo "PYTHONLEGACYWINDOWSSTDIO=0" >> $GITHUB_ENV + shell: bash + - name: Run Tests + run: poetry run python tests/testing.py + shell: bash + env: + PYTHONIOENCODING: utf-8 + PYTHONUTF8: 1 + PYTHONLEGACYWINDOWSSTDIO: 0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 8b94c925..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Python package - -on: [ push ] - -jobs: - Lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.7 - uses: actions/setup-python@v4 - with: - python-version: "3.7" - - name: Python version - run: python --version - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - npm install - - name: python version - run: python --version - - name: Lint - run: flake8 - test-ubuntu: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: ["ubuntu-20.04", "windows-latest", "macos-latest"] - python-version: [ "3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2" ] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Python version - run: python --version - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: pip cache - uses: actions/cache@v3 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - npm install - - name: Run unittests - run: python tests/testing.py diff --git a/.poetry-version b/.poetry-version new file mode 100755 index 00000000..10bf840e --- /dev/null +++ b/.poetry-version @@ -0,0 +1 @@ +2.0.1 \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100755 index 00000000..920121e4 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.13 \ No newline at end of file diff --git a/hstest/__init__.py b/hstest/__init__.py index 86b7709e..772deb31 100644 --- a/hstest/__init__.py +++ b/hstest/__init__.py @@ -1,22 +1,20 @@ -__all__ = [ - 'StageTest', - 'DjangoTest', - 'FlaskTest', - 'PlottingTest', - 'SQLTest', - - 'TestCase', - 'SimpleTestCase', - - 'CheckResult', - 'correct', - 'wrong', +from __future__ import annotations - 'WrongAnswer', - 'TestPassed', - - 'dynamic_test', - 'TestedProgram', +__all__ = [ + "CheckResult", + "DjangoTest", + "FlaskTest", + "PlottingTest", + "SQLTest", + "SimpleTestCase", + "StageTest", + "TestCase", + "TestPassed", + "TestedProgram", + "WrongAnswer", + "correct", + "dynamic_test", + "wrong", ] from hstest.dynamic.dynamic_test import dynamic_test diff --git a/hstest/check_result.py b/hstest/check_result.py index 017fa1fd..5720ba3b 100644 --- a/hstest/check_result.py +++ b/hstest/check_result.py @@ -1,3 +1,5 @@ # deprecated, but old tests use "from hstest.check_result import CheckResult" # new way to import is "from hstest import CheckResult" +from __future__ import annotations + from hstest.test_case import CheckResult, correct, wrong # noqa: F401 diff --git a/hstest/common/file_utils.py b/hstest/common/file_utils.py index 099c5aee..9da6ea14 100644 --- a/hstest/common/file_utils.py +++ b/hstest/common/file_utils.py @@ -1,42 +1,42 @@ +from __future__ import annotations + +import contextlib import os -from typing import Dict from hstest.exception.testing import FileDeletionError -def create_files(files: Dict[str, str]): +def create_files(files: dict[str, str]) -> None: for file, content in files.items(): - with open(file, 'w') as f: + with open(file, "w", encoding="utf-8") as f: f.write(content) -def delete_files(files: Dict[str, str]): - for file in files.keys(): +def delete_files(files: dict[str, str]) -> None: + for file in files: if os.path.isfile(file): try: os.remove(file) except PermissionError: - raise FileDeletionError() + raise FileDeletionError -def safe_delete(filename): +def safe_delete(filename) -> None: if os.path.exists(filename): - try: + with contextlib.suppress(BaseException): os.remove(filename) - except BaseException: - pass def walk_user_files(folder): curr_folder = os.path.abspath(folder) - test_folder = os.path.join(curr_folder, 'test') + test_folder = os.path.join(curr_folder, "test") for folder, dirs, files in os.walk(curr_folder): if folder.startswith(test_folder): continue if folder == curr_folder: - for file in 'test.py', 'tests.py': + for file in "test.py", "tests.py": if file in files: files.remove(file) diff --git a/hstest/common/os_utils.py b/hstest/common/os_utils.py index b3364de5..4c346919 100644 --- a/hstest/common/os_utils.py +++ b/hstest/common/os_utils.py @@ -1,13 +1,15 @@ +from __future__ import annotations + import platform def is_windows(): - return platform.system() == 'Windows' + return platform.system() == "Windows" def is_mac(): - return platform.system() == 'Darwin' + return platform.system() == "Darwin" def is_linux(): - return platform.system() == 'Linux' + return platform.system() == "Linux" diff --git a/hstest/common/process_utils.py b/hstest/common/process_utils.py index c8457c02..0c29949f 100644 --- a/hstest/common/process_utils.py +++ b/hstest/common/process_utils.py @@ -1,42 +1,40 @@ -import sys +from __future__ import annotations + import threading import weakref from concurrent.futures import ThreadPoolExecutor from concurrent.futures.thread import _worker +from typing import TYPE_CHECKING -from hstest.dynamic.security.thread_group import ThreadGroup +if TYPE_CHECKING: + from hstest.dynamic.security.thread_group import ThreadGroup class DaemonThreadPoolExecutor(ThreadPoolExecutor): - def __init__(self, max_workers: int = 1, name: str = '', group: ThreadGroup = None): + def __init__(self, max_workers: int = 1, name: str = "", group: ThreadGroup = None) -> None: super().__init__(max_workers=max_workers, thread_name_prefix=name) self.group = group # Adjusted method from the ThreadPoolExecutor class just to create threads as daemons - def _adjust_thread_count(self): - if sys.version_info >= (3, 8): - # if idle threads are available, don't spin new threads - if self._idle_semaphore.acquire(timeout=0): - return + def _adjust_thread_count(self) -> None: + if self._idle_semaphore.acquire(timeout=0): + return # When the executor gets lost, the weakref callback will wake up # the worker threads. - def weakref_cb(_, q=self._work_queue): + def weakref_cb(_, q=self._work_queue) -> None: q.put(None) num_threads = len(self._threads) if num_threads < self._max_workers: - thread_name = '%s_%d' % (self._thread_name_prefix or self, - num_threads) - - if sys.version_info >= (3, 7): - args = (weakref.ref(self, weakref_cb), - self._work_queue, - self._initializer, - self._initargs) - else: - args = (weakref.ref(self, weakref_cb), - self._work_queue) + thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) + + args = ( + weakref.ref(self, weakref_cb), + self._work_queue, + self._initializer, + self._initargs, + ) t = threading.Thread(name=thread_name, target=_worker, args=args, group=self.group) t.daemon = True @@ -46,5 +44,6 @@ def weakref_cb(_, q=self._work_queue): def is_port_in_use(port): import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(('localhost', port)) == 0 + return s.connect_ex(("localhost", port)) == 0 diff --git a/hstest/common/reflection_utils.py b/hstest/common/reflection_utils.py index dc0f04ea..645ea821 100644 --- a/hstest/common/reflection_utils.py +++ b/hstest/common/reflection_utils.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import inspect +import itertools import os -from typing import List from hstest.exception.failure_handler import get_traceback_stack @@ -9,21 +11,21 @@ def is_tests(stage): package = inspect.getmodule(stage).__package__ file = inspect.getmodule(stage).__file__ return ( - package and package.startswith('tests.outcomes.') or - package and package.startswith('tests.projects.') or - file and f'{os.sep}hs-test-python{os.sep}tests{os.sep}outcomes{os.sep}' in file or - file and f'{os.sep}hs-test-python{os.sep}tests{os.sep}projects{os.sep}' in file or - file and f'{os.sep}hs-test-python{os.sep}tests{os.sep}sql{os.sep}' in file + (package and package.startswith("tests.outcomes.")) + or (package and package.startswith("tests.projects.")) + or (file and f"{os.sep}hs-test-python{os.sep}tests{os.sep}outcomes{os.sep}" in file) + or (file and f"{os.sep}hs-test-python{os.sep}tests{os.sep}projects{os.sep}" in file) + or (file and f"{os.sep}hs-test-python{os.sep}tests{os.sep}sql{os.sep}" in file) ) -def setup_cwd(stage): +def setup_cwd(stage) -> None: if stage.is_tests: test_file = inspect.getmodule(stage).__file__ test_folder = os.path.dirname(test_file) os.chdir(test_folder) - if os.path.basename(os.getcwd()) == 'test': + if os.path.basename(os.getcwd()) == "test": os.chdir(os.path.dirname(os.getcwd())) @@ -31,15 +33,15 @@ def get_stacktrace(ex: BaseException, hide_internals=False) -> str: traceback_stack = get_traceback_stack(ex) if not hide_internals: - return ''.join(traceback_stack) + return "".join(traceback_stack) if isinstance(ex, SyntaxError): - if ex.filename.startswith('<'): # "", or "" + if ex.filename.startswith("<"): # "", or "" user_dir = ex.filename else: user_dir = os.path.dirname(ex.filename) + os.sep else: - user_dir = '' + user_dir = "" user_traceback = [] for tr in traceback_stack[::-1][1:-1]: @@ -47,23 +49,21 @@ def get_stacktrace(ex: BaseException, hide_internals=False) -> str: break user_traceback += [tr] - user_traceback = [tr for tr in user_traceback - if f'{os.sep}hstest{os.sep}' not in tr] + user_traceback = [tr for tr in user_traceback if f"{os.sep}hstest{os.sep}" not in tr] return clean_stacktrace(traceback_stack, user_traceback[::-1], user_dir) def _fix_python_syntax_error(str_trace: str) -> str: - python_traceback_initial_phrase = 'Traceback (most recent call last):' + python_traceback_initial_phrase = "Traceback (most recent call last):" python_traceback_start = ' File "' - is_python_syntax_error = 'SyntaxError' in str_trace and ( - f'\n{python_traceback_start}' in str_trace or - str_trace.startswith(python_traceback_start) + is_python_syntax_error = "SyntaxError" in str_trace and ( + f"\n{python_traceback_start}" in str_trace or str_trace.startswith(python_traceback_start) ) if is_python_syntax_error and python_traceback_initial_phrase not in str_trace: - str_trace = python_traceback_initial_phrase + '\n' + str_trace + str_trace = python_traceback_initial_phrase + "\n" + str_trace return str_trace @@ -72,23 +72,23 @@ def str_to_stacktrace(str_trace: str) -> str: str_trace = _fix_python_syntax_error(str_trace) lines = str_trace.splitlines() - traceback_lines = [i for i, line in enumerate(lines) if line.startswith(' File ')] + traceback_lines = [i for i, line in enumerate(lines) if line.startswith(" File ")] if len(traceback_lines) < 1: return str_trace traceback_stack = [] - for line_from, line_to in zip(traceback_lines, traceback_lines[1:]): - actual_lines = lines[line_from: line_to] + for line_from, line_to in itertools.pairwise(traceback_lines): + actual_lines = lines[line_from:line_to] needed_lines = [line for line in actual_lines if line.startswith(" ")] - traceback_stack += ['\n'.join(needed_lines) + '\n'] + traceback_stack += ["\n".join(needed_lines) + "\n"] - last_traceback = '' - before = '\n'.join(lines[:traceback_lines[0]]) + '\n' - after = '' + last_traceback = "" + before = "\n".join(lines[: traceback_lines[0]]) + "\n" + after = "" - for line in lines[traceback_lines[-1]:]: + for line in lines[traceback_lines[-1] :]: if not after and line.startswith(" "): last_traceback += line + "\n" else: @@ -98,7 +98,7 @@ def str_to_stacktrace(str_trace: str) -> str: user_traceback = [] for trace in traceback_stack: - r''' + r""" Avoid traceback elements such as: File "C:\Users\**\JetBrains\**\plugins\python\helpers\pydev\pydevd.py", line 1477, in _exec @@ -107,26 +107,27 @@ def str_to_stacktrace(str_trace: str) -> str: exec(compile(contents+"\n", file, 'exec'), glob, loc) Which will appear when testing locally inside PyCharm. - ''' # noqa: W291, W505, E501 - if f'{os.sep}JetBrains{os.sep}' in trace: + """ # noqa: W291 + if f"{os.sep}JetBrains{os.sep}" in trace: continue - r''' + r""" Avoid traceback elements such as: File "C:\\Python39\\lib\\importlib\\__init__.py", line 127, in import_module return _bootstrap._gcd_import(name[level:], package, level) - ''' - if f'{os.sep}importlib{os.sep}' in trace: + """ + if f"{os.sep}importlib{os.sep}" in trace: continue user_traceback += [trace] - return clean_stacktrace([before] + user_traceback + [after], user_traceback) + return clean_stacktrace([before, *user_traceback, after], user_traceback) -def clean_stacktrace(full_traceback: List[str], - user_traceback: List[str], user_dir: str = '') -> str: +def clean_stacktrace( + full_traceback: list[str], user_traceback: list[str], user_dir: str = "" +) -> str: dir_names = [] for tr in user_traceback: try: @@ -135,17 +136,18 @@ def clean_stacktrace(full_traceback: List[str], except ValueError: continue - user_file = tr[start_index: end_index] + user_file = tr[start_index:end_index] - if user_file.startswith('<'): + if user_file.startswith("<"): continue - dir_name = os.path.dirname(tr[start_index: end_index]) + dir_name = os.path.dirname(tr[start_index:end_index]) if os.path.isdir(dir_name): dir_names += [os.path.abspath(dir_name)] if dir_names: from hstest.common.os_utils import is_windows + if is_windows(): drives = {} for dir_name in dir_names: @@ -154,28 +156,28 @@ def clean_stacktrace(full_traceback: List[str], if len(drives) > 1: max_drive = max(drives.values()) - drive_to_leave = [d for d in drives if drives[d] == max_drive][0] + drive_to_leave = next(d for d in drives if drives[d] == max_drive) dir_names = [d for d in dir_names if d.startswith(drive_to_leave)] user_dir = os.path.commonpath(dir_names) + os.sep cleaned_traceback = [] for trace in full_traceback[1:-1]: - if trace.startswith(' ' * 4): + if trace.startswith(" " * 4): # Trace line that starts with 4 is a line with SyntaxError cleaned_traceback += [trace] - elif user_dir in trace or ('<' in trace and '>' in trace and '" in trace and " lines that are always in the stacktrace # but include , because it's definitely user's code - if not user_dir.startswith('<'): + if not user_dir.startswith("<"): if user_dir in trace: - trace = trace.replace(user_dir, '') + trace = trace.replace(user_dir, "") else: folder_name = os.path.basename(user_dir[:-1]) if folder_name in trace: index = trace.index(folder_name) - trace = ' File "' + trace[index + len(folder_name + os.sep):] + trace = ' File "' + trace[index + len(folder_name + os.sep) :] cleaned_traceback += [trace] - return full_traceback[0] + ''.join(cleaned_traceback) + full_traceback[-1] + return full_traceback[0] + "".join(cleaned_traceback) + full_traceback[-1] diff --git a/hstest/common/utils.py b/hstest/common/utils.py index 2f85acdc..49acea4d 100644 --- a/hstest/common/utils.py +++ b/hstest/common/utils.py @@ -1,37 +1,37 @@ +from __future__ import annotations + from time import sleep -from typing import Callable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable -failed_msg_start = '#educational_plugin FAILED + ' -failed_msg_continue = '#educational_plugin ' -success_msg = '#educational_plugin test OK' +failed_msg_start = "#educational_plugin FAILED + " +failed_msg_continue = "#educational_plugin " +success_msg = "#educational_plugin test OK" def failed(message: str, is_unittest: bool): - """ Reports failure """ + """Reports failure.""" if not is_unittest: lines = message.splitlines() - print('\n' + failed_msg_start + lines[0]) - for line in lines[1:]: - print(failed_msg_continue + line) + for _line in lines[1:]: + pass return -1, message def passed(is_unittest: bool): - """ Reports success """ + """Reports success.""" if not is_unittest: - print('\n' + success_msg) - return 0, 'test OK' + pass + return 0, "test OK" def clean_text(text: str) -> str: - return ( - text.replace('\r\n', '\n') - .replace('\r', '\n') - .replace('\u00a0', '\u0020') - ) + return text.replace("\r\n", "\n").replace("\r", "\n").replace("\u00a0", "\u0020") -def try_many_times(times_to_try: int, sleep_time_ms: int, exit_func: Callable[[], bool]): +def try_many_times(times_to_try: int, sleep_time_ms: int, exit_func: Callable[[], bool]) -> bool: while times_to_try > 0: times_to_try -= 1 if exit_func(): diff --git a/hstest/dynamic/dynamic_test.py b/hstest/dynamic/dynamic_test.py index c0952a4b..344b69d4 100644 --- a/hstest/dynamic/dynamic_test.py +++ b/hstest/dynamic/dynamic_test.py @@ -1,23 +1,26 @@ +from __future__ import annotations + import inspect -from typing import Any, Dict, List +from typing import Any from hstest.stage_test import StageTest from hstest.test_case.test_case import DEFAULT_TIME_LIMIT -def dynamic_test(func=None, *, - order: int = 0, - time_limit: int = DEFAULT_TIME_LIMIT, - data: List[Any] = None, - feedback: str = "", - repeat: int = 1, - files: Dict[str, str] = None): - """ - Decorator for creating dynamic tests - """ +def dynamic_test( + func=None, + *, + order: int = 0, + time_limit: int = DEFAULT_TIME_LIMIT, + data: list[Any] | None = None, + feedback: str = "", + repeat: int = 1, + files: dict[str, str] | None = None, +): + """Decorator for creating dynamic tests.""" class DynamicTestingMethod: - def __init__(self, fn): + def __init__(self, fn) -> None: self.fn = fn def __set_name__(self, owner, name): @@ -32,17 +35,18 @@ def __set_name__(self, owner, name): return from hstest.dynamic.input.dynamic_testing import DynamicTestElement - methods: List[DynamicTestElement] = owner.dynamic_methods() + + methods: list[DynamicTestElement] = owner.dynamic_methods() methods += [ DynamicTestElement( - test=lambda *a, **k: self.fn(*a, **k), + test=self.fn, name=self.fn.__name__, order=(order, len(methods)), repeat=repeat, time_limit=time_limit, feedback=feedback, data=data, - files=files + files=files, ) ] diff --git a/hstest/dynamic/input/dynamic_input_func.py b/hstest/dynamic/input/dynamic_input_func.py index 25e33d8a..9a730366 100644 --- a/hstest/dynamic/input/dynamic_input_func.py +++ b/hstest/dynamic/input/dynamic_input_func.py @@ -1,17 +1,21 @@ -from typing import Callable, Optional, TYPE_CHECKING, Union +from __future__ import annotations + +from typing import TYPE_CHECKING if TYPE_CHECKING: + from collections.abc import Callable + from hstest import CheckResult - InputFunction = Callable[[str], Union[str, CheckResult]] - DynamicTestFunction = Callable[[], Optional[str]] + InputFunction = Callable[[str], str | CheckResult] + DynamicTestFunction = Callable[[], str | None] class DynamicInputFunction: - def __init__(self, trigger_count: int, func: 'InputFunction'): + def __init__(self, trigger_count: int, func: InputFunction) -> None: self.trigger_count = trigger_count self.input_function = func - def trigger(self): + def trigger(self) -> None: if self.trigger_count > 0: self.trigger_count -= 1 diff --git a/hstest/dynamic/input/dynamic_input_handler.py b/hstest/dynamic/input/dynamic_input_handler.py index c02edc7e..5226a317 100644 --- a/hstest/dynamic/input/dynamic_input_handler.py +++ b/hstest/dynamic/input/dynamic_input_handler.py @@ -1,4 +1,6 @@ -from typing import List, Optional, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.common.utils import clean_text from hstest.dynamic.output.infinite_loop_detector import loop_detector @@ -9,22 +11,22 @@ class DynamicInputHandler: - def __init__(self, func: 'DynamicTestFunction'): + def __init__(self, func: DynamicTestFunction) -> None: self._dynamic_input_function: DynamicTestFunction = func - self._input_lines: List[str] = [] + self._input_lines: list[str] = [] - def eject_next_line(self) -> Optional[str]: + def eject_next_line(self) -> str | None: if len(self._input_lines) == 0: self._eject_next_input() if len(self._input_lines) == 0: return None - next_line = self._input_lines.pop(0) + '\n' - OutputHandler.inject_input('> ' + next_line) + next_line = self._input_lines.pop(0) + "\n" + OutputHandler.inject_input("> " + next_line) loop_detector.input_requested() return next_line - def _eject_next_input(self): + def _eject_next_input(self) -> None: new_input = self._dynamic_input_function() if new_input is None: @@ -32,7 +34,7 @@ def _eject_next_input(self): new_input = clean_text(new_input) - if new_input.endswith('\n'): + if new_input.endswith("\n"): new_input = new_input[:-1] - self._input_lines += new_input.split('\n') + self._input_lines += new_input.split("\n") diff --git a/hstest/dynamic/input/dynamic_testing.py b/hstest/dynamic/input/dynamic_testing.py index 7cbd1f8b..774b0c59 100644 --- a/hstest/dynamic/input/dynamic_testing.py +++ b/hstest/dynamic/input/dynamic_testing.py @@ -1,53 +1,60 @@ +from __future__ import annotations + import typing -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any from hstest.common.utils import clean_text from hstest.exception.outcomes import TestPassed, UnexpectedError, WrongAnswer from hstest.testing.tested_program import TestedProgram if typing.TYPE_CHECKING: + from collections.abc import Callable + from hstest import CheckResult, StageTest, TestCase from hstest.dynamic.input.dynamic_input_func import DynamicInputFunction - DynamicTesting = Callable[[], Optional[CheckResult]] - DynamicTestingWithoutParams = Callable[[StageTest, Any], Optional[CheckResult]] + DynamicTesting = Callable[[], CheckResult | None] + DynamicTestingWithoutParams = Callable[[StageTest, Any], CheckResult | None] class DynamicTestElement: - def __init__(self, - test: 'DynamicTestingWithoutParams', - name: str, - order: Tuple[int, int], - repeat: int, - time_limit: int, - feedback: str, - data: List[Any], - files: Dict[str, str]): + def __init__( + self, + test: DynamicTestingWithoutParams, + name: str, + order: tuple[int, int], + repeat: int, + time_limit: int, + feedback: str, + data: list[Any], + files: dict[str, str], + ) -> None: self.test: DynamicTestingWithoutParams = test - self.name: str = f"Data passed to dynamic method \"{name}\"" + self.name: str = f'Data passed to dynamic method "{name}"' self.method_name = name - self.order: Tuple[int, int] = order + self.order: tuple[int, int] = order self.repeat: int = repeat self.time_limit: int = time_limit self.feedback: str = feedback - self.data: Optional[List[Any]] = data - self.files: Optional[Dict[str, str]] = files - self.args_list: Optional[List[List[Any]]] = None + self.data: list[Any] | None = data + self.files: dict[str, str] | None = files + self.args_list: list[list[Any]] | None = None - def extract_parametrized_data(self): + def extract_parametrized_data(self) -> None: if self.data is None: self.data = [[]] - if type(self.data) not in [list, tuple]: - raise UnexpectedError(f"{self.name} should be of type " - f"\"list\" or \"tuple\", found {type(self.data)}.") + if type(self.data) not in {list, tuple}: + msg = f"{self.name} should be of type " f'"list" or "tuple", found {type(self.data)}.' + raise UnexpectedError(msg) if len(self.data) == 0: - raise UnexpectedError(f"{self.name} should not be empty.") + msg = f"{self.name} should not be empty." + raise UnexpectedError(msg) found_lists_inside = True for obj in self.data: - if type(obj) not in [list, tuple]: + if type(obj) not in {list, tuple}: found_lists_inside = False break @@ -56,49 +63,57 @@ def extract_parametrized_data(self): else: self.args_list = [[obj] for obj in self.data] - def check_errors(self): + def check_errors(self) -> None: if self.repeat < 0: - raise UnexpectedError(f'Dynamic test "{self.method_name}" ' - f'should not be repeated < 0 times, found {self.repeat}') + msg = ( + f'Dynamic test "{self.method_name}" ' + f"should not be repeated < 0 times, found {self.repeat}" + ) + raise UnexpectedError(msg) if self.files is not None: if type(self.files) != dict: - raise UnexpectedError(f"'Files' parameter in dynamic test should be of type " - f"\"dict\", found {type(self.files)}.") + msg = ( + f"'Files' parameter in dynamic test should be of type " + f'"dict", found {type(self.files)}.' + ) + raise UnexpectedError(msg) for k, v in self.files.items(): if type(k) != str: - raise UnexpectedError( + msg = ( f"All keys in 'files' parameter in dynamic test should be " - f"of type \"str\", found {type(k)}." + f'of type "str", found {type(k)}.' ) + raise UnexpectedError(msg) if type(v) != str: - raise UnexpectedError( + msg = ( f"All values in 'files' parameter in dynamic test should be " - f"of type \"str\", found {type(v)}." + f'of type "str", found {type(v)}.' ) + raise UnexpectedError(msg) - def get_tests(self, obj) -> List['DynamicTesting']: + def get_tests(self, obj) -> list[DynamicTesting]: tests = [] - for i in range(self.repeat): + for _i in range(self.repeat): for args in self.args_list: tests += [lambda o=obj, a=args: self.test(o, *a)] return tests -def to_dynamic_testing(source: str, args: List[str], - input_funcs: List['DynamicInputFunction']) -> 'DynamicTesting': +def to_dynamic_testing( + source: str, args: list[str], input_funcs: list[DynamicInputFunction] +) -> DynamicTesting: from hstest.dynamic.input.dynamic_input_func import DynamicInputFunction from hstest.test_case.check_result import CheckResult class InputFunctionHandler: - def __init__(self, funcs: List[DynamicInputFunction]): - self.input_funcs: List[DynamicInputFunction] = [] + def __init__(self, funcs: list[DynamicInputFunction]) -> None: + self.input_funcs: list[DynamicInputFunction] = [] for func in funcs: - self.input_funcs += [ - DynamicInputFunction(func.trigger_count, func.input_function)] + self.input_funcs += [DynamicInputFunction(func.trigger_count, func.input_function)] - def eject_next_input(self, curr_output: str) -> Optional[str]: + def eject_next_input(self, curr_output: str) -> str | None: if len(self.input_funcs) == 0: return None @@ -109,22 +124,23 @@ def eject_next_input(self, curr_output: str) -> Optional[str]: next_func = input_function.input_function - new_input: Optional[str] + new_input: str | None try: obj = next_func(curr_output) if isinstance(obj, str) or obj is None: new_input = obj elif isinstance(obj, CheckResult): if obj.is_correct: - raise TestPassed() - else: - raise WrongAnswer(obj.feedback) + raise TestPassed + raise WrongAnswer(obj.feedback) else: raise UnexpectedError( - 'Dynamic input should return ' + - f'str or CheckResult objects only. Found: {type(obj)}') + "Dynamic input should return " + + f"str or CheckResult objects only. Found: {type(obj)}" + ) except BaseException as ex: from hstest.stage_test import StageTest + StageTest.curr_test_run.set_error_in_test(ex) return None @@ -136,7 +152,7 @@ def eject_next_input(self, curr_output: str) -> Optional[str]: return new_input - def dynamic_testing_function() -> Optional[CheckResult]: + def dynamic_testing_function() -> CheckResult | None: program = TestedProgram(source) output: str = program.start(*args) @@ -154,15 +170,16 @@ def dynamic_testing_function() -> Optional[CheckResult]: return dynamic_testing_function -def search_dynamic_tests(obj: 'StageTest') -> List['TestCase']: +def search_dynamic_tests(obj: StageTest) -> list[TestCase]: from hstest.test_case.test_case import TestCase - methods: List[DynamicTestElement] = obj.dynamic_methods() + + methods: list[DynamicTestElement] = obj.dynamic_methods() for m in methods: m.extract_parametrized_data() m.check_errors() - tests: List[TestCase] = [] + tests: list[TestCase] = [] for dte in sorted(methods, key=lambda x: x.order): for test in dte.get_tests(obj): @@ -171,7 +188,7 @@ def search_dynamic_tests(obj: 'StageTest') -> List['TestCase']: dynamic_testing=test, time_limit=dte.time_limit, feedback=dte.feedback, - files=dte.files + files=dte.files, ) ] diff --git a/hstest/dynamic/input/input_handler.py b/hstest/dynamic/input/input_handler.py index dd6c3421..42c702e2 100644 --- a/hstest/dynamic/input/input_handler.py +++ b/hstest/dynamic/input/input_handler.py @@ -1,10 +1,13 @@ -import io +from __future__ import annotations + import sys from typing import Any, TYPE_CHECKING from hstest.dynamic.input.input_mock import InputMock if TYPE_CHECKING: + import io + from hstest.dynamic.input.dynamic_input_func import DynamicTestFunction from hstest.dynamic.input.input_mock import Condition @@ -14,17 +17,19 @@ class InputHandler: mock_in: InputMock = InputMock() @staticmethod - def replace_input(): + def replace_input() -> None: sys.stdin = InputHandler.mock_in @staticmethod - def revert_input(): + def revert_input() -> None: sys.stdin = InputHandler.real_in @staticmethod - def install_input_handler(obj: Any, condition: 'Condition', input_func: 'DynamicTestFunction'): + def install_input_handler( + obj: Any, condition: Condition, input_func: DynamicTestFunction + ) -> None: InputHandler.mock_in.install_input_handler(obj, condition, input_func) @staticmethod - def uninstall_input_handler(obj: Any): + def uninstall_input_handler(obj: Any) -> None: InputHandler.mock_in.uninstall_input_handler(obj) diff --git a/hstest/dynamic/input/input_mock.py b/hstest/dynamic/input/input_mock.py index 44533385..3447c7f2 100644 --- a/hstest/dynamic/input/input_mock.py +++ b/hstest/dynamic/input/input_mock.py @@ -1,39 +1,43 @@ -from typing import Any, Callable, Dict, TYPE_CHECKING +from __future__ import annotations + +from typing import Any, TYPE_CHECKING from hstest.dynamic.input.dynamic_input_handler import DynamicInputHandler from hstest.dynamic.security.exit_exception import ExitException -from hstest.dynamic.security.thread_group import ThreadGroup from hstest.exception.outcomes import OutOfInputError, UnexpectedError from hstest.testing.settings import Settings if TYPE_CHECKING: + from collections.abc import Callable + from hstest.dynamic.input.dynamic_input_func import DynamicTestFunction + from hstest.dynamic.security.thread_group import ThreadGroup Condition = Callable[[], bool] class ConditionalInputHandler: - def __init__(self, condition: 'Condition', handler: DynamicInputHandler): + def __init__(self, condition: Condition, handler: DynamicInputHandler) -> None: self.condition = condition self.handler = handler class InputMock: - def __init__(self): - self.handlers: Dict[ThreadGroup, ConditionalInputHandler] = {} + def __init__(self) -> None: + self.handlers: dict[ThreadGroup, ConditionalInputHandler] = {} - def install_input_handler(self, obj: Any, condition: 'Condition', - input_func: 'DynamicTestFunction'): + def install_input_handler( + self, obj: Any, condition: Condition, input_func: DynamicTestFunction + ) -> None: if obj in self.handlers: - raise UnexpectedError("Cannot install input handler from the same program twice") - self.handlers[obj] = ConditionalInputHandler( - condition, - DynamicInputHandler(input_func) - ) + msg = "Cannot install input handler from the same program twice" + raise UnexpectedError(msg) + self.handlers[obj] = ConditionalInputHandler(condition, DynamicInputHandler(input_func)) - def uninstall_input_handler(self, obj: Any): + def uninstall_input_handler(self, obj: Any) -> None: if obj not in self.handlers: - raise UnexpectedError("Cannot uninstall input handler that doesn't exist") + msg = "Cannot uninstall input handler that doesn't exist" + raise UnexpectedError(msg) del self.handlers[obj] def __get_input_handler(self) -> DynamicInputHandler: @@ -42,17 +46,20 @@ def __get_input_handler(self) -> DynamicInputHandler: return handler.handler from hstest import StageTest - StageTest.curr_test_run.set_error_in_test(UnexpectedError( - "Cannot find input handler to read data")) - raise ExitException() + + StageTest.curr_test_run.set_error_in_test( + UnexpectedError("Cannot find input handler to read data") + ) + raise ExitException def readline(self) -> str: line = self.__get_input_handler().eject_next_line() if line is None: if not Settings.allow_out_of_input: from hstest import StageTest + StageTest.curr_test_run.set_error_in_test(OutOfInputError()) - raise ExitException() - else: - raise EOFError('EOF when reading a line') + raise ExitException + msg = "EOF when reading a line" + raise EOFError(msg) return line diff --git a/hstest/dynamic/output/colored_output.py b/hstest/dynamic/output/colored_output.py index 2266888f..c9476d9d 100644 --- a/hstest/dynamic/output/colored_output.py +++ b/hstest/dynamic/output/colored_output.py @@ -1,6 +1,8 @@ # Thanks to https://stackoverflow.com/a/45444716 # reset +from __future__ import annotations + RESET = "\033[0m" # Regular Colors diff --git a/hstest/dynamic/output/infinite_loop_detector.py b/hstest/dynamic/output/infinite_loop_detector.py index e41606b3..305f97c0 100644 --- a/hstest/dynamic/output/infinite_loop_detector.py +++ b/hstest/dynamic/output/infinite_loop_detector.py @@ -1,20 +1,21 @@ -from typing import List +from __future__ import annotations + +from typing import NoReturn from hstest.dynamic.security.exit_exception import ExitException from hstest.exception.testing import InfiniteLoopException class InfiniteLoopDetector: - - def __init__(self): + def __init__(self) -> None: self.working: bool = True self.check_same_input_between_requests = True self.check_no_input_requests_for_long = False self.check_repeatable_output = True - self._curr_line: List[str] = [] - self._since_last_input: List[str] = [] + self._curr_line: list[str] = [] + self._since_last_input: list[str] = [] self._between_input_requests = [] self._BETWEEN_INPUT_SAVED_SIZE = 20 @@ -33,7 +34,7 @@ def __init__(self): self._chars_since_last_check = 0 self._CHARS_SINCE_LAST_CHECK_MAX = 100 - def write(self, output: str): + def write(self, output: str) -> None: if not self.working: return @@ -48,11 +49,11 @@ def write(self, output: str): self._chars_since_last_input += len(output) self._chars_since_last_check += len(output) - new_lines = output.count('\n') + new_lines = output.count("\n") if new_lines: self._lines_since_last_input += new_lines - self._every_line += [''.join(self._curr_line)] + self._every_line += ["".join(self._curr_line)] self._curr_line = [] if len(self._every_line) > self._EVERY_LINE_SAVED_SIZE: self._every_line.pop(0) @@ -63,7 +64,7 @@ def write(self, output: str): self._check_inf_loop_chars() self._chars_since_last_check = 0 - def reset(self): + def reset(self) -> None: self._curr_line = [] self._chars_since_last_input = 0 self._lines_since_last_input = 0 @@ -72,11 +73,11 @@ def reset(self): self._between_input_requests = [] self._every_line = [] - def input_requested(self): + def input_requested(self) -> None: if not self.working: return - self._between_input_requests += [''.join(self._since_last_input)] + self._between_input_requests += ["".join(self._since_last_input)] if len(self._between_input_requests) > self._BETWEEN_INPUT_SAVED_SIZE: self._between_input_requests.pop(0) self._check_inf_loop_input_requests() @@ -86,17 +87,21 @@ def input_requested(self): self._chars_since_last_input = 0 self._lines_since_last_input = 0 - def _check_inf_loop_chars(self): - if (self.check_no_input_requests_for_long and - self._chars_since_last_input >= self._CHARS_SINCE_LAST_INPUT_MAX): + def _check_inf_loop_chars(self) -> None: + if ( + self.check_no_input_requests_for_long + and self._chars_since_last_input >= self._CHARS_SINCE_LAST_INPUT_MAX + ): self._fail( - f"No input request for the last {str(self._chars_since_last_input)} " + f"No input request for the last {self._chars_since_last_input!s} " f"characters being printed." ) - def _check_inf_loop_lines(self): - if (self.check_no_input_requests_for_long and - self._lines_since_last_input >= self._LINES_SINCE_LAST_INPUT_MAX): + def _check_inf_loop_lines(self) -> None: + if ( + self.check_no_input_requests_for_long + and self._lines_since_last_input >= self._LINES_SINCE_LAST_INPUT_MAX + ): self._fail( f"No input request for the last {self._lines_since_last_input} " f"lines being printed." @@ -111,7 +116,7 @@ def _check_inf_loop_lines(self): for lines_repeated in range(1, self._REPEATABLE_LINES_MAX + 1): self._check_repetition_size(lines_repeated) - def _check_repetition_size(self, lines_repeated: int): + def _check_repetition_size(self, lines_repeated: int) -> None: how_many_repetitions: int = len(self._every_line) // lines_repeated lines_to_check: int = lines_repeated * how_many_repetitions starting_from_index: int = len(self._every_line) - lines_to_check @@ -127,8 +132,7 @@ def _check_repetition_size(self, lines_repeated: int): return if lines_repeated == 1: - self._fail( - f"Last {lines_to_check} lines your program printed are the same.") + self._fail(f"Last {lines_to_check} lines your program printed are the same.") else: self._fail( f"Last {lines_to_check} lines your program printed have " @@ -136,7 +140,7 @@ def _check_repetition_size(self, lines_repeated: int): f"lines of the same text." ) - def _check_inf_loop_input_requests(self): + def _check_inf_loop_input_requests(self) -> None: if not self.check_no_input_requests_for_long: return @@ -146,18 +150,19 @@ def _check_inf_loop_input_requests(self): return self._fail( - f"Between the last {str(self._BETWEEN_INPUT_SAVED_SIZE)} " + f"Between the last {self._BETWEEN_INPUT_SAVED_SIZE!s} " f"input requests the texts being printed are identical." ) - def _fail(self, reason: str): + def _fail(self, reason: str) -> NoReturn: from hstest.stage_test import StageTest - StageTest.curr_test_run.set_error_in_test( - InfiniteLoopException(reason)) + + StageTest.curr_test_run.set_error_in_test(InfiniteLoopException(reason)) from hstest.dynamic.output.output_handler import OutputHandler + OutputHandler.print("INFINITE LOOP DETECTED") - raise ExitException() + raise ExitException loop_detector = InfiniteLoopDetector() diff --git a/hstest/dynamic/output/output_handler.py b/hstest/dynamic/output/output_handler.py index 66580ade..98004fcc 100644 --- a/hstest/dynamic/output/output_handler.py +++ b/hstest/dynamic/output/output_handler.py @@ -1,4 +1,5 @@ -import io +from __future__ import annotations + import sys from typing import Any, TYPE_CHECKING @@ -8,6 +9,8 @@ from hstest.dynamic.security.thread_group import ThreadGroup if TYPE_CHECKING: + import io + from hstest.dynamic.input.input_mock import Condition @@ -19,29 +22,26 @@ class OutputHandler: _mock_err: OutputMock = None @staticmethod - def print(obj): + def print(obj) -> None: if True: return - lines = obj.strip().split('\n') + lines = obj.strip().split("\n") group = ThreadGroup.curr_group() - if group: - name = group.name - else: - name = "Root" + name = group.name if group else "Root" - prepend = f'[{name}] ' + prepend = f"[{name}] " - output = prepend + ('\n' + prepend).join(lines) - full = BLUE + output + '\n' + RESET + output = prepend + ("\n" + prepend).join(lines) + full = BLUE + output + "\n" + RESET if group: OutputHandler.get_real_out().write(full) OutputHandler.get_real_out().flush() else: - print(full, end='') + pass @staticmethod def get_real_out() -> io.TextIOWrapper: @@ -52,7 +52,7 @@ def get_real_err() -> io.TextIOWrapper: return OutputHandler._mock_err.original @staticmethod - def replace_stdout(): + def replace_stdout() -> None: OutputHandler._real_out = sys.stdout OutputHandler._real_err = sys.stderr @@ -63,13 +63,13 @@ def replace_stdout(): sys.stderr = OutputHandler._mock_err @staticmethod - def revert_stdout(): + def revert_stdout() -> None: OutputHandler.reset_output() sys.stdout = OutputHandler._real_out sys.stderr = OutputHandler._real_err @staticmethod - def reset_output(): + def reset_output() -> None: OutputHandler._mock_out.reset() OutputHandler._mock_err.reset() @@ -90,18 +90,19 @@ def get_partial_output(obj: Any) -> str: return clean_text(OutputHandler._mock_out.partial(obj)) @staticmethod - def inject_input(user_input: str): + def inject_input(user_input: str) -> None: from hstest.stage_test import StageTest + if StageTest.curr_test_run is not None: StageTest.curr_test_run.set_input_used() OutputHandler._mock_out.inject_input(user_input) @staticmethod - def install_output_handler(obj: Any, condition: 'Condition'): + def install_output_handler(obj: Any, condition: Condition) -> None: OutputHandler._mock_out.install_output_handler(obj, condition) OutputHandler._mock_err.install_output_handler(obj, condition) @staticmethod - def uninstall_output_handler(obj: Any): + def uninstall_output_handler(obj: Any) -> None: OutputHandler._mock_out.uninstall_output_handler(obj) OutputHandler._mock_err.uninstall_output_handler(obj) diff --git a/hstest/dynamic/output/output_mock.py b/hstest/dynamic/output/output_mock.py index e21263e8..38303c9a 100644 --- a/hstest/dynamic/output/output_mock.py +++ b/hstest/dynamic/output/output_mock.py @@ -1,5 +1,6 @@ -import io -from typing import Any, Dict, List, TYPE_CHECKING +from __future__ import annotations + +from typing import Any, TYPE_CHECKING from hstest.dynamic.output.colored_output import BLUE, RESET from hstest.dynamic.output.infinite_loop_detector import loop_detector @@ -8,19 +9,20 @@ from hstest.testing.settings import Settings if TYPE_CHECKING: + import io + from hstest.dynamic.input.input_mock import Condition class ConditionalOutput: - def __init__(self, condition: 'Condition'): + def __init__(self, condition: Condition) -> None: self.condition = condition - self.output: List[str] = [] + self.output: list[str] = [] class OutputMock: - """ - original stream is used to actually see - the test in the console and nothing else + """original stream is used to actually see + the test in the console and nothing else. cloned stream is used to collect all output from the test and redirect to check function @@ -32,25 +34,25 @@ class OutputMock: but also injected input from the test """ - def __init__(self, real_out: io.TextIOWrapper, is_stderr: bool = False): + def __init__(self, real_out: io.TextIOWrapper, is_stderr: bool = False) -> None: class RealOutputMock: - def __init__(self, out: io.TextIOWrapper): + def __init__(self, out: io.TextIOWrapper) -> None: self.out = out - def write(self, text): + def write(self, text) -> None: if not ignore_stdout: self.out.write(text) - def flush(self): + def flush(self) -> None: self.out.flush() - def close(self): + def close(self) -> None: self.out.close() self._original: RealOutputMock = RealOutputMock(real_out) - self._cloned: List[str] = [] # used in check function - self._dynamic: List[str] = [] # used to append inputs - self._partial: Dict[Any, ConditionalOutput] = {} # separated outputs for each program + self._cloned: list[str] = [] # used in check function + self._dynamic: list[str] = [] # used to append inputs + self._partial: dict[Any, ConditionalOutput] = {} # separated outputs for each program self._is_stderr = is_stderr @property @@ -59,19 +61,19 @@ def original(self): @property def cloned(self) -> str: - return ''.join(self._cloned) + return "".join(self._cloned) @property def dynamic(self) -> str: - return ''.join(self._dynamic) + return "".join(self._dynamic) def partial(self, obj: Any) -> str: output = self._partial[obj].output - result = ''.join(output) + result = "".join(output) output.clear() return result - def write(self, text): + def write(self, text) -> None: partial_handler = self.__get_partial_handler() if partial_handler is None: @@ -86,34 +88,36 @@ def write(self, text): loop_detector.write(text) - def getvalue(self): + def getvalue(self) -> None: pass - def flush(self): + def flush(self) -> None: self._original.flush() - def close(self): + def close(self) -> None: self._original.close() - def inject_input(self, user_input: str): + def inject_input(self, user_input: str) -> None: self._original.write(user_input) self._dynamic.append(user_input) - def reset(self): + def reset(self) -> None: self._cloned.clear() self._dynamic.clear() for value in self._partial.values(): value.output.clear() loop_detector.reset() - def install_output_handler(self, obj: Any, condition: 'Condition'): + def install_output_handler(self, obj: Any, condition: Condition) -> None: if obj in self._partial: - raise UnexpectedError("Cannot install output handler from the same program twice") + msg = "Cannot install output handler from the same program twice" + raise UnexpectedError(msg) self._partial[obj] = ConditionalOutput(condition) - def uninstall_output_handler(self, obj: Any): + def uninstall_output_handler(self, obj: Any) -> None: if obj not in self._partial: - raise UnexpectedError("Cannot uninstall output handler that doesn't exist") + msg = "Cannot uninstall output handler that doesn't exist" + raise UnexpectedError(msg) del self._partial[obj] def __get_partial_handler(self): diff --git a/hstest/dynamic/security/exit_exception.py b/hstest/dynamic/security/exit_exception.py index f1c99b5b..8997147b 100644 --- a/hstest/dynamic/security/exit_exception.py +++ b/hstest/dynamic/security/exit_exception.py @@ -1,4 +1,9 @@ +from __future__ import annotations + +from typing import NoReturn + + class ExitException(BaseException): @staticmethod - def throw(): - raise ExitException() + def throw() -> NoReturn: + raise ExitException diff --git a/hstest/dynamic/security/exit_handler.py b/hstest/dynamic/security/exit_handler.py index 2aeef134..c48f82e9 100644 --- a/hstest/dynamic/security/exit_handler.py +++ b/hstest/dynamic/security/exit_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import builtins import os import signal @@ -28,20 +30,20 @@ def is_replaced(): return ExitHandler._replaced @staticmethod - def replace_exit(): + def replace_exit() -> None: if not ExitHandler._saved: ExitHandler._saved = True - ExitHandler._builtins_quit = builtins.quit if hasattr(builtins, 'quit') else None - ExitHandler._builtins_exit = builtins.exit if hasattr(builtins, 'exit') else None - ExitHandler._os_kill = os.kill if hasattr(os, 'kill') else None - ExitHandler._os__exit = os._exit if hasattr(os, '_exit') else None - ExitHandler._os_killpg = os.killpg if hasattr(os, 'killpg') else None - ExitHandler._sys_exit = sys.exit if hasattr(sys, 'exit') else None + ExitHandler._builtins_quit = builtins.quit if hasattr(builtins, "quit") else None + ExitHandler._builtins_exit = builtins.exit if hasattr(builtins, "exit") else None + ExitHandler._os_kill = os.kill if hasattr(os, "kill") else None + ExitHandler._os__exit = os._exit if hasattr(os, "_exit") else None + ExitHandler._os_killpg = os.killpg if hasattr(os, "killpg") else None + ExitHandler._sys_exit = sys.exit if hasattr(sys, "exit") else None ExitHandler._signal_pthread_kill = ( - signal.pthread_kill if hasattr(signal, 'pthread_kill') else None + signal.pthread_kill if hasattr(signal, "pthread_kill") else None ) ExitHandler._signal_siginterrupt = ( - signal.siginterrupt if hasattr(signal, 'siginterrupt') else None + signal.siginterrupt if hasattr(signal, "siginterrupt") else None ) builtins.quit = _throw_exit_exception @@ -56,7 +58,7 @@ def replace_exit(): ExitHandler._replaced = True @staticmethod - def revert_exit(): + def revert_exit() -> None: if ExitHandler._replaced: builtins.quit = ExitHandler._builtins_quit builtins.exit = ExitHandler._builtins_exit diff --git a/hstest/dynamic/security/thread_group.py b/hstest/dynamic/security/thread_group.py index 1365e2e5..dccbd3b2 100644 --- a/hstest/dynamic/security/thread_group.py +++ b/hstest/dynamic/security/thread_group.py @@ -1,23 +1,25 @@ +from __future__ import annotations + from threading import current_thread, Thread -from typing import List, Optional class ThreadGroup: - def __init__(self, name: str = None): + def __init__(self, name: str | None = None) -> None: if name: self._name: str = name else: from hstest import StageTest + test_num = StageTest.curr_test_global - self._name = f'Test {test_num}' + self._name = f"Test {test_num}" - self.threads: List[Thread] = [] + self.threads: list[Thread] = [] curr = current_thread() if hasattr(curr, "_group"): - self._parent: Optional[ThreadGroup] = curr._group + self._parent: ThreadGroup | None = curr._group else: - self._parent: Optional[ThreadGroup] = None + self._parent: ThreadGroup | None = None @property def name(self): @@ -27,9 +29,9 @@ def name(self): def parent(self): return self._parent - def add(self, thread: Thread): + def add(self, thread: Thread) -> None: self.threads.append(thread) @staticmethod - def curr_group() -> 'ThreadGroup': - return getattr(current_thread(), '_group', None) + def curr_group() -> ThreadGroup: + return getattr(current_thread(), "_group", None) diff --git a/hstest/dynamic/security/thread_handler.py b/hstest/dynamic/security/thread_handler.py index 469b8ddb..5c2d701e 100644 --- a/hstest/dynamic/security/thread_handler.py +++ b/hstest/dynamic/security/thread_handler.py @@ -1,24 +1,28 @@ +from __future__ import annotations + from threading import current_thread, Thread -from typing import Callable, Optional +from typing import TYPE_CHECKING from hstest.dynamic.security.thread_group import ThreadGroup +if TYPE_CHECKING: + from collections.abc import Callable -class ThreadHandler: +class ThreadHandler: _group = None - _old_init: Optional[Callable[[], Thread]] = None + _old_init: Callable[[], Thread] | None = None @classmethod - def install_thread_group(cls): + def install_thread_group(cls) -> None: if cls._old_init is None: cls._old_init = Thread.__init__ Thread.__init__ = ThreadHandler.init - cls._group = ThreadGroup('Main') + cls._group = ThreadGroup("Main") current_thread()._group = cls._group @classmethod - def uninstall_thread_group(cls): + def uninstall_thread_group(cls) -> None: if cls._old_init is not None: Thread.__init__ = cls._old_init cls._old_init = None @@ -26,9 +30,9 @@ def uninstall_thread_group(cls): cls._group = None @staticmethod - def init(self, group=None, target=None, name=None, - args=(), kwargs=None, *, daemon=None): - + def init( + self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None + ) -> None: ThreadHandler._old_init(self, None, target, name, args, kwargs, daemon=daemon) # Custom addition to Thread class (implement thread groups) @@ -37,7 +41,7 @@ def init(self, group=None, target=None, name=None, else: curr = current_thread() - if hasattr(curr, '_group'): + if hasattr(curr, "_group"): self._group = curr._group else: self._group = ThreadGroup(self._name) diff --git a/hstest/dynamic/system_handler.py b/hstest/dynamic/system_handler.py index 4b40fc9d..09f50cc6 100644 --- a/hstest/dynamic/system_handler.py +++ b/hstest/dynamic/system_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from threading import current_thread, Lock from typing import Any, TYPE_CHECKING @@ -18,7 +20,7 @@ class SystemHandler: __locker_thread = None @staticmethod - def set_up(): + def set_up() -> None: SystemHandler._lock_system_for_testing() OutputHandler.replace_stdout() @@ -27,7 +29,7 @@ def set_up(): ThreadHandler.install_thread_group() @staticmethod - def tear_down(): + def tear_down() -> None: SystemHandler._unlock_system_for_testing() OutputHandler.revert_stdout() @@ -36,33 +38,33 @@ def tear_down(): ThreadHandler.uninstall_thread_group() @staticmethod - def _lock_system_for_testing(): + def _lock_system_for_testing() -> None: with SystemHandler.__lock: if SystemHandler.__locked: - raise ErrorWithFeedback( - "Cannot start the testing process more than once") + msg = "Cannot start the testing process more than once" + raise ErrorWithFeedback(msg) SystemHandler.__locked = True SystemHandler.__locker_thread = current_thread() @staticmethod - def _unlock_system_for_testing(): + def _unlock_system_for_testing() -> None: if current_thread() != SystemHandler.__locker_thread: - raise ErrorWithFeedback( - "Cannot tear down the testing process from the other thread") + msg = "Cannot tear down the testing process from the other thread" + raise ErrorWithFeedback(msg) with SystemHandler.__lock: if not SystemHandler.__locked: - raise ErrorWithFeedback( - "Cannot tear down the testing process more than once") + msg = "Cannot tear down the testing process more than once" + raise ErrorWithFeedback(msg) SystemHandler.__locked = False SystemHandler.__locker_thread = None @staticmethod - def install_handler(obj: Any, condition: 'Condition', input_func: 'DynamicTestFunction'): + def install_handler(obj: Any, condition: Condition, input_func: DynamicTestFunction) -> None: InputHandler.install_input_handler(obj, condition, input_func) OutputHandler.install_output_handler(obj, condition) @staticmethod - def uninstall_handler(obj: Any): + def uninstall_handler(obj: Any) -> None: InputHandler.uninstall_input_handler(obj) OutputHandler.uninstall_output_handler(obj) diff --git a/hstest/exception/failure_handler.py b/hstest/exception/failure_handler.py index 21ba43f8..f2b3283f 100644 --- a/hstest/exception/failure_handler.py +++ b/hstest/exception/failure_handler.py @@ -1,34 +1,29 @@ +from __future__ import annotations + import platform -import sys import traceback -from typing import List from hstest.testing.execution_options import inside_docker -def get_report(): +def get_report() -> str: if not inside_docker: name_os = platform.system() + " " + platform.release() python = platform.python_version() implementation = platform.python_implementation() return ( - 'Submitted via IDE\n' - '\n' - f'OS {name_os}\n' - f'{implementation} {python}\n' - f'Testing library version 8' + "Submitted via IDE\n" + "\n" + f"OS {name_os}\n" + f"{implementation} {python}\n" + f"Testing library version 8" ) - else: - return 'Submitted via web' + return "Submitted via web" -def get_traceback_stack(ex: BaseException) -> List[str]: - if sys.version_info >= (3, 10): - return traceback.format_exception(ex) - else: - exc_tb = ex.__traceback__ - return traceback.format_exception(etype=type(ex), value=ex, tb=exc_tb) +def get_traceback_stack(ex: BaseException) -> list[str]: + return traceback.format_exception(ex) def get_exception_text(ex: BaseException) -> str: - return ''.join(get_traceback_stack(ex)) + return "".join(get_traceback_stack(ex)) diff --git a/hstest/exception/outcomes.py b/hstest/exception/outcomes.py index 74912d9c..7000582d 100644 --- a/hstest/exception/outcomes.py +++ b/hstest/exception/outcomes.py @@ -1,4 +1,4 @@ -from typing import Optional +from __future__ import annotations class OutcomeError(BaseException): @@ -6,35 +6,35 @@ class OutcomeError(BaseException): class SyntaxException(OutcomeError): - def __init__(self, exception: SyntaxError, file: str): + def __init__(self, exception: SyntaxError, file: str) -> None: self.file: str = file self.exception: SyntaxError = exception class ExceptionWithFeedback(OutcomeError): - def __init__(self, error_text: str, real_exception: Optional[BaseException]): + def __init__(self, error_text: str, real_exception: BaseException | None) -> None: self.error_text: str = error_text self.real_exception: BaseException = real_exception class ErrorWithFeedback(OutcomeError): - def __init__(self, error_text: str): + def __init__(self, error_text: str) -> None: self.error_text = error_text class OutOfInputError(ErrorWithFeedback): - def __init__(self): - super().__init__('Program ran out of input. You tried to read more than expected.') + def __init__(self) -> None: + super().__init__("Program ran out of input. You tried to read more than expected.") class UnexpectedError(OutcomeError): - def __init__(self, error_text: str, ex: Optional[BaseException] = None): + def __init__(self, error_text: str, ex: BaseException | None = None) -> None: self.error_text = error_text self.exception = ex class CompilationError(OutcomeError): - def __init__(self, error_text: str): + def __init__(self, error_text: str) -> None: self.error_text = error_text @@ -43,5 +43,5 @@ class TestPassed(OutcomeError): class WrongAnswer(OutcomeError): - def __init__(self, feedback: str): + def __init__(self, feedback: str) -> None: self.feedback = feedback diff --git a/hstest/exception/testing.py b/hstest/exception/testing.py index 19fb0ed9..dd9dba3f 100644 --- a/hstest/exception/testing.py +++ b/hstest/exception/testing.py @@ -1,6 +1,8 @@ +from __future__ import annotations + class TimeLimitException(BaseException): - def __init__(self, time_limit_ms: int): + def __init__(self, time_limit_ms: int) -> None: self.time_limit_ms: int = time_limit_ms @@ -13,7 +15,7 @@ class TestedProgramFinishedEarly(BaseException): class InfiniteLoopException(BaseException): - def __init__(self, message: str): + def __init__(self, message: str) -> None: self.message = message diff --git a/hstest/exceptions.py b/hstest/exceptions.py index 2c54a85c..1f64f026 100644 --- a/hstest/exceptions.py +++ b/hstest/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.exception.outcomes import TestPassed, WrongAnswer # deprecated, but have to be sure old tests work as expected diff --git a/hstest/outcomes/compilation_error_outcome.py b/hstest/outcomes/compilation_error_outcome.py index e88484af..615787cd 100644 --- a/hstest/outcomes/compilation_error_outcome.py +++ b/hstest/outcomes/compilation_error_outcome.py @@ -1,12 +1,18 @@ -from hstest.exception.outcomes import CompilationError +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.outcomes.outcome import Outcome +if TYPE_CHECKING: + from hstest.exception.outcomes import CompilationError + class CompilationErrorOutcome(Outcome): - def __init__(self, ex: CompilationError): + def __init__(self, ex: CompilationError) -> None: super().__init__() self.test_number = -1 self.error_text = ex.error_text def get_type(self) -> str: - return 'Compilation error' + return "Compilation error" diff --git a/hstest/outcomes/error_outcome.py b/hstest/outcomes/error_outcome.py index 1f5d3e1d..fdf3d78e 100644 --- a/hstest/outcomes/error_outcome.py +++ b/hstest/outcomes/error_outcome.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.common.reflection_utils import get_stacktrace from hstest.exception.outcomes import ErrorWithFeedback from hstest.exception.testing import FileDeletionError, InfiniteLoopException, TimeLimitException @@ -5,7 +7,7 @@ class ErrorOutcome(Outcome): - def __init__(self, test_num: int, cause: BaseException): + def __init__(self, test_num: int, cause: BaseException) -> None: super().__init__() self.test_number = test_num @@ -22,28 +24,30 @@ def __init__(self, test_num: int, cause: BaseException): self.error_text = "Infinite loop detected.\n" + cause.message elif isinstance(cause, KeyboardInterrupt): - self.error_text = "It seems like you've stopped the testing process forcibly.\n" \ - "If this is not the case, please report this issue to support" + self.error_text = ( + "It seems like you've stopped the testing process forcibly.\n" + "If this is not the case, please report this issue to support" + ) self.stack_trace = get_stacktrace(cause, hide_internals=False) - def _init_permission_error(self, _: FileDeletionError): + def _init_permission_error(self, _: FileDeletionError) -> None: self.error_text = ( - "The file you opened " + - "can't be deleted after the end of the test. " + - "Probably you didn't close it." + "The file you opened " + + "can't be deleted after the end of the test. " + + "Probably you didn't close it." ) - def _init_time_limit_exception(self, ex: TimeLimitException): + def _init_time_limit_exception(self, ex: TimeLimitException) -> None: time_limit: int = ex.time_limit_ms - time_unit: str = 'milliseconds' + time_unit: str = "milliseconds" if time_limit > 1999: time_limit //= 1000 - time_unit = 'seconds' + time_unit = "seconds" self.error_text = ( - 'In this test, the program is running for a long time, ' + - f'more than {time_limit} {time_unit}. Most likely, ' + - 'the program has gone into an infinite loop.' + "In this test, the program is running for a long time, " + + f"more than {time_limit} {time_unit}. Most likely, " + + "the program has gone into an infinite loop." ) def get_type(self) -> str: - return 'Error' + return "Error" diff --git a/hstest/outcomes/exception_outcome.py b/hstest/outcomes/exception_outcome.py index cda56014..2216402e 100644 --- a/hstest/outcomes/exception_outcome.py +++ b/hstest/outcomes/exception_outcome.py @@ -1,10 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.common.reflection_utils import get_stacktrace, str_to_stacktrace -from hstest.exception.outcomes import ExceptionWithFeedback from hstest.outcomes.outcome import Outcome +if TYPE_CHECKING: + from hstest.exception.outcomes import ExceptionWithFeedback + class ExceptionOutcome(Outcome): - def __init__(self, test_num: int, ex: ExceptionWithFeedback): + def __init__(self, test_num: int, ex: ExceptionWithFeedback) -> None: super().__init__() cause = ex.real_exception feedback = ex.error_text @@ -17,13 +23,13 @@ def __init__(self, test_num: int, ex: ExceptionWithFeedback): else: self.stack_trace = str_to_stacktrace(feedback) - self.error_text = '' + self.error_text = "" - eof = 'EOFError: EOF when reading a line' - eof_feedback = 'Probably your program run out of input (tried to read more than expected)' + eof = "EOFError: EOF when reading a line" + eof_feedback = "Probably your program run out of input (tried to read more than expected)" if self.stack_trace.strip().endswith(eof): - self.error_text += '\n\n' + eof_feedback + self.error_text += "\n\n" + eof_feedback def get_type(self) -> str: - return 'Exception' + return "Exception" diff --git a/hstest/outcomes/outcome.py b/hstest/outcomes/outcome.py index c67591c6..15ca9a37 100644 --- a/hstest/outcomes/outcome.py +++ b/hstest/outcomes/outcome.py @@ -1,36 +1,41 @@ +from __future__ import annotations + from hstest.common.reflection_utils import str_to_stacktrace from hstest.common.utils import clean_text from hstest.dynamic.output.output_handler import OutputHandler from hstest.exception.outcomes import ( - CompilationError, ErrorWithFeedback, ExceptionWithFeedback, WrongAnswer + CompilationError, + ErrorWithFeedback, + ExceptionWithFeedback, + WrongAnswer, ) from hstest.exception.testing import FileDeletionError, InfiniteLoopException, TimeLimitException class Outcome: - def __init__(self, test_number: int = 0, error_text: str = '', stack_trace: str = ''): + def __init__(self, test_number: int = 0, error_text: str = "", stack_trace: str = "") -> None: self.test_number: int = test_number self.error_text: str = error_text self.stack_trace: str = stack_trace def get_type(self) -> str: - raise NotImplementedError() + raise NotImplementedError - def __str__(self): + def __str__(self) -> str: if self.test_number == 0: - when_error_happened = ' during testing' + when_error_happened = " during testing" elif self.test_number > 0: - when_error_happened = f' in test #{self.test_number}' + when_error_happened = f" in test #{self.test_number}" else: - when_error_happened = '' + when_error_happened = "" result = self.get_type() + when_error_happened if self.error_text: - result += '\n\n' + clean_text(self.error_text.strip()) + result += "\n\n" + clean_text(self.error_text.strip()) if self.stack_trace: - result += '\n\n' + clean_text(self.stack_trace.strip()) + result += "\n\n" + clean_text(self.stack_trace.strip()) full_out = OutputHandler.get_dynamic_output() full_err = str_to_stacktrace(OutputHandler.get_err()) @@ -44,10 +49,11 @@ def __str__(self): worth_showing_args = len(arguments.strip()) != 0 from hstest.stage_test import StageTest + test_run = StageTest.curr_test_run if worth_showing_out or worth_showing_err or worth_showing_args: - result += '\n\n' + result += "\n\n" if worth_showing_out or worth_showing_err: result += "Please find below the output of your program during this failed test.\n" if test_run and test_run.input_used: @@ -57,12 +63,12 @@ def __str__(self): result += "\n---\n\n" if worth_showing_args: - result += arguments + '\n\n' + result += arguments + "\n\n" if worth_showing_out: if worth_showing_err: - result += 'stdout:\n' - result += trimmed_out + '\n\n' + result += "stdout:\n" + result += trimmed_out + "\n\n" if worth_showing_err: result += "stderr:\n" + trimmed_err @@ -71,9 +77,10 @@ def __str__(self): @staticmethod def __get_args(): - arguments = '' + arguments = "" from hstest.stage_test import StageTest + test_run = StageTest.curr_test_run if test_run is not None: @@ -81,10 +88,10 @@ def __get_args(): programs_with_args = [p for p in tested_programs if len(p.run_args)] for pr in programs_with_args: - arguments += 'Arguments' + arguments += "Arguments" if len(tested_programs) > 1: - arguments += f' for {pr}' - pr_args = [f'"{arg}"' if ' ' in arg else arg for arg in pr.run_args] + arguments += f" for {pr}" + pr_args = [f'"{arg}"' if " " in arg else arg for arg in pr.run_args] arguments += f': {" ".join(pr_args)}\n' arguments = arguments.strip() @@ -93,17 +100,19 @@ def __get_args(): @staticmethod def __trim_lines(full_out): - result = '' + result = "" max_lines_in_output = 250 lines = full_out.splitlines() is_output_too_long = len(lines) > max_lines_in_output if is_output_too_long: - result += f'[last {max_lines_in_output} lines of output are shown, ' \ - f'{len(lines) - max_lines_in_output} skipped]\n' + result += ( + f"[last {max_lines_in_output} lines of output are shown, " + f"{len(lines) - max_lines_in_output} skipped]\n" + ) last_lines = lines[-max_lines_in_output:] - result += '\n'.join(last_lines) + result += "\n".join(last_lines) else: result += full_out @@ -120,20 +129,20 @@ def get_outcome(ex: BaseException, curr_test: int): if isinstance(ex, WrongAnswer): return WrongAnswerOutcome(curr_test, ex) - elif isinstance(ex, ExceptionWithFeedback): + if isinstance(ex, ExceptionWithFeedback): return ExceptionOutcome(curr_test, ex) - elif isinstance(ex, CompilationError): + if isinstance(ex, CompilationError): return CompilationErrorOutcome(ex) - elif isinstance(ex, ( - ErrorWithFeedback, - FileDeletionError, - TimeLimitException, - InfiniteLoopException, - KeyboardInterrupt - )): + if isinstance( + ex, + ErrorWithFeedback + | FileDeletionError + | TimeLimitException + | InfiniteLoopException + | KeyboardInterrupt, + ): return ErrorOutcome(curr_test, ex) - else: - return UnexpectedErrorOutcome(curr_test, ex) + return UnexpectedErrorOutcome(curr_test, ex) diff --git a/hstest/outcomes/unexpected_error_outcome.py b/hstest/outcomes/unexpected_error_outcome.py index 01103fb5..6bf84364 100644 --- a/hstest/outcomes/unexpected_error_outcome.py +++ b/hstest/outcomes/unexpected_error_outcome.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.common.reflection_utils import get_stacktrace from hstest.exception.failure_handler import get_report from hstest.exception.outcomes import UnexpectedError @@ -5,14 +7,13 @@ class UnexpectedErrorOutcome(Outcome): - def __init__(self, test_num: int, cause: BaseException): + def __init__(self, test_num: int, cause: BaseException) -> None: super().__init__() self.test_number = test_num - self.error_text = 'We have recorded this bug ' \ - 'and will fix it soon.\n\n' + get_report() + self.error_text = "We have recorded this bug " "and will fix it soon.\n\n" + get_report() self.stack_trace = get_stacktrace(cause, hide_internals=False) if isinstance(cause, UnexpectedError) and cause.exception is not None: - self.stack_trace += '\n' + get_stacktrace(cause.exception, hide_internals=False) + self.stack_trace += "\n" + get_stacktrace(cause.exception, hide_internals=False) def get_type(self) -> str: - return 'Unexpected error' + return "Unexpected error" diff --git a/hstest/outcomes/wrong_answer_outcome.py b/hstest/outcomes/wrong_answer_outcome.py index 427a49e1..01df0163 100644 --- a/hstest/outcomes/wrong_answer_outcome.py +++ b/hstest/outcomes/wrong_answer_outcome.py @@ -1,10 +1,16 @@ -from hstest.exception.outcomes import WrongAnswer +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.outcomes.outcome import Outcome +if TYPE_CHECKING: + from hstest.exception.outcomes import WrongAnswer + class WrongAnswerOutcome(Outcome): - def __init__(self, test_num: int, ex: WrongAnswer): - super().__init__(test_num, ex.feedback, '') + def __init__(self, test_num: int, ex: WrongAnswer) -> None: + super().__init__(test_num, ex.feedback, "") def get_type(self) -> str: - return 'Wrong answer' + return "Wrong answer" diff --git a/hstest/stage/__init__.py b/hstest/stage/__init__.py index f76d54f1..6cc14d38 100644 --- a/hstest/stage/__init__.py +++ b/hstest/stage/__init__.py @@ -1,9 +1,11 @@ +from __future__ import annotations + __all__ = [ - 'StageTest', - 'DjangoTest', - 'FlaskTest', - 'PlottingTest', - 'SQLTest', + "DjangoTest", + "FlaskTest", + "PlottingTest", + "SQLTest", + "StageTest", ] from hstest.stage.django_test import DjangoTest diff --git a/hstest/stage/django_test.py b/hstest/stage/django_test.py index 04a5a044..82f460a0 100644 --- a/hstest/stage/django_test.py +++ b/hstest/stage/django_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from urllib.request import urlopen from hstest.common.utils import clean_text @@ -15,24 +17,22 @@ class DjangoTest(StageTest): test_database: str = attach.test_database use_database: bool = attach.use_database - def __init__(self, args='', *, source: str = ''): + def __init__(self, args="", *, source: str = "") -> None: super().__init__(args, source=source) self.attach.use_database = self.use_database loop_detector.working = False Settings.do_reset_output = False def read_page(self, link: str) -> str: - """ - Deprecated, use get(...) instead - """ + """Deprecated, use get(...) instead.""" return clean_text(urlopen(link).read().decode()) - def get_url(self, link: str = ''): - if link.startswith('/'): + def get_url(self, link: str = "") -> str: + if link.startswith("/"): link = link[1:] - return f'http://localhost:{self.attach.port}/{link}' + return f"http://localhost:{self.attach.port}/{link}" def get(self, link: str) -> str: - if not link.startswith('http://'): + if not link.startswith("http://"): link = self.get_url(link) return clean_text(urlopen(link).read().decode()) diff --git a/hstest/stage/flask_test.py b/hstest/stage/flask_test.py index f0efdafe..3cc4b568 100644 --- a/hstest/stage/flask_test.py +++ b/hstest/stage/flask_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from urllib.request import urlopen from hstest.common.utils import clean_text @@ -13,7 +15,7 @@ class FlaskTest(StageTest): runner = FlaskApplicationRunner() attach: FlaskSettings = FlaskSettings() - def __init__(self, args='', *, source: str = ''): + def __init__(self, args="", *, source: str = "") -> None: super().__init__(args, source=source) loop_detector.working = False Settings.do_reset_output = False @@ -33,28 +35,31 @@ def __init__(self, args='', *, source: str = ''): else: self.attach.sources += [item] - def get_url(self, link: str = '', *, source: str = None): - if link.startswith('/'): + def get_url(self, link: str = "", *, source: str | None = None): + if link.startswith("/"): link = link[1:] def create_url(port: int) -> str: - return f'http://localhost:{port}/{link}' + return f"http://localhost:{port}/{link}" if len(self.attach.sources) == 1: return create_url(self.attach.sources[0][1]) - elif len(self.attach.sources) == 0: - raise UnexpectedError('Cannot find sources') + if len(self.attach.sources) == 0: + msg = "Cannot find sources" + raise UnexpectedError(msg) sources_fits = [i for i in self.attach.sources if i[0] == source] if len(sources_fits) == 0: - raise UnexpectedError(f'Bad source: {source}') - elif len(sources_fits) > 1: - raise UnexpectedError(f'Multiple sources ({len(sources_fits)}) found: {source}') + msg = f"Bad source: {source}" + raise UnexpectedError(msg) + if len(sources_fits) > 1: + msg = f"Multiple sources ({len(sources_fits)}) found: {source}" + raise UnexpectedError(msg) return create_url(sources_fits[0][1]) - def get(self, link: str, *, source: str = None) -> str: - if not link.startswith('http://'): + def get(self, link: str, *, source: str | None = None) -> str: + if not link.startswith("http://"): link = self.get_url(link, source=source) return clean_text(urlopen(link).read().decode()) diff --git a/hstest/stage/plotting_test.py b/hstest/stage/plotting_test.py index 2be941e4..fcc1e2f2 100644 --- a/hstest/stage/plotting_test.py +++ b/hstest/stage/plotting_test.py @@ -1,22 +1,25 @@ -from typing import List +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.stage.stage_test import StageTest -from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.runner.plot_testing_runner import PlottingTestingRunner +if TYPE_CHECKING: + from hstest.testing.plotting.drawing.drawing import Drawing -class PlottingTest(StageTest): - def __init__(self, args='', *, source: str = ''): +class PlottingTest(StageTest): + def __init__(self, args="", *, source: str = "") -> None: super().__init__(args, source=source) - self._all_drawings: List[Drawing] = [] - self._new_drawings: List[Drawing] = [] + self._all_drawings: list[Drawing] = [] + self._new_drawings: list[Drawing] = [] self.runner = PlottingTestingRunner(self._all_drawings, self._new_drawings) - def all_figures(self) -> List[Drawing]: + def all_figures(self) -> list[Drawing]: return self._all_drawings - def new_figures(self) -> List[Drawing]: + def new_figures(self) -> list[Drawing]: result = self._new_drawings[:] self._new_drawings.clear() return result diff --git a/hstest/stage/sql_test.py b/hstest/stage/sql_test.py index fa328fd9..615f486e 100644 --- a/hstest/stage/sql_test.py +++ b/hstest/stage/sql_test.py @@ -1,4 +1,4 @@ -from typing import Dict +from __future__ import annotations from hstest.exception.outcomes import WrongAnswer from hstest.stage.stage_test import StageTest @@ -6,10 +6,10 @@ class SQLTest(StageTest): - queries: Dict[str, str] = dict() + queries: dict[str, str] = {} db: any = None - def __init__(self, source: str = ''): + def __init__(self, source: str = "") -> None: super().__init__(source) self.runner = SQLRunner(self) @@ -22,7 +22,8 @@ def execute(self, query_name: str): try: return cursor.execute(self.queries[query_name]) except Exception as ex: - raise WrongAnswer(f"Error while running '{query_name}': \n\n{ex}") + msg = f"Error while running '{query_name}': \n\n{ex}" + raise WrongAnswer(msg) def execute_and_fetch_all(self, query_name: str): return self.execute(query_name).fetchall() diff --git a/hstest/stage/stage_test.py b/hstest/stage/stage_test.py index b2f1b7be..aca1c228 100644 --- a/hstest/stage/stage_test.py +++ b/hstest/stage/stage_test.py @@ -1,6 +1,9 @@ +from __future__ import annotations + +import contextlib import os import unittest -from typing import Any, Dict, List, Optional, Tuple, Type +from typing import Any, TYPE_CHECKING from hstest.common.file_utils import walk_user_files from hstest.common.reflection_utils import is_tests, setup_cwd @@ -12,8 +15,6 @@ from hstest.exception.failure_handler import get_exception_text, get_report from hstest.exception.outcomes import OutcomeError, UnexpectedError, WrongAnswer from hstest.outcomes.outcome import Outcome -from hstest.test_case.check_result import CheckResult -from hstest.test_case.test_case import TestCase from hstest.testing.execution.main_module_executor import MainModuleExecutor from hstest.testing.execution.process.cpp_executor import CppExecutor from hstest.testing.execution.process.go_executor import GoExecutor @@ -22,24 +23,34 @@ from hstest.testing.execution.process.shell_executor import ShellExecutor from hstest.testing.execution_options import force_process_testing from hstest.testing.runner.async_dynamic_testing_runner import AsyncDynamicTestingRunner -from hstest.testing.runner.test_runner import TestRunner from hstest.testing.test_run import TestRun +if TYPE_CHECKING: + from hstest.test_case.check_result import CheckResult + from hstest.test_case.test_case import TestCase + from hstest.testing.runner.test_runner import TestRunner + class DirMeta(type): - def __dir__(self): + def __dir__(cls): from hstest.testing.unittest.expected_fail_test import ExpectedFailTest from hstest.testing.unittest.unexepected_error_test import UnexpectedErrorTest from hstest.testing.unittest.user_error_test import UserErrorTest - if (not issubclass(self, StageTest) or self == StageTest or - self in {ExpectedFailTest, UserErrorTest, UnexpectedErrorTest}): + + if ( + not issubclass(cls, StageTest) + or cls == StageTest + or cls in {ExpectedFailTest, UserErrorTest, UnexpectedErrorTest} + ): return [] - init_dir = dir(super(DirMeta, self)) + list(self.__dict__.keys()) - filtered_dir = list(filter(lambda x: not str(x).startswith('test'), init_dir)) - filtered_dir.append('test_run_unittest') - if (not self.dynamic_methods() and - 'generate' not in init_dir and - not issubclass(self, ExpectedFailTest)): + init_dir = dir(super()) + list(cls.__dict__.keys()) + filtered_dir = list(filter(lambda x: not str(x).startswith("test"), init_dir)) + filtered_dir.append("test_run_unittest") + if ( + not cls.dynamic_methods() + and "generate" not in init_dir + and not issubclass(cls, ExpectedFailTest) + ): return [] return set(filtered_dir) @@ -48,11 +59,11 @@ class StageTest(unittest.TestCase, metaclass=DirMeta): runner: TestRunner = None attach: Any = None source: str = None - curr_test_run: Optional[TestRun] = None + curr_test_run: TestRun | None = None curr_test_global: int = 0 - def __init__(self, args='', *, source: str = ''): - super(StageTest, self).__init__('test_run_unittest') + def __init__(self, args="", *, source: str = "") -> None: + super().__init__("test_run_unittest") self.is_tests = False if self.source: @@ -60,46 +71,46 @@ def __init__(self, args='', *, source: str = ''): else: self.source_name: str = source - def test_run_unittest(self): + def test_run_unittest(self) -> None: result, feedback = self.run_tests(is_unittest=True) if result != 0: self.fail(feedback) - def after_all_tests(self): + def after_all_tests(self) -> None: pass def _init_runner(self) -> TestRunner: - for folder, dirs, files in walk_user_files(os.getcwd()): + for _folder, _dirs, files in walk_user_files(os.getcwd()): for f in files: - if f.endswith('.cpp'): + if f.endswith(".cpp"): return AsyncDynamicTestingRunner(CppExecutor) - if f.endswith('.go'): + if f.endswith(".go"): return AsyncDynamicTestingRunner(GoExecutor) - if f.endswith('.js'): + if f.endswith(".js"): return AsyncDynamicTestingRunner(JavascriptExecutor) - if f.endswith('.sh'): + if f.endswith(".sh"): return AsyncDynamicTestingRunner(ShellExecutor) - if f.endswith('.py'): + if f.endswith(".py"): if force_process_testing: return AsyncDynamicTestingRunner(PythonExecutor) - else: - return AsyncDynamicTestingRunner(MainModuleExecutor) + return AsyncDynamicTestingRunner(MainModuleExecutor) return AsyncDynamicTestingRunner(MainModuleExecutor) - def _init_tests(self) -> List[TestRun]: + def _init_tests(self) -> list[TestRun]: if self.runner is None: self.runner = self._init_runner() - test_runs: List[TestRun] = [] - test_cases: List[TestCase] = list(self.generate()) + test_runs: list[TestRun] = [] + test_cases: list[TestCase] = list(self.generate()) test_cases += search_dynamic_tests(self) if len(test_cases) == 0: - raise UnexpectedError("No tests found") + msg = "No tests found" + raise UnexpectedError(msg) curr_test: int = 0 test_count = len(test_cases) @@ -110,21 +121,20 @@ def _init_tests(self) -> List[TestRun]: if test_case.attach is None: test_case.attach = self.attach curr_test += 1 - test_runs += [ - TestRun(curr_test, test_count, test_case, self.runner) - ] + test_runs += [TestRun(curr_test, test_count, test_case, self.runner)] return test_runs - def __print_test_num(self, num: int): - total_tests = '' if num == self.curr_test_global else f' ({self.curr_test_global})' + def __print_test_num(self, num: int) -> None: + total_tests = "" if num == self.curr_test_global else f" ({self.curr_test_global})" OutputHandler.get_real_out().write( - RED_BOLD + f'\nStart test {num}{total_tests}' + RESET + '\n' + RED_BOLD + f"\nStart test {num}{total_tests}" + RESET + "\n" ) - def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str]: + def run_tests(self, *, debug=False, is_unittest: bool = False) -> tuple[int, str]: curr_test: int = 0 need_tear_down: bool = False + ex: OutcomeError = None try: if is_tests(self): self.is_tests = True @@ -133,9 +143,10 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str if self.is_tests or debug: import hstest.common.utils as hs - hs.failed_msg_start = '' - hs.failed_msg_continue = '' - hs.success_msg = '' + + hs.failed_msg_start = "" + hs.failed_msg_continue = "" + hs.success_msg = "" SystemHandler.set_up() test_runs = self._init_tests() @@ -153,7 +164,7 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str result: CheckResult = test_run.test() if not result.is_correct: - full_feedback = result.feedback + '\n\n' + test_run.test_case.feedback + full_feedback = result.feedback + "\n\n" + test_run.test_case.feedback raise WrongAnswer(full_feedback.strip()) if test_run.is_last_test(): @@ -163,7 +174,8 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str SystemHandler.tear_down() return passed(is_unittest) - except BaseException as ex: + except BaseException as caught_ex: + ex = caught_ex if need_tear_down: try: StageTest.curr_test_run.tear_down() @@ -171,7 +183,7 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str if isinstance(new_ex, OutcomeError): ex = new_ex - build = 'hs-test-python' + build = "hs-test-python" try: report = build + "\n\n" + get_report() @@ -194,23 +206,21 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str text = get_exception_text(e) except Exception: try: - text = f'{type(e)}: {str(e)}' + text = f"{type(e)}: {e!s}" except Exception: - text = 'Broken exception' + text = "Broken exception" if len(text): traceback += text + "\n\n" - fail_text = 'Unexpected error\n\n' + report + "\n\n" + traceback + fail_text = "Unexpected error\n\n" + report + "\n\n" + traceback except BaseException: # no code execution here allowed so not to throw an exception - fail_text = 'Unexpected error\n\nCannot check the submission\n\n' + report + fail_text = "Unexpected error\n\nCannot check the submission\n\n" + report - try: + with contextlib.suppress(BaseException): SystemHandler.tear_down() - except BaseException: - pass return failed(fail_text, is_unittest) @@ -221,18 +231,19 @@ def run_tests(self, *, debug=False, is_unittest: bool = False) -> Tuple[int, str StageTest.source = None self.after_all_tests() - _dynamic_methods: Dict[Type['StageTest'], List[DynamicTestElement]] = {} + _dynamic_methods: dict[type[StageTest], list[DynamicTestElement]] = {} @classmethod - def dynamic_methods(cls) -> List[DynamicTestElement]: + def dynamic_methods(cls) -> list[DynamicTestElement]: if cls in StageTest._dynamic_methods: return StageTest._dynamic_methods[cls] empty = [] StageTest._dynamic_methods[cls] = empty return empty - def generate(self) -> List[TestCase]: + def generate(self) -> list[TestCase]: return [] def check(self, reply: str, attach: Any) -> CheckResult: - raise UnexpectedError('Can\'t check result: override "check" method') + msg = 'Can\'t check result: override "check" method' + raise UnexpectedError(msg) diff --git a/hstest/stage_test.py b/hstest/stage_test.py index fd5cdea0..e3bf4869 100644 --- a/hstest/stage_test.py +++ b/hstest/stage_test.py @@ -1,3 +1,5 @@ # deprecated, but old tests use "from hstest.stage_test import StageTest" # new way to import is "from hstest import StageTest" -from hstest.stage.stage_test import * # noqa: F401, F403 +from __future__ import annotations + +from hstest.stage.stage_test import * # noqa: F403 diff --git a/hstest/test_case/__init__.py b/hstest/test_case/__init__.py index ae866d9d..63b0487e 100644 --- a/hstest/test_case/__init__.py +++ b/hstest/test_case/__init__.py @@ -1,9 +1,11 @@ +from __future__ import annotations + __all__ = [ - 'TestCase', - 'SimpleTestCase', - 'CheckResult', - 'correct', - 'wrong', + "CheckResult", + "SimpleTestCase", + "TestCase", + "correct", + "wrong", ] from hstest.test_case.check_result import CheckResult, correct, wrong diff --git a/hstest/test_case/attach/django_settings.py b/hstest/test_case/attach/django_settings.py index 1e29f000..ad2bf579 100644 --- a/hstest/test_case/attach/django_settings.py +++ b/hstest/test_case/attach/django_settings.py @@ -1,8 +1,8 @@ -from typing import List +from __future__ import annotations class DjangoSettings: port: int = None use_database: bool = False - test_database: str = 'db.test.sqlite3' - tryout_ports: List[int] = [i for i in range(8000, 8101)] + test_database: str = "db.test.sqlite3" + tryout_ports: list[int] = list(range(8000, 8101)) diff --git a/hstest/test_case/attach/flask_settings.py b/hstest/test_case/attach/flask_settings.py index d13cad20..97aa4324 100644 --- a/hstest/test_case/attach/flask_settings.py +++ b/hstest/test_case/attach/flask_settings.py @@ -1,6 +1,6 @@ -from typing import List, Tuple +from __future__ import annotations class FlaskSettings: - sources: List[Tuple[str, int]] = [] - tryout_ports: List[int] = [i for i in range(8000, 8101)] + sources: list[tuple[str, int]] = [] + tryout_ports: list[int] = list(range(8000, 8101)) diff --git a/hstest/test_case/check_result.py b/hstest/test_case/check_result.py index f2e5e1b9..cb291d49 100644 --- a/hstest/test_case/check_result.py +++ b/hstest/test_case/check_result.py @@ -1,11 +1,10 @@ -from typing import Optional +from __future__ import annotations from hstest.exception.outcomes import TestPassed, WrongAnswer class CheckResult: - - def __init__(self, result: bool, feedback: str): + def __init__(self, result: bool, feedback: str) -> None: self._result: bool = result self._feedback: str = feedback @@ -18,21 +17,20 @@ def feedback(self) -> str: return self._feedback @staticmethod - def correct() -> 'CheckResult': - return CheckResult(True, '') + def correct() -> CheckResult: + return CheckResult(True, "") @staticmethod - def wrong(feedback: str) -> 'CheckResult': + def wrong(feedback: str) -> CheckResult: return CheckResult(False, feedback) @staticmethod - def from_error(error: BaseException) -> Optional['CheckResult']: + def from_error(error: BaseException) -> CheckResult | None: if isinstance(error, TestPassed): return correct() - elif isinstance(error, WrongAnswer): + if isinstance(error, WrongAnswer): return wrong(error.feedback) - else: - return None + return None def correct() -> CheckResult: diff --git a/hstest/test_case/test_case.py b/hstest/test_case/test_case.py index 749cc9a9..8a10e852 100644 --- a/hstest/test_case/test_case.py +++ b/hstest/test_case/test_case.py @@ -1,4 +1,6 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from __future__ import annotations + +from typing import Any, TYPE_CHECKING, Union from hstest.dynamic.input.dynamic_input_func import DynamicInputFunction from hstest.dynamic.input.dynamic_testing import to_dynamic_testing @@ -6,49 +8,53 @@ from hstest.test_case.check_result import CheckResult if TYPE_CHECKING: + from collections.abc import Callable + from hstest.dynamic.input.dynamic_input_func import InputFunction from hstest.dynamic.input.dynamic_testing import DynamicTesting SimpleStepikTest = str - AdvancedStepikTest = Tuple[str, Any] + AdvancedStepikTest = tuple[str, Any] StepikTest = Union[SimpleStepikTest, AdvancedStepikTest] CheckFunction = Callable[[str, Any], CheckResult] PredefinedInput = str RuntimeEvaluatedInput = Union[ - PredefinedInput, InputFunction, Tuple[int, InputFunction], DynamicInputFunction] - DynamicInput = Union[PredefinedInput, List[RuntimeEvaluatedInput]] + PredefinedInput, InputFunction, tuple[int, InputFunction], DynamicInputFunction + ] + DynamicInput = Union[PredefinedInput, list[RuntimeEvaluatedInput]] DEFAULT_TIME_LIMIT: int = 15000 class TestCase: def __init__( - self, *, - stdin: 'DynamicInput' = '', - args: List[str] = None, + self, + *, + stdin: DynamicInput = "", + args: list[str] | None = None, attach: Any = None, - feedback: str = '', - files: Dict[str, str] = None, + feedback: str = "", + files: dict[str, str] | None = None, time_limit: int = DEFAULT_TIME_LIMIT, - check_function: 'CheckFunction' = None, - feedback_on_exception: Dict[Type[Exception], str] = None, + check_function: CheckFunction = None, + feedback_on_exception: dict[type[Exception], str] | None = None, copy_to_attach: bool = False, - dynamic_testing: 'DynamicTesting' = None - ): - + dynamic_testing: DynamicTesting = None, + ) -> None: self.source_name = None - self.input: Optional[str] = None - self.args: List[str] = [] if args is None else args + self.input: str | None = None + self.args: list[str] = [] if args is None else args self.attach: Any = attach self.feedback = feedback - self.files: Dict[str, str] = {} if files is None else files + self.files: dict[str, str] = {} if files is None else files self.time_limit: int = time_limit self.check_func: CheckFunction = check_function - self.feedback_on_exception: Dict[Type[Exception], str] = ( - {} if feedback_on_exception is None else feedback_on_exception) + self.feedback_on_exception: dict[type[Exception], str] = ( + {} if feedback_on_exception is None else feedback_on_exception + ) self.input_funcs = [] self._dynamic_testing: DynamicTesting = dynamic_testing @@ -58,13 +64,14 @@ def __init__( if copy_to_attach: if attach is not None: - raise UnexpectedError( - 'Attach is not None ' - 'but copying from stdin is specified') + msg = "Attach is not None " "but copying from stdin is specified" + raise UnexpectedError(msg) if type(stdin) != str: - raise UnexpectedError( - 'To copy stdin to attach stdin should be of type str ' - f'but found type {type(stdin)}') + msg = ( + "To copy stdin to attach stdin should be of type str " + f"but found type {type(stdin)}" + ) + raise UnexpectedError(msg) self.attach = stdin if type(stdin) == str: @@ -72,9 +79,8 @@ def __init__( self.input_funcs = [DynamicInputFunction(1, lambda x: stdin)] else: if type(stdin) != list: - raise UnexpectedError( - 'Stdin should be either of type str or list ' - f'but found type {type(stdin)}') + msg = "Stdin should be either of type str or list " f"but found type {type(stdin)}" + raise UnexpectedError(msg) for elem in stdin: # type: RuntimeEvaluatedInput if type(elem) == DynamicInputFunction: self.input_funcs += [elem] @@ -82,39 +88,45 @@ def __init__( elif type(elem) == str: self.input_funcs += [DynamicInputFunction(1, lambda x, inp=elem: inp)] - elif str(type(elem)) in ["", ""]: + elif str(type(elem)) in {"", ""}: self.input_funcs += [DynamicInputFunction(1, elem)] - elif type(elem) in (tuple, list): + elif type(elem) in {tuple, list}: if len(elem) == 2: trigger_count: int = elem[0] input_function: InputFunction = elem[1] if type(trigger_count) != int: - raise UnexpectedError( - f'Stdin element\'s 1st element should be of type int, ' - f'found {type(trigger_count)}') + msg = ( + f"Stdin element's 1st element should be of type int, " + f"found {type(trigger_count)}" + ) + raise UnexpectedError(msg) if str(type(input_function)) not in { - "", "" + "", + "", }: - raise UnexpectedError( - f'Stdin element\'s 2nd element should be of type function, ' - f'found {type(input_function)}' + msg = ( + f"Stdin element's 2nd element should be of type function, " + f"found {type(input_function)}" ) + raise UnexpectedError(msg) self.input_funcs += [DynamicInputFunction(trigger_count, input_function)] else: - raise UnexpectedError( - f'Stdin element should have size 2, found {len(elem)}') + msg = f"Stdin element should have size 2, found {len(elem)}" + raise UnexpectedError(msg) else: - raise UnexpectedError( - f'Stdin element should have type DynamicInputFunction or ' - f'tuple of size 1 or 2, found element of type {type(elem)}') + msg = ( + f"Stdin element should have type DynamicInputFunction or " + f"tuple of size 1 or 2, found element of type {type(elem)}" + ) + raise UnexpectedError(msg) @property - def dynamic_testing(self) -> 'DynamicTesting': + def dynamic_testing(self) -> DynamicTesting: if self._dynamic_testing is None: self._dynamic_testing = to_dynamic_testing( self.source_name, self.args, self.input_funcs @@ -122,10 +134,10 @@ def dynamic_testing(self) -> 'DynamicTesting': return self._dynamic_testing @staticmethod - def from_stepik(stepik_tests: List['StepikTest']) -> List['TestCase']: + def from_stepik(stepik_tests: list[StepikTest]) -> list[TestCase]: hs_tests = [] for test in stepik_tests: - if type(test) in (list, tuple): + if type(test) in {list, tuple}: hs_test = TestCase(stdin=test[0], attach=test[1]) elif type(test) is str: hs_test = TestCase(stdin=test) @@ -136,10 +148,10 @@ def from_stepik(stepik_tests: List['StepikTest']) -> List['TestCase']: class SimpleTestCase(TestCase): - def __init__(self, *, stdin: str, stdout: str, feedback: str, **kwargs): + def __init__(self, *, stdin: str, stdout: str, feedback: str, **kwargs) -> None: super().__init__(stdin=stdin, attach=stdout, feedback=feedback, **kwargs) self.check_func = self._custom_check def _custom_check(self, reply: str, expected: str): is_correct = reply.strip() == expected.strip() - return CheckResult(is_correct, '') + return CheckResult(is_correct, "") diff --git a/hstest/testing/execution/filtering/file_filter.py b/hstest/testing/execution/filtering/file_filter.py index c44e5a54..e002b364 100644 --- a/hstest/testing/execution/filtering/file_filter.py +++ b/hstest/testing/execution/filtering/file_filter.py @@ -1,39 +1,47 @@ +from __future__ import annotations + import re -from typing import Callable, Dict, Set +from collections.abc import Callable Folder = str File = str Source = str Module = str -Sources = Dict[File, Source] +Sources = dict[File, Source] Filter = Callable[[Folder, File, Source], bool] -no_filter: Filter = lambda *a, **kw: True + +def no_filter(*a, **kw) -> bool: + return True class FileFilter: - def __init__(self, - init_files: Callable[[Folder, Sources], None] = no_filter, - folder: Callable[[Folder], bool] = no_filter, - file: Callable[[File], bool] = no_filter, - source: Callable[[Source], bool] = no_filter, - generic: Filter = no_filter): + def __init__( + self, + init_files: Callable[[Folder, Sources], None] = no_filter, + folder: Callable[[Folder], bool] = no_filter, + file: Callable[[File], bool] = no_filter, + source: Callable[[Source], bool] = no_filter, + generic: Filter = no_filter, + ) -> None: self.init_files = init_files self.folder = folder self.file = file self.source = source self.generic = generic - self.filtered: Set[File] = set() + self.filtered: set[File] = set() @staticmethod def regex_filter(regex: str): - return lambda s: re.compile(regex, re.M).search(s) is not None + return lambda s: re.compile(regex, re.MULTILINE).search(s) is not None - def init_filter(self, folder: Folder, sources: Sources): + def init_filter(self, folder: Folder, sources: Sources) -> None: self.init_files(folder, sources) def filter(self, folder: Folder, file: File, source: Source) -> bool: - return self.folder(folder) \ - and self.file(file) \ - and self.source(source) \ + return ( + self.folder(folder) + and self.file(file) + and self.source(source) and self.generic(folder, file, source) + ) diff --git a/hstest/testing/execution/filtering/main_filter.py b/hstest/testing/execution/filtering/main_filter.py index 53ff76bb..2a9dbcf3 100644 --- a/hstest/testing/execution/filtering/main_filter.py +++ b/hstest/testing/execution/filtering/main_filter.py @@ -1,17 +1,30 @@ -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.testing.execution.filtering.file_filter import ( - File, FileFilter, Filter, Folder, no_filter, Source, Sources + File, + FileFilter, + Filter, + Folder, + no_filter, + Source, + Sources, ) +if TYPE_CHECKING: + from collections.abc import Callable + class MainFilter(FileFilter): - def __init__(self, - program_should_contain: str, - init_files: Callable[[Folder, Sources], None] = no_filter, - folder: Callable[[Folder], bool] = no_filter, - file: Callable[[File], bool] = no_filter, - source: Callable[[Source], bool] = no_filter, - generic: Filter = no_filter): + def __init__( + self, + program_should_contain: str, + init_files: Callable[[Folder, Sources], None] = no_filter, + folder: Callable[[Folder], bool] = no_filter, + file: Callable[[File], bool] = no_filter, + source: Callable[[Source], bool] = no_filter, + generic: Filter = no_filter, + ) -> None: super().__init__(init_files, folder, file, source, generic) self.program_should_contain = program_should_contain diff --git a/hstest/testing/execution/main_module_executor.py b/hstest/testing/execution/main_module_executor.py index e319317e..363c668f 100644 --- a/hstest/testing/execution/main_module_executor.py +++ b/hstest/testing/execution/main_module_executor.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import os import runpy import sys -from concurrent.futures import Future -from typing import Optional +from typing import TYPE_CHECKING from hstest.common.process_utils import DaemonThreadPoolExecutor from hstest.dynamic.output.output_handler import OutputHandler @@ -13,30 +14,30 @@ from hstest.testing.execution.program_executor import ProgramExecutor, ProgramState from hstest.testing.execution.searcher.python_searcher import PythonSearcher +if TYPE_CHECKING: + from concurrent.futures import Future + class MainModuleExecutor(ProgramExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__() - OutputHandler.print(f'MainModuleExecutor instantiating, source = {source_name}') + OutputHandler.print(f"MainModuleExecutor instantiating, source = {source_name}") self.runnable = PythonSearcher().find(source_name) - self.__executor: Optional[DaemonThreadPoolExecutor] = None - self.__task: Optional[Future] = None + self.__executor: DaemonThreadPoolExecutor | None = None + self.__task: Future | None = None self.__group = None self.working_directory_before = os.path.abspath(os.getcwd()) - def _invoke_method(self, *args: str): + def _invoke_method(self, *args: str) -> None: from hstest.stage_test import StageTest try: self._machine.set_state(ProgramState.RUNNING) - sys.argv = [self.runnable.file] + list(args) + sys.argv = [self.runnable.file, *list(args)] sys.path.insert(0, self.runnable.folder) - runpy.run_module( - self.runnable.module, - run_name="__main__" - ) + runpy.run_module(self.runnable.module, run_name="__main__") self._machine.set_state(ProgramState.FINISHED) @@ -48,30 +49,30 @@ def _invoke_method(self, *args: str): self._machine.set_state(ProgramState.FINISHED) return - StageTest.curr_test_run.set_error_in_test(ExceptionWithFeedback('', ex)) + StageTest.curr_test_run.set_error_in_test(ExceptionWithFeedback("", ex)) self._machine.set_state(ProgramState.EXCEPTION_THROWN) - def _launch(self, *args: str): - self.modules_before = [k for k in sys.modules.keys()] + def _launch(self, *args: str) -> None: + self.modules_before = list(sys.modules.keys()) from hstest.stage_test import StageTest + test_num = StageTest.curr_test_run.test_num self.__group = ThreadGroup() SystemHandler.install_handler( - self, - lambda: ThreadGroup.curr_group() == self.__group, - lambda: self.request_input() + self, lambda: ThreadGroup.curr_group() == self.__group, self.request_input ) self.__executor = DaemonThreadPoolExecutor( - name=f"MainModuleExecutor test #{test_num}", group=self.__group) + name=f"MainModuleExecutor test #{test_num}", group=self.__group + ) self.__task = self.__executor.submit(lambda: self._invoke_method(*args)) - def _terminate(self): + def _terminate(self) -> None: self.__executor.shutdown(wait=False) self.__task.cancel() with self._machine.cv: diff --git a/hstest/testing/execution/process/cpp_executor.py b/hstest/testing/execution/process/cpp_executor.py index 6849fb58..9e4a3ac7 100644 --- a/hstest/testing/execution/process/cpp_executor.py +++ b/hstest/testing/execution/process/cpp_executor.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from hstest.common.os_utils import is_windows @@ -6,27 +8,36 @@ class CppExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(CppSearcher().find(source_name)) self.without_extension = os.path.splitext(self.runnable.file)[0] if is_windows(): self.executable = self.without_extension - self.file_name = self.executable + '.exe' + self.file_name = self.executable + ".exe" else: - self.executable = f'./{self.without_extension}' + self.executable = f"./{self.without_extension}" self.file_name = self.without_extension def _compilation_command(self): - return ['g++', '-std=c++20', '-pipe', '-O2', '-static', '-o', self.file_name, self.runnable.file] + return [ + "g++", + "-std=c++20", + "-pipe", + "-O2", + "-static", + "-o", + self.file_name, + self.runnable.file, + ] def _filter_compilation_error(self, error: str) -> str: return error def _execution_command(self, *args: str): - return [self.executable] + list(args) + return [self.executable, *list(args)] - def _cleanup(self): + def _cleanup(self) -> None: if os.path.exists(self.file_name): os.remove(self.file_name) diff --git a/hstest/testing/execution/process/go_executor.py b/hstest/testing/execution/process/go_executor.py index 057ca243..e5d202dc 100644 --- a/hstest/testing/execution/process/go_executor.py +++ b/hstest/testing/execution/process/go_executor.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from hstest.common.os_utils import is_windows @@ -6,28 +8,28 @@ class GoExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(GoSearcher().find(source_name)) - self.without_go = self.runnable.file[:-len(GoSearcher().extension)] + self.without_go = self.runnable.file[: -len(GoSearcher().extension)] if is_windows(): self.executable = self.without_go - self.file_name = self.executable + '.exe' + self.file_name = self.executable + ".exe" else: - self.executable = f'./{self.without_go}' + self.executable = f"./{self.without_go}" self.file_name = self.without_go def _compilation_command(self): - return ['go', 'build', self.runnable.file] + return ["go", "build", self.runnable.file] def _filter_compilation_error(self, error: str) -> str: - error_lines = [line for line in error.splitlines() if not line.startswith('#')] - return '\n'.join(error_lines) + error_lines = [line for line in error.splitlines() if not line.startswith("#")] + return "\n".join(error_lines) def _execution_command(self, *args: str): - return [self.executable] + list(args) + return [self.executable, *list(args)] - def _cleanup(self): + def _cleanup(self) -> None: if os.path.exists(self.file_name): os.remove(self.file_name) diff --git a/hstest/testing/execution/process/javascript_executor.py b/hstest/testing/execution/process/javascript_executor.py index 8ec4dbaf..c90d3258 100644 --- a/hstest/testing/execution/process/javascript_executor.py +++ b/hstest/testing/execution/process/javascript_executor.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from hstest.testing.execution.process_executor import ProcessExecutor from hstest.testing.execution.searcher.javascript_searcher import JavascriptSearcher class JavascriptExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(JavascriptSearcher().find(source_name)) def _execution_command(self, *args: str): - return ['node', self.runnable.file] + list(args) + return ["node", self.runnable.file, *list(args)] diff --git a/hstest/testing/execution/process/python_executor.py b/hstest/testing/execution/process/python_executor.py index 23d032e9..32d3721d 100644 --- a/hstest/testing/execution/process/python_executor.py +++ b/hstest/testing/execution/process/python_executor.py @@ -1,10 +1,23 @@ +from __future__ import annotations + +import os + +from hstest.common.os_utils import is_windows from hstest.testing.execution.process_executor import ProcessExecutor from hstest.testing.execution.searcher.python_searcher import PythonSearcher class PythonExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(PythonSearcher().find(source_name)) + # Set UTF-8 encoding for Python I/O on Windows + if is_windows(): + os.environ["PYTHONIOENCODING"] = "utf8" def _execution_command(self, *args: str): - return ['python', '-u', self.runnable.file] + list(args) + cmd = ["python"] + if is_windows(): # Works on all Windows versions (32/64 bit) + # Set UTF-8 encoding for stdin/stdout on Windows + cmd.extend(["-X", "utf8"]) + cmd.extend(["-u", self.runnable.file, *list(args)]) + return cmd diff --git a/hstest/testing/execution/process/shell_executor.py b/hstest/testing/execution/process/shell_executor.py index 71387ea5..2868866b 100644 --- a/hstest/testing/execution/process/shell_executor.py +++ b/hstest/testing/execution/process/shell_executor.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from hstest.testing.execution.process_executor import ProcessExecutor from hstest.testing.execution.searcher.shell_searcher import ShellSearcher class ShellExecutor(ProcessExecutor): - def __init__(self, source_name: str = None): + def __init__(self, source_name: str | None = None) -> None: super().__init__(ShellSearcher().find(source_name)) def _execution_command(self, *args: str): - return ['bash', self.runnable.file] + list(args) + return ["bash", self.runnable.file, *list(args)] diff --git a/hstest/testing/execution/process_executor.py b/hstest/testing/execution/process_executor.py index 7e0bf46e..62391259 100644 --- a/hstest/testing/execution/process_executor.py +++ b/hstest/testing/execution/process_executor.py @@ -1,7 +1,10 @@ +from __future__ import annotations + +import contextlib import os from threading import Thread from time import sleep -from typing import List, Optional +from typing import TYPE_CHECKING from hstest.common.utils import try_many_times from hstest.dynamic.input.input_handler import InputHandler @@ -11,32 +14,35 @@ from hstest.dynamic.system_handler import SystemHandler from hstest.exception.outcomes import CompilationError, ExceptionWithFeedback, OutOfInputError from hstest.testing.execution.program_executor import ProgramExecutor, ProgramState -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.process_wrapper import ProcessWrapper +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile + class ProcessExecutor(ProgramExecutor): compiled = False - def __init__(self, runnable: RunnableFile): + def __init__(self, runnable: RunnableFile) -> None: super().__init__() - self.process: Optional[ProcessWrapper] = None + self.process: ProcessWrapper | None = None self.thread = None self.continue_executing = True self.runnable: RunnableFile = runnable - self.__group: Optional[ThreadGroup] = None + self.__group: ThreadGroup | None = None self.working_directory_before = os.path.abspath(os.getcwd()) - def _compilation_command(self, *args: str) -> List[str]: + def _compilation_command(self, *args: str) -> list[str]: return [] def _filter_compilation_error(self, error: str) -> str: return error - def _execution_command(self, *args: str) -> List[str]: - raise NotImplementedError('Method "_execution_command" isn\'t implemented') + def _execution_command(self, *args: str) -> list[str]: + msg = 'Method "_execution_command" isn\'t implemented' + raise NotImplementedError(msg) - def _cleanup(self): + def _cleanup(self) -> None: pass def __compile_program(self) -> bool: @@ -55,13 +61,14 @@ def __compile_program(self) -> bool: error_text = self._filter_compilation_error(process.stderr) from hstest import StageTest + StageTest.curr_test_run.set_error_in_test(CompilationError(error_text)) self._machine.set_state(ProgramState.COMPILATION_ERROR) return False return True - def __handle_process(self, *args: str): + def __handle_process(self, *args: str) -> None: from hstest import StageTest os.chdir(self.runnable.folder) @@ -77,109 +84,102 @@ def __handle_process(self, *args: str): self.process = ProcessWrapper(*command).start() while self.continue_executing: - OutputHandler.print('Handle process - one iteration') + OutputHandler.print("Handle process - one iteration") sleep(0.001) if self.process.is_finished(): - OutputHandler.print('Handle process - finished, breaking') + OutputHandler.print("Handle process - finished, breaking") break is_input_allowed = self.is_input_allowed() is_waiting_input = self.process.is_waiting_input() - OutputHandler.print(f'Handle process - ' - f'input allowed {is_input_allowed}, ' - f'waiting input {is_waiting_input}') + OutputHandler.print( + f"Handle process - " + f"input allowed {is_input_allowed}, " + f"waiting input {is_waiting_input}" + ) if is_input_allowed and is_waiting_input: - OutputHandler.print('Handle process - registering input request') + OutputHandler.print("Handle process - registering input request") self.process.register_input_request() try: - OutputHandler.print('Handle process - try readline') + OutputHandler.print("Handle process - try readline") next_input = InputHandler.mock_in.readline() - OutputHandler.print( - f'Handle process - requested input: {repr(next_input)}' - ) + OutputHandler.print(f"Handle process - requested input: {next_input!r}") self.process.provide_input(next_input) - OutputHandler.print( - f'Handle process - written to stdin: {repr(next_input)}' - ) + OutputHandler.print(f"Handle process - written to stdin: {next_input!r}") except ExitException: - OutputHandler.print('Handle process - EXIT EXCEPTION, stop input') + OutputHandler.print("Handle process - EXIT EXCEPTION, stop input") if self._wait_if_terminated(): if type(StageTest.curr_test_run.error_in_test) == OutOfInputError: StageTest.curr_test_run.set_error_in_test(None) OutputHandler.print( - 'Handle process - Abort stopping input, everything is OK' + "Handle process - Abort stopping input, everything is OK" ) break self.stop_input() except BaseException as ex: - OutputHandler.print(f'Handle process - SOME EXCEPTION {ex}') + OutputHandler.print(f"Handle process - SOME EXCEPTION {ex}") - OutputHandler.print('Handle process - TERMINATE') + OutputHandler.print("Handle process - TERMINATE") self.process.terminate() is_error_happened = self.process.is_error_happened() - OutputHandler.print('Handle process - after termination') - OutputHandler.print(f'Handle process - is error happened {is_error_happened}') + OutputHandler.print("Handle process - after termination") + OutputHandler.print(f"Handle process - is error happened {is_error_happened}") if StageTest.curr_test_run.error_in_test is not None: - OutputHandler.print('Handle process - set state EXCEPTION THROWN (ERROR IN TEST)') + OutputHandler.print("Handle process - set state EXCEPTION THROWN (ERROR IN TEST)") self._machine.set_state(ProgramState.EXCEPTION_THROWN) elif is_error_happened: - OutputHandler.print( - 'Handle process - set state EXCEPTION THROWN (REALLY EXCEPTION)' - ) + OutputHandler.print("Handle process - set state EXCEPTION THROWN (REALLY EXCEPTION)") StageTest.curr_test_run.set_error_in_test( ExceptionWithFeedback(self.process.stderr, None) ) self._machine.set_state(ProgramState.EXCEPTION_THROWN) else: - OutputHandler.print('Handle process - set state FINISHED') + OutputHandler.print("Handle process - set state FINISHED") self._machine.set_state(ProgramState.FINISHED) - OutputHandler.print('Handle process - finishing execution') + OutputHandler.print("Handle process - finishing execution") def _wait_if_terminated(self): return try_many_times(100, 10, lambda: self.process.is_finished(False)) - def _launch(self, *args: str): + def _launch(self, *args: str) -> None: self.__group = ThreadGroup() SystemHandler.install_handler( - self, - lambda: ThreadGroup.curr_group() == self.__group, - lambda: self.request_input() + self, lambda: ThreadGroup.curr_group() == self.__group, self.request_input ) - self.thread = Thread(target=lambda: self.__handle_process(*args), daemon=True, - group=self.__group) + self.thread = Thread( + target=lambda: self.__handle_process(*args), daemon=True, group=self.__group + ) self.thread.start() - def _terminate(self): + def _terminate(self) -> None: self.continue_executing = False self.process.terminate() - OutputHandler.print(f'TERMINATE {self.is_finished()}') + OutputHandler.print(f"TERMINATE {self.is_finished()}") os.chdir(self.working_directory_before) while not self.is_finished(): if self.is_waiting_input(): self._machine.set_state(ProgramState.RUNNING) - OutputHandler.print(f'NOT FINISHED {self._machine.state}') + OutputHandler.print(f"NOT FINISHED {self._machine.state}") sleep(0.001) - def tear_down(self): + def tear_down(self) -> None: working_directory_before = os.path.abspath(os.getcwd()) os.chdir(self.runnable.folder) - try: + with contextlib.suppress(BaseException): self._cleanup() - except BaseException: - pass ProcessExecutor.compiled = False os.chdir(working_directory_before) diff --git a/hstest/testing/execution/program_executor.py b/hstest/testing/execution/program_executor.py index a1b0dc80..2ad71f20 100644 --- a/hstest/testing/execution/program_executor.py +++ b/hstest/testing/execution/program_executor.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from enum import Enum -from typing import Optional +from typing import NoReturn from hstest.dynamic.output.output_handler import OutputHandler from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError @@ -17,8 +19,8 @@ class ProgramState(Enum): class ProgramExecutor: - def __init__(self, source_name: str = None): - self._input: Optional[str] = None + def __init__(self, source_name: str | None = None) -> None: + self._input: str | None = None self.__in_background: bool = False self.__no_more_input: bool = False @@ -33,18 +35,21 @@ def __init__(self, source_name: str = None): m.add_transition(ProgramState.RUNNING, ProgramState.EXCEPTION_THROWN) m.add_transition(ProgramState.RUNNING, ProgramState.FINISHED) - def _launch(self, *args: str): - raise NotImplementedError('Method "_launch" isn\'t implemented') + def _launch(self, *args: str) -> NoReturn: + msg = 'Method "_launch" isn\'t implemented' + raise NotImplementedError(msg) - def _terminate(self): - raise NotImplementedError('Method "_terminate" isn\'t implemented') + def _terminate(self) -> NoReturn: + msg = 'Method "_terminate" isn\'t implemented' + raise NotImplementedError(msg) def get_output(self) -> str: return OutputHandler.get_partial_output(self) def start(self, *args: str) -> str: if not self._machine.in_state(ProgramState.NOT_STARTED): - raise UnexpectedError(f"Cannot start the program {self} twice") + msg = f"Cannot start the program {self} twice" + raise UnexpectedError(msg) self._launch(*args) @@ -52,23 +57,23 @@ def start(self, *args: str) -> str: self._machine.wait_not_state(ProgramState.NOT_STARTED) return "" - self._machine.wait_not_states( - ProgramState.NOT_STARTED, ProgramState.RUNNING) + self._machine.wait_not_states(ProgramState.NOT_STARTED, ProgramState.RUNNING) - OutputHandler.print('Program executor - after waiting in start() method') + OutputHandler.print("Program executor - after waiting in start() method") return self.__get_execution_output() def execute(self, stdin: str) -> str: if self.is_finished(): from hstest.stage_test import StageTest + StageTest.curr_test_run.set_error_in_test( ErrorWithFeedback( - f"The program {self} has unexpectedly terminated.\n" + - "It finished execution too early, should continue running." + f"The program {self} has unexpectedly terminated.\n" + + "It finished execution too early, should continue running." ) ) - raise TestedProgramFinishedEarly() + raise TestedProgramFinishedEarly if stdin is None: self.stop_input() @@ -76,12 +81,13 @@ def execute(self, stdin: str) -> str: if not self.is_waiting_input(): raise UnexpectedError( - f"Program {self} is not waiting for the input " + - f"(state == \"{self._machine.state}\")") + f"Program {self} is not waiting for the input " + + f'(state == "{self._machine.state}")' + ) if self.__no_more_input: - raise UnexpectedError( - f"Can't pass input to the program {self} - input was prohibited.") + msg = f"Can't pass input to the program {self} - input was prohibited." + raise UnexpectedError(msg) self._input = stdin if self.__in_background: @@ -93,29 +99,29 @@ def execute(self, stdin: str) -> str: self._machine.set_and_wait(ProgramState.RUNNING) return self.__get_execution_output() - def stop(self): + def stop(self) -> None: self.__no_more_input = True self._terminate() def __get_execution_output(self) -> str: - OutputHandler.print('Program executor - __get_execution_output()') + OutputHandler.print("Program executor - __get_execution_output()") if self._machine.in_state(ProgramState.EXCEPTION_THROWN): - raise TestedProgramThrewException() - OutputHandler.print('Program executor - __get_execution_output() NO EXCEPTION') + raise TestedProgramThrewException + OutputHandler.print("Program executor - __get_execution_output() NO EXCEPTION") if self.__return_output_after_execution: return self.get_output() return "" - def request_input(self) -> Optional[str]: + def request_input(self) -> str | None: if self.__no_more_input: return None - OutputHandler.print('Program executor - _request_input() invoked, set state WAITING') + OutputHandler.print("Program executor - _request_input() invoked, set state WAITING") self._machine.set_and_wait(ProgramState.WAITING, ProgramState.RUNNING) input_local = self._input self._input = None return input_local - def set_return_output_after_execution(self, value: bool): + def set_return_output_after_execution(self, value: bool) -> None: self.__return_output_after_execution = value def is_finished(self) -> bool: @@ -123,7 +129,7 @@ def is_finished(self) -> bool: exception = self._machine.in_state(ProgramState.EXCEPTION_THROWN) return finished or exception - def stop_input(self): + def stop_input(self) -> None: self.__in_background = True self.__no_more_input = True if self.is_waiting_input(): @@ -135,22 +141,23 @@ def is_input_allowed(self) -> bool: def is_waiting_input(self) -> bool: return self._machine.in_state(ProgramState.WAITING) - def start_in_background(self, *args: str): + def start_in_background(self, *args: str) -> None: self.__in_background = True self.start(*args) - def go_background(self): + def go_background(self) -> None: self.__in_background = True - def stop_background(self): + def stop_background(self) -> None: self.__in_background = False self._machine.wait_state(ProgramState.WAITING) def is_in_background(self): return self.__in_background - def tear_down(self): + def tear_down(self) -> None: pass def __str__(self) -> str: - raise NotImplementedError('Method "__str__" isn\'t implemented') + msg = 'Method "__str__" isn\'t implemented' + raise NotImplementedError(msg) diff --git a/hstest/testing/execution/runnable/python_runnable_file.py b/hstest/testing/execution/runnable/python_runnable_file.py index 5c8987a4..ebe6d6dd 100644 --- a/hstest/testing/execution/runnable/python_runnable_file.py +++ b/hstest/testing/execution/runnable/python_runnable_file.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from hstest.testing.execution.runnable.runnable_file import RunnableFile class PythonRunnableFile(RunnableFile): - def __init__(self, folder: str, file: str, module: str): + def __init__(self, folder: str, file: str, module: str) -> None: super().__init__(folder, file) self.module = module diff --git a/hstest/testing/execution/runnable/runnable_file.py b/hstest/testing/execution/runnable/runnable_file.py index 506e65aa..5c90d645 100644 --- a/hstest/testing/execution/runnable/runnable_file.py +++ b/hstest/testing/execution/runnable/runnable_file.py @@ -1,7 +1,12 @@ -from hstest.testing.execution.filtering.file_filter import File, Folder +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from hstest.testing.execution.filtering.file_filter import File, Folder class RunnableFile: - def __init__(self, folder: Folder, file: File): + def __init__(self, folder: Folder, file: File) -> None: self.folder = folder self.file = file diff --git a/hstest/testing/execution/searcher/base_searcher.py b/hstest/testing/execution/searcher/base_searcher.py index 9457d3cf..4fe59cc4 100644 --- a/hstest/testing/execution/searcher/base_searcher.py +++ b/hstest/testing/execution/searcher/base_searcher.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import os import re -from typing import Dict, List, Optional, Set, Tuple, Union from hstest.common.file_utils import walk_user_files from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError @@ -15,13 +16,15 @@ class BaseSearcher: @property def extension(self) -> str: - raise NotImplementedError('Property "extension" should be implemented') + msg = 'Property "extension" should be implemented' + raise NotImplementedError(msg) - def search(self, where_to_search: str = None) -> RunnableFile: - raise NotImplementedError('Method "search" should be implemented') + def search(self, where_to_search: str | None = None) -> RunnableFile: + msg = 'Method "search" should be implemented' + raise NotImplementedError(msg) @staticmethod - def _get_contents(folder: Folder, files: List[File]) -> Dict[File, Source]: + def _get_contents(folder: Folder, files: list[File]) -> dict[File, Source]: contents = {} for file in files: @@ -29,7 +32,7 @@ def _get_contents(folder: Folder, files: List[File]) -> Dict[File, Source]: if path in file_contents_cached: contents[file] = file_contents_cached[path] elif os.path.exists(path): - with open(path) as f: + with open(path, encoding="utf-8") as f: try: file_content = f.read() except UnicodeDecodeError: @@ -48,21 +51,18 @@ def _search_non_cached( pre_main_filter: FileFilter, main_filter: MainFilter, post_main_filter: FileFilter, - force_content_filters: Union[List[MainFilter], None] = None + force_content_filters: list[MainFilter] | None = None, ) -> RunnableFile: - if not force_content_filters: force_content_filters = [] curr_folder = os.path.abspath(where_to_search) - for folder, dirs, files in walk_user_files(curr_folder): - + for folder, _dirs, files in walk_user_files(curr_folder): contents = self._get_contents(folder, files) initial_filter = FileFilter( - file=lambda f: f.endswith(self.extension), - generic=file_filter.filter + file=lambda f: f.endswith(self.extension), generic=file_filter.filter ) candidates = set(files) @@ -70,8 +70,9 @@ def _search_non_cached( for curr_filter in initial_filter, pre_main_filter, main_filter, post_main_filter: curr_filter.init_filter(folder, contents) - filtered_files: Set[File] = { - file for file in files + filtered_files: set[File] = { + file + for file in files if file in contents and curr_filter.filter(folder, file, contents[file]) } @@ -80,15 +81,14 @@ def _search_non_cached( if len(filtered_files) == 0: if curr_filter == initial_filter: break - else: - continue - elif curr_filter == initial_filter: + continue + if curr_filter == initial_filter: for forced_filter in force_content_filters: filtered_files = { - file for file in filtered_files - if file in contents and forced_filter.filter( - folder, file, contents[file] - ) + file + for file in filtered_files + if file in contents + and forced_filter.filter(folder, file, contents[file]) } if len(filtered_files) == 0: should_contain = [ @@ -96,9 +96,8 @@ def _search_non_cached( for forced_filter in force_content_filters if isinstance(forced_filter, MainFilter) ] - raise ErrorWithFeedback( - f'The runnable file should contain all the following lines: {should_contain}' - ) + msg = f"The runnable file should contain all the following lines: {should_contain}" + raise ErrorWithFeedback(msg) if len(filtered_files) == 1: file = filtered_files.pop() @@ -116,46 +115,55 @@ def _search_non_cached( continue if len(candidates) > 1 and len(main_filter.filtered) > 0: - str_files = ', '.join(f'"{f}"' for f in sorted(candidates)) + str_files = ", ".join(f'"{f}"' for f in sorted(candidates)) all_have = [] if main_filter.program_should_contain: all_have.append(main_filter.program_should_contain) - all_have.extend([ - forced_filter.program_should_contain for forced_filter in force_content_filters - if isinstance(forced_filter, MainFilter) - ]) - raise ErrorWithFeedback( - f'Cannot decide which file to run out of the following: {str_files}\n' - f'They all have {all_have}. ' - f'Leave one file with this lines.') + all_have.extend( + [ + forced_filter.program_should_contain + for forced_filter in force_content_filters + if isinstance(forced_filter, MainFilter) + ] + ) + msg = ( + f"Cannot decide which file to run out of the following: {str_files}\n" + f"They all have {all_have}. " + f"Leave one file with this lines." + ) + raise ErrorWithFeedback(msg) if len(candidates) == 0: candidates = initial_filter.filtered - str_files = ', '.join(f'"{f}"' for f in sorted(candidates)) + str_files = ", ".join(f'"{f}"' for f in sorted(candidates)) - raise ErrorWithFeedback( - f'Cannot decide which file to run out of the following: {str_files}\n' + msg = ( + f"Cannot decide which file to run out of the following: {str_files}\n" f'Write "{main_filter.program_should_contain}" ' - f'in one of them to mark it as an entry point.') + f"in one of them to mark it as an entry point." + ) + raise ErrorWithFeedback(msg) - raise ErrorWithFeedback( - 'Cannot find a file to execute your code.\n' - f'Are your project files located at \"{curr_folder}\"?') + msg = ( + "Cannot find a file to execute your code.\n" + f'Are your project files located at "{curr_folder}"?' + ) + raise ErrorWithFeedback(msg) def _search( self, - where_to_search: str = None, + where_to_search: str | None = None, *, file_filter: FileFilter = None, pre_main_filter: FileFilter = None, main_filter: MainFilter = None, post_main_filter: FileFilter = None, - force_content_filters: Union[List[MainFilter], None] = None + force_content_filters: list[MainFilter] | None = None, ) -> RunnableFile: - - if not self.extension.startswith('.'): - raise UnexpectedError(f'File extension "{self.extension}" should start with a dot') + if not self.extension.startswith("."): + msg = f'File extension "{self.extension}" should start with a dot' + raise UnexpectedError(msg) if where_to_search is None: where_to_search = os.getcwd() @@ -193,27 +201,25 @@ def _search( return result - def _simple_search(self, - where_to_search: str, - main_desc: str, - main_regex: str, - force_content_filters: Union[List[MainFilter], None] = None - ) -> RunnableFile: - main_searcher = re.compile(main_regex, re.M) + def _simple_search( + self, + where_to_search: str, + main_desc: str, + main_regex: str, + force_content_filters: list[MainFilter] | None = None, + ) -> RunnableFile: + main_searcher = re.compile(main_regex, re.MULTILINE) return self._search( where_to_search, - main_filter=MainFilter( - main_desc, - source=lambda s: main_searcher.search(s) is not None - ), - force_content_filters=force_content_filters + main_filter=MainFilter(main_desc, source=lambda s: main_searcher.search(s) is not None), + force_content_filters=force_content_filters, ) def _base_search(self, where_to_search: str) -> RunnableFile: - return self._simple_search(where_to_search, main_desc='', main_regex='') + return self._simple_search(where_to_search, main_desc="", main_regex="") - def find(self, source: Optional[str]) -> RunnableFile: - if source in [None, '']: + def find(self, source: str | None) -> RunnableFile: + if source in {None, ""}: return self.search() ext = self.extension @@ -223,38 +229,39 @@ def find(self, source: Optional[str]) -> RunnableFile: if source_folder is not None and os.path.isdir(source_folder): return self.search(source_folder) - elif source_file is not None and os.path.isfile(source_file): - path, sep, file = source_module.rpartition('.') - folder = os.path.abspath(path.replace('.', os.sep)) + if source_file is not None and os.path.isfile(source_file): + path, _sep, file = source_module.rpartition(".") + folder = os.path.abspath(path.replace(".", os.sep)) return RunnableFile(folder, file + ext) - else: - path, _, _ = source_module.rpartition('.') - folder = os.path.abspath(path.replace('.', os.sep)) - raise ErrorWithFeedback( - 'Cannot find a file to execute your code.\n' - f'Are your project files located at \"{folder}\"?') + path, _, _ = source_module.rpartition(".") + folder = os.path.abspath(path.replace(".", os.sep)) + msg = ( + "Cannot find a file to execute your code.\n" + f'Are your project files located at "{folder}"?' + ) + raise ErrorWithFeedback(msg) - def _parse_source(self, source: str) -> Tuple[Folder, File, Module]: + def _parse_source(self, source: str) -> tuple[Folder, File, Module]: ext = self.extension - source = source.replace('/', os.sep).replace('\\', os.sep) + source = source.replace("/", os.sep).replace("\\", os.sep) if source.endswith(ext): source_folder = None source_file = source - source_module = source[:-len(ext)].replace(os.sep, '.') + source_module = source[: -len(ext)].replace(os.sep, ".") elif os.sep in source: if source.endswith(os.sep): - source = source[:-len(os.sep)] + source = source[: -len(os.sep)] source_folder = source source_file = None - source_module = source.replace(os.sep, '.') + source_module = source.replace(os.sep, ".") else: - source_folder = source.replace('.', os.sep) + source_folder = source.replace(".", os.sep) source_file = source_folder + ext source_module = source diff --git a/hstest/testing/execution/searcher/cpp_searcher.py b/hstest/testing/execution/searcher/cpp_searcher.py index b01fac3a..c0173070 100644 --- a/hstest/testing/execution/searcher/cpp_searcher.py +++ b/hstest/testing/execution/searcher/cpp_searcher.py @@ -1,25 +1,26 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING from hstest.testing.execution.filtering.main_filter import MainFilter -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class CppSearcher(BaseSearcher): +class CppSearcher(BaseSearcher): @property def extension(self) -> str: - return '.cpp' + return ".cpp" - def search(self, where: str = None) -> RunnableFile: - main_func_searcher = re.compile(r'(^|\n)\s*int\s+main\s*\(.*\)', re.M) + def search(self, where: str | None = None) -> RunnableFile: + main_func_searcher = re.compile(r"(^|\n)\s*int\s+main\s*\(.*\)", re.MULTILINE) return self._search( where, force_content_filters=[ - MainFilter( - 'int main()', - source=lambda s: main_func_searcher.search(s) is not None - ), - ] + MainFilter("int main()", source=lambda s: main_func_searcher.search(s) is not None), + ], ) diff --git a/hstest/testing/execution/searcher/go_searcher.py b/hstest/testing/execution/searcher/go_searcher.py index aa00bbe2..838dfc5d 100644 --- a/hstest/testing/execution/searcher/go_searcher.py +++ b/hstest/testing/execution/searcher/go_searcher.py @@ -1,30 +1,30 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING from hstest.testing.execution.filtering.main_filter import MainFilter -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class GoSearcher(BaseSearcher): +class GoSearcher(BaseSearcher): @property def extension(self) -> str: - return '.go' + return ".go" - def search(self, where: str = None) -> RunnableFile: - package_searcher = re.compile(r'^\s*package\s*main', re.M) - main_func_searcher = re.compile(r'(^|\n)\s*func\s+main\s*\(\s*\)', re.M) + def search(self, where: str | None = None) -> RunnableFile: + package_searcher = re.compile(r"^\s*package\s*main", re.MULTILINE) + main_func_searcher = re.compile(r"(^|\n)\s*func\s+main\s*\(\s*\)", re.MULTILINE) return self._search( where, force_content_filters=[ + MainFilter("package main", source=lambda s: package_searcher.search(s) is not None), MainFilter( - 'package main', - source=lambda s: package_searcher.search(s) is not None - ), - MainFilter( - 'func main()', - source=lambda s: main_func_searcher.search(s) is not None + "func main()", source=lambda s: main_func_searcher.search(s) is not None ), - ] + ], ) diff --git a/hstest/testing/execution/searcher/javascript_searcher.py b/hstest/testing/execution/searcher/javascript_searcher.py index 027ea5a9..f1ec811f 100644 --- a/hstest/testing/execution/searcher/javascript_searcher.py +++ b/hstest/testing/execution/searcher/javascript_searcher.py @@ -1,12 +1,17 @@ -from hstest.testing.execution.runnable.runnable_file import RunnableFile +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class JavascriptSearcher(BaseSearcher): +class JavascriptSearcher(BaseSearcher): @property def extension(self) -> str: - return '.js' + return ".js" - def search(self, where: str = None) -> RunnableFile: - return self._simple_search(where, "function main()", r'(^|\n) *function +main +\( *\)') + def search(self, where: str | None = None) -> RunnableFile: + return self._simple_search(where, "function main()", r"(^|\n) *function +main +\( *\)") diff --git a/hstest/testing/execution/searcher/python_searcher.py b/hstest/testing/execution/searcher/python_searcher.py index f4f85c11..1afd32a0 100644 --- a/hstest/testing/execution/searcher/python_searcher.py +++ b/hstest/testing/execution/searcher/python_searcher.py @@ -1,32 +1,37 @@ +from __future__ import annotations + import os import re -from typing import Optional +from typing import TYPE_CHECKING from hstest.dynamic.output.output_handler import OutputHandler from hstest.testing.execution.filtering.file_filter import FileFilter, Folder, Sources from hstest.testing.execution.filtering.main_filter import MainFilter from hstest.testing.execution.runnable.python_runnable_file import PythonRunnableFile -from hstest.testing.execution.runnable.runnable_file import RunnableFile from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class PythonSearcher(BaseSearcher): +class PythonSearcher(BaseSearcher): @property def extension(self) -> str: - return '.py' + return ".py" - def search(self, where_to_search: str = None, file_filter: FileFilter = None) -> RunnableFile: + def search( + self, where_to_search: str | None = None, file_filter: FileFilter = None + ) -> RunnableFile: is_imported = {} - def init_regexes(_: Folder, sources: Sources): + def init_regexes(_: Folder, sources: Sources) -> None: import_regexes = {} for file, source in sources.items(): is_imported[file] = False import_regexes[file] = [ - re.compile(rf'(^|\n)import +[\w., ]*\b{file[:-3]}\b[\w., ]*', re.M), - re.compile(rf'(^|\n)from +\.? *\b{file[:-3]}\b +import +', re.M) + re.compile(rf"(^|\n)import +[\w., ]*\b{file[:-3]}\b[\w., ]*", re.MULTILINE), + re.compile(rf"(^|\n)from +\.? *\b{file[:-3]}\b +import +", re.MULTILINE), ] for file, source in sources.items(): @@ -37,22 +42,16 @@ def init_regexes(_: Folder, sources: Sources): return self._search( where_to_search, file_filter=file_filter, - - pre_main_filter=FileFilter( - init_files=init_regexes, - file=lambda f: not is_imported[f] - ), - + pre_main_filter=FileFilter(init_files=init_regexes, file=lambda f: not is_imported[f]), main_filter=MainFilter( - "if __name__ == '__main__'", - source=lambda s: '__name__' in s and '__main__' in s - ) + "if __name__ == '__main__'", source=lambda s: "__name__" in s and "__main__" in s + ), ) - def find(self, source: Optional[str]) -> PythonRunnableFile: - OutputHandler.print(f'PythonSearcher source = {source}, cwd = {os.getcwd()}') + def find(self, source: str | None) -> PythonRunnableFile: + OutputHandler.print(f"PythonSearcher source = {source}, cwd = {os.getcwd()}") runnable = super().find(source) - OutputHandler.print(f'PythonSearcher found runnable: {runnable.folder}/{runnable.file}') + OutputHandler.print(f"PythonSearcher found runnable: {runnable.folder}/{runnable.file}") return PythonRunnableFile( - runnable.folder, runnable.file, runnable.file[:-len(self.extension)] + runnable.folder, runnable.file, runnable.file[: -len(self.extension)] ) diff --git a/hstest/testing/execution/searcher/shell_searcher.py b/hstest/testing/execution/searcher/shell_searcher.py index effe194b..63f5a8a4 100644 --- a/hstest/testing/execution/searcher/shell_searcher.py +++ b/hstest/testing/execution/searcher/shell_searcher.py @@ -1,12 +1,17 @@ -from hstest.testing.execution.runnable.runnable_file import RunnableFile +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class ShellSearcher(BaseSearcher): +class ShellSearcher(BaseSearcher): @property def extension(self) -> str: - return '.sh' + return ".sh" - def search(self, where: str = None) -> RunnableFile: - return self._simple_search(where, "# main", r'(^|\n)# *main') + def search(self, where: str | None = None) -> RunnableFile: + return self._simple_search(where, "# main", r"(^|\n)# *main") diff --git a/hstest/testing/execution/searcher/sql_searcher.py b/hstest/testing/execution/searcher/sql_searcher.py index 986f2892..1f52c04f 100644 --- a/hstest/testing/execution/searcher/sql_searcher.py +++ b/hstest/testing/execution/searcher/sql_searcher.py @@ -1,12 +1,17 @@ -from hstest.testing.execution.runnable.runnable_file import RunnableFile +from __future__ import annotations + +from typing import TYPE_CHECKING + from hstest.testing.execution.searcher.base_searcher import BaseSearcher +if TYPE_CHECKING: + from hstest.testing.execution.runnable.runnable_file import RunnableFile -class SQLSearcher(BaseSearcher): +class SQLSearcher(BaseSearcher): @property def extension(self) -> str: - return '.sql' + return ".sql" - def search(self, where: str = None) -> RunnableFile: + def search(self, where: str | None = None) -> RunnableFile: return self._base_search(where) diff --git a/hstest/testing/execution_options.py b/hstest/testing/execution_options.py index da688918..d82273d6 100644 --- a/hstest/testing/execution_options.py +++ b/hstest/testing/execution_options.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import os import sys -skip_slow: bool = '--skip_slow' in sys.argv -ignore_stdout: bool = '--ignore_stdout' in sys.argv -inside_docker: bool = '--inside_docker' in sys.argv or os.environ.get('INSIDE_DOCKER', '') == '1' -debug_mode: bool = '--debug_mode' in sys.argv or sys.gettrace() is not None -force_process_testing: bool = '--force_process_testing' in sys.argv +skip_slow: bool = "--skip_slow" in sys.argv +ignore_stdout: bool = "--ignore_stdout" in sys.argv +inside_docker: bool = "--inside_docker" in sys.argv or os.environ.get("INSIDE_DOCKER", "") == "1" +debug_mode: bool = "--debug_mode" in sys.argv or sys.gettrace() is not None +force_process_testing: bool = "--force_process_testing" in sys.argv diff --git a/hstest/testing/plotting/drawing/drawing.py b/hstest/testing/plotting/drawing/drawing.py index 6f81db16..84728d72 100644 --- a/hstest/testing/plotting/drawing/drawing.py +++ b/hstest/testing/plotting/drawing/drawing.py @@ -1,16 +1,16 @@ -from typing import Any, Dict, Optional +from __future__ import annotations -from hstest.testing.plotting.drawing.drawing_data import DrawingData +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from hstest.testing.plotting.drawing.drawing_data import DrawingData -class Drawing: - def __init__(self, - library: str, - plot_type: str, - data: Optional[DrawingData], - kwargs: Dict[str, Any]): +class Drawing: + def __init__( + self, library: str, plot_type: str, data: DrawingData | None, kwargs: dict[str, Any] + ) -> None: self.library: str = library self.type: str = plot_type - self.data: Optional[DrawingData] = data - self.kwargs: Dict[str, Any] = kwargs + self.data: DrawingData | None = data + self.kwargs: dict[str, Any] = kwargs diff --git a/hstest/testing/plotting/drawing/drawing_builder.py b/hstest/testing/plotting/drawing/drawing_builder.py index 301cbac5..07ca4ac5 100644 --- a/hstest/testing/plotting/drawing/drawing_builder.py +++ b/hstest/testing/plotting/drawing/drawing_builder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_type import DrawingType from hstest.testing.plotting.drawing_data_normalizer import DrawingDataNormalizer diff --git a/hstest/testing/plotting/drawing/drawing_data.py b/hstest/testing/plotting/drawing/drawing_data.py index 952e4915..6296a0bd 100644 --- a/hstest/testing/plotting/drawing/drawing_data.py +++ b/hstest/testing/plotting/drawing/drawing_data.py @@ -1,25 +1,27 @@ -from typing import Optional +from __future__ import annotations import numpy as np class DrawingData: - def __init__(self, x: np.ndarray, y: np.ndarray): + def __init__(self, x: np.ndarray, y: np.ndarray) -> None: try: if type(x) != list and x is not None: x = list(x) if type(y) != list and y is not None: y = list(y) except Exception: - raise ValueError('The data argument should be an array') + msg = "The data argument should be an array" + raise ValueError(msg) if x is not None and y is not None and len(x) != len(y): - raise ValueError('Arrays should be the same length') + msg = "Arrays should be the same length" + raise ValueError(msg) if x is not None: x = np.array(x, dtype=object) if y is not None: y = np.array(y, dtype=object) - self.x: Optional[np.ndarray] = x - self.y: Optional[np.ndarray] = y + self.x: np.ndarray | None = x + self.y: np.ndarray | None = y diff --git a/hstest/testing/plotting/drawing/drawing_library.py b/hstest/testing/plotting/drawing/drawing_library.py index 6f1d5730..6af09a3a 100644 --- a/hstest/testing/plotting/drawing/drawing_library.py +++ b/hstest/testing/plotting/drawing/drawing_library.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class DrawingLibrary: matplotlib = "matplotlib" pandas = "pandas" diff --git a/hstest/testing/plotting/drawing/drawing_type.py b/hstest/testing/plotting/drawing/drawing_type.py index 0c95a5d7..f3febb89 100644 --- a/hstest/testing/plotting/drawing/drawing_type.py +++ b/hstest/testing/plotting/drawing/drawing_type.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class DrawingType: # ---------------------- # common types with data diff --git a/hstest/testing/plotting/drawing_data_normalizer.py b/hstest/testing/plotting/drawing_data_normalizer.py index e30928df..a0e0639c 100644 --- a/hstest/testing/plotting/drawing_data_normalizer.py +++ b/hstest/testing/plotting/drawing_data_normalizer.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import numpy as np class DrawingDataNormalizer: - @staticmethod def normalize_x_y_data(x, y) -> np.ndarray: try: @@ -11,21 +12,22 @@ def normalize_x_y_data(x, y) -> np.ndarray: if type(y) != list: y = list(y) except Exception: - raise ValueError('The data argument should be an array') + msg = "The data argument should be an array" + raise ValueError(msg) if len(x) != len(y): - raise ValueError('Arrays should be the same length') + msg = "Arrays should be the same length" + raise ValueError(msg) - result_data = list() + result_data = [] - for a, b in zip(x, y): + for a, b in zip(x, y, strict=False): result_data.append((a, b)) return np.array(result_data, dtype=object) @staticmethod def normalize_hist_data(data) -> np.ndarray: - if type(data) == str: data = [data] @@ -33,7 +35,8 @@ def normalize_hist_data(data) -> np.ndarray: try: data = list(data) except Exception: - raise ValueError('The data argument should be an array') + msg = "The data argument should be an array" + raise ValueError(msg) return np.array(data, dtype=object) @@ -73,6 +76,7 @@ def normalize_hist_data(data) -> np.ndarray: return np.array(result_data, dtype=object) """ # noqa: W293 + return None @staticmethod def normalize_bar_data(x, y) -> np.ndarray: diff --git a/hstest/testing/plotting/matplotlib_handler.py b/hstest/testing/plotting/matplotlib_handler.py index 32388b3e..d3c1f269 100644 --- a/hstest/testing/plotting/matplotlib_handler.py +++ b/hstest/testing/plotting/matplotlib_handler.py @@ -1,13 +1,16 @@ +from __future__ import annotations + +import contextlib from copy import deepcopy from importlib import reload from typing import TYPE_CHECKING from hstest.testing.plotting.drawing.drawing_data import DrawingData -try: +with contextlib.suppress(ImportError): import pandas as pd -except ImportError: - pass + +import contextlib from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_builder import DrawingBuilder @@ -37,30 +40,27 @@ class MatplotlibHandler: _matplotlib = None @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): - + def replace_plots(drawings: DrawingsStorage) -> None: try: - import matplotlib + import matplotlib as mpl import numpy as np except ModuleNotFoundError: return - def custom_show_func(*args, **kwargs): + def custom_show_func(*args, **kwargs) -> None: pass def hist(x, *args, data=None, **kw): if data is not None: - try: + with contextlib.suppress(Exception): x = data[x] - except Exception: - pass try: if type(x) == pd.DataFrame: for col in x.columns: hist(x[col], *args, **kw) - return - elif type(x) == pd.Series: + return None + if type(x) == pd.Series: return hist(x.to_numpy(), *args, **kw) except Exception: pass @@ -68,128 +68,111 @@ def hist(x, *args, data=None, **kw): if type(x) != np.ndarray: x = np.array(x, dtype=object) if len(x.shape) == 2: - import matplotlib.cbook as cbook - x = np.array(cbook._reshape_2D(x, 'x'), dtype=object) + from matplotlib import cbook + + x = np.array(cbook._reshape_2D(x, "x"), dtype=object) if len(x.shape) == 2: for i in range(x.shape[1]): hist(x[:, i], *args, **kw) - return + return None drawings.append( Drawing( DrawingLibrary.matplotlib, DrawingType.hist, DrawingData(x, np.array([1] * len(x), dtype=object)), - kw + kw, ) ) + return None def bar(x, height, *args, data=None, **kw): if data is not None: - try: + with contextlib.suppress(Exception): x = data[x] - except Exception: - pass - try: + with contextlib.suppress(Exception): height = data[height] - except Exception: - pass try: if type(x) == pd.DataFrame: for col in x.columns: bar(x[col], *args, **kw) - return - elif type(x) == pd.Series: + return None + if type(x) == pd.Series: return bar(x.to_numpy(), height, *args, **kw) - elif type(height) == pd.Series: + if type(height) == pd.Series: return bar(x, height.to_numpy(), *args, **kw) except Exception: pass - if type(height) in [int, float]: + if type(height) in {int, float}: height = np.full((len(x),), height) drawings.append( - Drawing( - DrawingLibrary.matplotlib, - DrawingType.bar, - DrawingData(x, height), - kw - ) + Drawing(DrawingLibrary.matplotlib, DrawingType.bar, DrawingData(x, height), kw) ) + return None def barh(x, width, *args, data=None, **kw): return bar(x, width, *args, data=data, **kw) - def plot(*args, **kwargs): - x = list() - y = list() + def plot(*args, **kwargs) -> None: + x = [] + y = [] - if len(args) > 0: - if type(args[0]) is list: - x = args[0] + if len(args) > 0 and type(args[0]) is list: + x = args[0] if len(args) > 1: if type(args[1]) is list: y = args[1] else: - y = [_ for _ in range(len(x))] + y = list(range(len(x))) drawings.append( DrawingBuilder.get_line_drawing( - x, y, + x, + y, DrawingLibrary.matplotlib, kwargs, ) ) - def scatter(x, y, *a, **kwargs): + def scatter(x, y, *a, **kwargs) -> None: drawings.append( DrawingBuilder.get_scatter_drawing( - x, y, + x, + y, DrawingLibrary.matplotlib, kwargs, ) ) - def pie(x, *a, **kw): + def pie(x, *a, **kw) -> None: # Normalize with other plot libraries y = x - x = [''] * len(y) + x = [""] * len(y) - if 'labels' in kw and kw['labels'] is not None: - x = kw['labels'] + if "labels" in kw and kw["labels"] is not None: + x = kw["labels"] drawings.append( - Drawing( - DrawingLibrary.matplotlib, - DrawingType.pie, - DrawingData(x, y), - kw - ) + Drawing(DrawingLibrary.matplotlib, DrawingType.pie, DrawingData(x, y), kw) ) - def violinplot(dataset, *, data=None, **kwargs): + def violinplot(dataset, *, data=None, **kwargs) -> None: if data is not None: - try: + with contextlib.suppress(Exception): dataset = data[dataset] - except Exception: - pass - drawing = Drawing( - DrawingLibrary.matplotlib, - DrawingType.violin, - dataset, - kwargs - ) + drawing = Drawing(DrawingLibrary.matplotlib, DrawingType.violin, dataset, kwargs) drawings.append(drawing) - def imshow(x, **kwargs): + def imshow(x, **kwargs) -> None: curr_data = { # noqa: F841 - 'x': np.array(x, dtype=object) + "x": np.array(x, dtype=object) } drawing = Drawing( @@ -200,10 +183,10 @@ def imshow(x, **kwargs): ) drawings.append(drawing) - def boxplot(x, **kwargs): + def boxplot(x, **kwargs) -> None: curr_data = { # noqa: F841 - 'x': np.array([None], dtype=object), - 'y': np.array(x, dtype=object) + "x": np.array([None], dtype=object), + "y": np.array(x, dtype=object), } drawing = Drawing( @@ -214,39 +197,37 @@ def boxplot(x, **kwargs): ) drawings.append(drawing) + import matplotlib as mpl import matplotlib.axes class CustomMatplotlibAxes(matplotlib.axes.Axes): - - def hist(self, x, *a, **kw): + def hist(self, x, *a, **kw) -> None: hist(x, *a, **kw) - def bar(self, x, height, *a, **kw): + def bar(self, x, height, *a, **kw) -> None: bar(x, height, *a, **kw) - def barh(self, y, width, *a, **kw): + def barh(self, y, width, *a, **kw) -> None: barh(y, width, *a, **kw) - def plot(self, *args, **kwargs): + def plot(self, *args, **kwargs) -> None: plot(*args, *kwargs) - def scatter(self, x, y, *a, **kwargs): + def scatter(self, x, y, *a, **kwargs) -> None: scatter(x, y, *a, **kwargs) - def pie(self, x, *a, **kw): + def pie(self, x, *a, **kw) -> None: pie(x, *a, **kw) - def violinplot(self, dataset, **kwargs): + def violinplot(self, dataset, **kwargs) -> None: violinplot(dataset, **kwargs) - def imshow(self, x, **kwargs): + def imshow(self, x, **kwargs) -> None: imshow(x, **kwargs) - def boxplot(self, x, **kwargs): + def boxplot(self, x, **kwargs) -> None: boxplot(x, **kwargs) - import matplotlib - if not MatplotlibHandler._saved: MatplotlibHandler._Axes = deepcopy(matplotlib.axes.Axes) @@ -254,7 +235,8 @@ def boxplot(self, x, **kwargs): matplotlib.axes.Axes = CustomMatplotlibAxes from matplotlib.projections import projection_registry - projection_registry.register(matplotlib.axes.Axes) + + projection_registry.register(CustomMatplotlibAxes) import matplotlib.pyplot as plt @@ -270,7 +252,7 @@ def boxplot(self, x, **kwargs): MatplotlibHandler._imshow = plt.imshow MatplotlibHandler._boxplot = plt.boxplot MatplotlibHandler._show = plt.show - MatplotlibHandler._backend = matplotlib.get_backend() + MatplotlibHandler._backend = mpl.get_backend() plt.hist = hist plt.plot = plot @@ -283,13 +265,12 @@ def boxplot(self, x, **kwargs): plt.boxplot = boxplot plt.show = custom_show_func - matplotlib.use('Agg') + mpl.use("Agg") MatplotlibHandler._replaced = True @staticmethod - def revert_plots(): - + def revert_plots() -> None: if not MatplotlibHandler._replaced: return diff --git a/hstest/testing/plotting/pandas_handler.py b/hstest/testing/plotting/pandas_handler.py index 98e9100a..ad1a6168 100644 --- a/hstest/testing/plotting/pandas_handler.py +++ b/hstest/testing/plotting/pandas_handler.py @@ -1,11 +1,12 @@ +from __future__ import annotations + +import contextlib from typing import TYPE_CHECKING from hstest.testing.plotting.drawing.drawing_data import DrawingData -try: +with contextlib.suppress(ImportError): import numpy as np -except ImportError: - pass try: import pandas as pd @@ -13,6 +14,8 @@ except ImportError: pass +import contextlib + from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_builder import DrawingBuilder from hstest.testing.plotting.drawing.drawing_library import DrawingLibrary @@ -43,67 +46,40 @@ class PandasHandler: plot_name_to_basic_name = { # 'barh': DrawingType.bar, - 'density': DrawingType.dis, - 'kde': DrawingType.dis, - } - - graph_type_to_normalized_data = { - 'scatter': lambda data, x, y: PandasHandler.get_scatter_drawings_with_normalized_data( - data, x, y - ), - 'line': lambda data, x, y: PandasHandler.get_line_drawings_with_normalized_data(data, x, y), - 'pie': lambda data, x, y: PandasHandler.get_pie_drawings_with_normalized_data(data, x, y), - # 'bar': lambda data, x, y: PandasHandler.get_bar_drawings_with_normalized_data(data, x, y), - 'box': lambda data, x, y: PandasHandler.get_box_drawings_with_normalized_data(data, x, y), - 'dis': lambda data, x, y: PandasHandler.get_dis_drawings_with_normalized_data(data, x, y), + "density": DrawingType.dis, + "kde": DrawingType.dis, } @staticmethod def get_line_drawings_with_normalized_data(data, x, y): - drawings = list() + drawings = [] if type(data) is pd.Series: drawings.append( - DrawingBuilder.get_line_drawing( - data.index, - data, - DrawingLibrary.pandas, - {} - ) + DrawingBuilder.get_line_drawing(data.index, data, DrawingLibrary.pandas, {}) ) return drawings for column in data.columns: drawings.append( - DrawingBuilder.get_line_drawing( - data.index, - data[column], - DrawingLibrary.pandas, - {} - ) + DrawingBuilder.get_line_drawing(data.index, data[column], DrawingLibrary.pandas, {}) ) return drawings @staticmethod def get_scatter_drawings_with_normalized_data(data, x, y): - return [ - DrawingBuilder.get_scatter_drawing( - data[x], data[y], - DrawingLibrary.pandas, - {} - ) - ] + return [DrawingBuilder.get_scatter_drawing(data[x], data[y], DrawingLibrary.pandas, {})] @staticmethod - def get_pie_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): + def get_pie_drawings_with_normalized_data(data: pd.DataFrame, x, y): if type(data) == pd.Series: return [ Drawing( DrawingLibrary.pandas, DrawingType.pie, DrawingData(data.index.to_numpy(), data.to_numpy()), - {} + {}, ) ] @@ -113,7 +89,7 @@ def get_pie_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): DrawingLibrary.pandas, DrawingType.pie, DrawingData(data.index.to_numpy(), data[y].to_numpy()), - {} + {}, ) ] @@ -127,43 +103,31 @@ def get_pie_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): DrawingLibrary.pandas, DrawingType.pie, DrawingData(data.index.to_numpy(), data[column].to_numpy()), - {} + {}, ) ) return drawings @staticmethod - def get_bar_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): + def get_bar_drawings_with_normalized_data(data: pd.DataFrame, x, y): drawings = [] - if x is not None: - x_arr = data[x].to_numpy() - else: - x_arr = data.index.to_numpy() + x_arr = data[x].to_numpy() if x is not None else data.index.to_numpy() if y is not None: - drawing = DrawingBuilder.get_bar_drawing( - x_arr, data[y], - DrawingLibrary.pandas, - {} - ) + drawing = DrawingBuilder.get_bar_drawing(x_arr, data[y], DrawingLibrary.pandas, {}) drawings.append(drawing) return drawings for column in data.columns: if not is_numeric_dtype(data[column]): continue - drawing = DrawingBuilder.get_bar_drawing( - x_arr, data[column], - DrawingLibrary.pandas, - {} - ) + drawing = DrawingBuilder.get_bar_drawing(x_arr, data[column], DrawingLibrary.pandas, {}) drawings.append(drawing) return drawings @staticmethod - def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): - + def get_box_drawings_with_normalized_data(data: pd.DataFrame, x, y): drawings = [] # Columns are not specified @@ -172,17 +136,9 @@ def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): if not is_numeric_dtype(data[column]): continue - curr_data = { # noqa: F841 - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() - } + curr_data = {"x": np.array([column], dtype=object), "y": data[column].to_numpy()} - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.box, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.box, None, {}) drawings.append(drawing) return drawings @@ -191,16 +147,11 @@ def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): continue curr_data = { # noqa: F841 - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() + "x": np.array([column], dtype=object), + "y": data[column].to_numpy(), } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.box, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.box, None, {}) drawings.append(drawing) return drawings @@ -209,42 +160,25 @@ def get_dis_drawings_with_normalized_data(data, x, y): drawings = [] if type(data) == pd.Series: - curr_data = { - 'x': data.to_numpy() - } + curr_data = {"x": data.to_numpy()} - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) return drawings if x: curr_data = { - 'x': np.array(data[x], dtype=object), + "x": np.array(data[x], dtype=object), } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) if y: curr_data = { - 'x': np.array(data[y], dtype=object), + "x": np.array(data[y], dtype=object), } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) if not x and not y: @@ -253,44 +187,29 @@ def get_dis_drawings_with_normalized_data(data, x, y): continue curr_data = { # noqa: F841 - 'x': data[column].to_numpy() + "x": data[column].to_numpy() } - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.dis, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) return drawings @staticmethod def get_area_drawings_with_normalized_data(data, x, y): drawings = [] - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.area, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.area, None, {}) drawings.append(drawing) return drawings @staticmethod def get_hexbin_drawings_with_normalized_data(data, x, y): drawings = [] - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.hexbin, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.hexbin, None, {}) drawings.append(drawing) return drawings @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): + def replace_plots(drawings: DrawingsStorage) -> None: try: import pandas.plotting from pandas.core.accessor import CachedAccessor @@ -308,22 +227,21 @@ def __call__(self, *args, **kw): ) if kind not in self._all_kinds: - raise ValueError(f"{kind} is not a valid plot kind") + msg = f"{kind} is not a valid plot kind" + raise ValueError(msg) data = self._parent.copy() - plot_name = kind if kind not in PandasHandler.plot_name_to_basic_name \ - else PandasHandler.plot_name_to_basic_name[kind] + plot_name = PandasHandler.plot_name_to_basic_name.get(kind, kind) # For boxplot from plot accessor - if plot_name == DrawingType.box: - if 'columns' in kwargs: - x = kwargs['columns'] + if plot_name == DrawingType.box and "columns" in kwargs: + x = kwargs["columns"] plot_to_func = { - 'hist': hist, - 'bar': bar, - 'barh': barh, + "hist": hist, + "bar": bar, + "barh": barh, } if plot_name in PandasHandler.graph_type_to_normalized_data: @@ -335,102 +253,79 @@ def __call__(self, *args, **kw): plot_to_func[plot_name](data, **kw) else: curr_data = { # noqa: F841 - 'data': data, - 'x': x, - 'y': y, - 'kwargs': kwargs + "data": data, + "x": x, + "y": y, + "kwargs": kwargs, } - drawing = Drawing( - DrawingLibrary.pandas, - plot_name, - None, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, plot_name, None, {}) drawings.append(drawing) import pandas.plotting._core - def boxplot( - self, - column=None, - **kwargs - ): + def boxplot(self, column=None, **kwargs) -> None: all_drawings = PandasHandler.get_box_drawings_with_normalized_data(self, column, None) drawings.extend(all_drawings) - def hist( - data, - column=None, - _process_by=True, - **kw - ): + def hist(data, column=None, _process_by=True, **kw): for k in list(kw.keys()): if kw[k] is None: kw.pop(k) - if _process_by and 'by' in kw and type(kw['by']) == str: - try: - kw['by'] = data[kw['by']] - except Exception: - pass + if _process_by and "by" in kw and type(kw["by"]) == str: + with contextlib.suppress(Exception): + kw["by"] = data[kw["by"]] - if 'y' in kw: - try: - data = data[kw.pop('y')] - except Exception: - pass + if "y" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("y")] - if 'x' in kw: - try: - data = data[kw.pop('x')] - except Exception: - pass + if "x" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("x")] if type(data) == pandas.DataFrame: if column is not None: return hist(data[column].to_numpy(), **kw) for col in data.columns: hist(data[col].to_numpy(), **kw) - return + return None - elif type(data) == pandas.Series: + if type(data) == pandas.Series: return hist(data.to_numpy(), **kw) - elif type(data) != np.ndarray: + if type(data) != np.ndarray: data = np.array(data, dtype=object) if len(data.shape) == 2: - import matplotlib.cbook as cbook - data = np.array(cbook._reshape_2D(data, 'x'), dtype=object) + from matplotlib import cbook + + data = np.array(cbook._reshape_2D(data, "x"), dtype=object) if len(data.shape) == 2: for i in range(data.shape[1]): hist(data[:, i], **kw) - return + return None - if _process_by and 'by' in kw: - by = kw['by'] + if _process_by and "by" in kw: + by = kw["by"] pictures = sorted(set(by), key=str) for pic in pictures: - subplot = [i for i, j in zip(data, by) if j == pic] + subplot = [i for i, j in zip(data, by, strict=False) if j == pic] hist(np.array(subplot, dtype=object), _process_by=False, **kw) - return + return None drawings.append( Drawing( DrawingLibrary.pandas, DrawingType.hist, DrawingData(data, np.array([1] * len(data), dtype=object)), - kw + kw, ) ) + return None - def bar( - data, - x=None, - y=None, - **kw - ): + def bar(data, x=None, y=None, **kw): for k in list(kw.keys()): if kw[k] is None: kw.pop(k) @@ -440,57 +335,35 @@ def bar( if type(y) == str: y = [y] for col in y: - bar(None, - data[x].array.to_numpy(), - data[col].array.to_numpy(), - **kw) - return + bar(None, data[x].array.to_numpy(), data[col].array.to_numpy(), **kw) + return None - elif x is not None: + if x is not None: for col in data.columns: if col != x: - bar(None, - data[x].array.to_numpy(), - data[col].array.to_numpy(), - **kw) - return + bar(None, data[x].array.to_numpy(), data[col].array.to_numpy(), **kw) + return None - elif y is not None: + if y is not None: if type(y) == str: y = [y] for col in y: - bar(None, - data[col].index.to_numpy(), - data[col].array.to_numpy(), - **kw) - return + bar(None, data[col].index.to_numpy(), data[col].array.to_numpy(), **kw) + return None - else: - for col in data.columns: - bar(None, - data[col].index.to_numpy(), - data[col].array.to_numpy(), - **kw) - return - - elif type(data) == pandas.Series: - return bar(None, - data.index.to_numpy(), - data.array.to_numpy(), - **kw) + for col in data.columns: + bar(None, data[col].index.to_numpy(), data[col].array.to_numpy(), **kw) + return None - drawings.append( - Drawing( - DrawingLibrary.pandas, - DrawingType.bar, - DrawingData(x, y), - kw - ) - ) + if type(data) == pandas.Series: + return bar(None, data.index.to_numpy(), data.array.to_numpy(), **kw) + + drawings.append(Drawing(DrawingLibrary.pandas, DrawingType.bar, DrawingData(x, y), kw)) + return None def barh( self, - ): + ) -> None: pass if not PandasHandler._saved: @@ -517,7 +390,7 @@ def barh( PandasHandler._replaced = True @staticmethod - def revert_plots(): + def revert_plots() -> None: if not PandasHandler._replaced: return @@ -535,3 +408,12 @@ def revert_plots(): pandas.DataFrame.boxplot = PandasHandler._dframe_boxplot PandasHandler._replaced = False + + graph_type_to_normalized_data = { + "scatter": get_scatter_drawings_with_normalized_data, + "line": get_line_drawings_with_normalized_data, + "pie": get_pie_drawings_with_normalized_data, + # 'bar': lambda data, x, y: get_bar_drawings_with_normalized_data(data, x, y), + "box": get_box_drawings_with_normalized_data, + "dis": get_dis_drawings_with_normalized_data, + } diff --git a/hstest/testing/plotting/seaborn_handler.py b/hstest/testing/plotting/seaborn_handler.py index b5b773b3..ed181955 100644 --- a/hstest/testing/plotting/seaborn_handler.py +++ b/hstest/testing/plotting/seaborn_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from importlib import reload from typing import TYPE_CHECKING @@ -9,6 +11,8 @@ except ImportError: pass +import contextlib + from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.drawing.drawing_builder import DrawingBuilder from hstest.testing.plotting.drawing.drawing_library import DrawingLibrary @@ -35,22 +39,19 @@ class SeabornHandler: _boxplot = None @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): + def replace_plots(drawings: DrawingsStorage) -> None: try: import numpy as np import seaborn as sns except ModuleNotFoundError: return - def displot(data=None, **kwargs): - x = None if 'x' not in kwargs else kwargs['x'] - y = None if 'y' not in kwargs else kwargs['y'] + def displot(data=None, **kwargs) -> None: + x = kwargs.get("x", None) + y = kwargs.get("y", None) if data is None: - curr_data = { - 'x': np.array(x, dtype=object), - 'y': np.array(y, dtype=object) - } + curr_data = {"x": np.array(x, dtype=object), "y": np.array(y, dtype=object)} drawing = Drawing( DrawingLibrary.seaborn, @@ -70,8 +71,8 @@ def displot(data=None, **kwargs): continue curr_data = { - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() + "x": np.array([column], dtype=object), + "y": data[column].to_numpy(), } drawing = Drawing( @@ -89,8 +90,8 @@ def displot(data=None, **kwargs): y_arr = data[y].to_numpy() curr_data = { # noqa: F841 - 'x': np.array(x_arr, dtype=object), - 'y': np.array(y_arr, dtype=object) + "x": np.array(x_arr, dtype=object), + "y": np.array(y_arr, dtype=object), } drawing = Drawing( @@ -103,66 +104,59 @@ def displot(data=None, **kwargs): def histplot(data=None, _process_hue=True, **kw): if data is None: - return + return None - if _process_hue and 'hue' in kw and type(kw['hue']) == str: - try: - kw['hue'] = data[kw['hue']] - except Exception: - pass + if _process_hue and "hue" in kw and type(kw["hue"]) == str: + with contextlib.suppress(Exception): + kw["hue"] = data[kw["hue"]] - if 'y' in kw: - try: - data = data[kw.pop('y')] - except Exception: - pass + if "y" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("y")] - if 'x' in kw: - try: - data = data[kw.pop('x')] - except Exception: - pass + if "x" in kw: + with contextlib.suppress(Exception): + data = data[kw.pop("x")] if type(data) == pd.DataFrame: for col in data.columns: histplot(data[col], **kw) - return - elif type(data) == pd.Series: + return None + if type(data) == pd.Series: return histplot(data.to_numpy(), **kw) - elif type(data) != np.ndarray: + if type(data) != np.ndarray: data = np.array(data, dtype=object) if len(data.shape) == 2: - import matplotlib.cbook as cbook - data = np.array(cbook._reshape_2D(data, 'x'), dtype=object) + from matplotlib import cbook + + data = np.array(cbook._reshape_2D(data, "x"), dtype=object) if len(data.shape) == 2: for i in range(data.shape[1]): histplot(data[:, i], **kw) - return + return None - if _process_hue and 'hue' in kw: - hue = kw['hue'] + if _process_hue and "hue" in kw: + hue = kw["hue"] colored_layers = sorted(set(hue), key=str) for pic in colored_layers: - subplot = [i for i, j in zip(data, hue) if j == pic] + subplot = [i for i, j in zip(data, hue, strict=False) if j == pic] histplot(np.array(subplot, dtype=object), _process_hue=False, **kw) - return + return None drawings.append( Drawing( DrawingLibrary.seaborn, DrawingType.hist, DrawingData(data, np.array([1] * len(data), dtype=object)), - kw + kw, ) ) + return None def lineplot(*, data=None, x=None, y=None, **kwargs): - if x is not None: - x_array = data[x].to_numpy() - else: - x_array = data.index.to_numpy() + x_array = data[x].to_numpy() if x is not None else data.index.to_numpy() if y is not None: y_array = data[y].to_numpy() @@ -189,13 +183,14 @@ def lineplot(*, data=None, x=None, y=None, **kwargs): kwargs, ) ) + return None - def lmplot(x=None, y=None, data=None, **kwargs): + def lmplot(x=None, y=None, data=None, **kwargs) -> None: curr_data = { # noqa: F841 - 'data': data, - 'x': x, - 'y': y, - 'kwargs': kwargs + "data": data, + "x": x, + "y": y, + "kwargs": kwargs, } drawing = Drawing( @@ -206,11 +201,12 @@ def lmplot(x=None, y=None, data=None, **kwargs): ) drawings.append(drawing) - def scatterplot(x=None, y=None, data=None, **kwargs): + def scatterplot(x=None, y=None, data=None, **kwargs) -> None: if x is not None and y is not None: drawings.append( DrawingBuilder.get_scatter_drawing( - data[x], data[y], + data[x], + data[y], DrawingLibrary.seaborn, kwargs, ) @@ -225,18 +221,19 @@ def scatterplot(x=None, y=None, data=None, **kwargs): x = data.index drawings.append( DrawingBuilder.get_scatter_drawing( - x, data[column], + x, + data[column], DrawingLibrary.seaborn, kwargs, ) ) - def catplot(x=None, y=None, data=None, **kwargs): + def catplot(x=None, y=None, data=None, **kwargs) -> None: curr_data = { # noqa: F841 - 'data': data, - 'x': x, - 'y': y, - 'kwargs': kwargs + "data": data, + "x": x, + "y": y, + "kwargs": kwargs, } drawing = Drawing( @@ -247,30 +244,23 @@ def catplot(x=None, y=None, data=None, **kwargs): ) drawings.append(drawing) - def barplot(x=None, y=None, data=None, **kwargs): - + def barplot(x=None, y=None, data=None, **kwargs) -> None: x_arr = np.array([], dtype=object) y_arr = np.array([], dtype=object) if data is not None: if x: x_arr = data[x].to_numpy() - y_arr = np.full((x_arr.size,), '', dtype=str) + y_arr = np.full((x_arr.size,), "", dtype=str) if y: y_arr = data[y].to_numpy() if x_arr.size == 0: - x_arr = np.full((y_arr.size,), '', dtype=str) + x_arr = np.full((y_arr.size,), "", dtype=str) drawings.append( - Drawing( - DrawingLibrary.seaborn, - DrawingType.bar, - DrawingData(x_arr, y_arr), - kwargs - ) + Drawing(DrawingLibrary.seaborn, DrawingType.bar, DrawingData(x_arr, y_arr), kwargs) ) - def violinplot(*, x=None, y=None, data=None, **kwargs): - + def violinplot(*, x=None, y=None, data=None, **kwargs) -> None: if data is not None: if x is None and y is not None: data = data[y] @@ -278,13 +268,12 @@ def violinplot(*, x=None, y=None, data=None, **kwargs): data = data[x] elif x is not None and y is not None: data = pd.concat([data[x], data[y]], axis=1).reset_index() + elif x is None: + data = y + elif y is None: + data = x else: - if x is None: - data = y - elif y is None: - data = x - else: - data = pd.concat([x, y], axis=1).reset_index() + data = pd.concat([x, y], axis=1).reset_index() drawing = Drawing( DrawingLibrary.seaborn, @@ -295,12 +284,12 @@ def violinplot(*, x=None, y=None, data=None, **kwargs): drawings.append(drawing) - def heatmap(data=None, **kwargs): + def heatmap(data=None, **kwargs) -> None: if data is None: return curr_data = { # noqa: F841 - 'x': np.array(data, dtype=object) + "x": np.array(data, dtype=object) } drawing = Drawing( @@ -312,13 +301,9 @@ def heatmap(data=None, **kwargs): drawings.append(drawing) - def boxplot(x=None, y=None, data=None, **kwargs): - + def boxplot(x=None, y=None, data=None, **kwargs) -> None: if data is None: - curr_data = { - 'x': np.array(x, dtype=object), - 'y': np.array(y, dtype=object) - } + curr_data = {"x": np.array(x, dtype=object), "y": np.array(y, dtype=object)} drawing = Drawing( DrawingLibrary.seaborn, @@ -339,8 +324,8 @@ def boxplot(x=None, y=None, data=None, **kwargs): continue curr_data = { - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() + "x": np.array([column], dtype=object), + "y": data[column].to_numpy(), } drawing = Drawing( @@ -358,8 +343,8 @@ def boxplot(x=None, y=None, data=None, **kwargs): y_arr = data[y].to_numpy() curr_data = { # noqa: F841 - 'x': np.array(x_arr, dtype=object), - 'y': np.array(y_arr, dtype=object) + "x": np.array(x_arr, dtype=object), + "y": np.array(y_arr, dtype=object), } drawing = Drawing( @@ -397,8 +382,7 @@ def boxplot(x=None, y=None, data=None, **kwargs): SeabornHandler._replaced = True @staticmethod - def revert_plots(): - + def revert_plots() -> None: if not SeabornHandler._replaced: return diff --git a/hstest/testing/process_wrapper.py b/hstest/testing/process_wrapper.py index 3b5866c1..c4988181 100644 --- a/hstest/testing/process_wrapper.py +++ b/hstest/testing/process_wrapper.py @@ -1,8 +1,10 @@ +from __future__ import annotations + +import os import subprocess import sys from threading import Lock, Thread from time import sleep -from typing import Optional from psutil import NoSuchProcess, Process @@ -19,17 +21,18 @@ class ProcessWrapper: initial_idle_wait = True initial_idle_wait_time = 150 - def __init__(self, *args, check_early_finish=False, register_output=True, - register_io_handler=False): + def __init__( + self, *args, check_early_finish=False, register_output=True, register_io_handler=False + ) -> None: self.lock = Lock() self.args = args - self.process: Optional[subprocess.Popen] = None - self.ps: Optional[Process] = None + self.process: subprocess.Popen | None = None + self.ps: Process | None = None - self.stdout = '' - self.stderr = '' + self.stdout = "" + self.stderr = "" self._alive = True self._pipes_watching = 0 self.terminated = False @@ -48,22 +51,32 @@ def __init__(self, *args, check_early_finish=False, register_output=True, self._group = None def start(self): - command = ' '.join(map(str, self.args)) + command = " ".join(map(str, self.args)) if self.process is not None: - raise UnexpectedError(f"Cannot start the same process twice\n\"{command}\"") + msg = f'Cannot start the same process twice\n"{command}"' + raise UnexpectedError(msg) try: args = [str(a) for a in self.args] - if is_windows(): - if args[0] == 'bash': - # bash doesn't like Windows' \r\n, - # so we use byte stream instead of text stream - # to communicate between processes - self._use_byte_stream = True + if is_windows() and args[0] == "bash": + # bash doesn't like Windows' \r\n, + # so we use byte stream instead of text stream + # to communicate between processes + self._use_byte_stream = True + + args = ["cmd", "/c", *args] - args = ['cmd', '/c'] + args + # Set environment variables for proper encoding on Windows + env = os.environ.copy() + if is_windows(): + env.update( + { + "PYTHONIOENCODING": "utf-8", + "PYTHONLEGACYWINDOWSSTDIO": "0", # Disable legacy stdio behavior on Windows + } + ) self.process = subprocess.Popen( args, @@ -72,12 +85,16 @@ def start(self): stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, - encoding='utf-8' if not self._use_byte_stream else None, + encoding="utf-8" if not self._use_byte_stream else None, + errors="replace", # Handle encoding errors gracefully + env=env, ) except Exception as e: from hstest import StageTest + StageTest.curr_test_run.set_error_in_test( - UnexpectedError(f"Cannot start process\n\"{command}\"", e)) + UnexpectedError(f'Cannot start process\n"{command}"', e) + ) self._alive = False self.terminated = True return self @@ -87,30 +104,29 @@ def start(self): if self.register_io_handler: self._group = ThreadGroup() SystemHandler.install_handler( - self, - lambda: ThreadGroup.curr_group() == self._group, - lambda: None + self, lambda: ThreadGroup.curr_group() == self._group, lambda: None ) - Thread(target=lambda: self.check_cpuload(), daemon=True, group=self._group).start() - Thread(target=lambda: self.check_output(), daemon=True, group=self._group).start() - Thread(target=lambda: self.check_stdout(), daemon=True, group=self._group).start() - Thread(target=lambda: self.check_stderr(), daemon=True, group=self._group).start() + Thread(target=self.check_cpuload, daemon=True, group=self._group).start() + Thread(target=self.check_output, daemon=True, group=self._group).start() + Thread(target=self.check_stdout, daemon=True, group=self._group).start() + Thread(target=self.check_stderr, daemon=True, group=self._group).start() return self - def check_alive(self): + def check_alive(self) -> None: if self._alive and self.process.returncode is not None: self._alive = False - def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=False): + def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=False) -> None: pipe_name = "stdout" if write_stdout else "stderr" with self.lock: self._pipes_watching += 1 - OutputHandler.print(f'Start watching {pipe_name} ' - f'Pipes watching = {self._pipes_watching}') + OutputHandler.print( + f"Start watching {pipe_name} " f"Pipes watching = {self._pipes_watching}" + ) while True: try: @@ -118,26 +134,25 @@ def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=Fal if self._use_byte_stream: new_output = new_output.decode() except ValueError: - OutputHandler.print(f'Value error for {pipe_name}... ') + OutputHandler.print(f"Value error for {pipe_name}... ") if self.is_finished(need_wait_output=False): break continue if write_stderr: - OutputHandler.print(f'STDERR + {len(new_output)} symbols: {new_output}') + OutputHandler.print(f"STDERR + {len(new_output)} symbols: {new_output}") if len(new_output) == 0: with self.lock: self._pipes_watching -= 1 OutputHandler.print( - f'Out of {pipe_name}... ' - f'Maybe program terminated. Pipes watching = {self._pipes_watching}' + f"Out of {pipe_name}... " + f"Maybe program terminated. Pipes watching = {self._pipes_watching}" ) if self._pipes_watching == 0: - OutputHandler.print( - f'Set alive = False for {pipe_name}... ') + OutputHandler.print(f"Set alive = False for {pipe_name}... ") self._alive = False self.terminate() @@ -147,7 +162,7 @@ def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=Fal if self.register_output: write_pipe.write(new_output) except ExitException: - OutputHandler.print(f'ExitException for {pipe_name}... ') + OutputHandler.print(f"ExitException for {pipe_name}... ") self._alive = False self.terminate() break @@ -158,17 +173,17 @@ def check_pipe(self, read_pipe, write_pipe, write_stdout=False, write_stderr=Fal if write_stderr: self.stderr += new_output - def check_stdout(self): + def check_stdout(self) -> None: self.check_pipe(self.process.stdout, sys.stdout, write_stdout=True) - def check_stderr(self): + def check_stderr(self) -> None: self.check_pipe(self.process.stderr, sys.stderr, write_stderr=True) - def check_cpuload(self): + def check_cpuload(self) -> None: while self._alive: try: cpu_load = self.ps.cpu_percent() - OutputHandler.print(f'Check cpuload - {cpu_load}') + OutputHandler.print(f"Check cpuload - {cpu_load}") if not self.initial_idle_wait: self.cpu_load_history.append(cpu_load) @@ -177,9 +192,9 @@ def check_cpuload(self): self.cpu_load_history.pop(0) except NoSuchProcess: - OutputHandler.print('Check cpuload finished, waiting output') + OutputHandler.print("Check cpuload finished, waiting output") self.wait_output() - OutputHandler.print('Check cpuload finished, set alive = false') + OutputHandler.print("Check cpuload finished, set alive = false") self._alive = False break @@ -191,7 +206,7 @@ def check_cpuload(self): if self.initial_idle_wait_time == 0: self.initial_idle_wait = False - def check_output(self): + def check_output(self) -> None: output_len_prev = len(self.stdout) while self._alive: @@ -200,7 +215,8 @@ def check_output(self): output_len_prev = output_len OutputHandler.print( - f'Check output diff - {diff}. Curr = {output_len}, prev = {output_len_prev}') + f"Check output diff - {diff}. Curr = {output_len}, prev = {output_len_prev}" + ) if not self.initial_idle_wait: self.output_diff_history.append(diff) @@ -219,24 +235,25 @@ def is_waiting_input(self) -> bool: return False program_not_loading_processor = ( - len(self.cpu_load_history) >= self.cpu_load_history_max and - sum(self.cpu_load_history) < 1 + len(self.cpu_load_history) >= self.cpu_load_history_max + and sum(self.cpu_load_history) < 1 ) program_not_printing_anything = ( - len(self.output_diff_history) >= self.output_diff_history_max and - sum(self.output_diff_history) == 0 + len(self.output_diff_history) >= self.output_diff_history_max + and sum(self.output_diff_history) == 0 ) return program_not_loading_processor and program_not_printing_anything - def register_input_request(self): + def register_input_request(self) -> None: if not self.is_waiting_input(): - raise RuntimeError('Program is not waiting for the input') + msg = "Program is not waiting for the input" + raise RuntimeError(msg) self.cpu_load_history = [] self.output_diff_history = [] - def is_finished(self, need_wait_output=True): + def is_finished(self, need_wait_output=True) -> bool: if not self.check_early_finish: return not self._alive @@ -244,7 +261,7 @@ def is_finished(self, need_wait_output=True): return True try: - is_running = self.ps.status() == 'running' + is_running = self.ps.status() == "running" if not is_running: self._alive = False except NoSuchProcess: @@ -257,54 +274,65 @@ def is_finished(self, need_wait_output=True): return not self._alive - def provide_input(self, stdin: str): - if self._use_byte_stream: - stdin = stdin.encode() - self.process.stdin.write(stdin) + def provide_input(self, stdin: str) -> None: + if not stdin.endswith("\n"): + stdin += "\n" + try: + if self._use_byte_stream: + stdin = stdin.encode("utf-8") + self.process.stdin.write(stdin) + self.process.stdin.flush() + else: + self.process.stdin.write(stdin) + self.process.stdin.flush() + except OSError: + # Handle pipe errors gracefully + if not self._alive: + return + raise - def terminate(self): - OutputHandler.print('Terminate called') + def terminate(self) -> None: + OutputHandler.print("Terminate called") with self.lock: - OutputHandler.print('Terminate - LOCK ACQUIRED') + OutputHandler.print("Terminate - LOCK ACQUIRED") if self.terminated: - OutputHandler.print('Terminate - finished') + OutputHandler.print("Terminate - finished") return - OutputHandler.print('Terminate - BEFORE WAIT STDERR') + OutputHandler.print("Terminate - BEFORE WAIT STDERR") self.wait_output() if self.register_io_handler: SystemHandler.uninstall_handler(self) - OutputHandler.print('Terminate - AFTER WAIT STDERR') + OutputHandler.print("Terminate - AFTER WAIT STDERR") self._alive = False - OutputHandler.print('Terminate - SELF ALIVE == FALSE') + OutputHandler.print("Terminate - SELF ALIVE == FALSE") is_exit_replaced = ExitHandler.is_replaced() if is_exit_replaced: ExitHandler.revert_exit() - OutputHandler.print('Terminate - EXIT REVERTED') + OutputHandler.print("Terminate - EXIT REVERTED") try: parent = Process(self.process.pid) - OutputHandler.print(f'Terminate - parent == {parent}') + OutputHandler.print(f"Terminate - parent == {parent}") for child in parent.children(recursive=True): - OutputHandler.print(f'Terminate - child kill {child}') + OutputHandler.print(f"Terminate - child kill {child}") child.kill() - OutputHandler.print(f'Terminate - parent kill {parent}') + OutputHandler.print(f"Terminate - parent kill {parent}") parent.kill() except NoSuchProcess: - OutputHandler.print('Terminate - NO SUCH PROCESS') - pass + OutputHandler.print("Terminate - NO SUCH PROCESS") finally: - OutputHandler.print('Terminate - finally before kill') + OutputHandler.print("Terminate - finally before kill") self.process.kill() - OutputHandler.print('Terminate - finally before wait') + OutputHandler.print("Terminate - finally before wait") self.process.wait() self.process.stdout.close() @@ -313,13 +341,13 @@ def terminate(self): if is_exit_replaced: ExitHandler.replace_exit() - OutputHandler.print('Terminate - EXIT REPLACED AGAIN') + OutputHandler.print("Terminate - EXIT REPLACED AGAIN") self.terminated = True - OutputHandler.print('Terminate - TERMINATED') - OutputHandler.print('Terminate - finished') + OutputHandler.print("Terminate - TERMINATED") + OutputHandler.print("Terminate - finished") - def wait_output(self): + def wait_output(self) -> None: iterations = 50 sleep_time = 50 / 1000 @@ -333,14 +361,12 @@ def wait_output(self): curr_stdout = self.stdout iterations -= 1 - def wait(self): + def wait(self) -> None: while not self.is_finished(): sleep(0.01) self.wait_output() def is_error_happened(self) -> bool: return ( - not self._alive and len(self.stderr) > 0 and - self.process.returncode != 0 or - 'Traceback' in self.stderr - ) + not self._alive and len(self.stderr) > 0 and self.process.returncode != 0 + ) or "Traceback" in self.stderr diff --git a/hstest/testing/runner/async_dynamic_testing_runner.py b/hstest/testing/runner/async_dynamic_testing_runner.py index 69faabda..974a910d 100644 --- a/hstest/testing/runner/async_dynamic_testing_runner.py +++ b/hstest/testing/runner/async_dynamic_testing_runner.py @@ -1,28 +1,30 @@ +from __future__ import annotations + import typing from concurrent.futures import Future, TimeoutError -from typing import Optional, Type from hstest.common.process_utils import DaemonThreadPoolExecutor from hstest.dynamic.output.output_handler import OutputHandler from hstest.exception.testing import ( - TestedProgramFinishedEarly, TestedProgramThrewException, TimeLimitException + TestedProgramFinishedEarly, + TestedProgramThrewException, + TimeLimitException, ) from hstest.exceptions import TestPassed, WrongAnswer from hstest.test_case.check_result import CheckResult, correct, wrong from hstest.testing.execution.main_module_executor import MainModuleExecutor -from hstest.testing.execution.program_executor import ProgramExecutor from hstest.testing.execution_options import debug_mode from hstest.testing.runner.test_runner import TestRunner -from hstest.testing.test_run import TestRun if typing.TYPE_CHECKING: from hstest import TestCase + from hstest.testing.execution.program_executor import ProgramExecutor + from hstest.testing.test_run import TestRun class AsyncDynamicTestingRunner(TestRunner): - - def __init__(self, executor: Type[ProgramExecutor] = MainModuleExecutor): - self.executor: Type[ProgramExecutor] = executor + def __init__(self, executor: type[ProgramExecutor] = MainModuleExecutor) -> None: + self.executor: type[ProgramExecutor] = executor def _run_dynamic_test(self, test_run: TestRun) -> CheckResult: test_case = test_run.test_case @@ -40,7 +42,7 @@ def _run_dynamic_test(self, test_run: TestRun) -> CheckResult: return result - def _run_file(self, test_run: TestRun) -> Optional[CheckResult]: + def _run_file(self, test_run: TestRun) -> CheckResult | None: test_case = test_run.test_case time_limit = test_case.time_limit @@ -49,8 +51,7 @@ def _run_file(self, test_run: TestRun) -> Optional[CheckResult]: future: Future = executor.submit(lambda: self._run_dynamic_test(test_run)) if time_limit <= 0 or debug_mode: return future.result() - else: - return future.result(timeout=time_limit / 1000) + return future.result(timeout=time_limit / 1000) except TimeoutError: test_run.set_error_in_test(TimeLimitException(time_limit)) except BaseException as ex: @@ -61,7 +62,7 @@ def _run_file(self, test_run: TestRun) -> Optional[CheckResult]: return None - def test(self, test_run: TestRun) -> Optional[CheckResult]: + def test(self, test_run: TestRun) -> CheckResult | None: test_case = test_run.test_case result: CheckResult = self._run_file(test_run) @@ -71,8 +72,7 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: if error is None: try: - return test_case.check_func( - OutputHandler.get_output(), test_case.attach) + return test_case.check_func(OutputHandler.get_output(), test_case.attach) except BaseException as ex: error = ex test_run.set_error_in_test(error) @@ -81,7 +81,8 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: return result - def tear_down(self, test_case: 'TestCase'): + def tear_down(self, test_case: TestCase) -> None: from hstest import StageTest + for program in StageTest.curr_test_run.tested_programs: program.executor.tear_down() diff --git a/hstest/testing/runner/django_application_runner.py b/hstest/testing/runner/django_application_runner.py index 6de0259a..6e7529cd 100644 --- a/hstest/testing/runner/django_application_runner.py +++ b/hstest/testing/runner/django_application_runner.py @@ -1,44 +1,49 @@ +from __future__ import annotations + import os import sys from time import sleep -from typing import List, Optional +from typing import TYPE_CHECKING from hstest.common.file_utils import safe_delete from hstest.common.process_utils import is_port_in_use from hstest.exception.outcomes import ErrorWithFeedback, ExceptionWithFeedback, UnexpectedError from hstest.test_case.attach.django_settings import DjangoSettings from hstest.test_case.check_result import CheckResult -from hstest.test_case.test_case import TestCase from hstest.testing.execution.filtering.file_filter import FileFilter from hstest.testing.execution.searcher.python_searcher import PythonSearcher from hstest.testing.process_wrapper import ProcessWrapper from hstest.testing.runner.test_runner import TestRunner -from hstest.testing.test_run import TestRun + +if TYPE_CHECKING: + from hstest.test_case.test_case import TestCase + from hstest.testing.test_run import TestRun class DjangoApplicationRunner(TestRunner): process: ProcessWrapper = None - port: Optional[int] = None - full_path: Optional[str] = None + port: int | None = None + full_path: str | None = None - def launch_django_application(self, test_case: TestCase): + def launch_django_application(self, test_case: TestCase) -> None: if not isinstance(test_case.attach, DjangoSettings): - raise UnexpectedError( - f'Django tests should have DjangoSettings class as an attach, ' - f'found {type(test_case.attach)}') + msg = ( + f"Django tests should have DjangoSettings class as an attach, " + f"found {type(test_case.attach)}" + ) + raise UnexpectedError(msg) source = test_case.source_name if source is None or not len(source): - source = 'manage' + source = "manage" - full_source = source.replace('.', os.sep) + '.py' + full_source = source.replace(".", os.sep) + ".py" full_path = os.path.abspath(full_source) if not os.path.exists(full_path): filename = os.path.basename(full_source) - runnable = PythonSearcher().search( - file_filter=FileFilter(file=lambda f: f == filename)) + runnable = PythonSearcher().search(file_filter=FileFilter(file=lambda f: f == filename)) full_path = os.path.abspath(runnable.folder + os.sep + runnable.file) self.full_path = full_path @@ -48,11 +53,16 @@ def launch_django_application(self, test_case: TestCase): self.__prepare_database(test_case.attach.test_database) self.process = ProcessWrapper( - sys.executable, self.full_path, 'runserver', self.port, '--noreload', - register_io_handler=True).start() + sys.executable, + self.full_path, + "runserver", + self.port, + "--noreload", + register_io_handler=True, + ).start() i: int = 100 - search_phrase = 'Starting development server at' + search_phrase = "Starting development server at" while i: if search_phrase in self.process.stdout: test_case.attach.port = self.port @@ -68,30 +78,32 @@ def launch_django_application(self, test_case: TestCase): stderr = self.process.stderr.strip() error_info = ( - f'Cannot start Django server because cannot find ' + f"Cannot start Django server because cannot find " f'"{search_phrase}" in process\' output' ) if len(stdout): - error_info += '\n\nstdout:\n' + stdout + error_info += "\n\nstdout:\n" + stdout if len(stderr): - error_info += '\n\nstderr:\n' + stderr + error_info += "\n\nstderr:\n" + stderr raise ErrorWithFeedback(error_info) - def __find_free_port(self, ports: List[int]) -> int: + def __find_free_port(self, ports: list[int]) -> int: for port in ports: if not is_port_in_use(port): return port - raise ErrorWithFeedback( - 'Cannot find a port to start Django application ' - f'(tried ports form {ports[0]} to {ports[-1]})') - - def __prepare_database(self, test_database: str): - os.environ['HYPERSKILL_TEST_DATABASE'] = test_database - with open(test_database, 'w'): + msg = ( + "Cannot find a port to start Django application " + f"(tried ports form {ports[0]} to {ports[-1]})" + ) + raise ErrorWithFeedback(msg) + + def __prepare_database(self, test_database: str) -> None: + os.environ["HYPERSKILL_TEST_DATABASE"] = test_database + with open(test_database, "w", encoding="utf-8"): pass - migrate = ProcessWrapper(sys.executable, self.full_path, 'migrate', check_early_finish=True) + migrate = ProcessWrapper(sys.executable, self.full_path, "migrate", check_early_finish=True) migrate.start() while not migrate.is_finished() and len(migrate.stderr) == 0: @@ -100,18 +112,21 @@ def __prepare_database(self, test_database: str): if len(migrate.stderr) != 0: migrate.wait_output() - if ('ModuleNotFoundError' in migrate.stderr or - 'ImportError' in migrate.stderr or - 'SyntaxError' in migrate.stderr): + if ( + "ModuleNotFoundError" in migrate.stderr + or "ImportError" in migrate.stderr + or "SyntaxError" in migrate.stderr + ): raise ExceptionWithFeedback(migrate.stderr, None) # stdout and stderr is collected and will be shown to the user - raise ErrorWithFeedback('Cannot apply migrations to an empty database.') + msg = "Cannot apply migrations to an empty database." + raise ErrorWithFeedback(msg) - def set_up(self, test_case: TestCase): + def set_up(self, test_case: TestCase) -> None: self.launch_django_application(test_case) - def tear_down(self, test_case: TestCase): + def tear_down(self, test_case: TestCase) -> None: self._check_errors() if isinstance(test_case.attach, DjangoSettings): @@ -119,12 +134,12 @@ def tear_down(self, test_case: TestCase): if self.process: self.process.terminate() - def _check_errors(self): + def _check_errors(self) -> None: if self.process.is_error_happened(): self.process.terminate() raise ErrorWithFeedback(self.process.stderr) - def test(self, test_run: TestRun) -> Optional[CheckResult]: + def test(self, test_run: TestRun) -> CheckResult | None: self._check_errors() test_case = test_run.test_case diff --git a/hstest/testing/runner/flask_application_runner.py b/hstest/testing/runner/flask_application_runner.py index 0a3391ff..5b870bf4 100644 --- a/hstest/testing/runner/flask_application_runner.py +++ b/hstest/testing/runner/flask_application_runner.py @@ -1,56 +1,65 @@ +from __future__ import annotations + import os import sys from time import sleep -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING from hstest.common.process_utils import is_port_in_use from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError from hstest.test_case.attach.flask_settings import FlaskSettings from hstest.test_case.check_result import CheckResult -from hstest.test_case.test_case import TestCase from hstest.testing.process_wrapper import ProcessWrapper from hstest.testing.runner.test_runner import TestRunner -from hstest.testing.test_run import TestRun + +if TYPE_CHECKING: + from hstest.test_case.test_case import TestCase + from hstest.testing.test_run import TestRun class FlaskApplicationRunner(TestRunner): - processes: List[Tuple[str, ProcessWrapper]] = [] + processes: list[tuple[str, ProcessWrapper]] = [] - def launch_flask_applications(self, test_case: TestCase): + def launch_flask_applications(self, test_case: TestCase) -> None: if not isinstance(test_case.attach, FlaskSettings): - raise UnexpectedError( - f'Flask tests should have FlaskSettings class as an attach, ' - f'found {type(test_case.attach)}') + msg = ( + f"Flask tests should have FlaskSettings class as an attach, " + f"found {type(test_case.attach)}" + ) + raise UnexpectedError(msg) sources = test_case.attach.sources if len(sources) == 0: - raise UnexpectedError( - 'Cannot find Flask applications to run, no sources were defined in tests') + msg = "Cannot find Flask applications to run, no sources were defined in tests" + raise UnexpectedError(msg) new_sources = [] for source in sources: filename, port = source - full_source = filename.replace('.', os.sep) + '.py' + full_source = filename.replace(".", os.sep) + ".py" full_path = os.path.abspath(full_source) if not os.path.exists(full_path): - raise ErrorWithFeedback( + msg = ( f'Cannot find file named "{os.path.basename(full_path)}" ' f'in folder "{os.path.dirname(full_path)}". ' - f'Check if you deleted it.') + f"Check if you deleted it." + ) + raise ErrorWithFeedback(msg) if port is None: port = self.__find_free_port(test_case.attach.tryout_ports) process = ProcessWrapper( - sys.executable, full_path, f'localhost:{port}', register_io_handler=True).start() + sys.executable, full_path, f"localhost:{port}", register_io_handler=True + ).start() self.processes += [(full_source, process)] i: int = 100 - search_phrase = 'Press CTRL+C to quit' + search_phrase = "Press CTRL+C to quit" while i: if search_phrase in process.stderr: break @@ -65,14 +74,14 @@ def launch_flask_applications(self, test_case: TestCase): stderr = process.stderr.strip() error_info = ( - f'Cannot start Flask server {full_source} ' + f"Cannot start Flask server {full_source} " f'because cannot find "{search_phrase}" in process\' output' ) if len(stdout): - error_info += '\n\nstdout:\n' + stdout + error_info += "\n\nstdout:\n" + stdout if len(stderr): - error_info += '\n\nstderr:\n' + stderr + error_info += "\n\nstderr:\n" + stderr raise ErrorWithFeedback(error_info) @@ -80,29 +89,32 @@ def launch_flask_applications(self, test_case: TestCase): test_case.attach.sources = new_sources - def __find_free_port(self, ports: List[int]) -> int: + def __find_free_port(self, ports: list[int]) -> int: for port in ports: if not is_port_in_use(port): return port - raise ErrorWithFeedback( - 'Cannot find a port to start Flask application ' - f'(tried ports form {ports[0]} to {ports[-1]})') + msg = ( + "Cannot find a port to start Flask application " + f"(tried ports form {ports[0]} to {ports[-1]})" + ) + raise ErrorWithFeedback(msg) - def set_up(self, test_case: TestCase): + def set_up(self, test_case: TestCase) -> None: self.launch_flask_applications(test_case) - def tear_down(self, test_case: TestCase): + def tear_down(self, test_case: TestCase) -> None: for process_item in self.processes: - filename, process = process_item + _filename, process = process_item process.terminate() - def _check_errors(self): + def _check_errors(self) -> None: for process_item in self.processes: filename, process = process_item if process.is_error_happened(): - raise ErrorWithFeedback(f'Error running "{filename}"\n\n{process.stderr}') + msg = f'Error running "{filename}"\n\n{process.stderr}' + raise ErrorWithFeedback(msg) - def test(self, test_run: TestRun) -> Optional[CheckResult]: + def test(self, test_run: TestRun) -> CheckResult | None: self._check_errors() test_case = test_run.test_case diff --git a/hstest/testing/runner/plot_testing_runner.py b/hstest/testing/runner/plot_testing_runner.py index a4663dc1..b3856be7 100644 --- a/hstest/testing/runner/plot_testing_runner.py +++ b/hstest/testing/runner/plot_testing_runner.py @@ -1,6 +1,7 @@ -from typing import List, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING -from hstest.testing.plotting.drawing.drawing import Drawing from hstest.testing.plotting.matplotlib_handler import MatplotlibHandler from hstest.testing.plotting.pandas_handler import PandasHandler from hstest.testing.plotting.seaborn_handler import SeabornHandler @@ -8,47 +9,42 @@ if TYPE_CHECKING: from hstest import TestCase + from hstest.testing.plotting.drawing.drawing import Drawing class DrawingsStorage: - def __init__(self, - all_drawings: List[Drawing], - new_drawings: List[Drawing]): - self.all_drawings: List[Drawing] = all_drawings - self.new_drawings: List[Drawing] = new_drawings + def __init__(self, all_drawings: list[Drawing], new_drawings: list[Drawing]) -> None: + self.all_drawings: list[Drawing] = all_drawings + self.new_drawings: list[Drawing] = new_drawings - def append(self, drawing: Drawing): + def append(self, drawing: Drawing) -> None: self.all_drawings.append(drawing) self.new_drawings.append(drawing) - def extend(self, drawings: List[Drawing]): + def extend(self, drawings: list[Drawing]) -> None: self.all_drawings.extend(drawings) self.new_drawings.extend(drawings) class PlottingTestingRunner(AsyncDynamicTestingRunner): - - def __init__(self, - all_drawings: List[Drawing], - new_drawings: List[Drawing]): + def __init__(self, all_drawings: list[Drawing], new_drawings: list[Drawing]) -> None: super().__init__() - self.drawings_storage: DrawingsStorage = DrawingsStorage( - all_drawings, new_drawings) + self.drawings_storage: DrawingsStorage = DrawingsStorage(all_drawings, new_drawings) - def set_up(self, test_case: 'TestCase'): + def set_up(self, test_case: TestCase) -> None: super().set_up(test_case) self.replace_plots() - def tear_down(self, test_case: 'TestCase'): + def tear_down(self, test_case: TestCase) -> None: super().tear_down(test_case) self.revert_plots() - def replace_plots(self): + def replace_plots(self) -> None: MatplotlibHandler.replace_plots(self.drawings_storage) PandasHandler.replace_plots(self.drawings_storage) SeabornHandler.replace_plots(self.drawings_storage) - def revert_plots(self): + def revert_plots(self) -> None: MatplotlibHandler.revert_plots() PandasHandler.revert_plots() SeabornHandler.revert_plots() diff --git a/hstest/testing/runner/sql_runner.py b/hstest/testing/runner/sql_runner.py index 7cce21df..0fb928b6 100644 --- a/hstest/testing/runner/sql_runner.py +++ b/hstest/testing/runner/sql_runner.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +import contextlib import os import re import sqlite3 @@ -13,53 +16,51 @@ class SQLRunner(TestRunner): - - def __init__(self, sql_test_cls): + def __init__(self, sql_test_cls) -> None: self.sql_test_cls = sql_test_cls - super(SQLRunner, self).__init__() + super().__init__() - def test(self, test_run: 'TestRun'): + def test(self, test_run: TestRun): test_case = test_run.test_case try: - result = test_case.dynamic_testing() - return result + return test_case.dynamic_testing() except BaseException as ex: test_run.set_error_in_test(ex) return CheckResult.from_error(test_run.error_in_test) - def set_up(self, test_case: 'TestCase'): + def set_up(self, test_case: TestCase) -> None: self.parse_sql_file() self.set_up_database() - def set_up_database(self): + def set_up_database(self) -> None: if self.sql_test_cls.db is not None: return - self.sql_test_cls.db = sqlite3.connect(':memory:') + self.sql_test_cls.db = sqlite3.connect(":memory:") def parse_sql_file(self) -> None: sql_file = SQLSearcher().search() file_path = os.path.join(sql_file.folder, sql_file.file) - with open(file_path, 'r') as file: + with open(file_path, encoding="utf-8") as file: lines = file.readlines() sql_content = " ".join(lines).replace("\n", "") commands = re.findall(r'(\w+)\s+?=\s+?"(.*?)"', sql_content) - for (name, query) in commands: + for name, query in commands: if not query: - raise WrongAnswer(f"The '{name}' query shouldn't be empty!") + msg = f"The '{name}' query shouldn't be empty!" + raise WrongAnswer(msg) if name in self.sql_test_cls.queries: self.sql_test_cls.queries[name] = query for name in self.sql_test_cls.queries: if self.sql_test_cls.queries[name] is None: - raise WrongAnswer(f"Can't find '{name}' query from SQL files!") + msg = f"Can't find '{name}' query from SQL files!" + raise WrongAnswer(msg) - def tear_down(self, test_case: 'TestCase'): - try: + def tear_down(self, test_case: TestCase) -> None: + with contextlib.suppress(Exception): self.sql_test_cls.db.close() - except Exception: - pass diff --git a/hstest/testing/runner/test_runner.py b/hstest/testing/runner/test_runner.py index b0d70d75..05decb78 100644 --- a/hstest/testing/runner/test_runner.py +++ b/hstest/testing/runner/test_runner.py @@ -1,19 +1,20 @@ -import typing -from typing import Optional +from __future__ import annotations -from hstest.check_result import CheckResult +import typing if typing.TYPE_CHECKING: + from hstest.check_result import CheckResult from hstest.test_case.test_case import TestCase from hstest.testing.test_run import TestRun class TestRunner: - def set_up(self, test_case: 'TestCase'): + def set_up(self, test_case: TestCase) -> None: pass - def tear_down(self, test_case: 'TestCase'): + def tear_down(self, test_case: TestCase) -> None: pass - def test(self, test_run: 'TestRun') -> Optional[CheckResult]: - raise NotImplementedError("Test method is not implemented") + def test(self, test_run: TestRun) -> CheckResult | None: + msg = "Test method is not implemented" + raise NotImplementedError(msg) diff --git a/hstest/testing/settings.py b/hstest/testing/settings.py index d8fac536..5ed56072 100644 --- a/hstest/testing/settings.py +++ b/hstest/testing/settings.py @@ -1,7 +1,11 @@ +from __future__ import annotations + + class Settings: do_reset_output: bool = True allow_out_of_input: bool = False catch_stderr: bool = True - def __init__(self): - raise NotImplementedError('Instances of the class Settings are prohibited') + def __init__(self) -> None: + msg = "Instances of the class Settings are prohibited" + raise NotImplementedError(msg) diff --git a/hstest/testing/state_machine.py b/hstest/testing/state_machine.py index 8c013a8c..f693a8a7 100644 --- a/hstest/testing/state_machine.py +++ b/hstest/testing/state_machine.py @@ -1,16 +1,21 @@ +from __future__ import annotations + from threading import Condition -from typing import Any, Callable, Dict, Set +from typing import Any, TYPE_CHECKING from hstest.exception.outcomes import UnexpectedError +if TYPE_CHECKING: + from collections.abc import Callable + class StateMachine: - def __init__(self, initial_value: Any): + def __init__(self, initial_value: Any) -> None: self._state: Any = initial_value - self._transitions: Dict[Any, Set[Any]] = {} + self._transitions: dict[Any, set[Any]] = {} self.cv = Condition() - def add_transition(self, fr: Any, to: Any): + def add_transition(self, fr: Any, to: Any) -> None: if fr not in self._transitions: self._transitions[fr] = set() self._transitions[fr].add(to) @@ -22,7 +27,7 @@ def state(self) -> Any: def in_state(self, state: Any): return self.state == state - def set_and_wait(self, new_state: Any, waiting_state: Any = None): + def set_and_wait(self, new_state: Any, waiting_state: Any = None) -> None: with self.cv: self.set_state(new_state) @@ -31,32 +36,29 @@ def set_and_wait(self, new_state: Any, waiting_state: Any = None): else: self.wait_state(waiting_state) - def wait_state(self, waiting_state: Any): + def wait_state(self, waiting_state: Any) -> None: with self.cv: self._wait_while(lambda: self.state != waiting_state) - def wait_not_state(self, state_to_avoid: Any): + def wait_not_state(self, state_to_avoid: Any) -> None: with self.cv: self._wait_while(lambda: self.state == state_to_avoid) - def wait_not_states(self, *states_to_avoid: Any): - def wait_func(): - for curr_state in states_to_avoid: - if self.state == curr_state: - return True - return False + def wait_not_states(self, *states_to_avoid: Any) -> None: + def wait_func() -> bool: + return any(self.state == curr_state for curr_state in states_to_avoid) + with self.cv: self._wait_while(wait_func) - def _wait_while(self, check_wait: Callable[[], bool]): + def _wait_while(self, check_wait: Callable[[], bool]) -> None: with self.cv: while check_wait(): self.cv.wait() - def set_state(self, new_state: Any): + def set_state(self, new_state: Any) -> None: with self.cv: if new_state not in self._transitions[self.state]: - raise UnexpectedError( - "Cannot transit from " + self.state + " to " + new_state) + raise UnexpectedError("Cannot transit from " + self.state + " to " + new_state) self._state = new_state self.cv.notify_all() diff --git a/hstest/testing/test_run.py b/hstest/testing/test_run.py index ff6ae089..53487b22 100644 --- a/hstest/testing/test_run.py +++ b/hstest/testing/test_run.py @@ -1,4 +1,6 @@ -from typing import List, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.check_result import CheckResult, correct from hstest.common.file_utils import create_files, delete_files @@ -6,23 +8,26 @@ from hstest.dynamic.system_handler import SystemHandler from hstest.exception.outcomes import ExceptionWithFeedback, UnexpectedError from hstest.exceptions import TestPassed -from hstest.test_case.test_case import TestCase -from hstest.testing.runner.test_runner import TestRunner from hstest.testing.settings import Settings -from hstest.testing.tested_program import TestedProgram + +if TYPE_CHECKING: + from hstest.test_case.test_case import TestCase + from hstest.testing.runner.test_runner import TestRunner + from hstest.testing.tested_program import TestedProgram class TestRun: - def __init__(self, test_num: int, test_count: int, - test_case: TestCase, test_rummer: TestRunner): + def __init__( + self, test_num: int, test_count: int, test_case: TestCase, test_rummer: TestRunner + ) -> None: self._test_num: int = test_num self._test_count: int = test_count self._test_case: TestCase = test_case self._test_runner: TestRunner = test_rummer self._input_used: bool = False - self._error_in_test: Optional[BaseException] = None - self._tested_programs: List[TestedProgram] = [] + self._error_in_test: BaseException | None = None + self._tested_programs: list[TestedProgram] = [] def is_first_test(self) -> bool: return self._test_num == 1 @@ -51,35 +56,35 @@ def input_used(self) -> bool: return self._input_used @property - def tested_programs(self) -> List[TestedProgram]: + def tested_programs(self) -> list[TestedProgram]: return self._tested_programs @property - def error_in_test(self) -> Optional[BaseException]: + def error_in_test(self) -> BaseException | None: return self._error_in_test - def set_error_in_test(self, err: Optional[BaseException]): + def set_error_in_test(self, err: BaseException | None) -> None: if self._error_in_test is None or err is None: self._error_in_test = err - def set_input_used(self): + def set_input_used(self) -> None: self._input_used = True - def add_tested_program(self, tested_program: TestedProgram): + def add_tested_program(self, tested_program: TestedProgram) -> None: self._tested_programs += [tested_program] - def stop_tested_programs(self): + def stop_tested_programs(self) -> None: for tested_program in self._tested_programs: tested_program.stop() - def invalidate_handlers(self): + def invalidate_handlers(self) -> None: for tested_program in self._tested_programs: SystemHandler.uninstall_handler(tested_program.executor) - def set_up(self): + def set_up(self) -> None: self._test_runner.set_up(self._test_case) - def tear_down(self): + def tear_down(self) -> None: self._test_runner.tear_down(self._test_case) def test(self) -> CheckResult: @@ -105,11 +110,12 @@ def test(self) -> CheckResult: result = correct() if result is None: - raise UnexpectedError("Result is None after testing") + msg = "Result is None after testing" + raise UnexpectedError(msg) return result - def _check_errors(self): + def _check_errors(self) -> None: error_in_test = self._error_in_test test_case = self._test_case @@ -134,7 +140,7 @@ def _check_errors(self): if hint_in_feedback: raise ExceptionWithFeedback( - feedback + '\n\n' + error_in_test.error_text, None + feedback + "\n\n" + error_in_test.error_text, None ) raise error_in_test diff --git a/hstest/testing/tested_program.py b/hstest/testing/tested_program.py index c7f33e9a..fd88af5a 100644 --- a/hstest/testing/tested_program.py +++ b/hstest/testing/tested_program.py @@ -1,27 +1,34 @@ -from typing import List, Optional, Type +from __future__ import annotations + +from typing import TYPE_CHECKING from hstest.exception.outcomes import UnexpectedError -from hstest.testing.execution.program_executor import ProgramExecutor + +if TYPE_CHECKING: + from hstest.testing.execution.program_executor import ProgramExecutor class TestedProgram: - def __init__(self, source: str = None): + def __init__(self, source: str | None = None) -> None: from hstest import StageTest + runner = StageTest.curr_test_run.test_runner from hstest.testing.runner.async_dynamic_testing_runner import AsyncDynamicTestingRunner + if not isinstance(runner, AsyncDynamicTestingRunner): raise UnexpectedError( - 'TestedProgram is supported only while using AsyncDynamicTestingRunner runner, ' - 'not ' + str(type(runner)) + "TestedProgram is supported only while using AsyncDynamicTestingRunner runner, " + "not " + str(type(runner)) ) if source is None: from hstest.stage_test import StageTest + source = StageTest.curr_test_run.test_case.source_name self._program_executor: ProgramExecutor = runner.executor(source) - self._run_args: Optional[List[str]] = None + self._run_args: list[str] | None = None @property def run_args(self): @@ -31,17 +38,19 @@ def run_args(self): def executor(self): return self._program_executor - def _init_program(self, *args: str): + def _init_program(self, *args: str) -> None: self._run_args = args from hstest.stage_test import StageTest + if StageTest.curr_test_run: StageTest.curr_test_run.add_tested_program(self) - def feedback_on_exception(self, ex: Type[Exception], feedback: str): + def feedback_on_exception(self, ex: type[Exception], feedback: str) -> None: from hstest import StageTest + StageTest.curr_test_run.test_case.feedback_on_exception[ex] = feedback - def start_in_background(self, *args: str): + def start_in_background(self, *args: str) -> None: self._init_program(*args) self._program_executor.start_in_background(*args) @@ -49,22 +58,22 @@ def start(self, *args: str) -> str: self._init_program(*args) return self._program_executor.start(*args) - def execute(self, stdin: Optional[str]) -> str: + def execute(self, stdin: str | None) -> str: return self._program_executor.execute(stdin) def get_output(self) -> str: return self._program_executor.get_output() - def stop(self): + def stop(self) -> None: self._program_executor.stop() def is_finished(self) -> bool: return self._program_executor.is_finished() - def set_return_output_after_execution(self, value: bool): + def set_return_output_after_execution(self, value: bool) -> None: self._program_executor.set_return_output_after_execution(value) - def stop_input(self): + def stop_input(self) -> None: self._program_executor.stop_input() def is_input_allowed(self) -> bool: @@ -73,10 +82,10 @@ def is_input_allowed(self) -> bool: def is_waiting_input(self) -> bool: return self._program_executor.is_waiting_input() - def go_background(self): + def go_background(self) -> None: self._program_executor.go_background() - def stop_background(self): + def stop_background(self) -> None: self._program_executor.stop_background() def is_in_background(self) -> bool: diff --git a/hstest/testing/unittest/expected_fail_test.py b/hstest/testing/unittest/expected_fail_test.py index 75987c6d..50f5baa9 100644 --- a/hstest/testing/unittest/expected_fail_test.py +++ b/hstest/testing/unittest/expected_fail_test.py @@ -1,21 +1,28 @@ +from __future__ import annotations + +import re from inspect import cleandoc -from typing import List, Union -from hstest import StageTest +from hstest.stage.stage_test import StageTest class ExpectedFailTest(StageTest): - _base_contain: Union[str, List[str]] = [] - _base_not_contain: Union[str, List[str]] = [] + _base_contain: str | list[str] = [] + _base_not_contain: str | list[str] = [] - contain: Union[str, List[str]] = [] - not_contain: Union[str, List[str]] = [] + contain: str | list[str] = [] + not_contain: str | list[str] = [] - def __init__(self, args): + def __init__(self, args) -> None: super().__init__(args) - def test_run_unittest(self): + def normalize_error_message(self, message: str) -> str: + # Remove error pointer markers added in Python 3.11+ + message = re.sub(r"\s+[~^]+\s*", " ", message) + # Normalize whitespace and line breaks + return " ".join(message.split()) + def test_run_unittest(self) -> None: if not self.contain and not self.not_contain: self.fail("'contain' or 'not_contain' should not be empty") @@ -35,8 +42,20 @@ def test_run_unittest(self): should_contain = self._base_contain + self.contain should_not_contain = self._base_not_contain + self.not_contain + normalized_feedback = self.normalize_error_message(feedback) + for item in should_contain: - self.assertIn(cleandoc(item), feedback) + normalized_item = self.normalize_error_message(cleandoc(item)) + self.assertIn( + normalized_item, + normalized_feedback, + f"Expected to find:\n{normalized_item}\nin:\n{normalized_feedback}", + ) for item in should_not_contain: - self.assertNotIn(cleandoc(item), feedback) + normalized_item = self.normalize_error_message(cleandoc(item)) + self.assertNotIn( + normalized_item, + normalized_feedback, + f"Expected NOT to find:\n{normalized_item}\nin:\n{normalized_feedback}", + ) diff --git a/hstest/testing/unittest/unexepected_error_test.py b/hstest/testing/unittest/unexepected_error_test.py index 36492d4f..3433da0c 100644 --- a/hstest/testing/unittest/unexepected_error_test.py +++ b/hstest/testing/unittest/unexepected_error_test.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from hstest.testing.unittest.expected_fail_test import ExpectedFailTest class UnexpectedErrorTest(ExpectedFailTest): - _base_contain = 'Unexpected error' + _base_contain = "Unexpected error" diff --git a/hstest/testing/unittest/user_error_test.py b/hstest/testing/unittest/user_error_test.py index 1f7b4d9f..71310f1c 100644 --- a/hstest/testing/unittest/user_error_test.py +++ b/hstest/testing/unittest/user_error_test.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from hstest.testing.unittest.expected_fail_test import ExpectedFailTest class UserErrorTest(ExpectedFailTest): - _base_not_contain = 'Unexpected error' + _base_not_contain = "Unexpected error" diff --git a/poetry.lock b/poetry.lock new file mode 100755 index 00000000..0f6817b6 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1121 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.3.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, + {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, + {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, + {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, + {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, + {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, + {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, + {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, + {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, + {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, + {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, +] + +[package.dependencies] +numpy = ">=1.23" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fonttools" +version = "4.56.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"}, + {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"}, + {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311"}, + {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc"}, + {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f"}, + {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086"}, + {file = "fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786"}, + {file = "fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685"}, + {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df"}, + {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c"}, + {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c"}, + {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049"}, + {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62"}, + {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0"}, + {file = "fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b"}, + {file = "fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05"}, + {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9"}, + {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f"}, + {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2"}, + {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563"}, + {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a"}, + {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28"}, + {file = "fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c"}, + {file = "fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba"}, + {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692"}, + {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0"}, + {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1"}, + {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea"}, + {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3"}, + {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278"}, + {file = "fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f"}, + {file = "fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6"}, + {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fd3fccb7b9adaaecfa79ad51b759f2123e1aba97f857936ce044d4f029abd71"}, + {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193b86e9f769320bc98ffdb42accafb5d0c8c49bd62884f1c0702bc598b3f0a2"}, + {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e81c1cc80c1d8bf071356cc3e0e25071fbba1c75afc48d41b26048980b3c771"}, + {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9270505a19361e81eecdbc2c251ad1e1a9a9c2ad75fa022ccdee533f55535dc"}, + {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53f5e9767978a4daf46f28e09dbeb7d010319924ae622f7b56174b777258e5ba"}, + {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9da650cb29bc098b8cfd15ef09009c914b35c7986c8fa9f08b51108b7bc393b4"}, + {file = "fonttools-4.56.0-cp38-cp38-win32.whl", hash = "sha256:965d0209e6dbdb9416100123b6709cb13f5232e2d52d17ed37f9df0cc31e2b35"}, + {file = "fonttools-4.56.0-cp38-cp38-win_amd64.whl", hash = "sha256:654ac4583e2d7c62aebc6fc6a4c6736f078f50300e18aa105d87ce8925cfac31"}, + {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca7962e8e5fc047cc4e59389959843aafbf7445b6c08c20d883e60ced46370a5"}, + {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1af375734018951c31c0737d04a9d5fd0a353a0253db5fbed2ccd44eac62d8c"}, + {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:442ad4122468d0e47d83bc59d0e91b474593a8c813839e1872e47c7a0cb53b10"}, + {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf4f8d2a30b454ac682e12c61831dcb174950c406011418e739de592bbf8f76"}, + {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96a4271f63a615bcb902b9f56de00ea225d6896052c49f20d0c91e9f43529a29"}, + {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d38642ca2dddc7ae992ef5d026e5061a84f10ff2b906be5680ab089f55bb8"}, + {file = "fonttools-4.56.0-cp39-cp39-win32.whl", hash = "sha256:2d351275f73ebdd81dd5b09a8b8dac7a30f29a279d41e1c1192aedf1b6dced40"}, + {file = "fonttools-4.56.0-cp39-cp39-win_amd64.whl", hash = "sha256:d6ca96d1b61a707ba01a43318c9c40aaf11a5a568d1e61146fafa6ab20890793"}, + {file = "fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14"}, + {file = "fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e"}, + {file = "kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751"}, + {file = "kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67"}, + {file = "kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34"}, + {file = "kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8"}, + {file = "kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50"}, + {file = "kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb"}, + {file = "kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2"}, + {file = "kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b"}, + {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, +] + +[[package]] +name = "matplotlib" +version = "3.10.0" +description = "Python plotting package" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"}, + {file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"}, + {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"}, + {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"}, + {file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"}, + {file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"}, + {file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"}, + {file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"}, + {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"}, + {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"}, + {file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"}, + {file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"}, + {file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"}, + {file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"}, + {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"}, + {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"}, + {file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"}, + {file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"}, + {file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"}, + {file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"}, + {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"}, + {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"}, + {file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"}, + {file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"}, + {file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"}, + {file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"}, + {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"}, + {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"}, + {file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"}, + {file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"}, + {file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "mypy" +version = "1.10.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "2.2.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"}, + {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"}, + {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"}, + {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"}, + {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"}, + {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"}, + {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"}, + {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"}, + {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"}, + {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"}, + {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"}, + {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pandas" +version = "2.2.2" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pillow" +version = "11.1.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"linux\" and python_version < \"3.11\"" +files = [ + {file = "psutil-5.8.0-cp310-cp310-linux_x86_64.whl", hash = "sha256:4dd2052113d323b3fe9fd93e9915ab364bdb9ea3bd4995d7a0bf62c492713167"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp310-cp310-linux_x86_64.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"darwin\" and python_version < \"3.11\"" +files = [ + {file = "psutil-5.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b611197383b14311c87919eaa8fbeebf15eab5fdeb22d4c7b67f03772a79716"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp310-cp310-macosx_10_9_universal2.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"win32\" and python_version < \"3.11\"" +files = [ + {file = "psutil-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:07fd5212435883f325b925480b326bb5a2537e085e5552acd9877b91da4566ec"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp310-cp310-win_amd64.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"linux\" and python_version == \"3.11\"" +files = [ + {file = "psutil-5.8.0-cp311-cp311-linux_x86_64.whl", hash = "sha256:b3e5c23e33e96c582e5e3a99e2d2e432621784880e229c4a4e81655932ebd25f"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp311-cp311-linux_x86_64.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"darwin\" and python_version == \"3.11\"" +files = [ + {file = "psutil-5.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f126690bbb74f08cccaaf5defe9d9d4e0b629855e8eb24e07ba6f6087337edf8"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp311-cp311-macosx_10_9_universal2.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"win32\" and python_version == \"3.11\"" +files = [ + {file = "psutil-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b175a13518b8b3c6671f0e3e626706e2eff936a2519c38ca0a5f73138736099a"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp311-cp311-win_amd64.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"linux\" and python_version >= \"3.12\" and python_version < \"3.13\"" +files = [ + {file = "psutil-5.8.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:adf09ab4d9caaf3ac048a2d9486e14eba84e6b231df24b46ce7d495372672f05"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp312-cp312-linux_x86_64.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"darwin\" and python_version >= \"3.12\" and python_version < \"3.13\"" +files = [ + {file = "psutil-5.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7d30e6a6ca11ea83f50039b9dfefc9a1ef2c66028cc0db9071ac1edd9d9bbb9"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp312-cp312-macosx_10_13_universal2.whl" + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "sys_platform == \"win32\" and python_version >= \"3.12\" and python_version < \"3.13\"" +files = [ + {file = "psutil-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:2b478fe4266e25e7b0caea5eeec6df836b2a37308cb24efbce30f915b1f03cfe"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp312-cp312-win_amd64.whl" + +[[package]] +name = "pyparsing" +version = "3.2.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2025.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, +] + +[[package]] +name = "ruff" +version = "0.6.0" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "ruff-0.6.0-py3-none-linux_armv6l.whl", hash = "sha256:92dcce923e5df265781e5fc76f9a1edad52201a7aafe56e586b90988d5239013"}, + {file = "ruff-0.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:31b90ff9dc79ed476c04e957ba7e2b95c3fceb76148f2079d0d68a908d2cfae7"}, + {file = "ruff-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d834a9ec9f8287dd6c3297058b3a265ed6b59233db22593379ee38ebc4b9768"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2089267692696aba342179471831a085043f218706e642564812145df8b8d0d"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa62b423ee4bbd8765f2c1dbe8f6aac203e0583993a91453dc0a449d465c84da"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7344e1a964b16b1137ea361d6516ce4ee61a0403fa94252a1913ecc1311adcae"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:487f3a35c3f33bf82be212ce15dc6278ea854e35573a3f809442f73bec8b2760"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75db409984077a793cf344d499165298a6f65449e905747ac65983b12e3e64b1"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84908bd603533ecf1db456d8fc2665d1f4335d722e84bc871d3bbd2d1116c272"}, + {file = "ruff-0.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f1749a0aef3ec41ed91a0e2127a6ae97d2e2853af16dbd4f3c00d7a3af726c5"}, + {file = "ruff-0.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:016fea751e2bcfbbd2f8cb19b97b37b3fd33148e4df45b526e87096f4e17354f"}, + {file = "ruff-0.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6ae80f141b53b2e36e230017e64f5ea2def18fac14334ffceaae1b780d70c4f7"}, + {file = "ruff-0.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eaaaf33ea4b3f63fd264d6a6f4a73fa224bbfda4b438ffea59a5340f4afa2bb5"}, + {file = "ruff-0.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7667ddd1fc688150a7ca4137140867584c63309695a30016880caf20831503a0"}, + {file = "ruff-0.6.0-py3-none-win32.whl", hash = "sha256:ae48365aae60d40865a412356f8c6f2c0be1c928591168111eaf07eaefa6bea3"}, + {file = "ruff-0.6.0-py3-none-win_amd64.whl", hash = "sha256:774032b507c96f0c803c8237ce7d2ef3934df208a09c40fa809c2931f957fe5e"}, + {file = "ruff-0.6.0-py3-none-win_arm64.whl", hash = "sha256:a5366e8c3ae6b2dc32821749b532606c42e609a99b0ae1472cf601da931a048c"}, + {file = "ruff-0.6.0.tar.gz", hash = "sha256:272a81830f68f9bd19d49eaf7fa01a5545c5a2e86f32a9935bb0e4bb9a1db5b8"}, +] + +[[package]] +name = "scipy" +version = "1.15.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "scipy-1.15.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c64ded12dcab08afff9e805a67ff4480f5e69993310e093434b10e85dc9d43e1"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5b190b935e7db569960b48840e5bef71dc513314cc4e79a1b7d14664f57fd4ff"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4b17d4220df99bacb63065c76b0d1126d82bbf00167d1730019d2a30d6ae01ea"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:63b9b6cd0333d0eb1a49de6f834e8aeaefe438df8f6372352084535ad095219e"}, + {file = "scipy-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f151e9fb60fbf8e52426132f473221a49362091ce7a5e72f8aa41f8e0da4f25"}, + {file = "scipy-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e10b1dd56ce92fba3e786007322542361984f8463c6d37f6f25935a5a6ef52"}, + {file = "scipy-1.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5dff14e75cdbcf07cdaa1c7707db6017d130f0af9ac41f6ce443a93318d6c6e0"}, + {file = "scipy-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:f82fcf4e5b377f819542fbc8541f7b5fbcf1c0017d0df0bc22c781bf60abc4d8"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:5bd8d27d44e2c13d0c1124e6a556454f52cd3f704742985f6b09e75e163d20d2"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:be3deeb32844c27599347faa077b359584ba96664c5c79d71a354b80a0ad0ce0"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5eb0ca35d4b08e95da99a9f9c400dc9f6c21c424298a0ba876fdc69c7afacedf"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:74bb864ff7640dea310a1377d8567dc2cb7599c26a79ca852fc184cc851954ac"}, + {file = "scipy-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:667f950bf8b7c3a23b4199db24cb9bf7512e27e86d0e3813f015b74ec2c6e3df"}, + {file = "scipy-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395be70220d1189756068b3173853029a013d8c8dd5fd3d1361d505b2aa58fa7"}, + {file = "scipy-1.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce3a000cd28b4430426db2ca44d96636f701ed12e2b3ca1f2b1dd7abdd84b39a"}, + {file = "scipy-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fe1d95944f9cf6ba77aa28b82dd6bb2a5b52f2026beb39ecf05304b8392864b"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c09aa9d90f3500ea4c9b393ee96f96b0ccb27f2f350d09a47f533293c78ea776"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0ac102ce99934b162914b1e4a6b94ca7da0f4058b6d6fd65b0cef330c0f3346f"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:09c52320c42d7f5c7748b69e9f0389266fd4f82cf34c38485c14ee976cb8cb04"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cdde8414154054763b42b74fe8ce89d7f3d17a7ac5dd77204f0e142cdc9239e9"}, + {file = "scipy-1.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c9d8fc81d6a3b6844235e6fd175ee1d4c060163905a2becce8e74cb0d7554ce"}, + {file = "scipy-1.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb57b30f0017d4afa5fe5f5b150b8f807618819287c21cbe51130de7ccdaed2"}, + {file = "scipy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491d57fe89927fa1aafbe260f4cfa5ffa20ab9f1435025045a5315006a91b8f5"}, + {file = "scipy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:900f3fa3db87257510f011c292a5779eb627043dd89731b9c461cd16ef76ab3d"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:100193bb72fbff37dbd0bf14322314fc7cbe08b7ff3137f11a34d06dc0ee6b85"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:2114a08daec64980e4b4cbdf5bee90935af66d750146b1d2feb0d3ac30613692"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6b3e71893c6687fc5e29208d518900c24ea372a862854c9888368c0b267387ab"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:837299eec3d19b7e042923448d17d95a86e43941104d33f00da7e31a0f715d3c"}, + {file = "scipy-1.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82add84e8a9fb12af5c2c1a3a3f1cb51849d27a580cb9e6bd66226195142be6e"}, + {file = "scipy-1.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070d10654f0cb6abd295bc96c12656f948e623ec5f9a4eab0ddb1466c000716e"}, + {file = "scipy-1.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55cc79ce4085c702ac31e49b1e69b27ef41111f22beafb9b49fea67142b696c4"}, + {file = "scipy-1.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:c352c1b6d7cac452534517e022f8f7b8d139cd9f27e6fbd9f3cbd0bfd39f5bef"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0458839c9f873062db69a03de9a9765ae2e694352c76a16be44f93ea45c28d2b"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:af0b61c1de46d0565b4b39c6417373304c1d4f5220004058bdad3061c9fa8a95"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:71ba9a76c2390eca6e359be81a3e879614af3a71dfdabb96d1d7ab33da6f2364"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14eaa373c89eaf553be73c3affb11ec6c37493b7eaaf31cf9ac5dffae700c2e0"}, + {file = "scipy-1.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f735bc41bd1c792c96bc426dece66c8723283695f02df61dcc4d0a707a42fc54"}, + {file = "scipy-1.15.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2722a021a7929d21168830790202a75dbb20b468a8133c74a2c0230c72626b6c"}, + {file = "scipy-1.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc7136626261ac1ed988dca56cfc4ab5180f75e0ee52e58f1e6aa74b5f3eacd5"}, + {file = "scipy-1.15.1.tar.gz", hash = "sha256:033a75ddad1463970c96a88063a1df87ccfddd526437136b6ee81ff0312ebdf6"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.5" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.16.5)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "seaborn" +version = "0.13.2" +description = "Statistical data visualization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"}, + {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"}, +] + +[package.dependencies] +matplotlib = ">=3.4,<3.6.1 || >3.6.1" +numpy = ">=1.20,<1.24.0 || >1.24.0" +pandas = ">=1.2" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2025.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "104c9363ce679a986c71e1cb3e686696eddd22abcbae318e251ff44e4121bf29" diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 index adbf17d5..e9d9c035 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,218 @@ -[tool.isort] -py_version=36 -line_length = 100 -multi_line_output = 6 -honor_noqa = true -order_by_type = false -use_parentheses = true -combine_as_imports = true -only_modified = true -lexicographical = true -group_by_package = true -force_alphabetical_sort_within_sections = true -extend_skip_glob = [ - "tests/projects", - "tests/outcomes/**/main*.py", - "tests/outcomes/**/cleaning.py", - "tests/outcomes/**/pandas_*.py", - "tests/outcomes/**/matplotlib_*.py", - "tests/outcomes/**/seaborn_*.py", +[tool.poetry] +name = "hs-test-python" +version = "0.1.0" +description = "" +authors = ["Hyperskill Team"] +readme = "README.md" +packages = [ + { include = "hstest" }, ] + +[tool.poetry.dependencies] +python = "^3.10" + +# psutil with platform and Python version markers +psutil = [ + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp310-cp310-win_amd64.whl", markers = "sys_platform == 'win32' and python_version >= '3.10' and python_version < '3.11'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32' and python_version >= '3.11' and python_version < '3.12'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp312-cp312-win_amd64.whl", markers = "sys_platform == 'win32' and python_version >= '3.12' and python_version < '3.13'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp310-cp310-linux_x86_64.whl", markers = "sys_platform == 'linux' and python_version >= '3.10' and python_version < '3.11'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp311-cp311-linux_x86_64.whl", markers = "sys_platform == 'linux' and python_version >= '3.11' and python_version < '3.12'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp312-cp312-linux_x86_64.whl", markers = "sys_platform == 'linux' and python_version >= '3.12' and python_version < '3.13'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp310-cp310-macosx_10_9_universal2.whl", markers = "sys_platform == 'darwin' and python_version >= '3.10' and python_version < '3.11'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp311-cp311-macosx_10_9_universal2.whl", markers = "sys_platform == 'darwin' and python_version >= '3.11' and python_version < '3.12'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/download/v11.0.26/psutil-5.8.0-cp312-cp312-macosx_10_13_universal2.whl", markers = "sys_platform == 'darwin' and python_version >= '3.12' and python_version < '3.13'" } +] + +pandas = "2.2.2" +seaborn = "0.13.2" +scipy = "^1.12.0" +matplotlib = "^3.9.2" + +[tool.poetry.group.dev.dependencies] +mypy = "1.10.1" +ruff = "0.6.0" +pytest = "8.3.4" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 100 +target-version = "py310" +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "tests", +] + +[tool.ruff.lint] +select = [ + "ALL", +] +ignore = [ + "ANN001", # Missing type annotation for function argument + "ANN002", # Missing type annotation for `*args` + "ANN003", # Missing type annotation for `**kwargs` + "ANN101", # Missing type annotation for `self` in method + "ANN102", # Missing type annotation for `cls` in classmethod + "ANN201", # Missing return type annotation for public function + "ANN202", # Missing return type annotation for private function + "ANN204", # Missing return type annotation for special method + "ANN205", # Missing return type annotation for static method + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "ARG004", # Unused static method argument + "CPY001", # Missing copyright notice at top of file + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in __init__ + "E203", # Whitespace before ':' + "ERA001", # Found commented-out code + "INP001", # Implicit namespace package + "PLR2004", # Magic value comparison + "PTH100", # os.path.abspath used + "PTH107", # os.remove used + "PTH109", # os.getcwd used + "PTH110", # os.path.exists used + "PTH112", # os.path.isdir used + "PTH113", # os.path.isfile used + "PTH118", # os.path.join used + "PTH119", # os.path.basename used + "PTH120", # os.path.dirname used + "PTH122", # os.path.splitext used + "PTH123", # builtin open used + "S310", # Suspicious URL open usage + "S404", # Suspicious subprocess import + "S603", # subprocess without shell equals true + "BLE001", # Blind except + "E721", # Type comparison + "RUF012", # Mutable class default + "FBT001", # Boolean type hint positional argument + "FBT002", # Boolean default value positional argument + "FBT003", # Boolean positional value in function call + "SLF001", # Private member access + "B904", # Raise without from inside except + "ISC003", # Explicit string concatenation + "N818", # Error suffix on exception name + "DOC201", # Docstring missing returns + "TRY301", # Raise within try + "UP007", # Non PEP604 annotation + "S110", # Try except pass + "B007", # Unused loop control variable + "ARG005", # Unused lambda argument + "PLC2701", # Import private name + "PLW2901", # Redefined loop name + "TRY300", # Try consider else + "A004", # Builtin import shadowing + "SIM102", # Collapsible if + "SIM113", # Enumerate for loop + "PERF401", # Manual list comprehension + "E501", # Line too long + "D205", # Blank line after summary + "PLR1704", # Redefined argument from local + "PLW0211", # Bad staticmethod argument + "UP031", # Printf string formatting + "FURB101", # Read whole file + "FURB103", # Write whole file + "EXE002", # The file is executable but no shebang is present + "FIX002", # Line contains TODO, consider resolving the issue + "N806", # Variable in function should be lowercase + "PLC0415", # `import` should be at the top-level of a file + "PLC1901", # `record['bio'] == ''` can be simplified to `not record['bio']` as an empty string is falsey + "PLR0904", # Too many public methods + "PLR0916", # Too many Boolean expressions + "PLR6301", # Method could be a function, class method, or static method + "PT", # Use a regular `assert` instead of unittest-style `assertEqual` + "S101", # Use of `assert` detected + "TD002", # Missing author in TODO + "TD003", # Missing issue link on the line following this TODO + # Ruff format recommend disable trid rule + "COM812", # Trailing comma missing + "COM819", # Checks for the presence of prohibited trailing commas + "D206", # Docstring should be indented with spaces, not tabs + "D300", # Use """triple double quotes""" + "E111", # Indentation is not a multiple of four + "E114", # Indentation is not a multiple of four (comment) + "E117", # Over-indented + "ISC001", # Conflict with ruff format | Checks for implicitly concatenated strings on a single line. + "ISC002", # Checks for implicitly concatenated strings across multiple lines. + "Q000", # Conflict with ruff format | Remove bad quotes + "Q001", # Checks for multiline strings that use single quotes or double quotes + "Q002", # Checks for docstrings that use single quotes or double quotes + "Q003", # Conflict with ruff format | Change outer quotes to avoid escaping inner quotes + "W191", # Indentation contains tabs +] + +[tool.ruff.lint.mccabe] +max-complexity = 56 + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.pylint] +max-args = 11 +max-branches = 27 +max-returns = 7 +max-statements = 153 +max-nested-blocks = 7 + +[tool.ruff.lint.isort] +combine-as-imports = true +order-by-type = false +required-imports = ["from __future__ import annotations"] + +[tool.mypy] +python_version = "3.10" +check_untyped_defs = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +explicit_package_bases = true +ignore_errors = false +ignore_missing_imports = true +implicit_reexport = true +strict_equality = true +strict_optional = true +warn_no_return = true +warn_redundant_casts = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +exclude = [ + "tests", + "venv", +] + +[tool.cibuildwheel] +test-command = "pytest {project}/tests" +test-extras = ["test"] +test-skip = ["*universal2:arm64"] +skip = ["pp*"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index db2ad1b2..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -matplotlib -seaborn -pandas -scipy -flake8 -isort diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7c515cc5..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -psutil-wheels ; python_version >= '3.10' -psutil ; python_version < '3.10' diff --git a/setup.py b/setup.py deleted file mode 100644 index 63f4d875..00000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -from setuptools import find_namespace_packages, setup - -with open("README.md", "r") as readme_file: - readme = readme_file.read() - -setup( - name="hs-test-python", - version="11.0.0", - author="Vladimir Turov", - author_email="vladimir.turov@stepik.org", - description=( - "A framework that simplifies testing educational projects for https://hyperskill.org/." - ), - long_description=readme, - long_description_content_type="text/markdown", - url="https://github.com/hyperskill/hs-test-python", - packages=find_namespace_packages(exclude=['tests', 'package.json', 'requirements-dev.txt']), - python_requires=">=3.6", - install_requires=[ - "psutil-wheels ; python_version >= '3.10'", - "psutil ; python_version < '3.10'", - ], - classifiers=[ - "Programming Language :: Python :: 3.6" - ], -) diff --git a/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py b/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py index e14d20c1..ca6c43f8 100644 --- a/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py +++ b/tests/outcomes/dynamic_method/test_dynamic_method_exception/test.py @@ -5,23 +5,26 @@ class TestDynamicMethodException(UserErrorTest): - contain = """ - Exception in test #1 + contain = [ + """ + Exception in test #1 - Traceback (most recent call last): - File "main.py", line 3, in - print(0/0) - ZeroDivisionError: division by zero - - Please find below the output of your program during this failed test. - Note that the '>' character indicates the beginning of the input line. - - --- - - Server started! - > main - S1: main - """ # noqa: W293 + Traceback (most recent call last): + File "main.py", line 3, in + print(0/0) + ~^~ + ZeroDivisionError: division by zero + + Please find below the output of your program during this failed test. + Note that the '>' character indicates the beginning of the input line. + + --- + + Server started! + > main + S1: main + """ + ] @dynamic_test def test(self): diff --git a/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py b/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py index 91e51218..e5171a58 100644 --- a/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py +++ b/tests/outcomes/feedback_on_exception/feedback_on_exception_test_1/test.py @@ -14,6 +14,7 @@ class FeedbackOnExceptionTest1(UserErrorTest): Traceback (most recent call last): File "main.py", line 2, in print(1 / 0) + ~~^~~ ZeroDivisionError: division by zero Please find below the output of your program during this failed test. diff --git a/tests/outcomes/lib/exception_in_user_code_2/test.py b/tests/outcomes/lib/exception_in_user_code_2/test.py index b7789d55..1580fc10 100644 --- a/tests/outcomes/lib/exception_in_user_code_2/test.py +++ b/tests/outcomes/lib/exception_in_user_code_2/test.py @@ -12,8 +12,10 @@ class ExceptionInUserCodeTest(UserErrorTest): Traceback (most recent call last): File "main.py", line 3, in print(0 / 0) + ~~^~~ File "main.py", line 5, in print(0 / 0) + ~~^~~ ZeroDivisionError: division by zero Please find below the output of your program during this failed test. diff --git a/tests/outcomes/lib/exception_in_user_code_3/test.py b/tests/outcomes/lib/exception_in_user_code_3/test.py index 9410655c..a140e13e 100644 --- a/tests/outcomes/lib/exception_in_user_code_3/test.py +++ b/tests/outcomes/lib/exception_in_user_code_3/test.py @@ -6,14 +6,15 @@ class ExceptionInUserCodeTest3(UserErrorTest): - contain = """ + contain = ''' Exception in test #1 Traceback (most recent call last): File "main.py", line 2, in - Coffee is ready!\"\"\", raise_error_here) + Coffee is ready!""", raise_error_here) + ^^^^^^^^^^^^^^^^ NameError: name 'raise_error_here' is not defined - """ + ''' def generate(self) -> List[TestCase]: return [TestCase()] diff --git a/tests/outcomes/plot/bar/pandas/test_example/cleaning.py b/tests/outcomes/plot/bar/pandas/test_example/cleaning.py index f65f04ac..6f67738b 100644 --- a/tests/outcomes/plot/bar/pandas/test_example/cleaning.py +++ b/tests/outcomes/plot/bar/pandas/test_example/cleaning.py @@ -5,7 +5,7 @@ df_ab = pd.read_csv('ab_test.csv') # Plot dates -df_ab['date'] = df_ab['date'].astype('datetime64') +df_ab["date"] = pd.to_datetime(df_ab["date"]) control = df_ab[df_ab['group'] == 'Control'].groupby(df_ab['date'].dt.day).size().rename('Control') experiment = df_ab[df_ab['group'] == 'Experimental'].groupby(df_ab['date'].dt.day).size().rename('Experimental') diff --git a/tests/outcomes/plot/test_seaborn/test_revert/main.py b/tests/outcomes/plot/test_seaborn/test_revert/main.py index 0528854b..b3d92d4e 100644 --- a/tests/outcomes/plot/test_seaborn/test_revert/main.py +++ b/tests/outcomes/plot/test_seaborn/test_revert/main.py @@ -35,7 +35,7 @@ def plot(): sns.violinplot(data=df, palette="Set3", bw=.2, cut=1, linewidth=1) flights_long = sns.load_dataset("flights") - flights = flights_long.pivot("month", "year", "passengers") + flights = flights_long.pivot(index="month", columns="year", values="passengers") sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax) tips = sns.load_dataset("tips") diff --git a/tests/outcomes/syntax_error/test_empty_eval/test.py b/tests/outcomes/syntax_error/test_empty_eval/test.py index e24dce10..c6a1769c 100644 --- a/tests/outcomes/syntax_error/test_empty_eval/test.py +++ b/tests/outcomes/syntax_error/test_empty_eval/test.py @@ -12,8 +12,11 @@ class TestEmptyEval(UserErrorTest): Traceback (most recent call last): File "main.py", line 1, in print(eval("")) - File "", line 0""", - "SyntaxError: " + ^^^^^^^^ + File "", line 0 + + SyntaxError: invalid syntax + """ ] def generate(self) -> List[TestCase]: diff --git a/tests/outcomes/syntax_error/test_error_using_eval/test.py b/tests/outcomes/syntax_error/test_error_using_eval/test.py index 95cab756..477bbcbb 100644 --- a/tests/outcomes/syntax_error/test_error_using_eval/test.py +++ b/tests/outcomes/syntax_error/test_error_using_eval/test.py @@ -12,9 +12,12 @@ class TestEmptyEval(UserErrorTest): Traceback (most recent call last): File "main.py", line 1, in print(eval(")")) + ^^^^^^^^^ File "", line 1 - """, - "SyntaxError: " + ) + ^ + SyntaxError: unmatched ')' + """ ] def generate(self) -> List[TestCase]: diff --git a/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py b/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py index a34e5fe1..8d3715a4 100644 --- a/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py +++ b/tests/outcomes/syntax_error/test_error_using_eval_and_print/test.py @@ -12,12 +12,14 @@ class TestEmptyEval(UserErrorTest): Traceback (most recent call last): File "main.py", line 2, in print(eval(")")) + ^^^^^^^^^ File "", line 1 - """, - "SyntaxError: ", - """ + ) + ^ + SyntaxError: unmatched ')' + Please find below the output of your program during this failed test. - + --- 123 diff --git a/tests/projects/go/coffee_machine/stage1_ce/tests.py b/tests/projects/go/coffee_machine/stage1_ce/tests.py index 436a5500..42096780 100644 --- a/tests/projects/go/coffee_machine/stage1_ce/tests.py +++ b/tests/projects/go/coffee_machine/stage1_ce/tests.py @@ -1,7 +1,10 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest import os +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) @@ -21,7 +24,7 @@ class CoffeeMachineTest(UserErrorTest): contain = f""" Compilation error - .{os.sep}main.go:4:2: imported and not used: "fmt" + .{os.sep}main.go:4:2: "fmt" imported and not used .{os.sep}main.go:8:2: undefined: Println """ diff --git a/tests/projects/go/coffee_machine/stage1_ex/tests.py b/tests/projects/go/coffee_machine/stage1_ex/tests.py index 7d81161d..a6fdd1fc 100644 --- a/tests/projects/go/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/go/coffee_machine/stage1_ex/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage1_wa/tests.py b/tests/projects/go/coffee_machine/stage1_wa/tests.py index f2585377..49fb3cfb 100644 --- a/tests/projects/go/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/go/coffee_machine/stage1_wa/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage2/tests.py b/tests/projects/go/coffee_machine/stage2/tests.py index e2878fd7..89f36434 100644 --- a/tests/projects/go/coffee_machine/stage2/tests.py +++ b/tests/projects/go/coffee_machine/stage2/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage3/tests.py b/tests/projects/go/coffee_machine/stage3/tests.py index 3601e4c3..f96b322d 100644 --- a/tests/projects/go/coffee_machine/stage3/tests.py +++ b/tests/projects/go/coffee_machine/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage4/tests.py b/tests/projects/go/coffee_machine/stage4/tests.py index 213047e6..8d431f26 100644 --- a/tests/projects/go/coffee_machine/stage4/tests.py +++ b/tests/projects/go/coffee_machine/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage5/tests.py b/tests/projects/go/coffee_machine/stage5/tests.py index a36bc59a..77c1c85b 100644 --- a/tests/projects/go/coffee_machine/stage5/tests.py +++ b/tests/projects/go/coffee_machine/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1/tests.py b/tests/projects/javascript/coffee_machine/stage1/tests.py index fea90ed8..947ca431 100644 --- a/tests/projects/javascript/coffee_machine/stage1/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1_ce/tests.py b/tests/projects/javascript/coffee_machine/stage1_ce/tests.py index b455f2dd..4c6381d4 100644 --- a/tests/projects/javascript/coffee_machine/stage1_ce/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_ce/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1_ex/tests.py b/tests/projects/javascript/coffee_machine/stage1_ex/tests.py index 08c9cbe1..e7e0bed4 100644 --- a/tests/projects/javascript/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_ex/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage1_wa/tests.py b/tests/projects/javascript/coffee_machine/stage1_wa/tests.py index f2585377..49fb3cfb 100644 --- a/tests/projects/javascript/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_wa/tests.py @@ -1,6 +1,9 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage2/tests.py b/tests/projects/javascript/coffee_machine/stage2/tests.py index e2878fd7..6de71aba 100644 --- a/tests/projects/javascript/coffee_machine/stage2/tests.py +++ b/tests/projects/javascript/coffee_machine/stage2/tests.py @@ -1,5 +1,8 @@ -from hstest.stage_test import * +from typing import Any, List + +from hstest.stage_test import StageTest from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage3/tests.py b/tests/projects/javascript/coffee_machine/stage3/tests.py index 3601e4c3..f96b322d 100644 --- a/tests/projects/javascript/coffee_machine/stage3/tests.py +++ b/tests/projects/javascript/coffee_machine/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage4/tests.py b/tests/projects/javascript/coffee_machine/stage4/tests.py index 213047e6..8d431f26 100644 --- a/tests/projects/javascript/coffee_machine/stage4/tests.py +++ b/tests/projects/javascript/coffee_machine/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/coffee_machine/stage5/tests.py b/tests/projects/javascript/coffee_machine/stage5/tests.py index a36bc59a..77c1c85b 100644 --- a/tests/projects/javascript/coffee_machine/stage5/tests.py +++ b/tests/projects/javascript/coffee_machine/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage1/tests.py b/tests/projects/javascript/simple_chatty_bot/stage1/tests.py index da7967c9..38e97213 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage1/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage1/tests.py @@ -1,7 +1,9 @@ import re +from typing import List from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage2/tests.py b/tests/projects/javascript/simple_chatty_bot/stage2/tests.py index 82562c96..4672d935 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage2/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage2/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage3/tests.py b/tests/projects/javascript/simple_chatty_bot/stage3/tests.py index ea267bb5..5e6855b1 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage3/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage4/tests.py b/tests/projects/javascript/simple_chatty_bot/stage4/tests.py index 462b9a64..f4870050 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage4/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/javascript/simple_chatty_bot/stage5/tests.py b/tests/projects/javascript/simple_chatty_bot/stage5/tests.py index 092cffbd..7d3b7bcb 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage5/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage1/tests.py b/tests/projects/python/coffee_machine/stage1/tests.py index fea90ed8..947ca431 100644 --- a/tests/projects/python/coffee_machine/stage1/tests.py +++ b/tests/projects/python/coffee_machine/stage1/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage2/tests.py b/tests/projects/python/coffee_machine/stage2/tests.py index e2878fd7..89f36434 100644 --- a/tests/projects/python/coffee_machine/stage2/tests.py +++ b/tests/projects/python/coffee_machine/stage2/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage3/tests.py b/tests/projects/python/coffee_machine/stage3/tests.py index 3601e4c3..f96b322d 100644 --- a/tests/projects/python/coffee_machine/stage3/tests.py +++ b/tests/projects/python/coffee_machine/stage3/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage4/tests.py b/tests/projects/python/coffee_machine/stage4/tests.py index 213047e6..8d431f26 100644 --- a/tests/projects/python/coffee_machine/stage4/tests.py +++ b/tests/projects/python/coffee_machine/stage4/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/python/coffee_machine/stage5/tests.py b/tests/projects/python/coffee_machine/stage5/tests.py index a36bc59a..77c1c85b 100644 --- a/tests/projects/python/coffee_machine/stage5/tests.py +++ b/tests/projects/python/coffee_machine/stage5/tests.py @@ -1,5 +1,8 @@ +from typing import List + from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/shell/coffee_machine/stage1/tests.py b/tests/projects/shell/coffee_machine/stage1/tests.py index 60bb2b04..e7b4ee80 100644 --- a/tests/projects/shell/coffee_machine/stage1/tests.py +++ b/tests/projects/shell/coffee_machine/stage1/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.common.os_utils import is_windows diff --git a/tests/projects/shell/coffee_machine/stage1_ex/tests.py b/tests/projects/shell/coffee_machine/stage1_ex/tests.py index 98652a9b..75b14023 100644 --- a/tests/projects/shell/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/shell/coffee_machine/stage1_ex/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest diff --git a/tests/projects/shell/coffee_machine/stage1_wa/tests.py b/tests/projects/shell/coffee_machine/stage1_wa/tests.py index 7184b6f3..41dffdae 100644 --- a/tests/projects/shell/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/shell/coffee_machine/stage1_wa/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest diff --git a/tests/projects/shell/coffee_machine/stage2/tests.py b/tests/projects/shell/coffee_machine/stage2/tests.py index 06a8cbc1..5a22bcc9 100644 --- a/tests/projects/shell/coffee_machine/stage2/tests.py +++ b/tests/projects/shell/coffee_machine/stage2/tests.py @@ -1,3 +1,6 @@ +from typing import List + +from hstest.check_result import CheckResult from hstest.stage_test import * from hstest.test_case import TestCase from hstest.common.os_utils import is_windows diff --git a/tests/test_check_result.py b/tests/test_check_result.py index 3d3481d2..0f38e3ed 100644 --- a/tests/test_check_result.py +++ b/tests/test_check_result.py @@ -1,15 +1,17 @@ +from __future__ import annotations + import unittest from hstest.check_result import CheckResult class TestCheckResult(unittest.TestCase): - def test_true(self): + def test_true(self) -> None: r = CheckResult.correct() self.assertTrue(r.is_correct) - self.assertEqual(r.feedback, '') + self.assertEqual(r.feedback, "") - def test_false(self): - r = CheckResult.wrong('hello') + def test_false(self) -> None: + r = CheckResult.wrong("hello") self.assertFalse(r.is_correct) - self.assertEqual(r.feedback, 'hello') + self.assertEqual(r.feedback, "hello") diff --git a/tests/test_testcase.py b/tests/test_testcase.py index 45ebdb94..df068572 100644 --- a/tests/test_testcase.py +++ b/tests/test_testcase.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest from hstest.exception.outcomes import UnexpectedError @@ -5,71 +7,65 @@ class TestTestCase(unittest.TestCase): - def test_attach_none_default(self): + def test_attach_none_default(self) -> None: test_case = TestCase() self.assertIsNone(test_case.attach) - def test_attach(self): + def test_attach(self) -> None: attach = (1, "abc") test_case = TestCase(attach=attach) self.assertEqual(attach, test_case.attach) - def test_copy_to_attach(self): - test_case = TestCase(stdin='abc', copy_to_attach=True) - self.assertEqual(test_case.attach, 'abc') + def test_copy_to_attach(self) -> None: + test_case = TestCase(stdin="abc", copy_to_attach=True) + self.assertEqual(test_case.attach, "abc") - def test_copy_to_attach_exception(self): + def test_copy_to_attach_exception(self) -> None: with self.assertRaises(UnexpectedError): - TestCase(stdin='abc', attach=(1, 2, 3), copy_to_attach=True) + TestCase(stdin="abc", attach=(1, 2, 3), copy_to_attach=True) - def test_stdin_empty(self): + def test_stdin_empty(self) -> None: test_case = TestCase() - self.assertEqual(test_case.input, '') + self.assertEqual(test_case.input, "") - def test_stdin_passed(self): - stdin = 'abc' + def test_stdin_passed(self) -> None: + stdin = "abc" test_case = TestCase(stdin=stdin) self.assertEqual(test_case.input, stdin) - def test_from_stepik_length(self): - tests = TestCase.from_stepik(['123', '234', '345']) + def test_from_stepik_length(self) -> None: + tests = TestCase.from_stepik(["123", "234", "345"]) self.assertEqual(len(tests), 3) - def test_from_stepik_simple(self): - tests = TestCase.from_stepik(['123', '234', '345']) - self.assertEqual(tests[0].input, '123') + def test_from_stepik_simple(self) -> None: + tests = TestCase.from_stepik(["123", "234", "345"]) + self.assertEqual(tests[0].input, "123") self.assertEqual(tests[0].attach, None) - self.assertEqual(tests[1].input, '234') + self.assertEqual(tests[1].input, "234") self.assertEqual(tests[1].attach, None) - self.assertEqual(tests[2].input, '345') + self.assertEqual(tests[2].input, "345") self.assertEqual(tests[2].attach, None) - def test_from_stepik_with_attach(self): - tests = TestCase.from_stepik( - [('123', 234), ('234', 345), ('345', 456)] - ) - self.assertEqual(tests[0].input, '123') + def test_from_stepik_with_attach(self) -> None: + tests = TestCase.from_stepik([("123", 234), ("234", 345), ("345", 456)]) + self.assertEqual(tests[0].input, "123") self.assertEqual(tests[0].attach, 234) - self.assertEqual(tests[1].input, '234') + self.assertEqual(tests[1].input, "234") self.assertEqual(tests[1].attach, 345) - self.assertEqual(tests[2].input, '345') + self.assertEqual(tests[2].input, "345") self.assertEqual(tests[2].attach, 456) - def test_from_stepik_mixed(self): - tests = TestCase.from_stepik( - [('mixed1', 234567), 'mixed234', ('mixed345', 456234), '567'] - ) - self.assertEqual(tests[0].input, 'mixed1') + def test_from_stepik_mixed(self) -> None: + tests = TestCase.from_stepik([("mixed1", 234567), "mixed234", ("mixed345", 456234), "567"]) + self.assertEqual(tests[0].input, "mixed1") self.assertEqual(tests[0].attach, 234567) - self.assertEqual(tests[1].input, 'mixed234') + self.assertEqual(tests[1].input, "mixed234") self.assertEqual(tests[1].attach, None) - self.assertEqual(tests[2].input, 'mixed345') + self.assertEqual(tests[2].input, "mixed345") self.assertEqual(tests[2].attach, 456234) - self.assertEqual(tests[3].input, '567') + self.assertEqual(tests[3].input, "567") self.assertEqual(tests[3].attach, None) - def test_from_stepik_bad_data(self): + def test_from_stepik_bad_data(self) -> None: with self.assertRaises(UnexpectedError): - TestCase.from_stepik( - [('mixed1', 234567), 234345, ('mixed345', 456234), '567'] - ) + TestCase.from_stepik([("mixed1", 234567), 234345, ("mixed345", 456234), "567"]) diff --git a/tests/testing.py b/tests/testing.py index 1bfb5920..e9f02e3d 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -1,4 +1,5 @@ -import io +from __future__ import annotations + import re import sys import unittest @@ -6,41 +7,41 @@ from inspect import getmembers, isclass from os import listdir from os.path import abspath, dirname, isdir, isfile -from typing import List +from typing import TYPE_CHECKING -content_path = dirname( - dirname(abspath(__file__)) -) +content_path = dirname(dirname(abspath(__file__))) sys.path.insert(0, content_path) from hstest.common import utils as hs # noqa: E402 from hstest.dynamic.output.colored_output import GREEN_BOLD, RED_BOLD, RESET # noqa: E402 +if TYPE_CHECKING: + import io + class OutputForTest: - def __init__(self, real_out: io.TextIOWrapper): + def __init__(self, real_out: io.TextIOWrapper) -> None: self.original: io.TextIOWrapper = real_out - def write(self, text): - text = re.sub(r'(? None: + text = re.sub(r"(? None: self.original.flush() - def close(self): + def close(self) -> None: self.original.close() class UnitTesting: - @staticmethod def test_all() -> bool: old_run = unittest.TestCase.run @@ -48,33 +49,33 @@ def test_all() -> bool: def run(self, result=None, repeats=0): failures_before = 0 if result is None else len(result.failures) test_result = old_run(self, result=result) - is_project_test = 'tests.projects.' in str(self) + is_project_test = "tests.projects." in str(self) if repeats == 5: # max 5 times return test_result if is_project_test and test_result and failures_before < len(test_result.failures): - print('Rerun project test') test_result.failures.pop() return run(self, result=test_result, repeats=repeats + 1) return test_result unittest.TestCase.run = run - hs.failed_msg_start = '' - hs.failed_msg_continue = '' - hs.success_msg = '' + hs.failed_msg_start = "" + hs.failed_msg_continue = "" + hs.success_msg = "" tests_suite = [] loader = unittest.TestLoader() for module in UnitTesting.find_modules(dirname(__file__)): - if 'outcomes' in module and not module.endswith('.test') or \ - 'projects' in module and not module.endswith('.tests'): + if ("outcomes" in module and not module.endswith(".test")) or ( + "projects" in module and not module.endswith(".tests") + ): continue try: - imported = import_module(f'tests.{module}') + imported = import_module(f"tests.{module}") except ImportError: continue - for name, obj in getmembers(imported): + for _name, obj in getmembers(imported): if isclass(obj) and issubclass(obj, unittest.TestCase): tests_suite += [loader.loadTestsFromTestCase(obj)] @@ -84,8 +85,7 @@ def run(self, result=None, repeats=0): return result.wasSuccessful() @staticmethod - def find_modules(from_directory: str) -> List[str]: - + def find_modules(from_directory: str) -> list[str]: catalogs = [from_directory] curr_dir = from_directory @@ -94,17 +94,17 @@ def find_modules(from_directory: str) -> List[str]: while catalogs: curr_catalog = catalogs.pop() for file in listdir(curr_catalog): - curr_location = curr_catalog + '/' + file - if file.startswith('__'): + curr_location = curr_catalog + "/" + file + if file.startswith("__"): continue if isfile(curr_location): - if file.endswith('.py'): - modules += [curr_location[len(curr_dir) + 1:-3].replace('/', '.')] + if file.endswith(".py"): + modules += [curr_location[len(curr_dir) + 1 : -3].replace("/", ".")] elif isdir(curr_location): catalogs += [curr_location] return modules -if __name__ == '__main__': - exit(0 if UnitTesting.test_all() else -1) +if __name__ == "__main__": + sys.exit(0 if UnitTesting.test_all() else -1)