diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 3e135126..00000000 --- a/.flake8 +++ /dev/null @@ -1,23 +0,0 @@ -[flake8] -count = True -max-line-length = 100 -max-doc-length = 100 -exclude = - .git, - __pycache__, - build, - dist, - node_modules, - venv, - .mypy_cache, - tests/projects, - tests/outcomes/**/main*.py, - tests/outcomes/**/cleaning.py, - tests/outcomes/**/pandas_*.py, - tests/outcomes/**/matplotlib_*.py, - tests/outcomes/**/seaborn_*.py -ignore = - # visually indented line with same indent as next logical line - E129, - # W504 line break after binary operator line break after binary operator - W504 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a961036c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.yml text eol=lf +*.yaml text eol=lf +*.py text eol=lf +*.toml text eol=lf +*.md text eol=lf +*.txt text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..bb611915 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "00:00" + groups: + all-actions: + patterns: [ "*" ] + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + time: "00:00" diff --git a/.github/workflows/actions/prepare/action.yml b/.github/workflows/actions/prepare/action.yml new file mode 100644 index 00000000..445a677c --- /dev/null +++ b/.github/workflows/actions/prepare/action.yml @@ -0,0 +1,23 @@ +name: 'Prepare environment' +description: 'Prepare environment' + +inputs: + python-version: + description: 'Python version to use' + required: true + +runs: + using: "composite" + steps: + - name: Install Poetry + run: pipx install poetry==$(head -n 1 .poetry-version) + shell: bash + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + cache: 'poetry' + + - name: Install dependencies and package + run: poetry install --no-interaction --no-ansi + shell: bash diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml new file mode 100644 index 00000000..dc9ee73d --- /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: [self-hosted, small] + timeout-minutes: 30 + if: ${{ !github.event.pull_request.assignee }} + steps: + - uses: toshimaru/auto-author-assign@v2.1.1 diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml new file mode 100644 index 00000000..6152b9c6 --- /dev/null +++ b/.github/workflows/auto-format.yml @@ -0,0 +1,35 @@ +name: Format code +on: + pull_request: + push: + branches: + - master + - release + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + format_backend: + name: Format with ruff + runs-on: [ self-hosted, 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 }} + + - uses: ./.github/workflows/actions/prepare + + - run: poetry run ruff check --fix --unsafe-fixes --preview --exit-zero . + - run: poetry run ruff format . + + - 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 100644 index 00000000..9fd9b9d1 --- /dev/null +++ b/.github/workflows/build-wheels.yml @@ -0,0 +1,87 @@ +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: + include: + # Linux builds + - os: ubuntu-latest + python-version: '3.10' + - os: ubuntu-latest + python-version: '3.11' + - os: ubuntu-latest + python-version: '3.12' + + # Windows builds + - os: windows-latest + python-version: '3.10' + - os: windows-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.12' + + # macOS builds + - os: macos-latest + python-version: '3.10' + - os: macos-latest + python-version: '3.11' + - os: macos-latest + python-version: '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: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: dist/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..204a7314 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +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: [ self-hosted, small ] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/workflows/actions/prepare + with: + python-version: "3.11" + - name: Check files using the ruff formatter + run: poetry run ruff --fix --unsafe-fixes --preview . + shell: bash + + mypy: + name: Static Type Checking + runs-on: [ self-hosted, small ] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/workflows/actions/prepare + with: + python-version: "3.11" + - name: Mypy + run: poetry run mypy . + shell: bash + + test: + name: Run unit test on ${{ matrix.os }} with Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Self-hosted runner + - os: [ self-hosted, small ] + python-version: "3.10" + - os: [ self-hosted, small ] + python-version: "3.11" + - os: [ self-hosted, small ] + python-version: "3.12" + + # Windows + - os: windows-latest + python-version: "3.10.11" + - os: windows-latest + python-version: "3.11" + - os: windows-latest + python-version: "3.12" + + # macOS (arm64) + - os: macos-14 + python-version: "3.11" + - os: macos-14 + python-version: "3.12" + steps: + - uses: actions/checkout@v4 + - uses: ./.github/workflows/actions/prepare + with: + python-version: ${{ matrix.python-version }} + - name: Run Tests + run: poetry run python tests/testing.py + shell: bash 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 100644 index 00000000..943f9cbc --- /dev/null +++ b/.poetry-version @@ -0,0 +1 @@ +1.7.1 diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..9919bf8c --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.13 diff --git a/README.md b/README.md index 9f92aa73..c7555ed6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,33 @@ # hs-test-python + +Python testing library for Hyperskill projects + It is a framework that simplifies testing educational projects for [Hyperskill](https://hyperskill.org). -It is required to use for Hyperskill projects. The main features are: +The main features are: * black box testing * multiple types of tests in a simple unified way (without stdin, with stdin, files, Django, Flask, Matplotlib) * generating learner-friendly feedback (filtering stack-traces, hints) +## Installation + +Install the package directly from GitHub: + +```bash +pip install https://github.com/hyperskill/hs-test-python/archive/release.tar.gz +``` + +The package includes pre-built wheels for psutil, so you don't need a C++ compiler to install it. + +## Development + +To contribute to the project: + +1. Clone the repository +2. Install dependencies with poetry: +```bash +poetry install +``` + To learn how to use this library you can go here: https://github.com/hyperskill/hs-test-python/wiki 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/__init__.py b/hstest/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/common/file_utils.py b/hstest/common/file_utils.py index 099c5aee..9504e514 100644 --- a/hstest/common/file_utils.py +++ b/hstest/common/file_utils.py @@ -1,43 +1,43 @@ +from __future__ import annotations + +import contextlib import os -from typing import Dict +from pathlib import Path 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: - f.write(content) + Path(file).write_text(content, encoding="locale") -def delete_files(files: Dict[str, str]): - for file in files.keys(): - if os.path.isfile(file): +def delete_files(files: dict[str, str]) -> None: + for file in map(Path, files): + if file.is_file(): try: - os.remove(file) - except PermissionError: - raise FileDeletionError() + file.unlink() + except PermissionError as ex: + raise FileDeletionError from ex -def safe_delete(filename): - if os.path.exists(filename): - try: - os.remove(filename) - except BaseException: - pass +def safe_delete(filename: str) -> None: + with contextlib.suppress(BaseException): + Path(filename).unlink(missing_ok=True) -def walk_user_files(folder): - curr_folder = os.path.abspath(folder) - test_folder = os.path.join(curr_folder, 'test') +def walk_user_files(curr_folder: Path) -> tuple[Path, list[str], list[str]]: + curr_folder = curr_folder.resolve() + test_folder = curr_folder / "test" for folder, dirs, files in os.walk(curr_folder): - if folder.startswith(test_folder): + folder_ = Path(folder) + if folder_.is_relative_to(test_folder): continue - if folder == curr_folder: - for file in 'test.py', 'tests.py': + if folder_ == curr_folder: + for file in ("test.py", "tests.py"): if file in files: files.remove(file) - yield folder, dirs, files + yield Path(folder_), dirs, files diff --git a/hstest/common/os_utils.py b/hstest/common/os_utils.py index b3364de5..08498421 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' +def is_windows() -> bool: + return platform.system() == "Windows" -def is_mac(): - return platform.system() == 'Darwin' +def is_mac() -> bool: + return platform.system() == "Darwin" -def is_linux(): - return platform.system() == 'Linux' +def is_linux() -> bool: + return platform.system() == "Linux" diff --git a/hstest/common/process_utils.py b/hstest/common/process_utils.py index c8457c02..f2d66e0e 100644 --- a/hstest/common/process_utils.py +++ b/hstest/common/process_utils.py @@ -1,42 +1,43 @@ -import sys +from __future__ import annotations + import threading import weakref from concurrent.futures import ThreadPoolExecutor -from concurrent.futures.thread import _worker +from concurrent.futures.thread import _worker # noqa: PLC2701 +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + import queue + from concurrent.futures.process import _WorkItem -from hstest.dynamic.security.thread_group import ThreadGroup + 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(_: int, q: queue.SimpleQueue[_WorkItem[Any]] = 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 @@ -44,7 +45,8 @@ def weakref_cb(_, q=self._work_queue): self._threads.add(t) -def is_port_in_use(port): +def is_port_in_use(port: int) -> bool: 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..f08f2404 100644 --- a/hstest/common/reflection_utils.py +++ b/hstest/common/reflection_utils.py @@ -1,45 +1,53 @@ +from __future__ import annotations + import inspect import os -from typing import List +from itertools import pairwise +from pathlib import Path +from typing import TYPE_CHECKING from hstest.exception.failure_handler import get_traceback_stack +if TYPE_CHECKING: + from hstest import StageTest + -def is_tests(stage): +def is_tests(stage: object) -> bool: 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: StageTest) -> None: if stage.is_tests: - test_file = inspect.getmodule(stage).__file__ - test_folder = os.path.dirname(test_file) + test_file = Path(inspect.getmodule(stage).__file__) + test_folder = test_file.parent os.chdir(test_folder) - if os.path.basename(os.getcwd()) == 'test': - os.chdir(os.path.dirname(os.getcwd())) + cwd = Path.cwd() + if cwd.name == "test": + os.chdir(cwd.parent) -def get_stacktrace(ex: BaseException, hide_internals=False) -> str: +def get_stacktrace(ex: BaseException, *, hide_internals: bool = 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 + user_dir = Path(ex.filename).parent.name + os.sep else: - user_dir = '' + user_dir = "" user_traceback = [] for tr in traceback_stack[::-1][1:-1]: @@ -47,23 +55,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 +78,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 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 +104,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 +113,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, E501 + 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 +142,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]) - if os.path.isdir(dir_name): - dir_names += [os.path.abspath(dir_name)] + dir_name = Path(tr[start_index:end_index]).parent + if dir_name.is_dir(): + dir_names += [dir_name.resolve()] if dir_names: from hstest.common.os_utils import is_windows + if is_windows(): drives = {} for dir_name in dir_names: @@ -154,28 +162,32 @@ 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): - # 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 ' lines that are always in the stacktrace - # but include , because it's definitely user's code - if not user_dir.startswith('<'): - if user_dir in trace: - trace = trace.replace(user_dir, '') + # Trace line that starts with 4 is a line with SyntaxError + # avoid including lines that are always in the stacktrace + # but include , because it's definitely user's code + if (not trace.startswith(" " * 4) and user_dir in trace) or ( + ("<" in trace and ">" in trace and " tuple[int, str]: + """Reports failure.""" if not is_unittest: lines = message.splitlines() - print('\n' + failed_msg_start + lines[0]) + print("\n" + failed_msg_start + lines[0]) # noqa: T201 for line in lines[1:]: - print(failed_msg_continue + line) + print(failed_msg_continue + line) # noqa: T201 return -1, message -def passed(is_unittest: bool): - """ Reports success """ +def passed(*, is_unittest: bool) -> tuple[int, str]: + """Reports success.""" if not is_unittest: - print('\n' + success_msg) - return 0, 'test OK' + print("\n" + success_msg) # noqa: T201 + 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/__init__.py b/hstest/dynamic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/dynamic/dynamic_test.py b/hstest/dynamic/dynamic_test.py index c0952a4b..55ad36fb 100644 --- a/hstest/dynamic/dynamic_test.py +++ b/hstest/dynamic/dynamic_test.py @@ -1,28 +1,31 @@ +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: Any | None = 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, +) -> Any: + """Decorator for creating dynamic tests.""" class DynamicTestingMethod: - def __init__(self, fn): + def __init__(self, fn: Any) -> None: self.fn = fn - def __set_name__(self, owner, name): + def __set_name__(self, owner: StageTest, name: str) -> None: # do something with owner, i.e. - # print(f"Decorating {self.fn} and using {owner}") + # print(f"Decorating {self.fn} and using {owner}") # noqa: ERA001 self.fn.class_name = owner.__name__ # then replace ourself with the original method @@ -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/__init__.py b/hstest/dynamic/input/__init__.py new file mode 100644 index 00000000..e69de29b 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..52c68da5 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)}.") + if not isinstance(self.files, dict): + 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( + if not isinstance(k, str): + 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)}.' ) - if type(v) != str: - raise UnexpectedError( + raise UnexpectedError(msg) + if not isinstance(v, str): + 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: StageTest) -> 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,24 @@ 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 # noqa: TRY301 + raise WrongAnswer(obj.feedback) # noqa: TRY301 else: - raise UnexpectedError( - 'Dynamic input should return ' + - f'str or CheckResult objects only. Found: {type(obj)}') - except BaseException as ex: + msg = ( + "Dynamic input should return " + f"str or CheckResult objects only. Found: {type(obj)}" + ) + raise UnexpectedError(msg) # noqa: TRY301 + except BaseException as ex: # noqa: BLE001 from hstest.stage_test import StageTest + StageTest.curr_test_run.set_error_in_test(ex) return None @@ -136,7 +153,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 +171,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 +189,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/__init__.py b/hstest/dynamic/output/__init__.py new file mode 100644 index 00000000..e69de29b 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..39b49897 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: Any) -> 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='') + print(full, end="") # noqa: T201 @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..656f914a 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,34 @@ 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 RealOutputMock: + def __init__(self, out: io.TextIOWrapper) -> None: + self.out = out + + def write(self, text: str) -> None: + if not ignore_stdout: + self.out.write(text) + + def flush(self) -> None: + self.out.flush() + + def close(self) -> None: + self.out.close() 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,46 +48,32 @@ class OutputMock: but also injected input from the test """ - def __init__(self, real_out: io.TextIOWrapper, is_stderr: bool = False): - class RealOutputMock: - def __init__(self, out: io.TextIOWrapper): - self.out = out - - def write(self, text): - if not ignore_stdout: - self.out.write(text) - - def flush(self): - self.out.flush() - - def close(self): - self.out.close() - + def __init__(self, real_out: io.TextIOWrapper, *, is_stderr: bool = False) -> None: 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 - def original(self): + def original(self) -> RealOutputMock: return self._original @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: str) -> None: partial_handler = self.__get_partial_handler() if partial_handler is None: @@ -86,37 +88,39 @@ 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): + def __get_partial_handler(self) -> list[str] | None: for handler in self._partial.values(): if handler.condition(): return handler.output diff --git a/hstest/dynamic/security/__init__.py b/hstest/dynamic/security/__init__.py new file mode 100644 index 00000000..e69de29b 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..5779beb2 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 @@ -24,24 +26,24 @@ class ExitHandler: _signal_siginterrupt = None @staticmethod - def is_replaced(): + def is_replaced() -> bool: 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..32bdec36 100644 --- a/hstest/dynamic/security/thread_group.py +++ b/hstest/dynamic/security/thread_group.py @@ -1,35 +1,37 @@ +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 # noqa: SLF001 else: - self._parent: Optional[ThreadGroup] = None + self._parent: ThreadGroup | None = None @property - def name(self): + def name(self) -> str: return self._name @property - def parent(self): + def parent(self) -> ThreadGroup | None: 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..6a0d72e2 100644 --- a/hstest/dynamic/security/thread_handler.py +++ b/hstest/dynamic/security/thread_handler.py @@ -1,34 +1,45 @@ +from __future__ import annotations + from threading import current_thread, Thread -from typing import Callable, Optional +from typing import Any, 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') - current_thread()._group = cls._group + cls._group = ThreadGroup("Main") + current_thread()._group = cls._group # noqa: SLF001 @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 - del current_thread()._group + del current_thread()._group # noqa: SLF001 cls._group = None @staticmethod - def init(self, group=None, target=None, name=None, - args=(), kwargs=None, *, daemon=None): - + def init( + self: Thread, # noqa: PLW0211 + group: ThreadGroup | None = None, + target: Callable[..., Any] | None = None, + name: str | None = None, + args: tuple[Any, ...] | None = (), + kwargs: dict[str, Any] | None = None, + *, + daemon: bool | None = None, + ) -> None: ThreadHandler._old_init(self, None, target, name, args, kwargs, daemon=daemon) # Custom addition to Thread class (implement thread groups) @@ -37,8 +48,8 @@ def init(self, group=None, target=None, name=None, else: curr = current_thread() - if hasattr(curr, '_group'): - self._group = curr._group + if hasattr(curr, "_group"): + self._group = curr._group # noqa: SLF001 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/__init__.py b/hstest/exception/__init__.py new file mode 100644 index 00000000..e69de29b 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..71bdace7 100644 --- a/hstest/exception/outcomes.py +++ b/hstest/exception/outcomes.py @@ -1,47 +1,47 @@ -from typing import Optional +from __future__ import annotations class OutcomeError(BaseException): pass -class SyntaxException(OutcomeError): - def __init__(self, exception: SyntaxError, file: str): +class SyntaxException(OutcomeError): # noqa: N818 + 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]): +class ExceptionWithFeedback(OutcomeError): # noqa: N818 + 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): +class ErrorWithFeedback(OutcomeError): # noqa: N818 + 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 -class TestPassed(OutcomeError): +class TestPassed(OutcomeError): # noqa: N818 pass -class WrongAnswer(OutcomeError): - def __init__(self, feedback: str): +class WrongAnswer(OutcomeError): # noqa: N818 + 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/__init__.py b/hstest/outcomes/__init__.py new file mode 100644 index 00000000..e69de29b 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..f182e90a 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. " + + "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' - if time_limit > 1999: + time_unit: str = "milliseconds" + if time_limit > 1999: # noqa: PLR2004 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..0460e1ed 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 @@ -70,10 +76,11 @@ def __str__(self): return result.strip() @staticmethod - def __get_args(): - arguments = '' + def __get_args() -> str: + 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() @@ -92,25 +99,27 @@ def __get_args(): return arguments @staticmethod - def __trim_lines(full_out): - result = '' + def __trim_lines(full_out: str) -> str: + 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 return result.strip() @staticmethod - def get_outcome(ex: BaseException, curr_test: int): + def get_outcome(ex: BaseException, curr_test: int) -> Outcome: from hstest.outcomes.compilation_error_outcome import CompilationErrorOutcome from hstest.outcomes.error_outcome import ErrorOutcome from hstest.outcomes.exception_outcome import ExceptionOutcome @@ -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..445eeefb 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: str = "", *, 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 - """ - return clean_text(urlopen(link).read().decode()) + """Deprecated, use get(...) instead.""" + return clean_text(urlopen(link).read().decode()) # noqa: S310 - 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()) + return clean_text(urlopen(link).read().decode()) # noqa: S310 diff --git a/hstest/stage/flask_test.py b/hstest/stage/flask_test.py index f0efdafe..12865eef 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: str = "", *, source: str = "") -> None: super().__init__(args, source=source) loop_detector.working = False Settings.do_reset_output = False @@ -21,40 +23,43 @@ def __init__(self, args='', *, source: str = ''): if self.source_name: sources = self.source_name - if type(sources) != list: + if not isinstance(sources, str): sources = [sources] for item in sources: - if type(item) == str: + if isinstance(item, str): self.attach.sources += [(item, None)] - elif type(item) == tuple: + elif isinstance(item, tuple): if len(item) == 1: self.attach.sources += [(item[0], None)] 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) -> str: + 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()) + return clean_text(urlopen(link).read().decode()) # noqa: S310 diff --git a/hstest/stage/plotting_test.py b/hstest/stage/plotting_test.py index 2be941e4..f472f975 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: str = "", *, 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..c077c4a7 100644 --- a/hstest/stage/sql_test.py +++ b/hstest/stage/sql_test.py @@ -1,4 +1,6 @@ -from typing import Dict +from __future__ import annotations + +from typing import Any, ClassVar from hstest.exception.outcomes import WrongAnswer from hstest.stage.stage_test import StageTest @@ -6,14 +8,14 @@ class SQLTest(StageTest): - queries: Dict[str, str] = dict() - db: any = None + queries: ClassVar[dict[str, str]] = {} + db: Any = None - def __init__(self, source: str = ''): + def __init__(self, source: str = "") -> None: super().__init__(source) self.runner = SQLRunner(self) - def execute(self, query_name: str): + def execute(self, query_name: str) -> Any: cursor = self.db.cursor() if query_name not in self.queries: @@ -22,7 +24,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) from ex - def execute_and_fetch_all(self, query_name: str): + def execute_and_fetch_all(self, query_name: str) -> list[tuple]: return self.execute(query_name).fetchall() diff --git a/hstest/stage/stage_test.py b/hstest/stage/stage_test.py index b2f1b7be..7f4fd7d8 100644 --- a/hstest/stage/stage_test.py +++ b/hstest/stage/stage_test.py @@ -1,6 +1,9 @@ -import os +from __future__ import annotations + +import contextlib import unittest -from typing import Any, Dict, List, Optional, Tuple, Type +from pathlib import Path +from typing import Any, ClassVar, 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) -> list[str]: 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: str = "", *, source: str = "") -> None: + super().__init__("test_run_unittest") self.is_tests = False if self.source: @@ -60,69 +71,65 @@ 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(Path.cwd()): 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) - for test_case in test_cases: + for curr_test, test_case in enumerate(test_cases, start=1): test_case.source_name = self.source_name if test_case.check_func is None: test_case.check_func = self.check 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: bool = False, is_unittest: bool = False) -> tuple[int, str]: curr_test: int = 0 need_tear_down: bool = False try: @@ -133,9 +140,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,66 +161,64 @@ 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 - raise WrongAnswer(full_feedback.strip()) + full_feedback = result.feedback + "\n\n" + test_run.test_case.feedback + raise WrongAnswer(full_feedback.strip()) # noqa: TRY301 if test_run.is_last_test(): need_tear_down = False test_run.tear_down() SystemHandler.tear_down() - return passed(is_unittest) + return passed(is_unittest=is_unittest) - except BaseException as ex: + except BaseException: # noqa: BLE001 if need_tear_down: try: StageTest.curr_test_run.tear_down() - except BaseException as new_ex: + except BaseException as new_ex: # noqa: BLE001 if isinstance(new_ex, OutcomeError): ex = new_ex - build = 'hs-test-python' + build = "hs-test-python" try: report = build + "\n\n" + get_report() - except Exception: + except Exception: # noqa: BLE001 report = build try: outcome: Outcome = Outcome.get_outcome(ex, curr_test) fail_text = str(outcome) - except BaseException as new_ex: + except BaseException as new_ex: # noqa: BLE001 try: outcome: Outcome = Outcome.get_outcome(new_ex, curr_test) fail_text = str(outcome) - except BaseException as new_ex2: + except BaseException as new_ex2: # noqa: BLE001 try: traceback = "" for e in new_ex2, new_ex, ex: try: text = get_exception_text(e) - except Exception: + except Exception: # noqa: BLE001 try: - text = f'{type(e)}: {str(e)}' - except Exception: - text = 'Broken exception' + text = f"{type(e)}: {e!s}" + except Exception: # noqa: BLE001 + 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: + except BaseException: # noqa: BLE001 # 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) + return failed(fail_text, is_unittest=is_unittest) finally: StageTest.curr_test_run = None @@ -221,18 +227,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: ClassVar[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/__init__.py b/hstest/test_case/attach/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/test_case/attach/django_settings.py b/hstest/test_case/attach/django_settings.py index 1e29f000..bd8e0c42 100644 --- a/hstest/test_case/attach/django_settings.py +++ b/hstest/test_case/attach/django_settings.py @@ -1,8 +1,10 @@ -from typing import List +from __future__ import annotations + +from typing import ClassVar 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: ClassVar[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..eb794728 100644 --- a/hstest/test_case/attach/flask_settings.py +++ b/hstest/test_case/attach/flask_settings.py @@ -1,6 +1,8 @@ -from typing import List, Tuple +from __future__ import annotations + +from typing import ClassVar class FlaskSettings: - sources: List[Tuple[str, int]] = [] - tryout_ports: List[int] = [i for i in range(8000, 8101)] + sources: ClassVar[list[tuple[str, int]]] = [] + tryout_ports: ClassVar[list[int]] = list(range(8000, 8101)) diff --git a/hstest/test_case/check_result.py b/hstest/test_case/check_result.py index f2e5e1b9..f3fe0c24 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: # noqa: FBT001 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(result=True, feedback="") @staticmethod - def wrong(feedback: str) -> 'CheckResult': - return CheckResult(False, feedback) + def wrong(feedback: str) -> CheckResult: + return CheckResult(result=False, feedback=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..1451e70b 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 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] - StepikTest = Union[SimpleStepikTest, AdvancedStepikTest] + AdvancedStepikTest = tuple[str, Any] + StepikTest = SimpleStepikTest | AdvancedStepikTest CheckFunction = Callable[[str, Any], CheckResult] PredefinedInput = str - RuntimeEvaluatedInput = Union[ - PredefinedInput, InputFunction, Tuple[int, InputFunction], DynamicInputFunction] - DynamicInput = Union[PredefinedInput, List[RuntimeEvaluatedInput]] + RuntimeEvaluatedInput = [ + PredefinedInput | InputFunction | tuple[int, InputFunction] | DynamicInputFunction + ] + DynamicInput = 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,63 +64,69 @@ def __init__( if copy_to_attach: if attach is not None: - raise UnexpectedError( - 'Attach is not None ' - 'but copying from stdin is specified') - if type(stdin) != str: - raise UnexpectedError( - 'To copy stdin to attach stdin should be of type str ' - f'but found type {type(stdin)}') + msg = "Attach is not None " "but copying from stdin is specified" + raise UnexpectedError(msg) + if not isinstance(stdin, str): + 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: + if isinstance(stdin, str): self.input = stdin - self.input_funcs = [DynamicInputFunction(1, lambda x: stdin)] + self.input_funcs = [DynamicInputFunction(1, lambda _: stdin)] else: - if type(stdin) != list: - raise UnexpectedError( - 'Stdin should be either of type str or list ' - f'but found type {type(stdin)}') - for elem in stdin: # type: RuntimeEvaluatedInput - if type(elem) == DynamicInputFunction: + if not isinstance(stdin, list): + msg = "Stdin should be either of type str or list " f"but found type {type(stdin)}" + raise UnexpectedError(msg) + for elem in stdin: + if isinstance(elem, DynamicInputFunction): self.input_funcs += [elem] - elif type(elem) == str: - self.input_funcs += [DynamicInputFunction(1, lambda x, inp=elem: inp)] + elif isinstance(elem, str): + self.input_funcs += [DynamicInputFunction(1, lambda _, 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): - if len(elem) == 2: + elif type(elem) in {tuple, list}: + if len(elem) == 2: # noqa: PLR2004 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)}') + if not isinstance(trigger_count, int): + 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): + def _custom_check(self, reply: str, expected: str) -> CheckResult: is_correct = reply.strip() == expected.strip() - return CheckResult(is_correct, '') + return CheckResult(is_correct, "") diff --git a/hstest/testing/__init__.py b/hstest/testing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/execution/__init__.py b/hstest/testing/execution/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/execution/filtering/__init__.py b/hstest/testing/execution/filtering/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/execution/filtering/file_filter.py b/hstest/testing/execution/filtering/file_filter.py index c44e5a54..2d8280e4 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 + def regex_filter(regex: str) -> Filter: + 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..f18566ed 100644 --- a/hstest/testing/execution/main_module_executor.py +++ b/hstest/testing/execution/main_module_executor.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import os import runpy import sys -from concurrent.futures import Future -from typing import Optional +from pathlib import Path +from typing import TYPE_CHECKING from hstest.common.process_utils import DaemonThreadPoolExecutor from hstest.dynamic.output.output_handler import OutputHandler @@ -13,34 +15,34 @@ 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()) + self.working_directory_before = Path.cwd().resolve() - 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) - except BaseException as ex: + except BaseException as ex: # noqa: BLE001 if StageTest.curr_test_run.error_in_test is None: # ExitException is thrown in case of exit() or quit() # consider them like normal exit @@ -48,30 +50,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/__init__.py b/hstest/testing/execution/process/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/execution/process/cpp_executor.py b/hstest/testing/execution/process/cpp_executor.py index 6849fb58..4b74146a 100644 --- a/hstest/testing/execution/process/cpp_executor.py +++ b/hstest/testing/execution/process/cpp_executor.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import os +from pathlib import Path from hstest.common.os_utils import is_windows from hstest.testing.execution.process_executor import ProcessExecutor @@ -6,27 +9,35 @@ 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] + self.without_extension = os.path.splitext(self.runnable.file)[0] # noqa: PTH122 if is_windows(): self.executable = self.without_extension - self.file_name = self.executable + '.exe' + self.file_name = Path(self.executable + ".exe") else: - 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] + self.executable = f"./{self.without_extension}" + self.file_name = Path(self.without_extension) + + def _compilation_command(self) -> list[str]: + 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) + def _execution_command(self, *args: str) -> list[str]: + return [self.executable, *list(args)] - def _cleanup(self): - if os.path.exists(self.file_name): - os.remove(self.file_name) + def _cleanup(self) -> None: + self.file_name.unlink(missing_ok=True) diff --git a/hstest/testing/execution/process/go_executor.py b/hstest/testing/execution/process/go_executor.py index 057ca243..d1c0befc 100644 --- a/hstest/testing/execution/process/go_executor.py +++ b/hstest/testing/execution/process/go_executor.py @@ -1,4 +1,6 @@ -import os +from __future__ import annotations + +from pathlib import Path from hstest.common.os_utils import is_windows from hstest.testing.execution.process_executor import ProcessExecutor @@ -6,28 +8,27 @@ 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 = Path(self.executable + ".exe") else: - self.executable = f'./{self.without_go}' - self.file_name = self.without_go + self.executable = f"./{self.without_go}" + self.file_name = Path(self.without_go) - def _compilation_command(self): - return ['go', 'build', self.runnable.file] + def _compilation_command(self) -> list[str]: + 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) + def _execution_command(self, *args: str) -> list[str]: + return [self.executable, *list(args)] - def _cleanup(self): - if os.path.exists(self.file_name): - os.remove(self.file_name) + def _cleanup(self) -> None: + self.file_name.unlink(missing_ok=True) diff --git a/hstest/testing/execution/process/javascript_executor.py b/hstest/testing/execution/process/javascript_executor.py index 8ec4dbaf..5efa9940 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) + def _execution_command(self, *args: str) -> list[str]: + 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..f504a3cb 100644 --- a/hstest/testing/execution/process/python_executor.py +++ b/hstest/testing/execution/process/python_executor.py @@ -1,10 +1,12 @@ +from __future__ import annotations + 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)) - def _execution_command(self, *args: str): - return ['python', '-u', self.runnable.file] + list(args) + def _execution_command(self, *args: str) -> list[str]: + return ["python", "-u", self.runnable.file, *list(args)] diff --git a/hstest/testing/execution/process/shell_executor.py b/hstest/testing/execution/process/shell_executor.py index 71387ea5..724a1062 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) + def _execution_command(self, *args: str) -> list[str]: + 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..834028f6 100644 --- a/hstest/testing/execution/process_executor.py +++ b/hstest/testing/execution/process_executor.py @@ -1,7 +1,11 @@ +from __future__ import annotations + +import contextlib import os +from pathlib import Path 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 +15,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.working_directory_before = os.path.abspath(os.getcwd()) + self.__group: ThreadGroup | None = None + self.working_directory_before = Path.cwd().resolve() - 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 +62,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 +85,103 @@ 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') - 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' - ) - break + OutputHandler.print("Handle process - EXIT EXCEPTION, stop input") + if self._wait_if_terminated() and isinstance( + 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" + ) + break self.stop_input() - except BaseException as ex: - OutputHandler.print(f'Handle process - SOME EXCEPTION {ex}') + except BaseException as ex: # noqa: BLE001 + 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 _wait_if_terminated(self) -> bool: + return try_many_times(100, 10, lambda: self.process.is_finished(need_wait_output=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): - working_directory_before = os.path.abspath(os.getcwd()) + def tear_down(self) -> None: + working_directory_before = Path.cwd().resolve() 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..c127de14 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,36 +57,38 @@ 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" + + 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() return "" if not self.is_waiting_input(): - raise UnexpectedError( - f"Program {self} is not waiting for the input " + - f"(state == \"{self._machine.state}\")") + msg = ( + f"Program {self} is not waiting for the input " + f'(state == "{self._machine.state}")' + ) + raise UnexpectedError(msg) 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 +100,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 +130,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 +142,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): + def is_in_background(self) -> bool: 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/__init__.py b/hstest/testing/execution/runnable/__init__.py new file mode 100644 index 00000000..e69de29b 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..a246eaa3 100644 --- a/hstest/testing/execution/runnable/runnable_file.py +++ b/hstest/testing/execution/runnable/runnable_file.py @@ -1,7 +1,17 @@ -from hstest.testing.execution.filtering.file_filter import File, Folder +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + from hstest.testing.execution.filtering.file_filter import File class RunnableFile: - def __init__(self, folder: Folder, file: File): + def __init__(self, folder: Path, file: File) -> None: self.folder = folder self.file = file + + def path(self) -> Path: + return self.folder / self.file diff --git a/hstest/testing/execution/searcher/__init__.py b/hstest/testing/execution/searcher/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/execution/searcher/base_searcher.py b/hstest/testing/execution/searcher/base_searcher.py index 9457d3cf..d4501d52 100644 --- a/hstest/testing/execution/searcher/base_searcher.py +++ b/hstest/testing/execution/searcher/base_searcher.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import os import re -from typing import Dict, List, Optional, Set, Tuple, Union +from pathlib import Path from hstest.common.file_utils import walk_user_files from hstest.exception.outcomes import ErrorWithFeedback, UnexpectedError -from hstest.testing.execution.filtering.file_filter import File, FileFilter, Folder, Module, Source +from hstest.testing.execution.filtering.file_filter import File, FileFilter, Module, Source from hstest.testing.execution.filtering.main_filter import MainFilter from hstest.testing.execution.runnable.runnable_file import RunnableFile @@ -15,54 +17,52 @@ 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: Path | 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: Path, files: list[Path]) -> dict[Path, Source]: contents = {} for file in files: - path = os.path.abspath(os.path.join(folder, file)) + path = (folder / file).resolve() if path in file_contents_cached: contents[file] = file_contents_cached[path] - elif os.path.exists(path): - with open(path) as f: - try: - file_content = f.read() - except UnicodeDecodeError: - # binary bile, no need to process - continue - contents[file] = file_content - file_contents_cached[path] = contents[file] + elif path.exists(): + try: + file_content = Path(path).read_text(encoding="locale") + except UnicodeDecodeError: + # binary bile, no need to process + continue + contents[file] = file_content + file_contents_cached[path] = contents[file] return contents def _search_non_cached( self, - where_to_search: str, + where: Path, *, file_filter: FileFilter, 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): + curr_folder = where.resolve() + 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,11 @@ 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: " + f"{should_contain}" ) + raise ErrorWithFeedback(msg) if len(filtered_files) == 1: file = filtered_files.pop() @@ -116,52 +118,61 @@ 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: Path | 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("."): + msg = f'File extension "{self.extension}" should start with a dot' + raise UnexpectedError(msg) - if not self.extension.startswith('.'): - raise UnexpectedError(f'File extension "{self.extension}" should start with a dot') - - if where_to_search is None: - where_to_search = os.getcwd() + if where is None: + where = Path.cwd() do_caching = False - cache_key = self.extension, where_to_search + cache_key = self.extension, where if file_filter is None: if cache_key in search_cached: @@ -180,7 +191,7 @@ def _search( post_main_filter = FileFilter() result = self._search_non_cached( - where_to_search, + where, file_filter=file_filter, pre_main_filter=pre_main_filter, main_filter=main_filter, @@ -193,68 +204,67 @@ 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: Path, + 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 + where, + 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='') + def _base_search(self, where: Path) -> RunnableFile: + return self._simple_search(where, 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 source_folder, source_file, source_module = self._parse_source(source) - if source_folder is not None and os.path.isdir(source_folder): + if source_folder is not None and source_folder.is_dir(): 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 source_file.is_file(): + path, _sep, file = source_module.rpartition(".") + folder = Path(path.replace(".", os.sep)).resolve() 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 = Path(path.replace(".", os.sep)).resolve() + 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[Path, Path, Module]: ext = self.extension - source = source.replace('/', os.sep).replace('\\', os.sep) + source = Path(source.replace("/", os.sep).replace("\\", os.sep)) - if source.endswith(ext): + if source.name.endswith(ext): source_folder = None source_file = source - source_module = source[:-len(ext)].replace(os.sep, '.') + source_module = str(source)[:-len(ext)].replace(os.sep, ".") - elif os.sep in source: - if source.endswith(os.sep): - source = source[:-len(os.sep)] + elif os.sep in str(source): + if source.name.endswith(os.sep): + source = str(source)[:-len(os.sep)] source_folder = source source_file = None - source_module = source.replace(os.sep, '.') + source_module = source.name.replace(os.sep, ".") else: - source_folder = source.replace('.', os.sep) + source_folder = source.name.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..e730104e 100644 --- a/hstest/testing/execution/searcher/cpp_searcher.py +++ b/hstest/testing/execution/searcher/cpp_searcher.py @@ -1,25 +1,28 @@ +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 pathlib import Path -class CppSearcher(BaseSearcher): + from hstest.testing.execution.runnable.runnable_file import RunnableFile + +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: Path | 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..ae605fab 100644 --- a/hstest/testing/execution/searcher/go_searcher.py +++ b/hstest/testing/execution/searcher/go_searcher.py @@ -1,30 +1,32 @@ +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 pathlib import Path + + 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: Path | 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..b2fcfdfe 100644 --- a/hstest/testing/execution/searcher/javascript_searcher.py +++ b/hstest/testing/execution/searcher/javascript_searcher.py @@ -1,12 +1,19 @@ -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 pathlib import Path -class JavascriptSearcher(BaseSearcher): + from hstest.testing.execution.runnable.runnable_file import RunnableFile + +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: Path | 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..88aab919 100644 --- a/hstest/testing/execution/searcher/python_searcher.py +++ b/hstest/testing/execution/searcher/python_searcher.py @@ -1,35 +1,40 @@ -import os +from __future__ import annotations + import re -from typing import Optional +from pathlib import Path +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(): + for file in sources: 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(): + for source in sources.values(): for f, (r1, r2) in import_regexes.items(): if r1.search(source) is not None or r2.search(source) is not None: is_imported[f] = True @@ -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 = {Path.cwd()}") 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..b262b170 100644 --- a/hstest/testing/execution/searcher/shell_searcher.py +++ b/hstest/testing/execution/searcher/shell_searcher.py @@ -1,12 +1,19 @@ -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 pathlib import Path -class ShellSearcher(BaseSearcher): + from hstest.testing.execution.runnable.runnable_file import RunnableFile + +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: Path | 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..4e98c216 100644 --- a/hstest/testing/execution/searcher/sql_searcher.py +++ b/hstest/testing/execution/searcher/sql_searcher.py @@ -1,12 +1,19 @@ -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 pathlib import Path -class SQLSearcher(BaseSearcher): + from hstest.testing.execution.runnable.runnable_file import RunnableFile + +class SQLSearcher(BaseSearcher): @property def extension(self) -> str: - return '.sql' + return ".sql" - def search(self, where: str = None) -> RunnableFile: + def search(self, where: Path | 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/__init__.py b/hstest/testing/plotting/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/plotting/drawing/__init__.py b/hstest/testing/plotting/drawing/__init__.py new file mode 100644 index 00000000..e69de29b 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..ee89c987 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 @@ -5,7 +7,7 @@ class DrawingBuilder: @staticmethod - def get_hist_drawing(data, library, kwargs) -> Drawing: + def get_hist_drawing(data: list[float] | str, library: str, kwargs: dict[str, str]) -> Drawing: return Drawing( library, DrawingType.hist, @@ -14,7 +16,9 @@ def get_hist_drawing(data, library, kwargs) -> Drawing: ) @staticmethod - def get_line_drawing(x, y, library, kwargs) -> Drawing: + def get_line_drawing( + x: list[float], y: list[float], library: str, kwargs: dict[str, str] + ) -> Drawing: return Drawing( library, DrawingType.line, @@ -23,7 +27,9 @@ def get_line_drawing(x, y, library, kwargs) -> Drawing: ) @staticmethod - def get_scatter_drawing(x, y, library, kwargs) -> Drawing: + def get_scatter_drawing( + x: list[float], y: list[float], library: str, kwargs: dict[str, str] + ) -> Drawing: return Drawing( library, DrawingType.scatter, @@ -32,7 +38,9 @@ def get_scatter_drawing(x, y, library, kwargs) -> Drawing: ) @staticmethod - def get_pie_drawing(x, y, library, kwargs) -> Drawing: + def get_pie_drawing( + x: list[float], y: list[float], library: str, kwargs: dict[str, str] + ) -> Drawing: return Drawing( library, DrawingType.pie, @@ -41,7 +49,9 @@ def get_pie_drawing(x, y, library, kwargs) -> Drawing: ) @staticmethod - def get_bar_drawing(x, y, library, kwargs) -> Drawing: + def get_bar_drawing( + x: list[float], y: list[float], library: str, kwargs: dict[str, str] + ) -> Drawing: return Drawing( library, DrawingType.bar, diff --git a/hstest/testing/plotting/drawing/drawing_data.py b/hstest/testing/plotting/drawing/drawing_data.py index 952e4915..ba4f6805 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: + if not isinstance(x, list | None): x = list(x) - if type(y) != list and y is not None: + if not isinstance(y, list | None): y = list(y) - except Exception: - raise ValueError('The data argument should be an array') + except Exception as e: + msg = "The data argument should be an array" + raise ValueError(msg) from e 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..f03dddca 100644 --- a/hstest/testing/plotting/drawing_data_normalizer.py +++ b/hstest/testing/plotting/drawing_data_normalizer.py @@ -1,39 +1,42 @@ +from __future__ import annotations + import numpy as np class DrawingDataNormalizer: - @staticmethod - def normalize_x_y_data(x, y) -> np.ndarray: + def normalize_x_y_data(x: list[float], y: list[float]) -> np.ndarray: try: - if type(x) != list: + if not isinstance(x, list): x = list(x) - if type(y) != list: + if not isinstance(y, list): y = list(y) - except Exception: - raise ValueError('The data argument should be an array') + except Exception as e: + msg = "The data argument should be an array" + raise ValueError(msg) from e 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: + def normalize_hist_data(data: list[float] | str) -> np.ndarray: + if isinstance(data, str): data = [data] - if type(data) != list: + if not isinstance(data, list): try: data = list(data) - except Exception: - raise ValueError('The data argument should be an array') + except Exception as e: + msg = "The data argument should be an array" + raise ValueError(msg) from e return np.array(data, dtype=object) @@ -73,19 +76,20 @@ 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: + def normalize_bar_data(x: list[float], y: list[float]) -> np.ndarray: return DrawingDataNormalizer.normalize_x_y_data(x, y) @staticmethod - def normalize_line_data(x, y) -> np.ndarray: + def normalize_line_data(x: list[float], y: list[float]) -> np.ndarray: return DrawingDataNormalizer.normalize_x_y_data(x, y) @staticmethod - def normalize_scatter_data(x, y) -> np.ndarray: + def normalize_scatter_data(x: list[float], y: list[float]) -> np.ndarray: return DrawingDataNormalizer.normalize_x_y_data(x, y) @staticmethod - def normalize_pie_data(x, y) -> np.ndarray: + def normalize_pie_data(x: list[float], y: list[float]) -> np.ndarray: return DrawingDataNormalizer.normalize_x_y_data(x, y) diff --git a/hstest/testing/plotting/matplotlib_handler.py b/hstest/testing/plotting/matplotlib_handler.py index 32388b3e..7481c2a9 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 typing import Final, 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 @@ -17,6 +20,8 @@ if TYPE_CHECKING: from hstest.testing.runner.plot_testing_runner import DrawingsStorage +NUM_SHAPES: Final = 2 + class MatplotlibHandler: _saved = False @@ -37,159 +42,143 @@ 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): + def hist(x: list[float], *args, data: list[float] | None = None, **kw) -> None: 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: + except Exception: # noqa: BLE001, S110 pass 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) + if len(x.shape) == NUM_SHAPES: + from matplotlib import cbook + + x = np.array(cbook._reshape_2D(x, "x"), dtype=object) # noqa: SLF001 - if len(x.shape) == 2: + if len(x.shape) == NUM_SHAPES: 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): + def bar( + x: list[float], height: list[float], *args, data: list[float] | None = None, **kw + ) -> None: 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: + except Exception: # noqa: BLE001, S110 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): + def barh( + x: list[float], width: list[float], *args, data: list[float] | None = None, **kw + ) -> None: 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: list[float], y: list[float], *a, **kwargs) -> None: drawings.append( DrawingBuilder.get_scatter_drawing( - x, y, + x, + y, DrawingLibrary.matplotlib, kwargs, ) ) - def pie(x, *a, **kw): + def pie(x: list[float], *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: list[float], *, data: list[float] | None = 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: list[float], **kwargs) -> None: curr_data = { # noqa: F841 - 'x': np.array(x, dtype=object) + "x": np.array(x, dtype=object) } drawing = Drawing( @@ -200,10 +189,10 @@ def imshow(x, **kwargs): ) drawings.append(drawing) - def boxplot(x, **kwargs): + def boxplot(x: list[float], **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,47 +203,47 @@ def boxplot(x, **kwargs): ) drawings.append(drawing) - import matplotlib.axes - - class CustomMatplotlibAxes(matplotlib.axes.Axes): + import matplotlib as mpl - def hist(self, x, *a, **kw): + class CustomMatplotlibAxes(mpl.axes.Axes): + def hist(self, x: list[float], *a, **kw) -> None: hist(x, *a, **kw) - def bar(self, x, height, *a, **kw): + def bar(self, x: list[float], height: list[float], *a, **kw) -> None: bar(x, height, *a, **kw) - def barh(self, y, width, *a, **kw): + def barh(self, y: list[float], width: list[float], *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: list[float], y: list[float], *a, **kwargs) -> None: scatter(x, y, *a, **kwargs) - def pie(self, x, *a, **kw): + def pie(self, x: list[float], *a, **kw) -> None: pie(x, *a, **kw) - def violinplot(self, dataset, **kwargs): + def violinplot(self, dataset: list[float], **kwargs) -> None: violinplot(dataset, **kwargs) - def imshow(self, x, **kwargs): + def imshow(self, x: list[float], **kwargs) -> None: imshow(x, **kwargs) - def boxplot(self, x, **kwargs): + def boxplot(self, x: list[float], **kwargs) -> None: boxplot(x, **kwargs) - import matplotlib + import matplotlib as mpl if not MatplotlibHandler._saved: - MatplotlibHandler._Axes = deepcopy(matplotlib.axes.Axes) + MatplotlibHandler._Axes = deepcopy(mpl.axes.Axes) # should be replaced before import matplotlib.pyplot as plt - matplotlib.axes.Axes = CustomMatplotlibAxes + mpl.axes.Axes = CustomMatplotlibAxes from matplotlib.projections import projection_registry - projection_registry.register(matplotlib.axes.Axes) + + projection_registry.register(mpl.axes.Axes) import matplotlib.pyplot as plt @@ -270,7 +259,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 +272,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..36e86dd7 100644 --- a/hstest/testing/plotting/pandas_handler.py +++ b/hstest/testing/plotting/pandas_handler.py @@ -1,11 +1,12 @@ -from typing import TYPE_CHECKING +from __future__ import annotations + +import contextlib +from typing import ClassVar, Final, 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 @@ -20,277 +23,237 @@ from hstest.testing.plotting.matplotlib_handler import MatplotlibHandler if TYPE_CHECKING: - from hstest.testing.runner.plot_testing_runner import DrawingsStorage + from collections.abc import Callable + from hstest.testing.runner.plot_testing_runner import DrawingsStorage -class PandasHandler: - _saved = False - _replaced = False +NUM_SHAPES: Final = 2 - _PlotAccessor = None - _series_plot = None - _dframe_plot = None +def get_line_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + if type(data) is pd.Series: + return [DrawingBuilder.get_line_drawing(data.index, data, DrawingLibrary.pandas, {})] - _series_hist = None - _dframe_hist = None + return [ + DrawingBuilder.get_line_drawing(data.index, data[column], DrawingLibrary.pandas, {}) + for column in data.columns + ] - _series_bar = None - _dframe_bar = None - _series_boxplot = None - _dframe_boxplot = None +def get_hexbin_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + drawings = [] + drawing = Drawing(DrawingLibrary.pandas, DrawingType.hexbin, None, {}) + drawings.append(drawing) + return drawings - 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), - } +def get_area_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + drawings = [] + drawing = Drawing(DrawingLibrary.pandas, DrawingType.area, None, {}) + drawings.append(drawing) + return drawings - @staticmethod - def get_line_drawings_with_normalized_data(data, x, y): - drawings = list() - if type(data) is pd.Series: - drawings.append( - DrawingBuilder.get_line_drawing( - data.index, - data, - DrawingLibrary.pandas, - {} - ) - ) - return drawings +def get_dis_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + drawings = [] - for column in data.columns: - drawings.append( - DrawingBuilder.get_line_drawing( - data.index, - data[column], - DrawingLibrary.pandas, - {} - ) - ) + if type(data) == pd.Series: + curr_data = {"x": data.to_numpy()} + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) + drawings.append(drawing) return drawings - @staticmethod - def get_scatter_drawings_with_normalized_data(data, x, y): - return [ - DrawingBuilder.get_scatter_drawing( - data[x], data[y], - DrawingLibrary.pandas, - {} - ) - ] - - @staticmethod - 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()), - {} - ) - ] + if x: + curr_data = { + "x": np.array(data[x], dtype=object), + } - if y is not None: - return [ - Drawing( - DrawingLibrary.pandas, - DrawingType.pie, - DrawingData(data.index.to_numpy(), data[y].to_numpy()), - {} - ) - ] + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) + drawings.append(drawing) + if y: + curr_data = { + "x": np.array(data[y], dtype=object), + } - drawings = [] + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) + drawings.append(drawing) + if not x and not y: for column in data.columns: if not is_numeric_dtype(data[column]): continue - drawings.append( - Drawing( - 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): - drawings = [] - if x is not None: - x_arr = data[x].to_numpy() - else: - x_arr = data.index.to_numpy() + curr_data = { # noqa: F841 + "x": data[column].to_numpy() + } - if y is not None: - drawing = DrawingBuilder.get_bar_drawing( - x_arr, data[y], - DrawingLibrary.pandas, - {} - ) + drawing = Drawing(DrawingLibrary.pandas, DrawingType.dis, None, {}) drawings.append(drawing) - return drawings + return drawings + + +def get_box_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + drawings = [] + # Columns are not specified + if x is None: for column in data.columns: if not is_numeric_dtype(data[column]): continue - drawing = DrawingBuilder.get_bar_drawing( - x_arr, data[column], - DrawingLibrary.pandas, - {} - ) + + curr_data = {"x": np.array([column], dtype=object), "y": data[column].to_numpy()} + + drawing = Drawing(DrawingLibrary.pandas, DrawingType.box, None, {}) drawings.append(drawing) return drawings - @staticmethod - def get_box_drawings_with_normalized_data(data: 'pd.DataFrame', x, y): - - drawings = [] + for column in x: + if not is_numeric_dtype(data[column]): + continue - # Columns are not specified - if x is None: - for column in data.columns: - 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 = { # noqa: F841 - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() - } + drawing = Drawing(DrawingLibrary.pandas, DrawingType.box, None, {}) + drawings.append(drawing) + return drawings - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.box, - None, - {} - ) - drawings.append(drawing) - return drawings - for column in x: - if not is_numeric_dtype(data[column]): - continue +def get_bar_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + drawings = [] - curr_data = { # noqa: F841 - 'x': np.array([column], dtype=object), - 'y': data[column].to_numpy() - } + x_arr = data[x].to_numpy() if x is not None else data.index.to_numpy() - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.box, - None, - {} - ) - drawings.append(drawing) + if y is not None: + drawing = DrawingBuilder.get_bar_drawing(x_arr, data[y], DrawingLibrary.pandas, {}) + drawings.append(drawing) return drawings - @staticmethod - def get_dis_drawings_with_normalized_data(data, x, y): - 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, {}) + drawings.append(drawing) + return drawings - if type(data) == pd.Series: - curr_data = { - 'x': data.to_numpy() - } - drawing = Drawing( +def get_pie_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + if type(data) == pd.Series: + return [ + Drawing( DrawingLibrary.pandas, - DrawingType.dis, - None, - {} + DrawingType.pie, + DrawingData(data.index.to_numpy(), data.to_numpy()), + {}, ) - drawings.append(drawing) - return drawings - - if x: - curr_data = { - 'x': np.array(data[x], dtype=object), - } + ] - drawing = Drawing( + if y is not None: + return [ + Drawing( DrawingLibrary.pandas, - DrawingType.dis, - None, - {} + DrawingType.pie, + DrawingData(data.index.to_numpy(), data[y].to_numpy()), + {}, ) - drawings.append(drawing) - if y: - curr_data = { - 'x': np.array(data[y], dtype=object), - } + ] - drawing = Drawing( + drawings = [] + + for column in data.columns: + if not is_numeric_dtype(data[column]): + continue + drawings.append( + Drawing( DrawingLibrary.pandas, - DrawingType.dis, - None, - {} + DrawingType.pie, + DrawingData(data.index.to_numpy(), data[column].to_numpy()), + {}, ) - drawings.append(drawing) + ) + return drawings - if not x and not y: - for column in data.columns: - if not is_numeric_dtype(data[column]): - continue - curr_data = { # noqa: F841 - 'x': data[column].to_numpy() - } +def get_scatter_drawings_with_normalized_data( + data: pd.DataFrame, x: str | None, y: str | None +) -> list[Drawing]: + return [DrawingBuilder.get_scatter_drawing(data[x], data[y], DrawingLibrary.pandas, {})] - 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, - {} - ) - drawings.append(drawing) - return drawings +class PandasHandler: + _saved = False + _replaced = False + + _PlotAccessor = None + + _series_plot = None + _dframe_plot = None + + _series_hist = None + _dframe_hist = None + + _series_bar = None + _dframe_bar = None + + _series_boxplot = None + _dframe_boxplot = None + + plot_name_to_basic_name: ClassVar[dict[str, DrawingType]] = { + # 'barh': DrawingType.bar, # noqa: ERA001 + "density": DrawingType.dis, + "kde": DrawingType.dis, + } + + graph_type_to_normalized_data: ClassVar[ + dict[str, Callable[[pd.DataFrame, str | None, str | None], list[Drawing]]] + ] = { + "scatter": get_scatter_drawings_with_normalized_data, + "line": get_line_drawings_with_normalized_data, + "pie": get_pie_drawings_with_normalized_data, + # "bar": get_bar_drawings_with_normalized_data, # noqa: ERA001 + "box": get_box_drawings_with_normalized_data, + "dis": get_dis_drawings_with_normalized_data, + } @staticmethod - def get_hexbin_drawings_with_normalized_data(data, x, y): - drawings = [] - drawing = Drawing( - DrawingLibrary.pandas, - DrawingType.hexbin, - None, - {} - ) - drawings.append(drawing) - return drawings + def revert_plots() -> None: + if not PandasHandler._replaced: + return + + MatplotlibHandler.revert_plots() + + import pandas.plotting + from pandas.core.accessor import CachedAccessor + + pandas.Series.plot = CachedAccessor("plot", pandas.plotting.PlotAccessor) + pandas.DataFrame.plot = CachedAccessor("plot", pandas.plotting.PlotAccessor) + + pandas.Series.hist = PandasHandler._series_hist + pandas.DataFrame.hist = PandasHandler._dframe_hist + + pandas.DataFrame.boxplot = PandasHandler._dframe_boxplot + + PandasHandler._replaced = False @staticmethod - def replace_plots(drawings: 'DrawingsStorage'): + def replace_plots(drawings: DrawingsStorage) -> None: try: import pandas.plotting from pandas.core.accessor import CachedAccessor @@ -298,8 +261,8 @@ def replace_plots(drawings: 'DrawingsStorage'): return class CustomPlotAccessor(pandas.plotting.PlotAccessor): - def __call__(self, *args, **kw): - from pandas.plotting._core import _get_plot_backend + def __call__(self, *args, **kw) -> None: + from pandas.plotting._core import _get_plot_backend # noqa: PLC2701 plot_backend = _get_plot_backend(kw.pop("backend", None)) @@ -308,22 +271,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,162 +297,123 @@ 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 - ): - all_drawings = PandasHandler.get_box_drawings_with_normalized_data(self, column, None) + def boxplot(self: pandas.DataFrame, column: str | None = None, **kwargs) -> None: + all_drawings = get_box_drawings_with_normalized_data(self, column, None) drawings.extend(all_drawings) def hist( - data, - column=None, - _process_by=True, - **kw - ): + data: pandas.DataFrame | pandas.Series | np.ndarray, + column: str | None = None, + *, + _process_by: bool = True, + **kw, + ) -> None: 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 isinstance(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) + if len(data.shape) == NUM_SHAPES: + from matplotlib import cbook + + data = np.array(cbook._reshape_2D(data, "x"), dtype=object) # noqa: SLF001 - if len(data.shape) == 2: + if len(data.shape) == NUM_SHAPES: 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: pandas.DataFrame, x: str | None = None, y: str | None = None, **kw) -> None: for k in list(kw.keys()): if kw[k] is None: kw.pop(k) - if type(data) == pandas.DataFrame: + if isinstance(data, pandas.DataFrame): if y is not None and x is not None: - if type(y) == str: + if isinstance(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 - - elif y is not None: - if type(y) == str: + bar(None, data[x].array.to_numpy(), data[col].array.to_numpy(), **kw) + return None + + if y is not None: + if isinstance(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 isinstance(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, - ): + self: pandas.Series, + ) -> None: pass if not PandasHandler._saved: @@ -502,7 +425,7 @@ def barh( PandasHandler._series_hist = pandas.Series.hist PandasHandler._dframe_hist = pandas.DataFrame.hist - # PandasHandler._series_bar = pandas.Series.bar + # PandasHandler._series_bar = pandas.Series.bar # noqa: ERA001 PandasHandler._dframe_boxplot = pandas.DataFrame.boxplot @@ -515,23 +438,3 @@ def barh( pandas.DataFrame.boxplot = boxplot PandasHandler._replaced = True - - @staticmethod - def revert_plots(): - if not PandasHandler._replaced: - return - - MatplotlibHandler.revert_plots() - - import pandas.plotting - from pandas.core.accessor import CachedAccessor - - pandas.Series.plot = CachedAccessor("plot", pandas.plotting.PlotAccessor) - pandas.DataFrame.plot = CachedAccessor("plot", pandas.plotting.PlotAccessor) - - pandas.Series.hist = PandasHandler._series_hist - pandas.DataFrame.hist = PandasHandler._dframe_hist - - pandas.DataFrame.boxplot = PandasHandler._dframe_boxplot - - PandasHandler._replaced = False diff --git a/hstest/testing/plotting/seaborn_handler.py b/hstest/testing/plotting/seaborn_handler.py index b5b773b3..3ade1328 100644 --- a/hstest/testing/plotting/seaborn_handler.py +++ b/hstest/testing/plotting/seaborn_handler.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from importlib import reload -from typing import TYPE_CHECKING +from typing import Final, TYPE_CHECKING from hstest.testing.plotting.drawing.drawing_data import DrawingData @@ -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 @@ -18,6 +22,8 @@ if TYPE_CHECKING: from hstest.testing.runner.plot_testing_runner import DrawingsStorage +NUM_SHAPES: Final = 2 + class SeabornHandler: _saved = False @@ -35,22 +41,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: pd.DataFrame = 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 +73,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 +92,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( @@ -101,68 +104,69 @@ def displot(data=None, **kwargs): ) drawings.append(drawing) - def histplot(data=None, _process_hue=True, **kw): + def histplot( + data: pd.DataFrame | pd.Series | np.ndarray | None = None, + *, + _process_hue: bool = True, + **kw, + ) -> None: 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 isinstance(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) + if len(data.shape) == NUM_SHAPES: + from matplotlib import cbook + + data = np.array(cbook._reshape_2D(data, "x"), dtype=object) # noqa: SLF001 - if len(data.shape) == 2: + if len(data.shape) == NUM_SHAPES: 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() + def lineplot( + *, x: str | None = None, y: str | None = None, data: pd.DataFrame = None, **kwargs + ) -> DrawingsStorage | None: + 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 +193,16 @@ def lineplot(*, data=None, x=None, y=None, **kwargs): kwargs, ) ) + return None - def lmplot(x=None, y=None, data=None, **kwargs): + def lmplot( + x: str | None = None, y: str | None = None, data: pd.DataFrame = 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 +213,14 @@ def lmplot(x=None, y=None, data=None, **kwargs): ) drawings.append(drawing) - def scatterplot(x=None, y=None, data=None, **kwargs): + def scatterplot( + x: str | None = None, y: str | None = None, data: pd.DataFrame = 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 +235,21 @@ 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: str | None = None, y: str | None = None, data: pd.DataFrame = 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 +260,27 @@ def catplot(x=None, y=None, data=None, **kwargs): ) drawings.append(drawing) - def barplot(x=None, y=None, data=None, **kwargs): - + def barplot( + x: str | None = None, y: str | None = None, data: pd.DataFrame = 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: str | None = None, y: str | None = None, data: pd.DataFrame = None, **kwargs + ) -> None: if data is not None: if x is None and y is not None: data = data[y] @@ -278,13 +288,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 +304,12 @@ def violinplot(*, x=None, y=None, data=None, **kwargs): drawings.append(drawing) - def heatmap(data=None, **kwargs): + def heatmap(data: pd.DataFrame = 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 +321,11 @@ def heatmap(data=None, **kwargs): drawings.append(drawing) - def boxplot(x=None, y=None, data=None, **kwargs): - + def boxplot( + x: str | None = None, y: str | None = None, data: pd.DataFrame = 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 +346,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 +365,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 +404,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..78c89951 100644 --- a/hstest/testing/process_wrapper.py +++ b/hstest/testing/process_wrapper.py @@ -1,8 +1,10 @@ -import subprocess +from __future__ import annotations + +import subprocess # noqa: S404 import sys from threading import Lock, Thread from time import sleep -from typing import Optional +from typing import IO from psutil import NoSuchProcess, Process @@ -19,17 +21,22 @@ 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: bool = False, + register_output: bool = True, + register_io_handler: bool = 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 @@ -47,23 +54,23 @@ def __init__(self, *args, check_early_finish=False, register_output=True, self.register_io_handler = register_io_handler self._group = None - def start(self): - command = ' '.join(map(str, self.args)) + def start(self) -> ProcessWrapper: + 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] self.process = subprocess.Popen( args, @@ -72,12 +79,14 @@ 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, ) - except Exception as e: + except Exception as e: # noqa: BLE001 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 +96,36 @@ 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: IO, + write_pipe: IO, + *, + write_stdout: bool = False, + write_stderr: bool = 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 +133,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 +161,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 +172,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 +191,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 +205,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 +214,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 +234,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: bool = True) -> bool: if not self.check_early_finish: return not self._alive @@ -244,7 +260,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 +273,53 @@ def is_finished(self, need_wait_output=True): return not self._alive - def provide_input(self, stdin: str): + def provide_input(self, stdin: str) -> None: if self._use_byte_stream: stdin = stdin.encode() self.process.stdin.write(stdin) - 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 +328,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 +348,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/__init__.py b/hstest/testing/runner/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/runner/async_dynamic_testing_runner.py b/hstest/testing/runner/async_dynamic_testing_runner.py index 69faabda..b74f5eda 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,11 +51,10 @@ 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: + except BaseException as ex: # noqa: BLE001 test_run.set_error_in_test(ex) finally: test_run.invalidate_handlers() @@ -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,9 +72,8 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: if error is None: try: - return test_case.check_func( - OutputHandler.get_output(), test_case.attach) - except BaseException as ex: + return test_case.check_func(OutputHandler.get_output(), test_case.attach) + except BaseException as ex: # noqa: BLE001 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..2d184800 100644 --- a/hstest/testing/runner/django_application_runner.py +++ b/hstest/testing/runner/django_application_runner.py @@ -1,58 +1,69 @@ +from __future__ import annotations + import os import sys +from pathlib import Path 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_path = os.path.abspath(full_source) + full_source = Path(source.replace(".", os.sep) + ".py") + full_path = full_source.resolve() - if not os.path.exists(full_path): - filename = os.path.basename(full_source) - runnable = PythonSearcher().search( - file_filter=FileFilter(file=lambda f: f == filename)) - full_path = os.path.abspath(runnable.folder + os.sep + runnable.file) + if not full_path.exists(): + filename = full_source.name + runnable = PythonSearcher().search(file_filter=FileFilter(file=lambda f: f == filename)) + full_path = (Path(runnable.folder) / runnable.file).resolve() - self.full_path = full_path + self.full_path = full_path.name self.port = self.__find_free_port(test_case.attach.tryout_ports) if test_case.attach.use_database: 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 +79,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 Path(test_database).open("w", encoding="locale"): 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 +113,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 +135,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 @@ -132,8 +148,9 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: try: result = test_case.dynamic_testing() self._check_errors() - return result - except BaseException as ex: + except BaseException as ex: # noqa: BLE001 test_run.set_error_in_test(ex) + else: + return result return CheckResult.from_error(test_run.error_in_test) diff --git a/hstest/testing/runner/flask_application_runner.py b/hstest/testing/runner/flask_application_runner.py index 0a3391ff..7909d7ce 100644 --- a/hstest/testing/runner/flask_application_runner.py +++ b/hstest/testing/runner/flask_application_runner.py @@ -1,56 +1,66 @@ +from __future__ import annotations + import os import sys +from pathlib import Path from time import sleep -from typing import List, Optional, Tuple +from typing import ClassVar, 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: ClassVar[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_path = os.path.abspath(full_source) + full_source = Path(filename.replace(".", os.sep) + ".py") + full_path = full_source.resolve() - if not os.path.exists(full_path): - raise ErrorWithFeedback( - f'Cannot find file named "{os.path.basename(full_path)}" ' - f'in folder "{os.path.dirname(full_path)}". ' - f'Check if you deleted it.') + if not full_path.exists(): + msg = ( + f'Cannot find file named "{full_path.name}" ' + f'in folder "{full_path.parent}". ' + 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 +75,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 +90,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 @@ -110,8 +123,9 @@ def test(self, test_run: TestRun) -> Optional[CheckResult]: try: result = test_case.dynamic_testing() self._check_errors() - return result - except BaseException as ex: + except BaseException as ex: # noqa: BLE001 test_run.set_error_in_test(ex) + else: + return result return CheckResult.from_error(test_run.error_in_test) 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..7781fb63 100644 --- a/hstest/testing/runner/sql_runner.py +++ b/hstest/testing/runner/sql_runner.py @@ -1,4 +1,6 @@ -import os +from __future__ import annotations + +import contextlib import re import sqlite3 import typing @@ -9,57 +11,54 @@ from hstest.testing.runner.test_runner import TestRunner if typing.TYPE_CHECKING: + from hstest import SQLTest from hstest.testing.test_run import TestCase, TestRun class SQLRunner(TestRunner): - - def __init__(self, sql_test_cls): + def __init__(self, sql_test_cls: type[SQLTest]) -> 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) -> CheckResult | None: test_case = test_run.test_case try: - result = test_case.dynamic_testing() - return result - except BaseException as ex: + return test_case.dynamic_testing() + except BaseException as ex: # noqa: BLE001 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: - lines = file.readlines() - sql_content = " ".join(lines).replace("\n", "") - commands = re.findall(r'(\w+)\s+?=\s+?"(.*?)"', sql_content) - - for (name, query) in commands: - if not query: - raise WrongAnswer(f"The '{name}' query shouldn't be empty!") - 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!") - - def tear_down(self, test_case: 'TestCase'): - try: + file_path = SQLSearcher().search().path() + lines = file_path.read_text(encoding="locale").splitlines() + + sql_content = " ".join(lines).replace("\n", "") + commands = re.findall(r'(\w+)\s+?=\s+?"(.*?)"', sql_content) + + for name, query in commands: + if not query: + 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: + msg = f"Can't find '{name}' query from SQL files!" + raise WrongAnswer(msg) + + 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..1026eb0f 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) @@ -19,10 +24,10 @@ def add_transition(self, fr: Any, to: Any): def state(self) -> Any: return self._state - def in_state(self, state: Any): + def in_state(self, state: Any) -> bool: 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..a2d9a6dc 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,40 +56,40 @@ 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: create_files(self._test_case.files) - # startThreads(testCase.getProcesses()) + # startThreads(testCase.getProcesses()) # noqa: ERA001 if Settings.do_reset_output: OutputHandler.reset_output() @@ -92,10 +97,10 @@ def test(self) -> CheckResult: result = None try: result = self._test_runner.test(self) - except BaseException as ex: + except BaseException as ex: # noqa: BLE001 self.set_error_in_test(ex) - # stopThreads(testCase.getProcesses(), pool) + # stopThreads(testCase.getProcesses(), pool) # noqa: ERA001 delete_files(self._test_case.files) if result is None: @@ -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..b8d8716a 100644 --- a/hstest/testing/tested_program.py +++ b/hstest/testing/tested_program.py @@ -1,47 +1,56 @@ -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): + def run_args(self) -> list[str] | None: return self._run_args @property - def executor(self): + def executor(self) -> ProgramExecutor: 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/__init__.py b/hstest/testing/unittest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hstest/testing/unittest/expected_fail_test.py b/hstest/testing/unittest/expected_fail_test.py index 75987c6d..ab1a42a7 100644 --- a/hstest/testing/unittest/expected_fail_test.py +++ b/hstest/testing/unittest/expected_fail_test.py @@ -1,21 +1,22 @@ +from __future__ import annotations + from inspect import cleandoc -from typing import List, Union +from typing import ClassVar from hstest import StageTest class ExpectedFailTest(StageTest): - _base_contain: Union[str, List[str]] = [] - _base_not_contain: Union[str, List[str]] = [] + _base_contain: ClassVar[str | list[str]] = [] + _base_not_contain: ClassVar[str | list[str]] = [] - contain: Union[str, List[str]] = [] - not_contain: Union[str, List[str]] = [] + contain: ClassVar[str | list[str]] = [] + not_contain: ClassVar[str | list[str]] = [] - def __init__(self, args): + def __init__(self, args: str) -> None: super().__init__(args) - def test_run_unittest(self): - + 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") @@ -23,13 +24,13 @@ def test_run_unittest(self): self.assertEqual(result, -1) - if type(self._base_contain) != list: + if not isinstance(self._base_contain, list): self._base_contain = [self._base_contain] - if type(self._base_not_contain) != list: + if not isinstance(self._base_not_contain, list): self._base_not_contain = [self._base_not_contain] - if type(self.contain) != list: + if not isinstance(self.contain, list): self.contain = [self.contain] - if type(self.not_contain) != list: + if not isinstance(self.not_contain, list): self.not_contain = [self.not_contain] should_contain = self._base_contain + self.contain 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 100644 index 00000000..351f5450 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,984 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "contourpy" +version = "1.2.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, + {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, + {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, + {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, + {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, + {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, + {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, + {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, + {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, + {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, + {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, +] + +[package.dependencies] +numpy = ">=1.20" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "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 = "fonttools" +version = "4.51.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.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"}, + {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"}, + {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"}, + {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"}, + {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"}, + {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"}, + {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"}, + {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"}, + {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"}, + {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"}, + {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"}, + {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"}, + {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"}, + {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"}, + {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"}, + {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"}, + {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"}, + {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"}, + {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"}, + {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"}, + {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"}, + {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"}, + {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"}, + {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"}, + {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"}, + {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"}, + {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"}, + {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"}, + {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"}, + {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"}, + {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"}, + {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"}, + {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"}, + {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"}, + {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"}, + {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"}, + {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"}, + {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"}, + {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"}, + {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"}, + {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"}, + {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"}, +] + +[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 = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "matplotlib" +version = "3.9.2" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"}, + {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"}, + {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"}, + {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"}, + {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"}, + {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"}, + {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"}, + {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"}, + {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"}, + {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"}, + {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"}, + {file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"}, + {file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"}, + {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"}, +] + +[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)", "numpy (>=1.25)", "pybind11 (>=2.6)", "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 = ["main"] +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 = ["main"] +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 = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[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 = "10.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[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.10\"" +files = [ + {file = "psutil-5.8.0-cp310-cp310-linux_x86_64.whl", hash = "sha256:42e0ea03c226c387bd8cded5984f91f491359c26ac1827eb231076f34cc5d3d2"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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.10\"" +files = [ + {file = "psutil-5.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3410b4ab60a1d909e223104bf7a48fb9ede3eb058f29678f62622ff757a1da51"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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.10\"" +files = [ + {file = "psutil-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:7b79bca5b3532e65ddb35fae352ae09ab2f7c939da3b4f860a58e7f6e13d29df"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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:0553be18c29d96852bed5efb2d9ec45d0eff22f9e1a5c752fa7413909215800f"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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:4a1b843e133eba31289aee96334deebe6a69c9f0a7598066441fbd65e0992613"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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:e3273080f1000c7ab8a65714d0e97b3b14133ede4a67aae1aa66f4fd56228c95"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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\"" +files = [ + {file = "psutil-5.8.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:00e9a5b0ef778567cd7ddc0e1ec2051a93baefa43605209dc1e1e76779ce9d7a"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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\"" +files = [ + {file = "psutil-5.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab29673f576466d7eb34231a4b311676422a5bd8c2f1c0c76ee9b46ee6271f16"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/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\"" +files = [ + {file = "psutil-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:c659b4227bbcb9846403758e3a62ed22695930bf5bfe2602876da04a6a0e2ca7"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[package.source] +type = "url" +url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-win_amd64.whl" + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[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 = "2024.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-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[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 = ["main"] +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.13.0" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "scipy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d"}, + {file = "scipy-1.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e"}, + {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922"}, + {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4"}, + {file = "scipy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"}, + {file = "scipy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd"}, + {file = "scipy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa"}, + {file = "scipy-1.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5"}, + {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7"}, + {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d"}, + {file = "scipy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c"}, + {file = "scipy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6"}, + {file = "scipy-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b"}, + {file = "scipy-1.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551"}, + {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a"}, + {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42"}, + {file = "scipy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820"}, + {file = "scipy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21"}, + {file = "scipy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602"}, + {file = "scipy-1.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78"}, + {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5"}, + {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d"}, + {file = "scipy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86"}, + {file = "scipy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e"}, + {file = "scipy-1.13.0.tar.gz", hash = "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e"}, +] + +[package.dependencies] +numpy = ">=1.22.4,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "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.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "tzdata" +version = "2024.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-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "804085dc79fc7a4c17d4e5cde03b4e17cfe8f928193cabdd74161b2010ebc0d7" diff --git a/pyproject.toml b/pyproject.toml index adbf17d5..0e4005dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,166 @@ -[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 = [ +[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 = [ + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp310-cp310-win_amd64.whl", markers = "sys_platform == 'win32' and python_version == '3.10'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32' and python_version == '3.11'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-win_amd64.whl", markers = "sys_platform == 'win32' and python_version == '3.12'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp310-cp310-linux_x86_64.whl", markers = "sys_platform == 'linux' and python_version == '3.10'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp311-cp311-linux_x86_64.whl", markers = "sys_platform == 'linux' and python_version == '3.11'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-linux_x86_64.whl", markers = "sys_platform == 'linux' and python_version == '3.12'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp310-cp310-macosx_10_9_universal2.whl", markers = "sys_platform == 'darwin' and python_version == '3.10'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp311-cp311-macosx_10_9_universal2.whl", markers = "sys_platform == 'darwin' and python_version == '3.11'" }, + { url = "https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-macosx_10_13_universal2.whl", markers = "sys_platform == 'darwin' and python_version == '3.12'" } +] +mypy = "1.10.1" +pandas = "2.2.2" +ruff = "0.6.0" +matplotlib = "^3.9.2" +seaborn = "^0.13.2" +scipy = "^1.12.0" + +[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", + "tests/outcomes", + "tests/projects", + "tests/sql", + "venv", +] + +[tool.ruff.lint] +select = [ + "ALL", +] +ignore = [ + "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 + "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 ':' + "EXE002", # The file is executable but no shebang is present + "FBT003", # Boolean positional value in function call + "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/outcomes", "tests/projects", - "tests/outcomes/**/main*.py", - "tests/outcomes/**/cleaning.py", - "tests/outcomes/**/pandas_*.py", - "tests/outcomes/**/matplotlib_*.py", - "tests/outcomes/**/seaborn_*.py", + "tests/sql", + "venv", ] + +[tool.cibuildwheel] +test-command = "pytest {project}/tests" +test-extras = ["test"] +test-skip = ["*universal2:arm64"] +# Временно пропускаем сборку для PyPy +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 index 7c515cc5..888fd5c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,15 @@ -psutil-wheels ; python_version >= '3.10' -psutil ; python_version < '3.10' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp310-cp310-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.10' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp311-cp311-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.11' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.12' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp310-cp310-linux_x86_64.whl ; sys_platform == 'linux' and python_version == '3.10' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp311-cp311-linux_x86_64.whl ; sys_platform == 'linux' and python_version == '3.11' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-linux_x86_64.whl ; sys_platform == 'linux' and python_version == '3.12' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp310-cp310-macosx_10_9_universal2.whl ; sys_platform == 'darwin' and python_version == '3.10' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp311-cp311-macosx_10_9_universal2.whl ; sys_platform == 'darwin' and python_version == '3.11' +psutil @ https://github.com/hyperskill/hs-test-python/releases/latest/download/psutil-5.8.0-cp312-cp312-macosx_10_13_universal2.whl ; sys_platform == 'darwin' and python_version == '3.12' +mypy==1.10.1 +pandas==2.2.2 +ruff==0.6.0 +matplotlib==3.9.2 +seaborn==0.13.2 +scipy==1.12.0 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/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/outcomes/dynamic_method/test_dont_return_output_after_execution/test.py b/tests/outcomes/dynamic_method/test_dont_return_output_after_execution/test.py index b260e702..b64c032b 100644 --- a/tests/outcomes/dynamic_method/test_dont_return_output_after_execution/test.py +++ b/tests/outcomes/dynamic_method/test_dont_return_output_after_execution/test.py @@ -8,7 +8,7 @@ class TestDontReturnOutputAfterExecution(StageTest): @dynamic_test def test(self): main = TestedProgram('main') - main.set_return_output_after_execution(False) + main.set_return_output_after_execution(value=False) out = main.start() if len(out) != 0: @@ -26,7 +26,7 @@ def test(self): if out != "1 to 2\n2 to 3\n": return wrong("Output is wrong") - main.set_return_output_after_execution(True) + main.set_return_output_after_execution(value=True) if main.execute("") != "3 to 4\n": return wrong("Output should not be empty") diff --git a/tests/projects/go/coffee_machine/stage1/tests.py b/tests/projects/go/coffee_machine/stage1/tests.py index 1113c3d1..98a5d79b 100644 --- a/tests/projects/go/coffee_machine/stage1/tests.py +++ b/tests/projects/go/coffee_machine/stage1/tests.py @@ -3,6 +3,7 @@ from hstest.check_result import CheckResult from hstest.stage_test import StageTest from hstest.test_case import TestCase +from typing import List OUTPUT = """ Starting to make a coffee diff --git a/tests/projects/go/coffee_machine/stage1_ce/tests.py b/tests/projects/go/coffee_machine/stage1_ce/tests.py index 436a5500..37cf49e8 100644 --- a/tests/projects/go/coffee_machine/stage1_ce/tests.py +++ b/tests/projects/go/coffee_machine/stage1_ce/tests.py @@ -1,7 +1,9 @@ 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 import os +from typing import List CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/go/coffee_machine/stage1_ex/tests.py b/tests/projects/go/coffee_machine/stage1_ex/tests.py index 7d81161d..a1afac92 100644 --- a/tests/projects/go/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/go/coffee_machine/stage1_ex/tests.py @@ -1,6 +1,8 @@ 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 +from typing import List 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..cc4972a9 100644 --- a/tests/projects/go/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/go/coffee_machine/stage1_wa/tests.py @@ -1,6 +1,8 @@ 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 +from typing import List 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..33564d63 100644 --- a/tests/projects/go/coffee_machine/stage2/tests.py +++ b/tests/projects/go/coffee_machine/stage2/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..9dd33dd0 100644 --- a/tests/projects/go/coffee_machine/stage3/tests.py +++ b/tests/projects/go/coffee_machine/stage3/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..5b901da2 100644 --- a/tests/projects/go/coffee_machine/stage4/tests.py +++ b/tests/projects/go/coffee_machine/stage4/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..37c309a9 100644 --- a/tests/projects/go/coffee_machine/stage5/tests.py +++ b/tests/projects/go/coffee_machine/stage5/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..7e09982d 100644 --- a/tests/projects/javascript/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_ex/tests.py @@ -1,6 +1,8 @@ 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 +from typing import List 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..cc4972a9 100644 --- a/tests/projects/javascript/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/javascript/coffee_machine/stage1_wa/tests.py @@ -1,6 +1,8 @@ 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 +from typing import List 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..33564d63 100644 --- a/tests/projects/javascript/coffee_machine/stage2/tests.py +++ b/tests/projects/javascript/coffee_machine/stage2/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..9dd33dd0 100644 --- a/tests/projects/javascript/coffee_machine/stage3/tests.py +++ b/tests/projects/javascript/coffee_machine/stage3/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..5b901da2 100644 --- a/tests/projects/javascript/coffee_machine/stage4/tests.py +++ b/tests/projects/javascript/coffee_machine/stage4/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..37c309a9 100644 --- a/tests/projects/javascript/coffee_machine/stage5/tests.py +++ b/tests/projects/javascript/coffee_machine/stage5/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..ee283359 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage1/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage1/tests.py @@ -2,6 +2,8 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..c39604dc 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage2/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage2/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..e47e845f 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage3/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage3/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..dd871809 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage4/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage4/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..a3c1deb4 100644 --- a/tests/projects/javascript/simple_chatty_bot/stage5/tests.py +++ b/tests/projects/javascript/simple_chatty_bot/stage5/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..33564d63 100644 --- a/tests/projects/python/coffee_machine/stage2/tests.py +++ b/tests/projects/python/coffee_machine/stage2/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..9dd33dd0 100644 --- a/tests/projects/python/coffee_machine/stage3/tests.py +++ b/tests/projects/python/coffee_machine/stage3/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..5b901da2 100644 --- a/tests/projects/python/coffee_machine/stage4/tests.py +++ b/tests/projects/python/coffee_machine/stage4/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..37c309a9 100644 --- a/tests/projects/python/coffee_machine/stage5/tests.py +++ b/tests/projects/python/coffee_machine/stage5/tests.py @@ -1,5 +1,7 @@ from hstest.stage_test import * from hstest.test_case import TestCase +from hstest.check_result import CheckResult +from typing import List 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..86fb4c6c 100644 --- a/tests/projects/shell/coffee_machine/stage1/tests.py +++ b/tests/projects/shell/coffee_machine/stage1/tests.py @@ -1,6 +1,8 @@ from hstest.stage_test import * from hstest.test_case import TestCase from hstest.common.os_utils import is_windows +from hstest.check_result import CheckResult +from typing import List CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/shell/coffee_machine/stage1_ex/tests.py b/tests/projects/shell/coffee_machine/stage1_ex/tests.py index 98652a9b..34c05c0a 100644 --- a/tests/projects/shell/coffee_machine/stage1_ex/tests.py +++ b/tests/projects/shell/coffee_machine/stage1_ex/tests.py @@ -2,6 +2,8 @@ from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest from hstest.common.os_utils import is_windows +from hstest.check_result import CheckResult +from typing import List CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/shell/coffee_machine/stage1_wa/tests.py b/tests/projects/shell/coffee_machine/stage1_wa/tests.py index 7184b6f3..bb7a97c3 100644 --- a/tests/projects/shell/coffee_machine/stage1_wa/tests.py +++ b/tests/projects/shell/coffee_machine/stage1_wa/tests.py @@ -2,6 +2,8 @@ from hstest.test_case import TestCase from hstest.testing.unittest.user_error_test import UserErrorTest from hstest.common.os_utils import is_windows +from hstest.check_result import CheckResult +from typing import List CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) diff --git a/tests/projects/shell/coffee_machine/stage2/tests.py b/tests/projects/shell/coffee_machine/stage2/tests.py index 06a8cbc1..911b5a69 100644 --- a/tests/projects/shell/coffee_machine/stage2/tests.py +++ b/tests/projects/shell/coffee_machine/stage2/tests.py @@ -1,6 +1,8 @@ from hstest.stage_test import * from hstest.test_case import TestCase from hstest.common.os_utils import is_windows +from hstest.check_result import CheckResult +from typing import List CheckResult.correct = lambda: CheckResult(True, '') CheckResult.wrong = lambda feedback: CheckResult(False, feedback) 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..a6da0cf3 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -1,80 +1,87 @@ -import io +from __future__ import annotations + import re import sys import unittest from importlib import import_module from inspect import getmembers, isclass from os import listdir -from os.path import abspath, dirname, isdir, isfile -from typing import List +from pathlib import Path +from typing import Final, TYPE_CHECKING -content_path = dirname( - dirname(abspath(__file__)) -) -sys.path.insert(0, content_path) +content_path = Path(__file__).resolve().parent.parent +sys.path.insert(0, content_path.name) 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: +MAX_REPEATS: Final = 5 + +class UnitTesting: @staticmethod def test_all() -> bool: old_run = unittest.TestCase.run - def run(self, result=None, repeats=0): + def run( + self: unittest.TestCase, result: unittest.TestResult | None = None, repeats: int = 0 + ) -> unittest.TestResult: 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) - if repeats == 5: # max 5 times + is_project_test = "tests.projects." in str(self) + if repeats == MAX_REPEATS: return test_result if is_project_test and test_result and failures_before < len(test_result.failures): - print('Rerun project test') + print("Rerun project test") # noqa: T201 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'): + for module in UnitTesting.find_modules(Path(__file__).parent): + 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,27 +91,27 @@ def run(self, result=None, repeats=0): return result.wasSuccessful() @staticmethod - def find_modules(from_directory: str) -> List[str]: - - catalogs = [from_directory] - curr_dir = from_directory + def find_modules(from_directory: Path) -> list[str]: + catalogs = {from_directory} modules = [] 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('/', '.')] - elif isdir(curr_location): - catalogs += [curr_location] + if curr_location.is_file(): + if file.endswith(".py"): + modules.append( + ".".join(curr_location.relative_to(from_directory).parts)[:-3] + ) + elif curr_location.is_dir(): + catalogs.add(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)